fix: format script; chore: format
This commit is contained in:
parent
aeb0839da9
commit
6bf1cbc0ef
|
@ -48,8 +48,8 @@ Thank you for your PR! Before creating a PR, please check the following:
|
||||||
- If there is an Issue which will be resolved by this PR, please include a reference to the Issue in the text. Good examples include `Closing: #21` or `Resolves: #21`
|
- If there is an Issue which will be resolved by this PR, please include a reference to the Issue in the text. Good examples include `Closing: #21` or `Resolves: #21`
|
||||||
- Check if there are any documents that need to be created or updated due to this change.
|
- Check if there are any documents that need to be created or updated due to this change.
|
||||||
- If you have added a feature or fixed a bug, please add a test case if possible.
|
- If you have added a feature or fixed a bug, please add a test case if possible.
|
||||||
- Please make sure that tests and Lint are passed in advance.
|
- Please make sure that formatting, tests and Lint are passed in advance.
|
||||||
- You can run it with `pnpm run test` and `pnpm run lint`. [See more info](#testing)
|
- You can run it with `pnpm run format`, `pnpm run test` and `pnpm run lint`. [See more info](#testing)
|
||||||
- If this PR includes UI changes, please attach a screenshot in the text.
|
- If this PR includes UI changes, please attach a screenshot in the text.
|
||||||
|
|
||||||
Thanks for your cooperation 🤗
|
Thanks for your cooperation 🤗
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
"e2e": "start-server-and-test start:test http://localhost:61812 cy:run",
|
"e2e": "start-server-and-test start:test http://localhost:61812 cy:run",
|
||||||
"mocha": "pnpm --filter backend run mocha",
|
"mocha": "pnpm --filter backend run mocha",
|
||||||
"test": "pnpm run mocha",
|
"test": "pnpm run mocha",
|
||||||
"format": "gulp format",
|
"format": "pnpm rome format packages/**/* --write && pnpm --filter client run format",
|
||||||
"clean": "pnpm node ./scripts/clean.js",
|
"clean": "pnpm node ./scripts/clean.js",
|
||||||
"clean-all": "pnpm node ./scripts/clean-all.js",
|
"clean-all": "pnpm node ./scripts/clean-all.js",
|
||||||
"cleanall": "pnpm run clean-all"
|
"cleanall": "pnpm run clean-all"
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"tabWidth": 4,
|
||||||
|
"useTabs": true,
|
||||||
|
"singleQuote": false,
|
||||||
|
"vueIndentScriptAndStyle": false,
|
||||||
|
"plugins": ["vue"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.vue",
|
||||||
|
"options": {
|
||||||
|
"parser": "vue"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -4,7 +4,8 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"watch": "pnpm vite build --watch --mode development",
|
"watch": "pnpm vite build --watch --mode development",
|
||||||
"build": "pnpm vite build",
|
"build": "pnpm vite build",
|
||||||
"lint": "pnpm rome check \"src/**/*.{ts,vue}\""
|
"lint": "pnpm rome check \"src/**/*.{ts,vue}\"",
|
||||||
|
"format": "pnpm prettier --write '**/*.vue'"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@discordapp/twemoji": "14.0.2",
|
"@discordapp/twemoji": "14.0.2",
|
||||||
|
@ -54,6 +55,8 @@
|
||||||
"matter-js": "0.18.0",
|
"matter-js": "0.18.0",
|
||||||
"mfm-js": "0.23.2",
|
"mfm-js": "0.23.2",
|
||||||
"photoswipe": "5.3.4",
|
"photoswipe": "5.3.4",
|
||||||
|
"prettier": "2.8.7",
|
||||||
|
"prettier-plugin-vue": "1.1.6",
|
||||||
"prismjs": "1.29.0",
|
"prismjs": "1.29.0",
|
||||||
"punycode": "2.1.1",
|
"punycode": "2.1.1",
|
||||||
"querystring": "0.2.1",
|
"querystring": "0.2.1",
|
||||||
|
|
|
@ -1,16 +1,34 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="bcekxzvu _gap _panel">
|
<div class="bcekxzvu _gap _panel">
|
||||||
<div class="target">
|
<div class="target">
|
||||||
<MkA v-user-preview="report.targetUserId" class="info" :to="`/user-info/${report.targetUserId}`">
|
<MkA
|
||||||
<MkAvatar class="avatar" :user="report.targetUser" :show-indicator="true" :disable-link="true"/>
|
v-user-preview="report.targetUserId"
|
||||||
|
class="info"
|
||||||
|
:to="`/user-info/${report.targetUserId}`"
|
||||||
|
>
|
||||||
|
<MkAvatar
|
||||||
|
class="avatar"
|
||||||
|
:user="report.targetUser"
|
||||||
|
:show-indicator="true"
|
||||||
|
:disable-link="true"
|
||||||
|
/>
|
||||||
<div class="names">
|
<div class="names">
|
||||||
<MkUserName class="name" :user="report.targetUser" />
|
<MkUserName class="name" :user="report.targetUser" />
|
||||||
<MkAcct class="acct" :user="report.targetUser" style="display: block;"/>
|
<MkAcct
|
||||||
|
class="acct"
|
||||||
|
:user="report.targetUser"
|
||||||
|
style="display: block"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</MkA>
|
</MkA>
|
||||||
<MkKeyValue class="_formBlock">
|
<MkKeyValue class="_formBlock">
|
||||||
<template #key>{{ i18n.ts.registeredDate }}</template>
|
<template #key>{{ i18n.ts.registeredDate }}</template>
|
||||||
<template #value>{{ new Date(report.targetUser.createdAt).toLocaleString() }} (<MkTime :time="report.targetUser.createdAt"/>)</template>
|
<template #value
|
||||||
|
>{{
|
||||||
|
new Date(report.targetUser.createdAt).toLocaleString()
|
||||||
|
}}
|
||||||
|
(<MkTime :time="report.targetUser.createdAt" />)</template
|
||||||
|
>
|
||||||
</MkKeyValue>
|
</MkKeyValue>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail">
|
<div class="detail">
|
||||||
|
@ -18,47 +36,58 @@
|
||||||
<Mfm :text="report.comment" />
|
<Mfm :text="report.comment" />
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<div>{{ i18n.ts.reporter }}: <MkAcct :user="report.reporter"/></div>
|
<div>
|
||||||
|
{{ i18n.ts.reporter }}: <MkAcct :user="report.reporter" />
|
||||||
|
</div>
|
||||||
<div v-if="report.assignee">
|
<div v-if="report.assignee">
|
||||||
{{ i18n.ts.moderator }}:
|
{{ i18n.ts.moderator }}:
|
||||||
<MkAcct :user="report.assignee" />
|
<MkAcct :user="report.assignee" />
|
||||||
</div>
|
</div>
|
||||||
<div><MkTime :time="report.createdAt" /></div>
|
<div><MkTime :time="report.createdAt" /></div>
|
||||||
<div class="action">
|
<div class="action">
|
||||||
<MkSwitch v-model="forward" :disabled="report.targetUser.host == null || report.resolved">
|
<MkSwitch
|
||||||
|
v-model="forward"
|
||||||
|
:disabled="
|
||||||
|
report.targetUser.host == null || report.resolved
|
||||||
|
"
|
||||||
|
>
|
||||||
{{ i18n.ts.forwardReport }}
|
{{ i18n.ts.forwardReport }}
|
||||||
<template #caption>{{ i18n.ts.forwardReportIsAnonymous }}</template>
|
<template #caption>{{
|
||||||
|
i18n.ts.forwardReportIsAnonymous
|
||||||
|
}}</template>
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
<MkButton v-if="!report.resolved" primary @click="resolve">{{ i18n.ts.abuseMarkAsResolved }}</MkButton>
|
<MkButton v-if="!report.resolved" primary @click="resolve">{{
|
||||||
|
i18n.ts.abuseMarkAsResolved
|
||||||
|
}}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from "@/components/MkButton.vue";
|
||||||
import MkSwitch from '@/components/form/switch.vue';
|
import MkSwitch from "@/components/form/switch.vue";
|
||||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
import MkKeyValue from "@/components/MkKeyValue.vue";
|
||||||
import { acct, userPage } from '@/filters/user';
|
import { acct, userPage } from "@/filters/user";
|
||||||
import * as os from '@/os';
|
import * as os from "@/os";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
report: any;
|
report: any;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'resolved', reportId: string): void;
|
(ev: "resolved", reportId: string): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let forward = $ref(props.report.forwarded);
|
let forward = $ref(props.report.forwarded);
|
||||||
|
|
||||||
function resolve() {
|
function resolve() {
|
||||||
os.apiWithDialog('admin/resolve-abuse-user-report', {
|
os.apiWithDialog("admin/resolve-abuse-user-report", {
|
||||||
forward: forward,
|
forward: forward,
|
||||||
reportId: props.report.id,
|
reportId: props.report.id,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
emit('resolved', props.report.id);
|
emit("resolved", props.report.id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -81,7 +110,16 @@ function resolve() {
|
||||||
padding: 14px;
|
padding: 14px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
--c: rgb(255 196 0 / 15%);
|
--c: rgb(255 196 0 / 15%);
|
||||||
background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
|
background-image: linear-gradient(
|
||||||
|
45deg,
|
||||||
|
var(--c) 16.67%,
|
||||||
|
transparent 16.67%,
|
||||||
|
transparent 50%,
|
||||||
|
var(--c) 50%,
|
||||||
|
var(--c) 66.67%,
|
||||||
|
transparent 66.67%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
background-size: 16px 16px;
|
background-size: 16px 16px;
|
||||||
|
|
||||||
> .avatar {
|
> .avatar {
|
||||||
|
|
|
@ -1,7 +1,16 @@
|
||||||
<template>
|
<template>
|
||||||
<XWindow ref="uiWindow" :initial-width="400" :initial-height="500" :can-resize="true" @closed="emit('closed')">
|
<XWindow
|
||||||
|
ref="uiWindow"
|
||||||
|
:initial-width="400"
|
||||||
|
:initial-height="500"
|
||||||
|
:can-resize="true"
|
||||||
|
@closed="emit('closed')"
|
||||||
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<i class="ph-warning-circle ph-bold ph-lg" style="margin-right: 0.5em;"></i>
|
<i
|
||||||
|
class="ph-warning-circle ph-bold ph-lg"
|
||||||
|
style="margin-right: 0.5em"
|
||||||
|
></i>
|
||||||
<I18n :src="i18n.ts.reportAbuseOf" tag="span">
|
<I18n :src="i18n.ts.reportAbuseOf" tag="span">
|
||||||
<template #name>
|
<template #name>
|
||||||
<b><MkAcct :user="user" /></b>
|
<b><MkAcct :user="user" /></b>
|
||||||
|
@ -12,24 +21,32 @@
|
||||||
<div class="_section">
|
<div class="_section">
|
||||||
<MkTextarea v-model="comment">
|
<MkTextarea v-model="comment">
|
||||||
<template #label>{{ i18n.ts.details }}</template>
|
<template #label>{{ i18n.ts.details }}</template>
|
||||||
<template #caption>{{ i18n.ts.fillAbuseReportDescription }}</template>
|
<template #caption>{{
|
||||||
|
i18n.ts.fillAbuseReportDescription
|
||||||
|
}}</template>
|
||||||
</MkTextarea>
|
</MkTextarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="_section">
|
<div class="_section">
|
||||||
<MkButton primary full :disabled="comment.length === 0" @click="send">{{ i18n.ts.send }}</MkButton>
|
<MkButton
|
||||||
|
primary
|
||||||
|
full
|
||||||
|
:disabled="comment.length === 0"
|
||||||
|
@click="send"
|
||||||
|
>{{ i18n.ts.send }}</MkButton
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</XWindow>
|
</XWindow>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from "vue";
|
||||||
import * as Misskey from 'calckey-js';
|
import * as Misskey from "calckey-js";
|
||||||
import XWindow from '@/components/MkWindow.vue';
|
import XWindow from "@/components/MkWindow.vue";
|
||||||
import MkTextarea from '@/components/form/textarea.vue';
|
import MkTextarea from "@/components/form/textarea.vue";
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from "@/components/MkButton.vue";
|
||||||
import * as os from '@/os';
|
import * as os from "@/os";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
user: Misskey.entities.User;
|
user: Misskey.entities.User;
|
||||||
|
@ -37,23 +54,27 @@ const props = defineProps<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'closed'): void;
|
(ev: "closed"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const uiWindow = ref<InstanceType<typeof XWindow>>();
|
const uiWindow = ref<InstanceType<typeof XWindow>>();
|
||||||
const comment = ref(props.initialComment || '');
|
const comment = ref(props.initialComment || "");
|
||||||
|
|
||||||
function send() {
|
function send() {
|
||||||
os.apiWithDialog('users/report-abuse', {
|
os.apiWithDialog(
|
||||||
|
"users/report-abuse",
|
||||||
|
{
|
||||||
userId: props.user.id,
|
userId: props.user.id,
|
||||||
comment: comment.value,
|
comment: comment.value,
|
||||||
}, undefined).then(res => {
|
},
|
||||||
|
undefined
|
||||||
|
).then((res) => {
|
||||||
os.alert({
|
os.alert({
|
||||||
type: 'success',
|
type: "success",
|
||||||
text: i18n.ts.abuseReported
|
text: i18n.ts.abuseReported,
|
||||||
});
|
});
|
||||||
uiWindow.value?.close();
|
uiWindow.value?.close();
|
||||||
emit('closed');
|
emit("closed");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -3,26 +3,56 @@
|
||||||
<template v-if="props.graduations === 'dots'">
|
<template v-if="props.graduations === 'dots'">
|
||||||
<circle
|
<circle
|
||||||
v-for="(angle, i) in graduationsMajor"
|
v-for="(angle, i) in graduationsMajor"
|
||||||
:cx="5 + (Math.sin(angle) * (5 - graduationsPadding))"
|
:cx="5 + Math.sin(angle) * (5 - graduationsPadding)"
|
||||||
:cy="5 - (Math.cos(angle) * (5 - graduationsPadding))"
|
:cy="5 - Math.cos(angle) * (5 - graduationsPadding)"
|
||||||
:r="0.125"
|
:r="0.125"
|
||||||
:fill="(props.twentyfour ? h : h % 12) === i ? nowColor : majorGraduationColor"
|
:fill="
|
||||||
:opacity="!props.fadeGraduations || (props.twentyfour ? h : h % 12) === i ? 1 : Math.max(0, 1 - (angleDiff(hAngle, angle) / Math.PI) - numbersOpacityFactor)"
|
(props.twentyfour ? h : h % 12) === i
|
||||||
|
? nowColor
|
||||||
|
: majorGraduationColor
|
||||||
|
"
|
||||||
|
:opacity="
|
||||||
|
!props.fadeGraduations ||
|
||||||
|
(props.twentyfour ? h : h % 12) === i
|
||||||
|
? 1
|
||||||
|
: Math.max(
|
||||||
|
0,
|
||||||
|
1 -
|
||||||
|
angleDiff(hAngle, angle) / Math.PI -
|
||||||
|
numbersOpacityFactor
|
||||||
|
)
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="props.graduations === 'numbers'">
|
<template v-else-if="props.graduations === 'numbers'">
|
||||||
<text
|
<text
|
||||||
v-for="(angle, i) in texts"
|
v-for="(angle, i) in texts"
|
||||||
:x="5 + (Math.sin(angle) * (5 - textsPadding))"
|
:x="5 + Math.sin(angle) * (5 - textsPadding)"
|
||||||
:y="5 - (Math.cos(angle) * (5 - textsPadding))"
|
:y="5 - Math.cos(angle) * (5 - textsPadding)"
|
||||||
text-anchor="middle"
|
text-anchor="middle"
|
||||||
dominant-baseline="middle"
|
dominant-baseline="middle"
|
||||||
:font-size="(props.twentyfour ? h : h % 12) === i ? 1 : 0.7"
|
:font-size="(props.twentyfour ? h : h % 12) === i ? 1 : 0.7"
|
||||||
:font-weight="(props.twentyfour ? h : h % 12) === i ? 'bold' : 'normal'"
|
:font-weight="
|
||||||
:fill="(props.twentyfour ? h : h % 12) === i ? nowColor : 'currentColor'"
|
(props.twentyfour ? h : h % 12) === i ? 'bold' : 'normal'
|
||||||
:opacity="!props.fadeGraduations || (props.twentyfour ? h : h % 12) === i ? 1 : Math.max(0, 1 - (angleDiff(hAngle, angle) / Math.PI) - numbersOpacityFactor)"
|
"
|
||||||
|
:fill="
|
||||||
|
(props.twentyfour ? h : h % 12) === i
|
||||||
|
? nowColor
|
||||||
|
: 'currentColor'
|
||||||
|
"
|
||||||
|
:opacity="
|
||||||
|
!props.fadeGraduations ||
|
||||||
|
(props.twentyfour ? h : h % 12) === i
|
||||||
|
? 1
|
||||||
|
: Math.max(
|
||||||
|
0,
|
||||||
|
1 -
|
||||||
|
angleDiff(hAngle, angle) / Math.PI -
|
||||||
|
numbersOpacityFactor
|
||||||
|
)
|
||||||
|
"
|
||||||
>
|
>
|
||||||
{{ i === 0 ? (props.twentyfour ? '24' : '12') : i }}
|
{{ i === 0 ? (props.twentyfour ? "24" : "12") : i }}
|
||||||
</text>
|
</text>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -40,11 +70,15 @@
|
||||||
|
|
||||||
<line
|
<line
|
||||||
class="s"
|
class="s"
|
||||||
:class="{ animate: !disableSAnimate && sAnimation !== 'none', elastic: sAnimation === 'elastic', easeOut: sAnimation === 'easeOut' }"
|
:class="{
|
||||||
:x1="5 - (0 * (sHandLengthRatio * handsTailLength))"
|
animate: !disableSAnimate && sAnimation !== 'none',
|
||||||
:y1="5 + (1 * (sHandLengthRatio * handsTailLength))"
|
elastic: sAnimation === 'elastic',
|
||||||
:x2="5 + (0 * ((sHandLengthRatio * 5) - handsPadding))"
|
easeOut: sAnimation === 'easeOut',
|
||||||
:y2="5 - (1 * ((sHandLengthRatio * 5) - handsPadding))"
|
}"
|
||||||
|
:x1="5 - 0 * (sHandLengthRatio * handsTailLength)"
|
||||||
|
:y1="5 + 1 * (sHandLengthRatio * handsTailLength)"
|
||||||
|
:x2="5 + 0 * (sHandLengthRatio * 5 - handsPadding)"
|
||||||
|
:y2="5 - 1 * (sHandLengthRatio * 5 - handsPadding)"
|
||||||
:stroke="sHandColor"
|
:stroke="sHandColor"
|
||||||
:stroke-width="thickness / 2"
|
:stroke-width="thickness / 2"
|
||||||
:style="`transform: rotateZ(${sAngle}rad)`"
|
:style="`transform: rotateZ(${sAngle}rad)`"
|
||||||
|
@ -52,20 +86,20 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<line
|
<line
|
||||||
:x1="5 - (Math.sin(mAngle) * (mHandLengthRatio * handsTailLength))"
|
:x1="5 - Math.sin(mAngle) * (mHandLengthRatio * handsTailLength)"
|
||||||
:y1="5 + (Math.cos(mAngle) * (mHandLengthRatio * handsTailLength))"
|
:y1="5 + Math.cos(mAngle) * (mHandLengthRatio * handsTailLength)"
|
||||||
:x2="5 + (Math.sin(mAngle) * ((mHandLengthRatio * 5) - handsPadding))"
|
:x2="5 + Math.sin(mAngle) * (mHandLengthRatio * 5 - handsPadding)"
|
||||||
:y2="5 - (Math.cos(mAngle) * ((mHandLengthRatio * 5) - handsPadding))"
|
:y2="5 - Math.cos(mAngle) * (mHandLengthRatio * 5 - handsPadding)"
|
||||||
:stroke="mHandColor"
|
:stroke="mHandColor"
|
||||||
:stroke-width="thickness"
|
:stroke-width="thickness"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<line
|
<line
|
||||||
:x1="5 - (Math.sin(hAngle) * (hHandLengthRatio * handsTailLength))"
|
:x1="5 - Math.sin(hAngle) * (hHandLengthRatio * handsTailLength)"
|
||||||
:y1="5 + (Math.cos(hAngle) * (hHandLengthRatio * handsTailLength))"
|
:y1="5 + Math.cos(hAngle) * (hHandLengthRatio * handsTailLength)"
|
||||||
:x2="5 + (Math.sin(hAngle) * ((hHandLengthRatio * 5) - handsPadding))"
|
:x2="5 + Math.sin(hAngle) * (hHandLengthRatio * 5 - handsPadding)"
|
||||||
:y2="5 - (Math.cos(hAngle) * ((hHandLengthRatio * 5) - handsPadding))"
|
:y2="5 - Math.cos(hAngle) * (hHandLengthRatio * 5 - handsPadding)"
|
||||||
:stroke="hHandColor"
|
:stroke="hHandColor"
|
||||||
:stroke-width="thickness"
|
:stroke-width="thickness"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
|
@ -74,14 +108,21 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, computed, onMounted, onBeforeUnmount, shallowRef, nextTick } from 'vue';
|
import {
|
||||||
import tinycolor from 'tinycolor2';
|
ref,
|
||||||
import { globalEvents } from '@/events.js';
|
computed,
|
||||||
|
onMounted,
|
||||||
|
onBeforeUnmount,
|
||||||
|
shallowRef,
|
||||||
|
nextTick,
|
||||||
|
} from "vue";
|
||||||
|
import tinycolor from "tinycolor2";
|
||||||
|
import { globalEvents } from "@/events.js";
|
||||||
|
|
||||||
// https://stackoverflow.com/questions/1878907/how-can-i-find-the-difference-between-two-angles
|
// https://stackoverflow.com/questions/1878907/how-can-i-find-the-difference-between-two-angles
|
||||||
const angleDiff = (a: number, b: number) => {
|
const angleDiff = (a: number, b: number) => {
|
||||||
const x = Math.abs(a - b);
|
const x = Math.abs(a - b);
|
||||||
return Math.abs((x + Math.PI) % (Math.PI * 2) - Math.PI);
|
return Math.abs(((x + Math.PI) % (Math.PI * 2)) - Math.PI);
|
||||||
};
|
};
|
||||||
|
|
||||||
const graduationsPadding = 0.5;
|
const graduationsPadding = 0.5;
|
||||||
|
@ -93,28 +134,31 @@ const mHandLengthRatio = 1;
|
||||||
const sHandLengthRatio = 1;
|
const sHandLengthRatio = 1;
|
||||||
const numbersOpacityFactor = 0.35;
|
const numbersOpacityFactor = 0.35;
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
thickness?: number;
|
thickness?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
twentyfour?: boolean;
|
twentyfour?: boolean;
|
||||||
graduations?: 'none' | 'dots' | 'numbers';
|
graduations?: "none" | "dots" | "numbers";
|
||||||
fadeGraduations?: boolean;
|
fadeGraduations?: boolean;
|
||||||
sAnimation?: 'none' | 'elastic' | 'easeOut';
|
sAnimation?: "none" | "elastic" | "easeOut";
|
||||||
}>(), {
|
}>(),
|
||||||
|
{
|
||||||
numbers: false,
|
numbers: false,
|
||||||
thickness: 0.1,
|
thickness: 0.1,
|
||||||
offset: 0 - new Date().getTimezoneOffset(),
|
offset: 0 - new Date().getTimezoneOffset(),
|
||||||
twentyfour: false,
|
twentyfour: false,
|
||||||
graduations: 'dots',
|
graduations: "dots",
|
||||||
fadeGraduations: true,
|
fadeGraduations: true,
|
||||||
sAnimation: 'elastic',
|
sAnimation: "elastic",
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const graduationsMajor = computed(() => {
|
const graduationsMajor = computed(() => {
|
||||||
const angles: number[] = [];
|
const angles: number[] = [];
|
||||||
const times = props.twentyfour ? 24 : 12;
|
const times = props.twentyfour ? 24 : 12;
|
||||||
for (let i = 0; i < times; i++) {
|
for (let i = 0; i < times; i++) {
|
||||||
const angle = Math.PI * i / (times / 2);
|
const angle = (Math.PI * i) / (times / 2);
|
||||||
angles.push(angle);
|
angles.push(angle);
|
||||||
}
|
}
|
||||||
return angles;
|
return angles;
|
||||||
|
@ -123,7 +167,7 @@ const texts = computed(() => {
|
||||||
const angles: number[] = [];
|
const angles: number[] = [];
|
||||||
const times = props.twentyfour ? 24 : 12;
|
const times = props.twentyfour ? 24 : 12;
|
||||||
for (let i = 0; i < times; i++) {
|
for (let i = 0; i < times; i++) {
|
||||||
const angle = Math.PI * i / (times / 2);
|
const angle = (Math.PI * i) / (times / 2);
|
||||||
angles.push(angle);
|
angles.push(angle);
|
||||||
}
|
}
|
||||||
return angles;
|
return angles;
|
||||||
|
@ -147,14 +191,19 @@ let sOneRound = false;
|
||||||
|
|
||||||
function tick() {
|
function tick() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
now.setMinutes(now.getMinutes() + (new Date().getTimezoneOffset() + props.offset));
|
now.setMinutes(
|
||||||
|
now.getMinutes() + (new Date().getTimezoneOffset() + props.offset)
|
||||||
|
);
|
||||||
s = now.getSeconds();
|
s = now.getSeconds();
|
||||||
m = now.getMinutes();
|
m = now.getMinutes();
|
||||||
h = now.getHours();
|
h = now.getHours();
|
||||||
hAngle = Math.PI * (h % (props.twentyfour ? 24 : 12) + (m + s / 60) / 60) / (props.twentyfour ? 12 : 6);
|
hAngle =
|
||||||
mAngle = Math.PI * (m + s / 60) / 30;
|
(Math.PI * ((h % (props.twentyfour ? 24 : 12)) + (m + s / 60) / 60)) /
|
||||||
if (sOneRound) { // 秒針が一周した際のアニメーションをよしなに処理する(これが無いと秒が59->0になったときに期待したアニメーションにならない)
|
(props.twentyfour ? 12 : 6);
|
||||||
sAngle = Math.PI * 60 / 30;
|
mAngle = (Math.PI * (m + s / 60)) / 30;
|
||||||
|
if (sOneRound) {
|
||||||
|
// 秒針が一周した際のアニメーションをよしなに処理する(これが無いと秒が59->0になったときに期待したアニメーションにならない)
|
||||||
|
sAngle = (Math.PI * 60) / 30;
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
disableSAnimate = true;
|
disableSAnimate = true;
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
|
@ -165,7 +214,7 @@ function tick() {
|
||||||
}, 100);
|
}, 100);
|
||||||
}, 700);
|
}, 700);
|
||||||
} else {
|
} else {
|
||||||
sAngle = Math.PI * s / 30;
|
sAngle = (Math.PI * s) / 30;
|
||||||
}
|
}
|
||||||
sOneRound = s === 59;
|
sOneRound = s === 59;
|
||||||
}
|
}
|
||||||
|
@ -174,12 +223,18 @@ tick();
|
||||||
|
|
||||||
function calcColors() {
|
function calcColors() {
|
||||||
const computedStyle = getComputedStyle(document.documentElement);
|
const computedStyle = getComputedStyle(document.documentElement);
|
||||||
const dark = tinycolor(computedStyle.getPropertyValue('--bg')).isDark();
|
const dark = tinycolor(computedStyle.getPropertyValue("--bg")).isDark();
|
||||||
const accent = tinycolor(computedStyle.getPropertyValue('--accent')).toHexString();
|
const accent = tinycolor(
|
||||||
majorGraduationColor = dark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)';
|
computedStyle.getPropertyValue("--accent")
|
||||||
|
).toHexString();
|
||||||
|
majorGraduationColor = dark
|
||||||
|
? "rgba(255, 255, 255, 0.3)"
|
||||||
|
: "rgba(0, 0, 0, 0.3)";
|
||||||
//minorGraduationColor = dark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
//minorGraduationColor = dark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
||||||
sHandColor = dark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.3)';
|
sHandColor = dark ? "rgba(255, 255, 255, 0.5)" : "rgba(0, 0, 0, 0.3)";
|
||||||
mHandColor = tinycolor(computedStyle.getPropertyValue('--fg')).toHexString();
|
mHandColor = tinycolor(
|
||||||
|
computedStyle.getPropertyValue("--fg")
|
||||||
|
).toHexString();
|
||||||
hHandColor = accent;
|
hHandColor = accent;
|
||||||
nowColor = accent;
|
nowColor = accent;
|
||||||
}
|
}
|
||||||
|
@ -195,13 +250,13 @@ onMounted(() => {
|
||||||
};
|
};
|
||||||
update();
|
update();
|
||||||
|
|
||||||
globalEvents.on('themeChanged', calcColors);
|
globalEvents.on("themeChanged", calcColors);
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
enabled = false;
|
enabled = false;
|
||||||
|
|
||||||
globalEvents.off('themeChanged', calcColors);
|
globalEvents.off("themeChanged", calcColors);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -214,11 +269,11 @@ onBeforeUnmount(() => {
|
||||||
transform-origin: 50% 50%;
|
transform-origin: 50% 50%;
|
||||||
|
|
||||||
&.animate.elastic {
|
&.animate.elastic {
|
||||||
transition: transform .2s cubic-bezier(.4,2.08,.55,.44);
|
transition: transform 0.2s cubic-bezier(0.4, 2.08, 0.55, 0.44);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.animate.easeOut {
|
&.animate.easeOut {
|
||||||
transition: transform .7s cubic-bezier(0,.7,.3,1);
|
transition: transform 0.7s cubic-bezier(0, 0.7, 0.3, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,31 +1,81 @@
|
||||||
<template>
|
<template>
|
||||||
<div ref="rootEl" class="swhvrteh _popup _shadow" :style="{ zIndex }" @contextmenu.prevent="() => {}">
|
<div
|
||||||
|
ref="rootEl"
|
||||||
|
class="swhvrteh _popup _shadow"
|
||||||
|
:style="{ zIndex }"
|
||||||
|
@contextmenu.prevent="() => {}"
|
||||||
|
>
|
||||||
<ol v-if="type === 'user'" ref="suggests" class="users">
|
<ol v-if="type === 'user'" ref="suggests" class="users">
|
||||||
<li v-for="user in users" tabindex="-1" class="user" @click="complete(type, user)" @keydown="onKeydown">
|
<li
|
||||||
|
v-for="user in users"
|
||||||
|
tabindex="-1"
|
||||||
|
class="user"
|
||||||
|
@click="complete(type, user)"
|
||||||
|
@keydown="onKeydown"
|
||||||
|
>
|
||||||
<img class="avatar" :src="user.avatarUrl" />
|
<img class="avatar" :src="user.avatarUrl" />
|
||||||
<span class="name">
|
<span class="name">
|
||||||
<MkUserName :key="user.id" :user="user" />
|
<MkUserName :key="user.id" :user="user" />
|
||||||
</span>
|
</span>
|
||||||
<span class="username">@{{ acct(user) }}</span>
|
<span class="username">@{{ acct(user) }}</span>
|
||||||
</li>
|
</li>
|
||||||
<li tabindex="-1" class="choose" @click="chooseUser()" @keydown="onKeydown">{{ i18n.ts.selectUser }}</li>
|
<li
|
||||||
|
tabindex="-1"
|
||||||
|
class="choose"
|
||||||
|
@click="chooseUser()"
|
||||||
|
@keydown="onKeydown"
|
||||||
|
>
|
||||||
|
{{ i18n.ts.selectUser }}
|
||||||
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
<ol v-else-if="hashtags.length > 0" ref="suggests" class="hashtags">
|
<ol v-else-if="hashtags.length > 0" ref="suggests" class="hashtags">
|
||||||
<li v-for="hashtag in hashtags" tabindex="-1" @click="complete(type, hashtag)" @keydown="onKeydown">
|
<li
|
||||||
|
v-for="hashtag in hashtags"
|
||||||
|
tabindex="-1"
|
||||||
|
@click="complete(type, hashtag)"
|
||||||
|
@keydown="onKeydown"
|
||||||
|
>
|
||||||
<span class="name">{{ hashtag }}</span>
|
<span class="name">{{ hashtag }}</span>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
<ol v-else-if="emojis.length > 0" ref="suggests" class="emojis">
|
<ol v-else-if="emojis.length > 0" ref="suggests" class="emojis">
|
||||||
<li v-for="emoji in emojis" tabindex="-1" @click="complete(type, emoji.emoji)" @keydown="onKeydown">
|
<li
|
||||||
<span v-if="emoji.isCustomEmoji" class="emoji"><img :src="defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url" :alt="emoji.emoji"/></span>
|
v-for="emoji in emojis"
|
||||||
<span v-else-if="!defaultStore.state.useOsNativeEmojis" class="emoji"><img :src="emoji.url" :alt="emoji.emoji"/></span>
|
tabindex="-1"
|
||||||
|
@click="complete(type, emoji.emoji)"
|
||||||
|
@keydown="onKeydown"
|
||||||
|
>
|
||||||
|
<span v-if="emoji.isCustomEmoji" class="emoji"
|
||||||
|
><img
|
||||||
|
:src="
|
||||||
|
defaultStore.state.disableShowingAnimatedImages
|
||||||
|
? getStaticImageUrl(emoji.url)
|
||||||
|
: emoji.url
|
||||||
|
"
|
||||||
|
:alt="emoji.emoji"
|
||||||
|
/></span>
|
||||||
|
<span
|
||||||
|
v-else-if="!defaultStore.state.useOsNativeEmojis"
|
||||||
|
class="emoji"
|
||||||
|
><img :src="emoji.url" :alt="emoji.emoji"
|
||||||
|
/></span>
|
||||||
<span v-else class="emoji">{{ emoji.emoji }}</span>
|
<span v-else class="emoji">{{ emoji.emoji }}</span>
|
||||||
<span class="name" v-html="emoji.name.replace(q, `<b>${q}</b>`)"></span>
|
<span
|
||||||
<span v-if="emoji.aliasOf" class="alias">({{ emoji.aliasOf }})</span>
|
class="name"
|
||||||
|
v-html="emoji.name.replace(q, `<b>${q}</b>`)"
|
||||||
|
></span>
|
||||||
|
<span v-if="emoji.aliasOf" class="alias"
|
||||||
|
>({{ emoji.aliasOf }})</span
|
||||||
|
>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
<ol v-else-if="mfmTags.length > 0" ref="suggests" class="mfmTags">
|
<ol v-else-if="mfmTags.length > 0" ref="suggests" class="mfmTags">
|
||||||
<li v-for="tag in mfmTags" tabindex="-1" @click="complete(type, tag)" @keydown="onKeydown">
|
<li
|
||||||
|
v-for="tag in mfmTags"
|
||||||
|
tabindex="-1"
|
||||||
|
@click="complete(type, tag)"
|
||||||
|
@keydown="onKeydown"
|
||||||
|
>
|
||||||
<span class="tag">{{ tag }}</span>
|
<span class="tag">{{ tag }}</span>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
@ -33,17 +83,25 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { markRaw, ref, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
|
import {
|
||||||
import contains from '@/scripts/contains';
|
markRaw,
|
||||||
import { char2filePath } from '@/scripts/twemoji-base';
|
ref,
|
||||||
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
|
onUpdated,
|
||||||
import { acct } from '@/filters/user';
|
onMounted,
|
||||||
import * as os from '@/os';
|
onBeforeUnmount,
|
||||||
import { MFM_TAGS } from '@/scripts/mfm-tags';
|
nextTick,
|
||||||
import { defaultStore } from '@/store';
|
watch,
|
||||||
import { emojilist } from '@/scripts/emojilist';
|
} from "vue";
|
||||||
import { instance } from '@/instance';
|
import contains from "@/scripts/contains";
|
||||||
import { i18n } from '@/i18n';
|
import { char2filePath } from "@/scripts/twemoji-base";
|
||||||
|
import { getStaticImageUrl } from "@/scripts/get-static-image-url";
|
||||||
|
import { acct } from "@/filters/user";
|
||||||
|
import * as os from "@/os";
|
||||||
|
import { MFM_TAGS } from "@/scripts/mfm-tags";
|
||||||
|
import { defaultStore } from "@/store";
|
||||||
|
import { emojilist } from "@/scripts/emojilist";
|
||||||
|
import { instance } from "@/instance";
|
||||||
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
type EmojiDef = {
|
type EmojiDef = {
|
||||||
emoji: string;
|
emoji: string;
|
||||||
|
@ -53,9 +111,9 @@ type EmojiDef = {
|
||||||
isCustomEmoji?: boolean;
|
isCustomEmoji?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const lib = emojilist.filter(x => x.category !== 'flags');
|
const lib = emojilist.filter((x) => x.category !== "flags");
|
||||||
|
|
||||||
const emjdb: EmojiDef[] = lib.map(x => ({
|
const emjdb: EmojiDef[] = lib.map((x) => ({
|
||||||
emoji: x.char,
|
emoji: x.char,
|
||||||
name: x.name,
|
name: x.name,
|
||||||
url: char2filePath(x.char),
|
url: char2filePath(x.char),
|
||||||
|
@ -85,7 +143,7 @@ for (const x of customEmojis) {
|
||||||
name: x.name,
|
name: x.name,
|
||||||
emoji: `:${x.name}:`,
|
emoji: `:${x.name}:`,
|
||||||
url: x.url,
|
url: x.url,
|
||||||
isCustomEmoji: true
|
isCustomEmoji: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (x.aliases) {
|
if (x.aliases) {
|
||||||
|
@ -95,7 +153,7 @@ for (const x of customEmojis) {
|
||||||
aliasOf: x.name,
|
aliasOf: x.name,
|
||||||
emoji: `:${x.name}:`,
|
emoji: `:${x.name}:`,
|
||||||
url: x.url,
|
url: x.url,
|
||||||
isCustomEmoji: true
|
isCustomEmoji: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -125,8 +183,8 @@ const props = defineProps<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(event: 'done', value: { type: string; value: any }): void;
|
(event: "done", value: { type: string; value: any }): void;
|
||||||
(event: 'closed'): void;
|
(event: "closed"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const suggests = ref<Element>();
|
const suggests = ref<Element>();
|
||||||
|
@ -135,36 +193,37 @@ const rootEl = ref<HTMLDivElement>();
|
||||||
const fetching = ref(true);
|
const fetching = ref(true);
|
||||||
const users = ref<any[]>([]);
|
const users = ref<any[]>([]);
|
||||||
const hashtags = ref<any[]>([]);
|
const hashtags = ref<any[]>([]);
|
||||||
const emojis = ref<(EmojiDef)[]>([]);
|
const emojis = ref<EmojiDef[]>([]);
|
||||||
const items = ref<Element[] | HTMLCollection>([]);
|
const items = ref<Element[] | HTMLCollection>([]);
|
||||||
const mfmTags = ref<string[]>([]);
|
const mfmTags = ref<string[]>([]);
|
||||||
const select = ref(-1);
|
const select = ref(-1);
|
||||||
const zIndex = os.claimZIndex('high');
|
const zIndex = os.claimZIndex("high");
|
||||||
|
|
||||||
function complete(type: string, value: any) {
|
function complete(type: string, value: any) {
|
||||||
emit('done', { type, value });
|
emit("done", { type, value });
|
||||||
emit('closed');
|
emit("closed");
|
||||||
if (type === 'emoji') {
|
if (type === "emoji") {
|
||||||
let recents = defaultStore.state.recentlyUsedEmojis;
|
let recents = defaultStore.state.recentlyUsedEmojis;
|
||||||
recents = recents.filter((emoji: any) => emoji !== value);
|
recents = recents.filter((emoji: any) => emoji !== value);
|
||||||
recents.unshift(value);
|
recents.unshift(value);
|
||||||
defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32));
|
defaultStore.set("recentlyUsedEmojis", recents.splice(0, 32));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setPosition() {
|
function setPosition() {
|
||||||
if (!rootEl.value) return;
|
if (!rootEl.value) return;
|
||||||
if (props.x + rootEl.value.offsetWidth > window.innerWidth) {
|
if (props.x + rootEl.value.offsetWidth > window.innerWidth) {
|
||||||
rootEl.value.style.left = (window.innerWidth - rootEl.value.offsetWidth) + 'px';
|
rootEl.value.style.left =
|
||||||
|
window.innerWidth - rootEl.value.offsetWidth + "px";
|
||||||
} else {
|
} else {
|
||||||
rootEl.value.style.left = `${props.x}px`;
|
rootEl.value.style.left = `${props.x}px`;
|
||||||
}
|
}
|
||||||
if (props.y + rootEl.value.offsetHeight > window.innerHeight) {
|
if (props.y + rootEl.value.offsetHeight > window.innerHeight) {
|
||||||
rootEl.value.style.top = (props.y - rootEl.value.offsetHeight) + 'px';
|
rootEl.value.style.top = props.y - rootEl.value.offsetHeight + "px";
|
||||||
rootEl.value.style.marginTop = '0';
|
rootEl.value.style.marginTop = "0";
|
||||||
} else {
|
} else {
|
||||||
rootEl.value.style.top = props.y + 'px';
|
rootEl.value.style.top = props.y + "px";
|
||||||
rootEl.value.style.marginTop = 'calc(1em + 8px)';
|
rootEl.value.style.marginTop = "calc(1em + 8px)";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,10 +231,10 @@ function exec() {
|
||||||
select.value = -1;
|
select.value = -1;
|
||||||
if (suggests.value) {
|
if (suggests.value) {
|
||||||
for (const el of Array.from(items.value)) {
|
for (const el of Array.from(items.value)) {
|
||||||
el.removeAttribute('data-selected');
|
el.removeAttribute("data-selected");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (props.type === 'user') {
|
if (props.type === "user") {
|
||||||
if (!props.q) {
|
if (!props.q) {
|
||||||
users.value = [];
|
users.value = [];
|
||||||
fetching.value = false;
|
fetching.value = false;
|
||||||
|
@ -189,20 +248,22 @@ function exec() {
|
||||||
users.value = JSON.parse(cache);
|
users.value = JSON.parse(cache);
|
||||||
fetching.value = false;
|
fetching.value = false;
|
||||||
} else {
|
} else {
|
||||||
os.api('users/search-by-username-and-host', {
|
os.api("users/search-by-username-and-host", {
|
||||||
username: props.q,
|
username: props.q,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
detail: false
|
detail: false,
|
||||||
}).then(searchedUsers => {
|
}).then((searchedUsers) => {
|
||||||
users.value = searchedUsers as any[];
|
users.value = searchedUsers as any[];
|
||||||
fetching.value = false;
|
fetching.value = false;
|
||||||
// キャッシュ
|
// キャッシュ
|
||||||
sessionStorage.setItem(cacheKey, JSON.stringify(searchedUsers));
|
sessionStorage.setItem(cacheKey, JSON.stringify(searchedUsers));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (props.type === 'hashtag') {
|
} else if (props.type === "hashtag") {
|
||||||
if (!props.q || props.q === '') {
|
if (!props.q || props.q === "") {
|
||||||
hashtags.value = JSON.parse(localStorage.getItem('hashtags') || '[]');
|
hashtags.value = JSON.parse(
|
||||||
|
localStorage.getItem("hashtags") || "[]"
|
||||||
|
);
|
||||||
fetching.value = false;
|
fetching.value = false;
|
||||||
} else {
|
} else {
|
||||||
const cacheKey = `autocomplete:hashtag:${props.q}`;
|
const cacheKey = `autocomplete:hashtag:${props.q}`;
|
||||||
|
@ -212,59 +273,80 @@ function exec() {
|
||||||
hashtags.value = hashtags;
|
hashtags.value = hashtags;
|
||||||
fetching.value = false;
|
fetching.value = false;
|
||||||
} else {
|
} else {
|
||||||
os.api('hashtags/search', {
|
os.api("hashtags/search", {
|
||||||
query: props.q,
|
query: props.q,
|
||||||
limit: 30
|
limit: 30,
|
||||||
}).then(searchedHashtags => {
|
}).then((searchedHashtags) => {
|
||||||
hashtags.value = searchedHashtags as any[];
|
hashtags.value = searchedHashtags as any[];
|
||||||
fetching.value = false;
|
fetching.value = false;
|
||||||
// キャッシュ
|
// キャッシュ
|
||||||
sessionStorage.setItem(cacheKey, JSON.stringify(searchedHashtags));
|
sessionStorage.setItem(
|
||||||
|
cacheKey,
|
||||||
|
JSON.stringify(searchedHashtags)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (props.type === 'emoji') {
|
} else if (props.type === "emoji") {
|
||||||
if (!props.q || props.q === '') {
|
if (!props.q || props.q === "") {
|
||||||
// 最近使った絵文字をサジェスト
|
// 最近使った絵文字をサジェスト
|
||||||
emojis.value = defaultStore.state.recentlyUsedEmojis.map(emoji => emojiDb.find(dbEmoji => dbEmoji.emoji === emoji)).filter(x => x) as EmojiDef[];
|
emojis.value = defaultStore.state.recentlyUsedEmojis
|
||||||
|
.map((emoji) =>
|
||||||
|
emojiDb.find((dbEmoji) => dbEmoji.emoji === emoji)
|
||||||
|
)
|
||||||
|
.filter((x) => x) as EmojiDef[];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const matched: EmojiDef[] = [];
|
const matched: EmojiDef[] = [];
|
||||||
const max = 30;
|
const max = 30;
|
||||||
|
|
||||||
emojiDb.some(x => {
|
emojiDb.some((x) => {
|
||||||
if (x.name.startsWith(props.q ?? '') && !x.aliasOf && !matched.some(y => y.emoji === x.emoji)) matched.push(x);
|
if (
|
||||||
|
x.name.startsWith(props.q ?? "") &&
|
||||||
|
!x.aliasOf &&
|
||||||
|
!matched.some((y) => y.emoji === x.emoji)
|
||||||
|
)
|
||||||
|
matched.push(x);
|
||||||
return matched.length === max;
|
return matched.length === max;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (matched.length < max) {
|
if (matched.length < max) {
|
||||||
emojiDb.some(x => {
|
emojiDb.some((x) => {
|
||||||
if (x.name.startsWith(props.q ?? '') && !matched.some(y => y.emoji === x.emoji)) matched.push(x);
|
if (
|
||||||
|
x.name.startsWith(props.q ?? "") &&
|
||||||
|
!matched.some((y) => y.emoji === x.emoji)
|
||||||
|
)
|
||||||
|
matched.push(x);
|
||||||
return matched.length === max;
|
return matched.length === max;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matched.length < max) {
|
if (matched.length < max) {
|
||||||
emojiDb.some(x => {
|
emojiDb.some((x) => {
|
||||||
if (x.name.includes(props.q ?? '') && !matched.some(y => y.emoji === x.emoji)) matched.push(x);
|
if (
|
||||||
|
x.name.includes(props.q ?? "") &&
|
||||||
|
!matched.some((y) => y.emoji === x.emoji)
|
||||||
|
)
|
||||||
|
matched.push(x);
|
||||||
return matched.length === max;
|
return matched.length === max;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
emojis.value = matched;
|
emojis.value = matched;
|
||||||
} else if (props.type === 'mfmTag') {
|
} else if (props.type === "mfmTag") {
|
||||||
if (!props.q || props.q === '') {
|
if (!props.q || props.q === "") {
|
||||||
mfmTags.value = MFM_TAGS;
|
mfmTags.value = MFM_TAGS;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
mfmTags.value = MFM_TAGS.filter(tag => tag.startsWith(props.q ?? ''));
|
mfmTags.value = MFM_TAGS.filter((tag) => tag.startsWith(props.q ?? ""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMousedown(event: Event) {
|
function onMousedown(event: Event) {
|
||||||
if (!contains(rootEl.value, event.target) && (rootEl.value !== event.target)) props.close();
|
if (!contains(rootEl.value, event.target) && rootEl.value !== event.target)
|
||||||
|
props.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onKeydown(event: KeyboardEvent) {
|
function onKeydown(event: KeyboardEvent) {
|
||||||
|
@ -274,7 +356,7 @@ function onKeydown(event: KeyboardEvent) {
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case 'Enter':
|
case "Enter":
|
||||||
if (select.value !== -1) {
|
if (select.value !== -1) {
|
||||||
cancel();
|
cancel();
|
||||||
(items.value[select.value] as any).click();
|
(items.value[select.value] as any).click();
|
||||||
|
@ -283,12 +365,12 @@ function onKeydown(event: KeyboardEvent) {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'Escape':
|
case "Escape":
|
||||||
cancel();
|
cancel();
|
||||||
props.close();
|
props.close();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'ArrowUp':
|
case "ArrowUp":
|
||||||
if (select.value !== -1) {
|
if (select.value !== -1) {
|
||||||
cancel();
|
cancel();
|
||||||
selectPrev();
|
selectPrev();
|
||||||
|
@ -297,8 +379,8 @@ function onKeydown(event: KeyboardEvent) {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'Tab':
|
case "Tab":
|
||||||
case 'ArrowDown':
|
case "ArrowDown":
|
||||||
cancel();
|
cancel();
|
||||||
selectNext();
|
selectNext();
|
||||||
break;
|
break;
|
||||||
|
@ -322,19 +404,19 @@ function selectPrev() {
|
||||||
|
|
||||||
function applySelect() {
|
function applySelect() {
|
||||||
for (const el of Array.from(items.value)) {
|
for (const el of Array.from(items.value)) {
|
||||||
el.removeAttribute('data-selected');
|
el.removeAttribute("data-selected");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (select.value !== -1) {
|
if (select.value !== -1) {
|
||||||
items.value[select.value].setAttribute('data-selected', 'true');
|
items.value[select.value].setAttribute("data-selected", "true");
|
||||||
(items.value[select.value] as any).focus();
|
(items.value[select.value] as any).focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function chooseUser() {
|
function chooseUser() {
|
||||||
props.close();
|
props.close();
|
||||||
os.selectUser().then(user => {
|
os.selectUser().then((user) => {
|
||||||
complete('user', user);
|
complete("user", user);
|
||||||
props.textarea.focus();
|
props.textarea.focus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -347,28 +429,31 @@ onUpdated(() => {
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
setPosition();
|
setPosition();
|
||||||
|
|
||||||
props.textarea.addEventListener('keydown', onKeydown);
|
props.textarea.addEventListener("keydown", onKeydown);
|
||||||
|
|
||||||
for (const el of Array.from(document.querySelectorAll('body *'))) {
|
for (const el of Array.from(document.querySelectorAll("body *"))) {
|
||||||
el.addEventListener('mousedown', onMousedown);
|
el.addEventListener("mousedown", onMousedown);
|
||||||
}
|
}
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
exec();
|
exec();
|
||||||
|
|
||||||
watch(() => props.q, () => {
|
watch(
|
||||||
|
() => props.q,
|
||||||
|
() => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
exec();
|
exec();
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
props.textarea.removeEventListener('keydown', onKeydown);
|
props.textarea.removeEventListener("keydown", onKeydown);
|
||||||
|
|
||||||
for (const el of Array.from(document.querySelectorAll('body *'))) {
|
for (const el of Array.from(document.querySelectorAll("body *"))) {
|
||||||
el.removeEventListener('mousedown', onMousedown);
|
el.removeEventListener("mousedown", onMousedown);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -399,7 +484,8 @@ onBeforeUnmount(() => {
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
|
||||||
&, * {
|
&,
|
||||||
|
* {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -412,10 +498,11 @@ onBeforeUnmount(() => {
|
||||||
background: var(--X3);
|
background: var(--X3);
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-selected='true'] {
|
&[data-selected="true"] {
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
|
|
||||||
&, * {
|
&,
|
||||||
|
* {
|
||||||
color: #fff !important;
|
color: #fff !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -423,7 +510,8 @@ onBeforeUnmount(() => {
|
||||||
&:active {
|
&:active {
|
||||||
background: var(--accentDarken);
|
background: var(--accentDarken);
|
||||||
|
|
||||||
&, * {
|
&,
|
||||||
|
* {
|
||||||
color: #fff !important;
|
color: #fff !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -431,7 +519,6 @@ onBeforeUnmount(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
> .users > li {
|
> .users > li {
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
min-width: 28px;
|
min-width: 28px;
|
||||||
min-height: 28px;
|
min-height: 28px;
|
||||||
|
@ -447,7 +534,6 @@ onBeforeUnmount(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
> .emojis > li {
|
> .emojis > li {
|
||||||
|
|
||||||
.emoji {
|
.emoji {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 0 4px 0 0;
|
margin: 0 4px 0 0;
|
||||||
|
@ -465,7 +551,6 @@ onBeforeUnmount(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
> .mfmTags > li {
|
> .mfmTags > li {
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,8 +7,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from "vue";
|
||||||
import * as os from '@/os';
|
import * as os from "@/os";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
userIds: string[];
|
userIds: string[];
|
||||||
|
@ -17,8 +17,8 @@ const props = defineProps<{
|
||||||
const users = ref([]);
|
const users = ref([]);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
users.value = await os.api('users/show', {
|
users.value = await os.api("users/show", {
|
||||||
userIds: props.userIds
|
userIds: props.userIds,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<button
|
<button
|
||||||
v-if="!link" class="bghgjjyj _button"
|
v-if="!link"
|
||||||
|
class="bghgjjyj _button"
|
||||||
:class="{ inline, primary, gradate, danger, rounded, full }"
|
:class="{ inline, primary, gradate, danger, rounded, full }"
|
||||||
:type="type"
|
:type="type"
|
||||||
@click="emit('click', $event)"
|
@click="emit('click', $event)"
|
||||||
|
@ -12,7 +13,8 @@
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<MkA
|
<MkA
|
||||||
v-else class="bghgjjyj _button"
|
v-else
|
||||||
|
class="bghgjjyj _button"
|
||||||
:class="{ inline, primary, gradate, danger, rounded, full }"
|
:class="{ inline, primary, gradate, danger, rounded, full }"
|
||||||
:to="to"
|
:to="to"
|
||||||
@mousedown="onMousedown"
|
@mousedown="onMousedown"
|
||||||
|
@ -25,10 +27,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { nextTick, onMounted } from 'vue';
|
import { nextTick, onMounted } from "vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
type?: 'button' | 'submit' | 'reset';
|
type?: "button" | "submit" | "reset";
|
||||||
primary?: boolean;
|
primary?: boolean;
|
||||||
gradate?: boolean;
|
gradate?: boolean;
|
||||||
rounded?: boolean;
|
rounded?: boolean;
|
||||||
|
@ -42,7 +44,7 @@ const props = defineProps<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'click', payload: MouseEvent): void;
|
(ev: "click", payload: MouseEvent): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let el = $ref<HTMLElement | null>(null);
|
let el = $ref<HTMLElement | null>(null);
|
||||||
|
@ -73,23 +75,28 @@ function onMousedown(evt: MouseEvent): void {
|
||||||
const target = evt.target! as HTMLElement;
|
const target = evt.target! as HTMLElement;
|
||||||
const rect = target.getBoundingClientRect();
|
const rect = target.getBoundingClientRect();
|
||||||
|
|
||||||
const ripple = document.createElement('div');
|
const ripple = document.createElement("div");
|
||||||
ripple.style.top = (evt.clientY - rect.top - 1).toString() + 'px';
|
ripple.style.top = (evt.clientY - rect.top - 1).toString() + "px";
|
||||||
ripple.style.left = (evt.clientX - rect.left - 1).toString() + 'px';
|
ripple.style.left = (evt.clientX - rect.left - 1).toString() + "px";
|
||||||
|
|
||||||
ripples!.appendChild(ripple);
|
ripples!.appendChild(ripple);
|
||||||
|
|
||||||
const circleCenterX = evt.clientX - rect.left;
|
const circleCenterX = evt.clientX - rect.left;
|
||||||
const circleCenterY = evt.clientY - rect.top;
|
const circleCenterY = evt.clientY - rect.top;
|
||||||
|
|
||||||
const scale = calcCircleScale(target.clientWidth, target.clientHeight, circleCenterX, circleCenterY);
|
const scale = calcCircleScale(
|
||||||
|
target.clientWidth,
|
||||||
|
target.clientHeight,
|
||||||
|
circleCenterX,
|
||||||
|
circleCenterY
|
||||||
|
);
|
||||||
|
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
ripple.style.transform = 'scale(' + (scale / 2) + ')';
|
ripple.style.transform = "scale(" + scale / 2 + ")";
|
||||||
}, 1);
|
}, 1);
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
ripple.style.transition = 'all 1s ease';
|
ripple.style.transition = "all 1s ease";
|
||||||
ripple.style.opacity = '0';
|
ripple.style.opacity = "0";
|
||||||
}, 1000);
|
}, 1000);
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
if (ripples) ripples.removeChild(ripple);
|
if (ripples) ripples.removeChild(ripple);
|
||||||
|
@ -151,7 +158,11 @@ function onMousedown(evt: MouseEvent): void {
|
||||||
&.gradate {
|
&.gradate {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: var(--fgOnAccent) !important;
|
color: var(--fgOnAccent) !important;
|
||||||
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
var(--buttonGradateA),
|
||||||
|
var(--buttonGradateB)
|
||||||
|
);
|
||||||
|
|
||||||
&:not(:disabled):hover {
|
&:not(:disabled):hover {
|
||||||
background: linear-gradient(90deg, var(--X8), var(--X8));
|
background: linear-gradient(90deg, var(--X8), var(--X8));
|
||||||
|
@ -212,7 +223,7 @@ function onMousedown(evt: MouseEvent): void {
|
||||||
background: rgba(0, 0, 0, 0.1);
|
background: rgba(0, 0, 0, 0.1);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
transition: all 0.5s cubic-bezier(0,.5,0,1);
|
transition: all 0.5s cubic-bezier(0, 0.5, 0, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,21 +6,34 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue';
|
import { ref, computed, onMounted, onBeforeUnmount, watch } from "vue";
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from "@/store";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
type Captcha = {
|
type Captcha = {
|
||||||
render(container: string | Node, options: {
|
render(
|
||||||
readonly [_ in 'sitekey' | 'theme' | 'type' | 'size' | 'tabindex' | 'callback' | 'expired' | 'expired-callback' | 'error-callback' | 'endpoint']?: unknown;
|
container: string | Node,
|
||||||
}): string;
|
options: {
|
||||||
|
readonly [_ in
|
||||||
|
| "sitekey"
|
||||||
|
| "theme"
|
||||||
|
| "type"
|
||||||
|
| "size"
|
||||||
|
| "tabindex"
|
||||||
|
| "callback"
|
||||||
|
| "expired"
|
||||||
|
| "expired-callback"
|
||||||
|
| "error-callback"
|
||||||
|
| "endpoint"]?: unknown;
|
||||||
|
}
|
||||||
|
): string;
|
||||||
remove(id: string): void;
|
remove(id: string): void;
|
||||||
execute(id: string): void;
|
execute(id: string): void;
|
||||||
reset(id?: string): void;
|
reset(id?: string): void;
|
||||||
getResponse(id: string): string;
|
getResponse(id: string): string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CaptchaProvider = 'hcaptcha' | 'recaptcha';
|
type CaptchaProvider = "hcaptcha" | "recaptcha";
|
||||||
|
|
||||||
type CaptchaContainer = {
|
type CaptchaContainer = {
|
||||||
readonly [_ in CaptchaProvider]?: Captcha;
|
readonly [_ in CaptchaProvider]?: Captcha;
|
||||||
|
@ -37,7 +50,7 @@ const props = defineProps<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'update:modelValue', v: string | null): void;
|
(ev: "update:modelValue", v: string | null): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const available = ref(false);
|
const available = ref(false);
|
||||||
|
@ -46,8 +59,10 @@ const captchaEl = ref<HTMLDivElement | undefined>();
|
||||||
|
|
||||||
const variable = computed(() => {
|
const variable = computed(() => {
|
||||||
switch (props.provider) {
|
switch (props.provider) {
|
||||||
case 'hcaptcha': return 'hcaptcha';
|
case "hcaptcha":
|
||||||
case 'recaptcha': return 'grecaptcha';
|
return "hcaptcha";
|
||||||
|
case "recaptcha":
|
||||||
|
return "grecaptcha";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -55,22 +70,30 @@ const loaded = !!window[variable.value];
|
||||||
|
|
||||||
const src = computed(() => {
|
const src = computed(() => {
|
||||||
switch (props.provider) {
|
switch (props.provider) {
|
||||||
case 'hcaptcha': return 'https://js.hcaptcha.com/1/api.js?render=explicit&recaptchacompat=off';
|
case "hcaptcha":
|
||||||
case 'recaptcha': return 'https://www.recaptcha.net/recaptcha/api.js?render=explicit';
|
return "https://js.hcaptcha.com/1/api.js?render=explicit&recaptchacompat=off";
|
||||||
|
case "recaptcha":
|
||||||
|
return "https://www.recaptcha.net/recaptcha/api.js?render=explicit";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const captcha = computed<Captcha>(() => window[variable.value] || {} as unknown as Captcha);
|
const captcha = computed<Captcha>(
|
||||||
|
() => window[variable.value] || ({} as unknown as Captcha)
|
||||||
|
);
|
||||||
|
|
||||||
if (loaded) {
|
if (loaded) {
|
||||||
available.value = true;
|
available.value = true;
|
||||||
} else {
|
} else {
|
||||||
(document.getElementById(props.provider) || document.head.appendChild(Object.assign(document.createElement('script'), {
|
(
|
||||||
|
document.getElementById(props.provider) ||
|
||||||
|
document.head.appendChild(
|
||||||
|
Object.assign(document.createElement("script"), {
|
||||||
async: true,
|
async: true,
|
||||||
id: props.provider,
|
id: props.provider,
|
||||||
src: src.value,
|
src: src.value,
|
||||||
})))
|
})
|
||||||
.addEventListener('load', () => available.value = true);
|
)
|
||||||
|
).addEventListener("load", () => (available.value = true));
|
||||||
}
|
}
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
|
@ -81,10 +104,10 @@ function requestRender() {
|
||||||
if (captcha.value.render && captchaEl.value instanceof Element) {
|
if (captcha.value.render && captchaEl.value instanceof Element) {
|
||||||
captcha.value.render(captchaEl.value, {
|
captcha.value.render(captchaEl.value, {
|
||||||
sitekey: props.sitekey,
|
sitekey: props.sitekey,
|
||||||
theme: defaultStore.state.darkMode ? 'dark' : 'light',
|
theme: defaultStore.state.darkMode ? "dark" : "light",
|
||||||
callback: callback,
|
callback: callback,
|
||||||
'expired-callback': callback,
|
"expired-callback": callback,
|
||||||
'error-callback': callback,
|
"error-callback": callback,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
window.setTimeout(requestRender, 1);
|
window.setTimeout(requestRender, 1);
|
||||||
|
@ -92,7 +115,7 @@ function requestRender() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function callback(response?: string) {
|
function callback(response?: string) {
|
||||||
emit('update:modelValue', typeof response === 'string' ? response : null);
|
emit("update:modelValue", typeof response === "string" ? response : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
@ -110,5 +133,4 @@ onBeforeUnmount(() => {
|
||||||
defineExpose({
|
defineExpose({
|
||||||
reset,
|
reset,
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,34 +1,41 @@
|
||||||
<template>
|
<template>
|
||||||
<button class="hdcaacmi _button"
|
<button
|
||||||
|
class="hdcaacmi _button"
|
||||||
:class="{ wait, active: isFollowing, full }"
|
:class="{ wait, active: isFollowing, full }"
|
||||||
:disabled="wait"
|
:disabled="wait"
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
>
|
>
|
||||||
<template v-if="!wait">
|
<template v-if="!wait">
|
||||||
<template v-if="isFollowing">
|
<template v-if="isFollowing">
|
||||||
<span v-if="full">{{ i18n.ts.unfollow }}</span><i class="ph-minus ph-bold ph-lg"></i>
|
<span v-if="full">{{ i18n.ts.unfollow }}</span
|
||||||
|
><i class="ph-minus ph-bold ph-lg"></i>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<span v-if="full">{{ i18n.ts.follow }}</span><i class="ph-plus ph-bold ph-lg"></i>
|
<span v-if="full">{{ i18n.ts.follow }}</span
|
||||||
|
><i class="ph-plus ph-bold ph-lg"></i>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<span v-if="full">{{ i18n.ts.processing }}</span><i class="ph-circle-notch ph-bold ph-lg fa-pulse ph-fw ph-lg"></i>
|
<span v-if="full">{{ i18n.ts.processing }}</span
|
||||||
|
><i class="ph-circle-notch ph-bold ph-lg fa-pulse ph-fw ph-lg"></i>
|
||||||
</template>
|
</template>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from "vue";
|
||||||
import * as os from '@/os';
|
import * as os from "@/os";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
channel: Record<string, any>;
|
channel: Record<string, any>;
|
||||||
full?: boolean;
|
full?: boolean;
|
||||||
}>(), {
|
}>(),
|
||||||
|
{
|
||||||
full: false,
|
full: false,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const isFollowing = ref<boolean>(props.channel.isFollowing);
|
const isFollowing = ref<boolean>(props.channel.isFollowing);
|
||||||
const wait = ref(false);
|
const wait = ref(false);
|
||||||
|
@ -38,13 +45,13 @@ async function onClick() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isFollowing.value) {
|
if (isFollowing.value) {
|
||||||
await os.api('channels/unfollow', {
|
await os.api("channels/unfollow", {
|
||||||
channelId: props.channel.id
|
channelId: props.channel.id,
|
||||||
});
|
});
|
||||||
isFollowing.value = false;
|
isFollowing.value = false;
|
||||||
} else {
|
} else {
|
||||||
await os.api('channels/follow', {
|
await os.api("channels/follow", {
|
||||||
channelId: props.channel.id
|
channelId: props.channel.id,
|
||||||
});
|
});
|
||||||
isFollowing.value = true;
|
isFollowing.value = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,17 @@
|
||||||
<MkA :to="`/channels/${channel.id}`" class="eftoefju _panel" tabindex="-1">
|
<MkA :to="`/channels/${channel.id}`" class="eftoefju _panel" tabindex="-1">
|
||||||
<div class="banner" :style="bannerStyle">
|
<div class="banner" :style="bannerStyle">
|
||||||
<div class="fade"></div>
|
<div class="fade"></div>
|
||||||
<div class="name"><i class="ph-television ph-bold ph-lg"></i> {{ channel.name }}</div>
|
<div class="name">
|
||||||
|
<i class="ph-television ph-bold ph-lg"></i> {{ channel.name }}
|
||||||
|
</div>
|
||||||
<div class="status">
|
<div class="status">
|
||||||
<div>
|
<div>
|
||||||
<i class="ph-users ph-bold ph-lg ph-fw ph-lg"></i>
|
<i class="ph-users ph-bold ph-lg ph-fw ph-lg"></i>
|
||||||
<I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;">
|
<I18n
|
||||||
|
:src="i18n.ts._channel.usersCount"
|
||||||
|
tag="span"
|
||||||
|
style="margin-left: 4px"
|
||||||
|
>
|
||||||
<template #n>
|
<template #n>
|
||||||
<b>{{ channel.usersCount }}</b>
|
<b>{{ channel.usersCount }}</b>
|
||||||
</template>
|
</template>
|
||||||
|
@ -14,7 +20,11 @@
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<i class="ph-pencil ph-bold ph-lg ph-fw ph-lg"></i>
|
<i class="ph-pencil ph-bold ph-lg ph-fw ph-lg"></i>
|
||||||
<I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;">
|
<I18n
|
||||||
|
:src="i18n.ts._channel.notesCount"
|
||||||
|
tag="span"
|
||||||
|
style="margin-left: 4px"
|
||||||
|
>
|
||||||
<template #n>
|
<template #n>
|
||||||
<b>{{ channel.notesCount }}</b>
|
<b>{{ channel.notesCount }}</b>
|
||||||
</template>
|
</template>
|
||||||
|
@ -23,7 +33,13 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<article v-if="channel.description">
|
<article v-if="channel.description">
|
||||||
<p :title="channel.description">{{ channel.description.length > 85 ? channel.description.slice(0, 85) + '…' : channel.description }}</p>
|
<p :title="channel.description">
|
||||||
|
{{
|
||||||
|
channel.description.length > 85
|
||||||
|
? channel.description.slice(0, 85) + "…"
|
||||||
|
: channel.description
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
</article>
|
</article>
|
||||||
<footer>
|
<footer>
|
||||||
<span v-if="channel.lastNotedAt">
|
<span v-if="channel.lastNotedAt">
|
||||||
|
@ -34,8 +50,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from "vue";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
channel: Record<string, any>;
|
channel: Record<string, any>;
|
||||||
|
@ -45,7 +61,7 @@ const bannerStyle = computed(() => {
|
||||||
if (props.channel.bannerUrl) {
|
if (props.channel.bannerUrl) {
|
||||||
return { backgroundImage: `url(${props.channel.bannerUrl})` };
|
return { backgroundImage: `url(${props.channel.bannerUrl})` };
|
||||||
} else {
|
} else {
|
||||||
return { backgroundColor: '#4c5e6d' };
|
return { backgroundColor: "#4c5e6d" };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -155,5 +171,4 @@ const bannerStyle = computed(() => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,10 +1,25 @@
|
||||||
<template>
|
<template>
|
||||||
<MkTooltip ref="tooltip" :showing="showing" :x="x" :y="y" :max-width="340" :direction="'top'" :inner-margin="16" @closed="emit('closed')">
|
<MkTooltip
|
||||||
|
ref="tooltip"
|
||||||
|
:showing="showing"
|
||||||
|
:x="x"
|
||||||
|
:y="y"
|
||||||
|
:max-width="340"
|
||||||
|
:direction="'top'"
|
||||||
|
:inner-margin="16"
|
||||||
|
@closed="emit('closed')"
|
||||||
|
>
|
||||||
<div v-if="title || series" class="qpcyisrl">
|
<div v-if="title || series" class="qpcyisrl">
|
||||||
<div v-if="title" class="title">{{ title }}</div>
|
<div v-if="title" class="title">{{ title }}</div>
|
||||||
<template v-if="series">
|
<template v-if="series">
|
||||||
<div v-for="x in series" class="series">
|
<div v-for="x in series" class="series">
|
||||||
<span class="color" :style="{ background: x.backgroundColor, borderColor: x.borderColor }"></span>
|
<span
|
||||||
|
class="color"
|
||||||
|
:style="{
|
||||||
|
background: x.backgroundColor,
|
||||||
|
borderColor: x.borderColor,
|
||||||
|
}"
|
||||||
|
></span>
|
||||||
<span>{{ x.text }}</span>
|
<span>{{ x.text }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -13,8 +28,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import {} from "vue";
|
||||||
import MkTooltip from './MkTooltip.vue';
|
import MkTooltip from "./MkTooltip.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
showing: boolean;
|
showing: boolean;
|
||||||
|
@ -29,7 +44,7 @@ const props = defineProps<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'closed'): void;
|
(ev: "closed"): void;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,9 @@
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
:class="{
|
:class="{
|
||||||
isMe: isMe(message),
|
isMe: isMe(message),
|
||||||
isRead: message.groupId ? message.reads.includes($i?.id) : message.isRead,
|
isRead: message.groupId
|
||||||
|
? message.reads.includes($i?.id)
|
||||||
|
: message.isRead,
|
||||||
}"
|
}"
|
||||||
:to="
|
:to="
|
||||||
message.groupId
|
message.groupId
|
||||||
|
@ -31,14 +33,28 @@
|
||||||
<MkTime :time="message.createdAt" class="time" />
|
<MkTime :time="message.createdAt" class="time" />
|
||||||
</header>
|
</header>
|
||||||
<header v-else>
|
<header v-else>
|
||||||
<span class="name"><MkUserName :user="isMe(message) ? message.recipient : message.user"/></span>
|
<span class="name"
|
||||||
<span class="username">@{{ acct(isMe(message) ? message.recipient : message.user) }}</span>
|
><MkUserName
|
||||||
|
:user="
|
||||||
|
isMe(message) ? message.recipient : message.user
|
||||||
|
"
|
||||||
|
/></span>
|
||||||
|
<span class="username"
|
||||||
|
>@{{
|
||||||
|
acct(isMe(message) ? message.recipient : message.user)
|
||||||
|
}}</span
|
||||||
|
>
|
||||||
<MkTime :time="message.createdAt" class="time" />
|
<MkTime :time="message.createdAt" class="time" />
|
||||||
</header>
|
</header>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<p class="text">
|
<p class="text">
|
||||||
<span v-if="isMe(message)" class="me">{{ i18n.ts.you }}: </span>
|
<span v-if="isMe(message)" class="me"
|
||||||
<Mfm v-if="message.text != null && message.text.length > 0" :text="message.text"/>
|
>{{ i18n.ts.you }}:
|
||||||
|
</span>
|
||||||
|
<Mfm
|
||||||
|
v-if="message.text != null && message.text.length > 0"
|
||||||
|
:text="message.text"
|
||||||
|
/>
|
||||||
<span v-else> 📎</span>
|
<span v-else> 📎</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -47,10 +63,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import * as Acct from 'calckey-js/built/acct';
|
import * as Acct from "calckey-js/built/acct";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
import { acct } from '@/filters/user';
|
import { acct } from "@/filters/user";
|
||||||
import { $i } from '@/account';
|
import { $i } from "@/account";
|
||||||
|
|
||||||
const getAcct = Acct.toString;
|
const getAcct = Acct.toString;
|
||||||
|
|
||||||
|
@ -138,7 +154,6 @@ function isMe(message): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
> .body {
|
> .body {
|
||||||
|
|
||||||
> .text {
|
> .text {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0 0 0 0;
|
margin: 0 0 0 0;
|
||||||
|
|
|
@ -16,13 +16,13 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import XModalWindow from '@/components/MkModalWindow.vue';
|
import XModalWindow from "@/components/MkModalWindow.vue";
|
||||||
import XCheatSheet from '@/pages/mfm-cheat-sheet.vue';
|
import XCheatSheet from "@/pages/mfm-cheat-sheet.vue";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'done'): void;
|
(ev: "done"): void;
|
||||||
(ev: 'closed'): void;
|
(ev: "closed"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const dialog = $ref<InstanceType<typeof XModalWindow>>();
|
const dialog = $ref<InstanceType<typeof XModalWindow>>();
|
||||||
|
@ -41,6 +41,4 @@ function close(res) {
|
||||||
.fade-leave-to {
|
.fade-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<code v-if="inline" :class="`language-${prismLang}`" v-html="html"></code>
|
<code v-if="inline" :class="`language-${prismLang}`" v-html="html"></code>
|
||||||
<pre v-else :class="`language-${prismLang}`"><code :class="`language-${prismLang}`" v-html="html"></code></pre>
|
<pre
|
||||||
|
v-else
|
||||||
|
:class="`language-${prismLang}`"
|
||||||
|
><code :class="`language-${prismLang}`" v-html="html"></code></pre>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from "vue";
|
||||||
import Prism from 'prismjs';
|
import Prism from "prismjs";
|
||||||
import 'prismjs/themes/prism-okaidia.css';
|
import "prismjs/themes/prism-okaidia.css";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
code: string;
|
code: string;
|
||||||
|
@ -14,6 +17,14 @@ const props = defineProps<{
|
||||||
inline?: boolean;
|
inline?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const prismLang = computed(() => Prism.languages[props.lang] ? props.lang : 'js');
|
const prismLang = computed(() =>
|
||||||
const html = computed(() => Prism.highlight(props.code, Prism.languages[prismLang.value], prismLang.value));
|
Prism.languages[props.lang] ? props.lang : "js"
|
||||||
|
);
|
||||||
|
const html = computed(() =>
|
||||||
|
Prism.highlight(
|
||||||
|
props.code,
|
||||||
|
Prism.languages[prismLang.value],
|
||||||
|
prismLang.value
|
||||||
|
)
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineAsyncComponent } from 'vue';
|
import { defineAsyncComponent } from "vue";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
code: string;
|
code: string;
|
||||||
|
@ -11,5 +11,7 @@ defineProps<{
|
||||||
inline?: boolean;
|
inline?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const XCode = defineAsyncComponent(() => import('@/components/MkCode.core.vue'));
|
const XCode = defineAsyncComponent(
|
||||||
|
() => import("@/components/MkCode.core.vue")
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,12 +1,30 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-size="{ max: [380] }" class="ukygtjoj _panel" :class="{ naked, thin, hideHeader: !showHeader, scrollable, closed: !showBody }">
|
<div
|
||||||
|
v-size="{ max: [380] }"
|
||||||
|
class="ukygtjoj _panel"
|
||||||
|
:class="{
|
||||||
|
naked,
|
||||||
|
thin,
|
||||||
|
hideHeader: !showHeader,
|
||||||
|
scrollable,
|
||||||
|
closed: !showBody,
|
||||||
|
}"
|
||||||
|
>
|
||||||
<header v-if="showHeader" ref="header">
|
<header v-if="showHeader" ref="header">
|
||||||
<div class="title"><slot name="header"></slot></div>
|
<div class="title"><slot name="header"></slot></div>
|
||||||
<div class="sub">
|
<div class="sub">
|
||||||
<slot name="func"></slot>
|
<slot name="func"></slot>
|
||||||
<button v-if="foldable" class="_button" @click="() => showBody = !showBody">
|
<button
|
||||||
<template v-if="showBody"><i class="ph-caret-up ph-bold ph-lg"></i></template>
|
v-if="foldable"
|
||||||
<template v-else><i class="ph-caret-down ph-bold ph-lg"></i></template>
|
class="_button"
|
||||||
|
@click="() => (showBody = !showBody)"
|
||||||
|
>
|
||||||
|
<template v-if="showBody"
|
||||||
|
><i class="ph-caret-up ph-bold ph-lg"></i
|
||||||
|
></template>
|
||||||
|
<template v-else
|
||||||
|
><i class="ph-caret-down ph-bold ph-lg"></i
|
||||||
|
></template>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
@ -17,9 +35,23 @@
|
||||||
@leave="leave"
|
@leave="leave"
|
||||||
@after-leave="afterLeave"
|
@after-leave="afterLeave"
|
||||||
>
|
>
|
||||||
<div v-show="showBody" ref="content" class="content" :class="{ omitted }">
|
<div
|
||||||
|
v-show="showBody"
|
||||||
|
ref="content"
|
||||||
|
class="content"
|
||||||
|
:class="{ omitted }"
|
||||||
|
>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
<button v-if="omitted" class="fade _button" @click="() => { ignoreOmit = true; omitted = false; }">
|
<button
|
||||||
|
v-if="omitted"
|
||||||
|
class="fade _button"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
ignoreOmit = true;
|
||||||
|
omitted = false;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
<span>{{ i18n.ts.showMore }}</span>
|
<span>{{ i18n.ts.showMore }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -28,8 +60,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from "vue";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
|
@ -78,22 +110,29 @@ export default defineComponent({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$watch('showBody', showBody => {
|
this.$watch(
|
||||||
const headerHeight = this.showHeader ? this.$refs.header.offsetHeight : 0;
|
"showBody",
|
||||||
|
(showBody) => {
|
||||||
|
const headerHeight = this.showHeader
|
||||||
|
? this.$refs.header.offsetHeight
|
||||||
|
: 0;
|
||||||
this.$el.style.minHeight = `${headerHeight}px`;
|
this.$el.style.minHeight = `${headerHeight}px`;
|
||||||
if (showBody) {
|
if (showBody) {
|
||||||
this.$el.style.flexBasis = 'auto';
|
this.$el.style.flexBasis = "auto";
|
||||||
} else {
|
} else {
|
||||||
this.$el.style.flexBasis = `${headerHeight}px`;
|
this.$el.style.flexBasis = `${headerHeight}px`;
|
||||||
}
|
}
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
immediate: true,
|
immediate: true,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
this.$el.style.setProperty('--maxHeight', this.maxHeight + 'px');
|
this.$el.style.setProperty("--maxHeight", this.maxHeight + "px");
|
||||||
|
|
||||||
const calcOmit = () => {
|
const calcOmit = () => {
|
||||||
if (this.omitted || this.ignoreOmit || this.maxHeight == null) return;
|
if (this.omitted || this.ignoreOmit || this.maxHeight == null)
|
||||||
|
return;
|
||||||
const height = this.$refs.content.offsetHeight;
|
const height = this.$refs.content.offsetHeight;
|
||||||
this.omitted = height > this.maxHeight;
|
this.omitted = height > this.maxHeight;
|
||||||
};
|
};
|
||||||
|
@ -113,14 +152,14 @@ export default defineComponent({
|
||||||
const elementHeight = el.getBoundingClientRect().height;
|
const elementHeight = el.getBoundingClientRect().height;
|
||||||
el.style.height = 0;
|
el.style.height = 0;
|
||||||
el.offsetHeight; // reflow
|
el.offsetHeight; // reflow
|
||||||
el.style.height = elementHeight + 'px';
|
el.style.height = elementHeight + "px";
|
||||||
},
|
},
|
||||||
afterEnter(el) {
|
afterEnter(el) {
|
||||||
el.style.height = null;
|
el.style.height = null;
|
||||||
},
|
},
|
||||||
leave(el) {
|
leave(el) {
|
||||||
const elementHeight = el.getBoundingClientRect().height;
|
const elementHeight = el.getBoundingClientRect().height;
|
||||||
el.style.height = elementHeight + 'px';
|
el.style.height = elementHeight + "px";
|
||||||
el.offsetHeight; // reflow
|
el.offsetHeight; // reflow
|
||||||
el.style.height = 0;
|
el.style.height = 0;
|
||||||
},
|
},
|
||||||
|
@ -132,7 +171,8 @@ export default defineComponent({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.container-toggle-enter-active, .container-toggle-leave-active {
|
.container-toggle-enter-active,
|
||||||
|
.container-toggle-leave-active {
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
transition: opacity 0.5s, height 0.5s !important;
|
transition: opacity 0.5s, height 0.5s !important;
|
||||||
}
|
}
|
||||||
|
@ -236,7 +276,8 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.max-width_380px, &.thin {
|
&.max-width_380px,
|
||||||
|
&.thin {
|
||||||
> header {
|
> header {
|
||||||
> .title {
|
> .title {
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
|
|
|
@ -1,17 +1,22 @@
|
||||||
<template>
|
<template>
|
||||||
<transition :name="$store.state.animation ? 'fade' : ''" appear>
|
<transition :name="$store.state.animation ? 'fade' : ''" appear>
|
||||||
<div ref="rootEl" class="nvlagfpb" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}">
|
<div
|
||||||
|
ref="rootEl"
|
||||||
|
class="nvlagfpb"
|
||||||
|
:style="{ zIndex }"
|
||||||
|
@contextmenu.prevent.stop="() => {}"
|
||||||
|
>
|
||||||
<MkMenu :items="items" :align="'left'" @close="$emit('closed')" />
|
<MkMenu :items="items" :align="'left'" @close="$emit('closed')" />
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, onBeforeUnmount } from 'vue';
|
import { onMounted, onBeforeUnmount } from "vue";
|
||||||
import MkMenu from './MkMenu.vue';
|
import MkMenu from "./MkMenu.vue";
|
||||||
import { MenuItem } from './types/menu.vue';
|
import { MenuItem } from "./types/menu.vue";
|
||||||
import contains from '@/scripts/contains';
|
import contains from "@/scripts/contains";
|
||||||
import * as os from '@/os';
|
import * as os from "@/os";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
items: MenuItem[];
|
items: MenuItem[];
|
||||||
|
@ -19,12 +24,12 @@ const props = defineProps<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'closed'): void;
|
(ev: "closed"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let rootEl = $ref<HTMLDivElement>();
|
let rootEl = $ref<HTMLDivElement>();
|
||||||
|
|
||||||
let zIndex = $ref<number>(os.claimZIndex('high'));
|
let zIndex = $ref<number>(os.claimZIndex("high"));
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
let left = props.ev.pageX + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
|
let left = props.ev.pageX + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
|
||||||
|
@ -52,19 +57,19 @@ onMounted(() => {
|
||||||
rootEl.style.top = `${top}px`;
|
rootEl.style.top = `${top}px`;
|
||||||
rootEl.style.left = `${left}px`;
|
rootEl.style.left = `${left}px`;
|
||||||
|
|
||||||
for (const el of Array.from(document.querySelectorAll('body *'))) {
|
for (const el of Array.from(document.querySelectorAll("body *"))) {
|
||||||
el.addEventListener('mousedown', onMousedown);
|
el.addEventListener("mousedown", onMousedown);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
for (const el of Array.from(document.querySelectorAll('body *'))) {
|
for (const el of Array.from(document.querySelectorAll("body *"))) {
|
||||||
el.removeEventListener('mousedown', onMousedown);
|
el.removeEventListener("mousedown", onMousedown);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function onMousedown(evt: Event) {
|
function onMousedown(evt: Event) {
|
||||||
if (!contains(rootEl, evt.target) && (rootEl !== evt.target)) emit('closed');
|
if (!contains(rootEl, evt.target) && rootEl !== evt.target) emit("closed");
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -73,12 +78,15 @@ function onMousedown(evt: Event) {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter-active, .fade-leave-active {
|
.fade-enter-active,
|
||||||
transition: opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1), transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1),
|
||||||
|
transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
transform-origin: left top;
|
transform-origin: left top;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter-from, .fade-leave-to {
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: scale(0.9);
|
transform: scale(0.9);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,14 +11,22 @@
|
||||||
>
|
>
|
||||||
<template #header>{{ i18n.ts.cropImage }}</template>
|
<template #header>{{ i18n.ts.cropImage }}</template>
|
||||||
<template #default="{ width, height }">
|
<template #default="{ width, height }">
|
||||||
<div class="mk-cropper-dialog" :style="`--vw: ${width}px; --vh: ${height}px;`">
|
<div
|
||||||
|
class="mk-cropper-dialog"
|
||||||
|
:style="`--vw: ${width}px; --vh: ${height}px;`"
|
||||||
|
>
|
||||||
<Transition name="fade">
|
<Transition name="fade">
|
||||||
<div v-if="loading" class="loading">
|
<div v-if="loading" class="loading">
|
||||||
<MkLoading />
|
<MkLoading />
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<img ref="imgEl" :src="imgUrl" style="display: none;" @load="onImageLoad">
|
<img
|
||||||
|
ref="imgEl"
|
||||||
|
:src="imgUrl"
|
||||||
|
style="display: none"
|
||||||
|
@load="onImageLoad"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -26,22 +34,22 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { nextTick, onMounted } from 'vue';
|
import { nextTick, onMounted } from "vue";
|
||||||
import * as misskey from 'calckey-js';
|
import * as misskey from "calckey-js";
|
||||||
import Cropper from 'cropperjs';
|
import Cropper from "cropperjs";
|
||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from "tinycolor2";
|
||||||
import XModalWindow from '@/components/MkModalWindow.vue';
|
import XModalWindow from "@/components/MkModalWindow.vue";
|
||||||
import * as os from '@/os';
|
import * as os from "@/os";
|
||||||
import { $i } from '@/account';
|
import { $i } from "@/account";
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from "@/store";
|
||||||
import { apiUrl, url } from '@/config';
|
import { apiUrl, url } from "@/config";
|
||||||
import { query } from '@/scripts/url';
|
import { query } from "@/scripts/url";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'ok', cropped: misskey.entities.DriveFile): void;
|
(ev: "ok", cropped: misskey.entities.DriveFile): void;
|
||||||
(ev: 'cancel'): void;
|
(ev: "cancel"): void;
|
||||||
(ev: 'closed'): void;
|
(ev: "closed"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
@ -60,22 +68,22 @@ let loading = $ref(true);
|
||||||
const ok = async () => {
|
const ok = async () => {
|
||||||
const promise = new Promise<misskey.entities.DriveFile>(async (res) => {
|
const promise = new Promise<misskey.entities.DriveFile>(async (res) => {
|
||||||
const croppedCanvas = await cropper?.getCropperSelection()?.$toCanvas();
|
const croppedCanvas = await cropper?.getCropperSelection()?.$toCanvas();
|
||||||
croppedCanvas.toBlob(blob => {
|
croppedCanvas.toBlob((blob) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', blob);
|
formData.append("file", blob);
|
||||||
if (defaultStore.state.uploadFolder) {
|
if (defaultStore.state.uploadFolder) {
|
||||||
formData.append('folderId', defaultStore.state.uploadFolder);
|
formData.append("folderId", defaultStore.state.uploadFolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch(apiUrl + '/drive/files/create', {
|
fetch(apiUrl + "/drive/files/create", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
body: formData,
|
body: formData,
|
||||||
headers: {
|
headers: {
|
||||||
authorization: `Bearer ${$i.token}`,
|
authorization: `Bearer ${$i.token}`,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then((response) => response.json())
|
||||||
.then(f => {
|
.then((f) => {
|
||||||
res(f);
|
res(f);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -85,12 +93,12 @@ const ok = async () => {
|
||||||
|
|
||||||
const f = await promise;
|
const f = await promise;
|
||||||
|
|
||||||
emit('ok', f);
|
emit("ok", f);
|
||||||
dialogEl.close();
|
dialogEl.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
const cancel = () => {
|
const cancel = () => {
|
||||||
emit('cancel');
|
emit("cancel");
|
||||||
dialogEl.close();
|
dialogEl.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -98,31 +106,32 @@ const onImageLoad = () => {
|
||||||
loading = false;
|
loading = false;
|
||||||
|
|
||||||
if (cropper) {
|
if (cropper) {
|
||||||
cropper.getCropperImage()!.$center('contain');
|
cropper.getCropperImage()!.$center("contain");
|
||||||
cropper.getCropperSelection()!.$center();
|
cropper.getCropperSelection()!.$center();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
cropper = new Cropper(imgEl, {
|
cropper = new Cropper(imgEl, {});
|
||||||
});
|
|
||||||
|
|
||||||
const computedStyle = getComputedStyle(document.documentElement);
|
const computedStyle = getComputedStyle(document.documentElement);
|
||||||
|
|
||||||
const selection = cropper.getCropperSelection()!;
|
const selection = cropper.getCropperSelection()!;
|
||||||
selection.themeColor = tinycolor(computedStyle.getPropertyValue('--accent')).toHexString();
|
selection.themeColor = tinycolor(
|
||||||
|
computedStyle.getPropertyValue("--accent")
|
||||||
|
).toHexString();
|
||||||
selection.aspectRatio = props.aspectRatio;
|
selection.aspectRatio = props.aspectRatio;
|
||||||
selection.initialAspectRatio = props.aspectRatio;
|
selection.initialAspectRatio = props.aspectRatio;
|
||||||
selection.outlined = true;
|
selection.outlined = true;
|
||||||
|
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
cropper.getCropperImage()!.$center('contain');
|
cropper.getCropperImage()!.$center("contain");
|
||||||
selection.$center();
|
selection.$center();
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
// モーダルオープンアニメーションが終わったあとで再度調整
|
// モーダルオープンアニメーションが終わったあとで再度調整
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
cropper.getCropperImage()!.$center('contain');
|
cropper.getCropperImage()!.$center("contain");
|
||||||
selection.$center();
|
selection.$center();
|
||||||
}, 500);
|
}, 500);
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,11 +6,11 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from "vue";
|
||||||
import { length } from 'stringz';
|
import { length } from "stringz";
|
||||||
import * as misskey from 'calckey-js';
|
import * as misskey from "calckey-js";
|
||||||
import { concat } from '@/scripts/array';
|
import { concat } from "@/scripts/array";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: boolean;
|
modelValue: boolean;
|
||||||
|
@ -18,19 +18,23 @@ const props = defineProps<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'update:modelValue', v: boolean): void;
|
(ev: "update:modelValue", v: boolean): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const label = computed(() => {
|
const label = computed(() => {
|
||||||
return concat([
|
return concat([
|
||||||
props.note.text ? [i18n.t('_cw.chars', { count: length(props.note.text) })] : [],
|
props.note.text
|
||||||
props.note.files && props.note.files.length !== 0 ? [i18n.t('_cw.files', { count: props.note.files.length }) ] : [],
|
? [i18n.t("_cw.chars", { count: length(props.note.text) })]
|
||||||
props.note.poll != null ? [i18n.ts.poll] : []
|
: [],
|
||||||
] as string[][]).join(' / ');
|
props.note.files && props.note.files.length !== 0
|
||||||
|
? [i18n.t("_cw.files", { count: props.note.files.length })]
|
||||||
|
: [],
|
||||||
|
props.note.poll != null ? [i18n.ts.poll] : [],
|
||||||
|
] as string[][]).join(" / ");
|
||||||
});
|
});
|
||||||
|
|
||||||
const toggle = () => {
|
const toggle = () => {
|
||||||
emit('update:modelValue', !props.modelValue);
|
emit("update:modelValue", !props.modelValue);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -60,11 +64,11 @@ const toggle = () => {
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
content: '(';
|
content: "(";
|
||||||
}
|
}
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
content: ')';
|
content: ")";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,21 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, h, PropType, TransitionGroup } from 'vue';
|
import { defineComponent, h, PropType, TransitionGroup } from "vue";
|
||||||
import MkAd from '@/components/global/MkAd.vue';
|
import MkAd from "@/components/global/MkAd.vue";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from "@/store";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
items: {
|
items: {
|
||||||
type: Array as PropType<{ id: string; createdAt: string; _shouldInsertAd_: boolean; }[]>,
|
type: Array as PropType<
|
||||||
|
{ id: string; createdAt: string; _shouldInsertAd_: boolean }[]
|
||||||
|
>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
direction: {
|
direction: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: 'down',
|
default: "down",
|
||||||
},
|
},
|
||||||
reversed: {
|
reversed: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
@ -36,7 +38,7 @@ export default defineComponent({
|
||||||
function getDateText(time: string) {
|
function getDateText(time: string) {
|
||||||
const date = new Date(time).getDate();
|
const date = new Date(time).getDate();
|
||||||
const month = new Date(time).getMonth() + 1;
|
const month = new Date(time).getMonth() + 1;
|
||||||
return i18n.t('monthAndDay', {
|
return i18n.t("monthAndDay", {
|
||||||
month: month.toString(),
|
month: month.toString(),
|
||||||
day: date.toString(),
|
day: date.toString(),
|
||||||
});
|
});
|
||||||
|
@ -44,7 +46,8 @@ export default defineComponent({
|
||||||
|
|
||||||
if (props.items.length === 0) return;
|
if (props.items.length === 0) return;
|
||||||
|
|
||||||
const renderChildren = () => props.items.map((item, i) => {
|
const renderChildren = () =>
|
||||||
|
props.items.map((item, i) => {
|
||||||
if (!slots || !slots.default) return;
|
if (!slots || !slots.default) return;
|
||||||
|
|
||||||
const el = slots.default({
|
const el = slots.default({
|
||||||
|
@ -54,54 +57,70 @@ export default defineComponent({
|
||||||
|
|
||||||
if (
|
if (
|
||||||
i !== props.items.length - 1 &&
|
i !== props.items.length - 1 &&
|
||||||
new Date(item.createdAt).getDate() !== new Date(props.items[i + 1].createdAt).getDate()
|
new Date(item.createdAt).getDate() !==
|
||||||
|
new Date(props.items[i + 1].createdAt).getDate()
|
||||||
) {
|
) {
|
||||||
const separator = h('div', {
|
const separator = h(
|
||||||
class: 'separator',
|
"div",
|
||||||
key: item.id + ':separator',
|
{
|
||||||
}, h('p', {
|
class: "separator",
|
||||||
class: 'date',
|
key: item.id + ":separator",
|
||||||
}, [
|
},
|
||||||
h('span', [
|
h(
|
||||||
h('i', {
|
"p",
|
||||||
class: 'ph-caret-up ph-bold ph-lg icon',
|
{
|
||||||
|
class: "date",
|
||||||
|
},
|
||||||
|
[
|
||||||
|
h("span", [
|
||||||
|
h("i", {
|
||||||
|
class: "ph-caret-up ph-bold ph-lg icon",
|
||||||
}),
|
}),
|
||||||
getDateText(item.createdAt),
|
getDateText(item.createdAt),
|
||||||
]),
|
]),
|
||||||
h('span', [
|
h("span", [
|
||||||
getDateText(props.items[i + 1].createdAt),
|
getDateText(props.items[i + 1].createdAt),
|
||||||
h('i', {
|
h("i", {
|
||||||
class: 'ph-caret-down ph-bold ph-lg icon',
|
class: "ph-caret-down ph-bold ph-lg icon",
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
]));
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
return [el, separator];
|
return [el, separator];
|
||||||
} else {
|
} else {
|
||||||
if (props.ad && item._shouldInsertAd_) {
|
if (props.ad && item._shouldInsertAd_) {
|
||||||
return [h(MkAd, {
|
return [
|
||||||
class: 'a', // advertiseの意(ブロッカー対策)
|
h(MkAd, {
|
||||||
key: item.id + ':ad',
|
class: "a", // advertiseの意(ブロッカー対策)
|
||||||
prefer: ['horizontal', 'horizontal-big'],
|
key: item.id + ":ad",
|
||||||
}), el];
|
prefer: ["horizontal", "horizontal-big"],
|
||||||
|
}),
|
||||||
|
el,
|
||||||
|
];
|
||||||
} else {
|
} else {
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => h(
|
return () =>
|
||||||
defaultStore.state.animation ? TransitionGroup : 'div',
|
h(
|
||||||
defaultStore.state.animation ? {
|
defaultStore.state.animation ? TransitionGroup : "div",
|
||||||
class: 'sqadhkmv' + (props.noGap ? ' noGap' : ''),
|
defaultStore.state.animation
|
||||||
name: 'list',
|
? {
|
||||||
tag: 'div',
|
class: "sqadhkmv" + (props.noGap ? " noGap" : ""),
|
||||||
'data-direction': props.direction,
|
name: "list",
|
||||||
'data-reversed': props.reversed ? 'true' : 'false',
|
tag: "div",
|
||||||
} : {
|
"data-direction": props.direction,
|
||||||
class: 'sqadhkmv' + (props.noGap ? ' noGap' : ''),
|
"data-reversed": props.reversed ? "true" : "false",
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
class: "sqadhkmv" + (props.noGap ? " noGap" : ""),
|
||||||
},
|
},
|
||||||
{ default: renderChildren });
|
{ default: renderChildren }
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -121,7 +140,8 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
> .list-enter-active {
|
> .list-enter-active {
|
||||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1),
|
||||||
|
opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-direction="up"] {
|
&[data-direction="up"] {
|
||||||
|
|
|
@ -1,63 +1,171 @@
|
||||||
<template>
|
<template>
|
||||||
<MkModal ref="modal" :prefer-type="'dialog'" :z-priority="'high'" @click="done(true)" @closed="emit('closed')">
|
<MkModal
|
||||||
|
ref="modal"
|
||||||
|
:prefer-type="'dialog'"
|
||||||
|
:z-priority="'high'"
|
||||||
|
@click="done(true)"
|
||||||
|
@closed="emit('closed')"
|
||||||
|
>
|
||||||
<div :class="$style.root">
|
<div :class="$style.root">
|
||||||
<div v-if="icon" :class="$style.icon">
|
<div v-if="icon" :class="$style.icon">
|
||||||
<i :class="icon"></i>
|
<i :class="icon"></i>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="!input && !select" :class="[$style.icon, $style['type_' + type]]">
|
<div
|
||||||
<i v-if="type === 'success'" :class="$style.iconInner" class="ph-check ph-bold ph-lg"></i>
|
v-else-if="!input && !select"
|
||||||
<i v-else-if="type === 'error'" :class="$style.iconInner" class="ph-circle-wavy-warning ph-bold ph-lg"></i>
|
:class="[$style.icon, $style['type_' + type]]"
|
||||||
<i v-else-if="type === 'warning'" :class="$style.iconInner" class="ph-warning ph-bold ph-lg"></i>
|
>
|
||||||
<i v-else-if="type === 'info'" :class="$style.iconInner" class="ph-info ph-bold ph-lg"></i>
|
<i
|
||||||
<i v-else-if="type === 'question'" :class="$style.iconInner" class="ph-circle-question ph-bold ph-lg"></i>
|
v-if="type === 'success'"
|
||||||
<MkLoading v-else-if="type === 'waiting'" :class="$style.iconInner" :em="true"/>
|
:class="$style.iconInner"
|
||||||
|
class="ph-check ph-bold ph-lg"
|
||||||
|
></i>
|
||||||
|
<i
|
||||||
|
v-else-if="type === 'error'"
|
||||||
|
:class="$style.iconInner"
|
||||||
|
class="ph-circle-wavy-warning ph-bold ph-lg"
|
||||||
|
></i>
|
||||||
|
<i
|
||||||
|
v-else-if="type === 'warning'"
|
||||||
|
:class="$style.iconInner"
|
||||||
|
class="ph-warning ph-bold ph-lg"
|
||||||
|
></i>
|
||||||
|
<i
|
||||||
|
v-else-if="type === 'info'"
|
||||||
|
:class="$style.iconInner"
|
||||||
|
class="ph-info ph-bold ph-lg"
|
||||||
|
></i>
|
||||||
|
<i
|
||||||
|
v-else-if="type === 'question'"
|
||||||
|
:class="$style.iconInner"
|
||||||
|
class="ph-circle-question ph-bold ph-lg"
|
||||||
|
></i>
|
||||||
|
<MkLoading
|
||||||
|
v-else-if="type === 'waiting'"
|
||||||
|
:class="$style.iconInner"
|
||||||
|
:em="true"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<header v-if="title" :class="$style.title"><Mfm :text="title"/></header>
|
<header v-if="title" :class="$style.title">
|
||||||
<header v-if="title == null && (input && input.type === 'password')" :class="$style.title"><Mfm :text="i18n.ts.password"/></header>
|
<Mfm :text="title" />
|
||||||
|
</header>
|
||||||
|
<header
|
||||||
|
v-if="title == null && input && input.type === 'password'"
|
||||||
|
:class="$style.title"
|
||||||
|
>
|
||||||
|
<Mfm :text="i18n.ts.password" />
|
||||||
|
</header>
|
||||||
<div v-if="text" :class="$style.text"><Mfm :text="text" /></div>
|
<div v-if="text" :class="$style.text"><Mfm :text="text" /></div>
|
||||||
<MkInput v-if="input && input.type !== 'paragraph'" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder || undefined" @keydown="onInputKeydown">
|
<MkInput
|
||||||
<template v-if="input.type === 'password'" #prefix><i class="ph-password ph-bold ph-lg"></i></template>
|
v-if="input && input.type !== 'paragraph'"
|
||||||
|
v-model="inputValue"
|
||||||
|
autofocus
|
||||||
|
:type="input.type || 'text'"
|
||||||
|
:placeholder="input.placeholder || undefined"
|
||||||
|
@keydown="onInputKeydown"
|
||||||
|
>
|
||||||
|
<template v-if="input.type === 'password'" #prefix
|
||||||
|
><i class="ph-password ph-bold ph-lg"></i
|
||||||
|
></template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkTextarea v-if="input && input.type === 'paragraph'" v-model="inputValue" autofocus :type="paragraph" :placeholder="input.placeholder || undefined">
|
<MkTextarea
|
||||||
|
v-if="input && input.type === 'paragraph'"
|
||||||
|
v-model="inputValue"
|
||||||
|
autofocus
|
||||||
|
:type="paragraph"
|
||||||
|
:placeholder="input.placeholder || undefined"
|
||||||
|
>
|
||||||
</MkTextarea>
|
</MkTextarea>
|
||||||
<MkSelect v-if="select" v-model="selectedValue" autofocus>
|
<MkSelect v-if="select" v-model="selectedValue" autofocus>
|
||||||
<template v-if="select.items">
|
<template v-if="select.items">
|
||||||
<option v-for="item in select.items" :value="item.value">{{ item.text }}</option>
|
<option v-for="item in select.items" :value="item.value">
|
||||||
|
{{ item.text }}
|
||||||
|
</option>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<optgroup v-for="groupedItem in select.groupedItems" :label="groupedItem.label">
|
<optgroup
|
||||||
<option v-for="item in groupedItem.items" :value="item.value">{{ item.text }}</option>
|
v-for="groupedItem in select.groupedItems"
|
||||||
|
:label="groupedItem.label"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="item in groupedItem.items"
|
||||||
|
:value="item.value"
|
||||||
|
>
|
||||||
|
{{ item.text }}
|
||||||
|
</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
</template>
|
</template>
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<div v-if="(showOkButton || showCancelButton) && !actions" :class="$style.buttons">
|
<div
|
||||||
|
v-if="(showOkButton || showCancelButton) && !actions"
|
||||||
|
:class="$style.buttons"
|
||||||
|
>
|
||||||
<div v-if="!isYesNo">
|
<div v-if="!isYesNo">
|
||||||
<MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" @click="ok">{{ (showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt }}</MkButton>
|
<MkButton
|
||||||
<MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ i18n.ts.cancel }}</MkButton>
|
v-if="showOkButton"
|
||||||
|
inline
|
||||||
|
primary
|
||||||
|
:autofocus="!input && !select"
|
||||||
|
@click="ok"
|
||||||
|
>{{
|
||||||
|
showCancelButton || input || select
|
||||||
|
? i18n.ts.ok
|
||||||
|
: i18n.ts.gotIt
|
||||||
|
}}</MkButton
|
||||||
|
>
|
||||||
|
<MkButton
|
||||||
|
v-if="showCancelButton || input || select"
|
||||||
|
inline
|
||||||
|
@click="cancel"
|
||||||
|
>{{ i18n.ts.cancel }}</MkButton
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" @click="ok">{{ i18n.ts.yes }}</MkButton>
|
<MkButton
|
||||||
<MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ i18n.ts.no }}</MkButton>
|
v-if="showOkButton"
|
||||||
|
inline
|
||||||
|
primary
|
||||||
|
:autofocus="!input && !select"
|
||||||
|
@click="ok"
|
||||||
|
>{{ i18n.ts.yes }}</MkButton
|
||||||
|
>
|
||||||
|
<MkButton
|
||||||
|
v-if="showCancelButton || input || select"
|
||||||
|
inline
|
||||||
|
@click="cancel"
|
||||||
|
>{{ i18n.ts.no }}</MkButton
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="actions" :class="$style.buttons">
|
<div v-if="actions" :class="$style.buttons">
|
||||||
<MkButton v-for="action in actions" :key="action.text" inline :primary="action.primary" @click="() => { action.callback(); modal?.close(); }">{{ action.text }}</MkButton>
|
<MkButton
|
||||||
|
v-for="action in actions"
|
||||||
|
:key="action.text"
|
||||||
|
inline
|
||||||
|
:primary="action.primary"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
action.callback();
|
||||||
|
modal?.close();
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>{{ action.text }}</MkButton
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</MkModal>
|
</MkModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onBeforeUnmount, onMounted, ref, shallowRef } from 'vue';
|
import { onBeforeUnmount, onMounted, ref, shallowRef } from "vue";
|
||||||
import MkModal from '@/components/MkModal.vue';
|
import MkModal from "@/components/MkModal.vue";
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from "@/components/MkButton.vue";
|
||||||
import MkInput from '@/components/form/input.vue';
|
import MkInput from "@/components/form/input.vue";
|
||||||
import MkTextarea from '@/components/form/textarea.vue';
|
import MkTextarea from "@/components/form/textarea.vue";
|
||||||
import MkSelect from '@/components/form/select.vue';
|
import MkSelect from "@/components/form/select.vue";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
type Input = {
|
type Input = {
|
||||||
type: HTMLInputElement['type'];
|
type: HTMLInputElement["type"];
|
||||||
placeholder?: string | null;
|
placeholder?: string | null;
|
||||||
default: any | null;
|
default: any | null;
|
||||||
};
|
};
|
||||||
|
@ -77,8 +185,15 @@ type Select = {
|
||||||
default: string | null;
|
default: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(
|
||||||
type?: 'success' | 'error' | 'warning' | 'info' | 'question' | 'waiting';
|
defineProps<{
|
||||||
|
type?:
|
||||||
|
| "success"
|
||||||
|
| "error"
|
||||||
|
| "warning"
|
||||||
|
| "info"
|
||||||
|
| "question"
|
||||||
|
| "waiting";
|
||||||
title: string;
|
title: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
input?: Input;
|
input?: Input;
|
||||||
|
@ -86,7 +201,7 @@ const props = withDefaults(defineProps<{
|
||||||
icon?: string;
|
icon?: string;
|
||||||
actions?: {
|
actions?: {
|
||||||
text: string;
|
text: string;
|
||||||
primary?: boolean,
|
primary?: boolean;
|
||||||
callback: (...args: any[]) => void;
|
callback: (...args: any[]) => void;
|
||||||
}[];
|
}[];
|
||||||
showOkButton?: boolean;
|
showOkButton?: boolean;
|
||||||
|
@ -96,18 +211,20 @@ const props = withDefaults(defineProps<{
|
||||||
cancelableByBgClick?: boolean;
|
cancelableByBgClick?: boolean;
|
||||||
okText?: string;
|
okText?: string;
|
||||||
cancelText?: string;
|
cancelText?: string;
|
||||||
}>(), {
|
}>(),
|
||||||
type: 'info',
|
{
|
||||||
|
type: "info",
|
||||||
showOkButton: true,
|
showOkButton: true,
|
||||||
showCancelButton: false,
|
showCancelButton: false,
|
||||||
isYesNo: false,
|
isYesNo: false,
|
||||||
|
|
||||||
cancelableByBgClick: true,
|
cancelableByBgClick: true,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'done', v: { canceled: boolean; result: any }): void;
|
(ev: "done", v: { canceled: boolean; result: any }): void;
|
||||||
(ev: 'closed'): void;
|
(ev: "closed"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||||
|
@ -116,17 +233,18 @@ const inputValue = ref(props.input?.default || null);
|
||||||
const selectedValue = ref(props.select?.default || null);
|
const selectedValue = ref(props.select?.default || null);
|
||||||
|
|
||||||
function done(canceled: boolean, result?) {
|
function done(canceled: boolean, result?) {
|
||||||
emit('done', { canceled, result });
|
emit("done", { canceled, result });
|
||||||
modal.value?.close();
|
modal.value?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ok() {
|
async function ok() {
|
||||||
if (!props.showOkButton) return;
|
if (!props.showOkButton) return;
|
||||||
|
|
||||||
const result =
|
const result = props.input
|
||||||
props.input ? inputValue.value :
|
? inputValue.value
|
||||||
props.select ? selectedValue.value :
|
: props.select
|
||||||
true;
|
? selectedValue.value
|
||||||
|
: true;
|
||||||
done(false, result);
|
done(false, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,11 +257,11 @@ function onBgClick() {
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
function onKeydown(evt: KeyboardEvent) {
|
function onKeydown(evt: KeyboardEvent) {
|
||||||
if (evt.key === 'Escape') cancel();
|
if (evt.key === "Escape") cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onInputKeydown(evt: KeyboardEvent) {
|
function onInputKeydown(evt: KeyboardEvent) {
|
||||||
if (evt.key === 'Enter') {
|
if (evt.key === "Enter") {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
evt.stopPropagation();
|
evt.stopPropagation();
|
||||||
ok();
|
ok();
|
||||||
|
@ -151,11 +269,11 @@ function onInputKeydown(evt: KeyboardEvent) {
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
document.addEventListener('keydown', onKeydown);
|
document.addEventListener("keydown", onKeydown);
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
document.removeEventListener('keydown', onKeydown);
|
document.removeEventListener("keydown", onKeydown);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -11,23 +11,26 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onUnmounted, ref, watch } from 'vue';
|
import { onUnmounted, ref, watch } from "vue";
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
showS?: boolean;
|
showS?: boolean;
|
||||||
showMs?: boolean;
|
showMs?: boolean;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
}>(), {
|
}>(),
|
||||||
|
{
|
||||||
showS: true,
|
showS: true,
|
||||||
showMs: false,
|
showMs: false,
|
||||||
offset: 0 - new Date().getTimezoneOffset(),
|
offset: 0 - new Date().getTimezoneOffset(),
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
let intervalId;
|
let intervalId;
|
||||||
const hh = ref('');
|
const hh = ref("");
|
||||||
const mm = ref('');
|
const mm = ref("");
|
||||||
const ss = ref('');
|
const ss = ref("");
|
||||||
const ms = ref('');
|
const ms = ref("");
|
||||||
const showColon = ref(false);
|
const showColon = ref(false);
|
||||||
let prevSec: number | null = null;
|
let prevSec: number | null = null;
|
||||||
|
|
||||||
|
@ -41,21 +44,29 @@ watch(showColon, (v) => {
|
||||||
|
|
||||||
const tick = () => {
|
const tick = () => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
now.setMinutes(now.getMinutes() + (new Date().getTimezoneOffset() + props.offset));
|
now.setMinutes(
|
||||||
hh.value = now.getHours().toString().padStart(2, '0');
|
now.getMinutes() + (new Date().getTimezoneOffset() + props.offset)
|
||||||
mm.value = now.getMinutes().toString().padStart(2, '0');
|
);
|
||||||
ss.value = now.getSeconds().toString().padStart(2, '0');
|
hh.value = now.getHours().toString().padStart(2, "0");
|
||||||
ms.value = Math.floor(now.getMilliseconds() / 10).toString().padStart(2, '0');
|
mm.value = now.getMinutes().toString().padStart(2, "0");
|
||||||
|
ss.value = now.getSeconds().toString().padStart(2, "0");
|
||||||
|
ms.value = Math.floor(now.getMilliseconds() / 10)
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0");
|
||||||
if (now.getSeconds() !== prevSec) showColon.value = true;
|
if (now.getSeconds() !== prevSec) showColon.value = true;
|
||||||
prevSec = now.getSeconds();
|
prevSec = now.getSeconds();
|
||||||
};
|
};
|
||||||
|
|
||||||
tick();
|
tick();
|
||||||
|
|
||||||
watch(() => props.showMs, () => {
|
watch(
|
||||||
|
() => props.showMs,
|
||||||
|
() => {
|
||||||
if (intervalId) window.clearInterval(intervalId);
|
if (intervalId) window.clearInterval(intervalId);
|
||||||
intervalId = window.setInterval(tick, props.showMs ? 10 : 1000);
|
intervalId = window.setInterval(tick, props.showMs ? 10 : 1000);
|
||||||
}, { immediate: true });
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.clearInterval(intervalId);
|
window.clearInterval(intervalId);
|
||||||
|
|
|
@ -25,78 +25,107 @@
|
||||||
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain" />
|
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain" />
|
||||||
|
|
||||||
<p class="name">
|
<p class="name">
|
||||||
<span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span>
|
<span>{{
|
||||||
<span v-if="file.name.lastIndexOf('.') != -1" class="ext">{{ file.name.substr(file.name.lastIndexOf('.')) }}</span>
|
file.name.lastIndexOf(".") != -1
|
||||||
|
? file.name.substr(0, file.name.lastIndexOf("."))
|
||||||
|
: file.name
|
||||||
|
}}</span>
|
||||||
|
<span v-if="file.name.lastIndexOf('.') != -1" class="ext">{{
|
||||||
|
file.name.substr(file.name.lastIndexOf("."))
|
||||||
|
}}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, defineAsyncComponent, ref } from 'vue';
|
import { computed, defineAsyncComponent, ref } from "vue";
|
||||||
import * as Misskey from 'calckey-js';
|
import * as Misskey from "calckey-js";
|
||||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
import copyToClipboard from "@/scripts/copy-to-clipboard";
|
||||||
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
|
import MkDriveFileThumbnail from "@/components/MkDriveFileThumbnail.vue";
|
||||||
import bytes from '@/filters/bytes';
|
import bytes from "@/filters/bytes";
|
||||||
import * as os from '@/os';
|
import * as os from "@/os";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
import { $i } from '@/account';
|
import { $i } from "@/account";
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
file: Misskey.entities.DriveFile;
|
file: Misskey.entities.DriveFile;
|
||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
selectMode?: boolean;
|
selectMode?: boolean;
|
||||||
}>(), {
|
}>(),
|
||||||
|
{
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
selectMode: false,
|
selectMode: false,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'chosen', r: Misskey.entities.DriveFile): void;
|
(ev: "chosen", r: Misskey.entities.DriveFile): void;
|
||||||
(ev: 'dragstart'): void;
|
(ev: "dragstart"): void;
|
||||||
(ev: 'dragend'): void;
|
(ev: "dragend"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const isDragging = ref(false);
|
const isDragging = ref(false);
|
||||||
|
|
||||||
const title = computed(() => `${props.file.name}\n${props.file.type} ${bytes(props.file.size)}`);
|
const title = computed(
|
||||||
|
() => `${props.file.name}\n${props.file.type} ${bytes(props.file.size)}`
|
||||||
|
);
|
||||||
|
|
||||||
function getMenu() {
|
function getMenu() {
|
||||||
return [{
|
return [
|
||||||
|
{
|
||||||
text: i18n.ts.rename,
|
text: i18n.ts.rename,
|
||||||
icon: 'ph-cursor-text ph-bold ph-lg',
|
icon: "ph-cursor-text ph-bold ph-lg",
|
||||||
action: rename,
|
action: rename,
|
||||||
}, {
|
},
|
||||||
text: props.file.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive,
|
{
|
||||||
icon: props.file.isSensitive ? 'ph-eye ph-bold ph-lg' : 'ph-eye-slash ph-bold ph-lg',
|
text: props.file.isSensitive
|
||||||
|
? i18n.ts.unmarkAsSensitive
|
||||||
|
: i18n.ts.markAsSensitive,
|
||||||
|
icon: props.file.isSensitive
|
||||||
|
? "ph-eye ph-bold ph-lg"
|
||||||
|
: "ph-eye-slash ph-bold ph-lg",
|
||||||
action: toggleSensitive,
|
action: toggleSensitive,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
text: i18n.ts.describeFile,
|
text: i18n.ts.describeFile,
|
||||||
icon: 'ph-cursor-text ph-bold ph-lg',
|
icon: "ph-cursor-text ph-bold ph-lg",
|
||||||
action: describe,
|
action: describe,
|
||||||
}, null, {
|
},
|
||||||
|
null,
|
||||||
|
{
|
||||||
text: i18n.ts.copyUrl,
|
text: i18n.ts.copyUrl,
|
||||||
icon: 'ph-link-simple ph-bold ph-lg',
|
icon: "ph-link-simple ph-bold ph-lg",
|
||||||
action: copyUrl,
|
action: copyUrl,
|
||||||
}, {
|
},
|
||||||
type: 'a',
|
{
|
||||||
|
type: "a",
|
||||||
href: props.file.url,
|
href: props.file.url,
|
||||||
target: '_blank',
|
target: "_blank",
|
||||||
text: i18n.ts.download,
|
text: i18n.ts.download,
|
||||||
icon: 'ph-download-simple ph-bold ph-lg',
|
icon: "ph-download-simple ph-bold ph-lg",
|
||||||
download: props.file.name,
|
download: props.file.name,
|
||||||
}, null, {
|
},
|
||||||
|
null,
|
||||||
|
{
|
||||||
text: i18n.ts.delete,
|
text: i18n.ts.delete,
|
||||||
icon: 'ph-trash ph-bold ph-lg',
|
icon: "ph-trash ph-bold ph-lg",
|
||||||
danger: true,
|
danger: true,
|
||||||
action: deleteFile,
|
action: deleteFile,
|
||||||
}];
|
},
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function onClick(ev: MouseEvent) {
|
function onClick(ev: MouseEvent) {
|
||||||
if (props.selectMode) {
|
if (props.selectMode) {
|
||||||
emit('chosen', props.file);
|
emit("chosen", props.file);
|
||||||
} else {
|
} else {
|
||||||
os.popupMenu(getMenu(), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
|
os.popupMenu(
|
||||||
|
getMenu(),
|
||||||
|
(ev.currentTarget ?? ev.target ?? undefined) as
|
||||||
|
| HTMLElement
|
||||||
|
| undefined
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,17 +135,20 @@ function onContextmenu(ev: MouseEvent) {
|
||||||
|
|
||||||
function onDragstart(ev: DragEvent) {
|
function onDragstart(ev: DragEvent) {
|
||||||
if (ev.dataTransfer) {
|
if (ev.dataTransfer) {
|
||||||
ev.dataTransfer.effectAllowed = 'move';
|
ev.dataTransfer.effectAllowed = "move";
|
||||||
ev.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FILE_, JSON.stringify(props.file));
|
ev.dataTransfer.setData(
|
||||||
|
_DATA_TRANSFER_DRIVE_FILE_,
|
||||||
|
JSON.stringify(props.file)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
isDragging.value = true;
|
isDragging.value = true;
|
||||||
|
|
||||||
emit('dragstart');
|
emit("dragstart");
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDragend() {
|
function onDragend() {
|
||||||
isDragging.value = false;
|
isDragging.value = false;
|
||||||
emit('dragend');
|
emit("dragend");
|
||||||
}
|
}
|
||||||
|
|
||||||
function rename() {
|
function rename() {
|
||||||
|
@ -126,7 +158,7 @@ function rename() {
|
||||||
default: props.file.name,
|
default: props.file.name,
|
||||||
}).then(({ canceled, result: name }) => {
|
}).then(({ canceled, result: name }) => {
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
os.api('drive/files/update', {
|
os.api("drive/files/update", {
|
||||||
fileId: props.file.id,
|
fileId: props.file.id,
|
||||||
name: name,
|
name: name,
|
||||||
});
|
});
|
||||||
|
@ -134,27 +166,32 @@ function rename() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function describe() {
|
function describe() {
|
||||||
os.popup(defineAsyncComponent(() => import('@/components/MkMediaCaption.vue')), {
|
os.popup(
|
||||||
|
defineAsyncComponent(() => import("@/components/MkMediaCaption.vue")),
|
||||||
|
{
|
||||||
title: i18n.ts.describeFile,
|
title: i18n.ts.describeFile,
|
||||||
input: {
|
input: {
|
||||||
placeholder: i18n.ts.inputNewDescription,
|
placeholder: i18n.ts.inputNewDescription,
|
||||||
default: props.file.comment != null ? props.file.comment : '',
|
default: props.file.comment != null ? props.file.comment : "",
|
||||||
},
|
},
|
||||||
image: props.file,
|
image: props.file,
|
||||||
}, {
|
},
|
||||||
done: result => {
|
{
|
||||||
|
done: (result) => {
|
||||||
if (!result || result.canceled) return;
|
if (!result || result.canceled) return;
|
||||||
let comment = result.result;
|
let comment = result.result;
|
||||||
os.api('drive/files/update', {
|
os.api("drive/files/update", {
|
||||||
fileId: props.file.id,
|
fileId: props.file.id,
|
||||||
comment: comment.length === 0 ? null : comment,
|
comment: comment.length === 0 ? null : comment,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}, 'closed');
|
},
|
||||||
|
"closed"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleSensitive() {
|
function toggleSensitive() {
|
||||||
os.api('drive/files/update', {
|
os.api("drive/files/update", {
|
||||||
fileId: props.file.id,
|
fileId: props.file.id,
|
||||||
isSensitive: !props.file.isSensitive,
|
isSensitive: !props.file.isSensitive,
|
||||||
});
|
});
|
||||||
|
@ -171,12 +208,12 @@ function addApp() {
|
||||||
*/
|
*/
|
||||||
async function deleteFile() {
|
async function deleteFile() {
|
||||||
const { canceled } = await os.confirm({
|
const { canceled } = await os.confirm({
|
||||||
type: 'warning',
|
type: "warning",
|
||||||
text: i18n.t('driveFileDeleteConfirm', { name: props.file.name }),
|
text: i18n.t("driveFileDeleteConfirm", { name: props.file.name }),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
os.api('drive/files/delete', {
|
os.api("drive/files/delete", {
|
||||||
fileId: props.file.id,
|
fileId: props.file.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -189,7 +226,8 @@ async function deleteFile() {
|
||||||
min-height: 180px;
|
min-height: 180px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
||||||
&, * {
|
&,
|
||||||
|
* {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,41 +16,53 @@
|
||||||
@dragend="onDragend"
|
@dragend="onDragend"
|
||||||
>
|
>
|
||||||
<p class="name">
|
<p class="name">
|
||||||
<template v-if="hover"><i class="ph-folder-notch-open ph-bold ph-lg ph-fw ph-lg"></i></template>
|
<template v-if="hover"
|
||||||
<template v-if="!hover"><i class="ph-folder-notch ph-bold ph-lg ph-fw ph-lg"></i></template>
|
><i class="ph-folder-notch-open ph-bold ph-lg ph-fw ph-lg"></i
|
||||||
|
></template>
|
||||||
|
<template v-if="!hover"
|
||||||
|
><i class="ph-folder-notch ph-bold ph-lg ph-fw ph-lg"></i
|
||||||
|
></template>
|
||||||
{{ folder.name }}
|
{{ folder.name }}
|
||||||
</p>
|
</p>
|
||||||
<p v-if="defaultStore.state.uploadFolder == folder.id" class="upload">
|
<p v-if="defaultStore.state.uploadFolder == folder.id" class="upload">
|
||||||
{{ i18n.ts.uploadFolder }}
|
{{ i18n.ts.uploadFolder }}
|
||||||
</p>
|
</p>
|
||||||
<button v-if="selectMode" class="checkbox _button" :class="{ checked: isSelected }" @click.prevent.stop="checkboxClicked"></button>
|
<button
|
||||||
|
v-if="selectMode"
|
||||||
|
class="checkbox _button"
|
||||||
|
:class="{ checked: isSelected }"
|
||||||
|
@click.prevent.stop="checkboxClicked"
|
||||||
|
></button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, defineAsyncComponent, ref } from 'vue';
|
import { computed, defineAsyncComponent, ref } from "vue";
|
||||||
import * as Misskey from 'calckey-js';
|
import * as Misskey from "calckey-js";
|
||||||
import * as os from '@/os';
|
import * as os from "@/os";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from "@/store";
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
folder: Misskey.entities.DriveFolder;
|
folder: Misskey.entities.DriveFolder;
|
||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
selectMode?: boolean;
|
selectMode?: boolean;
|
||||||
}>(), {
|
}>(),
|
||||||
|
{
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
selectMode: false,
|
selectMode: false,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'chosen', v: Misskey.entities.DriveFolder): void;
|
(ev: "chosen", v: Misskey.entities.DriveFolder): void;
|
||||||
(ev: 'move', v: Misskey.entities.DriveFolder): void;
|
(ev: "move", v: Misskey.entities.DriveFolder): void;
|
||||||
(ev: 'upload', file: File, folder: Misskey.entities.DriveFolder);
|
(ev: "upload", file: File, folder: Misskey.entities.DriveFolder);
|
||||||
(ev: 'removeFile', v: Misskey.entities.DriveFile['id']): void;
|
(ev: "removeFile", v: Misskey.entities.DriveFile["id"]): void;
|
||||||
(ev: 'removeFolder', v: Misskey.entities.DriveFolder['id']): void;
|
(ev: "removeFolder", v: Misskey.entities.DriveFolder["id"]): void;
|
||||||
(ev: 'dragstart'): void;
|
(ev: "dragstart"): void;
|
||||||
(ev: 'dragend'): void;
|
(ev: "dragend"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const hover = ref(false);
|
const hover = ref(false);
|
||||||
|
@ -60,11 +72,11 @@ const isDragging = ref(false);
|
||||||
const title = computed(() => props.folder.name);
|
const title = computed(() => props.folder.name);
|
||||||
|
|
||||||
function checkboxClicked() {
|
function checkboxClicked() {
|
||||||
emit('chosen', props.folder);
|
emit("chosen", props.folder);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onClick() {
|
function onClick() {
|
||||||
emit('move', props.folder);
|
emit("move", props.folder);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMouseover() {
|
function onMouseover() {
|
||||||
|
@ -81,18 +93,20 @@ function onDragover(ev: DragEvent) {
|
||||||
// 自分自身がドラッグされている場合
|
// 自分自身がドラッグされている場合
|
||||||
if (isDragging.value) {
|
if (isDragging.value) {
|
||||||
// 自分自身にはドロップさせない
|
// 自分自身にはドロップさせない
|
||||||
ev.dataTransfer.dropEffect = 'none';
|
ev.dataTransfer.dropEffect = "none";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFile = ev.dataTransfer.items[0].kind === 'file';
|
const isFile = ev.dataTransfer.items[0].kind === "file";
|
||||||
const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_;
|
const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_;
|
||||||
const isDriveFolder = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FOLDER_;
|
const isDriveFolder =
|
||||||
|
ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FOLDER_;
|
||||||
|
|
||||||
if (isFile || isDriveFile || isDriveFolder) {
|
if (isFile || isDriveFile || isDriveFolder) {
|
||||||
ev.dataTransfer.dropEffect = ev.dataTransfer.effectAllowed === 'all' ? 'copy' : 'move';
|
ev.dataTransfer.dropEffect =
|
||||||
|
ev.dataTransfer.effectAllowed === "all" ? "copy" : "move";
|
||||||
} else {
|
} else {
|
||||||
ev.dataTransfer.dropEffect = 'none';
|
ev.dataTransfer.dropEffect = "none";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,17 +126,17 @@ function onDrop(ev: DragEvent) {
|
||||||
// ファイルだったら
|
// ファイルだったら
|
||||||
if (ev.dataTransfer.files.length > 0) {
|
if (ev.dataTransfer.files.length > 0) {
|
||||||
for (const file of Array.from(ev.dataTransfer.files)) {
|
for (const file of Array.from(ev.dataTransfer.files)) {
|
||||||
emit('upload', file, props.folder);
|
emit("upload", file, props.folder);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region ドライブのファイル
|
//#region ドライブのファイル
|
||||||
const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
|
const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
|
||||||
if (driveFile != null && driveFile !== '') {
|
if (driveFile != null && driveFile !== "") {
|
||||||
const file = JSON.parse(driveFile);
|
const file = JSON.parse(driveFile);
|
||||||
emit('removeFile', file.id);
|
emit("removeFile", file.id);
|
||||||
os.api('drive/files/update', {
|
os.api("drive/files/update", {
|
||||||
fileId: file.id,
|
fileId: file.id,
|
||||||
folderId: props.folder.id,
|
folderId: props.folder.id,
|
||||||
});
|
});
|
||||||
|
@ -131,21 +145,23 @@ function onDrop(ev: DragEvent) {
|
||||||
|
|
||||||
//#region ドライブのフォルダ
|
//#region ドライブのフォルダ
|
||||||
const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
|
const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
|
||||||
if (driveFolder != null && driveFolder !== '') {
|
if (driveFolder != null && driveFolder !== "") {
|
||||||
const folder = JSON.parse(driveFolder);
|
const folder = JSON.parse(driveFolder);
|
||||||
|
|
||||||
// 移動先が自分自身ならreject
|
// 移動先が自分自身ならreject
|
||||||
if (folder.id === props.folder.id) return;
|
if (folder.id === props.folder.id) return;
|
||||||
|
|
||||||
emit('removeFolder', folder.id);
|
emit("removeFolder", folder.id);
|
||||||
os.api('drive/folders/update', {
|
os.api("drive/folders/update", {
|
||||||
folderId: folder.id,
|
folderId: folder.id,
|
||||||
parentId: props.folder.id,
|
parentId: props.folder.id,
|
||||||
}).then(() => {
|
})
|
||||||
|
.then(() => {
|
||||||
// noop
|
// noop
|
||||||
}).catch(err => {
|
})
|
||||||
|
.catch((err) => {
|
||||||
switch (err) {
|
switch (err) {
|
||||||
case 'detected-circular-definition':
|
case "detected-circular-definition":
|
||||||
os.alert({
|
os.alert({
|
||||||
title: i18n.ts.unableToProcess,
|
title: i18n.ts.unableToProcess,
|
||||||
text: i18n.ts.circularReferenceFolder,
|
text: i18n.ts.circularReferenceFolder,
|
||||||
|
@ -153,7 +169,7 @@ function onDrop(ev: DragEvent) {
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
os.alert({
|
os.alert({
|
||||||
type: 'error',
|
type: "error",
|
||||||
text: i18n.ts.somethingHappened,
|
text: i18n.ts.somethingHappened,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -165,22 +181,25 @@ function onDrop(ev: DragEvent) {
|
||||||
function onDragstart(ev: DragEvent) {
|
function onDragstart(ev: DragEvent) {
|
||||||
if (!ev.dataTransfer) return;
|
if (!ev.dataTransfer) return;
|
||||||
|
|
||||||
ev.dataTransfer.effectAllowed = 'move';
|
ev.dataTransfer.effectAllowed = "move";
|
||||||
ev.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FOLDER_, JSON.stringify(props.folder));
|
ev.dataTransfer.setData(
|
||||||
|
_DATA_TRANSFER_DRIVE_FOLDER_,
|
||||||
|
JSON.stringify(props.folder)
|
||||||
|
);
|
||||||
isDragging.value = true;
|
isDragging.value = true;
|
||||||
|
|
||||||
// 親ブラウザに対して、ドラッグが開始されたフラグを立てる
|
// 親ブラウザに対して、ドラッグが開始されたフラグを立てる
|
||||||
// (=あなたの子供が、ドラッグを開始しましたよ)
|
// (=あなたの子供が、ドラッグを開始しましたよ)
|
||||||
emit('dragstart');
|
emit("dragstart");
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDragend() {
|
function onDragend() {
|
||||||
isDragging.value = false;
|
isDragging.value = false;
|
||||||
emit('dragend');
|
emit("dragend");
|
||||||
}
|
}
|
||||||
|
|
||||||
function go() {
|
function go() {
|
||||||
emit('move', props.folder.id);
|
emit("move", props.folder.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function rename() {
|
function rename() {
|
||||||
|
@ -190,7 +209,7 @@ function rename() {
|
||||||
default: props.folder.name,
|
default: props.folder.name,
|
||||||
}).then(({ canceled, result: name }) => {
|
}).then(({ canceled, result: name }) => {
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
os.api('drive/folders/update', {
|
os.api("drive/folders/update", {
|
||||||
folderId: props.folder.id,
|
folderId: props.folder.id,
|
||||||
name: name,
|
name: name,
|
||||||
});
|
});
|
||||||
|
@ -198,24 +217,26 @@ function rename() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteFolder() {
|
function deleteFolder() {
|
||||||
os.api('drive/folders/delete', {
|
os.api("drive/folders/delete", {
|
||||||
folderId: props.folder.id,
|
folderId: props.folder.id,
|
||||||
}).then(() => {
|
})
|
||||||
|
.then(() => {
|
||||||
if (defaultStore.state.uploadFolder === props.folder.id) {
|
if (defaultStore.state.uploadFolder === props.folder.id) {
|
||||||
defaultStore.set('uploadFolder', null);
|
defaultStore.set("uploadFolder", null);
|
||||||
}
|
}
|
||||||
}).catch(err => {
|
})
|
||||||
|
.catch((err) => {
|
||||||
switch (err.id) {
|
switch (err.id) {
|
||||||
case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
|
case "b0fc8a17-963c-405d-bfbc-859a487295e1":
|
||||||
os.alert({
|
os.alert({
|
||||||
type: 'error',
|
type: "error",
|
||||||
title: i18n.ts.unableToDelete,
|
title: i18n.ts.unableToDelete,
|
||||||
text: i18n.ts.hasChildFilesOrFolders,
|
text: i18n.ts.hasChildFilesOrFolders,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
os.alert({
|
os.alert({
|
||||||
type: 'error',
|
type: "error",
|
||||||
text: i18n.ts.unableToDelete,
|
text: i18n.ts.unableToDelete,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -223,29 +244,44 @@ function deleteFolder() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setAsUploadFolder() {
|
function setAsUploadFolder() {
|
||||||
defaultStore.set('uploadFolder', props.folder.id);
|
defaultStore.set("uploadFolder", props.folder.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onContextmenu(ev: MouseEvent) {
|
function onContextmenu(ev: MouseEvent) {
|
||||||
os.contextMenu([{
|
os.contextMenu(
|
||||||
|
[
|
||||||
|
{
|
||||||
text: i18n.ts.openInWindow,
|
text: i18n.ts.openInWindow,
|
||||||
icon: 'ph-copy ph-bold ph-lg',
|
icon: "ph-copy ph-bold ph-lg",
|
||||||
action: () => {
|
action: () => {
|
||||||
os.popup(defineAsyncComponent(() => import('@/components/MkDriveWindow.vue')), {
|
os.popup(
|
||||||
|
defineAsyncComponent(
|
||||||
|
() => import("@/components/MkDriveWindow.vue")
|
||||||
|
),
|
||||||
|
{
|
||||||
initialFolder: props.folder,
|
initialFolder: props.folder,
|
||||||
}, {
|
|
||||||
}, 'closed');
|
|
||||||
},
|
},
|
||||||
}, null, {
|
{},
|
||||||
|
"closed"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
{
|
||||||
text: i18n.ts.rename,
|
text: i18n.ts.rename,
|
||||||
icon: 'ph-cursor-text ph-bold ph-lg',
|
icon: "ph-cursor-text ph-bold ph-lg",
|
||||||
action: rename,
|
action: rename,
|
||||||
}, null, {
|
},
|
||||||
|
null,
|
||||||
|
{
|
||||||
text: i18n.ts.delete,
|
text: i18n.ts.delete,
|
||||||
icon: 'ph-trash ph-bold ph-lg',
|
icon: "ph-trash ph-bold ph-lg",
|
||||||
danger: true,
|
danger: true,
|
||||||
action: deleteFolder,
|
action: deleteFolder,
|
||||||
}], ev);
|
},
|
||||||
|
],
|
||||||
|
ev
|
||||||
|
);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -257,7 +293,8 @@ function onContextmenu(ev: MouseEvent) {
|
||||||
background: var(--driveFolderBg);
|
background: var(--driveFolderBg);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
&, * {
|
&,
|
||||||
|
* {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="drylbebk"
|
<div
|
||||||
|
class="drylbebk"
|
||||||
:class="{ draghover }"
|
:class="{ draghover }"
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
@dragover.prevent.stop="onDragover"
|
@dragover.prevent.stop="onDragover"
|
||||||
|
@ -13,10 +14,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from "vue";
|
||||||
import * as Misskey from 'calckey-js';
|
import * as Misskey from "calckey-js";
|
||||||
import * as os from '@/os';
|
import * as os from "@/os";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
folder?: Misskey.entities.DriveFolder;
|
folder?: Misskey.entities.DriveFolder;
|
||||||
|
@ -24,17 +25,21 @@ const props = defineProps<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'move', v?: Misskey.entities.DriveFolder): void;
|
(ev: "move", v?: Misskey.entities.DriveFolder): void;
|
||||||
(ev: 'upload', file: File, folder?: Misskey.entities.DriveFolder | null): void;
|
(
|
||||||
(ev: 'removeFile', v: Misskey.entities.DriveFile['id']): void;
|
ev: "upload",
|
||||||
(ev: 'removeFolder', v: Misskey.entities.DriveFolder['id']): void;
|
file: File,
|
||||||
|
folder?: Misskey.entities.DriveFolder | null
|
||||||
|
): void;
|
||||||
|
(ev: "removeFile", v: Misskey.entities.DriveFile["id"]): void;
|
||||||
|
(ev: "removeFolder", v: Misskey.entities.DriveFolder["id"]): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const hover = ref(false);
|
const hover = ref(false);
|
||||||
const draghover = ref(false);
|
const draghover = ref(false);
|
||||||
|
|
||||||
function onClick() {
|
function onClick() {
|
||||||
emit('move', props.folder);
|
emit("move", props.folder);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMouseover() {
|
function onMouseover() {
|
||||||
|
@ -50,17 +55,19 @@ function onDragover(ev: DragEvent) {
|
||||||
|
|
||||||
// このフォルダがルートかつカレントディレクトリならドロップ禁止
|
// このフォルダがルートかつカレントディレクトリならドロップ禁止
|
||||||
if (props.folder == null && props.parentFolder == null) {
|
if (props.folder == null && props.parentFolder == null) {
|
||||||
ev.dataTransfer.dropEffect = 'none';
|
ev.dataTransfer.dropEffect = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFile = ev.dataTransfer.items[0].kind === 'file';
|
const isFile = ev.dataTransfer.items[0].kind === "file";
|
||||||
const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_;
|
const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_;
|
||||||
const isDriveFolder = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FOLDER_;
|
const isDriveFolder =
|
||||||
|
ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FOLDER_;
|
||||||
|
|
||||||
if (isFile || isDriveFile || isDriveFolder) {
|
if (isFile || isDriveFile || isDriveFolder) {
|
||||||
ev.dataTransfer.dropEffect = ev.dataTransfer.effectAllowed === 'all' ? 'copy' : 'move';
|
ev.dataTransfer.dropEffect =
|
||||||
|
ev.dataTransfer.effectAllowed === "all" ? "copy" : "move";
|
||||||
} else {
|
} else {
|
||||||
ev.dataTransfer.dropEffect = 'none';
|
ev.dataTransfer.dropEffect = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
@ -82,33 +89,33 @@ function onDrop(ev: DragEvent) {
|
||||||
// ファイルだったら
|
// ファイルだったら
|
||||||
if (ev.dataTransfer.files.length > 0) {
|
if (ev.dataTransfer.files.length > 0) {
|
||||||
for (const file of Array.from(ev.dataTransfer.files)) {
|
for (const file of Array.from(ev.dataTransfer.files)) {
|
||||||
emit('upload', file, props.folder);
|
emit("upload", file, props.folder);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region ドライブのファイル
|
//#region ドライブのファイル
|
||||||
const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
|
const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
|
||||||
if (driveFile != null && driveFile !== '') {
|
if (driveFile != null && driveFile !== "") {
|
||||||
const file = JSON.parse(driveFile);
|
const file = JSON.parse(driveFile);
|
||||||
emit('removeFile', file.id);
|
emit("removeFile", file.id);
|
||||||
os.api('drive/files/update', {
|
os.api("drive/files/update", {
|
||||||
fileId: file.id,
|
fileId: file.id,
|
||||||
folderId: props.folder ? props.folder.id : null
|
folderId: props.folder ? props.folder.id : null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region ドライブのフォルダ
|
//#region ドライブのフォルダ
|
||||||
const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
|
const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
|
||||||
if (driveFolder != null && driveFolder !== '') {
|
if (driveFolder != null && driveFolder !== "") {
|
||||||
const folder = JSON.parse(driveFolder);
|
const folder = JSON.parse(driveFolder);
|
||||||
// 移動先が自分自身ならreject
|
// 移動先が自分自身ならreject
|
||||||
if (props.folder && folder.id === props.folder.id) return;
|
if (props.folder && folder.id === props.folder.id) return;
|
||||||
emit('removeFolder', folder.id);
|
emit("removeFolder", folder.id);
|
||||||
os.api('drive/folders/update', {
|
os.api("drive/folders/update", {
|
||||||
folderId: folder.id,
|
folderId: folder.id,
|
||||||
parentId: props.folder ? props.folder.id : null
|
parentId: props.folder ? props.folder.id : null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
|
@ -11,7 +11,9 @@
|
||||||
@removeFolder="removeFolder"
|
@removeFolder="removeFolder"
|
||||||
/>
|
/>
|
||||||
<template v-for="f in hierarchyFolders">
|
<template v-for="f in hierarchyFolders">
|
||||||
<span class="separator"><i class="ph-caret-right ph-bold ph-lg"></i></span>
|
<span class="separator"
|
||||||
|
><i class="ph-caret-right ph-bold ph-lg"></i
|
||||||
|
></span>
|
||||||
<XNavFolder
|
<XNavFolder
|
||||||
:folder="f"
|
:folder="f"
|
||||||
:parent-folder="folder"
|
:parent-folder="folder"
|
||||||
|
@ -21,13 +23,20 @@
|
||||||
@removeFolder="removeFolder"
|
@removeFolder="removeFolder"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<span v-if="folder != null" class="separator"><i class="ph-caret-right ph-bold ph-lg"></i></span>
|
<span v-if="folder != null" class="separator"
|
||||||
<span v-if="folder != null" class="folder current">{{ folder.name }}</span>
|
><i class="ph-caret-right ph-bold ph-lg"></i
|
||||||
|
></span>
|
||||||
|
<span v-if="folder != null" class="folder current">{{
|
||||||
|
folder.name
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="menu _button" @click="showMenu"><i class="ph-dots-three-outline ph-bold ph-lg"></i></button>
|
<button class="menu _button" @click="showMenu">
|
||||||
|
<i class="ph-dots-three-outline ph-bold ph-lg"></i>
|
||||||
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
<div
|
<div
|
||||||
ref="main" class="main"
|
ref="main"
|
||||||
|
class="main"
|
||||||
:class="{ uploading: uploadings.length > 0, fetching }"
|
:class="{ uploading: uploadings.length > 0, fetching }"
|
||||||
@dragover.prevent.stop="onDragover"
|
@dragover.prevent.stop="onDragover"
|
||||||
@dragenter="onDragenter"
|
@dragenter="onDragenter"
|
||||||
|
@ -36,7 +45,11 @@
|
||||||
@contextmenu.stop="onContextmenu"
|
@contextmenu.stop="onContextmenu"
|
||||||
>
|
>
|
||||||
<div ref="contents" class="contents">
|
<div ref="contents" class="contents">
|
||||||
<div v-show="folders.length > 0" ref="foldersContainer" class="folders">
|
<div
|
||||||
|
v-show="folders.length > 0"
|
||||||
|
ref="foldersContainer"
|
||||||
|
class="folders"
|
||||||
|
>
|
||||||
<XFolder
|
<XFolder
|
||||||
v-for="(f, i) in folders"
|
v-for="(f, i) in folders"
|
||||||
:key="f.id"
|
:key="f.id"
|
||||||
|
@ -44,7 +57,9 @@
|
||||||
class="folder"
|
class="folder"
|
||||||
:folder="f"
|
:folder="f"
|
||||||
:select-mode="select === 'folder'"
|
:select-mode="select === 'folder'"
|
||||||
:is-selected="selectedFolders.some(x => x.id === f.id)"
|
:is-selected="
|
||||||
|
selectedFolders.some((x) => x.id === f.id)
|
||||||
|
"
|
||||||
@chosen="chooseFolder"
|
@chosen="chooseFolder"
|
||||||
@move="move"
|
@move="move"
|
||||||
@upload="upload"
|
@upload="upload"
|
||||||
|
@ -55,9 +70,15 @@
|
||||||
/>
|
/>
|
||||||
<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
|
<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
|
||||||
<div v-for="(n, i) in 16" :key="i" class="padding"></div>
|
<div v-for="(n, i) in 16" :key="i" class="padding"></div>
|
||||||
<MkButton v-if="moreFolders" ref="moreFolders">{{ i18n.ts.loadMore }}</MkButton>
|
<MkButton v-if="moreFolders" ref="moreFolders">{{
|
||||||
|
i18n.ts.loadMore
|
||||||
|
}}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="files.length > 0" ref="filesContainer" class="files">
|
<div
|
||||||
|
v-show="files.length > 0"
|
||||||
|
ref="filesContainer"
|
||||||
|
class="files"
|
||||||
|
>
|
||||||
<XFile
|
<XFile
|
||||||
v-for="(file, i) in files"
|
v-for="(file, i) in files"
|
||||||
:key="file.id"
|
:key="file.id"
|
||||||
|
@ -65,57 +86,96 @@
|
||||||
class="file"
|
class="file"
|
||||||
:file="file"
|
:file="file"
|
||||||
:select-mode="select === 'file'"
|
:select-mode="select === 'file'"
|
||||||
:is-selected="selectedFiles.some(x => x.id === file.id)"
|
:is-selected="
|
||||||
|
selectedFiles.some((x) => x.id === file.id)
|
||||||
|
"
|
||||||
@chosen="chooseFile"
|
@chosen="chooseFile"
|
||||||
@dragstart="isDragSource = true"
|
@dragstart="isDragSource = true"
|
||||||
@dragend="isDragSource = false"
|
@dragend="isDragSource = false"
|
||||||
/>
|
/>
|
||||||
<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
|
<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
|
||||||
<div v-for="(n, i) in 16" :key="i" class="padding"></div>
|
<div v-for="(n, i) in 16" :key="i" class="padding"></div>
|
||||||
<MkButton v-show="moreFiles" ref="loadMoreFiles" @click="fetchMoreFiles">{{ i18n.ts.loadMore }}</MkButton>
|
<MkButton
|
||||||
|
v-show="moreFiles"
|
||||||
|
ref="loadMoreFiles"
|
||||||
|
@click="fetchMoreFiles"
|
||||||
|
>{{ i18n.ts.loadMore }}</MkButton
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="files.length == 0 && folders.length == 0 && !fetching" class="empty">
|
<div
|
||||||
<p v-if="draghover">{{ i18n.t('empty-draghover') }}</p>
|
v-if="files.length == 0 && folders.length == 0 && !fetching"
|
||||||
<p v-if="!draghover && folder == null"><strong>{{ i18n.ts.emptyDrive }}</strong><br/>{{ i18n.t('empty-drive-description') }}</p>
|
class="empty"
|
||||||
<p v-if="!draghover && folder != null">{{ i18n.ts.emptyFolder }}</p>
|
>
|
||||||
|
<p v-if="draghover">{{ i18n.t("empty-draghover") }}</p>
|
||||||
|
<p v-if="!draghover && folder == null">
|
||||||
|
<strong>{{ i18n.ts.emptyDrive }}</strong
|
||||||
|
><br />{{ i18n.t("empty-drive-description") }}
|
||||||
|
</p>
|
||||||
|
<p v-if="!draghover && folder != null">
|
||||||
|
{{ i18n.ts.emptyFolder }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<MkLoading v-if="fetching" />
|
<MkLoading v-if="fetching" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="draghover" class="dropzone"></div>
|
<div v-if="draghover" class="dropzone"></div>
|
||||||
<input ref="fileInput" type="file" accept="*/*" multiple tabindex="-1" @change="onChangeFileInput"/>
|
<input
|
||||||
|
ref="fileInput"
|
||||||
|
type="file"
|
||||||
|
accept="*/*"
|
||||||
|
multiple
|
||||||
|
tabindex="-1"
|
||||||
|
@change="onChangeFileInput"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { markRaw, nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
import {
|
||||||
import * as Misskey from 'calckey-js';
|
markRaw,
|
||||||
import MkButton from './MkButton.vue';
|
nextTick,
|
||||||
import XNavFolder from '@/components/MkDrive.navFolder.vue';
|
onActivated,
|
||||||
import XFolder from '@/components/MkDrive.folder.vue';
|
onBeforeUnmount,
|
||||||
import XFile from '@/components/MkDrive.file.vue';
|
onMounted,
|
||||||
import * as os from '@/os';
|
ref,
|
||||||
import { stream } from '@/stream';
|
watch,
|
||||||
import { defaultStore } from '@/store';
|
} from "vue";
|
||||||
import { i18n } from '@/i18n';
|
import * as Misskey from "calckey-js";
|
||||||
import { uploadFile, uploads } from '@/scripts/upload';
|
import MkButton from "./MkButton.vue";
|
||||||
|
import XNavFolder from "@/components/MkDrive.navFolder.vue";
|
||||||
|
import XFolder from "@/components/MkDrive.folder.vue";
|
||||||
|
import XFile from "@/components/MkDrive.file.vue";
|
||||||
|
import * as os from "@/os";
|
||||||
|
import { stream } from "@/stream";
|
||||||
|
import { defaultStore } from "@/store";
|
||||||
|
import { i18n } from "@/i18n";
|
||||||
|
import { uploadFile, uploads } from "@/scripts/upload";
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
initialFolder?: Misskey.entities.DriveFolder;
|
initialFolder?: Misskey.entities.DriveFolder;
|
||||||
type?: string;
|
type?: string;
|
||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
select?: 'file' | 'folder' | null;
|
select?: "file" | "folder" | null;
|
||||||
}>(), {
|
}>(),
|
||||||
|
{
|
||||||
multiple: false,
|
multiple: false,
|
||||||
select: null,
|
select: null,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'selected', v: Misskey.entities.DriveFile | Misskey.entities.DriveFolder): void;
|
(
|
||||||
(ev: 'change-selection', v: Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]): void;
|
ev: "selected",
|
||||||
(ev: 'move-root'): void;
|
v: Misskey.entities.DriveFile | Misskey.entities.DriveFolder
|
||||||
(ev: 'cd', v: Misskey.entities.DriveFolder | null): void;
|
): void;
|
||||||
(ev: 'open-folder', v: Misskey.entities.DriveFolder): void;
|
(
|
||||||
|
ev: "change-selection",
|
||||||
|
v: Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]
|
||||||
|
): void;
|
||||||
|
(ev: "move-root"): void;
|
||||||
|
(ev: "cd", v: Misskey.entities.DriveFolder | null): void;
|
||||||
|
(ev: "open-folder", v: Misskey.entities.DriveFolder): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const loadMoreFiles = ref<InstanceType<typeof MkButton>>();
|
const loadMoreFiles = ref<InstanceType<typeof MkButton>>();
|
||||||
|
@ -130,7 +190,7 @@ const hierarchyFolders = ref<Misskey.entities.DriveFolder[]>([]);
|
||||||
const selectedFiles = ref<Misskey.entities.DriveFile[]>([]);
|
const selectedFiles = ref<Misskey.entities.DriveFile[]>([]);
|
||||||
const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]);
|
const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]);
|
||||||
const uploadings = uploads;
|
const uploadings = uploads;
|
||||||
const connection = stream.useChannel('drive');
|
const connection = stream.useChannel("drive");
|
||||||
const keepOriginal = ref<boolean>(defaultStore.state.keepOriginalUploading); // 外部渡しが多いので$refは使わないほうがよい
|
const keepOriginal = ref<boolean>(defaultStore.state.keepOriginalUploading); // 外部渡しが多いので$refは使わないほうがよい
|
||||||
|
|
||||||
// ドロップされようとしているか
|
// ドロップされようとしているか
|
||||||
|
@ -143,10 +203,14 @@ const isDragSource = ref(false);
|
||||||
const fetching = ref(true);
|
const fetching = ref(true);
|
||||||
|
|
||||||
const ilFilesObserver = new IntersectionObserver(
|
const ilFilesObserver = new IntersectionObserver(
|
||||||
(entries) => entries.some((entry) => entry.isIntersecting) && !fetching.value && moreFiles.value && fetchMoreFiles(),
|
(entries) =>
|
||||||
|
entries.some((entry) => entry.isIntersecting) &&
|
||||||
|
!fetching.value &&
|
||||||
|
moreFiles.value &&
|
||||||
|
fetchMoreFiles()
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(folder, () => emit('cd', folder.value));
|
watch(folder, () => emit("cd", folder.value));
|
||||||
|
|
||||||
function onStreamDriveFileCreated(file: Misskey.entities.DriveFile) {
|
function onStreamDriveFileCreated(file: Misskey.entities.DriveFile) {
|
||||||
addFile(file, true);
|
addFile(file, true);
|
||||||
|
@ -165,11 +229,15 @@ function onStreamDriveFileDeleted(fileId: string) {
|
||||||
removeFile(fileId);
|
removeFile(fileId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onStreamDriveFolderCreated(createdFolder: Misskey.entities.DriveFolder) {
|
function onStreamDriveFolderCreated(
|
||||||
|
createdFolder: Misskey.entities.DriveFolder
|
||||||
|
) {
|
||||||
addFolder(createdFolder, true);
|
addFolder(createdFolder, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onStreamDriveFolderUpdated(updatedFolder: Misskey.entities.DriveFolder) {
|
function onStreamDriveFolderUpdated(
|
||||||
|
updatedFolder: Misskey.entities.DriveFolder
|
||||||
|
) {
|
||||||
const current = folder.value ? folder.value.id : null;
|
const current = folder.value ? folder.value.id : null;
|
||||||
if (current !== updatedFolder.parentId) {
|
if (current !== updatedFolder.parentId) {
|
||||||
removeFolder(updatedFolder);
|
removeFolder(updatedFolder);
|
||||||
|
@ -188,17 +256,19 @@ function onDragover(ev: DragEvent): any {
|
||||||
// ドラッグ元が自分自身の所有するアイテムだったら
|
// ドラッグ元が自分自身の所有するアイテムだったら
|
||||||
if (isDragSource.value) {
|
if (isDragSource.value) {
|
||||||
// 自分自身にはドロップさせない
|
// 自分自身にはドロップさせない
|
||||||
ev.dataTransfer.dropEffect = 'none';
|
ev.dataTransfer.dropEffect = "none";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFile = ev.dataTransfer.items[0].kind === 'file';
|
const isFile = ev.dataTransfer.items[0].kind === "file";
|
||||||
const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_;
|
const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_;
|
||||||
const isDriveFolder = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FOLDER_;
|
const isDriveFolder =
|
||||||
|
ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FOLDER_;
|
||||||
if (isFile || isDriveFile || isDriveFolder) {
|
if (isFile || isDriveFile || isDriveFolder) {
|
||||||
ev.dataTransfer.dropEffect = ev.dataTransfer.effectAllowed === 'all' ? 'copy' : 'move';
|
ev.dataTransfer.dropEffect =
|
||||||
|
ev.dataTransfer.effectAllowed === "all" ? "copy" : "move";
|
||||||
} else {
|
} else {
|
||||||
ev.dataTransfer.dropEffect = 'none';
|
ev.dataTransfer.dropEffect = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
@ -227,11 +297,11 @@ function onDrop(ev: DragEvent): any {
|
||||||
|
|
||||||
//#region ドライブのファイル
|
//#region ドライブのファイル
|
||||||
const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
|
const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
|
||||||
if (driveFile != null && driveFile !== '') {
|
if (driveFile != null && driveFile !== "") {
|
||||||
const file = JSON.parse(driveFile);
|
const file = JSON.parse(driveFile);
|
||||||
if (files.value.some(f => f.id === file.id)) return;
|
if (files.value.some((f) => f.id === file.id)) return;
|
||||||
removeFile(file.id);
|
removeFile(file.id);
|
||||||
os.api('drive/files/update', {
|
os.api("drive/files/update", {
|
||||||
fileId: file.id,
|
fileId: file.id,
|
||||||
folderId: folder.value ? folder.value.id : null,
|
folderId: folder.value ? folder.value.id : null,
|
||||||
});
|
});
|
||||||
|
@ -240,21 +310,23 @@ function onDrop(ev: DragEvent): any {
|
||||||
|
|
||||||
//#region ドライブのフォルダ
|
//#region ドライブのフォルダ
|
||||||
const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
|
const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
|
||||||
if (driveFolder != null && driveFolder !== '') {
|
if (driveFolder != null && driveFolder !== "") {
|
||||||
const droppedFolder = JSON.parse(driveFolder);
|
const droppedFolder = JSON.parse(driveFolder);
|
||||||
|
|
||||||
// 移動先が自分自身ならreject
|
// 移動先が自分自身ならreject
|
||||||
if (folder.value && droppedFolder.id === folder.value.id) return false;
|
if (folder.value && droppedFolder.id === folder.value.id) return false;
|
||||||
if (folders.value.some(f => f.id === droppedFolder.id)) return false;
|
if (folders.value.some((f) => f.id === droppedFolder.id)) return false;
|
||||||
removeFolder(droppedFolder.id);
|
removeFolder(droppedFolder.id);
|
||||||
os.api('drive/folders/update', {
|
os.api("drive/folders/update", {
|
||||||
folderId: droppedFolder.id,
|
folderId: droppedFolder.id,
|
||||||
parentId: folder.value ? folder.value.id : null,
|
parentId: folder.value ? folder.value.id : null,
|
||||||
}).then(() => {
|
})
|
||||||
|
.then(() => {
|
||||||
// noop
|
// noop
|
||||||
}).catch(err => {
|
})
|
||||||
|
.catch((err) => {
|
||||||
switch (err) {
|
switch (err) {
|
||||||
case 'detected-circular-definition':
|
case "detected-circular-definition":
|
||||||
os.alert({
|
os.alert({
|
||||||
title: i18n.ts.unableToProcess,
|
title: i18n.ts.unableToProcess,
|
||||||
text: i18n.ts.circularReferenceFolder,
|
text: i18n.ts.circularReferenceFolder,
|
||||||
|
@ -262,7 +334,7 @@ function onDrop(ev: DragEvent): any {
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
os.alert({
|
os.alert({
|
||||||
type: 'error',
|
type: "error",
|
||||||
text: i18n.ts.somethingHappened,
|
text: i18n.ts.somethingHappened,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -278,11 +350,11 @@ function selectLocalFile() {
|
||||||
function urlUpload() {
|
function urlUpload() {
|
||||||
os.inputText({
|
os.inputText({
|
||||||
title: i18n.ts.uploadFromUrl,
|
title: i18n.ts.uploadFromUrl,
|
||||||
type: 'url',
|
type: "url",
|
||||||
placeholder: i18n.ts.uploadFromUrlDescription,
|
placeholder: i18n.ts.uploadFromUrlDescription,
|
||||||
}).then(({ canceled, result: url }) => {
|
}).then(({ canceled, result: url }) => {
|
||||||
if (canceled || !url) return;
|
if (canceled || !url) return;
|
||||||
os.api('drive/files/upload-from-url', {
|
os.api("drive/files/upload-from-url", {
|
||||||
url: url,
|
url: url,
|
||||||
folderId: folder.value ? folder.value.id : undefined,
|
folderId: folder.value ? folder.value.id : undefined,
|
||||||
});
|
});
|
||||||
|
@ -300,10 +372,10 @@ function createFolder() {
|
||||||
placeholder: i18n.ts.folderName,
|
placeholder: i18n.ts.folderName,
|
||||||
}).then(({ canceled, result: name }) => {
|
}).then(({ canceled, result: name }) => {
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
os.api('drive/folders/create', {
|
os.api("drive/folders/create", {
|
||||||
name: name,
|
name: name,
|
||||||
parentId: folder.value ? folder.value.id : undefined,
|
parentId: folder.value ? folder.value.id : undefined,
|
||||||
}).then(createdFolder => {
|
}).then((createdFolder) => {
|
||||||
addFolder(createdFolder, true);
|
addFolder(createdFolder, true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -316,10 +388,10 @@ function renameFolder(folderToRename: Misskey.entities.DriveFolder) {
|
||||||
default: folderToRename.name,
|
default: folderToRename.name,
|
||||||
}).then(({ canceled, result: name }) => {
|
}).then(({ canceled, result: name }) => {
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
os.api('drive/folders/update', {
|
os.api("drive/folders/update", {
|
||||||
folderId: folderToRename.id,
|
folderId: folderToRename.id,
|
||||||
name: name,
|
name: name,
|
||||||
}).then(updatedFolder => {
|
}).then((updatedFolder) => {
|
||||||
// FIXME: 画面を更新するために自分自身に移動
|
// FIXME: 画面を更新するために自分自身に移動
|
||||||
move(updatedFolder);
|
move(updatedFolder);
|
||||||
});
|
});
|
||||||
|
@ -327,23 +399,25 @@ function renameFolder(folderToRename: Misskey.entities.DriveFolder) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) {
|
function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) {
|
||||||
os.api('drive/folders/delete', {
|
os.api("drive/folders/delete", {
|
||||||
folderId: folderToDelete.id,
|
folderId: folderToDelete.id,
|
||||||
}).then(() => {
|
})
|
||||||
|
.then(() => {
|
||||||
// 削除時に親フォルダに移動
|
// 削除時に親フォルダに移動
|
||||||
move(folderToDelete.parentId);
|
move(folderToDelete.parentId);
|
||||||
}).catch(err => {
|
})
|
||||||
|
.catch((err) => {
|
||||||
switch (err.id) {
|
switch (err.id) {
|
||||||
case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
|
case "b0fc8a17-963c-405d-bfbc-859a487295e1":
|
||||||
os.alert({
|
os.alert({
|
||||||
type: 'error',
|
type: "error",
|
||||||
title: i18n.ts.unableToDelete,
|
title: i18n.ts.unableToDelete,
|
||||||
text: i18n.ts.hasChildFilesOrFolders,
|
text: i18n.ts.hasChildFilesOrFolders,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
os.alert({
|
os.alert({
|
||||||
type: 'error',
|
type: "error",
|
||||||
text: i18n.ts.unableToDelete,
|
text: i18n.ts.unableToDelete,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -357,46 +431,62 @@ function onChangeFileInput() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function upload(file: File, folderToUpload?: Misskey.entities.DriveFolder | null) {
|
function upload(
|
||||||
uploadFile(file, (folderToUpload && typeof folderToUpload === 'object') ? folderToUpload.id : null, undefined, keepOriginal.value).then(res => {
|
file: File,
|
||||||
|
folderToUpload?: Misskey.entities.DriveFolder | null
|
||||||
|
) {
|
||||||
|
uploadFile(
|
||||||
|
file,
|
||||||
|
folderToUpload && typeof folderToUpload === "object"
|
||||||
|
? folderToUpload.id
|
||||||
|
: null,
|
||||||
|
undefined,
|
||||||
|
keepOriginal.value
|
||||||
|
).then((res) => {
|
||||||
addFile(res, true);
|
addFile(res, true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function chooseFile(file: Misskey.entities.DriveFile) {
|
function chooseFile(file: Misskey.entities.DriveFile) {
|
||||||
const isAlreadySelected = selectedFiles.value.some(f => f.id === file.id);
|
const isAlreadySelected = selectedFiles.value.some((f) => f.id === file.id);
|
||||||
if (props.multiple) {
|
if (props.multiple) {
|
||||||
if (isAlreadySelected) {
|
if (isAlreadySelected) {
|
||||||
selectedFiles.value = selectedFiles.value.filter(f => f.id !== file.id);
|
selectedFiles.value = selectedFiles.value.filter(
|
||||||
|
(f) => f.id !== file.id
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
selectedFiles.value.push(file);
|
selectedFiles.value.push(file);
|
||||||
}
|
}
|
||||||
emit('change-selection', selectedFiles.value);
|
emit("change-selection", selectedFiles.value);
|
||||||
} else {
|
} else {
|
||||||
if (isAlreadySelected) {
|
if (isAlreadySelected) {
|
||||||
emit('selected', file);
|
emit("selected", file);
|
||||||
} else {
|
} else {
|
||||||
selectedFiles.value = [file];
|
selectedFiles.value = [file];
|
||||||
emit('change-selection', [file]);
|
emit("change-selection", [file]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function chooseFolder(folderToChoose: Misskey.entities.DriveFolder) {
|
function chooseFolder(folderToChoose: Misskey.entities.DriveFolder) {
|
||||||
const isAlreadySelected = selectedFolders.value.some(f => f.id === folderToChoose.id);
|
const isAlreadySelected = selectedFolders.value.some(
|
||||||
|
(f) => f.id === folderToChoose.id
|
||||||
|
);
|
||||||
if (props.multiple) {
|
if (props.multiple) {
|
||||||
if (isAlreadySelected) {
|
if (isAlreadySelected) {
|
||||||
selectedFolders.value = selectedFolders.value.filter(f => f.id !== folderToChoose.id);
|
selectedFolders.value = selectedFolders.value.filter(
|
||||||
|
(f) => f.id !== folderToChoose.id
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
selectedFolders.value.push(folderToChoose);
|
selectedFolders.value.push(folderToChoose);
|
||||||
}
|
}
|
||||||
emit('change-selection', selectedFolders.value);
|
emit("change-selection", selectedFolders.value);
|
||||||
} else {
|
} else {
|
||||||
if (isAlreadySelected) {
|
if (isAlreadySelected) {
|
||||||
emit('selected', folderToChoose);
|
emit("selected", folderToChoose);
|
||||||
} else {
|
} else {
|
||||||
selectedFolders.value = [folderToChoose];
|
selectedFolders.value = [folderToChoose];
|
||||||
emit('change-selection', [folderToChoose]);
|
emit("change-selection", [folderToChoose]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -405,26 +495,26 @@ function move(target?: Misskey.entities.DriveFolder) {
|
||||||
if (!target) {
|
if (!target) {
|
||||||
goRoot();
|
goRoot();
|
||||||
return;
|
return;
|
||||||
} else if (typeof target === 'object') {
|
} else if (typeof target === "object") {
|
||||||
target = target.id;
|
target = target.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
fetching.value = true;
|
fetching.value = true;
|
||||||
|
|
||||||
os.api('drive/folders/show', {
|
os.api("drive/folders/show", {
|
||||||
folderId: target,
|
folderId: target,
|
||||||
}).then(folderToMove => {
|
}).then((folderToMove) => {
|
||||||
folder.value = folderToMove;
|
folder.value = folderToMove;
|
||||||
hierarchyFolders.value = [];
|
hierarchyFolders.value = [];
|
||||||
|
|
||||||
const dive = folderToDive => {
|
const dive = (folderToDive) => {
|
||||||
hierarchyFolders.value.unshift(folderToDive);
|
hierarchyFolders.value.unshift(folderToDive);
|
||||||
if (folderToDive.parent) dive(folderToDive.parent);
|
if (folderToDive.parent) dive(folderToDive.parent);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (folderToMove.parent) dive(folderToMove.parent);
|
if (folderToMove.parent) dive(folderToMove.parent);
|
||||||
|
|
||||||
emit('open-folder', folderToMove);
|
emit("open-folder", folderToMove);
|
||||||
fetch();
|
fetch();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -433,8 +523,8 @@ function addFolder(folderToAdd: Misskey.entities.DriveFolder, unshift = false) {
|
||||||
const current = folder.value ? folder.value.id : null;
|
const current = folder.value ? folder.value.id : null;
|
||||||
if (current !== folderToAdd.parentId) return;
|
if (current !== folderToAdd.parentId) return;
|
||||||
|
|
||||||
if (folders.value.some(f => f.id === folderToAdd.id)) {
|
if (folders.value.some((f) => f.id === folderToAdd.id)) {
|
||||||
const exist = folders.value.map(f => f.id).indexOf(folderToAdd.id);
|
const exist = folders.value.map((f) => f.id).indexOf(folderToAdd.id);
|
||||||
folders.value[exist] = folderToAdd;
|
folders.value[exist] = folderToAdd;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -450,8 +540,8 @@ function addFile(fileToAdd: Misskey.entities.DriveFile, unshift = false) {
|
||||||
const current = folder.value ? folder.value.id : null;
|
const current = folder.value ? folder.value.id : null;
|
||||||
if (current !== fileToAdd.folderId) return;
|
if (current !== fileToAdd.folderId) return;
|
||||||
|
|
||||||
if (files.value.some(f => f.id === fileToAdd.id)) {
|
if (files.value.some((f) => f.id === fileToAdd.id)) {
|
||||||
const exist = files.value.map(f => f.id).indexOf(fileToAdd.id);
|
const exist = files.value.map((f) => f.id).indexOf(fileToAdd.id);
|
||||||
files.value[exist] = fileToAdd;
|
files.value[exist] = fileToAdd;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -464,13 +554,14 @@ function addFile(fileToAdd: Misskey.entities.DriveFile, unshift = false) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeFolder(folderToRemove: Misskey.entities.DriveFolder | string) {
|
function removeFolder(folderToRemove: Misskey.entities.DriveFolder | string) {
|
||||||
const folderIdToRemove = typeof folderToRemove === 'object' ? folderToRemove.id : folderToRemove;
|
const folderIdToRemove =
|
||||||
folders.value = folders.value.filter(f => f.id !== folderIdToRemove);
|
typeof folderToRemove === "object" ? folderToRemove.id : folderToRemove;
|
||||||
|
folders.value = folders.value.filter((f) => f.id !== folderIdToRemove);
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeFile(file: Misskey.entities.DriveFile | string) {
|
function removeFile(file: Misskey.entities.DriveFile | string) {
|
||||||
const fileId = typeof file === 'object' ? file.id : file;
|
const fileId = typeof file === "object" ? file.id : file;
|
||||||
files.value = files.value.filter(f => f.id !== fileId);
|
files.value = files.value.filter((f) => f.id !== fileId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendFile(file: Misskey.entities.DriveFile) {
|
function appendFile(file: Misskey.entities.DriveFile) {
|
||||||
|
@ -495,7 +586,7 @@ function goRoot() {
|
||||||
|
|
||||||
folder.value = null;
|
folder.value = null;
|
||||||
hierarchyFolders.value = [];
|
hierarchyFolders.value = [];
|
||||||
emit('move-root');
|
emit("move-root");
|
||||||
fetch();
|
fetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -509,10 +600,12 @@ async function fetch() {
|
||||||
const foldersMax = 30;
|
const foldersMax = 30;
|
||||||
const filesMax = 30;
|
const filesMax = 30;
|
||||||
|
|
||||||
const foldersPromise = os.api('drive/folders', {
|
const foldersPromise = os
|
||||||
|
.api("drive/folders", {
|
||||||
folderId: folder.value ? folder.value.id : null,
|
folderId: folder.value ? folder.value.id : null,
|
||||||
limit: foldersMax + 1,
|
limit: foldersMax + 1,
|
||||||
}).then(fetchedFolders => {
|
})
|
||||||
|
.then((fetchedFolders) => {
|
||||||
if (fetchedFolders.length === foldersMax + 1) {
|
if (fetchedFolders.length === foldersMax + 1) {
|
||||||
moreFolders.value = true;
|
moreFolders.value = true;
|
||||||
fetchedFolders.pop();
|
fetchedFolders.pop();
|
||||||
|
@ -520,11 +613,13 @@ async function fetch() {
|
||||||
return fetchedFolders;
|
return fetchedFolders;
|
||||||
});
|
});
|
||||||
|
|
||||||
const filesPromise = os.api('drive/files', {
|
const filesPromise = os
|
||||||
|
.api("drive/files", {
|
||||||
folderId: folder.value ? folder.value.id : null,
|
folderId: folder.value ? folder.value.id : null,
|
||||||
type: props.type,
|
type: props.type,
|
||||||
limit: filesMax + 1,
|
limit: filesMax + 1,
|
||||||
}).then(fetchedFiles => {
|
})
|
||||||
|
.then((fetchedFiles) => {
|
||||||
if (fetchedFiles.length === filesMax + 1) {
|
if (fetchedFiles.length === filesMax + 1) {
|
||||||
moreFiles.value = true;
|
moreFiles.value = true;
|
||||||
fetchedFiles.pop();
|
fetchedFiles.pop();
|
||||||
|
@ -532,7 +627,10 @@ async function fetch() {
|
||||||
return fetchedFiles;
|
return fetchedFiles;
|
||||||
});
|
});
|
||||||
|
|
||||||
const [fetchedFolders, fetchedFiles] = await Promise.all([foldersPromise, filesPromise]);
|
const [fetchedFolders, fetchedFiles] = await Promise.all([
|
||||||
|
foldersPromise,
|
||||||
|
filesPromise,
|
||||||
|
]);
|
||||||
|
|
||||||
for (const x of fetchedFolders) appendFolder(x);
|
for (const x of fetchedFolders) appendFolder(x);
|
||||||
for (const x of fetchedFiles) appendFile(x);
|
for (const x of fetchedFiles) appendFile(x);
|
||||||
|
@ -546,12 +644,12 @@ function fetchMoreFiles() {
|
||||||
const max = 30;
|
const max = 30;
|
||||||
|
|
||||||
// ファイル一覧取得
|
// ファイル一覧取得
|
||||||
os.api('drive/files', {
|
os.api("drive/files", {
|
||||||
folderId: folder.value ? folder.value.id : null,
|
folderId: folder.value ? folder.value.id : null,
|
||||||
type: props.type,
|
type: props.type,
|
||||||
untilId: files.value[files.value.length - 1].id,
|
untilId: files.value[files.value.length - 1].id,
|
||||||
limit: max + 1,
|
limit: max + 1,
|
||||||
}).then(files => {
|
}).then((files) => {
|
||||||
if (files.length === max + 1) {
|
if (files.length === max + 1) {
|
||||||
moreFiles.value = true;
|
moreFiles.value = true;
|
||||||
files.pop();
|
files.pop();
|
||||||
|
@ -564,41 +662,71 @@ function fetchMoreFiles() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMenu() {
|
function getMenu() {
|
||||||
return [{
|
return [
|
||||||
type: 'switch',
|
{
|
||||||
|
type: "switch",
|
||||||
text: i18n.ts.keepOriginalUploading,
|
text: i18n.ts.keepOriginalUploading,
|
||||||
ref: keepOriginal,
|
ref: keepOriginal,
|
||||||
}, null, {
|
},
|
||||||
|
null,
|
||||||
|
{
|
||||||
text: i18n.ts.addFile,
|
text: i18n.ts.addFile,
|
||||||
type: 'label',
|
type: "label",
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
text: i18n.ts.upload,
|
text: i18n.ts.upload,
|
||||||
icon: 'ph-upload-simple ph-bold ph-lg',
|
icon: "ph-upload-simple ph-bold ph-lg",
|
||||||
action: () => { selectLocalFile(); },
|
action: () => {
|
||||||
}, {
|
selectLocalFile();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
text: i18n.ts.fromUrl,
|
text: i18n.ts.fromUrl,
|
||||||
icon: 'ph-link-simple ph-bold ph-lg',
|
icon: "ph-link-simple ph-bold ph-lg",
|
||||||
action: () => { urlUpload(); },
|
action: () => {
|
||||||
}, null, {
|
urlUpload();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
{
|
||||||
text: folder.value ? folder.value.name : i18n.ts.drive,
|
text: folder.value ? folder.value.name : i18n.ts.drive,
|
||||||
type: 'label',
|
type: "label",
|
||||||
}, folder.value ? {
|
},
|
||||||
|
folder.value
|
||||||
|
? {
|
||||||
text: i18n.ts.renameFolder,
|
text: i18n.ts.renameFolder,
|
||||||
icon: 'ph-cursor-text ph-bold ph-lg',
|
icon: "ph-cursor-text ph-bold ph-lg",
|
||||||
action: () => { renameFolder(folder.value); },
|
action: () => {
|
||||||
} : undefined, folder.value ? {
|
renameFolder(folder.value);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
folder.value
|
||||||
|
? {
|
||||||
text: i18n.ts.deleteFolder,
|
text: i18n.ts.deleteFolder,
|
||||||
icon: 'ph-trash ph-bold ph-lg',
|
icon: "ph-trash ph-bold ph-lg",
|
||||||
action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); },
|
action: () => {
|
||||||
} : undefined, {
|
deleteFolder(
|
||||||
|
folder.value as Misskey.entities.DriveFolder
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
{
|
||||||
text: i18n.ts.createFolder,
|
text: i18n.ts.createFolder,
|
||||||
icon: 'ph-folder-notch-plus ph-bold ph-lg',
|
icon: "ph-folder-notch-plus ph-bold ph-lg",
|
||||||
action: () => { createFolder(); },
|
action: () => {
|
||||||
}];
|
createFolder();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function showMenu(ev: MouseEvent) {
|
function showMenu(ev: MouseEvent) {
|
||||||
os.popupMenu(getMenu(), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
|
os.popupMenu(
|
||||||
|
getMenu(),
|
||||||
|
(ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onContextmenu(ev: MouseEvent) {
|
function onContextmenu(ev: MouseEvent) {
|
||||||
|
@ -612,12 +740,12 @@ onMounted(() => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
connection.on('fileCreated', onStreamDriveFileCreated);
|
connection.on("fileCreated", onStreamDriveFileCreated);
|
||||||
connection.on('fileUpdated', onStreamDriveFileUpdated);
|
connection.on("fileUpdated", onStreamDriveFileUpdated);
|
||||||
connection.on('fileDeleted', onStreamDriveFileDeleted);
|
connection.on("fileDeleted", onStreamDriveFileDeleted);
|
||||||
connection.on('folderCreated', onStreamDriveFolderCreated);
|
connection.on("folderCreated", onStreamDriveFolderCreated);
|
||||||
connection.on('folderUpdated', onStreamDriveFolderUpdated);
|
connection.on("folderUpdated", onStreamDriveFolderUpdated);
|
||||||
connection.on('folderDeleted', onStreamDriveFolderDeleted);
|
connection.on("folderDeleted", onStreamDriveFolderDeleted);
|
||||||
|
|
||||||
if (props.initialFolder) {
|
if (props.initialFolder) {
|
||||||
move(props.initialFolder);
|
move(props.initialFolder);
|
||||||
|
@ -656,7 +784,8 @@ onBeforeUnmount(() => {
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
box-shadow: 0 1px 0 var(--divider);
|
box-shadow: 0 1px 0 var(--divider);
|
||||||
|
|
||||||
&, * {
|
&,
|
||||||
|
* {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -714,7 +843,8 @@ onBeforeUnmount(() => {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: var(--margin);
|
padding: var(--margin);
|
||||||
|
|
||||||
&, * {
|
&,
|
||||||
|
* {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -735,7 +865,6 @@ onBeforeUnmount(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
> .contents {
|
> .contents {
|
||||||
|
|
||||||
> .folders,
|
> .folders,
|
||||||
> .files {
|
> .files {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -1,23 +1,48 @@
|
||||||
<template>
|
<template>
|
||||||
<div ref="thumbnail" class="zdjebgpv">
|
<div ref="thumbnail" class="zdjebgpv">
|
||||||
<ImgWithBlurhash v-if="isThumbnailAvailable" :hash="file.blurhash" :src="file.thumbnailUrl" :alt="file.name" :title="file.name" :cover="fit !== 'contain'"/>
|
<ImgWithBlurhash
|
||||||
<i v-else-if="is === 'image'" class="ph-file-image ph-bold ph-lg icon"></i>
|
v-if="isThumbnailAvailable"
|
||||||
<i v-else-if="is === 'video'" class="ph-file-video ph-bold ph-lg icon"></i>
|
:hash="file.blurhash"
|
||||||
<i v-else-if="is === 'audio' || is === 'midi'" class="ph-file-audio ph-bold ph-lg icon"></i>
|
:src="file.thumbnailUrl"
|
||||||
|
:alt="file.name"
|
||||||
|
:title="file.name"
|
||||||
|
:cover="fit !== 'contain'"
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
v-else-if="is === 'image'"
|
||||||
|
class="ph-file-image ph-bold ph-lg icon"
|
||||||
|
></i>
|
||||||
|
<i
|
||||||
|
v-else-if="is === 'video'"
|
||||||
|
class="ph-file-video ph-bold ph-lg icon"
|
||||||
|
></i>
|
||||||
|
<i
|
||||||
|
v-else-if="is === 'audio' || is === 'midi'"
|
||||||
|
class="ph-file-audio ph-bold ph-lg icon"
|
||||||
|
></i>
|
||||||
<i v-else-if="is === 'csv'" class="ph-file-csv ph-bold ph-lg icon"></i>
|
<i v-else-if="is === 'csv'" class="ph-file-csv ph-bold ph-lg icon"></i>
|
||||||
<i v-else-if="is === 'pdf'" class="ph-file-pdf ph-bold ph-lg icon"></i>
|
<i v-else-if="is === 'pdf'" class="ph-file-pdf ph-bold ph-lg icon"></i>
|
||||||
<i v-else-if="is === 'textfile'" class="ph-file-text ph-bold ph-lg icon"></i>
|
<i
|
||||||
<i v-else-if="is === 'archive'" class="ph-file-zip ph-bold ph-lg icon"></i>
|
v-else-if="is === 'textfile'"
|
||||||
|
class="ph-file-text ph-bold ph-lg icon"
|
||||||
|
></i>
|
||||||
|
<i
|
||||||
|
v-else-if="is === 'archive'"
|
||||||
|
class="ph-file-zip ph-bold ph-lg icon"
|
||||||
|
></i>
|
||||||
<i v-else class="ph-file ph-bold ph-lg icon"></i>
|
<i v-else class="ph-file ph-bold ph-lg icon"></i>
|
||||||
|
|
||||||
<i v-if="isThumbnailAvailable && is === 'video'" class="ph-file-video ph-bold ph-lg icon-sub"></i>
|
<i
|
||||||
|
v-if="isThumbnailAvailable && is === 'video'"
|
||||||
|
class="ph-file-video ph-bold ph-lg icon-sub"
|
||||||
|
></i>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from "vue";
|
||||||
import type * as Misskey from 'calckey-js';
|
import type * as Misskey from "calckey-js";
|
||||||
import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
|
import ImgWithBlurhash from "@/components/MkImgWithBlurhash.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
file: Misskey.entities.DriveFile;
|
file: Misskey.entities.DriveFile;
|
||||||
|
@ -25,30 +50,33 @@ const props = defineProps<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const is = computed(() => {
|
const is = computed(() => {
|
||||||
if (props.file.type.startsWith('image/')) return 'image';
|
if (props.file.type.startsWith("image/")) return "image";
|
||||||
if (props.file.type.startsWith('video/')) return 'video';
|
if (props.file.type.startsWith("video/")) return "video";
|
||||||
if (props.file.type === 'audio/midi') return 'midi';
|
if (props.file.type === "audio/midi") return "midi";
|
||||||
if (props.file.type.startsWith('audio/')) return 'audio';
|
if (props.file.type.startsWith("audio/")) return "audio";
|
||||||
if (props.file.type.endsWith('/csv')) return 'csv';
|
if (props.file.type.endsWith("/csv")) return "csv";
|
||||||
if (props.file.type.endsWith('/pdf')) return 'pdf';
|
if (props.file.type.endsWith("/pdf")) return "pdf";
|
||||||
if (props.file.type.startsWith('text/')) return 'textfile';
|
if (props.file.type.startsWith("text/")) return "textfile";
|
||||||
if ([
|
if (
|
||||||
'application/zip',
|
[
|
||||||
'application/x-cpio',
|
"application/zip",
|
||||||
'application/x-bzip',
|
"application/x-cpio",
|
||||||
'application/x-bzip2',
|
"application/x-bzip",
|
||||||
'application/java-archive',
|
"application/x-bzip2",
|
||||||
'application/x-rar-compressed',
|
"application/java-archive",
|
||||||
'application/x-tar',
|
"application/x-rar-compressed",
|
||||||
'application/gzip',
|
"application/x-tar",
|
||||||
'application/x-7z-compressed',
|
"application/gzip",
|
||||||
].some(archiveType => archiveType === props.file.type)) return 'archive';
|
"application/x-7z-compressed",
|
||||||
return 'unknown';
|
].some((archiveType) => archiveType === props.file.type)
|
||||||
|
)
|
||||||
|
return "archive";
|
||||||
|
return "unknown";
|
||||||
});
|
});
|
||||||
|
|
||||||
const isThumbnailAvailable = computed(() => {
|
const isThumbnailAvailable = computed(() => {
|
||||||
return props.file.thumbnailUrl
|
return props.file.thumbnailUrl
|
||||||
? (is.value === 'image' as const || is.value === 'video')
|
? is.value === ("image" as const) || is.value === "video"
|
||||||
: false;
|
: false;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -4,38 +4,58 @@
|
||||||
:width="800"
|
:width="800"
|
||||||
:height="500"
|
:height="500"
|
||||||
:with-ok-button="true"
|
:with-ok-button="true"
|
||||||
:ok-button-disabled="(type === 'file') && (selected.length === 0)"
|
:ok-button-disabled="type === 'file' && selected.length === 0"
|
||||||
@click="cancel()"
|
@click="cancel()"
|
||||||
@close="cancel()"
|
@close="cancel()"
|
||||||
@ok="ok()"
|
@ok="ok()"
|
||||||
@closed="emit('closed')"
|
@closed="emit('closed')"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
{{ multiple ? ((type === 'file') ? i18n.ts.selectFiles : i18n.ts.selectFolders) : ((type === 'file') ? i18n.ts.selectFile : i18n.ts.selectFolder) }}
|
{{
|
||||||
<span v-if="selected.length > 0" style="margin-left: 8px; opacity: 0.5;">({{ number(selected.length) }})</span>
|
multiple
|
||||||
|
? type === "file"
|
||||||
|
? i18n.ts.selectFiles
|
||||||
|
: i18n.ts.selectFolders
|
||||||
|
: type === "file"
|
||||||
|
? i18n.ts.selectFile
|
||||||
|
: i18n.ts.selectFolder
|
||||||
|
}}
|
||||||
|
<span
|
||||||
|
v-if="selected.length > 0"
|
||||||
|
style="margin-left: 8px; opacity: 0.5"
|
||||||
|
>({{ number(selected.length) }})</span
|
||||||
|
>
|
||||||
</template>
|
</template>
|
||||||
<XDrive :multiple="multiple" :select="type" @changeSelection="onChangeSelection" @selected="ok()"/>
|
<XDrive
|
||||||
|
:multiple="multiple"
|
||||||
|
:select="type"
|
||||||
|
@changeSelection="onChangeSelection"
|
||||||
|
@selected="ok()"
|
||||||
|
/>
|
||||||
</XModalWindow>
|
</XModalWindow>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from "vue";
|
||||||
import * as Misskey from 'calckey-js';
|
import * as Misskey from "calckey-js";
|
||||||
import XDrive from '@/components/MkDrive.vue';
|
import XDrive from "@/components/MkDrive.vue";
|
||||||
import XModalWindow from '@/components/MkModalWindow.vue';
|
import XModalWindow from "@/components/MkModalWindow.vue";
|
||||||
import number from '@/filters/number';
|
import number from "@/filters/number";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
withDefaults(defineProps<{
|
withDefaults(
|
||||||
type?: 'file' | 'folder';
|
defineProps<{
|
||||||
|
type?: "file" | "folder";
|
||||||
multiple: boolean;
|
multiple: boolean;
|
||||||
}>(), {
|
}>(),
|
||||||
type: 'file',
|
{
|
||||||
});
|
type: "file",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'done', r?: Misskey.entities.DriveFile[]): void;
|
(ev: "done", r?: Misskey.entities.DriveFile[]): void;
|
||||||
(ev: 'closed'): void;
|
(ev: "closed"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const dialog = ref<InstanceType<typeof XModalWindow>>();
|
const dialog = ref<InstanceType<typeof XModalWindow>>();
|
||||||
|
@ -43,12 +63,12 @@ const dialog = ref<InstanceType<typeof XModalWindow>>();
|
||||||
const selected = ref<Misskey.entities.DriveFile[]>([]);
|
const selected = ref<Misskey.entities.DriveFile[]>([]);
|
||||||
|
|
||||||
function ok() {
|
function ok() {
|
||||||
emit('done', selected.value);
|
emit("done", selected.value);
|
||||||
dialog.value?.close();
|
dialog.value?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancel() {
|
function cancel() {
|
||||||
emit('done');
|
emit("done");
|
||||||
dialog.value?.close();
|
dialog.value?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,17 +14,17 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import {} from "vue";
|
||||||
import * as Misskey from 'calckey-js';
|
import * as Misskey from "calckey-js";
|
||||||
import XDrive from '@/components/MkDrive.vue';
|
import XDrive from "@/components/MkDrive.vue";
|
||||||
import XWindow from '@/components/MkWindow.vue';
|
import XWindow from "@/components/MkWindow.vue";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
initialFolder?: Misskey.entities.DriveFolder;
|
initialFolder?: Misskey.entities.DriveFolder;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'closed'): void;
|
(ev: "closed"): void;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -2,7 +2,15 @@
|
||||||
<!-- このコンポーネントの要素のclassは親から利用されるのでむやみに弄らないこと -->
|
<!-- このコンポーネントの要素のclassは親から利用されるのでむやみに弄らないこと -->
|
||||||
<section>
|
<section>
|
||||||
<header class="_acrylic" @click="shown = !shown">
|
<header class="_acrylic" @click="shown = !shown">
|
||||||
<i class="toggle ph-fw ph-lg" :class="shown ? 'ph-caret-down-bold ph-lg' : 'ph-caret-up ph-bold ph-lg'"></i> <slot></slot> ({{ emojis.length }})
|
<i
|
||||||
|
class="toggle ph-fw ph-lg"
|
||||||
|
:class="
|
||||||
|
shown
|
||||||
|
? 'ph-caret-down-bold ph-lg'
|
||||||
|
: 'ph-caret-up ph-bold ph-lg'
|
||||||
|
"
|
||||||
|
></i>
|
||||||
|
<slot></slot> ({{ emojis.length }})
|
||||||
</header>
|
</header>
|
||||||
<div v-if="shown" class="body">
|
<div v-if="shown" class="body">
|
||||||
<button
|
<button
|
||||||
|
@ -18,7 +26,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from "vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
emojis: string[];
|
emojis: string[];
|
||||||
|
@ -26,11 +34,10 @@ const props = defineProps<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'chosen', v: string, event: MouseEvent): void;
|
(ev: "chosen", v: string, event: MouseEvent): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const shown = ref(!!props.initialShown);
|
const shown = ref(!!props.initialShown);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped></style>
|
||||||
</style>
|
|
||||||
|
|
|
@ -1,6 +1,20 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
|
<div
|
||||||
<input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" @paste.stop="paste" @keyup.enter="done()">
|
class="omfetrab"
|
||||||
|
:class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]"
|
||||||
|
:style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref="search"
|
||||||
|
v-model.trim="q"
|
||||||
|
class="search"
|
||||||
|
data-prevent-emoji-insert
|
||||||
|
:class="{ filled: q != null && q != '' }"
|
||||||
|
:placeholder="i18n.ts.search"
|
||||||
|
type="search"
|
||||||
|
@paste.stop="paste"
|
||||||
|
@keyup.enter="done()"
|
||||||
|
/>
|
||||||
<div ref="emojis" class="emojis">
|
<div ref="emojis" class="emojis">
|
||||||
<section class="result">
|
<section class="result">
|
||||||
<div v-if="searchResultCustom.length > 0" class="body">
|
<div v-if="searchResultCustom.length > 0" class="body">
|
||||||
|
@ -13,7 +27,14 @@
|
||||||
@click="chosen(emoji, $event)"
|
@click="chosen(emoji, $event)"
|
||||||
>
|
>
|
||||||
<!--<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>-->
|
<!--<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>-->
|
||||||
<img class="emoji" :src="disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
|
<img
|
||||||
|
class="emoji"
|
||||||
|
:src="
|
||||||
|
disableShowingAnimatedImages
|
||||||
|
? getStaticImageUrl(emoji.url)
|
||||||
|
: emoji.url
|
||||||
|
"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="searchResultUnicode.length > 0" class="body">
|
<div v-if="searchResultUnicode.length > 0" class="body">
|
||||||
|
@ -40,13 +61,20 @@
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@click="chosen(emoji, $event)"
|
@click="chosen(emoji, $event)"
|
||||||
>
|
>
|
||||||
<MkEmoji class="emoji" :emoji="emoji" :normal="true"/>
|
<MkEmoji
|
||||||
|
class="emoji"
|
||||||
|
:emoji="emoji"
|
||||||
|
:normal="true"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<header class="_acrylic"><i class="ph-alarm ph-bold ph-fw ph-lg"></i> {{ i18n.ts.recentUsed }}</header>
|
<header class="_acrylic">
|
||||||
|
<i class="ph-alarm ph-bold ph-fw ph-lg"></i>
|
||||||
|
{{ i18n.ts.recentUsed }}
|
||||||
|
</header>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<button
|
<button
|
||||||
v-for="emoji in recentlyUsedEmojis"
|
v-for="emoji in recentlyUsedEmojis"
|
||||||
|
@ -54,54 +82,110 @@
|
||||||
class="_button item"
|
class="_button item"
|
||||||
@click="chosen(emoji, $event)"
|
@click="chosen(emoji, $event)"
|
||||||
>
|
>
|
||||||
<MkEmoji class="emoji" :emoji="emoji" :normal="true"/>
|
<MkEmoji
|
||||||
|
class="emoji"
|
||||||
|
:emoji="emoji"
|
||||||
|
:normal="true"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<div v-once class="group">
|
<div v-once class="group">
|
||||||
<header>{{ i18n.ts.customEmojis }}</header>
|
<header>{{ i18n.ts.customEmojis }}</header>
|
||||||
<XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':')" @chosen="chosen">{{ category || i18n.ts.other }}</XSection>
|
<XSection
|
||||||
|
v-for="category in customEmojiCategories"
|
||||||
|
:key="'custom:' + category"
|
||||||
|
:initial-shown="false"
|
||||||
|
:emojis="
|
||||||
|
customEmojis
|
||||||
|
.filter((e) => e.category === category)
|
||||||
|
.map((e) => ':' + e.name + ':')
|
||||||
|
"
|
||||||
|
@chosen="chosen"
|
||||||
|
>{{ category || i18n.ts.other }}</XSection
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div v-once class="group">
|
<div v-once class="group">
|
||||||
<header>{{ i18n.ts.emoji }}</header>
|
<header>{{ i18n.ts.emoji }}</header>
|
||||||
<XSection v-for="category in categories" :key="category" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)" @chosen="chosen">{{ category }}</XSection>
|
<XSection
|
||||||
|
v-for="category in categories"
|
||||||
|
:key="category"
|
||||||
|
:emojis="
|
||||||
|
emojilist
|
||||||
|
.filter((e) => e.category === category)
|
||||||
|
.map((e) => e.char)
|
||||||
|
"
|
||||||
|
@chosen="chosen"
|
||||||
|
>{{ category }}</XSection
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<button class="_button tab" :class="{ active: tab === 'index' }" @click="tab = 'index'"><i class="ph-asterisk ph-bold ph-lg ph-fw ph-lg"></i></button>
|
<button
|
||||||
<button class="_button tab" :class="{ active: tab === 'custom' }" @click="tab = 'custom'"><i class="ph-smiley ph-bold ph-lg ph-fw ph-lg"></i></button>
|
class="_button tab"
|
||||||
<button class="_button tab" :class="{ active: tab === 'unicode' }" @click="tab = 'unicode'"><i class="ph-leaf ph-bold ph-lg ph-fw ph-lg"></i></button>
|
:class="{ active: tab === 'index' }"
|
||||||
<button class="_button tab" :class="{ active: tab === 'tags' }" @click="tab = 'tags'"><i class="ph-hash ph-bold ph-lg ph-fw ph-lg"></i></button>
|
@click="tab = 'index'"
|
||||||
|
>
|
||||||
|
<i class="ph-asterisk ph-bold ph-lg ph-fw ph-lg"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="_button tab"
|
||||||
|
:class="{ active: tab === 'custom' }"
|
||||||
|
@click="tab = 'custom'"
|
||||||
|
>
|
||||||
|
<i class="ph-smiley ph-bold ph-lg ph-fw ph-lg"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="_button tab"
|
||||||
|
:class="{ active: tab === 'unicode' }"
|
||||||
|
@click="tab = 'unicode'"
|
||||||
|
>
|
||||||
|
<i class="ph-leaf ph-bold ph-lg ph-fw ph-lg"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="_button tab"
|
||||||
|
:class="{ active: tab === 'tags' }"
|
||||||
|
@click="tab = 'tags'"
|
||||||
|
>
|
||||||
|
<i class="ph-hash ph-bold ph-lg ph-fw ph-lg"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, computed, watch, onMounted } from 'vue';
|
import { ref, computed, watch, onMounted } from "vue";
|
||||||
import * as Misskey from 'calckey-js';
|
import * as Misskey from "calckey-js";
|
||||||
import XSection from '@/components/MkEmojiPicker.section.vue';
|
import XSection from "@/components/MkEmojiPicker.section.vue";
|
||||||
import { emojilist, UnicodeEmojiDef, unicodeEmojiCategories as categories } from '@/scripts/emojilist';
|
import {
|
||||||
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
|
emojilist,
|
||||||
import Ripple from '@/components/MkRipple.vue';
|
UnicodeEmojiDef,
|
||||||
import * as os from '@/os';
|
unicodeEmojiCategories as categories,
|
||||||
import { isTouchUsing } from '@/scripts/touch';
|
} from "@/scripts/emojilist";
|
||||||
import { deviceKind } from '@/scripts/device-kind';
|
import { getStaticImageUrl } from "@/scripts/get-static-image-url";
|
||||||
import { emojiCategories, instance } from '@/instance';
|
import Ripple from "@/components/MkRipple.vue";
|
||||||
import { i18n } from '@/i18n';
|
import * as os from "@/os";
|
||||||
import { defaultStore } from '@/store';
|
import { isTouchUsing } from "@/scripts/touch";
|
||||||
|
import { deviceKind } from "@/scripts/device-kind";
|
||||||
|
import { emojiCategories, instance } from "@/instance";
|
||||||
|
import { i18n } from "@/i18n";
|
||||||
|
import { defaultStore } from "@/store";
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
showPinned?: boolean;
|
showPinned?: boolean;
|
||||||
asReactionPicker?: boolean;
|
asReactionPicker?: boolean;
|
||||||
maxHeight?: number;
|
maxHeight?: number;
|
||||||
asDrawer?: boolean;
|
asDrawer?: boolean;
|
||||||
}>(), {
|
}>(),
|
||||||
|
{
|
||||||
showPinned: true,
|
showPinned: true,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'chosen', v: string): void;
|
(ev: "chosen", v: string): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const search = ref<HTMLInputElement>();
|
const search = ref<HTMLInputElement>();
|
||||||
|
@ -116,41 +200,48 @@ const {
|
||||||
recentlyUsedEmojis,
|
recentlyUsedEmojis,
|
||||||
} = defaultStore.reactiveState;
|
} = defaultStore.reactiveState;
|
||||||
|
|
||||||
const size = computed(() => props.asReactionPicker ? reactionPickerSize.value : 1);
|
const size = computed(() =>
|
||||||
const width = computed(() => props.asReactionPicker ? reactionPickerWidth.value : 3);
|
props.asReactionPicker ? reactionPickerSize.value : 1
|
||||||
const height = computed(() => props.asReactionPicker ? reactionPickerHeight.value : 2);
|
);
|
||||||
|
const width = computed(() =>
|
||||||
|
props.asReactionPicker ? reactionPickerWidth.value : 3
|
||||||
|
);
|
||||||
|
const height = computed(() =>
|
||||||
|
props.asReactionPicker ? reactionPickerHeight.value : 2
|
||||||
|
);
|
||||||
const customEmojiCategories = emojiCategories;
|
const customEmojiCategories = emojiCategories;
|
||||||
const customEmojis = instance.emojis;
|
const customEmojis = instance.emojis;
|
||||||
const q = ref<string | null>(null);
|
const q = ref<string | null>(null);
|
||||||
const searchResultCustom = ref<Misskey.entities.CustomEmoji[]>([]);
|
const searchResultCustom = ref<Misskey.entities.CustomEmoji[]>([]);
|
||||||
const searchResultUnicode = ref<UnicodeEmojiDef[]>([]);
|
const searchResultUnicode = ref<UnicodeEmojiDef[]>([]);
|
||||||
const tab = ref<'index' | 'custom' | 'unicode' | 'tags'>('index');
|
const tab = ref<"index" | "custom" | "unicode" | "tags">("index");
|
||||||
|
|
||||||
watch(q, () => {
|
watch(q, () => {
|
||||||
if (emojis.value) emojis.value.scrollTop = 0;
|
if (emojis.value) emojis.value.scrollTop = 0;
|
||||||
|
|
||||||
if (q.value == null || q.value === '') {
|
if (q.value == null || q.value === "") {
|
||||||
searchResultCustom.value = [];
|
searchResultCustom.value = [];
|
||||||
searchResultUnicode.value = [];
|
searchResultUnicode.value = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newQ = q.value.replace(/:/g, '').toLowerCase();
|
const newQ = q.value.replace(/:/g, "").toLowerCase();
|
||||||
|
|
||||||
const searchCustom = () => {
|
const searchCustom = () => {
|
||||||
const max = 8;
|
const max = 8;
|
||||||
const emojis = customEmojis;
|
const emojis = customEmojis;
|
||||||
const matches = new Set<Misskey.entities.CustomEmoji>();
|
const matches = new Set<Misskey.entities.CustomEmoji>();
|
||||||
|
|
||||||
const exactMatch = emojis.find(emoji => emoji.name === newQ);
|
const exactMatch = emojis.find((emoji) => emoji.name === newQ);
|
||||||
if (exactMatch) matches.add(exactMatch);
|
if (exactMatch) matches.add(exactMatch);
|
||||||
|
|
||||||
if (newQ.includes(' ')) { // AND検索
|
if (newQ.includes(" ")) {
|
||||||
const keywords = newQ.split(' ');
|
// AND検索
|
||||||
|
const keywords = newQ.split(" ");
|
||||||
|
|
||||||
// 名前にキーワードが含まれている
|
// 名前にキーワードが含まれている
|
||||||
for (const emoji of emojis) {
|
for (const emoji of emojis) {
|
||||||
if (keywords.every(keyword => emoji.name.includes(keyword))) {
|
if (keywords.every((keyword) => emoji.name.includes(keyword))) {
|
||||||
matches.add(emoji);
|
matches.add(emoji);
|
||||||
if (matches.size >= max) break;
|
if (matches.size >= max) break;
|
||||||
}
|
}
|
||||||
|
@ -159,7 +250,15 @@ watch(q, () => {
|
||||||
|
|
||||||
// 名前またはエイリアスにキーワードが含まれている
|
// 名前またはエイリアスにキーワードが含まれている
|
||||||
for (const emoji of emojis) {
|
for (const emoji of emojis) {
|
||||||
if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.aliases.some(alias => alias.includes(keyword)))) {
|
if (
|
||||||
|
keywords.every(
|
||||||
|
(keyword) =>
|
||||||
|
emoji.name.includes(keyword) ||
|
||||||
|
emoji.aliases.some((alias) =>
|
||||||
|
alias.includes(keyword)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
matches.add(emoji);
|
matches.add(emoji);
|
||||||
if (matches.size >= max) break;
|
if (matches.size >= max) break;
|
||||||
}
|
}
|
||||||
|
@ -174,7 +273,7 @@ watch(q, () => {
|
||||||
if (matches.size >= max) return matches;
|
if (matches.size >= max) return matches;
|
||||||
|
|
||||||
for (const emoji of emojis) {
|
for (const emoji of emojis) {
|
||||||
if (emoji.aliases.some(alias => alias.startsWith(newQ))) {
|
if (emoji.aliases.some((alias) => alias.startsWith(newQ))) {
|
||||||
matches.add(emoji);
|
matches.add(emoji);
|
||||||
if (matches.size >= max) break;
|
if (matches.size >= max) break;
|
||||||
}
|
}
|
||||||
|
@ -190,7 +289,7 @@ watch(q, () => {
|
||||||
if (matches.size >= max) return matches;
|
if (matches.size >= max) return matches;
|
||||||
|
|
||||||
for (const emoji of emojis) {
|
for (const emoji of emojis) {
|
||||||
if (emoji.aliases.some(alias => alias.includes(newQ))) {
|
if (emoji.aliases.some((alias) => alias.includes(newQ))) {
|
||||||
matches.add(emoji);
|
matches.add(emoji);
|
||||||
if (matches.size >= max) break;
|
if (matches.size >= max) break;
|
||||||
}
|
}
|
||||||
|
@ -205,15 +304,16 @@ watch(q, () => {
|
||||||
const emojis = emojilist;
|
const emojis = emojilist;
|
||||||
const matches = new Set<UnicodeEmojiDef>();
|
const matches = new Set<UnicodeEmojiDef>();
|
||||||
|
|
||||||
const exactMatch = emojis.find(emoji => emoji.name === newQ);
|
const exactMatch = emojis.find((emoji) => emoji.name === newQ);
|
||||||
if (exactMatch) matches.add(exactMatch);
|
if (exactMatch) matches.add(exactMatch);
|
||||||
|
|
||||||
if (newQ.includes(' ')) { // AND検索
|
if (newQ.includes(" ")) {
|
||||||
const keywords = newQ.split(' ');
|
// AND検索
|
||||||
|
const keywords = newQ.split(" ");
|
||||||
|
|
||||||
// 名前にキーワードが含まれている
|
// 名前にキーワードが含まれている
|
||||||
for (const emoji of emojis) {
|
for (const emoji of emojis) {
|
||||||
if (keywords.every(keyword => emoji.name.includes(keyword))) {
|
if (keywords.every((keyword) => emoji.name.includes(keyword))) {
|
||||||
matches.add(emoji);
|
matches.add(emoji);
|
||||||
if (matches.size >= max) break;
|
if (matches.size >= max) break;
|
||||||
}
|
}
|
||||||
|
@ -222,7 +322,15 @@ watch(q, () => {
|
||||||
|
|
||||||
// 名前またはエイリアスにキーワードが含まれている
|
// 名前またはエイリアスにキーワードが含まれている
|
||||||
for (const emoji of emojis) {
|
for (const emoji of emojis) {
|
||||||
if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.keywords.some(alias => alias.includes(keyword)))) {
|
if (
|
||||||
|
keywords.every(
|
||||||
|
(keyword) =>
|
||||||
|
emoji.name.includes(keyword) ||
|
||||||
|
emoji.keywords.some((alias) =>
|
||||||
|
alias.includes(keyword)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
matches.add(emoji);
|
matches.add(emoji);
|
||||||
if (matches.size >= max) break;
|
if (matches.size >= max) break;
|
||||||
}
|
}
|
||||||
|
@ -237,7 +345,9 @@ watch(q, () => {
|
||||||
if (matches.size >= max) return matches;
|
if (matches.size >= max) return matches;
|
||||||
|
|
||||||
for (const emoji of emojis) {
|
for (const emoji of emojis) {
|
||||||
if (emoji.keywords.some(keyword => keyword.startsWith(newQ))) {
|
if (
|
||||||
|
emoji.keywords.some((keyword) => keyword.startsWith(newQ))
|
||||||
|
) {
|
||||||
matches.add(emoji);
|
matches.add(emoji);
|
||||||
if (matches.size >= max) break;
|
if (matches.size >= max) break;
|
||||||
}
|
}
|
||||||
|
@ -253,7 +363,7 @@ watch(q, () => {
|
||||||
if (matches.size >= max) return matches;
|
if (matches.size >= max) return matches;
|
||||||
|
|
||||||
for (const emoji of emojis) {
|
for (const emoji of emojis) {
|
||||||
if (emoji.keywords.some(keyword => keyword.includes(newQ))) {
|
if (emoji.keywords.some((keyword) => keyword.includes(newQ))) {
|
||||||
matches.add(emoji);
|
matches.add(emoji);
|
||||||
if (matches.size >= max) break;
|
if (matches.size >= max) break;
|
||||||
}
|
}
|
||||||
|
@ -268,7 +378,7 @@ watch(q, () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
function focus() {
|
function focus() {
|
||||||
if (!['smartphone', 'tablet'].includes(deviceKind) && !isTouchUsing) {
|
if (!["smartphone", "tablet"].includes(deviceKind) && !isTouchUsing) {
|
||||||
search.value?.focus({
|
search.value?.focus({
|
||||||
preventScroll: true,
|
preventScroll: true,
|
||||||
});
|
});
|
||||||
|
@ -277,36 +387,40 @@ function focus() {
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
if (emojis.value) emojis.value.scrollTop = 0;
|
if (emojis.value) emojis.value.scrollTop = 0;
|
||||||
q.value = '';
|
q.value = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getKey(emoji: string | Misskey.entities.CustomEmoji | UnicodeEmojiDef): string {
|
function getKey(
|
||||||
return typeof emoji === 'string' ? emoji : (emoji.char || `:${emoji.name}:`);
|
emoji: string | Misskey.entities.CustomEmoji | UnicodeEmojiDef
|
||||||
|
): string {
|
||||||
|
return typeof emoji === "string" ? emoji : emoji.char || `:${emoji.name}:`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function chosen(emoji: any, ev?: MouseEvent) {
|
function chosen(emoji: any, ev?: MouseEvent) {
|
||||||
const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined;
|
const el =
|
||||||
|
ev &&
|
||||||
|
((ev.currentTarget ?? ev.target) as HTMLElement | null | undefined);
|
||||||
if (el) {
|
if (el) {
|
||||||
const rect = el.getBoundingClientRect();
|
const rect = el.getBoundingClientRect();
|
||||||
const x = rect.left + (el.offsetWidth / 2);
|
const x = rect.left + el.offsetWidth / 2;
|
||||||
const y = rect.top + (el.offsetHeight / 2);
|
const y = rect.top + el.offsetHeight / 2;
|
||||||
os.popup(Ripple, { x, y }, {}, 'end');
|
os.popup(Ripple, { x, y }, {}, "end");
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = getKey(emoji);
|
const key = getKey(emoji);
|
||||||
emit('chosen', key);
|
emit("chosen", key);
|
||||||
|
|
||||||
// 最近使った絵文字更新
|
// 最近使った絵文字更新
|
||||||
if (!pinned.value.includes(key)) {
|
if (!pinned.value.includes(key)) {
|
||||||
let recents = defaultStore.state.recentlyUsedEmojis;
|
let recents = defaultStore.state.recentlyUsedEmojis;
|
||||||
recents = recents.filter((emoji: any) => emoji !== key);
|
recents = recents.filter((emoji: any) => emoji !== key);
|
||||||
recents.unshift(key);
|
recents.unshift(key);
|
||||||
defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32));
|
defaultStore.set("recentlyUsedEmojis", recents.splice(0, 32));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function paste(event: ClipboardEvent) {
|
function paste(event: ClipboardEvent) {
|
||||||
const paste = (event.clipboardData || window.clipboardData).getData('text');
|
const paste = (event.clipboardData || window.clipboardData).getData("text");
|
||||||
if (done(paste)) {
|
if (done(paste)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
@ -314,15 +428,17 @@ function paste(event: ClipboardEvent) {
|
||||||
|
|
||||||
function done(query?: any): boolean | void {
|
function done(query?: any): boolean | void {
|
||||||
if (query == null) query = q.value;
|
if (query == null) query = q.value;
|
||||||
if (query == null || typeof query !== 'string') return;
|
if (query == null || typeof query !== "string") return;
|
||||||
|
|
||||||
const q2 = query.replaceAll(':', '');
|
const q2 = query.replaceAll(":", "");
|
||||||
const exactMatchCustom = customEmojis.find(emoji => emoji.name === q2);
|
const exactMatchCustom = customEmojis.find((emoji) => emoji.name === q2);
|
||||||
if (exactMatchCustom) {
|
if (exactMatchCustom) {
|
||||||
chosen(exactMatchCustom);
|
chosen(exactMatchCustom);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const exactMatchUnicode = emojilist.find(emoji => emoji.char === q2 || emoji.name === q2);
|
const exactMatchUnicode = emojilist.find(
|
||||||
|
(emoji) => emoji.char === q2 || emoji.name === q2
|
||||||
|
);
|
||||||
if (exactMatchUnicode) {
|
if (exactMatchUnicode) {
|
||||||
chosen(exactMatchUnicode);
|
chosen(exactMatchUnicode);
|
||||||
return true;
|
return true;
|
||||||
|
@ -543,7 +659,7 @@ defineExpose({
|
||||||
|
|
||||||
> .emoji {
|
> .emoji {
|
||||||
height: 1.25em;
|
height: 1.25em;
|
||||||
vertical-align: -.25em;
|
vertical-align: -0.25em;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,12 @@
|
||||||
ref="modal"
|
ref="modal"
|
||||||
v-slot="{ type, maxHeight }"
|
v-slot="{ type, maxHeight }"
|
||||||
:z-priority="'middle'"
|
:z-priority="'middle'"
|
||||||
:prefer-type="asReactionPicker && defaultStore.state.reactionPickerUseDrawerForMobile === false ? 'popup' : 'auto'"
|
:prefer-type="
|
||||||
|
asReactionPicker &&
|
||||||
|
defaultStore.state.reactionPickerUseDrawerForMobile === false
|
||||||
|
? 'popup'
|
||||||
|
: 'auto'
|
||||||
|
"
|
||||||
:transparent-bg="true"
|
:transparent-bg="true"
|
||||||
:manual-showing="manualShowing"
|
:manual-showing="manualShowing"
|
||||||
:src="src"
|
:src="src"
|
||||||
|
@ -26,33 +31,36 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from "vue";
|
||||||
import MkModal from '@/components/MkModal.vue';
|
import MkModal from "@/components/MkModal.vue";
|
||||||
import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
|
import MkEmojiPicker from "@/components/MkEmojiPicker.vue";
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from "@/store";
|
||||||
|
|
||||||
withDefaults(defineProps<{
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
manualShowing?: boolean | null;
|
manualShowing?: boolean | null;
|
||||||
src?: HTMLElement;
|
src?: HTMLElement;
|
||||||
showPinned?: boolean;
|
showPinned?: boolean;
|
||||||
asReactionPicker?: boolean;
|
asReactionPicker?: boolean;
|
||||||
}>(), {
|
}>(),
|
||||||
|
{
|
||||||
manualShowing: null,
|
manualShowing: null,
|
||||||
showPinned: true,
|
showPinned: true,
|
||||||
asReactionPicker: false,
|
asReactionPicker: false,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'done', v: any): void;
|
(ev: "done", v: any): void;
|
||||||
(ev: 'close'): void;
|
(ev: "close"): void;
|
||||||
(ev: 'closed'): void;
|
(ev: "closed"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const modal = ref<InstanceType<typeof MkModal>>();
|
const modal = ref<InstanceType<typeof MkModal>>();
|
||||||
const picker = ref<InstanceType<typeof MkEmojiPicker>>();
|
const picker = ref<InstanceType<typeof MkEmojiPicker>>();
|
||||||
|
|
||||||
function chosen(emoji: any) {
|
function chosen(emoji: any) {
|
||||||
emit('done', emoji);
|
emit("done", emoji);
|
||||||
modal.value?.close();
|
modal.value?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,19 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="meta" class="xfbouadm" :style="{ backgroundImage: `url(${ meta.backgroundImageUrl })` }"></div>
|
<div
|
||||||
|
v-if="meta"
|
||||||
|
class="xfbouadm"
|
||||||
|
:style="{ backgroundImage: `url(${meta.backgroundImageUrl})` }"
|
||||||
|
></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from "vue";
|
||||||
import * as Misskey from 'calckey-js';
|
import * as Misskey from "calckey-js";
|
||||||
import * as os from '@/os';
|
import * as os from "@/os";
|
||||||
|
|
||||||
const meta = ref<Misskey.entities.DetailedInstanceMetadata>();
|
const meta = ref<Misskey.entities.DetailedInstanceMetadata>();
|
||||||
|
|
||||||
os.api('meta', { detail: true }).then(gotMeta => {
|
os.api("meta", { detail: true }).then((gotMeta) => {
|
||||||
meta.value = gotMeta;
|
meta.value = gotMeta;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,29 +1,49 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<MkPagination v-slot="{items}" :pagination="pagination" class="urempief" :class="{ grid: viewMode === 'grid' }">
|
<MkPagination
|
||||||
|
v-slot="{ items }"
|
||||||
|
:pagination="pagination"
|
||||||
|
class="urempief"
|
||||||
|
:class="{ grid: viewMode === 'grid' }"
|
||||||
|
>
|
||||||
<MkA
|
<MkA
|
||||||
v-for="file in items"
|
v-for="file in items"
|
||||||
:key="file.id"
|
:key="file.id"
|
||||||
v-tooltip.mfm="`${file.type}\n${bytes(file.size)}\n${new Date(file.createdAt).toLocaleString()}\nby ${file.user ? '@' + Acct.toString(file.user) : 'system'}`"
|
v-tooltip.mfm="
|
||||||
|
`${file.type}\n${bytes(file.size)}\n${new Date(
|
||||||
|
file.createdAt
|
||||||
|
).toLocaleString()}\nby ${
|
||||||
|
file.user ? '@' + Acct.toString(file.user) : 'system'
|
||||||
|
}`
|
||||||
|
"
|
||||||
:to="`/admin/file/${file.id}`"
|
:to="`/admin/file/${file.id}`"
|
||||||
class="file _button"
|
class="file _button"
|
||||||
>
|
>
|
||||||
<div v-if="file.isSensitive" class="sensitive-label">{{ i18n.ts.sensitive }}</div>
|
<div v-if="file.isSensitive" class="sensitive-label">
|
||||||
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
|
{{ i18n.ts.sensitive }}
|
||||||
|
</div>
|
||||||
|
<MkDriveFileThumbnail
|
||||||
|
class="thumbnail"
|
||||||
|
:file="file"
|
||||||
|
fit="contain"
|
||||||
|
/>
|
||||||
<div v-if="viewMode === 'list'" class="body">
|
<div v-if="viewMode === 'list'" class="body">
|
||||||
<div>
|
<div>
|
||||||
<small style="opacity: 0.7;">{{ file.name }}</small>
|
<small style="opacity: 0.7">{{ file.name }}</small>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<MkAcct v-if="file.user" :user="file.user" />
|
<MkAcct v-if="file.user" :user="file.user" />
|
||||||
<div v-else>{{ i18n.ts.system }}</div>
|
<div v-else>{{ i18n.ts.system }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span style="margin-right: 1em;">{{ file.type }}</span>
|
<span style="margin-right: 1em">{{ file.type }}</span>
|
||||||
<span>{{ bytes(file.size) }}</span>
|
<span>{{ bytes(file.size) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span>{{ i18n.ts.registeredDate }}: <MkTime :time="file.createdAt" mode="detail"/></span>
|
<span
|
||||||
|
>{{ i18n.ts.registeredDate }}:
|
||||||
|
<MkTime :time="file.createdAt" mode="detail"
|
||||||
|
/></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</MkA>
|
</MkA>
|
||||||
|
@ -32,25 +52,29 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from "vue";
|
||||||
import * as Acct from 'calckey-js/built/acct';
|
import * as Acct from "calckey-js/built/acct";
|
||||||
import MkSwitch from '@/components/ui/switch.vue';
|
import MkSwitch from "@/components/ui/switch.vue";
|
||||||
import MkPagination from '@/components/MkPagination.vue';
|
import MkPagination from "@/components/MkPagination.vue";
|
||||||
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
|
import MkDriveFileThumbnail from "@/components/MkDriveFileThumbnail.vue";
|
||||||
import bytes from '@/filters/bytes';
|
import bytes from "@/filters/bytes";
|
||||||
import * as os from '@/os';
|
import * as os from "@/os";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
pagination: any;
|
pagination: any;
|
||||||
viewMode: 'grid' | 'list';
|
viewMode: "grid" | "list";
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@keyframes sensitive-blink {
|
@keyframes sensitive-blink {
|
||||||
0% { opacity: 1; }
|
0% {
|
||||||
50% { opacity: 0; }
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.urempief {
|
.urempief {
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
<template>
|
<template>
|
||||||
<span class="mk-file-type-icon">
|
<span class="mk-file-type-icon">
|
||||||
<template v-if="kind == 'image'"><i class="ph-file-image ph-bold ph-lg"></i></template>
|
<template v-if="kind == 'image'"
|
||||||
|
><i class="ph-file-image ph-bold ph-lg"></i
|
||||||
|
></template>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from "vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
type: string;
|
type: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const kind = computed(() => props.type.split('/')[0]);
|
const kind = computed(() => props.type.split("/")[0]);
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,11 +1,19 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-size="{ max: [500] }" class="ssazuxis">
|
<div v-size="{ max: [500] }" class="ssazuxis">
|
||||||
<header class="_button" :style="{ background: bg }" @click="showBody = !showBody">
|
<header
|
||||||
|
class="_button"
|
||||||
|
:style="{ background: bg }"
|
||||||
|
@click="showBody = !showBody"
|
||||||
|
>
|
||||||
<div class="title"><slot name="header"></slot></div>
|
<div class="title"><slot name="header"></slot></div>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<button class="_button">
|
<button class="_button">
|
||||||
<template v-if="showBody"><i class="ph-caret-up ph-bold ph-lg"></i></template>
|
<template v-if="showBody"
|
||||||
<template v-else><i class="ph-caret-down ph-bold ph-lg"></i></template>
|
><i class="ph-caret-up ph-bold ph-lg"></i
|
||||||
|
></template>
|
||||||
|
<template v-else
|
||||||
|
><i class="ph-caret-down ph-bold ph-lg"></i
|
||||||
|
></template>
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
<transition
|
<transition
|
||||||
|
@ -23,11 +31,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from "vue";
|
||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from "tinycolor2";
|
||||||
|
|
||||||
const localStoragePrefix = 'ui:folder:';
|
|
||||||
|
|
||||||
|
const localStoragePrefix = "ui:folder:";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
|
@ -45,19 +52,28 @@ export default defineComponent({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
bg: null,
|
bg: null,
|
||||||
showBody: (this.persistKey && localStorage.getItem(localStoragePrefix + this.persistKey)) ? localStorage.getItem(localStoragePrefix + this.persistKey) === 't' : this.expanded,
|
showBody:
|
||||||
|
this.persistKey &&
|
||||||
|
localStorage.getItem(localStoragePrefix + this.persistKey)
|
||||||
|
? localStorage.getItem(
|
||||||
|
localStoragePrefix + this.persistKey
|
||||||
|
) === "t"
|
||||||
|
: this.expanded,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
showBody() {
|
showBody() {
|
||||||
if (this.persistKey) {
|
if (this.persistKey) {
|
||||||
localStorage.setItem(localStoragePrefix + this.persistKey, this.showBody ? 't' : 'f');
|
localStorage.setItem(
|
||||||
|
localStoragePrefix + this.persistKey,
|
||||||
|
this.showBody ? "t" : "f"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
function getParentBg(el: Element | null): string {
|
function getParentBg(el: Element | null): string {
|
||||||
if (el == null || el.tagName === 'BODY') return 'var(--bg)';
|
if (el == null || el.tagName === "BODY") return "var(--bg)";
|
||||||
const bg = el.style.background || el.style.backgroundColor;
|
const bg = el.style.background || el.style.backgroundColor;
|
||||||
if (bg) {
|
if (bg) {
|
||||||
return bg;
|
return bg;
|
||||||
|
@ -66,7 +82,13 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const rawBg = getParentBg(this.$el);
|
const rawBg = getParentBg(this.$el);
|
||||||
const bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
|
const bg = tinycolor(
|
||||||
|
rawBg.startsWith("var(")
|
||||||
|
? getComputedStyle(document.documentElement).getPropertyValue(
|
||||||
|
rawBg.slice(4, -1)
|
||||||
|
)
|
||||||
|
: rawBg
|
||||||
|
);
|
||||||
bg.setAlpha(0.85);
|
bg.setAlpha(0.85);
|
||||||
this.bg = bg.toRgbString();
|
this.bg = bg.toRgbString();
|
||||||
},
|
},
|
||||||
|
@ -79,14 +101,14 @@ export default defineComponent({
|
||||||
const elementHeight = el.getBoundingClientRect().height;
|
const elementHeight = el.getBoundingClientRect().height;
|
||||||
el.style.height = 0;
|
el.style.height = 0;
|
||||||
el.offsetHeight; // reflow
|
el.offsetHeight; // reflow
|
||||||
el.style.height = elementHeight + 'px';
|
el.style.height = elementHeight + "px";
|
||||||
},
|
},
|
||||||
afterEnter(el) {
|
afterEnter(el) {
|
||||||
el.style.height = null;
|
el.style.height = null;
|
||||||
},
|
},
|
||||||
leave(el) {
|
leave(el) {
|
||||||
const elementHeight = el.getBoundingClientRect().height;
|
const elementHeight = el.getBoundingClientRect().height;
|
||||||
el.style.height = elementHeight + 'px';
|
el.style.height = elementHeight + "px";
|
||||||
el.offsetHeight; // reflow
|
el.offsetHeight; // reflow
|
||||||
el.style.height = 0;
|
el.style.height = 0;
|
||||||
},
|
},
|
||||||
|
@ -98,7 +120,8 @@ export default defineComponent({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.folder-toggle-enter-active, .folder-toggle-leave-active {
|
.folder-toggle-enter-active,
|
||||||
|
.folder-toggle-leave-active {
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
transition: opacity 0.5s, height 0.5s !important;
|
transition: opacity 0.5s, height 0.5s !important;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,67 +6,82 @@
|
||||||
active: isFollowing || hasPendingFollowRequestFromYou,
|
active: isFollowing || hasPendingFollowRequestFromYou,
|
||||||
full,
|
full,
|
||||||
large,
|
large,
|
||||||
blocking: isBlocking
|
blocking: isBlocking,
|
||||||
}"
|
}"
|
||||||
:disabled="wait"
|
:disabled="wait"
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
>
|
>
|
||||||
<template v-if="!wait">
|
<template v-if="!wait">
|
||||||
<template v-if="isBlocking">
|
<template v-if="isBlocking">
|
||||||
<span v-if="full">{{ i18n.ts.blocked }}</span><i class="ph-prohibit ph-bold ph-lg"></i>
|
<span v-if="full">{{ i18n.ts.blocked }}</span
|
||||||
|
><i class="ph-prohibit ph-bold ph-lg"></i>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="hasPendingFollowRequestFromYou && user.isLocked">
|
<template
|
||||||
<span v-if="full">{{ i18n.ts.followRequestPending }}</span><i class="ph-hourglass-medium ph-bold ph-lg"></i>
|
v-else-if="hasPendingFollowRequestFromYou && user.isLocked"
|
||||||
|
>
|
||||||
|
<span v-if="full">{{ i18n.ts.followRequestPending }}</span
|
||||||
|
><i class="ph-hourglass-medium ph-bold ph-lg"></i>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked">
|
<template
|
||||||
|
v-else-if="hasPendingFollowRequestFromYou && !user.isLocked"
|
||||||
|
>
|
||||||
<!-- つまりリモートフォローの場合。 -->
|
<!-- つまりリモートフォローの場合。 -->
|
||||||
<span v-if="full">{{ i18n.ts.processing }}</span><i class="ph-circle-notch ph-bold ph-lg fa-pulse"></i>
|
<span v-if="full">{{ i18n.ts.processing }}</span
|
||||||
|
><i class="ph-circle-notch ph-bold ph-lg fa-pulse"></i>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="isFollowing">
|
<template v-else-if="isFollowing">
|
||||||
<span v-if="full">{{ i18n.ts.unfollow }}</span><i class="ph-minus ph-bold ph-lg"></i>
|
<span v-if="full">{{ i18n.ts.unfollow }}</span
|
||||||
|
><i class="ph-minus ph-bold ph-lg"></i>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="!isFollowing && user.isLocked">
|
<template v-else-if="!isFollowing && user.isLocked">
|
||||||
<span v-if="full">{{ i18n.ts.followRequest }}</span><i class="ph-plus ph-bold ph-lg"></i>
|
<span v-if="full">{{ i18n.ts.followRequest }}</span
|
||||||
|
><i class="ph-plus ph-bold ph-lg"></i>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="!isFollowing && !user.isLocked">
|
<template v-else-if="!isFollowing && !user.isLocked">
|
||||||
<span v-if="full">{{ i18n.ts.follow }}</span><i class="ph-plus ph-bold ph-lg"></i>
|
<span v-if="full">{{ i18n.ts.follow }}</span
|
||||||
|
><i class="ph-plus ph-bold ph-lg"></i>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<span v-if="full">{{ i18n.ts.processing }}</span><i class="ph-circle-notch ph-bold ph-lg fa-pulse ph-fw ph-lg"></i>
|
<span v-if="full">{{ i18n.ts.processing }}</span
|
||||||
|
><i class="ph-circle-notch ph-bold ph-lg fa-pulse ph-fw ph-lg"></i>
|
||||||
</template>
|
</template>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onBeforeUnmount, onMounted } from 'vue';
|
import { computed, onBeforeUnmount, onMounted } from "vue";
|
||||||
import type * as Misskey from 'calckey-js';
|
import type * as Misskey from "calckey-js";
|
||||||
import * as os from '@/os';
|
import * as os from "@/os";
|
||||||
import { stream } from '@/stream';
|
import { stream } from "@/stream";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
const emit = defineEmits(['refresh'])
|
const emit = defineEmits(["refresh"]);
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(
|
||||||
user: Misskey.entities.UserDetailed,
|
defineProps<{
|
||||||
full?: boolean,
|
user: Misskey.entities.UserDetailed;
|
||||||
large?: boolean,
|
full?: boolean;
|
||||||
}>(), {
|
large?: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
full: false,
|
full: false,
|
||||||
large: false,
|
large: false,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const isBlocking = computed(() => props.user.isBlocking);
|
const isBlocking = computed(() => props.user.isBlocking);
|
||||||
|
|
||||||
let isFollowing = $ref(props.user.isFollowing);
|
let isFollowing = $ref(props.user.isFollowing);
|
||||||
let hasPendingFollowRequestFromYou = $ref(props.user.hasPendingFollowRequestFromYou);
|
let hasPendingFollowRequestFromYou = $ref(
|
||||||
|
props.user.hasPendingFollowRequestFromYou
|
||||||
|
);
|
||||||
let wait = $ref(false);
|
let wait = $ref(false);
|
||||||
const connection = stream.useChannel('main');
|
const connection = stream.useChannel("main");
|
||||||
|
|
||||||
if (props.user.isFollowing == null) {
|
if (props.user.isFollowing == null) {
|
||||||
os.api('users/show', {
|
os.api("users/show", {
|
||||||
userId: props.user.id,
|
userId: props.user.id,
|
||||||
})
|
}).then(onFollowChange);
|
||||||
.then(onFollowChange);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onFollowChange(user: Misskey.entities.UserDetailed) {
|
function onFollowChange(user: Misskey.entities.UserDetailed) {
|
||||||
|
@ -82,40 +97,41 @@ async function onClick() {
|
||||||
try {
|
try {
|
||||||
if (isBlocking.value) {
|
if (isBlocking.value) {
|
||||||
const { canceled } = await os.confirm({
|
const { canceled } = await os.confirm({
|
||||||
type: 'warning',
|
type: "warning",
|
||||||
text: i18n.t('unblockConfirm'),
|
text: i18n.t("unblockConfirm"),
|
||||||
});
|
});
|
||||||
if (canceled) return
|
if (canceled) return;
|
||||||
|
|
||||||
await os.api("blocking/delete", {
|
await os.api("blocking/delete", {
|
||||||
userId: props.user.id,
|
userId: props.user.id,
|
||||||
})
|
});
|
||||||
if (props.user.isMuted) {
|
if (props.user.isMuted) {
|
||||||
await os.api("mute/delete", {
|
await os.api("mute/delete", {
|
||||||
userId: props.user.id,
|
userId: props.user.id,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
emit('refresh')
|
emit("refresh");
|
||||||
}
|
} else if (isFollowing) {
|
||||||
else if (isFollowing) {
|
|
||||||
const { canceled } = await os.confirm({
|
const { canceled } = await os.confirm({
|
||||||
type: 'warning',
|
type: "warning",
|
||||||
text: i18n.t('unfollowConfirm', { name: props.user.name || props.user.username }),
|
text: i18n.t("unfollowConfirm", {
|
||||||
|
name: props.user.name || props.user.username,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
|
|
||||||
await os.api('following/delete', {
|
await os.api("following/delete", {
|
||||||
userId: props.user.id,
|
userId: props.user.id,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (hasPendingFollowRequestFromYou) {
|
if (hasPendingFollowRequestFromYou) {
|
||||||
await os.api('following/requests/cancel', {
|
await os.api("following/requests/cancel", {
|
||||||
userId: props.user.id,
|
userId: props.user.id,
|
||||||
});
|
});
|
||||||
hasPendingFollowRequestFromYou = false;
|
hasPendingFollowRequestFromYou = false;
|
||||||
} else {
|
} else {
|
||||||
await os.api('following/create', {
|
await os.api("following/create", {
|
||||||
userId: props.user.id,
|
userId: props.user.id,
|
||||||
});
|
});
|
||||||
hasPendingFollowRequestFromYou = true;
|
hasPendingFollowRequestFromYou = true;
|
||||||
|
@ -129,8 +145,8 @@ async function onClick() {
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
connection.on('follow', onFollowChange);
|
connection.on("follow", onFollowChange);
|
||||||
connection.on('unfollow', onFollowChange);
|
connection.on("unfollow", onFollowChange);
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<XModalWindow ref="dialog"
|
<XModalWindow
|
||||||
|
ref="dialog"
|
||||||
:width="370"
|
:width="370"
|
||||||
:height="400"
|
:height="400"
|
||||||
@close="dialog.close()"
|
@close="dialog.close()"
|
||||||
|
@ -7,22 +8,51 @@
|
||||||
>
|
>
|
||||||
<template #header>{{ i18n.ts.forgotPassword }}</template>
|
<template #header>{{ i18n.ts.forgotPassword }}</template>
|
||||||
|
|
||||||
<form v-if="instance.enableEmail" class="bafeceda" @submit.prevent="onSubmit">
|
<form
|
||||||
|
v-if="instance.enableEmail"
|
||||||
|
class="bafeceda"
|
||||||
|
@submit.prevent="onSubmit"
|
||||||
|
>
|
||||||
<div class="main _formRoot">
|
<div class="main _formRoot">
|
||||||
<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autofocus required>
|
<MkInput
|
||||||
|
v-model="username"
|
||||||
|
class="_formBlock"
|
||||||
|
type="text"
|
||||||
|
pattern="^[a-zA-Z0-9_]+$"
|
||||||
|
:spellcheck="false"
|
||||||
|
autofocus
|
||||||
|
required
|
||||||
|
>
|
||||||
<template #label>{{ i18n.ts.username }}</template>
|
<template #label>{{ i18n.ts.username }}</template>
|
||||||
<template #prefix>@</template>
|
<template #prefix>@</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
<MkInput v-model="email" class="_formBlock" type="email" :spellcheck="false" required>
|
<MkInput
|
||||||
|
v-model="email"
|
||||||
|
class="_formBlock"
|
||||||
|
type="email"
|
||||||
|
:spellcheck="false"
|
||||||
|
required
|
||||||
|
>
|
||||||
<template #label>{{ i18n.ts.emailAddress }}</template>
|
<template #label>{{ i18n.ts.emailAddress }}</template>
|
||||||
<template #caption>{{ i18n.ts._forgotPassword.enterEmail }}</template>
|
<template #caption>{{
|
||||||
|
i18n.ts._forgotPassword.enterEmail
|
||||||
|
}}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
<MkButton class="_formBlock" type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ i18n.ts.send }}</MkButton>
|
<MkButton
|
||||||
|
class="_formBlock"
|
||||||
|
type="submit"
|
||||||
|
:disabled="processing"
|
||||||
|
primary
|
||||||
|
style="margin: 0 auto"
|
||||||
|
>{{ i18n.ts.send }}</MkButton
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="sub">
|
<div class="sub">
|
||||||
<MkA to="/about" class="_link">{{ i18n.ts._forgotPassword.ifNoEmail }}</MkA>
|
<MkA to="/about" class="_link">{{
|
||||||
|
i18n.ts._forgotPassword.ifNoEmail
|
||||||
|
}}</MkA>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div v-else class="bafecedb">
|
<div v-else class="bafecedb">
|
||||||
|
@ -32,32 +62,32 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import {} from "vue";
|
||||||
import XModalWindow from '@/components/MkModalWindow.vue';
|
import XModalWindow from "@/components/MkModalWindow.vue";
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from "@/components/MkButton.vue";
|
||||||
import MkInput from '@/components/form/input.vue';
|
import MkInput from "@/components/form/input.vue";
|
||||||
import * as os from '@/os';
|
import * as os from "@/os";
|
||||||
import { instance } from '@/instance';
|
import { instance } from "@/instance";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'done'): void;
|
(ev: "done"): void;
|
||||||
(ev: 'closed'): void;
|
(ev: "closed"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let dialog: InstanceType<typeof XModalWindow> = $ref();
|
let dialog: InstanceType<typeof XModalWindow> = $ref();
|
||||||
|
|
||||||
let username = $ref('');
|
let username = $ref("");
|
||||||
let email = $ref('');
|
let email = $ref("");
|
||||||
let processing = $ref(false);
|
let processing = $ref(false);
|
||||||
|
|
||||||
async function onSubmit() {
|
async function onSubmit() {
|
||||||
processing = true;
|
processing = true;
|
||||||
await os.apiWithDialog('request-reset-password', {
|
await os.apiWithDialog("request-reset-password", {
|
||||||
username,
|
username,
|
||||||
email,
|
email,
|
||||||
});
|
});
|
||||||
emit('done');
|
emit("done");
|
||||||
dialog.close();
|
dialog.close();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -16,36 +16,136 @@
|
||||||
|
|
||||||
<MkSpacer :margin-min="20" :margin-max="32">
|
<MkSpacer :margin-min="20" :margin-max="32">
|
||||||
<div class="xkpnjxcv _formRoot">
|
<div class="xkpnjxcv _formRoot">
|
||||||
<template v-for="item in Object.keys(form).filter(item => !form[item].hidden)">
|
<template
|
||||||
<FormInput v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1" class="_formBlock">
|
v-for="item in Object.keys(form).filter(
|
||||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
|
(item) => !form[item].hidden
|
||||||
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
|
)"
|
||||||
|
>
|
||||||
|
<FormInput
|
||||||
|
v-if="form[item].type === 'number'"
|
||||||
|
v-model="values[item]"
|
||||||
|
type="number"
|
||||||
|
:step="form[item].step || 1"
|
||||||
|
class="_formBlock"
|
||||||
|
>
|
||||||
|
<template #label
|
||||||
|
><span v-text="form[item].label || item"></span
|
||||||
|
><span v-if="form[item].required === false">
|
||||||
|
({{ i18n.ts.optional }})</span
|
||||||
|
></template
|
||||||
|
>
|
||||||
|
<template v-if="form[item].description" #caption>{{
|
||||||
|
form[item].description
|
||||||
|
}}</template>
|
||||||
</FormInput>
|
</FormInput>
|
||||||
<FormInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model="values[item]" type="text" class="_formBlock">
|
<FormInput
|
||||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
|
v-else-if="
|
||||||
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
|
form[item].type === 'string' &&
|
||||||
|
!form[item].multiline
|
||||||
|
"
|
||||||
|
v-model="values[item]"
|
||||||
|
type="text"
|
||||||
|
class="_formBlock"
|
||||||
|
>
|
||||||
|
<template #label
|
||||||
|
><span v-text="form[item].label || item"></span
|
||||||
|
><span v-if="form[item].required === false">
|
||||||
|
({{ i18n.ts.optional }})</span
|
||||||
|
></template
|
||||||
|
>
|
||||||
|
<template v-if="form[item].description" #caption>{{
|
||||||
|
form[item].description
|
||||||
|
}}</template>
|
||||||
</FormInput>
|
</FormInput>
|
||||||
<FormTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model="values[item]" class="_formBlock">
|
<FormTextarea
|
||||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
|
v-else-if="
|
||||||
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
|
form[item].type === 'string' && form[item].multiline
|
||||||
|
"
|
||||||
|
v-model="values[item]"
|
||||||
|
class="_formBlock"
|
||||||
|
>
|
||||||
|
<template #label
|
||||||
|
><span v-text="form[item].label || item"></span
|
||||||
|
><span v-if="form[item].required === false">
|
||||||
|
({{ i18n.ts.optional }})</span
|
||||||
|
></template
|
||||||
|
>
|
||||||
|
<template v-if="form[item].description" #caption>{{
|
||||||
|
form[item].description
|
||||||
|
}}</template>
|
||||||
</FormTextarea>
|
</FormTextarea>
|
||||||
<FormSwitch v-else-if="form[item].type === 'boolean'" v-model="values[item]" class="_formBlock">
|
<FormSwitch
|
||||||
|
v-else-if="form[item].type === 'boolean'"
|
||||||
|
v-model="values[item]"
|
||||||
|
class="_formBlock"
|
||||||
|
>
|
||||||
<span v-text="form[item].label || item"></span>
|
<span v-text="form[item].label || item"></span>
|
||||||
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
|
<template v-if="form[item].description" #caption>{{
|
||||||
|
form[item].description
|
||||||
|
}}</template>
|
||||||
</FormSwitch>
|
</FormSwitch>
|
||||||
<FormSelect v-else-if="form[item].type === 'enum'" v-model="values[item]" class="_formBlock">
|
<FormSelect
|
||||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
|
v-else-if="form[item].type === 'enum'"
|
||||||
<option v-for="item in form[item].enum" :key="item.value" :value="item.value">{{ item.label }}</option>
|
v-model="values[item]"
|
||||||
|
class="_formBlock"
|
||||||
|
>
|
||||||
|
<template #label
|
||||||
|
><span v-text="form[item].label || item"></span
|
||||||
|
><span v-if="form[item].required === false">
|
||||||
|
({{ i18n.ts.optional }})</span
|
||||||
|
></template
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="item in form[item].enum"
|
||||||
|
:key="item.value"
|
||||||
|
:value="item.value"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</option>
|
||||||
</FormSelect>
|
</FormSelect>
|
||||||
<FormRadios v-else-if="form[item].type === 'radio'" v-model="values[item]" class="_formBlock">
|
<FormRadios
|
||||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
|
v-else-if="form[item].type === 'radio'"
|
||||||
<option v-for="item in form[item].options" :key="item.value" :value="item.value">{{ item.label }}</option>
|
v-model="values[item]"
|
||||||
|
class="_formBlock"
|
||||||
|
>
|
||||||
|
<template #label
|
||||||
|
><span v-text="form[item].label || item"></span
|
||||||
|
><span v-if="form[item].required === false">
|
||||||
|
({{ i18n.ts.optional }})</span
|
||||||
|
></template
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="item in form[item].options"
|
||||||
|
:key="item.value"
|
||||||
|
:value="item.value"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</option>
|
||||||
</FormRadios>
|
</FormRadios>
|
||||||
<FormRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].min" :max="form[item].max" :step="form[item].step" :text-converter="form[item].textConverter" class="_formBlock">
|
<FormRange
|
||||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
|
v-else-if="form[item].type === 'range'"
|
||||||
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
|
v-model="values[item]"
|
||||||
|
:min="form[item].min"
|
||||||
|
:max="form[item].max"
|
||||||
|
:step="form[item].step"
|
||||||
|
:text-converter="form[item].textConverter"
|
||||||
|
class="_formBlock"
|
||||||
|
>
|
||||||
|
<template #label
|
||||||
|
><span v-text="form[item].label || item"></span
|
||||||
|
><span v-if="form[item].required === false">
|
||||||
|
({{ i18n.ts.optional }})</span
|
||||||
|
></template
|
||||||
|
>
|
||||||
|
<template v-if="form[item].description" #caption>{{
|
||||||
|
form[item].description
|
||||||
|
}}</template>
|
||||||
</FormRange>
|
</FormRange>
|
||||||
<MkButton v-else-if="form[item].type === 'button'" class="_formBlock" @click="form[item].action($event, values)">
|
<MkButton
|
||||||
|
v-else-if="form[item].type === 'button'"
|
||||||
|
class="_formBlock"
|
||||||
|
@click="form[item].action($event, values)"
|
||||||
|
>
|
||||||
<span v-text="form[item].content || item"></span>
|
<span v-text="form[item].content || item"></span>
|
||||||
</MkButton>
|
</MkButton>
|
||||||
</template>
|
</template>
|
||||||
|
@ -55,16 +155,16 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from "vue";
|
||||||
import FormInput from './form/input.vue';
|
import FormInput from "./form/input.vue";
|
||||||
import FormTextarea from './form/textarea.vue';
|
import FormTextarea from "./form/textarea.vue";
|
||||||
import FormSwitch from './form/switch.vue';
|
import FormSwitch from "./form/switch.vue";
|
||||||
import FormSelect from './form/select.vue';
|
import FormSelect from "./form/select.vue";
|
||||||
import FormRange from './form/range.vue';
|
import FormRange from "./form/range.vue";
|
||||||
import MkButton from './MkButton.vue';
|
import MkButton from "./MkButton.vue";
|
||||||
import FormRadios from './form/radios.vue';
|
import FormRadios from "./form/radios.vue";
|
||||||
import XModalWindow from '@/components/MkModalWindow.vue';
|
import XModalWindow from "@/components/MkModalWindow.vue";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
|
@ -89,7 +189,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
emits: ['done'],
|
emits: ["done"],
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -106,14 +206,14 @@ export default defineComponent({
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
ok() {
|
ok() {
|
||||||
this.$emit('done', {
|
this.$emit("done", {
|
||||||
result: this.values,
|
result: this.values,
|
||||||
});
|
});
|
||||||
this.$refs.dialog.close();
|
this.$refs.dialog.close();
|
||||||
},
|
},
|
||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
this.$emit('done', {
|
this.$emit("done", {
|
||||||
canceled: true,
|
canceled: true,
|
||||||
});
|
});
|
||||||
this.$refs.dialog.close();
|
this.$refs.dialog.close();
|
||||||
|
@ -124,6 +224,5 @@ export default defineComponent({
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.xkpnjxcv {
|
.xkpnjxcv {
|
||||||
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -3,12 +3,14 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, defineAsyncComponent } from 'vue';
|
import { defineComponent, defineAsyncComponent } from "vue";
|
||||||
import * as os from '@/os';
|
import * as os from "@/os";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
XFormula: defineAsyncComponent(() => import('@/components/MkFormulaCore.vue')),
|
XFormula: defineAsyncComponent(
|
||||||
|
() => import("@/components/MkFormulaCore.vue")
|
||||||
|
),
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
formula: {
|
formula: {
|
||||||
|
|
|
@ -4,27 +4,27 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from "vue";
|
||||||
import katex from 'katex';
|
import katex from "katex";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
formula: {
|
formula: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
block: {
|
block: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true
|
required: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
compiledFormula(): any {
|
compiledFormula(): any {
|
||||||
return katex.renderToString(this.formula, {
|
return katex.renderToString(this.formula, {
|
||||||
throwOnError: false
|
throwOnError: false,
|
||||||
} as any);
|
} as any);
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<MkA :to="`/gallery/${post.id}`" class="ttasepnz _panel" tabindex="-1">
|
<MkA :to="`/gallery/${post.id}`" class="ttasepnz _panel" tabindex="-1">
|
||||||
<div class="thumbnail">
|
<div class="thumbnail">
|
||||||
<ImgWithBlurhash class="img" :src="post.files[0].thumbnailUrl" :hash="post.files[0].blurhash"/>
|
<ImgWithBlurhash
|
||||||
|
class="img"
|
||||||
|
:src="post.files[0].thumbnailUrl"
|
||||||
|
:hash="post.files[0].blurhash"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<article>
|
<article>
|
||||||
<header>
|
<header>
|
||||||
|
@ -15,9 +19,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import {} from "vue";
|
||||||
import { userName } from '@/filters/user';
|
import { userName } from "@/filters/user";
|
||||||
import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
|
import ImgWithBlurhash from "@/components/MkImgWithBlurhash.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
post: any;
|
post: any;
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="mk-google">
|
<div class="mk-google">
|
||||||
<input v-model="query" type="search" :placeholder="q">
|
<input v-model="query" type="search" :placeholder="q" />
|
||||||
<button @click="search"><i class="ph-magnifying-glass ph-bold ph-lg"></i> {{ i18n.ts.searchByGoogle }}</button>
|
<button @click="search">
|
||||||
|
<i class="ph-magnifying-glass ph-bold ph-lg"></i>
|
||||||
|
{{ i18n.ts.searchByGoogle }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from "vue";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
q: string;
|
q: string;
|
||||||
|
@ -16,7 +19,10 @@ const props = defineProps<{
|
||||||
const query = ref(props.q);
|
const query = ref(props.q);
|
||||||
|
|
||||||
const search = () => {
|
const search = () => {
|
||||||
window.open(`https://search.annoyingorange.xyz/search?q=${query.value}`, '_blank');
|
window.open(
|
||||||
|
`https://search.annoyingorange.xyz/search?q=${query.value}`,
|
||||||
|
"_blank"
|
||||||
|
);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -8,16 +8,23 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
|
import {
|
||||||
import { Chart } from 'chart.js';
|
markRaw,
|
||||||
import tinycolor from 'tinycolor2';
|
version as vueVersion,
|
||||||
import { MatrixController, MatrixElement } from 'chartjs-chart-matrix';
|
onMounted,
|
||||||
import * as os from '@/os';
|
onBeforeUnmount,
|
||||||
import { defaultStore } from '@/store';
|
nextTick,
|
||||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
|
watch,
|
||||||
import { chartVLine } from '@/scripts/chart-vline';
|
} from "vue";
|
||||||
import { alpha } from '@/scripts/color';
|
import { Chart } from "chart.js";
|
||||||
import { initChart } from '@/scripts/init-chart';
|
import tinycolor from "tinycolor2";
|
||||||
|
import { MatrixController, MatrixElement } from "chartjs-chart-matrix";
|
||||||
|
import * as os from "@/os";
|
||||||
|
import { defaultStore } from "@/store";
|
||||||
|
import { useChartTooltip } from "@/scripts/use-chart-tooltip";
|
||||||
|
import { chartVLine } from "@/scripts/chart-vline";
|
||||||
|
import { alpha } from "@/scripts/color";
|
||||||
|
import { initChart } from "@/scripts/init-chart";
|
||||||
|
|
||||||
initChart();
|
initChart();
|
||||||
|
|
||||||
|
@ -32,7 +39,7 @@ let chartInstance: Chart = null;
|
||||||
let fetching = $ref(true);
|
let fetching = $ref(true);
|
||||||
|
|
||||||
const { handler: externalTooltipHandler } = useChartTooltip({
|
const { handler: externalTooltipHandler } = useChartTooltip({
|
||||||
position: 'middle',
|
position: "middle",
|
||||||
});
|
});
|
||||||
|
|
||||||
async function renderChart() {
|
async function renderChart() {
|
||||||
|
@ -57,7 +64,9 @@ async function renderChart() {
|
||||||
const format = (arr) => {
|
const format = (arr) => {
|
||||||
return arr.map((v, i) => {
|
return arr.map((v, i) => {
|
||||||
const dt = getDate(i);
|
const dt = getDate(i);
|
||||||
const iso = `${dt.getFullYear()}-${(dt.getMonth() + 1).toString().padStart(2, '0')}-${dt.getDate().toString().padStart(2, '0')}`;
|
const iso = `${dt.getFullYear()}-${(dt.getMonth() + 1)
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0")}-${dt.getDate().toString().padStart(2, "0")}`;
|
||||||
return {
|
return {
|
||||||
x: iso,
|
x: iso,
|
||||||
y: dt.getDay(),
|
y: dt.getDay(),
|
||||||
|
@ -69,20 +78,35 @@ async function renderChart() {
|
||||||
|
|
||||||
let values;
|
let values;
|
||||||
|
|
||||||
if (props.src === 'active-users') {
|
if (props.src === "active-users") {
|
||||||
const raw = await os.api('charts/active-users', { limit: chartLimit, span: 'day' });
|
const raw = await os.api("charts/active-users", {
|
||||||
|
limit: chartLimit,
|
||||||
|
span: "day",
|
||||||
|
});
|
||||||
values = raw.readWrite;
|
values = raw.readWrite;
|
||||||
} else if (props.src === 'notes') {
|
} else if (props.src === "notes") {
|
||||||
const raw = await os.api('charts/notes', { limit: chartLimit, span: 'day' });
|
const raw = await os.api("charts/notes", {
|
||||||
|
limit: chartLimit,
|
||||||
|
span: "day",
|
||||||
|
});
|
||||||
values = raw.local.inc;
|
values = raw.local.inc;
|
||||||
} else if (props.src === 'ap-requests-inbox-received') {
|
} else if (props.src === "ap-requests-inbox-received") {
|
||||||
const raw = await os.api('charts/ap-request', { limit: chartLimit, span: 'day' });
|
const raw = await os.api("charts/ap-request", {
|
||||||
|
limit: chartLimit,
|
||||||
|
span: "day",
|
||||||
|
});
|
||||||
values = raw.inboxReceived;
|
values = raw.inboxReceived;
|
||||||
} else if (props.src === 'ap-requests-deliver-succeeded') {
|
} else if (props.src === "ap-requests-deliver-succeeded") {
|
||||||
const raw = await os.api('charts/ap-request', { limit: chartLimit, span: 'day' });
|
const raw = await os.api("charts/ap-request", {
|
||||||
|
limit: chartLimit,
|
||||||
|
span: "day",
|
||||||
|
});
|
||||||
values = raw.deliverSucceeded;
|
values = raw.deliverSucceeded;
|
||||||
} else if (props.src === 'ap-requests-deliver-failed') {
|
} else if (props.src === "ap-requests-deliver-failed") {
|
||||||
const raw = await os.api('charts/ap-request', { limit: chartLimit, span: 'day' });
|
const raw = await os.api("charts/ap-request", {
|
||||||
|
limit: chartLimit,
|
||||||
|
span: "day",
|
||||||
|
});
|
||||||
values = raw.deliverFailed;
|
values = raw.deliverFailed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,29 +114,36 @@ async function renderChart() {
|
||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
const color = defaultStore.state.darkMode ? '#ebbcba' : '#d7827e';
|
const color = defaultStore.state.darkMode ? "#ebbcba" : "#d7827e";
|
||||||
|
|
||||||
// 視覚上の分かりやすさのため上から最も大きい3つの値の平均を最大値とする
|
// 視覚上の分かりやすさのため上から最も大きい3つの値の平均を最大値とする
|
||||||
const max = values.slice().sort((a, b) => b - a).slice(0, 3).reduce((a, b) => a + b, 0) / 3;
|
const max =
|
||||||
|
values
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => b - a)
|
||||||
|
.slice(0, 3)
|
||||||
|
.reduce((a, b) => a + b, 0) / 3;
|
||||||
|
|
||||||
const min = Math.max(0, Math.min(...values) - 1);
|
const min = Math.max(0, Math.min(...values) - 1);
|
||||||
|
|
||||||
const marginEachCell = 4;
|
const marginEachCell = 4;
|
||||||
|
|
||||||
chartInstance = new Chart(chartEl, {
|
chartInstance = new Chart(chartEl, {
|
||||||
type: 'matrix',
|
type: "matrix",
|
||||||
data: {
|
data: {
|
||||||
datasets: [{
|
datasets: [
|
||||||
label: 'Read & Write',
|
{
|
||||||
|
label: "Read & Write",
|
||||||
data: format(values),
|
data: format(values),
|
||||||
pointRadius: 0,
|
pointRadius: 0,
|
||||||
borderWidth: 0,
|
borderWidth: 0,
|
||||||
borderJoinStyle: 'round',
|
borderJoinStyle: "round",
|
||||||
borderRadius: 3,
|
borderRadius: 3,
|
||||||
backgroundColor(c) {
|
backgroundColor(c) {
|
||||||
const value = c.dataset.data[c.dataIndex].v;
|
const value = c.dataset.data[c.dataIndex].v;
|
||||||
let a = (value - min) / max;
|
let a = (value - min) / max;
|
||||||
if (value !== 0) { // 0でない限りは完全に不可視にはしない
|
if (value !== 0) {
|
||||||
|
// 0でない限りは完全に不可視にはしない
|
||||||
a = Math.max(a, 0.05);
|
a = Math.max(a, 0.05);
|
||||||
}
|
}
|
||||||
return alpha(color, a);
|
return alpha(color, a);
|
||||||
|
@ -126,7 +157,8 @@ async function renderChart() {
|
||||||
const a = c.chart.chartArea ?? {};
|
const a = c.chart.chartArea ?? {};
|
||||||
return (a.bottom - a.top) / 7 - marginEachCell;
|
return (a.bottom - a.top) / 7 - marginEachCell;
|
||||||
},
|
},
|
||||||
}],
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
aspectRatio: wide ? 6 : narrow ? 1.8 : 3.2,
|
aspectRatio: wide ? 6 : narrow ? 1.8 : 3.2,
|
||||||
|
@ -140,17 +172,17 @@ async function renderChart() {
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
x: {
|
x: {
|
||||||
type: 'time',
|
type: "time",
|
||||||
offset: true,
|
offset: true,
|
||||||
position: 'bottom',
|
position: "bottom",
|
||||||
time: {
|
time: {
|
||||||
unit: 'week',
|
unit: "week",
|
||||||
round: 'week',
|
round: "week",
|
||||||
isoWeekday: 0,
|
isoWeekday: 0,
|
||||||
displayFormats: {
|
displayFormats: {
|
||||||
day: 'M/d',
|
day: "M/d",
|
||||||
month: 'Y/M',
|
month: "Y/M",
|
||||||
week: 'M/d',
|
week: "M/d",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
|
@ -165,7 +197,7 @@ async function renderChart() {
|
||||||
y: {
|
y: {
|
||||||
offset: true,
|
offset: true,
|
||||||
reverse: true,
|
reverse: true,
|
||||||
position: 'right',
|
position: "right",
|
||||||
grid: {
|
grid: {
|
||||||
display: false,
|
display: false,
|
||||||
},
|
},
|
||||||
|
@ -176,7 +208,8 @@ async function renderChart() {
|
||||||
font: {
|
font: {
|
||||||
size: 9,
|
size: 9,
|
||||||
},
|
},
|
||||||
callback: (value, index, values) => ['', 'Mon', '', 'Wed', '', 'Fri', ''][value],
|
callback: (value, index, values) =>
|
||||||
|
["", "Mon", "", "Wed", "", "Fri", ""][value],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -188,12 +221,13 @@ async function renderChart() {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
callbacks: {
|
callbacks: {
|
||||||
title(context) {
|
title(context) {
|
||||||
const v = context[0].dataset.data[context[0].dataIndex];
|
const v =
|
||||||
|
context[0].dataset.data[context[0].dataIndex];
|
||||||
return v.d;
|
return v.d;
|
||||||
},
|
},
|
||||||
label(context) {
|
label(context) {
|
||||||
const v = context.dataset.data[context.dataIndex];
|
const v = context.dataset.data[context.dataIndex];
|
||||||
return ['Active: ' + v.v];
|
return ["Active: " + v.v];
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
//mode: 'index',
|
//mode: 'index',
|
||||||
|
@ -207,10 +241,13 @@ async function renderChart() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(() => props.src, () => {
|
watch(
|
||||||
|
() => props.src,
|
||||||
|
() => {
|
||||||
fetching = true;
|
fetching = true;
|
||||||
renderChart();
|
renderChart();
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
renderChart();
|
renderChart();
|
||||||
|
|
|
@ -1,31 +1,46 @@
|
||||||
<template>
|
<template>
|
||||||
<MkModal ref="modal" :z-priority="'middle'" @click="modal.close()" @closed="emit('closed')">
|
<MkModal
|
||||||
|
ref="modal"
|
||||||
|
:z-priority="'middle'"
|
||||||
|
@click="modal.close()"
|
||||||
|
@closed="emit('closed')"
|
||||||
|
>
|
||||||
<div class="xubzgfga">
|
<div class="xubzgfga">
|
||||||
<header>{{ image.name }}</header>
|
<header>{{ image.name }}</header>
|
||||||
<img :src="image.url" :alt="image.comment" :title="image.comment" @click="modal.close()"/>
|
<img
|
||||||
|
:src="image.url"
|
||||||
|
:alt="image.comment"
|
||||||
|
:title="image.comment"
|
||||||
|
@click="modal.close()"
|
||||||
|
/>
|
||||||
<footer>
|
<footer>
|
||||||
<span>{{ image.type }}</span>
|
<span>{{ image.type }}</span>
|
||||||
<span>{{ bytes(image.size) }}</span>
|
<span>{{ bytes(image.size) }}</span>
|
||||||
<span v-if="image.properties && image.properties.width">{{ number(image.properties.width) }}px × {{ number(image.properties.height) }}px</span>
|
<span v-if="image.properties && image.properties.width"
|
||||||
|
>{{ number(image.properties.width) }}px ×
|
||||||
|
{{ number(image.properties.height) }}px</span
|
||||||
|
>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</MkModal>
|
</MkModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import {} from "vue";
|
||||||
import type * as misskey from 'calckey-js';
|
import type * as misskey from "calckey-js";
|
||||||
import bytes from '@/filters/bytes';
|
import bytes from "@/filters/bytes";
|
||||||
import number from '@/filters/number';
|
import number from "@/filters/number";
|
||||||
import MkModal from '@/components/MkModal.vue';
|
import MkModal from "@/components/MkModal.vue";
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
image: misskey.entities.DriveFile;
|
image: misskey.entities.DriveFile;
|
||||||
}>(), {
|
}>(),
|
||||||
});
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'closed'): void;
|
(ev: "closed"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const modal = $ref<InstanceType<typeof MkModal>>();
|
const modal = $ref<InstanceType<typeof MkModal>>();
|
||||||
|
|
|
@ -1,15 +1,29 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="xubzgfgb" :class="{ cover }" :title="title">
|
<div class="xubzgfgb" :class="{ cover }" :title="title">
|
||||||
<canvas v-if="!loaded" ref="canvas" :width="size" :height="size" :title="title"/>
|
<canvas
|
||||||
<img v-if="src" :src="src" :title="title" :type="type" :alt="alt" @load="onLoad"/>
|
v-if="!loaded"
|
||||||
|
ref="canvas"
|
||||||
|
:width="size"
|
||||||
|
:height="size"
|
||||||
|
:title="title"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
v-if="src"
|
||||||
|
:src="src"
|
||||||
|
:title="title"
|
||||||
|
:type="type"
|
||||||
|
:alt="alt"
|
||||||
|
@load="onLoad"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted } from 'vue';
|
import { onMounted } from "vue";
|
||||||
import { decode } from 'blurhash';
|
import { decode } from "blurhash";
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
src?: string | null;
|
src?: string | null;
|
||||||
hash?: string;
|
hash?: string;
|
||||||
alt?: string;
|
alt?: string;
|
||||||
|
@ -17,14 +31,16 @@ const props = withDefaults(defineProps<{
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
size?: number;
|
size?: number;
|
||||||
cover?: boolean;
|
cover?: boolean;
|
||||||
}>(), {
|
}>(),
|
||||||
|
{
|
||||||
src: null,
|
src: null,
|
||||||
type: null,
|
type: null,
|
||||||
alt: '',
|
alt: "",
|
||||||
title: null,
|
title: null,
|
||||||
size: 64,
|
size: 64,
|
||||||
cover: true,
|
cover: true,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const canvas = $ref<HTMLCanvasElement>();
|
const canvas = $ref<HTMLCanvasElement>();
|
||||||
let loaded = $ref(false);
|
let loaded = $ref(false);
|
||||||
|
@ -32,7 +48,7 @@ let loaded = $ref(false);
|
||||||
function draw() {
|
function draw() {
|
||||||
if (props.hash == null) return;
|
if (props.hash == null) return;
|
||||||
const pixels = decode(props.hash, props.size, props.size);
|
const pixels = decode(props.hash, props.size, props.size);
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext("2d");
|
||||||
const imageData = ctx!.createImageData(props.size, props.size);
|
const imageData = ctx!.createImageData(props.size, props.size);
|
||||||
imageData.data.set(pixels);
|
imageData.data.set(pixels);
|
||||||
ctx!.putImageData(imageData, 0, 0);
|
ctx!.putImageData(imageData, 0, 0);
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import {} from "vue";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
warn?: boolean;
|
warn?: boolean;
|
||||||
|
|
|
@ -1,19 +1,32 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class="[$style.root, { yellow: instance.isNotResponding, red: instance.isBlocked, gray: instance.isSuspended }]">
|
<div
|
||||||
|
:class="[
|
||||||
|
$style.root,
|
||||||
|
{
|
||||||
|
yellow: instance.isNotResponding,
|
||||||
|
red: instance.isBlocked,
|
||||||
|
gray: instance.isSuspended,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
<img class="icon" :src="getInstanceIcon(instance)" alt="" />
|
<img class="icon" :src="getInstanceIcon(instance)" alt="" />
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<span class="host">{{ instance.name ?? instance.host }}</span>
|
<span class="host">{{ instance.name ?? instance.host }}</span>
|
||||||
<span class="sub _monospace"><b>{{ instance.host }}</b> / {{ instance.softwareName || '?' }} {{ instance.softwareVersion }}</span>
|
<span class="sub _monospace"
|
||||||
|
><b>{{ instance.host }}</b> /
|
||||||
|
{{ instance.softwareName || "?" }}
|
||||||
|
{{ instance.softwareVersion }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<MkMiniChart v-if="chartValues" class="chart" :src="chartValues" />
|
<MkMiniChart v-if="chartValues" class="chart" :src="chartValues" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import * as misskey from 'calckey-js';
|
import * as misskey from "calckey-js";
|
||||||
import MkMiniChart from '@/components/MkMiniChart.vue';
|
import MkMiniChart from "@/components/MkMiniChart.vue";
|
||||||
import * as os from '@/os';
|
import * as os from "@/os";
|
||||||
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy';
|
import { getProxiedImageUrlNullable } from "@/scripts/media-proxy";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
instance: misskey.entities.Instance;
|
instance: misskey.entities.Instance;
|
||||||
|
@ -21,14 +34,22 @@ const props = defineProps<{
|
||||||
|
|
||||||
let chartValues = $ref<number[] | null>(null);
|
let chartValues = $ref<number[] | null>(null);
|
||||||
|
|
||||||
os.apiGet('charts/instance', { host: props.instance.host, limit: 16 + 1, span: 'day' }).then(res => {
|
os.apiGet("charts/instance", {
|
||||||
|
host: props.instance.host,
|
||||||
|
limit: 16 + 1,
|
||||||
|
span: "day",
|
||||||
|
}).then((res) => {
|
||||||
// 今日のぶんの値はまだ途中の値であり、それも含めると大抵の場合前日よりも下降しているようなグラフになってしまうため今日は弾く
|
// 今日のぶんの値はまだ途中の値であり、それも含めると大抵の場合前日よりも下降しているようなグラフになってしまうため今日は弾く
|
||||||
res.requests.received.splice(0, 1);
|
res.requests.received.splice(0, 1);
|
||||||
chartValues = res.requests.received;
|
chartValues = res.requests.received;
|
||||||
});
|
});
|
||||||
|
|
||||||
function getInstanceIcon(instance): string {
|
function getInstanceIcon(instance): string {
|
||||||
return getProxiedImageUrlNullable(instance.iconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.faviconUrl, 'preview') ?? '/client-assets/dummy.png';
|
return (
|
||||||
|
getProxiedImageUrlNullable(instance.iconUrl, "preview") ??
|
||||||
|
getProxiedImageUrlNullable(instance.faviconUrl, "preview") ??
|
||||||
|
"/client-assets/dummy.png"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -86,19 +107,46 @@ function getInstanceIcon(instance): string {
|
||||||
|
|
||||||
&:global(.yellow) {
|
&:global(.yellow) {
|
||||||
--c: rgb(255 196 0 / 15%);
|
--c: rgb(255 196 0 / 15%);
|
||||||
background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
|
background-image: linear-gradient(
|
||||||
|
45deg,
|
||||||
|
var(--c) 16.67%,
|
||||||
|
transparent 16.67%,
|
||||||
|
transparent 50%,
|
||||||
|
var(--c) 50%,
|
||||||
|
var(--c) 66.67%,
|
||||||
|
transparent 66.67%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
background-size: 16px 16px;
|
background-size: 16px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:global(.red) {
|
&:global(.red) {
|
||||||
--c: rgb(255 0 0 / 15%);
|
--c: rgb(255 0 0 / 15%);
|
||||||
background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
|
background-image: linear-gradient(
|
||||||
|
45deg,
|
||||||
|
var(--c) 16.67%,
|
||||||
|
transparent 16.67%,
|
||||||
|
transparent 50%,
|
||||||
|
var(--c) 50%,
|
||||||
|
var(--c) 66.67%,
|
||||||
|
transparent 66.67%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
background-size: 16px 16px;
|
background-size: 16px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:global(.gray) {
|
&:global(.gray) {
|
||||||
--c: var(--bg);
|
--c: var(--bg);
|
||||||
background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
|
background-image: linear-gradient(
|
||||||
|
45deg,
|
||||||
|
var(--c) 16.67%,
|
||||||
|
transparent 16.67%,
|
||||||
|
transparent 50%,
|
||||||
|
var(--c) 50%,
|
||||||
|
var(--c) 66.67%,
|
||||||
|
transparent 66.67%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
background-size: 16px 16px;
|
background-size: 16px 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,15 +11,38 @@
|
||||||
<template #header>{{ i18n.ts.selectInstance }}</template>
|
<template #header>{{ i18n.ts.selectInstance }}</template>
|
||||||
<div class="mehkoush">
|
<div class="mehkoush">
|
||||||
<div class="form">
|
<div class="form">
|
||||||
<MkInput v-model="hostname" :autofocus="true" @update:modelValue="search">
|
<MkInput
|
||||||
|
v-model="hostname"
|
||||||
|
:autofocus="true"
|
||||||
|
@update:modelValue="search"
|
||||||
|
>
|
||||||
<template #label>{{ i18n.ts.instance }}</template>
|
<template #label>{{ i18n.ts.instance }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="hostname != ''" class="result" :class="{ hit: instances.length > 0 }">
|
<div
|
||||||
|
v-if="hostname != ''"
|
||||||
|
class="result"
|
||||||
|
:class="{ hit: instances.length > 0 }"
|
||||||
|
>
|
||||||
<div v-if="instances.length > 0" class="instances">
|
<div v-if="instances.length > 0" class="instances">
|
||||||
<div v-for="item in instances" :key="item.id" class="instance" :class="{ selected: selected && selected.id === item.id }" @click="selected = item" @dblclick="ok()">
|
<div
|
||||||
|
v-for="item in instances"
|
||||||
|
:key="item.id"
|
||||||
|
class="instance"
|
||||||
|
:class="{
|
||||||
|
selected: selected && selected.id === item.id,
|
||||||
|
}"
|
||||||
|
@click="selected = item"
|
||||||
|
@dblclick="ok()"
|
||||||
|
>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<img class="icon" :src="item.iconUrl ?? '/client-assets/dummy.png'" aria-hidden="true"/>
|
<img
|
||||||
|
class="icon"
|
||||||
|
:src="
|
||||||
|
item.iconUrl ?? '/client-assets/dummy.png'
|
||||||
|
"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
<span class="name">{{ item.host }}</span>
|
<span class="name">{{ item.host }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -33,55 +56,58 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import MkInput from '@/components/form/input.vue';
|
import MkInput from "@/components/form/input.vue";
|
||||||
import XModalWindow from '@/components/MkModalWindow.vue';
|
import XModalWindow from "@/components/MkModalWindow.vue";
|
||||||
import * as os from '@/os';
|
import * as os from "@/os";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
import { Instance } from 'calckey-js/built/entities';
|
import { Instance } from "calckey-js/built/entities";
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'ok', selected: Instance): void;
|
(ev: "ok", selected: Instance): void;
|
||||||
(ev: 'cancel'): void;
|
(ev: "cancel"): void;
|
||||||
(ev: 'closed'): void;
|
(ev: "closed"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let hostname = $ref('');
|
let hostname = $ref("");
|
||||||
let instances: Instance[] = $ref([]);
|
let instances: Instance[] = $ref([]);
|
||||||
let selected: Instance | null = $ref(null);
|
let selected: Instance | null = $ref(null);
|
||||||
let dialogEl = $ref<InstanceType<typeof XModalWindow>>();
|
let dialogEl = $ref<InstanceType<typeof XModalWindow>>();
|
||||||
|
|
||||||
let searchOrderLatch = 0;
|
let searchOrderLatch = 0;
|
||||||
const search = () => {
|
const search = () => {
|
||||||
if (hostname === '') {
|
if (hostname === "") {
|
||||||
instances = [];
|
instances = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchId = ++searchOrderLatch;
|
const searchId = ++searchOrderLatch;
|
||||||
os.api('federation/instances', {
|
os.api("federation/instances", {
|
||||||
host: hostname,
|
host: hostname,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
blocked: false,
|
blocked: false,
|
||||||
suspended: false,
|
suspended: false,
|
||||||
sort: '+pubSub',
|
sort: "+pubSub",
|
||||||
}).then(_instances => {
|
}).then((_instances) => {
|
||||||
if (searchId !== searchOrderLatch) return;
|
if (searchId !== searchOrderLatch) return;
|
||||||
instances = _instances.map(x => ({
|
instances = _instances.map(
|
||||||
|
(x) =>
|
||||||
|
({
|
||||||
id: x.id,
|
id: x.id,
|
||||||
host: x.host,
|
host: x.host,
|
||||||
iconUrl: x.iconUrl,
|
iconUrl: x.iconUrl,
|
||||||
} as Instance));
|
} as Instance)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const ok = () => {
|
const ok = () => {
|
||||||
if (selected == null) return;
|
if (selected == null) return;
|
||||||
emit('ok', selected);
|
emit("ok", selected);
|
||||||
dialogEl?.close();
|
dialogEl?.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
const cancel = () => {
|
const cancel = () => {
|
||||||
emit('cancel');
|
emit("cancel");
|
||||||
dialogEl?.close();
|
dialogEl?.close();
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -4,46 +4,79 @@
|
||||||
<template #header>Chart</template>
|
<template #header>Chart</template>
|
||||||
<div :class="$style.chart">
|
<div :class="$style.chart">
|
||||||
<div class="selects">
|
<div class="selects">
|
||||||
<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
|
<MkSelect v-model="chartSrc" style="margin: 0; flex: 1">
|
||||||
<optgroup :label="i18n.ts.federation">
|
<optgroup :label="i18n.ts.federation">
|
||||||
<option value="federation">{{ i18n.ts._charts.federation }}</option>
|
<option value="federation">
|
||||||
<option value="ap-request">{{ i18n.ts._charts.apRequest }}</option>
|
{{ i18n.ts._charts.federation }}
|
||||||
|
</option>
|
||||||
|
<option value="ap-request">
|
||||||
|
{{ i18n.ts._charts.apRequest }}
|
||||||
|
</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
<optgroup :label="i18n.ts.users">
|
<optgroup :label="i18n.ts.users">
|
||||||
<option value="users">{{ i18n.ts._charts.usersIncDec }}</option>
|
<option value="users">
|
||||||
<option value="users-total">{{ i18n.ts._charts.usersTotal }}</option>
|
{{ i18n.ts._charts.usersIncDec }}
|
||||||
<option value="active-users">{{ i18n.ts._charts.activeUsers }}</option>
|
</option>
|
||||||
|
<option value="users-total">
|
||||||
|
{{ i18n.ts._charts.usersTotal }}
|
||||||
|
</option>
|
||||||
|
<option value="active-users">
|
||||||
|
{{ i18n.ts._charts.activeUsers }}
|
||||||
|
</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
<optgroup :label="i18n.ts.notes">
|
<optgroup :label="i18n.ts.notes">
|
||||||
<option value="notes">{{ i18n.ts._charts.notesIncDec }}</option>
|
<option value="notes">
|
||||||
<option value="local-notes">{{ i18n.ts._charts.localNotesIncDec }}</option>
|
{{ i18n.ts._charts.notesIncDec }}
|
||||||
<option value="remote-notes">{{ i18n.ts._charts.remoteNotesIncDec }}</option>
|
</option>
|
||||||
<option value="notes-total">{{ i18n.ts._charts.notesTotal }}</option>
|
<option value="local-notes">
|
||||||
|
{{ i18n.ts._charts.localNotesIncDec }}
|
||||||
|
</option>
|
||||||
|
<option value="remote-notes">
|
||||||
|
{{ i18n.ts._charts.remoteNotesIncDec }}
|
||||||
|
</option>
|
||||||
|
<option value="notes-total">
|
||||||
|
{{ i18n.ts._charts.notesTotal }}
|
||||||
|
</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
<optgroup :label="i18n.ts.drive">
|
<optgroup :label="i18n.ts.drive">
|
||||||
<option value="drive-files">{{ i18n.ts._charts.filesIncDec }}</option>
|
<option value="drive-files">
|
||||||
<option value="drive">{{ i18n.ts._charts.storageUsageIncDec }}</option>
|
{{ i18n.ts._charts.filesIncDec }}
|
||||||
|
</option>
|
||||||
|
<option value="drive">
|
||||||
|
{{ i18n.ts._charts.storageUsageIncDec }}
|
||||||
|
</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkSelect v-model="chartSpan" style="margin: 0 0 0 10px;">
|
<MkSelect v-model="chartSpan" style="margin: 0 0 0 10px">
|
||||||
<option value="hour">{{ i18n.ts.perHour }}</option>
|
<option value="hour">{{ i18n.ts.perHour }}</option>
|
||||||
<option value="day">{{ i18n.ts.perDay }}</option>
|
<option value="day">{{ i18n.ts.perDay }}</option>
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart _panel">
|
<div class="chart _panel">
|
||||||
<MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="true"></MkChart>
|
<MkChart
|
||||||
|
:src="chartSrc"
|
||||||
|
:span="chartSpan"
|
||||||
|
:limit="chartLimit"
|
||||||
|
:detailed="true"
|
||||||
|
></MkChart>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
<MkFolder class="item">
|
<MkFolder class="item">
|
||||||
<template #header>Active users heatmap</template>
|
<template #header>Active users heatmap</template>
|
||||||
<MkSelect v-model="heatmapSrc" style="margin: 0 0 12px 0;">
|
<MkSelect v-model="heatmapSrc" style="margin: 0 0 12px 0">
|
||||||
<option value="active-users">Active users</option>
|
<option value="active-users">Active users</option>
|
||||||
<option value="notes">Notes</option>
|
<option value="notes">Notes</option>
|
||||||
<option value="ap-requests-inbox-received">Fediverse Requests: inboxReceived</option>
|
<option value="ap-requests-inbox-received">
|
||||||
<option value="ap-requests-deliver-succeeded">Fediverse Requests: deliverSucceeded</option>
|
Fediverse Requests: inboxReceived
|
||||||
<option value="ap-requests-deliver-failed">Fediverse Requests: deliverFailed</option>
|
</option>
|
||||||
|
<option value="ap-requests-deliver-succeeded">
|
||||||
|
Fediverse Requests: deliverSucceeded
|
||||||
|
</option>
|
||||||
|
<option value="ap-requests-deliver-failed">
|
||||||
|
Fediverse Requests: deliverFailed
|
||||||
|
</option>
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<div class="_panel" :class="$style.heatmap">
|
<div class="_panel" :class="$style.heatmap">
|
||||||
<MkHeatmap :src="heatmapSrc" />
|
<MkHeatmap :src="heatmapSrc" />
|
||||||
|
@ -69,45 +102,49 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted } from 'vue';
|
import { onMounted } from "vue";
|
||||||
import { Chart } from 'chart.js';
|
import { Chart } from "chart.js";
|
||||||
import MkSelect from '@/components/form/select.vue';
|
import MkSelect from "@/components/form/select.vue";
|
||||||
import MkChart from '@/components/MkChart.vue';
|
import MkChart from "@/components/MkChart.vue";
|
||||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
|
import { useChartTooltip } from "@/scripts/use-chart-tooltip";
|
||||||
import * as os from '@/os';
|
import * as os from "@/os";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
import MkHeatmap from '@/components/MkHeatmap.vue';
|
import MkHeatmap from "@/components/MkHeatmap.vue";
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from "@/components/MkFolder.vue";
|
||||||
import { initChart } from '@/scripts/init-chart';
|
import { initChart } from "@/scripts/init-chart";
|
||||||
|
|
||||||
initChart();
|
initChart();
|
||||||
|
|
||||||
const chartLimit = 500;
|
const chartLimit = 500;
|
||||||
let chartSpan = $ref<'hour' | 'day'>('hour');
|
let chartSpan = $ref<"hour" | "day">("hour");
|
||||||
let chartSrc = $ref('active-users');
|
let chartSrc = $ref("active-users");
|
||||||
let heatmapSrc = $ref('active-users');
|
let heatmapSrc = $ref("active-users");
|
||||||
let subDoughnutEl = $shallowRef<HTMLCanvasElement>();
|
let subDoughnutEl = $shallowRef<HTMLCanvasElement>();
|
||||||
let pubDoughnutEl = $shallowRef<HTMLCanvasElement>();
|
let pubDoughnutEl = $shallowRef<HTMLCanvasElement>();
|
||||||
|
|
||||||
const { handler: externalTooltipHandler1 } = useChartTooltip({
|
const { handler: externalTooltipHandler1 } = useChartTooltip({
|
||||||
position: 'middle',
|
position: "middle",
|
||||||
});
|
});
|
||||||
const { handler: externalTooltipHandler2 } = useChartTooltip({
|
const { handler: externalTooltipHandler2 } = useChartTooltip({
|
||||||
position: 'middle',
|
position: "middle",
|
||||||
});
|
});
|
||||||
|
|
||||||
function createDoughnut(chartEl, tooltip, data) {
|
function createDoughnut(chartEl, tooltip, data) {
|
||||||
const chartInstance = new Chart(chartEl, {
|
const chartInstance = new Chart(chartEl, {
|
||||||
type: 'doughnut',
|
type: "doughnut",
|
||||||
data: {
|
data: {
|
||||||
labels: data.map(x => x.name),
|
labels: data.map((x) => x.name),
|
||||||
datasets: [{
|
datasets: [
|
||||||
backgroundColor: data.map(x => x.color),
|
{
|
||||||
borderColor: getComputedStyle(document.documentElement).getPropertyValue('--panel'),
|
backgroundColor: data.map((x) => x.color),
|
||||||
|
borderColor: getComputedStyle(
|
||||||
|
document.documentElement
|
||||||
|
).getPropertyValue("--panel"),
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
hoverOffset: 0,
|
hoverOffset: 0,
|
||||||
data: data.map(x => x.value),
|
data: data.map((x) => x.value),
|
||||||
}],
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
|
@ -120,7 +157,12 @@ function createDoughnut(chartEl, tooltip, data) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
onClick: (ev) => {
|
onClick: (ev) => {
|
||||||
const hit = chartInstance.getElementsAtEventForMode(ev, 'nearest', { intersect: true }, false)[0];
|
const hit = chartInstance.getElementsAtEventForMode(
|
||||||
|
ev,
|
||||||
|
"nearest",
|
||||||
|
{ intersect: true },
|
||||||
|
false
|
||||||
|
)[0];
|
||||||
if (hit && data[hit.index].onClick) {
|
if (hit && data[hit.index].onClick) {
|
||||||
data[hit.index].onClick();
|
data[hit.index].onClick();
|
||||||
}
|
}
|
||||||
|
@ -131,7 +173,7 @@ function createDoughnut(chartEl, tooltip, data) {
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
mode: 'index',
|
mode: "index",
|
||||||
animation: {
|
animation: {
|
||||||
duration: 0,
|
duration: 0,
|
||||||
},
|
},
|
||||||
|
@ -145,24 +187,48 @@ function createDoughnut(chartEl, tooltip, data) {
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
os.apiGet('federation/stats', { limit: 30 }).then(fedStats => {
|
os.apiGet("federation/stats", { limit: 30 }).then((fedStats) => {
|
||||||
createDoughnut(subDoughnutEl, externalTooltipHandler1, fedStats.topSubInstances.map(x => ({
|
createDoughnut(
|
||||||
|
subDoughnutEl,
|
||||||
|
externalTooltipHandler1,
|
||||||
|
fedStats.topSubInstances
|
||||||
|
.map((x) => ({
|
||||||
name: x.host,
|
name: x.host,
|
||||||
color: x.themeColor,
|
color: x.themeColor,
|
||||||
value: x.followersCount,
|
value: x.followersCount,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
os.pageWindow(`/instance-info/${x.host}`);
|
os.pageWindow(`/instance-info/${x.host}`);
|
||||||
},
|
},
|
||||||
})).concat([{ name: '(other)', color: '#80808080', value: fedStats.otherFollowersCount }]));
|
}))
|
||||||
|
.concat([
|
||||||
|
{
|
||||||
|
name: "(other)",
|
||||||
|
color: "#80808080",
|
||||||
|
value: fedStats.otherFollowersCount,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
createDoughnut(pubDoughnutEl, externalTooltipHandler2, fedStats.topPubInstances.map(x => ({
|
createDoughnut(
|
||||||
|
pubDoughnutEl,
|
||||||
|
externalTooltipHandler2,
|
||||||
|
fedStats.topPubInstances
|
||||||
|
.map((x) => ({
|
||||||
name: x.host,
|
name: x.host,
|
||||||
color: x.themeColor,
|
color: x.themeColor,
|
||||||
value: x.followingCount,
|
value: x.followingCount,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
os.pageWindow(`/instance-info/${x.host}`);
|
os.pageWindow(`/instance-info/${x.host}`);
|
||||||
},
|
},
|
||||||
})).concat([{ name: '(other)', color: '#80808080', value: fedStats.otherFollowingCount }]));
|
}))
|
||||||
|
.concat([
|
||||||
|
{
|
||||||
|
name: "(other)",
|
||||||
|
color: "#80808080",
|
||||||
|
value: fedStats.otherFollowingCount,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -205,7 +271,8 @@ onMounted(() => {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
|
||||||
> .sub, > .pub {
|
> .sub,
|
||||||
|
> .pub {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
|
@ -6,36 +6,45 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { instanceName } from '@/config';
|
import { instanceName } from "@/config";
|
||||||
import { instance as Instance } from '@/instance';
|
import { instance as Instance } from "@/instance";
|
||||||
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy';
|
import { getProxiedImageUrlNullable } from "@/scripts/media-proxy";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
instance?: {
|
instance?: {
|
||||||
faviconUrl?: string
|
faviconUrl?: string;
|
||||||
name: string
|
name: string;
|
||||||
themeColor?: string
|
themeColor?: string;
|
||||||
}
|
};
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let ticker = $ref<HTMLElement | null>(null);
|
let ticker = $ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
// if no instance data is given, this is for the local instance
|
// if no instance data is given, this is for the local instance
|
||||||
const instance = props.instance ?? {
|
const instance = props.instance ?? {
|
||||||
faviconUrl: Instance.iconUrl || Instance.faviconUrl || '/favicon.ico',
|
faviconUrl: Instance.iconUrl || Instance.faviconUrl || "/favicon.ico",
|
||||||
name: instanceName,
|
name: instanceName,
|
||||||
themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement)?.content
|
themeColor: (
|
||||||
|
document.querySelector(
|
||||||
|
'meta[name="theme-color-orig"]'
|
||||||
|
) as HTMLMetaElement
|
||||||
|
)?.content,
|
||||||
};
|
};
|
||||||
|
|
||||||
const computedStyle = getComputedStyle(document.documentElement);
|
const computedStyle = getComputedStyle(document.documentElement);
|
||||||
const themeColor = instance.themeColor ?? computedStyle.getPropertyValue('--bg');
|
const themeColor =
|
||||||
|
instance.themeColor ?? computedStyle.getPropertyValue("--bg");
|
||||||
|
|
||||||
const bg = {
|
const bg = {
|
||||||
background: `linear-gradient(90deg, ${themeColor}, ${themeColor}55)`,
|
background: `linear-gradient(90deg, ${themeColor}, ${themeColor}55)`,
|
||||||
};
|
};
|
||||||
|
|
||||||
function getInstanceIcon(instance): string {
|
function getInstanceIcon(instance): string {
|
||||||
return getProxiedImageUrlNullable(instance.iconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.faviconUrl, 'preview') ?? '/client-assets/dummy.png';
|
return (
|
||||||
|
getProxiedImageUrlNullable(instance.iconUrl, "preview") ??
|
||||||
|
getProxiedImageUrlNullable(instance.faviconUrl, "preview") ??
|
||||||
|
"/client-assets/dummy.png"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -48,10 +57,10 @@ function getInstanceIcon(instance): string {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 1.1em;
|
height: 1.1em;
|
||||||
justify-self: flex-end;
|
justify-self: flex-end;
|
||||||
padding: .2em .4em;
|
padding: 0.2em 0.4em;
|
||||||
padding: .2em .4em;
|
padding: 0.2em 0.4em;
|
||||||
border-radius: 100px;
|
border-radius: 100px;
|
||||||
font-size: .8em;
|
font-size: 0.8em;
|
||||||
text-shadow: 0 2px 2px var(--shadow);
|
text-shadow: 0 2px 2px var(--shadow);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
.header > .body & {
|
.header > .body & {
|
||||||
|
@ -80,11 +89,14 @@ function getInstanceIcon(instance): string {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-shadow: -1px -1px 0 var(--bg), 1px -1px 0 var(--bg), -1px 1px 0 var(--bg), 1px 1px 0 var(--bg);
|
text-shadow: -1px -1px 0 var(--bg), 1px -1px 0 var(--bg),
|
||||||
.article > .main &, .header > .body & {
|
-1px 1px 0 var(--bg), 1px 1px 0 var(--bg);
|
||||||
|
.article > .main &,
|
||||||
|
.header > .body & {
|
||||||
display: unset;
|
display: unset;
|
||||||
}
|
}
|
||||||
.article > .main &, .header > .body & {
|
.article > .main &,
|
||||||
|
.header > .body & {
|
||||||
display: unset;
|
display: unset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,24 +5,35 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="value">
|
<div class="value">
|
||||||
<slot name="value"></slot>
|
<slot name="value"></slot>
|
||||||
<button v-if="copy" v-tooltip="i18n.ts.copy" class="_textButton" style="margin-left: 0.5em;" @click="copy_"><i class="ph-clipboard-text ph-bold"></i></button>
|
<button
|
||||||
|
v-if="copy"
|
||||||
|
v-tooltip="i18n.ts.copy"
|
||||||
|
class="_textButton"
|
||||||
|
style="margin-left: 0.5em"
|
||||||
|
@click="copy_"
|
||||||
|
>
|
||||||
|
<i class="ph-clipboard-text ph-bold"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import {} from "vue";
|
||||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
import copyToClipboard from "@/scripts/copy-to-clipboard";
|
||||||
import * as os from '@/os';
|
import * as os from "@/os";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
copy?: string | null;
|
copy?: string | null;
|
||||||
oneline?: boolean;
|
oneline?: boolean;
|
||||||
}>(), {
|
}>(),
|
||||||
|
{
|
||||||
copy: null,
|
copy: null,
|
||||||
oneline: false,
|
oneline: false,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const copy_ = () => {
|
const copy_ = () => {
|
||||||
copyToClipboard(props.copy);
|
copyToClipboard(props.copy);
|
||||||
|
|
|
@ -1,17 +1,49 @@
|
||||||
<template>
|
<template>
|
||||||
<MkModal ref="modal" v-slot="{ type, maxHeight }" :prefer-type="preferedModalType" :anchor="anchor" :transparent-bg="true" :src="src" @click="modal.close()" @closed="emit('closed')">
|
<MkModal
|
||||||
<div class="szkkfdyq _popup _shadow" :class="{ asDrawer: type === 'drawer' }" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : '' }">
|
ref="modal"
|
||||||
|
v-slot="{ type, maxHeight }"
|
||||||
|
:prefer-type="preferedModalType"
|
||||||
|
:anchor="anchor"
|
||||||
|
:transparent-bg="true"
|
||||||
|
:src="src"
|
||||||
|
@click="modal.close()"
|
||||||
|
@closed="emit('closed')"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="szkkfdyq _popup _shadow"
|
||||||
|
:class="{ asDrawer: type === 'drawer' }"
|
||||||
|
:style="{ maxHeight: maxHeight ? maxHeight + 'px' : '' }"
|
||||||
|
>
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<template v-for="item in items">
|
<template v-for="item in items">
|
||||||
<button v-if="item.action" v-click-anime class="_button" @click="$event => { item.action($event); close(); }">
|
<button
|
||||||
|
v-if="item.action"
|
||||||
|
v-click-anime
|
||||||
|
class="_button"
|
||||||
|
@click="
|
||||||
|
($event) => {
|
||||||
|
item.action($event);
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
<i class="icon" :class="item.icon"></i>
|
<i class="icon" :class="item.icon"></i>
|
||||||
<div class="text">{{ item.text }}</div>
|
<div class="text">{{ item.text }}</div>
|
||||||
<span v-if="item.indicate" class="indicator"><i class="ph-circle ph-fill"></i></span>
|
<span v-if="item.indicate" class="indicator"
|
||||||
|
><i class="ph-circle ph-fill"></i
|
||||||
|
></span>
|
||||||
</button>
|
</button>
|
||||||
<MkA v-else v-click-anime :to="item.to" @click.passive="close()">
|
<MkA
|
||||||
|
v-else
|
||||||
|
v-click-anime
|
||||||
|
:to="item.to"
|
||||||
|
@click.passive="close()"
|
||||||
|
>
|
||||||
<i class="icon" :class="item.icon"></i>
|
<i class="icon" :class="item.icon"></i>
|
||||||
<div class="text">{{ item.text }}</div>
|
<div class="text">{{ item.text }}</div>
|
||||||
<span v-if="item.indicate" class="indicator"><i class="ph-circle ph-fill"></i></span>
|
<span v-if="item.indicate" class="indicator"
|
||||||
|
><i class="ph-circle ph-fill"></i
|
||||||
|
></span>
|
||||||
</MkA>
|
</MkA>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
@ -20,36 +52,46 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import {} from "vue";
|
||||||
import MkModal from '@/components/MkModal.vue';
|
import MkModal from "@/components/MkModal.vue";
|
||||||
import { navbarItemDef } from '@/navbar';
|
import { navbarItemDef } from "@/navbar";
|
||||||
import { instanceName } from '@/config';
|
import { instanceName } from "@/config";
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from "@/store";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
import { deviceKind } from '@/scripts/device-kind';
|
import { deviceKind } from "@/scripts/device-kind";
|
||||||
import * as os from '@/os';
|
import * as os from "@/os";
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
src?: HTMLElement;
|
src?: HTMLElement;
|
||||||
anchor?: { x: string; y: string; };
|
anchor?: { x: string; y: string };
|
||||||
}>(), {
|
}>(),
|
||||||
anchor: () => ({ x: 'right', y: 'center' }),
|
{
|
||||||
});
|
anchor: () => ({ x: "right", y: "center" }),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'closed'): void;
|
(ev: "closed"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const preferedModalType = (deviceKind === 'desktop' && props.src != null) ? 'popup' :
|
const preferedModalType =
|
||||||
deviceKind === 'smartphone' ? 'drawer' :
|
deviceKind === "desktop" && props.src != null
|
||||||
'dialog';
|
? "popup"
|
||||||
|
: deviceKind === "smartphone"
|
||||||
|
? "drawer"
|
||||||
|
: "dialog";
|
||||||
|
|
||||||
const modal = $ref<InstanceType<typeof MkModal>>();
|
const modal = $ref<InstanceType<typeof MkModal>>();
|
||||||
|
|
||||||
const menu = defaultStore.state.menu;
|
const menu = defaultStore.state.menu;
|
||||||
|
|
||||||
const items = Object.keys(navbarItemDef).filter(k => !menu.includes(k)).map(k => navbarItemDef[k]).filter(def => def.show == null ? true : def.show).map(def => ({
|
const items = Object.keys(navbarItemDef)
|
||||||
type: def.to ? 'link' : 'button',
|
.filter((k) => !menu.includes(k))
|
||||||
|
.map((k) => navbarItemDef[k])
|
||||||
|
.filter((def) => (def.show == null ? true : def.show))
|
||||||
|
.map((def) => ({
|
||||||
|
type: def.to ? "link" : "button",
|
||||||
text: i18n.ts[def.title],
|
text: i18n.ts[def.title],
|
||||||
icon: def.icon,
|
icon: def.icon,
|
||||||
to: def.to,
|
to: def.to,
|
||||||
|
@ -82,7 +124,8 @@ function close() {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .main, > .sub {
|
> .main,
|
||||||
|
> .sub {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||||
|
|
||||||
|
|
|
@ -1,36 +1,55 @@
|
||||||
<template>
|
<template>
|
||||||
<component :is="self ? 'MkA' : 'a'" ref="el" class="xlcxczvw _link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target"
|
<component
|
||||||
:title="url" @click.stop
|
:is="self ? 'MkA' : 'a'"
|
||||||
|
ref="el"
|
||||||
|
class="xlcxczvw _link"
|
||||||
|
:[attr]="self ? url.substr(local.length) : url"
|
||||||
|
:rel="rel"
|
||||||
|
:target="target"
|
||||||
|
:title="url"
|
||||||
|
@click.stop
|
||||||
>
|
>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
<i v-if="target === '_blank'" class="ph-arrow-square-out ph-bold ph-lg icon"></i>
|
<i
|
||||||
|
v-if="target === '_blank'"
|
||||||
|
class="ph-arrow-square-out ph-bold ph-lg icon"
|
||||||
|
></i>
|
||||||
</component>
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineAsyncComponent } from 'vue';
|
import { defineAsyncComponent } from "vue";
|
||||||
import { url as local } from '@/config';
|
import { url as local } from "@/config";
|
||||||
import { useTooltip } from '@/scripts/use-tooltip';
|
import { useTooltip } from "@/scripts/use-tooltip";
|
||||||
import * as os from '@/os';
|
import * as os from "@/os";
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
url: string;
|
url: string;
|
||||||
rel?: null | string;
|
rel?: null | string;
|
||||||
}>(), {
|
}>(),
|
||||||
});
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
const self = props.url.startsWith(local);
|
const self = props.url.startsWith(local);
|
||||||
const attr = self ? 'to' : 'href';
|
const attr = self ? "to" : "href";
|
||||||
const target = self ? null : '_blank';
|
const target = self ? null : "_blank";
|
||||||
|
|
||||||
const el = $ref();
|
const el = $ref();
|
||||||
|
|
||||||
useTooltip($$(el), (showing) => {
|
useTooltip($$(el), (showing) => {
|
||||||
os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), {
|
os.popup(
|
||||||
|
defineAsyncComponent(
|
||||||
|
() => import("@/components/MkUrlPreviewPopup.vue")
|
||||||
|
),
|
||||||
|
{
|
||||||
showing,
|
showing,
|
||||||
url: props.url,
|
url: props.url,
|
||||||
source: el,
|
source: el,
|
||||||
}, {}, 'closed');
|
},
|
||||||
|
{},
|
||||||
|
"closed"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -40,7 +59,7 @@ useTooltip($$(el), (showing) => {
|
||||||
|
|
||||||
> .icon {
|
> .icon {
|
||||||
padding-left: 2px;
|
padding-left: 2px;
|
||||||
font-size: .9em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { h, onMounted, onUnmounted, ref, watch } from 'vue';
|
import { h, onMounted, onUnmounted, ref, watch } from "vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'MarqueeText',
|
name: "MarqueeText",
|
||||||
props: {
|
props: {
|
||||||
duration: {
|
duration: {
|
||||||
type: Number,
|
type: Number,
|
||||||
|
@ -38,37 +38,35 @@ export default {
|
||||||
calc();
|
calc();
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {});
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
contentEl,
|
contentEl,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
render({
|
render({ $slots, $style, $props: { duration, repeat, paused, reverse } }) {
|
||||||
$slots, $style, $props: {
|
return h("div", { class: [$style.wrap] }, [
|
||||||
duration, repeat, paused, reverse,
|
h(
|
||||||
|
"span",
|
||||||
|
{
|
||||||
|
ref: "contentEl",
|
||||||
|
class: [paused ? $style.paused : undefined, $style.content],
|
||||||
},
|
},
|
||||||
}) {
|
Array(repeat).fill(
|
||||||
return h('div', { class: [$style.wrap] }, [
|
h(
|
||||||
h('span', {
|
"span",
|
||||||
ref: 'contentEl',
|
{
|
||||||
class: [
|
|
||||||
paused
|
|
||||||
? $style.paused
|
|
||||||
: undefined,
|
|
||||||
$style.content,
|
|
||||||
],
|
|
||||||
}, Array(repeat).fill(
|
|
||||||
h('span', {
|
|
||||||
class: $style.text,
|
class: $style.text,
|
||||||
style: {
|
style: {
|
||||||
animationDirection: reverse
|
animationDirection: reverse
|
||||||
? 'reverse'
|
? "reverse"
|
||||||
: undefined,
|
: undefined,
|
||||||
},
|
},
|
||||||
}, $slots.default()),
|
},
|
||||||
)),
|
$slots.default()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -100,7 +98,11 @@ export default {
|
||||||
animation-play-state: paused;
|
animation-play-state: paused;
|
||||||
}
|
}
|
||||||
@keyframes marquee {
|
@keyframes marquee {
|
||||||
0% { transform:translateX(0); }
|
0% {
|
||||||
100% { transform:translateX(-100%); }
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,11 +1,20 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="mk-media-banner">
|
<div class="mk-media-banner">
|
||||||
<div v-if="media.isSensitive && hide" class="sensitive" @click="hide = false">
|
<div
|
||||||
|
v-if="media.isSensitive && hide"
|
||||||
|
class="sensitive"
|
||||||
|
@click="hide = false"
|
||||||
|
>
|
||||||
<span class="icon"><i class="ph-warning ph-bold ph-lg"></i></span>
|
<span class="icon"><i class="ph-warning ph-bold ph-lg"></i></span>
|
||||||
<b>{{ i18n.ts.sensitive }}</b>
|
<b>{{ i18n.ts.sensitive }}</b>
|
||||||
<span>{{ i18n.ts.clickToShow }}</span>
|
<span>{{ i18n.ts.clickToShow }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="media.type.startsWith('audio') && media.type !== 'audio/midi'" class="audio">
|
<div
|
||||||
|
v-else-if="
|
||||||
|
media.type.startsWith('audio') && media.type !== 'audio/midi'
|
||||||
|
"
|
||||||
|
class="audio"
|
||||||
|
>
|
||||||
<VuePlyr
|
<VuePlyr
|
||||||
:options="{
|
:options="{
|
||||||
controls: [
|
controls: [
|
||||||
|
@ -32,39 +41,44 @@
|
||||||
</VuePlyr>
|
</VuePlyr>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
v-else class="download"
|
v-else
|
||||||
|
class="download"
|
||||||
:href="media.url"
|
:href="media.url"
|
||||||
:title="media.name"
|
:title="media.name"
|
||||||
:download="media.name"
|
:download="media.name"
|
||||||
>
|
>
|
||||||
<span class="icon"><i class="ph-download-simple ph-bold ph-lg"></i></span>
|
<span class="icon"
|
||||||
|
><i class="ph-download-simple ph-bold ph-lg"></i
|
||||||
|
></span>
|
||||||
<b>{{ media.name }}</b>
|
<b>{{ media.name }}</b>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted } from 'vue';
|
import { onMounted } from "vue";
|
||||||
import VuePlyr from 'vue-plyr';
|
import VuePlyr from "vue-plyr";
|
||||||
import type * as misskey from 'calckey-js';
|
import type * as misskey from "calckey-js";
|
||||||
import { ColdDeviceStorage } from '@/store';
|
import { ColdDeviceStorage } from "@/store";
|
||||||
import 'vue-plyr/dist/vue-plyr.css';
|
import "vue-plyr/dist/vue-plyr.css";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
media: misskey.entities.DriveFile;
|
media: misskey.entities.DriveFile;
|
||||||
}>(), {
|
}>(),
|
||||||
});
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
const audioEl = $ref<HTMLAudioElement | null>();
|
const audioEl = $ref<HTMLAudioElement | null>();
|
||||||
let hide = $ref(true);
|
let hide = $ref(true);
|
||||||
|
|
||||||
function volumechange() {
|
function volumechange() {
|
||||||
if (audioEl) ColdDeviceStorage.set('mediaVolume', audioEl.volume);
|
if (audioEl) ColdDeviceStorage.set("mediaVolume", audioEl.volume);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (audioEl) audioEl.volume = ColdDeviceStorage.get('mediaVolume');
|
if (audioEl) audioEl.volume = ColdDeviceStorage.get("mediaVolume");
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -97,7 +111,7 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
> *:not(:last-child) {
|
> *:not(:last-child) {
|
||||||
margin-right: .2em;
|
margin-right: 0.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .icon {
|
> .icon {
|
||||||
|
|
|
@ -5,24 +5,60 @@
|
||||||
<div class="mk-dialog">
|
<div class="mk-dialog">
|
||||||
<header>
|
<header>
|
||||||
<Mfm v-if="title" class="title" :text="title" />
|
<Mfm v-if="title" class="title" :text="title" />
|
||||||
<span class="text-count" :class="{ over: remainingLength < 0 }">{{ remainingLength }}</span>
|
<span
|
||||||
|
class="text-count"
|
||||||
|
:class="{ over: remainingLength < 0 }"
|
||||||
|
>{{ remainingLength }}</span
|
||||||
|
>
|
||||||
<br />
|
<br />
|
||||||
</header>
|
</header>
|
||||||
<textarea id="captioninput" v-model="inputValue" autofocus :placeholder="input.placeholder" @keydown="onInputKeydown"></textarea>
|
<textarea
|
||||||
<div v-if="(showOkButton || showCaptionButton || showCancelButton)" class="buttons">
|
id="captioninput"
|
||||||
<MkButton inline primary :disabled="remainingLength < 0" @click="ok">{{ i18n.ts.ok }}</MkButton>
|
v-model="inputValue"
|
||||||
<MkButton inline @click="caption">{{ i18n.ts.caption }}</MkButton>
|
autofocus
|
||||||
<MkButton inline @click="cancel">{{ i18n.ts.cancel }}</MkButton>
|
:placeholder="input.placeholder"
|
||||||
|
@keydown="onInputKeydown"
|
||||||
|
></textarea>
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
showOkButton ||
|
||||||
|
showCaptionButton ||
|
||||||
|
showCancelButton
|
||||||
|
"
|
||||||
|
class="buttons"
|
||||||
|
>
|
||||||
|
<MkButton
|
||||||
|
inline
|
||||||
|
primary
|
||||||
|
:disabled="remainingLength < 0"
|
||||||
|
@click="ok"
|
||||||
|
>{{ i18n.ts.ok }}</MkButton
|
||||||
|
>
|
||||||
|
<MkButton inline @click="caption">{{
|
||||||
|
i18n.ts.caption
|
||||||
|
}}</MkButton>
|
||||||
|
<MkButton inline @click="cancel">{{
|
||||||
|
i18n.ts.cancel
|
||||||
|
}}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="hdrwpsaf fullwidth">
|
<div class="hdrwpsaf fullwidth">
|
||||||
<header>{{ image.name }}</header>
|
<header>{{ image.name }}</header>
|
||||||
<img id="imgtocaption" :src="image.url" :alt="image.comment" :title="image.comment" @click="$refs.modal.close()"/>
|
<img
|
||||||
|
id="imgtocaption"
|
||||||
|
:src="image.url"
|
||||||
|
:alt="image.comment"
|
||||||
|
:title="image.comment"
|
||||||
|
@click="$refs.modal.close()"
|
||||||
|
/>
|
||||||
<footer>
|
<footer>
|
||||||
<span>{{ image.type }}</span>
|
<span>{{ image.type }}</span>
|
||||||
<span>{{ bytes(image.size) }}</span>
|
<span>{{ bytes(image.size) }}</span>
|
||||||
<span v-if="image.properties && image.properties.width">{{ number(image.properties.width) }}px × {{ number(image.properties.height) }}px</span>
|
<span v-if="image.properties && image.properties.width"
|
||||||
|
>{{ number(image.properties.width) }}px ×
|
||||||
|
{{ number(image.properties.height) }}px</span
|
||||||
|
>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -30,16 +66,16 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from "vue";
|
||||||
import insertTextAtCursor from 'insert-text-at-cursor';
|
import insertTextAtCursor from "insert-text-at-cursor";
|
||||||
import { length } from 'stringz';
|
import { length } from "stringz";
|
||||||
import * as os from '@/os';
|
import * as os from "@/os";
|
||||||
import MkModal from '@/components/MkModal.vue';
|
import MkModal from "@/components/MkModal.vue";
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from "@/components/MkButton.vue";
|
||||||
import bytes from '@/filters/bytes';
|
import bytes from "@/filters/bytes";
|
||||||
import number from '@/filters/number';
|
import number from "@/filters/number";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
import { instance } from '@/instance';
|
import { instance } from "@/instance";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
|
@ -77,7 +113,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
emits: ['done', 'closed'],
|
emits: ["done", "closed"],
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -91,15 +127,15 @@ export default defineComponent({
|
||||||
const maxCaptionLength = instance.maxCaptionTextLength ?? 512;
|
const maxCaptionLength = instance.maxCaptionTextLength ?? 512;
|
||||||
if (typeof this.inputValue !== "string") return maxCaptionLength;
|
if (typeof this.inputValue !== "string") return maxCaptionLength;
|
||||||
return maxCaptionLength - length(this.inputValue);
|
return maxCaptionLength - length(this.inputValue);
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
document.addEventListener('keydown', this.onKeydown);
|
document.addEventListener("keydown", this.onKeydown);
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
document.removeEventListener('keydown', this.onKeydown);
|
document.removeEventListener("keydown", this.onKeydown);
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -107,7 +143,7 @@ export default defineComponent({
|
||||||
number,
|
number,
|
||||||
|
|
||||||
done(canceled, result?) {
|
done(canceled, result?) {
|
||||||
this.$emit('done', { canceled, result });
|
this.$emit("done", { canceled, result });
|
||||||
this.$refs.modal.close();
|
this.$refs.modal.close();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -129,13 +165,15 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
onKeydown(evt) {
|
onKeydown(evt) {
|
||||||
if (evt.which === 27) { // ESC
|
if (evt.which === 27) {
|
||||||
|
// ESC
|
||||||
this.cancel();
|
this.cancel();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onInputKeydown(evt) {
|
onInputKeydown(evt) {
|
||||||
if (evt.which === 13) { // Enter
|
if (evt.which === 13) {
|
||||||
|
// Enter
|
||||||
if (evt.ctrlKey) {
|
if (evt.ctrlKey) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
evt.stopPropagation();
|
evt.stopPropagation();
|
||||||
|
@ -145,12 +183,16 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
caption() {
|
caption() {
|
||||||
const img = document.getElementById('imgtocaption') as HTMLImageElement;
|
const img = document.getElementById(
|
||||||
const ta = document.getElementById('captioninput') as HTMLTextAreaElement;
|
"imgtocaption"
|
||||||
os.api('drive/files/caption-image', {
|
) as HTMLImageElement;
|
||||||
|
const ta = document.getElementById(
|
||||||
|
"captioninput"
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
os.api("drive/files/caption-image", {
|
||||||
url: img.src,
|
url: img.src,
|
||||||
}).then(text => {
|
}).then((text) => {
|
||||||
insertTextAtCursor(ta, text.slice(0, (512 - ta.value.length)));
|
insertTextAtCursor(ta, text.slice(0, 512 - ta.value.length));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,32 +1,50 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="hide" class="qjewsnkg" @click="hide = false">
|
<div v-if="hide" class="qjewsnkg" @click="hide = false">
|
||||||
<ImgWithBlurhash class="bg" :hash="image.blurhash" :title="image.comment" :alt="image.comment"/>
|
<ImgWithBlurhash
|
||||||
|
class="bg"
|
||||||
|
:hash="image.blurhash"
|
||||||
|
:title="image.comment"
|
||||||
|
:alt="image.comment"
|
||||||
|
/>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<b style="display: block;"><i class="ph-warning ph-bold ph-lg"></i> {{ i18n.ts.sensitive }}</b>
|
<b style="display: block"
|
||||||
<span style="display: block;">{{ i18n.ts.clickToShow }}</span>
|
><i class="ph-warning ph-bold ph-lg"></i>
|
||||||
|
{{ i18n.ts.sensitive }}</b
|
||||||
|
>
|
||||||
|
<span style="display: block">{{ i18n.ts.clickToShow }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="gqnyydlz">
|
<div v-else class="gqnyydlz">
|
||||||
<a
|
<a :href="image.url" :title="image.name">
|
||||||
:href="image.url"
|
<ImgWithBlurhash
|
||||||
:title="image.name"
|
:hash="image.blurhash"
|
||||||
>
|
:src="url"
|
||||||
<ImgWithBlurhash :hash="image.blurhash" :src="url" :alt="image.comment" :type="image.type" :title="image.comment" :cover="false"/>
|
:alt="image.comment"
|
||||||
|
:type="image.type"
|
||||||
|
:title="image.comment"
|
||||||
|
:cover="false"
|
||||||
|
/>
|
||||||
<div v-if="image.type === 'image/gif'" class="gif">GIF</div>
|
<div v-if="image.type === 'image/gif'" class="gif">GIF</div>
|
||||||
</a>
|
</a>
|
||||||
<button v-tooltip="i18n.ts.hide" class="_button hide" @click="hide = true"><i class="ph-eye-slash ph-bold ph-lg"></i></button>
|
<button
|
||||||
|
v-tooltip="i18n.ts.hide"
|
||||||
|
class="_button hide"
|
||||||
|
@click="hide = true"
|
||||||
|
>
|
||||||
|
<i class="ph-eye-slash ph-bold ph-lg"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { watch } from 'vue';
|
import { watch } from "vue";
|
||||||
import type * as misskey from 'calckey-js';
|
import type * as misskey from "calckey-js";
|
||||||
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
|
import { getStaticImageUrl } from "@/scripts/get-static-image-url";
|
||||||
import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
|
import ImgWithBlurhash from "@/components/MkImgWithBlurhash.vue";
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from "@/store";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
image: misskey.entities.DriveFile;
|
image: misskey.entities.DriveFile;
|
||||||
|
@ -35,19 +53,28 @@ const props = defineProps<{
|
||||||
|
|
||||||
let hide = $ref(true);
|
let hide = $ref(true);
|
||||||
|
|
||||||
const url = (props.raw || defaultStore.state.loadRawImages)
|
const url =
|
||||||
|
props.raw || defaultStore.state.loadRawImages
|
||||||
? props.image.url
|
? props.image.url
|
||||||
: defaultStore.state.disableShowingAnimatedImages
|
: defaultStore.state.disableShowingAnimatedImages
|
||||||
? getStaticImageUrl(props.image.thumbnailUrl)
|
? getStaticImageUrl(props.image.thumbnailUrl)
|
||||||
: props.image.thumbnailUrl;
|
: props.image.thumbnailUrl;
|
||||||
|
|
||||||
// Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする
|
// Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする
|
||||||
watch(() => props.image, () => {
|
watch(
|
||||||
hide = (defaultStore.state.nsfw === 'force') ? true : props.image.isSensitive && (defaultStore.state.nsfw !== 'ignore');
|
() => props.image,
|
||||||
}, {
|
() => {
|
||||||
|
hide =
|
||||||
|
defaultStore.state.nsfw === "force"
|
||||||
|
? true
|
||||||
|
: props.image.isSensitive &&
|
||||||
|
defaultStore.state.nsfw !== "ignore";
|
||||||
|
},
|
||||||
|
{
|
||||||
deep: true,
|
deep: true,
|
||||||
immediate: true,
|
immediate: true,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -120,7 +147,7 @@ watch(() => props.image, () => {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
left: 12px;
|
left: 12px;
|
||||||
opacity: .5;
|
opacity: 0.5;
|
||||||
padding: 0 6px;
|
padding: 0 6px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
top: 12px;
|
top: 12px;
|
||||||
|
|
|
@ -1,11 +1,40 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="hoawjimk">
|
<div class="hoawjimk">
|
||||||
<XBanner v-for="media in mediaList.filter(media => !previewable(media))" :key="media.id" :media="media"/>
|
<XBanner
|
||||||
<div v-if="mediaList.filter(media => previewable(media)).length > 0" class="gird-container" :class="{ dmWidth: inDm }">
|
v-for="media in mediaList.filter((media) => !previewable(media))"
|
||||||
<div ref="gallery" :data-count="mediaList.filter(media => previewable(media)).length" @click.stop>
|
:key="media.id"
|
||||||
<template v-for="media in mediaList.filter(media => previewable(media))">
|
:media="media"
|
||||||
<XVideo v-if="media.type.startsWith('video')" :key="media.id" :video="media"/>
|
/>
|
||||||
<XImage v-else-if="media.type.startsWith('image')" :key="media.id" class="image" :data-id="media.id" :image="media" :raw="raw"/>
|
<div
|
||||||
|
v-if="mediaList.filter((media) => previewable(media)).length > 0"
|
||||||
|
class="gird-container"
|
||||||
|
:class="{ dmWidth: inDm }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref="gallery"
|
||||||
|
:data-count="
|
||||||
|
mediaList.filter((media) => previewable(media)).length
|
||||||
|
"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-for="media in mediaList.filter((media) =>
|
||||||
|
previewable(media)
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<XVideo
|
||||||
|
v-if="media.type.startsWith('video')"
|
||||||
|
:key="media.id"
|
||||||
|
:video="media"
|
||||||
|
/>
|
||||||
|
<XImage
|
||||||
|
v-else-if="media.type.startsWith('image')"
|
||||||
|
:key="media.id"
|
||||||
|
class="image"
|
||||||
|
:data-id="media.id"
|
||||||
|
:image="media"
|
||||||
|
:raw="raw"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,17 +42,17 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from "vue";
|
||||||
import * as misskey from 'calckey-js';
|
import * as misskey from "calckey-js";
|
||||||
import PhotoSwipeLightbox from 'photoswipe/lightbox';
|
import PhotoSwipeLightbox from "photoswipe/lightbox";
|
||||||
import PhotoSwipe from 'photoswipe';
|
import PhotoSwipe from "photoswipe";
|
||||||
import 'photoswipe/style.css';
|
import "photoswipe/style.css";
|
||||||
import XBanner from '@/components/MkMediaBanner.vue';
|
import XBanner from "@/components/MkMediaBanner.vue";
|
||||||
import XImage from '@/components/MkMediaImage.vue';
|
import XImage from "@/components/MkMediaImage.vue";
|
||||||
import XVideo from '@/components/MkMediaVideo.vue';
|
import XVideo from "@/components/MkMediaVideo.vue";
|
||||||
import * as os from '@/os';
|
import * as os from "@/os";
|
||||||
import { FILE_TYPE_BROWSERSAFE } from '@/const';
|
import { FILE_TYPE_BROWSERSAFE } from "@/const";
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from "@/store";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
mediaList: misskey.entities.DriveFile[];
|
mediaList: misskey.entities.DriveFile[];
|
||||||
|
@ -32,60 +61,72 @@ const props = defineProps<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const gallery = ref(null);
|
const gallery = ref(null);
|
||||||
const pswpZIndex = os.claimZIndex('middle');
|
const pswpZIndex = os.claimZIndex("middle");
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const lightbox = new PhotoSwipeLightbox({
|
const lightbox = new PhotoSwipeLightbox({
|
||||||
dataSource: props.mediaList
|
dataSource: props.mediaList
|
||||||
.filter(media => {
|
.filter((media) => {
|
||||||
if (media.type === 'image/svg+xml') return true; // svgのwebpublicはpngなのでtrue
|
if (media.type === "image/svg+xml") return true; // svgのwebpublicはpngなのでtrue
|
||||||
return media.type.startsWith('image') && FILE_TYPE_BROWSERSAFE.includes(media.type);
|
return (
|
||||||
|
media.type.startsWith("image") &&
|
||||||
|
FILE_TYPE_BROWSERSAFE.includes(media.type)
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.map(media => {
|
.map((media) => {
|
||||||
const item = {
|
const item = {
|
||||||
src: media.url,
|
src: media.url,
|
||||||
w: media.properties.width,
|
w: media.properties.width,
|
||||||
h: media.properties.height,
|
h: media.properties.height,
|
||||||
alt: media.comment,
|
alt: media.comment,
|
||||||
};
|
};
|
||||||
if (media.properties.orientation != null && media.properties.orientation >= 5) {
|
if (
|
||||||
|
media.properties.orientation != null &&
|
||||||
|
media.properties.orientation >= 5
|
||||||
|
) {
|
||||||
[item.w, item.h] = [item.h, item.w];
|
[item.w, item.h] = [item.h, item.w];
|
||||||
}
|
}
|
||||||
return item;
|
return item;
|
||||||
}),
|
}),
|
||||||
gallery: gallery.value,
|
gallery: gallery.value,
|
||||||
children: '.image',
|
children: ".image",
|
||||||
thumbSelector: '.image',
|
thumbSelector: ".image",
|
||||||
loop: false,
|
loop: false,
|
||||||
padding: window.innerWidth > 500 ? {
|
padding:
|
||||||
|
window.innerWidth > 500
|
||||||
|
? {
|
||||||
top: 32,
|
top: 32,
|
||||||
bottom: 32,
|
bottom: 32,
|
||||||
left: 32,
|
left: 32,
|
||||||
right: 32,
|
right: 32,
|
||||||
} : {
|
}
|
||||||
|
: {
|
||||||
top: 0,
|
top: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
},
|
},
|
||||||
imageClickAction: 'close',
|
imageClickAction: "close",
|
||||||
tapAction: 'toggle-controls',
|
tapAction: "toggle-controls",
|
||||||
pswpModule: PhotoSwipe,
|
pswpModule: PhotoSwipe,
|
||||||
});
|
});
|
||||||
|
|
||||||
lightbox.on('itemData', (ev) => {
|
lightbox.on("itemData", (ev) => {
|
||||||
const { itemData } = ev;
|
const { itemData } = ev;
|
||||||
|
|
||||||
// element is children
|
// element is children
|
||||||
const { element } = itemData;
|
const { element } = itemData;
|
||||||
|
|
||||||
const id = element.dataset.id;
|
const id = element.dataset.id;
|
||||||
const file = props.mediaList.find(media => media.id === id);
|
const file = props.mediaList.find((media) => media.id === id);
|
||||||
|
|
||||||
itemData.src = file.url;
|
itemData.src = file.url;
|
||||||
itemData.w = Number(file.properties.width);
|
itemData.w = Number(file.properties.width);
|
||||||
itemData.h = Number(file.properties.height);
|
itemData.h = Number(file.properties.height);
|
||||||
if (file.properties.orientation != null && file.properties.orientation >= 5) {
|
if (
|
||||||
|
file.properties.orientation != null &&
|
||||||
|
file.properties.orientation >= 5
|
||||||
|
) {
|
||||||
[itemData.w, itemData.h] = [itemData.h, itemData.w];
|
[itemData.w, itemData.h] = [itemData.h, itemData.w];
|
||||||
}
|
}
|
||||||
itemData.msrc = file.thumbnailUrl;
|
itemData.msrc = file.thumbnailUrl;
|
||||||
|
@ -93,14 +134,14 @@ onMounted(() => {
|
||||||
itemData.thumbCropped = true;
|
itemData.thumbCropped = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
lightbox.on('uiRegister', () => {
|
lightbox.on("uiRegister", () => {
|
||||||
lightbox.pswp.ui.registerElement({
|
lightbox.pswp.ui.registerElement({
|
||||||
name: 'altText',
|
name: "altText",
|
||||||
className: 'pwsp__alt-text-container',
|
className: "pwsp__alt-text-container",
|
||||||
appendTo: 'wrapper',
|
appendTo: "wrapper",
|
||||||
onInit: (el, pwsp) => {
|
onInit: (el, pwsp) => {
|
||||||
let textBox = document.createElement('p');
|
let textBox = document.createElement("p");
|
||||||
textBox.className = 'pwsp__alt-text';
|
textBox.className = "pwsp__alt-text";
|
||||||
el.appendChild(textBox);
|
el.appendChild(textBox);
|
||||||
|
|
||||||
let preventProp = function (ev: Event): void {
|
let preventProp = function (ev: Event): void {
|
||||||
|
@ -114,7 +155,7 @@ onMounted(() => {
|
||||||
el.onpointercancel = preventProp;
|
el.onpointercancel = preventProp;
|
||||||
el.onpointermove = preventProp;
|
el.onpointermove = preventProp;
|
||||||
|
|
||||||
pwsp.on('change', () => {
|
pwsp.on("change", () => {
|
||||||
textBox.textContent = pwsp.currSlide.data.alt?.trim();
|
textBox.textContent = pwsp.currSlide.data.alt?.trim();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -125,15 +166,17 @@ onMounted(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const previewable = (file: misskey.entities.DriveFile): boolean => {
|
const previewable = (file: misskey.entities.DriveFile): boolean => {
|
||||||
if (file.type === 'image/svg+xml') return true; // svgのwebpublic/thumbnailはpngなのでtrue
|
if (file.type === "image/svg+xml") return true; // svgのwebpublic/thumbnailはpngなのでtrue
|
||||||
// FILE_TYPE_BROWSERSAFEに適合しないものはブラウザで表示するのに不適切
|
// FILE_TYPE_BROWSERSAFEに適合しないものはブラウザで表示するのに不適切
|
||||||
return (file.type.startsWith('video') || file.type.startsWith('image')) && FILE_TYPE_BROWSERSAFE.includes(file.type);
|
return (
|
||||||
|
(file.type.startsWith("video") || file.type.startsWith("image")) &&
|
||||||
|
FILE_TYPE_BROWSERSAFE.includes(file.type)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.hoawjimk {
|
.hoawjimk {
|
||||||
|
|
||||||
> .dmWidth {
|
> .dmWidth {
|
||||||
min-width: 20rem;
|
min-width: 20rem;
|
||||||
max-width: 40rem;
|
max-width: 40rem;
|
||||||
|
@ -147,9 +190,9 @@ const previewable = (file: misskey.entities.DriveFile): boolean => {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
content: '';
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
padding-top: 56.25% // 16:9;
|
padding-top: 56.25%; // 16:9;
|
||||||
}
|
}
|
||||||
|
|
||||||
> div {
|
> div {
|
||||||
|
@ -254,5 +297,4 @@ const previewable = (file: misskey.entities.DriveFile): boolean => {
|
||||||
.pwsp__alt-text:empty {
|
.pwsp__alt-text:empty {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="hide" class="icozogqfvdetwohsdglrbswgrejoxbdj" @click="hide = false">
|
<div
|
||||||
|
v-if="hide"
|
||||||
|
class="icozogqfvdetwohsdglrbswgrejoxbdj"
|
||||||
|
@click="hide = false"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<b><i class="ph-warning ph-bold ph-lg"></i> {{ i18n.ts.sensitive }}</b>
|
<b
|
||||||
|
><i class="ph-warning ph-bold ph-lg"></i>
|
||||||
|
{{ i18n.ts.sensitive }}</b
|
||||||
|
>
|
||||||
<span>{{ i18n.ts.clickToShow }}</span>
|
<span>{{ i18n.ts.clickToShow }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -17,7 +24,7 @@
|
||||||
'volume',
|
'volume',
|
||||||
'pip',
|
'pip',
|
||||||
'download',
|
'download',
|
||||||
'fullscreen'
|
'fullscreen',
|
||||||
],
|
],
|
||||||
disableContextMenu: false,
|
disableContextMenu: false,
|
||||||
}"
|
}"
|
||||||
|
@ -30,10 +37,7 @@
|
||||||
controls
|
controls
|
||||||
@contextmenu.stop
|
@contextmenu.stop
|
||||||
>
|
>
|
||||||
<source
|
<source :src="video.url" :type="video.type" />
|
||||||
:src="video.url"
|
|
||||||
:type="video.type"
|
|
||||||
>
|
|
||||||
</video>
|
</video>
|
||||||
</VuePlyr>
|
</VuePlyr>
|
||||||
<i class="ph-eye-slash ph-bold ph-lg" @click="hide = true"></i>
|
<i class="ph-eye-slash ph-bold ph-lg" @click="hide = true"></i>
|
||||||
|
@ -41,18 +45,22 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from "vue";
|
||||||
import VuePlyr from 'vue-plyr';
|
import VuePlyr from "vue-plyr";
|
||||||
import type * as misskey from 'calckey-js';
|
import type * as misskey from "calckey-js";
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from "@/store";
|
||||||
import 'vue-plyr/dist/vue-plyr.css';
|
import "vue-plyr/dist/vue-plyr.css";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
video: misskey.entities.DriveFile;
|
video: misskey.entities.DriveFile;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const hide = ref((defaultStore.state.nsfw === 'force') ? true : props.video.isSensitive && (defaultStore.state.nsfw !== 'ignore'));
|
const hide = ref(
|
||||||
|
defaultStore.state.nsfw === "force"
|
||||||
|
? true
|
||||||
|
: props.video.isSensitive && defaultStore.state.nsfw !== "ignore"
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -67,7 +75,7 @@ const hide = ref((defaultStore.state.nsfw === 'force') ? true : props.video.isSe
|
||||||
background-color: var(--fg);
|
background-color: var(--fg);
|
||||||
color: var(--accentLighten);
|
color: var(--accentLighten);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
opacity: .5;
|
opacity: 0.5;
|
||||||
padding: 3px 6px;
|
padding: 3px 6px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
|
@ -1,12 +1,32 @@
|
||||||
<template>
|
<template>
|
||||||
<MkA v-if="url.startsWith('/')" v-user-preview="canonical" class="akbvjaqn" :class="{ isMe }" :to="url" :style="{ background: bgCss }" @click.stop>
|
<MkA
|
||||||
<img class="icon" :src="`/avatar/@${username}@${host}`" alt="">
|
v-if="url.startsWith('/')"
|
||||||
|
v-user-preview="canonical"
|
||||||
|
class="akbvjaqn"
|
||||||
|
:class="{ isMe }"
|
||||||
|
:to="url"
|
||||||
|
:style="{ background: bgCss }"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<img class="icon" :src="`/avatar/@${username}@${host}`" alt="" />
|
||||||
<span class="main">
|
<span class="main">
|
||||||
<span class="username">@{{ username }}</span>
|
<span class="username">@{{ username }}</span>
|
||||||
<span v-if="(host != localHost) || $store.state.showFullAcct" class="host">@{{ toUnicode(host) }}</span>
|
<span
|
||||||
|
v-if="host != localHost || $store.state.showFullAcct"
|
||||||
|
class="host"
|
||||||
|
>@{{ toUnicode(host) }}</span
|
||||||
|
>
|
||||||
</span>
|
</span>
|
||||||
</MkA>
|
</MkA>
|
||||||
<a v-else class="akbvjaqn" :href="url" target="_blank" rel="noopener" :style="{ background: bgCss }" @click.stop>
|
<a
|
||||||
|
v-else
|
||||||
|
class="akbvjaqn"
|
||||||
|
:href="url"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
:style="{ background: bgCss }"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
<span class="main">
|
<span class="main">
|
||||||
<span class="username">@{{ username }}</span>
|
<span class="username">@{{ username }}</span>
|
||||||
<span class="host">@{{ toUnicode(host) }}</span>
|
<span class="host">@{{ toUnicode(host) }}</span>
|
||||||
|
@ -15,26 +35,34 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { toUnicode } from 'punycode';
|
import { toUnicode } from "punycode";
|
||||||
import { } from 'vue';
|
import {} from "vue";
|
||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from "tinycolor2";
|
||||||
import { host as localHost } from '@/config';
|
import { host as localHost } from "@/config";
|
||||||
import { $i } from '@/account';
|
import { $i } from "@/account";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
username: string;
|
username: string;
|
||||||
host: string;
|
host: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const canonical = props.host === localHost ? `@${props.username}` : `@${props.username}@${toUnicode(props.host)}`;
|
const canonical =
|
||||||
|
props.host === localHost
|
||||||
|
? `@${props.username}`
|
||||||
|
: `@${props.username}@${toUnicode(props.host)}`;
|
||||||
|
|
||||||
const url = `/${canonical}`;
|
const url = `/${canonical}`;
|
||||||
|
|
||||||
const isMe = $i && (
|
const isMe =
|
||||||
`@${props.username}@${toUnicode(props.host)}` === `@${$i.username}@${toUnicode(localHost)}`.toLowerCase()
|
$i &&
|
||||||
);
|
`@${props.username}@${toUnicode(props.host)}` ===
|
||||||
|
`@${$i.username}@${toUnicode(localHost)}`.toLowerCase();
|
||||||
|
|
||||||
const bg = tinycolor(getComputedStyle(document.documentElement).getPropertyValue(isMe ? '--mentionMe' : '--mention'));
|
const bg = tinycolor(
|
||||||
|
getComputedStyle(document.documentElement).getPropertyValue(
|
||||||
|
isMe ? "--mentionMe" : "--mention"
|
||||||
|
)
|
||||||
|
);
|
||||||
bg.setAlpha(0.1);
|
bg.setAlpha(0.1);
|
||||||
const bgCss = bg.toRgbString();
|
const bgCss = bg.toRgbString();
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,15 +1,29 @@
|
||||||
<template>
|
<template>
|
||||||
<div ref="el" class="sfhdhdhr">
|
<div ref="el" class="sfhdhdhr">
|
||||||
<MkMenu ref="menu" :items="items" :align="align" :width="width" :as-drawer="false" @close="onChildClosed"/>
|
<MkMenu
|
||||||
|
ref="menu"
|
||||||
|
:items="items"
|
||||||
|
:align="align"
|
||||||
|
:width="width"
|
||||||
|
:as-drawer="false"
|
||||||
|
@close="onChildClosed"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { on } from 'events';
|
import { on } from "events";
|
||||||
import { nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, watch } from 'vue';
|
import {
|
||||||
import MkMenu from './MkMenu.vue';
|
nextTick,
|
||||||
import { MenuItem } from '@/types/menu';
|
onBeforeUnmount,
|
||||||
import * as os from '@/os';
|
onMounted,
|
||||||
|
onUnmounted,
|
||||||
|
ref,
|
||||||
|
watch,
|
||||||
|
} from "vue";
|
||||||
|
import MkMenu from "./MkMenu.vue";
|
||||||
|
import { MenuItem } from "@/types/menu";
|
||||||
|
import * as os from "@/os";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
items: MenuItem[];
|
items: MenuItem[];
|
||||||
|
@ -20,27 +34,27 @@ const props = defineProps<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'closed'): void;
|
(ev: "closed"): void;
|
||||||
(ev: 'actioned'): void;
|
(ev: "actioned"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const el = ref<HTMLElement>();
|
const el = ref<HTMLElement>();
|
||||||
const align = 'left';
|
const align = "left";
|
||||||
|
|
||||||
function setPosition() {
|
function setPosition() {
|
||||||
const rootRect = props.rootElement.getBoundingClientRect();
|
const rootRect = props.rootElement.getBoundingClientRect();
|
||||||
const rect = props.targetElement.getBoundingClientRect();
|
const rect = props.targetElement.getBoundingClientRect();
|
||||||
const left = props.targetElement.offsetWidth;
|
const left = props.targetElement.offsetWidth;
|
||||||
const top = (rect.top - rootRect.top) - 8;
|
const top = rect.top - rootRect.top - 8;
|
||||||
el.value.style.left = left + 'px';
|
el.value.style.left = left + "px";
|
||||||
el.value.style.top = top + 'px';
|
el.value.style.top = top + "px";
|
||||||
}
|
}
|
||||||
|
|
||||||
function onChildClosed(actioned?: boolean) {
|
function onChildClosed(actioned?: boolean) {
|
||||||
if (actioned) {
|
if (actioned) {
|
||||||
emit('actioned');
|
emit("actioned");
|
||||||
} else {
|
} else {
|
||||||
emit('closed');
|
emit("closed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,7 +67,7 @@ onMounted(() => {
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
checkHit: (ev: MouseEvent) => {
|
checkHit: (ev: MouseEvent) => {
|
||||||
return (ev.target === el.value || el.value.contains(ev.target));
|
return ev.target === el.value || el.value.contains(ev.target);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,60 +1,174 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
ref="itemsEl" v-hotkey="keymap"
|
ref="itemsEl"
|
||||||
|
v-hotkey="keymap"
|
||||||
class="rrevdjwt _popup _shadow"
|
class="rrevdjwt _popup _shadow"
|
||||||
:class="{ center: align === 'center', asDrawer }"
|
:class="{ center: align === 'center', asDrawer }"
|
||||||
:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
|
:style="{
|
||||||
@contextmenu.self="e => e.preventDefault()"
|
width: width && !asDrawer ? width + 'px' : '',
|
||||||
|
maxHeight: maxHeight ? maxHeight + 'px' : '',
|
||||||
|
}"
|
||||||
|
@contextmenu.self="(e) => e.preventDefault()"
|
||||||
>
|
>
|
||||||
<template v-for="(item, i) in items2">
|
<template v-for="(item, i) in items2">
|
||||||
<div v-if="item === null" class="divider"></div>
|
<div v-if="item === null" class="divider"></div>
|
||||||
<span v-else-if="item.type === 'label'" class="label item">
|
<span v-else-if="item.type === 'label'" class="label item">
|
||||||
<span :style="item.textStyle || ''">{{ item.text }}</span>
|
<span :style="item.textStyle || ''">{{ item.text }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="item.type === 'pending'" :tabindex="i" class="pending item">
|
<span
|
||||||
|
v-else-if="item.type === 'pending'"
|
||||||
|
:tabindex="i"
|
||||||
|
class="pending item"
|
||||||
|
>
|
||||||
<span><MkEllipsis /></span>
|
<span><MkEllipsis /></span>
|
||||||
</span>
|
</span>
|
||||||
<MkA v-else-if="item.type === 'link'" :to="item.to" :tabindex="i" class="_button item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
<MkA
|
||||||
<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
|
v-else-if="item.type === 'link'"
|
||||||
|
:to="item.to"
|
||||||
|
:tabindex="i"
|
||||||
|
class="_button item"
|
||||||
|
@click.passive="close(true)"
|
||||||
|
@mouseenter.passive="onItemMouseEnter(item)"
|
||||||
|
@mouseleave.passive="onItemMouseLeave(item)"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
v-if="item.icon"
|
||||||
|
class="ph-fw ph-lg"
|
||||||
|
:class="item.icon"
|
||||||
|
></i>
|
||||||
<span v-else-if="item.icons">
|
<span v-else-if="item.icons">
|
||||||
<i v-for="icon in item.icons" class="ph-fw ph-lg" :class="icon"></i>
|
<i
|
||||||
|
v-for="icon in item.icons"
|
||||||
|
class="ph-fw ph-lg"
|
||||||
|
:class="icon"
|
||||||
|
></i>
|
||||||
</span>
|
</span>
|
||||||
<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
|
<MkAvatar
|
||||||
|
v-if="item.avatar"
|
||||||
|
:user="item.avatar"
|
||||||
|
class="avatar"
|
||||||
|
/>
|
||||||
<span :style="item.textStyle || ''">{{ item.text }}</span>
|
<span :style="item.textStyle || ''">{{ item.text }}</span>
|
||||||
<span v-if="item.indicate" class="indicator"><i class="ph-circle ph-fill"></i></span>
|
<span v-if="item.indicate" class="indicator"
|
||||||
|
><i class="ph-circle ph-fill"></i
|
||||||
|
></span>
|
||||||
</MkA>
|
</MkA>
|
||||||
<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" :tabindex="i" class="_button item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
<a
|
||||||
<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
|
v-else-if="item.type === 'a'"
|
||||||
|
:href="item.href"
|
||||||
|
:target="item.target"
|
||||||
|
:download="item.download"
|
||||||
|
:tabindex="i"
|
||||||
|
class="_button item"
|
||||||
|
@click="close(true)"
|
||||||
|
@mouseenter.passive="onItemMouseEnter(item)"
|
||||||
|
@mouseleave.passive="onItemMouseLeave(item)"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
v-if="item.icon"
|
||||||
|
class="ph-fw ph-lg"
|
||||||
|
:class="item.icon"
|
||||||
|
></i>
|
||||||
<span v-else-if="item.icons">
|
<span v-else-if="item.icons">
|
||||||
<i v-for="icon in item.icons" class="ph-fw ph-lg" :class="icon"></i>
|
<i
|
||||||
|
v-for="icon in item.icons"
|
||||||
|
class="ph-fw ph-lg"
|
||||||
|
:class="icon"
|
||||||
|
></i>
|
||||||
</span>
|
</span>
|
||||||
<span :style="item.textStyle || ''">{{ item.text }}</span>
|
<span :style="item.textStyle || ''">{{ item.text }}</span>
|
||||||
<span v-if="item.indicate" class="indicator"><i class="ph-circle ph-fill"></i></span>
|
<span v-if="item.indicate" class="indicator"
|
||||||
|
><i class="ph-circle ph-fill"></i
|
||||||
|
></span>
|
||||||
</a>
|
</a>
|
||||||
<button v-else-if="item.type === 'user' && !items.hidden" :tabindex="i" class="_button item" :class="{ active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
<button
|
||||||
<MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/>
|
v-else-if="item.type === 'user' && !items.hidden"
|
||||||
<span v-if="item.indicate" class="indicator"><i class="ph-circle ph-fill"></i></span>
|
:tabindex="i"
|
||||||
|
class="_button item"
|
||||||
|
:class="{ active: item.active }"
|
||||||
|
:disabled="item.active"
|
||||||
|
@click="clicked(item.action, $event)"
|
||||||
|
@mouseenter.passive="onItemMouseEnter(item)"
|
||||||
|
@mouseleave.passive="onItemMouseLeave(item)"
|
||||||
|
>
|
||||||
|
<MkAvatar :user="item.user" class="avatar" /><MkUserName
|
||||||
|
:user="item.user"
|
||||||
|
/>
|
||||||
|
<span v-if="item.indicate" class="indicator"
|
||||||
|
><i class="ph-circle ph-fill"></i
|
||||||
|
></span>
|
||||||
</button>
|
</button>
|
||||||
<span v-else-if="item.type === 'switch'" :tabindex="i" class="item" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
<span
|
||||||
<FormSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch" :style="item.textStyle || ''">{{ item.text }}</FormSwitch>
|
v-else-if="item.type === 'switch'"
|
||||||
|
:tabindex="i"
|
||||||
|
class="item"
|
||||||
|
@mouseenter.passive="onItemMouseEnter(item)"
|
||||||
|
@mouseleave.passive="onItemMouseLeave(item)"
|
||||||
|
>
|
||||||
|
<FormSwitch
|
||||||
|
v-model="item.ref"
|
||||||
|
:disabled="item.disabled"
|
||||||
|
class="form-switch"
|
||||||
|
:style="item.textStyle || ''"
|
||||||
|
>{{ item.text }}</FormSwitch
|
||||||
|
>
|
||||||
</span>
|
</span>
|
||||||
<button v-else-if="item.type === 'parent'" :tabindex="i" class="_button item parent" :class="{ childShowing: childShowingItem === item }" @mouseenter="showChildren(item, $event)">
|
<button
|
||||||
<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
|
v-else-if="item.type === 'parent'"
|
||||||
|
:tabindex="i"
|
||||||
|
class="_button item parent"
|
||||||
|
:class="{ childShowing: childShowingItem === item }"
|
||||||
|
@mouseenter="showChildren(item, $event)"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
v-if="item.icon"
|
||||||
|
class="ph-fw ph-lg"
|
||||||
|
:class="item.icon"
|
||||||
|
></i>
|
||||||
<span v-else-if="item.icons">
|
<span v-else-if="item.icons">
|
||||||
<i v-for="icon in item.icons" class="ph-fw ph-lg" :class="icon"></i>
|
<i
|
||||||
|
v-for="icon in item.icons"
|
||||||
|
class="ph-fw ph-lg"
|
||||||
|
:class="icon"
|
||||||
|
></i>
|
||||||
</span>
|
</span>
|
||||||
<span :style="item.textStyle || ''">{{ item.text }}</span>
|
<span :style="item.textStyle || ''">{{ item.text }}</span>
|
||||||
<span class="caret"><i class="ph-caret-right ph-bold ph-lg ph-fw ph-lg"></i></span>
|
<span class="caret"
|
||||||
|
><i class="ph-caret-right ph-bold ph-lg ph-fw ph-lg"></i
|
||||||
|
></span>
|
||||||
</button>
|
</button>
|
||||||
<button v-else-if="!item.hidden" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
<button
|
||||||
<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
|
v-else-if="!item.hidden"
|
||||||
|
:tabindex="i"
|
||||||
|
class="_button item"
|
||||||
|
:class="{ danger: item.danger, active: item.active }"
|
||||||
|
:disabled="item.active"
|
||||||
|
@click="clicked(item.action, $event)"
|
||||||
|
@mouseenter.passive="onItemMouseEnter(item)"
|
||||||
|
@mouseleave.passive="onItemMouseLeave(item)"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
v-if="item.icon"
|
||||||
|
class="ph-fw ph-lg"
|
||||||
|
:class="item.icon"
|
||||||
|
></i>
|
||||||
<span v-else-if="item.icons">
|
<span v-else-if="item.icons">
|
||||||
<i v-for="icon in item.icons" class="ph-fw ph-lg" :class="icon"></i>
|
<i
|
||||||
|
v-for="icon in item.icons"
|
||||||
|
class="ph-fw ph-lg"
|
||||||
|
:class="icon"
|
||||||
|
></i>
|
||||||
</span>
|
</span>
|
||||||
<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
|
<MkAvatar
|
||||||
|
v-if="item.avatar"
|
||||||
|
:user="item.avatar"
|
||||||
|
class="avatar"
|
||||||
|
/>
|
||||||
<span :style="item.textStyle || ''">{{ item.text }}</span>
|
<span :style="item.textStyle || ''">{{ item.text }}</span>
|
||||||
<span v-if="item.indicate" class="indicator"><i class="ph-circle ph-fill"></i></span>
|
<span v-if="item.indicate" class="indicator"
|
||||||
|
><i class="ph-circle ph-fill"></i
|
||||||
|
></span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<span v-if="items2.length === 0" class="none item">
|
<span v-if="items2.length === 0" class="none item">
|
||||||
|
@ -62,32 +176,50 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="childMenu" class="child">
|
<div v-if="childMenu" class="child">
|
||||||
<XChild ref="child" :items="childMenu" :target-element="childTarget" :root-element="itemsEl" showing @actioned="childActioned"/>
|
<XChild
|
||||||
|
ref="child"
|
||||||
|
:items="childMenu"
|
||||||
|
:target-element="childTarget"
|
||||||
|
:root-element="itemsEl"
|
||||||
|
showing
|
||||||
|
@actioned="childActioned"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, menu, defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
|
import {
|
||||||
import { focusPrev, focusNext } from '@/scripts/focus';
|
computed,
|
||||||
import FormSwitch from '@/components/form/switch.vue';
|
menu,
|
||||||
import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from '@/types/menu';
|
defineAsyncComponent,
|
||||||
import * as os from '@/os';
|
nextTick,
|
||||||
import { i18n } from '@/i18n';
|
onBeforeUnmount,
|
||||||
|
onMounted,
|
||||||
|
onUnmounted,
|
||||||
|
Ref,
|
||||||
|
ref,
|
||||||
|
watch,
|
||||||
|
} from "vue";
|
||||||
|
import { focusPrev, focusNext } from "@/scripts/focus";
|
||||||
|
import FormSwitch from "@/components/form/switch.vue";
|
||||||
|
import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from "@/types/menu";
|
||||||
|
import * as os from "@/os";
|
||||||
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
const XChild = defineAsyncComponent(() => import('./MkMenu.child.vue'));
|
const XChild = defineAsyncComponent(() => import("./MkMenu.child.vue"));
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
items: MenuItem[];
|
items: MenuItem[];
|
||||||
viaKeyboard?: boolean;
|
viaKeyboard?: boolean;
|
||||||
asDrawer?: boolean;
|
asDrawer?: boolean;
|
||||||
align?: 'center' | string;
|
align?: "center" | string;
|
||||||
width?: number;
|
width?: number;
|
||||||
maxHeight?: number;
|
maxHeight?: number;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'close', actioned?: boolean): void;
|
(ev: "close", actioned?: boolean): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let itemsEl = $ref<HTMLDivElement>();
|
let itemsEl = $ref<HTMLDivElement>();
|
||||||
|
@ -97,31 +229,38 @@ let items2: InnerMenuItem[] = $ref([]);
|
||||||
let child = $ref<InstanceType<typeof XChild>>();
|
let child = $ref<InstanceType<typeof XChild>>();
|
||||||
|
|
||||||
let keymap = computed(() => ({
|
let keymap = computed(() => ({
|
||||||
'up|k|shift+tab': focusUp,
|
"up|k|shift+tab": focusUp,
|
||||||
'down|j|tab': focusDown,
|
"down|j|tab": focusDown,
|
||||||
'esc': close,
|
esc: close,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let childShowingItem = $ref<MenuItem | null>();
|
let childShowingItem = $ref<MenuItem | null>();
|
||||||
|
|
||||||
watch(() => props.items, () => {
|
watch(
|
||||||
const items: (MenuItem | MenuPending)[] = [...props.items].filter(item => item !== undefined);
|
() => props.items,
|
||||||
|
() => {
|
||||||
|
const items: (MenuItem | MenuPending)[] = [...props.items].filter(
|
||||||
|
(item) => item !== undefined
|
||||||
|
);
|
||||||
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
for (let i = 0; i < items.length; i++) {
|
||||||
const item = items[i];
|
const item = items[i];
|
||||||
|
|
||||||
if (item && 'then' in item) { // if item is Promise
|
if (item && "then" in item) {
|
||||||
items[i] = { type: 'pending' };
|
// if item is Promise
|
||||||
item.then(actualItem => {
|
items[i] = { type: "pending" };
|
||||||
|
item.then((actualItem) => {
|
||||||
items2[i] = actualItem;
|
items2[i] = actualItem;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items2 = items as InnerMenuItem[];
|
items2 = items as InnerMenuItem[];
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
immediate: true,
|
immediate: true,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
let childMenu = $ref<MenuItem[] | null>();
|
let childMenu = $ref<MenuItem[] | null>();
|
||||||
let childTarget = $ref<HTMLElement | null>();
|
let childTarget = $ref<HTMLElement | null>();
|
||||||
|
@ -137,7 +276,11 @@ function childActioned() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function onGlobalMousedown(event: MouseEvent) {
|
function onGlobalMousedown(event: MouseEvent) {
|
||||||
if (childTarget && (event.target === childTarget || childTarget.contains(event.target))) return;
|
if (
|
||||||
|
childTarget &&
|
||||||
|
(event.target === childTarget || childTarget.contains(event.target))
|
||||||
|
)
|
||||||
|
return;
|
||||||
if (child && child.checkHit(event)) return;
|
if (child && child.checkHit(event)) return;
|
||||||
closeChild();
|
closeChild();
|
||||||
}
|
}
|
||||||
|
@ -169,7 +312,7 @@ function clicked(fn: MenuAction, ev: MouseEvent) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function close(actioned = false) {
|
function close(actioned = false) {
|
||||||
emit('close', actioned);
|
emit("close", actioned);
|
||||||
}
|
}
|
||||||
|
|
||||||
function focusUp() {
|
function focusUp() {
|
||||||
|
@ -187,11 +330,13 @@ onMounted(() => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('mousedown', onGlobalMousedown, { passive: true });
|
document.addEventListener("mousedown", onGlobalMousedown, {
|
||||||
|
passive: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
document.removeEventListener('mousedown', onGlobalMousedown);
|
document.removeEventListener("mousedown", onGlobalMousedown);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,11 @@
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient :id="gradientId" x1="0" x2="0" y1="1" y2="0">
|
<linearGradient :id="gradientId" x1="0" x2="0" y1="1" y2="0">
|
||||||
<stop offset="0%" :stop-color="color" stop-opacity="0"></stop>
|
<stop offset="0%" :stop-color="color" stop-opacity="0"></stop>
|
||||||
<stop offset="100%" :stop-color="color" stop-opacity="0.65"></stop>
|
<stop
|
||||||
|
offset="100%"
|
||||||
|
:stop-color="color"
|
||||||
|
stop-opacity="0.65"
|
||||||
|
></stop>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<polygon
|
<polygon
|
||||||
|
@ -16,20 +20,15 @@
|
||||||
:stroke="color"
|
:stroke="color"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
/>
|
/>
|
||||||
<circle
|
<circle :cx="headX" :cy="headY" r="3" :fill="color" />
|
||||||
:cx="headX"
|
|
||||||
:cy="headY"
|
|
||||||
r="3"
|
|
||||||
:fill="color"
|
|
||||||
/>
|
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onUnmounted, watch } from 'vue';
|
import { onUnmounted, watch } from "vue";
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from "uuid";
|
||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from "tinycolor2";
|
||||||
import { useInterval } from '@/scripts/use-interval';
|
import { useInterval } from "@/scripts/use-interval";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
src: number[];
|
src: number[];
|
||||||
|
@ -38,12 +37,14 @@ const props = defineProps<{
|
||||||
const viewBoxX = 50;
|
const viewBoxX = 50;
|
||||||
const viewBoxY = 50;
|
const viewBoxY = 50;
|
||||||
const gradientId = uuid();
|
const gradientId = uuid();
|
||||||
let polylinePoints = $ref('');
|
let polylinePoints = $ref("");
|
||||||
let polygonPoints = $ref('');
|
let polygonPoints = $ref("");
|
||||||
let headX = $ref<number | null>(null);
|
let headX = $ref<number | null>(null);
|
||||||
let headY = $ref<number | null>(null);
|
let headY = $ref<number | null>(null);
|
||||||
let clock = $ref<number | null>(null);
|
let clock = $ref<number | null>(null);
|
||||||
const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--accent'));
|
const accent = tinycolor(
|
||||||
|
getComputedStyle(document.documentElement).getPropertyValue("--accent")
|
||||||
|
);
|
||||||
const color = accent.toRgbString();
|
const color = accent.toRgbString();
|
||||||
|
|
||||||
function draw(): void {
|
function draw(): void {
|
||||||
|
@ -52,10 +53,10 @@ function draw(): void {
|
||||||
|
|
||||||
const _polylinePoints = stats.map((n, i) => [
|
const _polylinePoints = stats.map((n, i) => [
|
||||||
i * (viewBoxX / (stats.length - 1)),
|
i * (viewBoxX / (stats.length - 1)),
|
||||||
(1 - (n / peak)) * viewBoxY,
|
(1 - n / peak) * viewBoxY,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
polylinePoints = _polylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
|
polylinePoints = _polylinePoints.map((xy) => `${xy[0]},${xy[1]}`).join(" ");
|
||||||
|
|
||||||
polygonPoints = `0,${viewBoxY} ${polylinePoints} ${viewBoxX},${viewBoxY}`;
|
polygonPoints = `0,${viewBoxY} ${polylinePoints} ${viewBoxX},${viewBoxY}`;
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,64 @@
|
||||||
<template>
|
<template>
|
||||||
<Transition
|
<Transition
|
||||||
:name="transitionName"
|
:name="transitionName"
|
||||||
:enter-active-class="$style['transition_' + transitionName + '_enterActive']"
|
:enter-active-class="
|
||||||
:leave-active-class="$style['transition_' + transitionName + '_leaveActive']"
|
$style['transition_' + transitionName + '_enterActive']
|
||||||
:enter-from-class="$style['transition_' + transitionName + '_enterFrom']"
|
"
|
||||||
|
:leave-active-class="
|
||||||
|
$style['transition_' + transitionName + '_leaveActive']
|
||||||
|
"
|
||||||
|
:enter-from-class="
|
||||||
|
$style['transition_' + transitionName + '_enterFrom']
|
||||||
|
"
|
||||||
:leave-to-class="$style['transition_' + transitionName + '_leaveTo']"
|
:leave-to-class="$style['transition_' + transitionName + '_leaveTo']"
|
||||||
:duration="transitionDuration" appear @after-leave="emit('closed')" @enter="emit('opening')" @after-enter="onOpened"
|
:duration="transitionDuration"
|
||||||
|
appear
|
||||||
|
@after-leave="emit('closed')"
|
||||||
|
@enter="emit('opening')"
|
||||||
|
@after-enter="onOpened"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-show="manualShowing != null ? manualShowing : showing"
|
||||||
|
v-hotkey.global="keymap"
|
||||||
|
:class="[
|
||||||
|
$style.root,
|
||||||
|
{
|
||||||
|
[$style.drawer]: type === 'drawer',
|
||||||
|
[$style.dialog]: type === 'dialog' || type === 'dialog:top',
|
||||||
|
[$style.popup]: type === 'popup',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
:style="{
|
||||||
|
zIndex,
|
||||||
|
pointerEvents: (manualShowing != null ? manualShowing : showing)
|
||||||
|
? 'auto'
|
||||||
|
: 'none',
|
||||||
|
'--transformOrigin': transformOrigin,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="_modalBg data-cy-bg"
|
||||||
|
:class="[
|
||||||
|
$style.bg,
|
||||||
|
{
|
||||||
|
[$style.bgTransparent]: isEnableBgTransparent,
|
||||||
|
'data-cy-transparent': isEnableBgTransparent,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
:style="{ zIndex }"
|
||||||
|
@click="onBgClick"
|
||||||
|
@mousedown="onBgClick"
|
||||||
|
@contextmenu.prevent.stop="() => {}"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
ref="content"
|
||||||
|
:class="[
|
||||||
|
$style.content,
|
||||||
|
{ [$style.fixed]: fixed, top: type === 'dialog:top' },
|
||||||
|
]"
|
||||||
|
:style="{ zIndex }"
|
||||||
|
@click.self="onBgClick"
|
||||||
>
|
>
|
||||||
<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" :class="[$style.root, { [$style.drawer]: type === 'drawer', [$style.dialog]: type === 'dialog' || type === 'dialog:top', [$style.popup]: type === 'popup' }]" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
|
|
||||||
<div class="_modalBg data-cy-bg" :class="[$style.bg, { [$style.bgTransparent]: isEnableBgTransparent, 'data-cy-transparent': isEnableBgTransparent }]" :style="{ zIndex }" @click="onBgClick" @mousedown="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
|
|
||||||
<div ref="content" :class="[$style.content, { [$style.fixed]: fixed, top: type === 'dialog:top' }]" :style="{ zIndex }" @click.self="onBgClick">
|
|
||||||
<slot :max-height="maxHeight" :type="type"></slot>
|
<slot :max-height="maxHeight" :type="type"></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -17,94 +66,103 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { nextTick, onMounted, watch, provide } from 'vue';
|
import { nextTick, onMounted, watch, provide } from "vue";
|
||||||
import * as os from '@/os';
|
import * as os from "@/os";
|
||||||
import { isTouchUsing } from '@/scripts/touch';
|
import { isTouchUsing } from "@/scripts/touch";
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from "@/store";
|
||||||
import { deviceKind } from '@/scripts/device-kind';
|
import { deviceKind } from "@/scripts/device-kind";
|
||||||
|
|
||||||
function getFixedContainer(el: Element | null): Element | null {
|
function getFixedContainer(el: Element | null): Element | null {
|
||||||
if (el == null || el.tagName === 'BODY') return null;
|
if (el == null || el.tagName === "BODY") return null;
|
||||||
const position = window.getComputedStyle(el).getPropertyValue('position');
|
const position = window.getComputedStyle(el).getPropertyValue("position");
|
||||||
if (position === 'fixed') {
|
if (position === "fixed") {
|
||||||
return el;
|
return el;
|
||||||
} else {
|
} else {
|
||||||
return getFixedContainer(el.parentElement);
|
return getFixedContainer(el.parentElement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ModalTypes = 'popup' | 'dialog' | 'dialog:top' | 'drawer';
|
type ModalTypes = "popup" | "dialog" | "dialog:top" | "drawer";
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
manualShowing?: boolean | null;
|
manualShowing?: boolean | null;
|
||||||
anchor?: { x: string; y: string; };
|
anchor?: { x: string; y: string };
|
||||||
src?: HTMLElement;
|
src?: HTMLElement;
|
||||||
preferType?: ModalTypes | 'auto';
|
preferType?: ModalTypes | "auto";
|
||||||
zPriority?: 'low' | 'middle' | 'high';
|
zPriority?: "low" | "middle" | "high";
|
||||||
noOverlap?: boolean;
|
noOverlap?: boolean;
|
||||||
transparentBg?: boolean;
|
transparentBg?: boolean;
|
||||||
}>(), {
|
}>(),
|
||||||
|
{
|
||||||
manualShowing: null,
|
manualShowing: null,
|
||||||
src: null,
|
src: null,
|
||||||
anchor: () => ({ x: 'center', y: 'bottom' }),
|
anchor: () => ({ x: "center", y: "bottom" }),
|
||||||
preferType: 'auto',
|
preferType: "auto",
|
||||||
zPriority: 'low',
|
zPriority: "low",
|
||||||
noOverlap: true,
|
noOverlap: true,
|
||||||
transparentBg: false,
|
transparentBg: false,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'opening'): void;
|
(ev: "opening"): void;
|
||||||
(ev: 'opened'): void;
|
(ev: "opened"): void;
|
||||||
(ev: 'click'): void;
|
(ev: "click"): void;
|
||||||
(ev: 'esc'): void;
|
(ev: "esc"): void;
|
||||||
(ev: 'close'): void;
|
(ev: "close"): void;
|
||||||
(ev: 'closed'): void;
|
(ev: "closed"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
provide('modal', true);
|
provide("modal", true);
|
||||||
|
|
||||||
let maxHeight = $ref<number>();
|
let maxHeight = $ref<number>();
|
||||||
let fixed = $ref(false);
|
let fixed = $ref(false);
|
||||||
let transformOrigin = $ref('center');
|
let transformOrigin = $ref("center");
|
||||||
let showing = $ref(true);
|
let showing = $ref(true);
|
||||||
let content = $shallowRef<HTMLElement>();
|
let content = $shallowRef<HTMLElement>();
|
||||||
const zIndex = os.claimZIndex(props.zPriority);
|
const zIndex = os.claimZIndex(props.zPriority);
|
||||||
let useSendAnime = $ref(false);
|
let useSendAnime = $ref(false);
|
||||||
const type = $computed<ModalTypes>(() => {
|
const type = $computed<ModalTypes>(() => {
|
||||||
if (props.preferType === 'auto') {
|
if (props.preferType === "auto") {
|
||||||
if (!defaultStore.state.disableDrawer && isTouchUsing && deviceKind === 'smartphone') {
|
if (
|
||||||
return 'drawer';
|
!defaultStore.state.disableDrawer &&
|
||||||
|
isTouchUsing &&
|
||||||
|
deviceKind === "smartphone"
|
||||||
|
) {
|
||||||
|
return "drawer";
|
||||||
} else {
|
} else {
|
||||||
return props.src != null ? 'popup' : 'dialog';
|
return props.src != null ? "popup" : "dialog";
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return props.preferType!;
|
return props.preferType!;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const isEnableBgTransparent = $computed(() => props.transparentBg && (type === 'popup'));
|
const isEnableBgTransparent = $computed(
|
||||||
let transitionName = $computed((() =>
|
() => props.transparentBg && type === "popup"
|
||||||
|
);
|
||||||
|
let transitionName = $computed(() =>
|
||||||
defaultStore.state.animation
|
defaultStore.state.animation
|
||||||
? useSendAnime
|
? useSendAnime
|
||||||
? 'send'
|
? "send"
|
||||||
: type === 'drawer'
|
: type === "drawer"
|
||||||
? 'modal-drawer'
|
? "modal-drawer"
|
||||||
: type === 'popup'
|
: type === "popup"
|
||||||
? 'modal-popup'
|
? "modal-popup"
|
||||||
: 'modal'
|
: "modal"
|
||||||
: ''
|
: ""
|
||||||
));
|
);
|
||||||
let transitionDuration = $computed((() =>
|
let transitionDuration = $computed(() =>
|
||||||
transitionName === 'send'
|
transitionName === "send"
|
||||||
? 400
|
? 400
|
||||||
: transitionName === 'modal-popup'
|
: transitionName === "modal-popup"
|
||||||
? 100
|
? 100
|
||||||
: transitionName === 'modal'
|
: transitionName === "modal"
|
||||||
? 200
|
? 200
|
||||||
: transitionName === 'modal-drawer'
|
: transitionName === "modal-drawer"
|
||||||
? 200
|
? 200
|
||||||
: 0
|
: 0
|
||||||
));
|
);
|
||||||
|
|
||||||
let contentClicking = false;
|
let contentClicking = false;
|
||||||
|
|
||||||
|
@ -114,30 +172,30 @@ function close(opts: { useSendAnimation?: boolean } = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line vue/no-mutating-props
|
// eslint-disable-next-line vue/no-mutating-props
|
||||||
if (props.src) props.src.style.pointerEvents = 'auto';
|
if (props.src) props.src.style.pointerEvents = "auto";
|
||||||
showing = false;
|
showing = false;
|
||||||
emit('close');
|
emit("close");
|
||||||
}
|
}
|
||||||
|
|
||||||
function onBgClick() {
|
function onBgClick() {
|
||||||
if (contentClicking) return;
|
if (contentClicking) return;
|
||||||
emit('click');
|
emit("click");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'drawer') {
|
if (type === "drawer") {
|
||||||
maxHeight = window.innerHeight / 1.5;
|
maxHeight = window.innerHeight / 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
const keymap = {
|
const keymap = {
|
||||||
'esc': () => emit('esc'),
|
esc: () => emit("esc"),
|
||||||
};
|
};
|
||||||
|
|
||||||
const MARGIN = 16;
|
const MARGIN = 16;
|
||||||
|
|
||||||
const align = () => {
|
const align = () => {
|
||||||
if (props.src == null) return;
|
if (props.src == null) return;
|
||||||
if (type === 'drawer') return;
|
if (type === "drawer") return;
|
||||||
if (type === 'dialog') return;
|
if (type === "dialog") return;
|
||||||
|
|
||||||
if (content == null) return;
|
if (content == null) return;
|
||||||
|
|
||||||
|
@ -152,19 +210,19 @@ const align = () => {
|
||||||
const x = srcRect.left + (fixed ? 0 : window.pageXOffset);
|
const x = srcRect.left + (fixed ? 0 : window.pageXOffset);
|
||||||
const y = srcRect.top + (fixed ? 0 : window.pageYOffset);
|
const y = srcRect.top + (fixed ? 0 : window.pageYOffset);
|
||||||
|
|
||||||
if (props.anchor.x === 'center') {
|
if (props.anchor.x === "center") {
|
||||||
left = x + (props.src.offsetWidth / 2) - (width / 2);
|
left = x + props.src.offsetWidth / 2 - width / 2;
|
||||||
} else if (props.anchor.x === 'left') {
|
} else if (props.anchor.x === "left") {
|
||||||
// TODO
|
// TODO
|
||||||
} else if (props.anchor.x === 'right') {
|
} else if (props.anchor.x === "right") {
|
||||||
left = x + props.src.offsetWidth;
|
left = x + props.src.offsetWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.anchor.y === 'center') {
|
if (props.anchor.y === "center") {
|
||||||
top = (y - (height / 2));
|
top = y - height / 2;
|
||||||
} else if (props.anchor.y === 'top') {
|
} else if (props.anchor.y === "top") {
|
||||||
// TODO
|
// TODO
|
||||||
} else if (props.anchor.y === 'bottom') {
|
} else if (props.anchor.y === "bottom") {
|
||||||
top = y + props.src.offsetHeight;
|
top = y + props.src.offsetHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,20 +232,20 @@ const align = () => {
|
||||||
left = window.innerWidth - width;
|
left = window.innerWidth - width;
|
||||||
}
|
}
|
||||||
|
|
||||||
const underSpace = (window.innerHeight - MARGIN) - top;
|
const underSpace = window.innerHeight - MARGIN - top;
|
||||||
const upperSpace = (srcRect.top - MARGIN);
|
const upperSpace = srcRect.top - MARGIN;
|
||||||
|
|
||||||
// 画面から縦にはみ出る場合
|
// 画面から縦にはみ出る場合
|
||||||
if (top + height > (window.innerHeight - MARGIN)) {
|
if (top + height > window.innerHeight - MARGIN) {
|
||||||
if (props.noOverlap && props.anchor.x === 'center') {
|
if (props.noOverlap && props.anchor.x === "center") {
|
||||||
if (underSpace >= (upperSpace / 3)) {
|
if (underSpace >= upperSpace / 3) {
|
||||||
maxHeight = underSpace;
|
maxHeight = underSpace;
|
||||||
} else {
|
} else {
|
||||||
maxHeight = upperSpace;
|
maxHeight = upperSpace;
|
||||||
top = (upperSpace + MARGIN) - height;
|
top = upperSpace + MARGIN - height;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
top = (window.innerHeight - MARGIN) - height;
|
top = window.innerHeight - MARGIN - height;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
maxHeight = underSpace;
|
maxHeight = underSpace;
|
||||||
|
@ -198,20 +256,26 @@ const align = () => {
|
||||||
left = window.innerWidth - width + window.scrollX - 1;
|
left = window.innerWidth - width + window.scrollX - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const underSpace = (window.innerHeight - MARGIN) - (top - window.pageYOffset);
|
const underSpace =
|
||||||
const upperSpace = (srcRect.top - MARGIN);
|
window.innerHeight - MARGIN - (top - window.pageYOffset);
|
||||||
|
const upperSpace = srcRect.top - MARGIN;
|
||||||
|
|
||||||
// 画面から縦にはみ出る場合
|
// 画面から縦にはみ出る場合
|
||||||
if (top + height - window.scrollY > (window.innerHeight - MARGIN)) {
|
if (top + height - window.scrollY > window.innerHeight - MARGIN) {
|
||||||
if (props.noOverlap && props.anchor.x === 'center') {
|
if (props.noOverlap && props.anchor.x === "center") {
|
||||||
if (underSpace >= (upperSpace / 3)) {
|
if (underSpace >= upperSpace / 3) {
|
||||||
maxHeight = underSpace;
|
maxHeight = underSpace;
|
||||||
} else {
|
} else {
|
||||||
maxHeight = upperSpace;
|
maxHeight = upperSpace;
|
||||||
top = window.scrollY + ((upperSpace + MARGIN) - height);
|
top = window.scrollY + (upperSpace + MARGIN - height);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
top = (window.innerHeight - MARGIN) - height + window.pageYOffset - 1;
|
top =
|
||||||
|
window.innerHeight -
|
||||||
|
MARGIN -
|
||||||
|
height +
|
||||||
|
window.pageYOffset -
|
||||||
|
1;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
maxHeight = underSpace;
|
maxHeight = underSpace;
|
||||||
|
@ -226,55 +290,76 @@ const align = () => {
|
||||||
left = 0;
|
left = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let transformOriginX = 'center';
|
let transformOriginX = "center";
|
||||||
let transformOriginY = 'center';
|
let transformOriginY = "center";
|
||||||
|
|
||||||
if (top >= srcRect.top + props.src.offsetHeight + (fixed ? 0 : window.pageYOffset)) {
|
if (
|
||||||
transformOriginY = 'top';
|
top >=
|
||||||
} else if ((top + height) <= srcRect.top + (fixed ? 0 : window.pageYOffset)) {
|
srcRect.top + props.src.offsetHeight + (fixed ? 0 : window.pageYOffset)
|
||||||
transformOriginY = 'bottom';
|
) {
|
||||||
|
transformOriginY = "top";
|
||||||
|
} else if (top + height <= srcRect.top + (fixed ? 0 : window.pageYOffset)) {
|
||||||
|
transformOriginY = "bottom";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (left >= srcRect.left + props.src.offsetWidth + (fixed ? 0 : window.pageXOffset)) {
|
if (
|
||||||
transformOriginX = 'left';
|
left >=
|
||||||
} else if ((left + width) <= srcRect.left + (fixed ? 0 : window.pageXOffset)) {
|
srcRect.left + props.src.offsetWidth + (fixed ? 0 : window.pageXOffset)
|
||||||
transformOriginX = 'right';
|
) {
|
||||||
|
transformOriginX = "left";
|
||||||
|
} else if (
|
||||||
|
left + width <=
|
||||||
|
srcRect.left + (fixed ? 0 : window.pageXOffset)
|
||||||
|
) {
|
||||||
|
transformOriginX = "right";
|
||||||
}
|
}
|
||||||
|
|
||||||
transformOrigin = `${transformOriginX} ${transformOriginY}`;
|
transformOrigin = `${transformOriginX} ${transformOriginY}`;
|
||||||
|
|
||||||
content.style.left = left + 'px';
|
content.style.left = left + "px";
|
||||||
content.style.top = top + 'px';
|
content.style.top = top + "px";
|
||||||
};
|
};
|
||||||
|
|
||||||
const onOpened = () => {
|
const onOpened = () => {
|
||||||
emit('opened');
|
emit("opened");
|
||||||
|
|
||||||
// モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する
|
// モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する
|
||||||
const el = content!.children[0];
|
const el = content!.children[0];
|
||||||
el.addEventListener('mousedown', ev => {
|
el.addEventListener(
|
||||||
|
"mousedown",
|
||||||
|
(ev) => {
|
||||||
contentClicking = true;
|
contentClicking = true;
|
||||||
window.addEventListener('mouseup', ev => {
|
window.addEventListener(
|
||||||
|
"mouseup",
|
||||||
|
(ev) => {
|
||||||
// click イベントより先に mouseup イベントが発生するかもしれないのでちょっと待つ
|
// click イベントより先に mouseup イベントが発生するかもしれないのでちょっと待つ
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
contentClicking = false;
|
contentClicking = false;
|
||||||
}, 100);
|
}, 100);
|
||||||
}, { passive: true, once: true });
|
},
|
||||||
}, { passive: true });
|
{ passive: true, once: true }
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ passive: true }
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
watch(() => props.src, async () => {
|
watch(
|
||||||
|
() => props.src,
|
||||||
|
async () => {
|
||||||
if (props.src) {
|
if (props.src) {
|
||||||
// eslint-disable-next-line vue/no-mutating-props
|
// eslint-disable-next-line vue/no-mutating-props
|
||||||
props.src.style.pointerEvents = 'none';
|
props.src.style.pointerEvents = "none";
|
||||||
}
|
}
|
||||||
fixed = (type === 'drawer') || (getFixedContainer(props.src) != null);
|
fixed = type === "drawer" || getFixedContainer(props.src) != null;
|
||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
align();
|
align();
|
||||||
}, { immediate: true });
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
new ResizeObserver((entries, observer) => {
|
new ResizeObserver((entries, observer) => {
|
||||||
|
@ -297,7 +382,8 @@ defineExpose({
|
||||||
|
|
||||||
> .content {
|
> .content {
|
||||||
transform: translateY(0px);
|
transform: translateY(0px);
|
||||||
transition: opacity 0.3s ease-in, transform 0.3s cubic-bezier(.5,-0.5,1,.5) !important;
|
transition: opacity 0.3s ease-in,
|
||||||
|
transform 0.3s cubic-bezier(0.5, -0.5, 1, 0.5) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.transition_send_enterFrom,
|
.transition_send_enterFrom,
|
||||||
|
@ -346,7 +432,8 @@ defineExpose({
|
||||||
|
|
||||||
> .content {
|
> .content {
|
||||||
transform-origin: var(--transformOrigin);
|
transform-origin: var(--transformOrigin);
|
||||||
transition: opacity 0.2s cubic-bezier(0, 0, 0.2, 1), transform 0.2s cubic-bezier(0, 0, 0.2, 1) !important;
|
transition: opacity 0.2s cubic-bezier(0, 0, 0.2, 1),
|
||||||
|
transform 0.2s cubic-bezier(0, 0, 0.2, 1) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.transition_modal-popup_enterFrom,
|
.transition_modal-popup_enterFrom,
|
||||||
|
@ -369,7 +456,7 @@ defineExpose({
|
||||||
}
|
}
|
||||||
|
|
||||||
> .content {
|
> .content {
|
||||||
transition: transform 0.2s cubic-bezier(0,.5,0,1) !important;
|
transition: transform 0.2s cubic-bezier(0, 0.5, 0, 1) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.transition_modal-drawer_leaveActive {
|
.transition_modal-drawer_leaveActive {
|
||||||
|
@ -378,7 +465,7 @@ defineExpose({
|
||||||
}
|
}
|
||||||
|
|
||||||
> .content {
|
> .content {
|
||||||
transition: transform 0.2s cubic-bezier(0,.5,0,1) !important;
|
transition: transform 0.2s cubic-bezier(0, 0.5, 0, 1) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.transition_modal-drawer_enterFrom,
|
.transition_modal-drawer_enterFrom,
|
||||||
|
@ -404,15 +491,39 @@ defineExpose({
|
||||||
margin: auto;
|
margin: auto;
|
||||||
padding: 32px;
|
padding: 32px;
|
||||||
// TODO: mask-imageはiOSだとやたら重い。なんとかしたい
|
// TODO: mask-imageはiOSだとやたら重い。なんとかしたい
|
||||||
-webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 32px, rgba(0,0,0,1) calc(100% - 32px), rgba(0,0,0,0) 100%);
|
-webkit-mask-image: linear-gradient(
|
||||||
mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 32px, rgba(0,0,0,1) calc(100% - 32px), rgba(0,0,0,0) 100%);
|
0deg,
|
||||||
|
rgba(0, 0, 0, 0) 0%,
|
||||||
|
rgba(0, 0, 0, 1) 32px,
|
||||||
|
rgba(0, 0, 0, 1) calc(100% - 32px),
|
||||||
|
rgba(0, 0, 0, 0) 100%
|
||||||
|
);
|
||||||
|
mask-image: linear-gradient(
|
||||||
|
0deg,
|
||||||
|
rgba(0, 0, 0, 0) 0%,
|
||||||
|
rgba(0, 0, 0, 1) 32px,
|
||||||
|
rgba(0, 0, 0, 1) calc(100% - 32px),
|
||||||
|
rgba(0, 0, 0, 0) 100%
|
||||||
|
);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
@media (max-width: 500px) {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
-webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 16px, rgba(0,0,0,1) calc(100% - 16px), rgba(0,0,0,0) 100%);
|
-webkit-mask-image: linear-gradient(
|
||||||
mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 16px, rgba(0,0,0,1) calc(100% - 16px), rgba(0,0,0,0) 100%);
|
0deg,
|
||||||
|
rgba(0, 0, 0, 0) 0%,
|
||||||
|
rgba(0, 0, 0, 1) 16px,
|
||||||
|
rgba(0, 0, 0, 1) calc(100% - 16px),
|
||||||
|
rgba(0, 0, 0, 0) 100%
|
||||||
|
);
|
||||||
|
mask-image: linear-gradient(
|
||||||
|
0deg,
|
||||||
|
rgba(0, 0, 0, 0) 0%,
|
||||||
|
rgba(0, 0, 0, 1) 16px,
|
||||||
|
rgba(0, 0, 0, 1) calc(100% - 16px),
|
||||||
|
rgba(0, 0, 0, 0) 100%
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
> ::v-deep(*) {
|
> ::v-deep(*) {
|
||||||
|
@ -424,7 +535,6 @@ defineExpose({
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,45 @@
|
||||||
<template>
|
<template>
|
||||||
<MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')">
|
<MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')">
|
||||||
<div ref="rootEl" class="hrmcaedk _narrow_" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }">
|
<div
|
||||||
|
ref="rootEl"
|
||||||
|
class="hrmcaedk _narrow_"
|
||||||
|
:style="{
|
||||||
|
width: `${width}px`,
|
||||||
|
height: height ? `min(${height}px, 100%)` : '100%',
|
||||||
|
}"
|
||||||
|
>
|
||||||
<div class="header" @contextmenu="onContextmenu">
|
<div class="header" @contextmenu="onContextmenu">
|
||||||
<button v-if="history.length > 0" v-tooltip="i18n.ts.goBack" class="_button" @click="back()"><i class="ph-caret-left ph-bold ph-lg"></i></button>
|
<button
|
||||||
|
v-if="history.length > 0"
|
||||||
|
v-tooltip="i18n.ts.goBack"
|
||||||
|
class="_button"
|
||||||
|
@click="back()"
|
||||||
|
>
|
||||||
|
<i class="ph-caret-left ph-bold ph-lg"></i>
|
||||||
|
</button>
|
||||||
<span v-else style="display: inline-block; width: 20px"></span>
|
<span v-else style="display: inline-block; width: 20px"></span>
|
||||||
<span v-if="pageMetadata?.value" class="title">
|
<span v-if="pageMetadata?.value" class="title">
|
||||||
<i v-if="pageMetadata?.value.icon" class="icon" :class="pageMetadata?.value.icon"></i>
|
<i
|
||||||
|
v-if="pageMetadata?.value.icon"
|
||||||
|
class="icon"
|
||||||
|
:class="pageMetadata?.value.icon"
|
||||||
|
></i>
|
||||||
<span>{{ pageMetadata?.value.title }}</span>
|
<span>{{ pageMetadata?.value.title }}</span>
|
||||||
</span>
|
</span>
|
||||||
<button class="_button" @click="$refs.modal.close()"><i class="ph-x ph-bold ph-lg"></i></button>
|
<button class="_button" @click="$refs.modal.close()">
|
||||||
|
<i class="ph-x ph-bold ph-lg"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<MkStickyContainer>
|
<MkStickyContainer>
|
||||||
<template #header><MkPageHeader v-if="pageMetadata?.value && !pageMetadata?.value.hideHeader" :info="pageMetadata?.value"/></template>
|
<template #header
|
||||||
|
><MkPageHeader
|
||||||
|
v-if="
|
||||||
|
pageMetadata?.value &&
|
||||||
|
!pageMetadata?.value.hideHeader
|
||||||
|
"
|
||||||
|
:info="pageMetadata?.value"
|
||||||
|
/></template>
|
||||||
<RouterView :router="router" />
|
<RouterView :router="router" />
|
||||||
</MkStickyContainer>
|
</MkStickyContainer>
|
||||||
</div>
|
</div>
|
||||||
|
@ -21,31 +48,33 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ComputedRef, provide } from 'vue';
|
import { ComputedRef, provide } from "vue";
|
||||||
import MkModal from '@/components/MkModal.vue';
|
import MkModal from "@/components/MkModal.vue";
|
||||||
import { popout as _popout } from '@/scripts/popout';
|
import { popout as _popout } from "@/scripts/popout";
|
||||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
import copyToClipboard from "@/scripts/copy-to-clipboard";
|
||||||
import { url } from '@/config';
|
import { url } from "@/config";
|
||||||
import * as os from '@/os';
|
import * as os from "@/os";
|
||||||
import { mainRouter, routes } from '@/router';
|
import { mainRouter, routes } from "@/router";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
import { PageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
|
import {
|
||||||
import { Router } from '@/nirax';
|
PageMetadata,
|
||||||
|
provideMetadataReceiver,
|
||||||
|
setPageMetadata,
|
||||||
|
} from "@/scripts/page-metadata";
|
||||||
|
import { Router } from "@/nirax";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
initialPath: string;
|
initialPath: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
(ev: 'closed'): void;
|
(ev: "closed"): void;
|
||||||
(ev: 'click'): void;
|
(ev: "click"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const router = new Router(routes, props.initialPath);
|
const router = new Router(routes, props.initialPath);
|
||||||
|
|
||||||
router.addListener('push', ctx => {
|
router.addListener("push", (ctx) => {});
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
|
let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
|
||||||
let rootEl = $ref();
|
let rootEl = $ref();
|
||||||
|
@ -55,40 +84,47 @@ let width = $ref(860);
|
||||||
let height = $ref(660);
|
let height = $ref(660);
|
||||||
const history = [];
|
const history = [];
|
||||||
|
|
||||||
provide('router', router);
|
provide("router", router);
|
||||||
provideMetadataReceiver((info) => {
|
provideMetadataReceiver((info) => {
|
||||||
pageMetadata = info;
|
pageMetadata = info;
|
||||||
});
|
});
|
||||||
provide('shouldOmitHeaderTitle', true);
|
provide("shouldOmitHeaderTitle", true);
|
||||||
provide('shouldHeaderThin', true);
|
provide("shouldHeaderThin", true);
|
||||||
|
|
||||||
const pageUrl = $computed(() => url + path);
|
const pageUrl = $computed(() => url + path);
|
||||||
const contextmenu = $computed(() => {
|
const contextmenu = $computed(() => {
|
||||||
return [{
|
return [
|
||||||
type: 'label',
|
{
|
||||||
|
type: "label",
|
||||||
text: path,
|
text: path,
|
||||||
}, {
|
},
|
||||||
icon: 'ph-arrows-out-simple ph-bold ph-lg',
|
{
|
||||||
|
icon: "ph-arrows-out-simple ph-bold ph-lg",
|
||||||
text: i18n.ts.showInPage,
|
text: i18n.ts.showInPage,
|
||||||
action: expand,
|
action: expand,
|
||||||
}, {
|
},
|
||||||
icon: 'ph-arrow-square-out ph-bold ph-lg',
|
{
|
||||||
|
icon: "ph-arrow-square-out ph-bold ph-lg",
|
||||||
text: i18n.ts.popout,
|
text: i18n.ts.popout,
|
||||||
action: popout,
|
action: popout,
|
||||||
}, null, {
|
},
|
||||||
icon: 'ph-arrow-square-out ph-bold ph-lg',
|
null,
|
||||||
|
{
|
||||||
|
icon: "ph-arrow-square-out ph-bold ph-lg",
|
||||||
text: i18n.ts.openInNewTab,
|
text: i18n.ts.openInNewTab,
|
||||||
action: () => {
|
action: () => {
|
||||||
window.open(pageUrl, '_blank');
|
window.open(pageUrl, "_blank");
|
||||||
modal.close();
|
modal.close();
|
||||||
},
|
},
|
||||||
}, {
|
},
|
||||||
icon: 'ph-link-simple ph-bold ph-lg',
|
{
|
||||||
|
icon: "ph-link-simple ph-bold ph-lg",
|
||||||
text: i18n.ts.copyLink,
|
text: i18n.ts.copyLink,
|
||||||
action: () => {
|
action: () => {
|
||||||
copyToClipboard(pageUrl);
|
copyToClipboard(pageUrl);
|
||||||
},
|
},
|
||||||
}];
|
},
|
||||||
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
function navigate(path, record = true) {
|
function navigate(path, record = true) {
|
||||||
|
|
|
@ -1,13 +1,51 @@
|
||||||
<template>
|
<template>
|
||||||
<MkModal ref="modal" :prefer-type="'dialog'" @click="onBgClick" @closed="$emit('closed')">
|
<MkModal
|
||||||
<div ref="rootEl" class="ebkgoccj" :style="{ width: `${width}px`, height: scroll ? (height ? `${height}px` : null) : (height ? `min(${height}px, 100%)` : '100%') }" @keydown="onKeydown">
|
ref="modal"
|
||||||
|
:prefer-type="'dialog'"
|
||||||
|
@click="onBgClick"
|
||||||
|
@closed="$emit('closed')"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref="rootEl"
|
||||||
|
class="ebkgoccj"
|
||||||
|
:style="{
|
||||||
|
width: `${width}px`,
|
||||||
|
height: scroll
|
||||||
|
? height
|
||||||
|
? `${height}px`
|
||||||
|
: null
|
||||||
|
: height
|
||||||
|
? `min(${height}px, 100%)`
|
||||||
|
: '100%',
|
||||||
|
}"
|
||||||
|
@keydown="onKeydown"
|
||||||
|
>
|
||||||
<div ref="headerEl" class="header">
|
<div ref="headerEl" class="header">
|
||||||
<button v-if="withOkButton" class="_button" @click="$emit('close')"><i class="ph-x ph-bold ph-lg"></i></button>
|
<button
|
||||||
|
v-if="withOkButton"
|
||||||
|
class="_button"
|
||||||
|
@click="$emit('close')"
|
||||||
|
>
|
||||||
|
<i class="ph-x ph-bold ph-lg"></i>
|
||||||
|
</button>
|
||||||
<span class="title">
|
<span class="title">
|
||||||
<slot name="header"></slot>
|
<slot name="header"></slot>
|
||||||
</span>
|
</span>
|
||||||
<button v-if="!withOkButton" class="_button" @click="$emit('close')"><i class="ph-x ph-bold ph-lg"></i></button>
|
<button
|
||||||
<button v-if="withOkButton" class="_button" :disabled="okButtonDisabled" @click="$emit('ok')"><i class="ph-check ph-bold ph-lg"></i></button>
|
v-if="!withOkButton"
|
||||||
|
class="_button"
|
||||||
|
@click="$emit('close')"
|
||||||
|
>
|
||||||
|
<i class="ph-x ph-bold ph-lg"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="withOkButton"
|
||||||
|
class="_button"
|
||||||
|
:disabled="okButtonDisabled"
|
||||||
|
@click="$emit('ok')"
|
||||||
|
>
|
||||||
|
<i class="ph-check ph-bold ph-lg"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<slot :width="bodyWidth" :height="bodyHeight"></slot>
|
<slot :width="bodyWidth" :height="bodyHeight"></slot>
|
||||||
|
@ -17,28 +55,31 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, onUnmounted } from 'vue';
|
import { onMounted, onUnmounted } from "vue";
|
||||||
import MkModal from './MkModal.vue';
|
import MkModal from "./MkModal.vue";
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
withOkButton: boolean;
|
withOkButton: boolean;
|
||||||
okButtonDisabled: boolean;
|
okButtonDisabled: boolean;
|
||||||
width: number;
|
width: number;
|
||||||
height: number | null;
|
height: number | null;
|
||||||
scroll: boolean;
|
scroll: boolean;
|
||||||
}>(), {
|
}>(),
|
||||||
|
{
|
||||||
withOkButton: false,
|
withOkButton: false,
|
||||||
okButtonDisabled: false,
|
okButtonDisabled: false,
|
||||||
width: 400,
|
width: 400,
|
||||||
height: null,
|
height: null,
|
||||||
scroll: true,
|
scroll: true,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(event: 'click'): void;
|
(event: "click"): void;
|
||||||
(event: 'close'): void;
|
(event: "close"): void;
|
||||||
(event: 'closed'): void;
|
(event: "closed"): void;
|
||||||
(event: 'ok'): void;
|
(event: "ok"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let modal = $shallowRef<InstanceType<typeof MkModal>>();
|
let modal = $shallowRef<InstanceType<typeof MkModal>>();
|
||||||
|
@ -52,11 +93,12 @@ const close = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const onBgClick = () => {
|
const onBgClick = () => {
|
||||||
emit('click');
|
emit("click");
|
||||||
};
|
};
|
||||||
|
|
||||||
const onKeydown = (evt) => {
|
const onKeydown = (evt) => {
|
||||||
if (evt.which === 27) { // Esc
|
if (evt.which === 27) {
|
||||||
|
// Esc
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
evt.stopPropagation();
|
evt.stopPropagation();
|
||||||
close();
|
close();
|
||||||
|
@ -146,4 +188,3 @@ defineExpose({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="msjugskd _block">
|
<div class="msjugskd _block">
|
||||||
<i class="ph-airplane-takeoff ph-bold ph-lg" style="margin-right: 8px;"/>
|
<i
|
||||||
|
class="ph-airplane-takeoff ph-bold ph-lg"
|
||||||
|
style="margin-right: 8px"
|
||||||
|
/>
|
||||||
{{ i18n.ts.accountMoved }}
|
{{ i18n.ts.accountMoved }}
|
||||||
<MkMention class="link" :username="acct" :host="host" />
|
<MkMention class="link" :username="acct" :host="host" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import MkMention from './MkMention.vue';
|
import MkMention from "./MkMention.vue";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
acct: string;
|
acct: string;
|
||||||
|
|
|
@ -9,87 +9,222 @@
|
||||||
:tabindex="!isDeleted ? '-1' : null"
|
:tabindex="!isDeleted ? '-1' : null"
|
||||||
:class="{ renote: isRenote }"
|
:class="{ renote: isRenote }"
|
||||||
>
|
>
|
||||||
<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/>
|
<MkNoteSub
|
||||||
|
v-if="appearNote.reply"
|
||||||
|
:note="appearNote.reply"
|
||||||
|
class="reply-to"
|
||||||
|
/>
|
||||||
<div class="note-context" @click="noteClick">
|
<div class="note-context" @click="noteClick">
|
||||||
<div class="line"></div>
|
<div class="line"></div>
|
||||||
<div v-if="appearNote._prId_" class="info"><i class="ph-megaphone-simple-bold ph-lg"></i> {{ i18n.ts.promotion }}<button class="_textButton hide" @click.stop="readPromo()">{{ i18n.ts.hideThisNote }} <i class="ph-x ph-bold ph-lg"></i></button></div>
|
<div v-if="appearNote._prId_" class="info">
|
||||||
<div v-if="appearNote._featuredId_" class="info"><i class="ph-lightning ph-bold ph-lg"></i> {{ i18n.ts.featured }}</div>
|
<i class="ph-megaphone-simple-bold ph-lg"></i>
|
||||||
<div v-if="pinned" class="info"><i class="ph-push-pin ph-bold ph-lg"></i>{{ i18n.ts.pinnedNote }}</div>
|
{{ i18n.ts.promotion
|
||||||
|
}}<button class="_textButton hide" @click.stop="readPromo()">
|
||||||
|
{{ i18n.ts.hideThisNote }}
|
||||||
|
<i class="ph-x ph-bold ph-lg"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="appearNote._featuredId_" class="info">
|
||||||
|
<i class="ph-lightning ph-bold ph-lg"></i>
|
||||||
|
{{ i18n.ts.featured }}
|
||||||
|
</div>
|
||||||
|
<div v-if="pinned" class="info">
|
||||||
|
<i class="ph-push-pin ph-bold ph-lg"></i
|
||||||
|
>{{ i18n.ts.pinnedNote }}
|
||||||
|
</div>
|
||||||
<div v-if="isRenote" class="renote">
|
<div v-if="isRenote" class="renote">
|
||||||
<i class="ph-repeat ph-bold ph-lg"></i>
|
<i class="ph-repeat ph-bold ph-lg"></i>
|
||||||
<I18n :src="i18n.ts.renotedBy" tag="span">
|
<I18n :src="i18n.ts.renotedBy" tag="span">
|
||||||
<template #user>
|
<template #user>
|
||||||
<MkA v-user-preview="note.userId" class="name" :to="userPage(note.user)" @click.stop>
|
<MkA
|
||||||
|
v-user-preview="note.userId"
|
||||||
|
class="name"
|
||||||
|
:to="userPage(note.user)"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
<MkUserName :user="note.user" />
|
<MkUserName :user="note.user" />
|
||||||
</MkA>
|
</MkA>
|
||||||
</template>
|
</template>
|
||||||
</I18n>
|
</I18n>
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<button ref="renoteTime" class="_button time" @click.stop="showRenoteMenu()">
|
<button
|
||||||
<i v-if="isMyRenote" class="ph-dots-three-outline ph-bold ph-lg dropdownIcon"></i>
|
ref="renoteTime"
|
||||||
|
class="_button time"
|
||||||
|
@click.stop="showRenoteMenu()"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
v-if="isMyRenote"
|
||||||
|
class="ph-dots-three-outline ph-bold ph-lg dropdownIcon"
|
||||||
|
></i>
|
||||||
<MkTime :time="note.createdAt" />
|
<MkTime :time="note.createdAt" />
|
||||||
</button>
|
</button>
|
||||||
<MkVisibility :note="note" />
|
<MkVisibility :note="note" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<article class="article" @contextmenu.stop="onContextmenu" @click="noteClick">
|
<article
|
||||||
|
class="article"
|
||||||
|
@contextmenu.stop="onContextmenu"
|
||||||
|
@click="noteClick"
|
||||||
|
>
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
<MkAvatar class="avatar" :user="appearNote.user" />
|
<MkAvatar class="avatar" :user="appearNote.user" />
|
||||||
<XNoteHeader class="header" :note="appearNote" :mini="true"/>
|
<XNoteHeader
|
||||||
|
class="header"
|
||||||
|
:note="appearNote"
|
||||||
|
:mini="true"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<p v-if="appearNote.cw != null" class="cw">
|
<p v-if="appearNote.cw != null" class="cw">
|
||||||
<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :custom-emojis="appearNote.emojis" :i="$i"/>
|
<Mfm
|
||||||
|
v-if="appearNote.cw != ''"
|
||||||
|
class="text"
|
||||||
|
:text="appearNote.cw"
|
||||||
|
:author="appearNote.user"
|
||||||
|
:custom-emojis="appearNote.emojis"
|
||||||
|
:i="$i"
|
||||||
|
/>
|
||||||
<br />
|
<br />
|
||||||
<XCwButton v-model="showContent" :note="appearNote" />
|
<XCwButton v-model="showContent" :note="appearNote" />
|
||||||
</p>
|
</p>
|
||||||
<div v-show="appearNote.cw == null || showContent" class="content" :class="{ collapsed, isLong }">
|
<div
|
||||||
|
v-show="appearNote.cw == null || showContent"
|
||||||
|
class="content"
|
||||||
|
:class="{ collapsed, isLong }"
|
||||||
|
>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
<Mfm
|
||||||
|
v-if="appearNote.text"
|
||||||
|
:text="appearNote.text"
|
||||||
|
:author="appearNote.user"
|
||||||
|
:i="$i"
|
||||||
|
:custom-emojis="appearNote.emojis"
|
||||||
|
/>
|
||||||
<!-- <a v-if="appearNote.renote != null" class="rp">RN:</a> -->
|
<!-- <a v-if="appearNote.renote != null" class="rp">RN:</a> -->
|
||||||
<div v-if="translating || translation" class="translation">
|
<div
|
||||||
|
v-if="translating || translation"
|
||||||
|
class="translation"
|
||||||
|
>
|
||||||
<MkLoading v-if="translating" mini />
|
<MkLoading v-if="translating" mini />
|
||||||
<div v-else class="translated">
|
<div v-else class="translated">
|
||||||
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
<b
|
||||||
<Mfm :text="translation.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
>{{
|
||||||
|
i18n.t("translatedFrom", {
|
||||||
|
x: translation.sourceLang,
|
||||||
|
})
|
||||||
|
}}:
|
||||||
|
</b>
|
||||||
|
<Mfm
|
||||||
|
:text="translation.text"
|
||||||
|
:author="appearNote.user"
|
||||||
|
:i="$i"
|
||||||
|
:custom-emojis="appearNote.emojis"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="appearNote.files.length > 0" class="files">
|
<div v-if="appearNote.files.length > 0" class="files">
|
||||||
<XMediaList :media-list="appearNote.files" />
|
<XMediaList :media-list="appearNote.files" />
|
||||||
</div>
|
</div>
|
||||||
<XPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" class="poll"/>
|
<XPoll
|
||||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" class="url-preview"/>
|
v-if="appearNote.poll"
|
||||||
<div v-if="appearNote.renote" class="renote"><XNoteSimple :note="appearNote.renote" @click.stop="router.push(notePage(appearNote.renote))"/></div>
|
ref="pollViewer"
|
||||||
<button v-if="isLong && collapsed" class="fade _button" @click.stop="collapsed = false">
|
:note="appearNote"
|
||||||
|
class="poll"
|
||||||
|
/>
|
||||||
|
<MkUrlPreview
|
||||||
|
v-for="url in urls"
|
||||||
|
:key="url"
|
||||||
|
:url="url"
|
||||||
|
:compact="true"
|
||||||
|
:detail="false"
|
||||||
|
class="url-preview"
|
||||||
|
/>
|
||||||
|
<div v-if="appearNote.renote" class="renote">
|
||||||
|
<XNoteSimple
|
||||||
|
:note="appearNote.renote"
|
||||||
|
@click.stop="
|
||||||
|
router.push(notePage(appearNote.renote))
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="isLong && collapsed"
|
||||||
|
class="fade _button"
|
||||||
|
@click.stop="collapsed = false"
|
||||||
|
>
|
||||||
<span>{{ i18n.ts.showMore }}</span>
|
<span>{{ i18n.ts.showMore }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button v-else-if="isLong && !collapsed" class="showLess _button" @click.stop="collapsed = true">
|
<button
|
||||||
|
v-else-if="isLong && !collapsed"
|
||||||
|
class="showLess _button"
|
||||||
|
@click.stop="collapsed = true"
|
||||||
|
>
|
||||||
<span>{{ i18n.ts.showLess }}</span>
|
<span>{{ i18n.ts.showLess }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`" @click.stop><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA>
|
<MkA
|
||||||
|
v-if="appearNote.channel && !inChannel"
|
||||||
|
class="channel"
|
||||||
|
:to="`/channels/${appearNote.channel.id}`"
|
||||||
|
@click.stop
|
||||||
|
><i class="ph-television ph-bold ph-lg"></i>
|
||||||
|
{{ appearNote.channel.name }}</MkA
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<footer ref="el" class="footer" @click.stop>
|
<footer ref="el" class="footer" @click.stop>
|
||||||
<XReactionsViewer ref="reactionsViewer" :note="appearNote"/>
|
<XReactionsViewer
|
||||||
<button v-tooltip.noDelay.bottom="i18n.ts.reply" class="button _button" @click="reply()">
|
ref="reactionsViewer"
|
||||||
|
:note="appearNote"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-tooltip.noDelay.bottom="i18n.ts.reply"
|
||||||
|
class="button _button"
|
||||||
|
@click="reply()"
|
||||||
|
>
|
||||||
<i class="ph-arrow-u-up-left ph-bold ph-lg"></i>
|
<i class="ph-arrow-u-up-left ph-bold ph-lg"></i>
|
||||||
<template v-if="appearNote.repliesCount > 0">
|
<template v-if="appearNote.repliesCount > 0">
|
||||||
<p class="count">{{ appearNote.repliesCount }}</p>
|
<p class="count">{{ appearNote.repliesCount }}</p>
|
||||||
</template>
|
</template>
|
||||||
</button>
|
</button>
|
||||||
<XRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/>
|
<XRenoteButton
|
||||||
<XStarButton v-if="appearNote.myReaction == null" ref="starButton" class="button" :note="appearNote"/>
|
ref="renoteButton"
|
||||||
<button v-if="appearNote.myReaction == null" ref="reactButton" v-tooltip.noDelay.bottom="i18n.ts.reaction" class="button _button" @click="react()">
|
class="button"
|
||||||
|
:note="appearNote"
|
||||||
|
:count="appearNote.renoteCount"
|
||||||
|
/>
|
||||||
|
<XStarButton
|
||||||
|
v-if="appearNote.myReaction == null"
|
||||||
|
ref="starButton"
|
||||||
|
class="button"
|
||||||
|
:note="appearNote"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-if="appearNote.myReaction == null"
|
||||||
|
ref="reactButton"
|
||||||
|
v-tooltip.noDelay.bottom="i18n.ts.reaction"
|
||||||
|
class="button _button"
|
||||||
|
@click="react()"
|
||||||
|
>
|
||||||
<i class="ph-smiley ph-bold ph-lg"></i>
|
<i class="ph-smiley ph-bold ph-lg"></i>
|
||||||
</button>
|
</button>
|
||||||
<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
|
<button
|
||||||
|
v-if="appearNote.myReaction != null"
|
||||||
|
ref="reactButton"
|
||||||
|
class="button _button reacted"
|
||||||
|
@click="undoReact(appearNote)"
|
||||||
|
>
|
||||||
<i class="ph-minus ph-bold ph-lg"></i>
|
<i class="ph-minus ph-bold ph-lg"></i>
|
||||||
</button>
|
</button>
|
||||||
<XQuoteButton class="button" :note="appearNote" />
|
<XQuoteButton class="button" :note="appearNote" />
|
||||||
<button ref="menuButton" v-tooltip.noDelay.bottom="i18n.ts.more" class="button _button" @click="menu()">
|
<button
|
||||||
|
ref="menuButton"
|
||||||
|
v-tooltip.noDelay.bottom="i18n.ts.more"
|
||||||
|
class="button _button"
|
||||||
|
@click="menu()"
|
||||||
|
>
|
||||||
<i class="ph-dots-three-outline ph-bold ph-lg"></i>
|
<i class="ph-dots-three-outline ph-bold ph-lg"></i>
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -99,7 +234,11 @@
|
||||||
<div v-else class="muted" @click="muted.muted = false">
|
<div v-else class="muted" @click="muted.muted = false">
|
||||||
<I18n :src="i18n.ts.userSaysSomethingReason" tag="small">
|
<I18n :src="i18n.ts.userSaysSomethingReason" tag="small">
|
||||||
<template #name>
|
<template #name>
|
||||||
<MkA v-user-preview="appearNote.userId" class="name" :to="userPage(appearNote.user)">
|
<MkA
|
||||||
|
v-user-preview="appearNote.userId"
|
||||||
|
class="name"
|
||||||
|
:to="userPage(appearNote.user)"
|
||||||
|
>
|
||||||
<MkUserName :user="appearNote.user" />
|
<MkUserName :user="appearNote.user" />
|
||||||
</MkA>
|
</MkA>
|
||||||
</template>
|
</template>
|
||||||
|
@ -111,37 +250,37 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, inject, onMounted, onUnmounted, reactive, ref } from 'vue';
|
import { computed, inject, onMounted, onUnmounted, reactive, ref } from "vue";
|
||||||
import * as mfm from 'mfm-js';
|
import * as mfm from "mfm-js";
|
||||||
import type { Ref } from 'vue';
|
import type { Ref } from "vue";
|
||||||
import type * as misskey from 'calckey-js';
|
import type * as misskey from "calckey-js";
|
||||||
import MkNoteSub from '@/components/MkNoteSub.vue';
|
import MkNoteSub from "@/components/MkNoteSub.vue";
|
||||||
import XNoteHeader from '@/components/MkNoteHeader.vue';
|
import XNoteHeader from "@/components/MkNoteHeader.vue";
|
||||||
import XNoteSimple from '@/components/MkNoteSimple.vue';
|
import XNoteSimple from "@/components/MkNoteSimple.vue";
|
||||||
import XMediaList from '@/components/MkMediaList.vue';
|
import XMediaList from "@/components/MkMediaList.vue";
|
||||||
import XCwButton from '@/components/MkCwButton.vue';
|
import XCwButton from "@/components/MkCwButton.vue";
|
||||||
import XPoll from '@/components/MkPoll.vue';
|
import XPoll from "@/components/MkPoll.vue";
|
||||||
import XRenoteButton from '@/components/MkRenoteButton.vue';
|
import XRenoteButton from "@/components/MkRenoteButton.vue";
|
||||||
import XReactionsViewer from '@/components/MkReactionsViewer.vue';
|
import XReactionsViewer from "@/components/MkReactionsViewer.vue";
|
||||||
import XStarButton from '@/components/MkStarButton.vue';
|
import XStarButton from "@/components/MkStarButton.vue";
|
||||||
import XQuoteButton from '@/components/MkQuoteButton.vue';
|
import XQuoteButton from "@/components/MkQuoteButton.vue";
|
||||||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
import MkUrlPreview from "@/components/MkUrlPreview.vue";
|
||||||
import MkVisibility from '@/components/MkVisibility.vue';
|
import MkVisibility from "@/components/MkVisibility.vue";
|
||||||
import { pleaseLogin } from '@/scripts/please-login';
|
import { pleaseLogin } from "@/scripts/please-login";
|
||||||
import { focusPrev, focusNext } from '@/scripts/focus';
|
import { focusPrev, focusNext } from "@/scripts/focus";
|
||||||
import { getWordMute } from '@/scripts/check-word-mute';
|
import { getWordMute } from "@/scripts/check-word-mute";
|
||||||
import { useRouter } from '@/router';
|
import { useRouter } from "@/router";
|
||||||
import { userPage } from '@/filters/user';
|
import { userPage } from "@/filters/user";
|
||||||
import * as os from '@/os';
|
import * as os from "@/os";
|
||||||
import { defaultStore, noteViewInterruptors } from '@/store';
|
import { defaultStore, noteViewInterruptors } from "@/store";
|
||||||
import { reactionPicker } from '@/scripts/reaction-picker';
|
import { reactionPicker } from "@/scripts/reaction-picker";
|
||||||
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
|
import { extractUrlFromMfm } from "@/scripts/extract-url-from-mfm";
|
||||||
import { $i } from '@/account';
|
import { $i } from "@/account";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
import { getNoteMenu } from '@/scripts/get-note-menu';
|
import { getNoteMenu } from "@/scripts/get-note-menu";
|
||||||
import { useNoteCapture } from '@/scripts/use-note-capture';
|
import { useNoteCapture } from "@/scripts/use-note-capture";
|
||||||
import { notePage } from '@/filters/note';
|
import { notePage } from "@/filters/note";
|
||||||
import { deepClone } from '@/scripts/clone';
|
import { deepClone } from "@/scripts/clone";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@ -150,7 +289,7 @@ const props = defineProps<{
|
||||||
pinned?: boolean;
|
pinned?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const inChannel = inject('inChannel', null);
|
const inChannel = inject("inChannel", null);
|
||||||
|
|
||||||
let note = $ref(deepClone(props.note));
|
let note = $ref(deepClone(props.note));
|
||||||
|
|
||||||
|
@ -165,12 +304,11 @@ if (noteViewInterruptors.length > 0) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const isRenote = (
|
const isRenote =
|
||||||
note.renote != null &&
|
note.renote != null &&
|
||||||
note.text == null &&
|
note.text == null &&
|
||||||
note.fileIds.length === 0 &&
|
note.fileIds.length === 0 &&
|
||||||
note.poll == null
|
note.poll == null;
|
||||||
);
|
|
||||||
|
|
||||||
const el = ref<HTMLElement>();
|
const el = ref<HTMLElement>();
|
||||||
const menuButton = ref<HTMLElement>();
|
const menuButton = ref<HTMLElement>();
|
||||||
|
@ -178,29 +316,33 @@ const starButton = ref<InstanceType<typeof XStarButton>>();
|
||||||
const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
|
const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
|
||||||
const renoteTime = ref<HTMLElement>();
|
const renoteTime = ref<HTMLElement>();
|
||||||
const reactButton = ref<HTMLElement>();
|
const reactButton = ref<HTMLElement>();
|
||||||
let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note);
|
let appearNote = $computed(() =>
|
||||||
const isMyRenote = $i && ($i.id === note.userId);
|
isRenote ? (note.renote as misskey.entities.Note) : note
|
||||||
|
);
|
||||||
|
const isMyRenote = $i && $i.id === note.userId;
|
||||||
const showContent = ref(false);
|
const showContent = ref(false);
|
||||||
const isLong = (appearNote.cw == null && appearNote.text != null && (
|
const isLong =
|
||||||
(appearNote.text.split('\n').length > 9) ||
|
appearNote.cw == null &&
|
||||||
(appearNote.text.length > 500)
|
appearNote.text != null &&
|
||||||
));
|
(appearNote.text.split("\n").length > 9 || appearNote.text.length > 500);
|
||||||
const collapsed = ref(appearNote.cw == null && isLong);
|
const collapsed = ref(appearNote.cw == null && isLong);
|
||||||
const isDeleted = ref(false);
|
const isDeleted = ref(false);
|
||||||
const muted = ref(getWordMute(appearNote, $i, defaultStore.state.mutedWords));
|
const muted = ref(getWordMute(appearNote, $i, defaultStore.state.mutedWords));
|
||||||
const translation = ref(null);
|
const translation = ref(null);
|
||||||
const translating = ref(false);
|
const translating = ref(false);
|
||||||
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)).slice(0, 5) : null;
|
const urls = appearNote.text
|
||||||
|
? extractUrlFromMfm(mfm.parse(appearNote.text)).slice(0, 5)
|
||||||
|
: null;
|
||||||
|
|
||||||
const keymap = {
|
const keymap = {
|
||||||
'r': () => reply(true),
|
r: () => reply(true),
|
||||||
'e|a|plus': () => react(true),
|
"e|a|plus": () => react(true),
|
||||||
'q': () => renoteButton.value.renote(true),
|
q: () => renoteButton.value.renote(true),
|
||||||
'up|k|shift+tab': focusBefore,
|
"up|k|shift+tab": focusBefore,
|
||||||
'down|j|tab': focusAfter,
|
"down|j|tab": focusAfter,
|
||||||
'esc': blur,
|
esc: blur,
|
||||||
'm|o': () => menu(true),
|
"m|o": () => menu(true),
|
||||||
's': () => showContent.value !== showContent.value,
|
s: () => showContent.value !== showContent.value,
|
||||||
};
|
};
|
||||||
|
|
||||||
useNoteCapture({
|
useNoteCapture({
|
||||||
|
@ -211,76 +353,113 @@ useNoteCapture({
|
||||||
|
|
||||||
function reply(viaKeyboard = false): void {
|
function reply(viaKeyboard = false): void {
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
os.post({
|
os.post(
|
||||||
|
{
|
||||||
reply: appearNote,
|
reply: appearNote,
|
||||||
animation: !viaKeyboard,
|
animation: !viaKeyboard,
|
||||||
}, () => {
|
},
|
||||||
|
() => {
|
||||||
focus();
|
focus();
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function react(viaKeyboard = false): void {
|
function react(viaKeyboard = false): void {
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
blur();
|
blur();
|
||||||
reactionPicker.show(reactButton.value, reaction => {
|
reactionPicker.show(
|
||||||
os.api('notes/reactions/create', {
|
reactButton.value,
|
||||||
|
(reaction) => {
|
||||||
|
os.api("notes/reactions/create", {
|
||||||
noteId: appearNote.id,
|
noteId: appearNote.id,
|
||||||
reaction: reaction,
|
reaction: reaction,
|
||||||
});
|
});
|
||||||
}, () => {
|
},
|
||||||
|
() => {
|
||||||
focus();
|
focus();
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function undoReact(note): void {
|
function undoReact(note): void {
|
||||||
const oldReaction = note.myReaction;
|
const oldReaction = note.myReaction;
|
||||||
if (!oldReaction) return;
|
if (!oldReaction) return;
|
||||||
os.api('notes/reactions/delete', {
|
os.api("notes/reactions/delete", {
|
||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentClipPage = inject<Ref<misskey.entities.Clip> | null>('currentClipPage', null);
|
const currentClipPage = inject<Ref<misskey.entities.Clip> | null>(
|
||||||
|
"currentClipPage",
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
function onContextmenu(ev: MouseEvent): void {
|
function onContextmenu(ev: MouseEvent): void {
|
||||||
const isLink = (el: HTMLElement) => {
|
const isLink = (el: HTMLElement) => {
|
||||||
if (el.tagName === 'A') return true;
|
if (el.tagName === "A") return true;
|
||||||
if (el.parentElement) {
|
if (el.parentElement) {
|
||||||
return isLink(el.parentElement);
|
return isLink(el.parentElement);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (isLink(ev.target)) return;
|
if (isLink(ev.target)) return;
|
||||||
if (window.getSelection().toString() !== '') return;
|
if (window.getSelection().toString() !== "") return;
|
||||||
|
|
||||||
if (defaultStore.state.useReactionPickerForContextMenu) {
|
if (defaultStore.state.useReactionPickerForContextMenu) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
react();
|
react();
|
||||||
} else {
|
} else {
|
||||||
os.contextMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClipPage }), ev).then(focus);
|
os.contextMenu(
|
||||||
|
getNoteMenu({
|
||||||
|
note: note,
|
||||||
|
translating,
|
||||||
|
translation,
|
||||||
|
menuButton,
|
||||||
|
isDeleted,
|
||||||
|
currentClipPage,
|
||||||
|
}),
|
||||||
|
ev
|
||||||
|
).then(focus);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function menu(viaKeyboard = false): void {
|
function menu(viaKeyboard = false): void {
|
||||||
os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClipPage }), menuButton.value, {
|
os.popupMenu(
|
||||||
|
getNoteMenu({
|
||||||
|
note: note,
|
||||||
|
translating,
|
||||||
|
translation,
|
||||||
|
menuButton,
|
||||||
|
isDeleted,
|
||||||
|
currentClipPage,
|
||||||
|
}),
|
||||||
|
menuButton.value,
|
||||||
|
{
|
||||||
viaKeyboard,
|
viaKeyboard,
|
||||||
}).then(focus);
|
}
|
||||||
|
).then(focus);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showRenoteMenu(viaKeyboard = false): void {
|
function showRenoteMenu(viaKeyboard = false): void {
|
||||||
if (!isMyRenote) return;
|
if (!isMyRenote) return;
|
||||||
os.popupMenu([{
|
os.popupMenu(
|
||||||
|
[
|
||||||
|
{
|
||||||
text: i18n.ts.unrenote,
|
text: i18n.ts.unrenote,
|
||||||
icon: 'ph-trash ph-bold ph-lg',
|
icon: "ph-trash ph-bold ph-lg",
|
||||||
danger: true,
|
danger: true,
|
||||||
action: () => {
|
action: () => {
|
||||||
os.api('notes/delete', {
|
os.api("notes/delete", {
|
||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
});
|
});
|
||||||
isDeleted.value = true;
|
isDeleted.value = true;
|
||||||
},
|
},
|
||||||
}], renoteTime.value, {
|
},
|
||||||
|
],
|
||||||
|
renoteTime.value,
|
||||||
|
{
|
||||||
viaKeyboard: viaKeyboard,
|
viaKeyboard: viaKeyboard,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function focus() {
|
function focus() {
|
||||||
|
@ -300,15 +479,15 @@ function focusAfter() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function noteClick(e) {
|
function noteClick(e) {
|
||||||
if (document.getSelection().type === 'Range') {
|
if (document.getSelection().type === "Range") {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
} else {
|
} else {
|
||||||
router.push(notePage(appearNote))
|
router.push(notePage(appearNote));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function readPromo() {
|
function readPromo() {
|
||||||
os.api('promo/read', {
|
os.api("promo/read", {
|
||||||
noteId: appearNote.id,
|
noteId: appearNote.id,
|
||||||
});
|
});
|
||||||
isDeleted.value = true;
|
isDeleted.value = true;
|
||||||
|
@ -354,7 +533,8 @@ function readPromo() {
|
||||||
}
|
}
|
||||||
|
|
||||||
& > .article > .main {
|
& > .article > .main {
|
||||||
&:hover, &:focus-within {
|
&:hover,
|
||||||
|
&:focus-within {
|
||||||
:deep(.footer .button) {
|
:deep(.footer .button) {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
@ -395,7 +575,7 @@ function readPromo() {
|
||||||
}
|
}
|
||||||
|
|
||||||
> div > i {
|
> div > i {
|
||||||
margin-left: -.5px;
|
margin-left: -0.5px;
|
||||||
}
|
}
|
||||||
> .info {
|
> .info {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -414,7 +594,6 @@ function readPromo() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
> .renote {
|
> .renote {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -459,7 +638,6 @@ function readPromo() {
|
||||||
padding: 4px 32px 10px;
|
padding: 4px 32px 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
|
||||||
@media (pointer: coarse) {
|
@media (pointer: coarse) {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
@ -486,7 +664,7 @@ function readPromo() {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
||||||
> .body {
|
> .body {
|
||||||
margin-top: .7em;
|
margin-top: 0.7em;
|
||||||
|
|
||||||
> .cw {
|
> .cw {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
@ -525,8 +703,14 @@ function readPromo() {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
> .text {
|
> .text {
|
||||||
max-height: 9em;
|
max-height: 9em;
|
||||||
mask: linear-gradient(black calc(100% - 64px), transparent);
|
mask: linear-gradient(
|
||||||
-webkit-mask: linear-gradient(black calc(100% - 64px), transparent);
|
black calc(100% - 64px),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
-webkit-mask: linear-gradient(
|
||||||
|
black calc(100% - 64px),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
}
|
}
|
||||||
> .fade {
|
> .fade {
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -576,8 +760,8 @@ function readPromo() {
|
||||||
}
|
}
|
||||||
|
|
||||||
> .files {
|
> .files {
|
||||||
margin-top: .4em;
|
margin-top: 0.4em;
|
||||||
margin-bottom: .4em;
|
margin-bottom: 0.4em;
|
||||||
}
|
}
|
||||||
> .url-preview {
|
> .url-preview {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
|
@ -594,8 +778,9 @@ function readPromo() {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border: solid 1px var(--renote);
|
border: solid 1px var(--renote);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
transition: background .2s;
|
transition: background 0.2s;
|
||||||
&:hover, &:focus-within {
|
&:hover,
|
||||||
|
&:focus-within {
|
||||||
background-color: var(--panelHighlight);
|
background-color: var(--panelHighlight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -623,9 +808,9 @@ function readPromo() {
|
||||||
width: max-content;
|
width: max-content;
|
||||||
min-width: max-content;
|
min-width: max-content;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
transition: opacity .2s;
|
transition: opacity 0.2s;
|
||||||
&:first-of-type {
|
&:first-of-type {
|
||||||
margin-left: -.5em;
|
margin-left: -0.5em;
|
||||||
}
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--fgHighlighted);
|
color: var(--fgHighlighted);
|
||||||
|
@ -645,7 +830,6 @@ function readPromo() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
> .reply {
|
> .reply {
|
||||||
border-top: solid 0.5px var(--divider);
|
border-top: solid 0.5px var(--divider);
|
||||||
}
|
}
|
||||||
|
@ -679,8 +863,6 @@ function readPromo() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
&.max-width_300px {
|
&.max-width_300px {
|
||||||
--avatarSize: 40px;
|
--avatarSize: 40px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,103 +9,251 @@
|
||||||
:tabindex="!isDeleted ? '-1' : null"
|
:tabindex="!isDeleted ? '-1' : null"
|
||||||
:class="{ renote: isRenote }"
|
:class="{ renote: isRenote }"
|
||||||
>
|
>
|
||||||
<MkNoteSub v-for="note in conversation" :key="note.id" class="reply-to-more" :note="note"/>
|
<MkNoteSub
|
||||||
<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/>
|
v-for="note in conversation"
|
||||||
|
:key="note.id"
|
||||||
|
class="reply-to-more"
|
||||||
|
:note="note"
|
||||||
|
/>
|
||||||
|
<MkNoteSub
|
||||||
|
v-if="appearNote.reply"
|
||||||
|
:note="appearNote.reply"
|
||||||
|
class="reply-to"
|
||||||
|
/>
|
||||||
<div v-if="isRenote" class="renote">
|
<div v-if="isRenote" class="renote">
|
||||||
<MkAvatar class="avatar" :user="note.user" />
|
<MkAvatar class="avatar" :user="note.user" />
|
||||||
<i class="ph-repeat ph-bold ph-lg"></i>
|
<i class="ph-repeat ph-bold ph-lg"></i>
|
||||||
<I18n :src="i18n.ts.renotedBy" tag="span">
|
<I18n :src="i18n.ts.renotedBy" tag="span">
|
||||||
<template #user>
|
<template #user>
|
||||||
<MkA v-user-preview="note.userId" class="name" :to="userPage(note.user)">
|
<MkA
|
||||||
|
v-user-preview="note.userId"
|
||||||
|
class="name"
|
||||||
|
:to="userPage(note.user)"
|
||||||
|
>
|
||||||
<MkUserName :user="note.user" />
|
<MkUserName :user="note.user" />
|
||||||
</MkA>
|
</MkA>
|
||||||
</template>
|
</template>
|
||||||
</I18n>
|
</I18n>
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<button ref="renoteTime" class="_button time" @click="showRenoteMenu()">
|
<button
|
||||||
<i v-if="isMyRenote" class="ph-dots-three-outline ph-bold ph-lg dropdownIcon"></i>
|
ref="renoteTime"
|
||||||
|
class="_button time"
|
||||||
|
@click="showRenoteMenu()"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
v-if="isMyRenote"
|
||||||
|
class="ph-dots-three-outline ph-bold ph-lg dropdownIcon"
|
||||||
|
></i>
|
||||||
<MkTime :time="note.createdAt" />
|
<MkTime :time="note.createdAt" />
|
||||||
</button>
|
</button>
|
||||||
<MkVisibility :note="note" />
|
<MkVisibility :note="note" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<article ref="noteEl" class="article" @contextmenu.stop="onContextmenu" tabindex="-1">
|
<article
|
||||||
|
ref="noteEl"
|
||||||
|
class="article"
|
||||||
|
@contextmenu.stop="onContextmenu"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
<header class="header">
|
<header class="header">
|
||||||
<MkAvatar class="avatar" :user="appearNote.user" :show-indicator="true"/>
|
<MkAvatar
|
||||||
|
class="avatar"
|
||||||
|
:user="appearNote.user"
|
||||||
|
:show-indicator="true"
|
||||||
|
/>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<div class="top">
|
<div class="top">
|
||||||
<MkA v-user-preview="appearNote.user.id" class="name" :to="userPage(appearNote.user)">
|
<MkA
|
||||||
|
v-user-preview="appearNote.user.id"
|
||||||
|
class="name"
|
||||||
|
:to="userPage(appearNote.user)"
|
||||||
|
>
|
||||||
<MkUserName :user="appearNote.user" />
|
<MkUserName :user="appearNote.user" />
|
||||||
</MkA>
|
</MkA>
|
||||||
<span v-if="appearNote.user.isBot" class="is-bot">bot</span>
|
<span v-if="appearNote.user.isBot" class="is-bot"
|
||||||
|
>bot</span
|
||||||
|
>
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<MkVisibility :note="appearNote" />
|
<MkVisibility :note="appearNote" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="username"><MkAcct :user="appearNote.user"/></div>
|
<div class="username">
|
||||||
<MkInstanceTicker v-if="showTicker" class="ticker" :instance="appearNote.user.instance"/>
|
<MkAcct :user="appearNote.user" />
|
||||||
|
</div>
|
||||||
|
<MkInstanceTicker
|
||||||
|
v-if="showTicker"
|
||||||
|
class="ticker"
|
||||||
|
:instance="appearNote.user.instance"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<div v-if="appearNote.cw != null" class="cw">
|
<div v-if="appearNote.cw != null" class="cw">
|
||||||
<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
<Mfm
|
||||||
|
v-if="appearNote.cw != ''"
|
||||||
|
class="text"
|
||||||
|
:text="appearNote.cw"
|
||||||
|
:author="appearNote.user"
|
||||||
|
:i="$i"
|
||||||
|
:custom-emojis="appearNote.emojis"
|
||||||
|
/>
|
||||||
<br />
|
<br />
|
||||||
<XCwButton v-model="showContent" :note="appearNote" />
|
<XCwButton v-model="showContent" :note="appearNote" />
|
||||||
</div>
|
</div>
|
||||||
<div v-show="appearNote.cw == null || showContent" class="content">
|
<div
|
||||||
|
v-show="appearNote.cw == null || showContent"
|
||||||
|
class="content"
|
||||||
|
>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
<Mfm
|
||||||
<div v-if="translating || translation" class="translation">
|
v-if="appearNote.text"
|
||||||
|
:text="appearNote.text"
|
||||||
|
:author="appearNote.user"
|
||||||
|
:i="$i"
|
||||||
|
:custom-emojis="appearNote.emojis"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="translating || translation"
|
||||||
|
class="translation"
|
||||||
|
>
|
||||||
<MkLoading v-if="translating" mini />
|
<MkLoading v-if="translating" mini />
|
||||||
<div v-else class="translated">
|
<div v-else class="translated">
|
||||||
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
<b
|
||||||
<Mfm :text="translation.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
>{{
|
||||||
|
i18n.t("translatedFrom", {
|
||||||
|
x: translation.sourceLang,
|
||||||
|
})
|
||||||
|
}}:
|
||||||
|
</b>
|
||||||
|
<Mfm
|
||||||
|
:text="translation.text"
|
||||||
|
:author="appearNote.user"
|
||||||
|
:i="$i"
|
||||||
|
:custom-emojis="appearNote.emojis"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="appearNote.files.length > 0" class="files">
|
<div v-if="appearNote.files.length > 0" class="files">
|
||||||
<XMediaList :media-list="appearNote.files" />
|
<XMediaList :media-list="appearNote.files" />
|
||||||
</div>
|
</div>
|
||||||
<XPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" class="poll"/>
|
<XPoll
|
||||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" class="url-preview"/>
|
v-if="appearNote.poll"
|
||||||
<div v-if="appearNote.renote" class="renote"><XNoteSimple :note="appearNote.renote" @click.stop="router.push(notePage(appearNote.renote))"/></div>
|
ref="pollViewer"
|
||||||
|
:note="appearNote"
|
||||||
|
class="poll"
|
||||||
|
/>
|
||||||
|
<MkUrlPreview
|
||||||
|
v-for="url in urls"
|
||||||
|
:key="url"
|
||||||
|
:url="url"
|
||||||
|
:compact="true"
|
||||||
|
:detail="true"
|
||||||
|
class="url-preview"
|
||||||
|
/>
|
||||||
|
<div v-if="appearNote.renote" class="renote">
|
||||||
|
<XNoteSimple
|
||||||
|
:note="appearNote.renote"
|
||||||
|
@click.stop="
|
||||||
|
router.push(notePage(appearNote.renote))
|
||||||
|
"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA>
|
</div>
|
||||||
|
<MkA
|
||||||
|
v-if="appearNote.channel && !inChannel"
|
||||||
|
class="channel"
|
||||||
|
:to="`/channels/${appearNote.channel.id}`"
|
||||||
|
><i class="ph-television ph-bold ph-lg"></i>
|
||||||
|
{{ appearNote.channel.name }}</MkA
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<MkA class="created-at" :to="notePage(appearNote)">
|
<MkA class="created-at" :to="notePage(appearNote)">
|
||||||
<MkTime :time="appearNote.createdAt" mode="detail"/>
|
<MkTime
|
||||||
|
:time="appearNote.createdAt"
|
||||||
|
mode="detail"
|
||||||
|
/>
|
||||||
</MkA>
|
</MkA>
|
||||||
</div>
|
</div>
|
||||||
<XReactionsViewer ref="reactionsViewer" :note="appearNote"/>
|
<XReactionsViewer
|
||||||
<button v-tooltip.noDelay.bottom="i18n.ts.reply" class="button _button" @click="reply()">
|
ref="reactionsViewer"
|
||||||
<template v-if="appearNote.reply"><i class="ph-arrow-u-up-left ph-bold ph-lg"></i></template>
|
:note="appearNote"
|
||||||
<template v-else><i class="ph-arrow-bend-up-left ph-bold ph-lg"></i></template>
|
/>
|
||||||
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
|
<button
|
||||||
|
v-tooltip.noDelay.bottom="i18n.ts.reply"
|
||||||
|
class="button _button"
|
||||||
|
@click="reply()"
|
||||||
|
>
|
||||||
|
<template v-if="appearNote.reply"
|
||||||
|
><i class="ph-arrow-u-up-left ph-bold ph-lg"></i
|
||||||
|
></template>
|
||||||
|
<template v-else
|
||||||
|
><i class="ph-arrow-bend-up-left ph-bold ph-lg"></i
|
||||||
|
></template>
|
||||||
|
<p v-if="appearNote.repliesCount > 0" class="count">
|
||||||
|
{{ appearNote.repliesCount }}
|
||||||
|
</p>
|
||||||
</button>
|
</button>
|
||||||
<XRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/>
|
<XRenoteButton
|
||||||
<XStarButton v-if="appearNote.myReaction == null" ref="starButton" class="button" :note="appearNote"/>
|
ref="renoteButton"
|
||||||
<button v-if="appearNote.myReaction == null" ref="reactButton" v-tooltip.noDelay.bottom="i18n.ts.reaction" class="button _button" @click="react()">
|
class="button"
|
||||||
|
:note="appearNote"
|
||||||
|
:count="appearNote.renoteCount"
|
||||||
|
/>
|
||||||
|
<XStarButton
|
||||||
|
v-if="appearNote.myReaction == null"
|
||||||
|
ref="starButton"
|
||||||
|
class="button"
|
||||||
|
:note="appearNote"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-if="appearNote.myReaction == null"
|
||||||
|
ref="reactButton"
|
||||||
|
v-tooltip.noDelay.bottom="i18n.ts.reaction"
|
||||||
|
class="button _button"
|
||||||
|
@click="react()"
|
||||||
|
>
|
||||||
<i class="ph-smiley ph-bold ph-lg"></i>
|
<i class="ph-smiley ph-bold ph-lg"></i>
|
||||||
</button>
|
</button>
|
||||||
<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
|
<button
|
||||||
|
v-if="appearNote.myReaction != null"
|
||||||
|
ref="reactButton"
|
||||||
|
class="button _button reacted"
|
||||||
|
@click="undoReact(appearNote)"
|
||||||
|
>
|
||||||
<i class="ph-minus ph-bold ph-lg"></i>
|
<i class="ph-minus ph-bold ph-lg"></i>
|
||||||
</button>
|
</button>
|
||||||
<XQuoteButton class="button" :note="appearNote" />
|
<XQuoteButton class="button" :note="appearNote" />
|
||||||
<button ref="menuButton" v-tooltip.noDelay.bottom="i18n.ts.more" class="button _button" @click="menu()">
|
<button
|
||||||
|
ref="menuButton"
|
||||||
|
v-tooltip.noDelay.bottom="i18n.ts.more"
|
||||||
|
class="button _button"
|
||||||
|
@click="menu()"
|
||||||
|
>
|
||||||
<i class="ph-dots-three-outline ph-bold ph-lg"></i>
|
<i class="ph-dots-three-outline ph-bold ph-lg"></i>
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
<MkNoteSub v-for="note in directReplies" :key="note.id" :note="note" class="reply" :conversation="replies"/>
|
<MkNoteSub
|
||||||
|
v-for="note in directReplies"
|
||||||
|
:key="note.id"
|
||||||
|
:note="note"
|
||||||
|
class="reply"
|
||||||
|
:conversation="replies"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="_panel muted" @click="muted.muted = false">
|
<div v-else class="_panel muted" @click="muted.muted = false">
|
||||||
<I18n :src="i18n.ts.userSaysSomethingReason" tag="small">
|
<I18n :src="i18n.ts.userSaysSomethingReason" tag="small">
|
||||||
<template #name>
|
<template #name>
|
||||||
<MkA v-user-preview="appearNote.userId" class="name" :to="userPage(appearNote.user)">
|
<MkA
|
||||||
|
v-user-preview="appearNote.userId"
|
||||||
|
class="name"
|
||||||
|
:to="userPage(appearNote.user)"
|
||||||
|
>
|
||||||
<MkUserName :user="appearNote.user" />
|
<MkUserName :user="appearNote.user" />
|
||||||
</MkA>
|
</MkA>
|
||||||
</template>
|
</template>
|
||||||
|
@ -117,37 +265,45 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, inject, onMounted, onUnmounted, onUpdated, reactive, ref } from 'vue';
|
import {
|
||||||
import * as mfm from 'mfm-js';
|
computed,
|
||||||
import type * as misskey from 'calckey-js';
|
inject,
|
||||||
import MkNoteSub from '@/components/MkNoteSub.vue';
|
onMounted,
|
||||||
import XNoteSimple from '@/components/MkNoteSimple.vue';
|
onUnmounted,
|
||||||
import XReactionsViewer from '@/components/MkReactionsViewer.vue';
|
onUpdated,
|
||||||
import XMediaList from '@/components/MkMediaList.vue';
|
reactive,
|
||||||
import XCwButton from '@/components/MkCwButton.vue';
|
ref,
|
||||||
import XPoll from '@/components/MkPoll.vue';
|
} from "vue";
|
||||||
import XStarButton from '@/components/MkStarButton.vue';
|
import * as mfm from "mfm-js";
|
||||||
import XRenoteButton from '@/components/MkRenoteButton.vue';
|
import type * as misskey from "calckey-js";
|
||||||
import XQuoteButton from '@/components/MkQuoteButton.vue';
|
import MkNoteSub from "@/components/MkNoteSub.vue";
|
||||||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
import XNoteSimple from "@/components/MkNoteSimple.vue";
|
||||||
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
|
import XReactionsViewer from "@/components/MkReactionsViewer.vue";
|
||||||
import MkVisibility from '@/components/MkVisibility.vue';
|
import XMediaList from "@/components/MkMediaList.vue";
|
||||||
import { pleaseLogin } from '@/scripts/please-login';
|
import XCwButton from "@/components/MkCwButton.vue";
|
||||||
import { getWordMute } from '@/scripts/check-word-mute';
|
import XPoll from "@/components/MkPoll.vue";
|
||||||
import { userPage } from '@/filters/user';
|
import XStarButton from "@/components/MkStarButton.vue";
|
||||||
import { notePage } from '@/filters/note';
|
import XRenoteButton from "@/components/MkRenoteButton.vue";
|
||||||
import { useRouter } from '@/router';
|
import XQuoteButton from "@/components/MkQuoteButton.vue";
|
||||||
import * as os from '@/os';
|
import MkUrlPreview from "@/components/MkUrlPreview.vue";
|
||||||
import { defaultStore, noteViewInterruptors } from '@/store';
|
import MkInstanceTicker from "@/components/MkInstanceTicker.vue";
|
||||||
import { reactionPicker } from '@/scripts/reaction-picker';
|
import MkVisibility from "@/components/MkVisibility.vue";
|
||||||
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
|
import { pleaseLogin } from "@/scripts/please-login";
|
||||||
import { $i } from '@/account';
|
import { getWordMute } from "@/scripts/check-word-mute";
|
||||||
import { i18n } from '@/i18n';
|
import { userPage } from "@/filters/user";
|
||||||
import { getNoteMenu } from '@/scripts/get-note-menu';
|
import { notePage } from "@/filters/note";
|
||||||
import { useNoteCapture } from '@/scripts/use-note-capture';
|
import { useRouter } from "@/router";
|
||||||
import { deepClone } from '@/scripts/clone';
|
import * as os from "@/os";
|
||||||
import { stream } from '@/stream';
|
import { defaultStore, noteViewInterruptors } from "@/store";
|
||||||
import { NoteUpdatedEvent } from 'calckey-js/built/streaming.types';
|
import { reactionPicker } from "@/scripts/reaction-picker";
|
||||||
|
import { extractUrlFromMfm } from "@/scripts/extract-url-from-mfm";
|
||||||
|
import { $i } from "@/account";
|
||||||
|
import { i18n } from "@/i18n";
|
||||||
|
import { getNoteMenu } from "@/scripts/get-note-menu";
|
||||||
|
import { useNoteCapture } from "@/scripts/use-note-capture";
|
||||||
|
import { deepClone } from "@/scripts/clone";
|
||||||
|
import { stream } from "@/stream";
|
||||||
|
import { NoteUpdatedEvent } from "calckey-js/built/streaming.types";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@ -156,7 +312,7 @@ const props = defineProps<{
|
||||||
pinned?: boolean;
|
pinned?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const inChannel = inject('inChannel', null);
|
const inChannel = inject("inChannel", null);
|
||||||
|
|
||||||
let note = $ref(deepClone(props.note));
|
let note = $ref(deepClone(props.note));
|
||||||
|
|
||||||
|
@ -171,12 +327,11 @@ if (noteViewInterruptors.length > 0) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const isRenote = (
|
const isRenote =
|
||||||
note.renote != null &&
|
note.renote != null &&
|
||||||
note.text == null &&
|
note.text == null &&
|
||||||
note.fileIds.length === 0 &&
|
note.fileIds.length === 0 &&
|
||||||
note.poll == null
|
note.poll == null;
|
||||||
);
|
|
||||||
|
|
||||||
const el = ref<HTMLElement>();
|
const el = ref<HTMLElement>();
|
||||||
const noteEl = $ref();
|
const noteEl = $ref();
|
||||||
|
@ -185,28 +340,34 @@ const starButton = ref<InstanceType<typeof XStarButton>>();
|
||||||
const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
|
const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
|
||||||
const renoteTime = ref<HTMLElement>();
|
const renoteTime = ref<HTMLElement>();
|
||||||
const reactButton = ref<HTMLElement>();
|
const reactButton = ref<HTMLElement>();
|
||||||
let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note);
|
let appearNote = $computed(() =>
|
||||||
const isMyRenote = $i && ($i.id === note.userId);
|
isRenote ? (note.renote as misskey.entities.Note) : note
|
||||||
|
);
|
||||||
|
const isMyRenote = $i && $i.id === note.userId;
|
||||||
const showContent = ref(false);
|
const showContent = ref(false);
|
||||||
const isDeleted = ref(false);
|
const isDeleted = ref(false);
|
||||||
const muted = ref(getWordMute(appearNote, $i, defaultStore.state.mutedWords));
|
const muted = ref(getWordMute(appearNote, $i, defaultStore.state.mutedWords));
|
||||||
const translation = ref(null);
|
const translation = ref(null);
|
||||||
const translating = ref(false);
|
const translating = ref(false);
|
||||||
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)).slice(0, 5) : null;
|
const urls = appearNote.text
|
||||||
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
|
? extractUrlFromMfm(mfm.parse(appearNote.text)).slice(0, 5)
|
||||||
|
: null;
|
||||||
|
const showTicker =
|
||||||
|
defaultStore.state.instanceTicker === "always" ||
|
||||||
|
(defaultStore.state.instanceTicker === "remote" &&
|
||||||
|
appearNote.user.instance);
|
||||||
const conversation = ref<misskey.entities.Note[]>([]);
|
const conversation = ref<misskey.entities.Note[]>([]);
|
||||||
const replies = ref<misskey.entities.Note[]>([]);
|
const replies = ref<misskey.entities.Note[]>([]);
|
||||||
const directReplies = ref<misskey.entities.Note[]>([]);
|
const directReplies = ref<misskey.entities.Note[]>([]);
|
||||||
let isScrolling;
|
let isScrolling;
|
||||||
|
|
||||||
|
|
||||||
const keymap = {
|
const keymap = {
|
||||||
'r': () => reply(true),
|
r: () => reply(true),
|
||||||
'e|a|plus': () => react(true),
|
"e|a|plus": () => react(true),
|
||||||
'q': () => renoteButton.value.renote(true),
|
q: () => renoteButton.value.renote(true),
|
||||||
'esc': blur,
|
esc: blur,
|
||||||
'm|o': () => menu(true),
|
"m|o": () => menu(true),
|
||||||
's': () => showContent.value !== showContent.value,
|
s: () => showContent.value !== showContent.value,
|
||||||
};
|
};
|
||||||
|
|
||||||
useNoteCapture({
|
useNoteCapture({
|
||||||
|
@ -217,74 +378,106 @@ useNoteCapture({
|
||||||
|
|
||||||
function reply(viaKeyboard = false): void {
|
function reply(viaKeyboard = false): void {
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
os.post({
|
os.post(
|
||||||
|
{
|
||||||
reply: appearNote,
|
reply: appearNote,
|
||||||
animation: !viaKeyboard,
|
animation: !viaKeyboard,
|
||||||
}, () => {
|
},
|
||||||
|
() => {
|
||||||
focus();
|
focus();
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function react(viaKeyboard = false): void {
|
function react(viaKeyboard = false): void {
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
blur();
|
blur();
|
||||||
reactionPicker.show(reactButton.value, reaction => {
|
reactionPicker.show(
|
||||||
os.api('notes/reactions/create', {
|
reactButton.value,
|
||||||
|
(reaction) => {
|
||||||
|
os.api("notes/reactions/create", {
|
||||||
noteId: appearNote.id,
|
noteId: appearNote.id,
|
||||||
reaction: reaction,
|
reaction: reaction,
|
||||||
});
|
});
|
||||||
}, () => {
|
},
|
||||||
|
() => {
|
||||||
focus();
|
focus();
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function undoReact(note): void {
|
function undoReact(note): void {
|
||||||
const oldReaction = note.myReaction;
|
const oldReaction = note.myReaction;
|
||||||
if (!oldReaction) return;
|
if (!oldReaction) return;
|
||||||
os.api('notes/reactions/delete', {
|
os.api("notes/reactions/delete", {
|
||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onContextmenu(ev: MouseEvent): void {
|
function onContextmenu(ev: MouseEvent): void {
|
||||||
const isLink = (el: HTMLElement) => {
|
const isLink = (el: HTMLElement) => {
|
||||||
if (el.tagName === 'A') return true;
|
if (el.tagName === "A") return true;
|
||||||
if (el.parentElement) {
|
if (el.parentElement) {
|
||||||
return isLink(el.parentElement);
|
return isLink(el.parentElement);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (isLink(ev.target)) return;
|
if (isLink(ev.target)) return;
|
||||||
if (window.getSelection().toString() !== '') return;
|
if (window.getSelection().toString() !== "") return;
|
||||||
|
|
||||||
if (defaultStore.state.useReactionPickerForContextMenu) {
|
if (defaultStore.state.useReactionPickerForContextMenu) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
react();
|
react();
|
||||||
} else {
|
} else {
|
||||||
os.contextMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted }), ev).then(focus);
|
os.contextMenu(
|
||||||
|
getNoteMenu({
|
||||||
|
note: note,
|
||||||
|
translating,
|
||||||
|
translation,
|
||||||
|
menuButton,
|
||||||
|
isDeleted,
|
||||||
|
}),
|
||||||
|
ev
|
||||||
|
).then(focus);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function menu(viaKeyboard = false): void {
|
function menu(viaKeyboard = false): void {
|
||||||
os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted }), menuButton.value, {
|
os.popupMenu(
|
||||||
|
getNoteMenu({
|
||||||
|
note: note,
|
||||||
|
translating,
|
||||||
|
translation,
|
||||||
|
menuButton,
|
||||||
|
isDeleted,
|
||||||
|
}),
|
||||||
|
menuButton.value,
|
||||||
|
{
|
||||||
viaKeyboard,
|
viaKeyboard,
|
||||||
}).then(focus);
|
}
|
||||||
|
).then(focus);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showRenoteMenu(viaKeyboard = false): void {
|
function showRenoteMenu(viaKeyboard = false): void {
|
||||||
if (!isMyRenote) return;
|
if (!isMyRenote) return;
|
||||||
os.popupMenu([{
|
os.popupMenu(
|
||||||
|
[
|
||||||
|
{
|
||||||
text: i18n.ts.unrenote,
|
text: i18n.ts.unrenote,
|
||||||
icon: 'ph-trash ph-bold ph-lg',
|
icon: "ph-trash ph-bold ph-lg",
|
||||||
danger: true,
|
danger: true,
|
||||||
action: () => {
|
action: () => {
|
||||||
os.api('notes/delete', {
|
os.api("notes/delete", {
|
||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
});
|
});
|
||||||
isDeleted.value = true;
|
isDeleted.value = true;
|
||||||
},
|
},
|
||||||
}], renoteTime.value, {
|
},
|
||||||
|
],
|
||||||
|
renoteTime.value,
|
||||||
|
{
|
||||||
viaKeyboard: viaKeyboard,
|
viaKeyboard: viaKeyboard,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function focus() {
|
function focus() {
|
||||||
|
@ -295,20 +488,26 @@ function blur() {
|
||||||
noteEl.blur();
|
noteEl.blur();
|
||||||
}
|
}
|
||||||
|
|
||||||
os.api('notes/children', {
|
os.api("notes/children", {
|
||||||
noteId: appearNote.id,
|
noteId: appearNote.id,
|
||||||
limit: 30,
|
limit: 30,
|
||||||
depth: 12,
|
depth: 12,
|
||||||
}).then(res => {
|
}).then((res) => {
|
||||||
replies.value = res;
|
replies.value = res;
|
||||||
directReplies.value = res.filter(note => note.replyId === appearNote.id || note.renoteId === appearNote.id).reverse();
|
directReplies.value = res
|
||||||
|
.filter(
|
||||||
|
(note) =>
|
||||||
|
note.replyId === appearNote.id ||
|
||||||
|
note.renoteId === appearNote.id
|
||||||
|
)
|
||||||
|
.reverse();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (appearNote.replyId) {
|
if (appearNote.replyId) {
|
||||||
os.api('notes/conversation', {
|
os.api("notes/conversation", {
|
||||||
noteId: appearNote.replyId,
|
noteId: appearNote.replyId,
|
||||||
limit: 30,
|
limit: 30,
|
||||||
}).then(res => {
|
}).then((res) => {
|
||||||
conversation.value = res.reverse();
|
conversation.value = res.reverse();
|
||||||
focus();
|
focus();
|
||||||
});
|
});
|
||||||
|
@ -316,24 +515,23 @@ if (appearNote.replyId) {
|
||||||
|
|
||||||
function onNoteReplied(noteData: NoteUpdatedEvent): void {
|
function onNoteReplied(noteData: NoteUpdatedEvent): void {
|
||||||
const { type, id, body } = noteData;
|
const { type, id, body } = noteData;
|
||||||
if (type === 'replied' && id === appearNote.id) {
|
if (type === "replied" && id === appearNote.id) {
|
||||||
const { id: createdId } = body;
|
const { id: createdId } = body;
|
||||||
|
|
||||||
os.api('notes/show', {
|
os.api("notes/show", {
|
||||||
noteId: createdId,
|
noteId: createdId,
|
||||||
}).then(note => {
|
}).then((note) => {
|
||||||
if (note.replyId === appearNote.id) {
|
if (note.replyId === appearNote.id) {
|
||||||
replies.value.unshift(note);
|
replies.value.unshift(note);
|
||||||
directReplies.value.unshift(note);
|
directReplies.value.unshift(note);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("wheel", () => {
|
document.addEventListener("wheel", () => {
|
||||||
isScrolling = true;
|
isScrolling = true;
|
||||||
})
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
stream.on("noteUpdated", onNoteReplied);
|
stream.on("noteUpdated", onNoteReplied);
|
||||||
|
@ -343,14 +541,13 @@ onMounted(() => {
|
||||||
|
|
||||||
onUpdated(() => {
|
onUpdated(() => {
|
||||||
if (!isScrolling) {
|
if (!isScrolling) {
|
||||||
noteEl.scrollIntoView()
|
noteEl.scrollIntoView();
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
stream.off("noteUpdated", onNoteReplied);
|
stream.off("noteUpdated", onNoteReplied);
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -558,8 +755,9 @@ onUnmounted(() => {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border: solid 1px var(--renote);
|
border: solid 1px var(--renote);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
transition: background .2s;
|
transition: background 0.2s;
|
||||||
&:hover, &:focus-within {
|
&:hover,
|
||||||
|
&:focus-within {
|
||||||
background-color: var(--panelHighlight);
|
background-color: var(--panelHighlight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -617,7 +815,10 @@ onUnmounted(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hover
|
// Hover
|
||||||
.reply :deep(.main), .reply-to, .reply-to-more, :deep(.more) {
|
.reply :deep(.main),
|
||||||
|
.reply-to,
|
||||||
|
.reply-to-more,
|
||||||
|
:deep(.more) {
|
||||||
position: relative;
|
position: relative;
|
||||||
&::before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
|
@ -627,10 +828,11 @@ onUnmounted(() => {
|
||||||
background: var(--panelHighlight);
|
background: var(--panelHighlight);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity .2s;
|
transition: opacity 0.2s;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
}
|
}
|
||||||
&.reply-to, &.reply-to-more {
|
&.reply-to,
|
||||||
|
&.reply-to-more {
|
||||||
&::before {
|
&::before {
|
||||||
inset: 0px 8px;
|
inset: 0px 8px;
|
||||||
}
|
}
|
||||||
|
@ -651,7 +853,8 @@ onUnmounted(() => {
|
||||||
&.more::before {
|
&.more::before {
|
||||||
inset: 0 !important;
|
inset: 0 !important;
|
||||||
}
|
}
|
||||||
&:hover, &:focus-within {
|
&:hover,
|
||||||
|
&:focus-within {
|
||||||
&::before {
|
&::before {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
@ -668,13 +871,11 @@ onUnmounted(() => {
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
&.max-width_500px {
|
&.max-width_500px {
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.max-width_450px {
|
&.max-width_450px {
|
||||||
|
|
||||||
> .reply-to-more:first-child {
|
> .reply-to-more:first-child {
|
||||||
padding-top: 14px;
|
padding-top: 14px;
|
||||||
}
|
}
|
||||||
|
@ -708,7 +909,6 @@ onUnmounted(() => {
|
||||||
font-size: 0.825em;
|
font-size: 0.825em;
|
||||||
|
|
||||||
> .article {
|
> .article {
|
||||||
|
|
||||||
> .main {
|
> .main {
|
||||||
> .footer {
|
> .footer {
|
||||||
> .button {
|
> .button {
|
||||||
|
|
|
@ -2,7 +2,12 @@
|
||||||
<header class="kkwtjztg">
|
<header class="kkwtjztg">
|
||||||
<div class="user-info">
|
<div class="user-info">
|
||||||
<div>
|
<div>
|
||||||
<MkA v-user-preview="note.user.id" class="name" :to="userPage(note.user)" @click.stop>
|
<MkA
|
||||||
|
v-user-preview="note.user.id"
|
||||||
|
class="name"
|
||||||
|
:to="userPage(note.user)"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
<MkUserName :user="note.user" class="mkusername">
|
<MkUserName :user="note.user" class="mkusername">
|
||||||
<span v-if="note.user.isBot" class="is-bot">bot</span>
|
<span v-if="note.user.isBot" class="is-bot">bot</span>
|
||||||
</MkUserName>
|
</MkUserName>
|
||||||
|
@ -16,21 +21,25 @@
|
||||||
</MkA>
|
</MkA>
|
||||||
<MkVisibility :note="note" />
|
<MkVisibility :note="note" />
|
||||||
</div>
|
</div>
|
||||||
<MkInstanceTicker v-if="showTicker" class="ticker" :instance="note.user.instance"/>
|
<MkInstanceTicker
|
||||||
|
v-if="showTicker"
|
||||||
|
class="ticker"
|
||||||
|
:instance="note.user.instance"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import {} from "vue";
|
||||||
import type * as misskey from 'calckey-js';
|
import type * as misskey from "calckey-js";
|
||||||
import { defaultStore, noteViewInterruptors } from '@/store';
|
import { defaultStore, noteViewInterruptors } from "@/store";
|
||||||
import MkVisibility from '@/components/MkVisibility.vue';
|
import MkVisibility from "@/components/MkVisibility.vue";
|
||||||
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
|
import MkInstanceTicker from "@/components/MkInstanceTicker.vue";
|
||||||
import { notePage } from '@/filters/note';
|
import { notePage } from "@/filters/note";
|
||||||
import { userPage } from '@/filters/user';
|
import { userPage } from "@/filters/user";
|
||||||
import { deepClone } from '@/scripts/clone';
|
import { deepClone } from "@/scripts/clone";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
note: misskey.entities.Note;
|
note: misskey.entities.Note;
|
||||||
|
@ -39,10 +48,9 @@ const props = defineProps<{
|
||||||
|
|
||||||
let note = $ref(deepClone(props.note));
|
let note = $ref(deepClone(props.note));
|
||||||
|
|
||||||
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && note.user.instance);
|
const showTicker =
|
||||||
|
defaultStore.state.instanceTicker === "always" ||
|
||||||
|
(defaultStore.state.instanceTicker === "remote" && note.user.instance);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -54,7 +62,7 @@ const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultS
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
justify-self: flex-end;
|
justify-self: flex-end;
|
||||||
border-radius: 100px;
|
border-radius: 100px;
|
||||||
font-size: .8em;
|
font-size: 0.8em;
|
||||||
text-shadow: 0 2px 2px var(--shadow);
|
text-shadow: 0 2px 2px var(--shadow);
|
||||||
> .avatar {
|
> .avatar {
|
||||||
width: 3.7em;
|
width: 3.7em;
|
||||||
|
@ -73,11 +81,11 @@ const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultS
|
||||||
width: 0;
|
width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
gap: .1em 0;
|
gap: 0.1em 0;
|
||||||
}
|
}
|
||||||
&:last-child {
|
&:last-child {
|
||||||
max-width: 50%;
|
max-width: 50%;
|
||||||
gap: .3em .5em;
|
gap: 0.3em 0.5em;
|
||||||
}
|
}
|
||||||
.article > .main & {
|
.article > .main & {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -94,7 +102,7 @@ const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultS
|
||||||
.name {
|
.name {
|
||||||
// flex: 1 1 0px;
|
// flex: 1 1 0px;
|
||||||
display: inline;
|
display: inline;
|
||||||
margin: 0 .5em 0 0;
|
margin: 0 0.5em 0 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
@ -104,7 +112,7 @@ const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultS
|
||||||
.mkusername > .is-bot {
|
.mkusername > .is-bot {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
margin: 0 .5em 0 0;
|
margin: 0 0.5em 0 0;
|
||||||
padding: 1px 6px;
|
padding: 1px 6px;
|
||||||
font-size: 80%;
|
font-size: 80%;
|
||||||
border: solid 0.5px var(--divider);
|
border: solid 0.5px var(--divider);
|
||||||
|
@ -118,17 +126,17 @@ const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultS
|
||||||
|
|
||||||
.username {
|
.username {
|
||||||
display: inline;
|
display: inline;
|
||||||
margin: 0 .5em 0 0;
|
margin: 0 0.5em 0 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
font-size: .9em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-left: .5em;
|
margin-left: 0.5em;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
.created-at {
|
.created-at {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
@ -139,7 +147,7 @@ const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultS
|
||||||
|
|
||||||
.ticker {
|
.ticker {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
margin-left: .5em;
|
margin-left: 0.5em;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
> .name {
|
> .name {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
|
@ -15,8 +15,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import {} from "vue";
|
||||||
import { preprocess } from '@/scripts/preprocess';
|
import { preprocess } from "@/scripts/preprocess";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
text: string;
|
text: string;
|
||||||
|
@ -67,7 +67,6 @@ const props = defineProps<{
|
||||||
}
|
}
|
||||||
|
|
||||||
> .body {
|
> .body {
|
||||||
|
|
||||||
> .cw {
|
> .cw {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
display: block;
|
display: block;
|
||||||
|
|
|
@ -5,7 +5,14 @@
|
||||||
<XNoteHeader class="header" :note="note" :mini="true" />
|
<XNoteHeader class="header" :note="note" :mini="true" />
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<p v-if="note.cw != null" class="cw">
|
<p v-if="note.cw != null" class="cw">
|
||||||
<Mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$i" :custom-emojis="note.emojis"/>
|
<Mfm
|
||||||
|
v-if="note.cw != ''"
|
||||||
|
class="text"
|
||||||
|
:text="note.cw"
|
||||||
|
:author="note.user"
|
||||||
|
:i="$i"
|
||||||
|
:custom-emojis="note.emojis"
|
||||||
|
/>
|
||||||
<br />
|
<br />
|
||||||
<XCwButton v-model="showContent" :note="note" />
|
<XCwButton v-model="showContent" :note="note" />
|
||||||
</p>
|
</p>
|
||||||
|
@ -18,11 +25,11 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import {} from "vue";
|
||||||
import * as misskey from 'calckey-js';
|
import * as misskey from "calckey-js";
|
||||||
import XNoteHeader from '@/components/MkNoteHeader.vue';
|
import XNoteHeader from "@/components/MkNoteHeader.vue";
|
||||||
import MkSubNoteContent from '@/components/MkSubNoteContent.vue';
|
import MkSubNoteContent from "@/components/MkSubNoteContent.vue";
|
||||||
import XCwButton from '@/components/MkCwButton.vue';
|
import XCwButton from "@/components/MkCwButton.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
note: misskey.entities.Note;
|
note: misskey.entities.Note;
|
||||||
|
@ -74,7 +81,6 @@ const showContent = $ref(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
> .body {
|
> .body {
|
||||||
|
|
||||||
> .cw {
|
> .cw {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
display: block;
|
display: block;
|
||||||
|
|
|
@ -1,58 +1,141 @@
|
||||||
<template>
|
<template>
|
||||||
<div ref="el"
|
<div
|
||||||
|
ref="el"
|
||||||
v-size="{ max: [450, 500] }"
|
v-size="{ max: [450, 500] }"
|
||||||
class="wrpstxzv"
|
class="wrpstxzv"
|
||||||
:class="{ children: depth > 1, singleStart: replies.length == 1, firstColumn: depth == 1 && conversation }"
|
:class="{
|
||||||
|
children: depth > 1,
|
||||||
|
singleStart: replies.length == 1,
|
||||||
|
firstColumn: depth == 1 && conversation,
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<div v-if="conversation && depth > 1" class="line"></div>
|
<div v-if="conversation && depth > 1" class="line"></div>
|
||||||
<div class="main" @click="noteClick">
|
<div class="main" @click="noteClick">
|
||||||
<div class="avatar-container">
|
<div class="avatar-container">
|
||||||
<MkAvatar class="avatar" :user="appearNote.user" />
|
<MkAvatar class="avatar" :user="appearNote.user" />
|
||||||
<div v-if="(!conversation) || replies.length > 0" class="line"></div>
|
<div
|
||||||
|
v-if="!conversation || replies.length > 0"
|
||||||
|
class="line"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<XNoteHeader class="header" :note="note" :mini="true" />
|
<XNoteHeader class="header" :note="note" :mini="true" />
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<p v-if="appearNote.cw != null" class="cw">
|
<p v-if="appearNote.cw != null" class="cw">
|
||||||
<MkA v-if="appearNote.replyId" :to="`/notes/${appearNote.replyId}`" class="reply-icon" @click.stop>
|
<MkA
|
||||||
|
v-if="appearNote.replyId"
|
||||||
|
:to="`/notes/${appearNote.replyId}`"
|
||||||
|
class="reply-icon"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
<i class="ph-arrow-bend-left-up ph-bold ph-lg"></i>
|
<i class="ph-arrow-bend-left-up ph-bold ph-lg"></i>
|
||||||
</MkA>
|
</MkA>
|
||||||
<MkA v-if="conversation && appearNote.renoteId && appearNote.renoteId != parentId && !appearNote.replyId" :to="`/notes/${appearNote.renoteId}`" class="reply-icon" @click.stop>
|
<MkA
|
||||||
|
v-if="
|
||||||
|
conversation &&
|
||||||
|
appearNote.renoteId &&
|
||||||
|
appearNote.renoteId != parentId &&
|
||||||
|
!appearNote.replyId
|
||||||
|
"
|
||||||
|
:to="`/notes/${appearNote.renoteId}`"
|
||||||
|
class="reply-icon"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
<i class="ph-quotes ph-bold ph-lg"></i>
|
<i class="ph-quotes ph-bold ph-lg"></i>
|
||||||
</MkA>
|
</MkA>
|
||||||
<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
<Mfm
|
||||||
|
v-if="appearNote.cw != ''"
|
||||||
|
class="text"
|
||||||
|
:text="appearNote.cw"
|
||||||
|
:author="appearNote.user"
|
||||||
|
:i="$i"
|
||||||
|
:custom-emojis="appearNote.emojis"
|
||||||
|
/>
|
||||||
<br />
|
<br />
|
||||||
<XCwButton v-model="showContent" :note="note" />
|
<XCwButton v-model="showContent" :note="note" />
|
||||||
</p>
|
</p>
|
||||||
<div v-show="appearNote.cw == null || showContent" class="content">
|
<div
|
||||||
<MkSubNoteContent class="text" :note="note" :detailed="true" :parentId="appearNote.parentId" :conversation="conversation"/>
|
v-show="appearNote.cw == null || showContent"
|
||||||
|
class="content"
|
||||||
|
>
|
||||||
|
<MkSubNoteContent
|
||||||
|
class="text"
|
||||||
|
:note="note"
|
||||||
|
:detailed="true"
|
||||||
|
:parentId="appearNote.parentId"
|
||||||
|
:conversation="conversation"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="translating || translation" class="translation">
|
<div v-if="translating || translation" class="translation">
|
||||||
<MkLoading v-if="translating" mini />
|
<MkLoading v-if="translating" mini />
|
||||||
<div v-else class="translated">
|
<div v-else class="translated">
|
||||||
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
<b
|
||||||
<Mfm :text="translation.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
>{{
|
||||||
|
i18n.t("translatedFrom", {
|
||||||
|
x: translation.sourceLang,
|
||||||
|
})
|
||||||
|
}}:
|
||||||
|
</b>
|
||||||
|
<Mfm
|
||||||
|
:text="translation.text"
|
||||||
|
:author="appearNote.user"
|
||||||
|
:i="$i"
|
||||||
|
:custom-emojis="appearNote.emojis"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<footer class="footer" @click.stop>
|
<footer class="footer" @click.stop>
|
||||||
<XReactionsViewer ref="reactionsViewer" :note="appearNote"/>
|
<XReactionsViewer
|
||||||
<button v-tooltip.noDelay.bottom="i18n.ts.reply" class="button _button" @click="reply()">
|
ref="reactionsViewer"
|
||||||
|
:note="appearNote"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-tooltip.noDelay.bottom="i18n.ts.reply"
|
||||||
|
class="button _button"
|
||||||
|
@click="reply()"
|
||||||
|
>
|
||||||
<i class="ph-arrow-u-up-left ph-bold ph-lg"></i>
|
<i class="ph-arrow-u-up-left ph-bold ph-lg"></i>
|
||||||
<template v-if="appearNote.repliesCount > 0">
|
<template v-if="appearNote.repliesCount > 0">
|
||||||
<p class="count">{{ appearNote.repliesCount }}</p>
|
<p class="count">{{ appearNote.repliesCount }}</p>
|
||||||
</template>
|
</template>
|
||||||
</button>
|
</button>
|
||||||
<XRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/>
|
<XRenoteButton
|
||||||
<XStarButton v-if="appearNote.myReaction == null" ref="starButton" class="button" :note="appearNote"/>
|
ref="renoteButton"
|
||||||
<button v-if="appearNote.myReaction == null" ref="reactButton" v-tooltip.noDelay.bottom="i18n.ts.reaction" class="button _button" @click="react()">
|
class="button"
|
||||||
|
:note="appearNote"
|
||||||
|
:count="appearNote.renoteCount"
|
||||||
|
/>
|
||||||
|
<XStarButton
|
||||||
|
v-if="appearNote.myReaction == null"
|
||||||
|
ref="starButton"
|
||||||
|
class="button"
|
||||||
|
:note="appearNote"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-if="appearNote.myReaction == null"
|
||||||
|
ref="reactButton"
|
||||||
|
v-tooltip.noDelay.bottom="i18n.ts.reaction"
|
||||||
|
class="button _button"
|
||||||
|
@click="react()"
|
||||||
|
>
|
||||||
<i class="ph-smiley ph-bold ph-lg"></i>
|
<i class="ph-smiley ph-bold ph-lg"></i>
|
||||||
</button>
|
</button>
|
||||||
<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
|
<button
|
||||||
|
v-if="appearNote.myReaction != null"
|
||||||
|
ref="reactButton"
|
||||||
|
class="button _button reacted"
|
||||||
|
@click="undoReact(appearNote)"
|
||||||
|
>
|
||||||
<i class="ph-minus ph-bold ph-lg"></i>
|
<i class="ph-minus ph-bold ph-lg"></i>
|
||||||
</button>
|
</button>
|
||||||
<XQuoteButton class="button" :note="appearNote" />
|
<XQuoteButton class="button" :note="appearNote" />
|
||||||
<button ref="menuButton" v-tooltip.noDelay.bottom="i18n.ts.more" class="button _button" @click="menu()">
|
<button
|
||||||
|
ref="menuButton"
|
||||||
|
v-tooltip.noDelay.bottom="i18n.ts.more"
|
||||||
|
class="button _button"
|
||||||
|
@click="menu()"
|
||||||
|
>
|
||||||
<i class="ph-dots-three-outline ph-bold ph-lg"></i>
|
<i class="ph-dots-three-outline ph-bold ph-lg"></i>
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -60,119 +143,167 @@
|
||||||
</div>
|
</div>
|
||||||
<template v-if="conversation">
|
<template v-if="conversation">
|
||||||
<template v-if="replies.length == 1">
|
<template v-if="replies.length == 1">
|
||||||
<MkNoteSub v-for="reply in replies" :key="reply.id" :note="reply" class="reply single" :conversation="conversation" :depth="depth" :parentId="appearNote.replyId"/>
|
<MkNoteSub
|
||||||
|
v-for="reply in replies"
|
||||||
|
:key="reply.id"
|
||||||
|
:note="reply"
|
||||||
|
class="reply single"
|
||||||
|
:conversation="conversation"
|
||||||
|
:depth="depth"
|
||||||
|
:parentId="appearNote.replyId"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="depth < 5">
|
<template v-else-if="depth < 5">
|
||||||
<MkNoteSub v-for="reply in replies" :key="reply.id" :note="reply" class="reply" :conversation="conversation" :depth="depth + 1" :parentId="appearNote.replyId"/>
|
<MkNoteSub
|
||||||
|
v-for="reply in replies"
|
||||||
|
:key="reply.id"
|
||||||
|
:note="reply"
|
||||||
|
class="reply"
|
||||||
|
:conversation="conversation"
|
||||||
|
:depth="depth + 1"
|
||||||
|
:parentId="appearNote.replyId"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<div v-else-if="replies.length > 0" class="more">
|
<div v-else-if="replies.length > 0" class="more">
|
||||||
<div class="line"></div>
|
<div class="line"></div>
|
||||||
<MkA class="text _link" :to="notePage(note)">{{ i18n.ts.continueThread }} <i class="ph-caret-double-right ph-bold ph-lg"></i></MkA>
|
<MkA class="text _link" :to="notePage(note)"
|
||||||
|
>{{ i18n.ts.continueThread }}
|
||||||
|
<i class="ph-caret-double-right ph-bold ph-lg"></i
|
||||||
|
></MkA>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { inject, ref } from 'vue';
|
import { inject, ref } from "vue";
|
||||||
import type { Ref } from 'vue';
|
import type { Ref } from "vue";
|
||||||
import * as misskey from 'calckey-js';
|
import * as misskey from "calckey-js";
|
||||||
import XNoteHeader from '@/components/MkNoteHeader.vue';
|
import XNoteHeader from "@/components/MkNoteHeader.vue";
|
||||||
import MkSubNoteContent from '@/components/MkSubNoteContent.vue';
|
import MkSubNoteContent from "@/components/MkSubNoteContent.vue";
|
||||||
import XReactionsViewer from '@/components/MkReactionsViewer.vue';
|
import XReactionsViewer from "@/components/MkReactionsViewer.vue";
|
||||||
import XStarButton from '@/components/MkStarButton.vue';
|
import XStarButton from "@/components/MkStarButton.vue";
|
||||||
import XRenoteButton from '@/components/MkRenoteButton.vue';
|
import XRenoteButton from "@/components/MkRenoteButton.vue";
|
||||||
import XQuoteButton from '@/components/MkQuoteButton.vue';
|
import XQuoteButton from "@/components/MkQuoteButton.vue";
|
||||||
import XCwButton from '@/components/MkCwButton.vue';
|
import XCwButton from "@/components/MkCwButton.vue";
|
||||||
import { pleaseLogin } from '@/scripts/please-login';
|
import { pleaseLogin } from "@/scripts/please-login";
|
||||||
import { getNoteMenu } from '@/scripts/get-note-menu';
|
import { getNoteMenu } from "@/scripts/get-note-menu";
|
||||||
import { notePage } from '@/filters/note';
|
import { notePage } from "@/filters/note";
|
||||||
import { useRouter } from '@/router';
|
import { useRouter } from "@/router";
|
||||||
import * as os from '@/os';
|
import * as os from "@/os";
|
||||||
import { reactionPicker } from '@/scripts/reaction-picker';
|
import { reactionPicker } from "@/scripts/reaction-picker";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
import { deepClone } from '@/scripts/clone';
|
import { deepClone } from "@/scripts/clone";
|
||||||
import { useNoteCapture } from '@/scripts/use-note-capture';
|
import { useNoteCapture } from "@/scripts/use-note-capture";
|
||||||
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
note: misskey.entities.Note;
|
note: misskey.entities.Note;
|
||||||
conversation?: misskey.entities.Note[];
|
conversation?: misskey.entities.Note[];
|
||||||
parentId?;
|
parentId?;
|
||||||
|
|
||||||
// how many notes are in between this one and the note being viewed in detail
|
// how many notes are in between this one and the note being viewed in detail
|
||||||
depth?: number;
|
depth?: number;
|
||||||
}>(), {
|
}>(),
|
||||||
|
{
|
||||||
depth: 1,
|
depth: 1,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
let note = $ref(deepClone(props.note));
|
let note = $ref(deepClone(props.note));
|
||||||
|
|
||||||
const isRenote = (
|
const isRenote =
|
||||||
note.renote != null &&
|
note.renote != null &&
|
||||||
note.text == null &&
|
note.text == null &&
|
||||||
note.fileIds.length === 0 &&
|
note.fileIds.length === 0 &&
|
||||||
note.poll == null
|
note.poll == null;
|
||||||
);
|
|
||||||
|
|
||||||
const el = ref<HTMLElement>();
|
const el = ref<HTMLElement>();
|
||||||
const menuButton = ref<HTMLElement>();
|
const menuButton = ref<HTMLElement>();
|
||||||
const starButton = ref<InstanceType<typeof XStarButton>>();
|
const starButton = ref<InstanceType<typeof XStarButton>>();
|
||||||
const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
|
const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
|
||||||
const reactButton = ref<HTMLElement>();
|
const reactButton = ref<HTMLElement>();
|
||||||
let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note);
|
let appearNote = $computed(() =>
|
||||||
|
isRenote ? (note.renote as misskey.entities.Note) : note
|
||||||
|
);
|
||||||
const isDeleted = ref(false);
|
const isDeleted = ref(false);
|
||||||
const translation = ref(null);
|
const translation = ref(null);
|
||||||
const translating = ref(false);
|
const translating = ref(false);
|
||||||
let showContent = $ref(false);
|
let showContent = $ref(false);
|
||||||
const replies: misskey.entities.Note[] = props.conversation?.filter(item => item.replyId === props.note.id || item.renoteId === props.note.id).reverse() ?? [];
|
const replies: misskey.entities.Note[] =
|
||||||
|
props.conversation
|
||||||
|
?.filter(
|
||||||
|
(item) =>
|
||||||
|
item.replyId === props.note.id ||
|
||||||
|
item.renoteId === props.note.id
|
||||||
|
)
|
||||||
|
.reverse() ?? [];
|
||||||
|
|
||||||
useNoteCapture({
|
useNoteCapture({
|
||||||
rootEl: el,
|
rootEl: el,
|
||||||
note: $$(appearNote)
|
note: $$(appearNote),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
function reply(viaKeyboard = false): void {
|
function reply(viaKeyboard = false): void {
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
os.post({
|
os.post(
|
||||||
|
{
|
||||||
reply: appearNote,
|
reply: appearNote,
|
||||||
animation: !viaKeyboard,
|
animation: !viaKeyboard,
|
||||||
}, () => {
|
},
|
||||||
|
() => {
|
||||||
focus();
|
focus();
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function react(viaKeyboard = false): void {
|
function react(viaKeyboard = false): void {
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
blur();
|
blur();
|
||||||
reactionPicker.show(reactButton.value, reaction => {
|
reactionPicker.show(
|
||||||
os.api('notes/reactions/create', {
|
reactButton.value,
|
||||||
|
(reaction) => {
|
||||||
|
os.api("notes/reactions/create", {
|
||||||
noteId: appearNote.id,
|
noteId: appearNote.id,
|
||||||
reaction: reaction,
|
reaction: reaction,
|
||||||
});
|
});
|
||||||
}, () => {
|
},
|
||||||
|
() => {
|
||||||
focus();
|
focus();
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function undoReact(note): void {
|
function undoReact(note): void {
|
||||||
const oldReaction = note.myReaction;
|
const oldReaction = note.myReaction;
|
||||||
if (!oldReaction) return;
|
if (!oldReaction) return;
|
||||||
os.api('notes/reactions/delete', {
|
os.api("notes/reactions/delete", {
|
||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentClipPage = inject<Ref<misskey.entities.Clip> | null>('currentClipPage', null);
|
const currentClipPage = inject<Ref<misskey.entities.Clip> | null>(
|
||||||
|
"currentClipPage",
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
function menu(viaKeyboard = false): void {
|
function menu(viaKeyboard = false): void {
|
||||||
os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClipPage }), menuButton.value, {
|
os.popupMenu(
|
||||||
|
getNoteMenu({
|
||||||
|
note: note,
|
||||||
|
translating,
|
||||||
|
translation,
|
||||||
|
menuButton,
|
||||||
|
isDeleted,
|
||||||
|
currentClipPage,
|
||||||
|
}),
|
||||||
|
menuButton.value,
|
||||||
|
{
|
||||||
viaKeyboard,
|
viaKeyboard,
|
||||||
}).then(focus);
|
}
|
||||||
|
).then(focus);
|
||||||
}
|
}
|
||||||
|
|
||||||
function focus() {
|
function focus() {
|
||||||
|
@ -184,10 +315,10 @@ function blur() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function noteClick(e) {
|
function noteClick(e) {
|
||||||
if (document.getSelection().type === 'Range') {
|
if (document.getSelection().type === "Range") {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
} else {
|
} else {
|
||||||
router.push(notePage(props.note))
|
router.push(notePage(props.note));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -206,7 +337,6 @@ function noteClick(e) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
> .main {
|
> .main {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
|
@ -241,11 +371,12 @@ function noteClick(e) {
|
||||||
.reply-icon {
|
.reply-icon {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: .2em .2em;
|
padding: 0.2em 0.2em;
|
||||||
margin-right: .2em;
|
margin-right: 0.2em;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
transition: background .2s;
|
transition: background 0.2s;
|
||||||
&:hover, &:focus {
|
&:hover,
|
||||||
|
&:focus {
|
||||||
background: var(--buttonHoverBg);
|
background: var(--buttonHoverBg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -289,9 +420,9 @@ function noteClick(e) {
|
||||||
width: max-content;
|
width: max-content;
|
||||||
min-width: max-content;
|
min-width: max-content;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
transition: opacity .2s;
|
transition: opacity 0.2s;
|
||||||
&:first-of-type {
|
&:first-of-type {
|
||||||
margin-left: -.5em;
|
margin-left: -0.5em;
|
||||||
}
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--fgHighlighted);
|
color: var(--fgHighlighted);
|
||||||
|
@ -320,7 +451,8 @@ function noteClick(e) {
|
||||||
margin-right: 8px !important;
|
margin-right: 8px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
> .reply, > .more {
|
> .reply,
|
||||||
|
> .more {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
&.single {
|
&.single {
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
|
@ -361,15 +493,19 @@ function noteClick(e) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.reply, &.reply-to, &.reply-to-more {
|
&.reply,
|
||||||
> .main:hover, > .main:focus-within {
|
&.reply-to,
|
||||||
|
&.reply-to-more {
|
||||||
|
> .main:hover,
|
||||||
|
> .main:focus-within {
|
||||||
:deep(.footer .button) {
|
:deep(.footer .button) {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.reply-to, &.reply-to-more {
|
&.reply-to,
|
||||||
|
&.reply-to-more {
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
&:first-child {
|
&:first-child {
|
||||||
padding-top: 24px;
|
padding-top: 24px;
|
||||||
|
@ -380,7 +516,9 @@ function noteClick(e) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reply Lines
|
// Reply Lines
|
||||||
&.reply, &.reply-to, &.reply-to-more {
|
&.reply,
|
||||||
|
&.reply-to,
|
||||||
|
&.reply-to-more {
|
||||||
--indent: calc(var(--avatarSize) - 5px);
|
--indent: calc(var(--avatarSize) - 5px);
|
||||||
> .main {
|
> .main {
|
||||||
> .avatar-container {
|
> .avatar-container {
|
||||||
|
@ -413,17 +551,20 @@ function noteClick(e) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.reply-to, &.reply-to-more {
|
&.reply-to,
|
||||||
|
&.reply-to-more {
|
||||||
> .main > .avatar-container > .line {
|
> .main > .avatar-container > .line {
|
||||||
margin-bottom: 0px !important;
|
margin-bottom: 0px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.single, &.singleStart {
|
&.single,
|
||||||
|
&.singleStart {
|
||||||
> .main > .avatar-container > .line {
|
> .main > .avatar-container > .line {
|
||||||
margin-bottom: -10px !important;
|
margin-bottom: -10px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.reply.children:not(:last-child) { // Line that goes through multiple replies
|
.reply.children:not(:last-child) {
|
||||||
|
// Line that goes through multiple replies
|
||||||
position: relative;
|
position: relative;
|
||||||
> .line {
|
> .line {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -473,7 +614,9 @@ function noteClick(e) {
|
||||||
}
|
}
|
||||||
&.firstColumn > .children:last-child > .main {
|
&.firstColumn > .children:last-child > .main {
|
||||||
padding-bottom: 0 !important;
|
padding-bottom: 0 !important;
|
||||||
&::before { bottom: 0 !important }
|
&::before {
|
||||||
|
bottom: 0 !important;
|
||||||
|
}
|
||||||
// &::after { content: unset }
|
// &::after { content: unset }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -485,7 +628,9 @@ function noteClick(e) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.firstColumn {
|
&.firstColumn {
|
||||||
> .main, > .line, > .children:not(.single) > .line {
|
> .main,
|
||||||
|
> .line,
|
||||||
|
> .children:not(.single) > .line {
|
||||||
--avatarSize: 35px;
|
--avatarSize: 35px;
|
||||||
--indent: 35px;
|
--indent: 35px;
|
||||||
}
|
}
|
||||||
|
@ -496,7 +641,8 @@ function noteClick(e) {
|
||||||
}
|
}
|
||||||
&.max-width_450px {
|
&.max-width_450px {
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
&.reply-to, &.reply-to-more {
|
&.reply-to,
|
||||||
|
&.reply-to-more {
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
padding-top: 14px !important;
|
padding-top: 14px !important;
|
||||||
padding-bottom: 0 !important;
|
padding-bottom: 0 !important;
|
||||||
|
|
|
@ -2,15 +2,32 @@
|
||||||
<MkPagination ref="pagingComponent" :pagination="pagination">
|
<MkPagination ref="pagingComponent" :pagination="pagination">
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<div class="_fullinfo">
|
<div class="_fullinfo">
|
||||||
<img src="/static-assets/badges/info.png" class="_ghost" alt="Info"/>
|
<img
|
||||||
|
src="/static-assets/badges/info.png"
|
||||||
|
class="_ghost"
|
||||||
|
alt="Info"
|
||||||
|
/>
|
||||||
<div>{{ i18n.ts.noNotes }}</div>
|
<div>{{ i18n.ts.noNotes }}</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #default="{ items: notes }">
|
<template #default="{ items: notes }">
|
||||||
<div class="giivymft" :class="{ noGap }">
|
<div class="giivymft" :class="{ noGap }">
|
||||||
<XList ref="notes" v-slot="{ item: note }" :items="notes" :direction="pagination.reversed ? 'up' : 'down'" :reversed="pagination.reversed" :no-gap="noGap" :ad="true" class="notes">
|
<XList
|
||||||
<XNote :key="note._featuredId_ || note._prId_ || note.id" class="qtqtichx" :note="note"/>
|
ref="notes"
|
||||||
|
v-slot="{ item: note }"
|
||||||
|
:items="notes"
|
||||||
|
:direction="pagination.reversed ? 'up' : 'down'"
|
||||||
|
:reversed="pagination.reversed"
|
||||||
|
:no-gap="noGap"
|
||||||
|
:ad="true"
|
||||||
|
class="notes"
|
||||||
|
>
|
||||||
|
<XNote
|
||||||
|
:key="note._featuredId_ || note._prId_ || note.id"
|
||||||
|
class="qtqtichx"
|
||||||
|
:note="note"
|
||||||
|
/>
|
||||||
</XList>
|
</XList>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -18,12 +35,12 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from "vue";
|
||||||
import type { Paging } from '@/components/MkPagination.vue';
|
import type { Paging } from "@/components/MkPagination.vue";
|
||||||
import XNote from '@/components/MkNote.vue';
|
import XNote from "@/components/MkNote.vue";
|
||||||
import XList from '@/components/MkDateSeparatedList.vue';
|
import XList from "@/components/MkDateSeparatedList.vue";
|
||||||
import MkPagination from '@/components/MkPagination.vue';
|
import MkPagination from "@/components/MkPagination.vue";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
pagination: Paging;
|
pagination: Paging;
|
||||||
|
|
|
@ -1,25 +1,80 @@
|
||||||
<template>
|
<template>
|
||||||
<div ref="elRef" v-size="{ max: [500, 600] }" class="qglefbjs" :class="notification.type">
|
<div
|
||||||
|
ref="elRef"
|
||||||
|
v-size="{ max: [500, 600] }"
|
||||||
|
class="qglefbjs"
|
||||||
|
:class="notification.type"
|
||||||
|
>
|
||||||
<div class="head">
|
<div class="head">
|
||||||
<MkAvatar v-if="notification.type === 'pollEnded'" class="icon" :user="notification.note.user"/>
|
<MkAvatar
|
||||||
<MkAvatar v-else-if="notification.user" class="icon" :user="notification.user"/>
|
v-if="notification.type === 'pollEnded'"
|
||||||
<img v-else-if="notification.icon" class="icon" :src="notification.icon" alt=""/>
|
class="icon"
|
||||||
|
:user="notification.note.user"
|
||||||
|
/>
|
||||||
|
<MkAvatar
|
||||||
|
v-else-if="notification.user"
|
||||||
|
class="icon"
|
||||||
|
:user="notification.user"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
v-else-if="notification.icon"
|
||||||
|
class="icon"
|
||||||
|
:src="notification.icon"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
<div class="sub-icon" :class="notification.type">
|
<div class="sub-icon" :class="notification.type">
|
||||||
<i v-if="notification.type === 'follow'" class="ph-hand-waving ph-bold"></i>
|
<i
|
||||||
<i v-else-if="notification.type === 'receiveFollowRequest'" class="ph-clock ph-bold"></i>
|
v-if="notification.type === 'follow'"
|
||||||
<i v-else-if="notification.type === 'followRequestAccepted'" class="ph-check ph-bold"></i>
|
class="ph-hand-waving ph-bold"
|
||||||
<i v-else-if="notification.type === 'groupInvited'" class="ph-identification-card ph-bold"></i>
|
></i>
|
||||||
<i v-else-if="notification.type === 'renote'" class="ph-repeat ph-bold"></i>
|
<i
|
||||||
<i v-else-if="notification.type === 'reply'" class="ph-arrow-bend-up-left ph-bold"></i>
|
v-else-if="notification.type === 'receiveFollowRequest'"
|
||||||
<i v-else-if="notification.type === 'mention'" class="ph-at ph-bold"></i>
|
class="ph-clock ph-bold"
|
||||||
<i v-else-if="notification.type === 'quote'" class="ph-quotes ph-bold"></i>
|
></i>
|
||||||
<i v-else-if="notification.type === 'pollVote'" class="ph-microphone-stage ph-bold"></i>
|
<i
|
||||||
<i v-else-if="notification.type === 'pollEnded'" class="ph-microphone-stage ph-bold"></i>
|
v-else-if="notification.type === 'followRequestAccepted'"
|
||||||
|
class="ph-check ph-bold"
|
||||||
|
></i>
|
||||||
|
<i
|
||||||
|
v-else-if="notification.type === 'groupInvited'"
|
||||||
|
class="ph-identification-card ph-bold"
|
||||||
|
></i>
|
||||||
|
<i
|
||||||
|
v-else-if="notification.type === 'renote'"
|
||||||
|
class="ph-repeat ph-bold"
|
||||||
|
></i>
|
||||||
|
<i
|
||||||
|
v-else-if="notification.type === 'reply'"
|
||||||
|
class="ph-arrow-bend-up-left ph-bold"
|
||||||
|
></i>
|
||||||
|
<i
|
||||||
|
v-else-if="notification.type === 'mention'"
|
||||||
|
class="ph-at ph-bold"
|
||||||
|
></i>
|
||||||
|
<i
|
||||||
|
v-else-if="notification.type === 'quote'"
|
||||||
|
class="ph-quotes ph-bold"
|
||||||
|
></i>
|
||||||
|
<i
|
||||||
|
v-else-if="notification.type === 'pollVote'"
|
||||||
|
class="ph-microphone-stage ph-bold"
|
||||||
|
></i>
|
||||||
|
<i
|
||||||
|
v-else-if="notification.type === 'pollEnded'"
|
||||||
|
class="ph-microphone-stage ph-bold"
|
||||||
|
></i>
|
||||||
<!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
|
<!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
|
||||||
<XReactionIcon
|
<XReactionIcon
|
||||||
v-else-if="notification.type === 'reaction'"
|
v-else-if="notification.type === 'reaction'"
|
||||||
ref="reactionRef"
|
ref="reactionRef"
|
||||||
:reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction"
|
:reaction="
|
||||||
|
notification.reaction
|
||||||
|
? notification.reaction.replace(
|
||||||
|
/^:(\w+):$/,
|
||||||
|
':$1@.:'
|
||||||
|
)
|
||||||
|
: notification.reaction
|
||||||
|
"
|
||||||
:custom-emojis="notification.note.emojis"
|
:custom-emojis="notification.note.emojis"
|
||||||
:no-style="true"
|
:no-style="true"
|
||||||
/>
|
/>
|
||||||
|
@ -27,44 +82,176 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="tail">
|
<div class="tail">
|
||||||
<header>
|
<header>
|
||||||
<span v-if="notification.type === 'pollEnded'">{{ i18n.ts._notification.pollEnded }}</span>
|
<span v-if="notification.type === 'pollEnded'">{{
|
||||||
<MkA v-else-if="notification.user" v-user-preview="notification.user.id" class="name" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
|
i18n.ts._notification.pollEnded
|
||||||
|
}}</span>
|
||||||
|
<MkA
|
||||||
|
v-else-if="notification.user"
|
||||||
|
v-user-preview="notification.user.id"
|
||||||
|
class="name"
|
||||||
|
:to="userPage(notification.user)"
|
||||||
|
><MkUserName :user="notification.user"
|
||||||
|
/></MkA>
|
||||||
<span v-else>{{ notification.header }}</span>
|
<span v-else>{{ notification.header }}</span>
|
||||||
<MkTime v-if="withTime" :time="notification.createdAt" class="time"/>
|
<MkTime
|
||||||
|
v-if="withTime"
|
||||||
|
:time="notification.createdAt"
|
||||||
|
class="time"
|
||||||
|
/>
|
||||||
</header>
|
</header>
|
||||||
<MkA v-if="notification.type === 'reaction'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
<MkA
|
||||||
|
v-if="notification.type === 'reaction'"
|
||||||
|
class="text"
|
||||||
|
:to="notePage(notification.note)"
|
||||||
|
:title="getNoteSummary(notification.note)"
|
||||||
|
>
|
||||||
<i class="ph-quotes ph-fill ph-lg"></i>
|
<i class="ph-quotes ph-fill ph-lg"></i>
|
||||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
|
<Mfm
|
||||||
|
:text="getNoteSummary(notification.note)"
|
||||||
|
:plain="true"
|
||||||
|
:nowrap="!full"
|
||||||
|
:custom-emojis="notification.note.emojis"
|
||||||
|
/>
|
||||||
<i class="ph-quotes ph-fill ph-lg"></i>
|
<i class="ph-quotes ph-fill ph-lg"></i>
|
||||||
</MkA>
|
</MkA>
|
||||||
<MkA v-if="notification.type === 'renote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)">
|
<MkA
|
||||||
|
v-if="notification.type === 'renote'"
|
||||||
|
class="text"
|
||||||
|
:to="notePage(notification.note)"
|
||||||
|
:title="getNoteSummary(notification.note.renote)"
|
||||||
|
>
|
||||||
<i class="ph-quotes ph-fill ph-lg"></i>
|
<i class="ph-quotes ph-fill ph-lg"></i>
|
||||||
<Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.renote.emojis"/>
|
<Mfm
|
||||||
|
:text="getNoteSummary(notification.note.renote)"
|
||||||
|
:plain="true"
|
||||||
|
:nowrap="!full"
|
||||||
|
:custom-emojis="notification.note.renote.emojis"
|
||||||
|
/>
|
||||||
<i class="ph-quotes ph-fill ph-lg"></i>
|
<i class="ph-quotes ph-fill ph-lg"></i>
|
||||||
</MkA>
|
</MkA>
|
||||||
<MkA v-if="notification.type === 'reply'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
<MkA
|
||||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
|
v-if="notification.type === 'reply'"
|
||||||
|
class="text"
|
||||||
|
:to="notePage(notification.note)"
|
||||||
|
:title="getNoteSummary(notification.note)"
|
||||||
|
>
|
||||||
|
<Mfm
|
||||||
|
:text="getNoteSummary(notification.note)"
|
||||||
|
:plain="true"
|
||||||
|
:nowrap="!full"
|
||||||
|
:custom-emojis="notification.note.emojis"
|
||||||
|
/>
|
||||||
</MkA>
|
</MkA>
|
||||||
<MkA v-if="notification.type === 'mention'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
<MkA
|
||||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
|
v-if="notification.type === 'mention'"
|
||||||
|
class="text"
|
||||||
|
:to="notePage(notification.note)"
|
||||||
|
:title="getNoteSummary(notification.note)"
|
||||||
|
>
|
||||||
|
<Mfm
|
||||||
|
:text="getNoteSummary(notification.note)"
|
||||||
|
:plain="true"
|
||||||
|
:nowrap="!full"
|
||||||
|
:custom-emojis="notification.note.emojis"
|
||||||
|
/>
|
||||||
</MkA>
|
</MkA>
|
||||||
<MkA v-if="notification.type === 'quote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
<MkA
|
||||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
|
v-if="notification.type === 'quote'"
|
||||||
|
class="text"
|
||||||
|
:to="notePage(notification.note)"
|
||||||
|
:title="getNoteSummary(notification.note)"
|
||||||
|
>
|
||||||
|
<Mfm
|
||||||
|
:text="getNoteSummary(notification.note)"
|
||||||
|
:plain="true"
|
||||||
|
:nowrap="!full"
|
||||||
|
:custom-emojis="notification.note.emojis"
|
||||||
|
/>
|
||||||
</MkA>
|
</MkA>
|
||||||
<MkA v-if="notification.type === 'pollVote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
<MkA
|
||||||
|
v-if="notification.type === 'pollVote'"
|
||||||
|
class="text"
|
||||||
|
:to="notePage(notification.note)"
|
||||||
|
:title="getNoteSummary(notification.note)"
|
||||||
|
>
|
||||||
<i class="ph-quotes ph-fill ph-lg"></i>
|
<i class="ph-quotes ph-fill ph-lg"></i>
|
||||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
|
<Mfm
|
||||||
|
:text="getNoteSummary(notification.note)"
|
||||||
|
:plain="true"
|
||||||
|
:nowrap="!full"
|
||||||
|
:custom-emojis="notification.note.emojis"
|
||||||
|
/>
|
||||||
<i class="ph-quotes ph-fill ph-lg"></i>
|
<i class="ph-quotes ph-fill ph-lg"></i>
|
||||||
</MkA>
|
</MkA>
|
||||||
<MkA v-if="notification.type === 'pollEnded'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
<MkA
|
||||||
|
v-if="notification.type === 'pollEnded'"
|
||||||
|
class="text"
|
||||||
|
:to="notePage(notification.note)"
|
||||||
|
:title="getNoteSummary(notification.note)"
|
||||||
|
>
|
||||||
<i class="ph-quotes ph-fill ph-lg"></i>
|
<i class="ph-quotes ph-fill ph-lg"></i>
|
||||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
|
<Mfm
|
||||||
|
:text="getNoteSummary(notification.note)"
|
||||||
|
:plain="true"
|
||||||
|
:nowrap="!full"
|
||||||
|
:custom-emojis="notification.note.emojis"
|
||||||
|
/>
|
||||||
<i class="ph-quotes ph-fill ph-lg"></i>
|
<i class="ph-quotes ph-fill ph-lg"></i>
|
||||||
</MkA>
|
</MkA>
|
||||||
<span v-if="notification.type === 'follow'" class="text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div></span>
|
<span
|
||||||
<span v-if="notification.type === 'followRequestAccepted'" class="text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span>
|
v-if="notification.type === 'follow'"
|
||||||
<span v-if="notification.type === 'receiveFollowRequest'" class="text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}<div v-if="full && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ i18n.ts.reject }}</button></div></span>
|
class="text"
|
||||||
<span v-if="notification.type === 'groupInvited'" class="text" style="opacity: 0.6;">{{ i18n.ts.groupInvited }}: <b>{{ notification.invitation.group.name }}</b><div v-if="full && !groupInviteDone"><button class="_textButton" @click="acceptGroupInvitation()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectGroupInvitation()">{{ i18n.ts.reject }}</button></div></span>
|
style="opacity: 0.6"
|
||||||
|
>{{ i18n.ts.youGotNewFollower }}
|
||||||
|
<div v-if="full">
|
||||||
|
<MkFollowButton
|
||||||
|
:user="notification.user"
|
||||||
|
:full="true"
|
||||||
|
/></div
|
||||||
|
></span>
|
||||||
|
<span
|
||||||
|
v-if="notification.type === 'followRequestAccepted'"
|
||||||
|
class="text"
|
||||||
|
style="opacity: 0.6"
|
||||||
|
>{{ i18n.ts.followRequestAccepted }}</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="notification.type === 'receiveFollowRequest'"
|
||||||
|
class="text"
|
||||||
|
style="opacity: 0.6"
|
||||||
|
>{{ i18n.ts.receiveFollowRequest }}
|
||||||
|
<div v-if="full && !followRequestDone">
|
||||||
|
<button class="_textButton" @click="acceptFollowRequest()">
|
||||||
|
{{ i18n.ts.accept }}
|
||||||
|
</button>
|
||||||
|
|
|
||||||
|
<button class="_textButton" @click="rejectFollowRequest()">
|
||||||
|
{{ i18n.ts.reject }}
|
||||||
|
</button>
|
||||||
|
</div></span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="notification.type === 'groupInvited'"
|
||||||
|
class="text"
|
||||||
|
style="opacity: 0.6"
|
||||||
|
>{{ i18n.ts.groupInvited }}:
|
||||||
|
<b>{{ notification.invitation.group.name }}</b>
|
||||||
|
<div v-if="full && !groupInviteDone">
|
||||||
|
<button
|
||||||
|
class="_textButton"
|
||||||
|
@click="acceptGroupInvitation()"
|
||||||
|
>
|
||||||
|
{{ i18n.ts.accept }}
|
||||||
|
</button>
|
||||||
|
|
|
||||||
|
<button
|
||||||
|
class="_textButton"
|
||||||
|
@click="rejectGroupInvitation()"
|
||||||
|
>
|
||||||
|
{{ i18n.ts.reject }}
|
||||||
|
</button>
|
||||||
|
</div></span
|
||||||
|
>
|
||||||
<span v-if="notification.type === 'app'" class="text">
|
<span v-if="notification.type === 'app'" class="text">
|
||||||
<Mfm :text="notification.body" :nowrap="!full" />
|
<Mfm :text="notification.body" :nowrap="!full" />
|
||||||
</span>
|
</span>
|
||||||
|
@ -73,27 +260,30 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
import { ref, onMounted, onUnmounted, watch } from "vue";
|
||||||
import * as misskey from 'calckey-js';
|
import * as misskey from "calckey-js";
|
||||||
import XReactionIcon from '@/components/MkReactionIcon.vue';
|
import XReactionIcon from "@/components/MkReactionIcon.vue";
|
||||||
import MkFollowButton from '@/components/MkFollowButton.vue';
|
import MkFollowButton from "@/components/MkFollowButton.vue";
|
||||||
import XReactionTooltip from '@/components/MkReactionTooltip.vue';
|
import XReactionTooltip from "@/components/MkReactionTooltip.vue";
|
||||||
import { getNoteSummary } from '@/scripts/get-note-summary';
|
import { getNoteSummary } from "@/scripts/get-note-summary";
|
||||||
import { notePage } from '@/filters/note';
|
import { notePage } from "@/filters/note";
|
||||||
import { userPage } from '@/filters/user';
|
import { userPage } from "@/filters/user";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
import * as os from '@/os';
|
import * as os from "@/os";
|
||||||
import { stream } from '@/stream';
|
import { stream } from "@/stream";
|
||||||
import { useTooltip } from '@/scripts/use-tooltip';
|
import { useTooltip } from "@/scripts/use-tooltip";
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
notification: misskey.entities.Notification;
|
notification: misskey.entities.Notification;
|
||||||
withTime?: boolean;
|
withTime?: boolean;
|
||||||
full?: boolean;
|
full?: boolean;
|
||||||
}>(), {
|
}>(),
|
||||||
|
{
|
||||||
withTime: false,
|
withTime: false,
|
||||||
full: false,
|
full: false,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const elRef = ref<HTMLElement>(null);
|
const elRef = ref<HTMLElement>(null);
|
||||||
const reactionRef = ref(null);
|
const reactionRef = ref(null);
|
||||||
|
@ -104,8 +294,8 @@ let connection;
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!props.notification.isRead) {
|
if (!props.notification.isRead) {
|
||||||
readObserver = new IntersectionObserver((entries, observer) => {
|
readObserver = new IntersectionObserver((entries, observer) => {
|
||||||
if (!entries.some(entry => entry.isIntersecting)) return;
|
if (!entries.some((entry) => entry.isIntersecting)) return;
|
||||||
stream.send('readNotification', {
|
stream.send("readNotification", {
|
||||||
id: props.notification.id,
|
id: props.notification.id,
|
||||||
});
|
});
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
|
@ -113,8 +303,8 @@ onMounted(() => {
|
||||||
|
|
||||||
readObserver.observe(elRef.value);
|
readObserver.observe(elRef.value);
|
||||||
|
|
||||||
connection = stream.useChannel('main');
|
connection = stream.useChannel("main");
|
||||||
connection.on('readAllNotifications', () => readObserver.disconnect());
|
connection.on("readAllNotifications", () => readObserver.disconnect());
|
||||||
|
|
||||||
watch(props.notification.isRead, () => {
|
watch(props.notification.isRead, () => {
|
||||||
readObserver.disconnect();
|
readObserver.disconnect();
|
||||||
|
@ -132,31 +322,42 @@ const groupInviteDone = ref(false);
|
||||||
|
|
||||||
const acceptFollowRequest = () => {
|
const acceptFollowRequest = () => {
|
||||||
followRequestDone.value = true;
|
followRequestDone.value = true;
|
||||||
os.api('following/requests/accept', { userId: props.notification.user.id });
|
os.api("following/requests/accept", { userId: props.notification.user.id });
|
||||||
};
|
};
|
||||||
|
|
||||||
const rejectFollowRequest = () => {
|
const rejectFollowRequest = () => {
|
||||||
followRequestDone.value = true;
|
followRequestDone.value = true;
|
||||||
os.api('following/requests/reject', { userId: props.notification.user.id });
|
os.api("following/requests/reject", { userId: props.notification.user.id });
|
||||||
};
|
};
|
||||||
|
|
||||||
const acceptGroupInvitation = () => {
|
const acceptGroupInvitation = () => {
|
||||||
groupInviteDone.value = true;
|
groupInviteDone.value = true;
|
||||||
os.apiWithDialog('users/groups/invitations/accept', { invitationId: props.notification.invitation.id });
|
os.apiWithDialog("users/groups/invitations/accept", {
|
||||||
|
invitationId: props.notification.invitation.id,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const rejectGroupInvitation = () => {
|
const rejectGroupInvitation = () => {
|
||||||
groupInviteDone.value = true;
|
groupInviteDone.value = true;
|
||||||
os.api('users/groups/invitations/reject', { invitationId: props.notification.invitation.id });
|
os.api("users/groups/invitations/reject", {
|
||||||
|
invitationId: props.notification.invitation.id,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useTooltip(reactionRef, (showing) => {
|
useTooltip(reactionRef, (showing) => {
|
||||||
os.popup(XReactionTooltip, {
|
os.popup(
|
||||||
|
XReactionTooltip,
|
||||||
|
{
|
||||||
showing,
|
showing,
|
||||||
reaction: props.notification.reaction ? props.notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : props.notification.reaction,
|
reaction: props.notification.reaction
|
||||||
|
? props.notification.reaction.replace(/^:(\w+):$/, ":$1@.:")
|
||||||
|
: props.notification.reaction,
|
||||||
emojis: props.notification.note.emojis,
|
emojis: props.notification.note.emojis,
|
||||||
targetElement: reactionRef.value.$el,
|
targetElement: reactionRef.value.$el,
|
||||||
}, {}, 'closed');
|
},
|
||||||
|
{},
|
||||||
|
"closed"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -219,7 +420,10 @@ useTooltip(reactionRef, (showing) => {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.follow, &.followRequestAccepted, &.receiveFollowRequest, &.groupInvited {
|
&.follow,
|
||||||
|
&.followRequestAccepted,
|
||||||
|
&.receiveFollowRequest,
|
||||||
|
&.groupInvited {
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
background: #31748f;
|
background: #31748f;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
|
@ -14,47 +14,64 @@
|
||||||
<div v-if="showGlobalToggle" class="_section">
|
<div v-if="showGlobalToggle" class="_section">
|
||||||
<MkSwitch v-model="useGlobalSetting">
|
<MkSwitch v-model="useGlobalSetting">
|
||||||
{{ i18n.ts.useGlobalSetting }}
|
{{ i18n.ts.useGlobalSetting }}
|
||||||
<template #caption>{{ i18n.ts.useGlobalSettingDesc }}</template>
|
<template #caption>{{
|
||||||
|
i18n.ts.useGlobalSettingDesc
|
||||||
|
}}</template>
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!useGlobalSetting" class="_section">
|
<div v-if="!useGlobalSetting" class="_section">
|
||||||
<MkInfo>{{ i18n.ts.notificationSettingDesc }}</MkInfo>
|
<MkInfo>{{ i18n.ts.notificationSettingDesc }}</MkInfo>
|
||||||
<MkButton inline @click="disableAll">{{ i18n.ts.disableAll }}</MkButton>
|
<MkButton inline @click="disableAll">{{
|
||||||
<MkButton inline @click="enableAll">{{ i18n.ts.enableAll }}</MkButton>
|
i18n.ts.disableAll
|
||||||
<MkSwitch v-for="ntype in notificationTypes" :key="ntype" v-model="typesMap[ntype]">{{ i18n.t(`_notification._types.${ntype}`) }}</MkSwitch>
|
}}</MkButton>
|
||||||
|
<MkButton inline @click="enableAll">{{
|
||||||
|
i18n.ts.enableAll
|
||||||
|
}}</MkButton>
|
||||||
|
<MkSwitch
|
||||||
|
v-for="ntype in notificationTypes"
|
||||||
|
:key="ntype"
|
||||||
|
v-model="typesMap[ntype]"
|
||||||
|
>{{ i18n.t(`_notification._types.${ntype}`) }}</MkSwitch
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</XModalWindow>
|
</XModalWindow>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import {} from "vue";
|
||||||
import { notificationTypes } from 'calckey-js';
|
import { notificationTypes } from "calckey-js";
|
||||||
import MkSwitch from './form/switch.vue';
|
import MkSwitch from "./form/switch.vue";
|
||||||
import MkInfo from './MkInfo.vue';
|
import MkInfo from "./MkInfo.vue";
|
||||||
import MkButton from './MkButton.vue';
|
import MkButton from "./MkButton.vue";
|
||||||
import XModalWindow from '@/components/MkModalWindow.vue';
|
import XModalWindow from "@/components/MkModalWindow.vue";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'done', v: { includingTypes: string[] | null }): void,
|
(ev: "done", v: { includingTypes: string[] | null }): void;
|
||||||
(ev: 'closed'): void,
|
(ev: "closed"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(
|
||||||
includingTypes?: typeof notificationTypes[number][] | null;
|
defineProps<{
|
||||||
|
includingTypes?: (typeof notificationTypes)[number][] | null;
|
||||||
showGlobalToggle?: boolean;
|
showGlobalToggle?: boolean;
|
||||||
}>(), {
|
}>(),
|
||||||
|
{
|
||||||
includingTypes: () => [],
|
includingTypes: () => [],
|
||||||
showGlobalToggle: true,
|
showGlobalToggle: true,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
let includingTypes = $computed(() => props.includingTypes || []);
|
let includingTypes = $computed(() => props.includingTypes || []);
|
||||||
|
|
||||||
const dialog = $ref<InstanceType<typeof XModalWindow>>();
|
const dialog = $ref<InstanceType<typeof XModalWindow>>();
|
||||||
|
|
||||||
let typesMap = $ref<Record<typeof notificationTypes[number], boolean>>({});
|
let typesMap = $ref<Record<(typeof notificationTypes)[number], boolean>>({});
|
||||||
let useGlobalSetting = $ref((includingTypes === null || includingTypes.length === 0) && props.showGlobalToggle);
|
let useGlobalSetting = $ref(
|
||||||
|
(includingTypes === null || includingTypes.length === 0) &&
|
||||||
|
props.showGlobalToggle
|
||||||
|
);
|
||||||
|
|
||||||
for (const ntype of notificationTypes) {
|
for (const ntype of notificationTypes) {
|
||||||
typesMap[ntype] = includingTypes.includes(ntype);
|
typesMap[ntype] = includingTypes.includes(ntype);
|
||||||
|
@ -62,11 +79,12 @@ for (const ntype of notificationTypes) {
|
||||||
|
|
||||||
function ok() {
|
function ok() {
|
||||||
if (useGlobalSetting) {
|
if (useGlobalSetting) {
|
||||||
emit('done', { includingTypes: null });
|
emit("done", { includingTypes: null });
|
||||||
} else {
|
} else {
|
||||||
emit('done', {
|
emit("done", {
|
||||||
includingTypes: (Object.keys(typesMap) as typeof notificationTypes[number][])
|
includingTypes: (
|
||||||
.filter(type => typesMap[type]),
|
Object.keys(typesMap) as (typeof notificationTypes)[number][]
|
||||||
|
).filter((type) => typesMap[type]),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,13 +93,13 @@ function ok() {
|
||||||
|
|
||||||
function disableAll() {
|
function disableAll() {
|
||||||
for (const type in typesMap) {
|
for (const type in typesMap) {
|
||||||
typesMap[type as typeof notificationTypes[number]] = false;
|
typesMap[type as (typeof notificationTypes)[number]] = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function enableAll() {
|
function enableAll() {
|
||||||
for (const type in typesMap) {
|
for (const type in typesMap) {
|
||||||
typesMap[type as typeof notificationTypes[number]] = true;
|
typesMap[type as (typeof notificationTypes)[number]] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,25 +1,33 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="mk-notification-toast" :style="{ zIndex }">
|
<div class="mk-notification-toast" :style="{ zIndex }">
|
||||||
<transition :name="$store.state.animation ? 'notification-toast' : ''" appear @after-leave="$emit('closed')">
|
<transition
|
||||||
<XNotification v-if="showing" :notification="notification" class="notification _acrylic"/>
|
:name="$store.state.animation ? 'notification-toast' : ''"
|
||||||
|
appear
|
||||||
|
@after-leave="$emit('closed')"
|
||||||
|
>
|
||||||
|
<XNotification
|
||||||
|
v-if="showing"
|
||||||
|
:notification="notification"
|
||||||
|
class="notification _acrylic"
|
||||||
|
/>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted } from 'vue';
|
import { onMounted } from "vue";
|
||||||
import XNotification from '@/components/MkNotification.vue';
|
import XNotification from "@/components/MkNotification.vue";
|
||||||
import * as os from '@/os';
|
import * as os from "@/os";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
notification: any; // TODO
|
notification: any; // TODO
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'closed'): void;
|
(ev: "closed"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const zIndex = os.claimZIndex('high');
|
const zIndex = os.claimZIndex("high");
|
||||||
let showing = $ref(true);
|
let showing = $ref(true);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
@ -30,10 +38,12 @@ onMounted(() => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.notification-toast-enter-active, .notification-toast-leave-active {
|
.notification-toast-enter-active,
|
||||||
|
.notification-toast-leave-active {
|
||||||
transition: opacity 0.3s, transform 0.3s !important;
|
transition: opacity 0.3s, transform 0.3s !important;
|
||||||
}
|
}
|
||||||
.notification-toast-enter-from, .notification-toast-leave-to {
|
.notification-toast-enter-from,
|
||||||
|
.notification-toast-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(-250px);
|
transform: translateX(-250px);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,53 +2,88 @@
|
||||||
<MkPagination ref="pagingComponent" :pagination="pagination">
|
<MkPagination ref="pagingComponent" :pagination="pagination">
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<div class="_fullinfo">
|
<div class="_fullinfo">
|
||||||
<img src="/static-assets/badges/info.png" class="_ghost" alt="Info"/>
|
<img
|
||||||
|
src="/static-assets/badges/info.png"
|
||||||
|
class="_ghost"
|
||||||
|
alt="Info"
|
||||||
|
/>
|
||||||
<div>{{ i18n.ts.noNotifications }}</div>
|
<div>{{ i18n.ts.noNotifications }}</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #default="{ items: notifications }">
|
<template #default="{ items: notifications }">
|
||||||
<XList v-slot="{ item: notification }" class="elsfgstc" :items="notifications" :no-gap="true">
|
<XList
|
||||||
<XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/>
|
v-slot="{ item: notification }"
|
||||||
<XNotification v-else :key="notification.id" :notification="notification" :with-time="true" :full="true" class="_panel notification"/>
|
class="elsfgstc"
|
||||||
|
:items="notifications"
|
||||||
|
:no-gap="true"
|
||||||
|
>
|
||||||
|
<XNote
|
||||||
|
v-if="
|
||||||
|
['reply', 'quote', 'mention'].includes(
|
||||||
|
notification.type
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:key="notification.id"
|
||||||
|
:note="notification.note"
|
||||||
|
/>
|
||||||
|
<XNotification
|
||||||
|
v-else
|
||||||
|
:key="notification.id"
|
||||||
|
:notification="notification"
|
||||||
|
:with-time="true"
|
||||||
|
:full="true"
|
||||||
|
class="_panel notification"
|
||||||
|
/>
|
||||||
</XList>
|
</XList>
|
||||||
</template>
|
</template>
|
||||||
</MkPagination>
|
</MkPagination>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineComponent, markRaw, onUnmounted, onMounted, computed, ref } from 'vue';
|
import {
|
||||||
import { notificationTypes } from 'calckey-js';
|
defineComponent,
|
||||||
import MkPagination, { Paging } from '@/components/MkPagination.vue';
|
markRaw,
|
||||||
import XNotification from '@/components/MkNotification.vue';
|
onUnmounted,
|
||||||
import XList from '@/components/MkDateSeparatedList.vue';
|
onMounted,
|
||||||
import XNote from '@/components/MkNote.vue';
|
computed,
|
||||||
import * as os from '@/os';
|
ref,
|
||||||
import { stream } from '@/stream';
|
} from "vue";
|
||||||
import { $i } from '@/account';
|
import { notificationTypes } from "calckey-js";
|
||||||
import { i18n } from '@/i18n';
|
import MkPagination, { Paging } from "@/components/MkPagination.vue";
|
||||||
|
import XNotification from "@/components/MkNotification.vue";
|
||||||
|
import XList from "@/components/MkDateSeparatedList.vue";
|
||||||
|
import XNote from "@/components/MkNote.vue";
|
||||||
|
import * as os from "@/os";
|
||||||
|
import { stream } from "@/stream";
|
||||||
|
import { $i } from "@/account";
|
||||||
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
includeTypes?: typeof notificationTypes[number][];
|
includeTypes?: (typeof notificationTypes)[number][];
|
||||||
unreadOnly?: boolean;
|
unreadOnly?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const pagingComponent = ref<InstanceType<typeof MkPagination>>();
|
const pagingComponent = ref<InstanceType<typeof MkPagination>>();
|
||||||
|
|
||||||
const pagination: Paging = {
|
const pagination: Paging = {
|
||||||
endpoint: 'i/notifications' as const,
|
endpoint: "i/notifications" as const,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
params: computed(() => ({
|
params: computed(() => ({
|
||||||
includeTypes: props.includeTypes ?? undefined,
|
includeTypes: props.includeTypes ?? undefined,
|
||||||
excludeTypes: props.includeTypes ? undefined : $i.mutingNotificationTypes,
|
excludeTypes: props.includeTypes
|
||||||
|
? undefined
|
||||||
|
: $i.mutingNotificationTypes,
|
||||||
unreadOnly: props.unreadOnly,
|
unreadOnly: props.unreadOnly,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
const onNotification = (notification) => {
|
const onNotification = (notification) => {
|
||||||
const isMuted = props.includeTypes ? !props.includeTypes.includes(notification.type) : $i.mutingNotificationTypes.includes(notification.type);
|
const isMuted = props.includeTypes
|
||||||
if (isMuted || document.visibilityState === 'visible') {
|
? !props.includeTypes.includes(notification.type)
|
||||||
stream.send('readNotification', {
|
: $i.mutingNotificationTypes.includes(notification.type);
|
||||||
|
if (isMuted || document.visibilityState === "visible") {
|
||||||
|
stream.send("readNotification", {
|
||||||
id: notification.id,
|
id: notification.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -56,7 +91,7 @@ const onNotification = (notification) => {
|
||||||
if (!isMuted) {
|
if (!isMuted) {
|
||||||
pagingComponent.value.prepend({
|
pagingComponent.value.prepend({
|
||||||
...notification,
|
...notification,
|
||||||
isRead: document.visibilityState === 'visible',
|
isRead: document.visibilityState === "visible",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -64,9 +99,9 @@ const onNotification = (notification) => {
|
||||||
let connection;
|
let connection;
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
connection = stream.useChannel('main');
|
connection = stream.useChannel("main");
|
||||||
connection.on('notification', onNotification);
|
connection.on("notification", onNotification);
|
||||||
connection.on('readAllNotifications', () => {
|
connection.on("readAllNotifications", () => {
|
||||||
if (pagingComponent.value) {
|
if (pagingComponent.value) {
|
||||||
for (const item of pagingComponent.value.queue) {
|
for (const item of pagingComponent.value.queue) {
|
||||||
item.isRead = true;
|
item.isRead = true;
|
||||||
|
@ -76,15 +111,23 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
connection.on('readNotifications', notificationIds => {
|
connection.on("readNotifications", (notificationIds) => {
|
||||||
if (pagingComponent.value) {
|
if (pagingComponent.value) {
|
||||||
for (let i = 0; i < pagingComponent.value.queue.length; i++) {
|
for (let i = 0; i < pagingComponent.value.queue.length; i++) {
|
||||||
if (notificationIds.includes(pagingComponent.value.queue[i].id)) {
|
if (
|
||||||
|
notificationIds.includes(pagingComponent.value.queue[i].id)
|
||||||
|
) {
|
||||||
pagingComponent.value.queue[i].isRead = true;
|
pagingComponent.value.queue[i].isRead = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (let i = 0; i < (pagingComponent.value.items || []).length; i++) {
|
for (
|
||||||
if (notificationIds.includes(pagingComponent.value.items[i].id)) {
|
let i = 0;
|
||||||
|
i < (pagingComponent.value.items || []).length;
|
||||||
|
i++
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
notificationIds.includes(pagingComponent.value.items[i].id)
|
||||||
|
) {
|
||||||
pagingComponent.value.items[i].isRead = true;
|
pagingComponent.value.items[i].isRead = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, reactive, watch } from 'vue';
|
import { ref, reactive, watch } from "vue";
|
||||||
import gsap from 'gsap';
|
import gsap from "gsap";
|
||||||
import number from '@/filters/number';
|
import number from "@/filters/number";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
value: number;
|
value: number;
|
||||||
|
@ -15,9 +15,13 @@ const tweened = reactive({
|
||||||
number: 0,
|
number: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(() => props.value, (n) => {
|
watch(
|
||||||
|
() => props.value,
|
||||||
|
(n) => {
|
||||||
gsap.to(tweened, { duration: 0.6, number: Number(n) || 0 });
|
gsap.to(tweened, { duration: 0.6, number: Number(n) || 0 });
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
immediate: true,
|
immediate: true,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<span class="ceaaebcd" :class="{ isPlus, isMinus, isZero }">
|
<span class="ceaaebcd" :class="{ isPlus, isMinus, isZero }">
|
||||||
<slot name="before"></slot>{{ isPlus ? '+' : '' }}{{ number(value) }}<slot name="after"></slot>
|
<slot name="before"></slot>{{ isPlus ? "+" : "" }}{{ number(value)
|
||||||
|
}}<slot name="after"></slot>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent } from 'vue';
|
import { computed, defineComponent } from "vue";
|
||||||
import number from '@/filters/number';
|
import number from "@/filters/number";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
|
|
|
@ -1,25 +1,52 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="igpposuu _monospace">
|
<div class="igpposuu _monospace">
|
||||||
<div v-if="value === null" class="null">null</div>
|
<div v-if="value === null" class="null">null</div>
|
||||||
<div v-else-if="typeof value === 'boolean'" class="boolean" :class="{ true: value, false: !value }">{{ value ? 'true' : 'false' }}</div>
|
<div
|
||||||
<div v-else-if="typeof value === 'string'" class="string">"{{ value }}"</div>
|
v-else-if="typeof value === 'boolean'"
|
||||||
<div v-else-if="typeof value === 'number'" class="number">{{ number(value) }}</div>
|
class="boolean"
|
||||||
<div v-else-if="isArray(value) && isEmpty(value)" class="array empty">[]</div>
|
:class="{ true: value, false: !value }"
|
||||||
|
>
|
||||||
|
{{ value ? "true" : "false" }}
|
||||||
|
</div>
|
||||||
|
<div v-else-if="typeof value === 'string'" class="string">
|
||||||
|
"{{ value }}"
|
||||||
|
</div>
|
||||||
|
<div v-else-if="typeof value === 'number'" class="number">
|
||||||
|
{{ number(value) }}
|
||||||
|
</div>
|
||||||
|
<div v-else-if="isArray(value) && isEmpty(value)" class="array empty">
|
||||||
|
[]
|
||||||
|
</div>
|
||||||
<div v-else-if="isArray(value)" class="array">
|
<div v-else-if="isArray(value)" class="array">
|
||||||
<div v-for="i in value.length" class="element">
|
<div v-for="i in value.length" class="element">
|
||||||
{{ i }}: <XValue :value="value[i - 1]" collapsed />
|
{{ i }}: <XValue :value="value[i - 1]" collapsed />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="isObject(value) && isEmpty(value)" class="object empty">{}</div>
|
<div v-else-if="isObject(value) && isEmpty(value)" class="object empty">
|
||||||
|
{}
|
||||||
|
</div>
|
||||||
<div v-else-if="isObject(value)" class="object">
|
<div v-else-if="isObject(value)" class="object">
|
||||||
<div v-for="k in Object.keys(value)" class="kv">
|
<div v-for="k in Object.keys(value)" class="kv">
|
||||||
<button class="toggle _button" :class="{ visible: collapsable(value[k]) }" @click="collapsed[k] = !collapsed[k]">{{ collapsed[k] ? '+' : '-' }}</button>
|
<button
|
||||||
|
class="toggle _button"
|
||||||
|
:class="{ visible: collapsable(value[k]) }"
|
||||||
|
@click="collapsed[k] = !collapsed[k]"
|
||||||
|
>
|
||||||
|
{{ collapsed[k] ? "+" : "-" }}
|
||||||
|
</button>
|
||||||
<div class="k">{{ k }}:</div>
|
<div class="k">{{ k }}:</div>
|
||||||
<div v-if="collapsed[k]" class="v">
|
<div v-if="collapsed[k]" class="v">
|
||||||
<button class="_button" @click="collapsed[k] = !collapsed[k]">
|
<button
|
||||||
<template v-if="typeof value[k] === 'string'">"..."</template>
|
class="_button"
|
||||||
|
@click="collapsed[k] = !collapsed[k]"
|
||||||
|
>
|
||||||
|
<template v-if="typeof value[k] === 'string'"
|
||||||
|
>"..."</template
|
||||||
|
>
|
||||||
<template v-else-if="isArray(value[k])">[...]</template>
|
<template v-else-if="isArray(value[k])">[...]</template>
|
||||||
<template v-else-if="isObject(value[k])">{...}</template>
|
<template v-else-if="isObject(value[k])"
|
||||||
|
>{...}</template
|
||||||
|
>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="v"><XValue :value="value[k]" /></div>
|
<div v-else class="v"><XValue :value="value[k]" /></div>
|
||||||
|
@ -29,11 +56,11 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent, reactive, ref } from 'vue';
|
import { computed, defineComponent, reactive, ref } from "vue";
|
||||||
import number from '@/filters/number';
|
import number from "@/filters/number";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'XValue',
|
name: "XValue",
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
value: {
|
value: {
|
||||||
|
@ -51,7 +78,7 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
function isObject(v): boolean {
|
function isObject(v): boolean {
|
||||||
return typeof v === 'object' && !Array.isArray(v) && v !== null;
|
return typeof v === "object" && !Array.isArray(v) && v !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isArray(v): boolean {
|
function isArray(v): boolean {
|
||||||
|
@ -59,7 +86,10 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
function isEmpty(v): boolean {
|
function isEmpty(v): boolean {
|
||||||
return (isArray(v) && v.length === 0) || (isObject(v) && Object.keys(v).length === 0);
|
return (
|
||||||
|
(isArray(v) && v.length === 0) ||
|
||||||
|
(isObject(v) && Object.keys(v).length === 0)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function collapsable(v): boolean {
|
function collapsable(v): boolean {
|
||||||
|
|
|
@ -5,8 +5,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import {} from "vue";
|
||||||
import XValue from './MkObjectView.value.vue';
|
import XValue from "./MkObjectView.value.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
value: Record<string, unknown>;
|
value: Record<string, unknown>;
|
||||||
|
@ -15,6 +15,5 @@ const props = defineProps<{
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.zhyxdalp {
|
.zhyxdalp {
|
||||||
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,13 +1,32 @@
|
||||||
<template>
|
<template>
|
||||||
<MkA :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj _block" tabindex="-1" :behavior="`${ui === 'deck' ? 'window' : null}`">
|
<MkA
|
||||||
<div v-if="page.eyeCatchingImage" class="thumbnail" :style="`background-image: url('${page.eyeCatchingImage.thumbnailUrl}')`"></div>
|
:to="`/@${page.user.username}/pages/${page.name}`"
|
||||||
|
class="vhpxefrj _block"
|
||||||
|
tabindex="-1"
|
||||||
|
:behavior="`${ui === 'deck' ? 'window' : null}`"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="page.eyeCatchingImage"
|
||||||
|
class="thumbnail"
|
||||||
|
:style="`background-image: url('${page.eyeCatchingImage.thumbnailUrl}')`"
|
||||||
|
></div>
|
||||||
<article>
|
<article>
|
||||||
<header>
|
<header>
|
||||||
<h1 :title="page.title">{{ page.title }}</h1>
|
<h1 :title="page.title">{{ page.title }}</h1>
|
||||||
</header>
|
</header>
|
||||||
<p v-if="page.summary" :title="page.summary">{{ page.summary.length > 85 ? page.summary.slice(0, 85) + '…' : page.summary }}</p>
|
<p v-if="page.summary" :title="page.summary">
|
||||||
|
{{
|
||||||
|
page.summary.length > 85
|
||||||
|
? page.summary.slice(0, 85) + "…"
|
||||||
|
: page.summary
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
<footer>
|
<footer>
|
||||||
<img class="icon" :src="page.user.avatarUrl" aria-label="none"/>
|
<img
|
||||||
|
class="icon"
|
||||||
|
:src="page.user.avatarUrl"
|
||||||
|
aria-label="none"
|
||||||
|
/>
|
||||||
<p>{{ userName(page.user) }}</p>
|
<p>{{ userName(page.user) }}</p>
|
||||||
</footer>
|
</footer>
|
||||||
</article>
|
</article>
|
||||||
|
@ -15,8 +34,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { userName } from '@/filters/user';
|
import { userName } from "@/filters/user";
|
||||||
import { ui } from '@/config';
|
import { ui } from "@/config";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
page: any;
|
page: any;
|
||||||
|
@ -149,5 +168,4 @@ defineProps<{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -12,7 +12,12 @@
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<template v-if="pageMetadata?.value">
|
<template v-if="pageMetadata?.value">
|
||||||
<i v-if="pageMetadata.value.icon" class="icon" :class="pageMetadata.value.icon" style="margin-right: 0.5em;"></i>
|
<i
|
||||||
|
v-if="pageMetadata.value.icon"
|
||||||
|
class="icon"
|
||||||
|
:class="pageMetadata.value.icon"
|
||||||
|
style="margin-right: 0.5em"
|
||||||
|
></i>
|
||||||
<span>{{ pageMetadata.value.title }}</span>
|
<span>{{ pageMetadata.value.title }}</span>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
@ -24,40 +29,46 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ComputedRef, inject, provide } from 'vue';
|
import { ComputedRef, inject, provide } from "vue";
|
||||||
import RouterView from '@/components/global/RouterView.vue';
|
import RouterView from "@/components/global/RouterView.vue";
|
||||||
import XWindow from '@/components/MkWindow.vue';
|
import XWindow from "@/components/MkWindow.vue";
|
||||||
import { popout as _popout } from '@/scripts/popout';
|
import { popout as _popout } from "@/scripts/popout";
|
||||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
import copyToClipboard from "@/scripts/copy-to-clipboard";
|
||||||
import { url } from '@/config';
|
import { url } from "@/config";
|
||||||
import * as os from '@/os';
|
import * as os from "@/os";
|
||||||
import { mainRouter, routes } from '@/router';
|
import { mainRouter, routes } from "@/router";
|
||||||
import { Router } from '@/nirax';
|
import { Router } from "@/nirax";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
import { PageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
|
import {
|
||||||
|
PageMetadata,
|
||||||
|
provideMetadataReceiver,
|
||||||
|
setPageMetadata,
|
||||||
|
} from "@/scripts/page-metadata";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
initialPath: string;
|
initialPath: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
(ev: 'closed'): void;
|
(ev: "closed"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const router = new Router(routes, props.initialPath);
|
const router = new Router(routes, props.initialPath);
|
||||||
|
|
||||||
let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
|
let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
|
||||||
let windowEl = $ref<InstanceType<typeof XWindow>>();
|
let windowEl = $ref<InstanceType<typeof XWindow>>();
|
||||||
const history = $ref<{ path: string; key: any; }[]>([{
|
const history = $ref<{ path: string; key: any }[]>([
|
||||||
|
{
|
||||||
path: router.getCurrentPath(),
|
path: router.getCurrentPath(),
|
||||||
key: router.getCurrentKey(),
|
key: router.getCurrentKey(),
|
||||||
}]);
|
},
|
||||||
|
]);
|
||||||
const buttonsLeft = $computed(() => {
|
const buttonsLeft = $computed(() => {
|
||||||
const buttons = [];
|
const buttons = [];
|
||||||
|
|
||||||
if (history.length > 1) {
|
if (history.length > 1) {
|
||||||
buttons.push({
|
buttons.push({
|
||||||
icon: 'ph-caret-left ph-bold ph-lg',
|
icon: "ph-caret-left ph-bold ph-lg",
|
||||||
onClick: back,
|
onClick: back,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -65,48 +76,55 @@ const buttonsLeft = $computed(() => {
|
||||||
return buttons;
|
return buttons;
|
||||||
});
|
});
|
||||||
const buttonsRight = $computed(() => {
|
const buttonsRight = $computed(() => {
|
||||||
const buttons = [{
|
const buttons = [
|
||||||
icon: 'ph-arrows-out-simple ph-bold ph-lg',
|
{
|
||||||
|
icon: "ph-arrows-out-simple ph-bold ph-lg",
|
||||||
title: i18n.ts.showInPage,
|
title: i18n.ts.showInPage,
|
||||||
onClick: expand,
|
onClick: expand,
|
||||||
}];
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return buttons;
|
return buttons;
|
||||||
});
|
});
|
||||||
|
|
||||||
router.addListener('push', ctx => {
|
router.addListener("push", (ctx) => {
|
||||||
history.push({ path: ctx.path, key: ctx.key });
|
history.push({ path: ctx.path, key: ctx.key });
|
||||||
});
|
});
|
||||||
|
|
||||||
provide('router', router);
|
provide("router", router);
|
||||||
provideMetadataReceiver((info) => {
|
provideMetadataReceiver((info) => {
|
||||||
pageMetadata = info;
|
pageMetadata = info;
|
||||||
});
|
});
|
||||||
provide('shouldOmitHeaderTitle', true);
|
provide("shouldOmitHeaderTitle", true);
|
||||||
provide('shouldHeaderThin', true);
|
provide("shouldHeaderThin", true);
|
||||||
|
|
||||||
const contextmenu = $computed(() => ([{
|
const contextmenu = $computed(() => [
|
||||||
icon: 'ph-arrows-out-simple ph-bold ph-lg',
|
{
|
||||||
|
icon: "ph-arrows-out-simple ph-bold ph-lg",
|
||||||
text: i18n.ts.showInPage,
|
text: i18n.ts.showInPage,
|
||||||
action: expand,
|
action: expand,
|
||||||
}, {
|
},
|
||||||
icon: 'ph-arrow-square-out ph-bold ph-lg',
|
{
|
||||||
|
icon: "ph-arrow-square-out ph-bold ph-lg",
|
||||||
text: i18n.ts.popout,
|
text: i18n.ts.popout,
|
||||||
action: popout,
|
action: popout,
|
||||||
}, {
|
},
|
||||||
icon: 'ph-arrow-square-out ph-bold ph-lg',
|
{
|
||||||
|
icon: "ph-arrow-square-out ph-bold ph-lg",
|
||||||
text: i18n.ts.openInNewTab,
|
text: i18n.ts.openInNewTab,
|
||||||
action: () => {
|
action: () => {
|
||||||
window.open(url + router.getCurrentPath(), '_blank');
|
window.open(url + router.getCurrentPath(), "_blank");
|
||||||
windowEl.close();
|
windowEl.close();
|
||||||
},
|
},
|
||||||
}, {
|
},
|
||||||
icon: 'ph-link-simple ph-bold ph-lg',
|
{
|
||||||
|
icon: "ph-link-simple ph-bold ph-lg",
|
||||||
text: i18n.ts.copyLink,
|
text: i18n.ts.copyLink,
|
||||||
action: () => {
|
action: () => {
|
||||||
copyToClipboard(url + router.getCurrentPath());
|
copyToClipboard(url + router.getCurrentPath());
|
||||||
},
|
},
|
||||||
}]));
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
function menu(ev) {
|
function menu(ev) {
|
||||||
os.popupMenu(contextmenu, ev.currentTarget ?? ev.target);
|
os.popupMenu(contextmenu, ev.currentTarget ?? ev.target);
|
||||||
|
@ -114,7 +132,10 @@ function menu(ev) {
|
||||||
|
|
||||||
function back() {
|
function back() {
|
||||||
history.pop();
|
history.pop();
|
||||||
router.replace(history[history.length - 1].path, history[history.length - 1].key);
|
router.replace(
|
||||||
|
history[history.length - 1].path,
|
||||||
|
history[history.length - 1].key
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
|
@ -122,7 +143,7 @@ function close() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function expand() {
|
function expand() {
|
||||||
mainRouter.push(router.getCurrentPath(), 'forcePage');
|
mainRouter.push(router.getCurrentPath(), "forcePage");
|
||||||
windowEl.close();
|
windowEl.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,22 +7,53 @@
|
||||||
<div v-else-if="empty" key="_empty_" class="empty">
|
<div v-else-if="empty" key="_empty_" class="empty">
|
||||||
<slot name="empty">
|
<slot name="empty">
|
||||||
<div class="_fullinfo">
|
<div class="_fullinfo">
|
||||||
<img src="/static-assets/badges/info.png" class="_ghost" alt="Error"/>
|
<img
|
||||||
|
src="/static-assets/badges/info.png"
|
||||||
|
class="_ghost"
|
||||||
|
alt="Error"
|
||||||
|
/>
|
||||||
<div>{{ i18n.ts.nothing }}</div>
|
<div>{{ i18n.ts.nothing }}</div>
|
||||||
</div>
|
</div>
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else ref="rootEl">
|
<div v-else ref="rootEl">
|
||||||
<div v-show="pagination.reversed && more" key="_more_" class="cxiknjgy _gap">
|
<div
|
||||||
<MkButton v-if="!moreFetching" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMoreAhead">
|
v-show="pagination.reversed && more"
|
||||||
|
key="_more_"
|
||||||
|
class="cxiknjgy _gap"
|
||||||
|
>
|
||||||
|
<MkButton
|
||||||
|
v-if="!moreFetching"
|
||||||
|
class="button"
|
||||||
|
:disabled="moreFetching"
|
||||||
|
:style="{ cursor: moreFetching ? 'wait' : 'pointer' }"
|
||||||
|
primary
|
||||||
|
@click="fetchMoreAhead"
|
||||||
|
>
|
||||||
{{ i18n.ts.loadMore }}
|
{{ i18n.ts.loadMore }}
|
||||||
</MkButton>
|
</MkButton>
|
||||||
<MkLoading v-else class="loading" />
|
<MkLoading v-else class="loading" />
|
||||||
</div>
|
</div>
|
||||||
<slot :items="items"></slot>
|
<slot :items="items"></slot>
|
||||||
<div v-show="!pagination.reversed && more" key="_more_" class="cxiknjgy _gap">
|
<div
|
||||||
<MkButton v-if="!moreFetching" v-appear="($store.state.enableInfiniteScroll && !disableAutoLoad) ? fetchMore : null" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore">
|
v-show="!pagination.reversed && more"
|
||||||
|
key="_more_"
|
||||||
|
class="cxiknjgy _gap"
|
||||||
|
>
|
||||||
|
<MkButton
|
||||||
|
v-if="!moreFetching"
|
||||||
|
v-appear="
|
||||||
|
$store.state.enableInfiniteScroll && !disableAutoLoad
|
||||||
|
? fetchMore
|
||||||
|
: null
|
||||||
|
"
|
||||||
|
class="button"
|
||||||
|
:disabled="moreFetching"
|
||||||
|
:style="{ cursor: moreFetching ? 'wait' : 'pointer' }"
|
||||||
|
primary
|
||||||
|
@click="fetchMore"
|
||||||
|
>
|
||||||
{{ i18n.ts.loadMore }}
|
{{ i18n.ts.loadMore }}
|
||||||
</MkButton>
|
</MkButton>
|
||||||
<MkLoading v-else class="loading" />
|
<MkLoading v-else class="loading" />
|
||||||
|
@ -32,17 +63,36 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ComputedRef, isRef, markRaw, onActivated, onDeactivated, Ref, ref, watch } from 'vue';
|
import {
|
||||||
import * as misskey from 'calckey-js';
|
computed,
|
||||||
import * as os from '@/os';
|
ComputedRef,
|
||||||
import { onScrollTop, isTopVisible, getScrollPosition, getScrollContainer } from '@/scripts/scroll';
|
isRef,
|
||||||
import MkButton from '@/components/MkButton.vue';
|
markRaw,
|
||||||
import { i18n } from '@/i18n';
|
onActivated,
|
||||||
|
onDeactivated,
|
||||||
|
Ref,
|
||||||
|
ref,
|
||||||
|
watch,
|
||||||
|
} from "vue";
|
||||||
|
import * as misskey from "calckey-js";
|
||||||
|
import * as os from "@/os";
|
||||||
|
import {
|
||||||
|
onScrollTop,
|
||||||
|
isTopVisible,
|
||||||
|
getScrollPosition,
|
||||||
|
getScrollContainer,
|
||||||
|
} from "@/scripts/scroll";
|
||||||
|
import MkButton from "@/components/MkButton.vue";
|
||||||
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
export type Paging<E extends keyof misskey.Endpoints = keyof misskey.Endpoints> = {
|
export type Paging<
|
||||||
|
E extends keyof misskey.Endpoints = keyof misskey.Endpoints
|
||||||
|
> = {
|
||||||
endpoint: E;
|
endpoint: E;
|
||||||
limit: number;
|
limit: number;
|
||||||
params?: misskey.Endpoints[E]['req'] | ComputedRef<misskey.Endpoints[E]['req']>;
|
params?:
|
||||||
|
| misskey.Endpoints[E]["req"]
|
||||||
|
| ComputedRef<misskey.Endpoints[E]["req"]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 検索APIのような、ページング不可なエンドポイントを利用する場合
|
* 検索APIのような、ページング不可なエンドポイントを利用する場合
|
||||||
|
@ -60,19 +110,22 @@
|
||||||
|
|
||||||
const SECOND_FETCH_LIMIT = 30;
|
const SECOND_FETCH_LIMIT = 30;
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
pagination: Paging;
|
pagination: Paging;
|
||||||
disableAutoLoad?: boolean;
|
disableAutoLoad?: boolean;
|
||||||
displayLimit?: number;
|
displayLimit?: number;
|
||||||
}>(), {
|
}>(),
|
||||||
|
{
|
||||||
displayLimit: 30,
|
displayLimit: 30,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'queue', count: number): void;
|
(ev: "queue", count: number): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
type Item = { id: string; [another: string]: unknown; };
|
type Item = { id: string; [another: string]: unknown };
|
||||||
|
|
||||||
const rootEl = ref<HTMLElement>();
|
const rootEl = ref<HTMLElement>();
|
||||||
const items = ref<Item[]>([]);
|
const items = ref<Item[]>([]);
|
||||||
|
@ -89,11 +142,20 @@
|
||||||
const init = async (): Promise<void> => {
|
const init = async (): Promise<void> => {
|
||||||
queue.value = [];
|
queue.value = [];
|
||||||
fetching.value = true;
|
fetching.value = true;
|
||||||
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
const params = props.pagination.params
|
||||||
await os.api(props.pagination.endpoint, {
|
? isRef(props.pagination.params)
|
||||||
|
? props.pagination.params.value
|
||||||
|
: props.pagination.params
|
||||||
|
: {};
|
||||||
|
await os
|
||||||
|
.api(props.pagination.endpoint, {
|
||||||
...params,
|
...params,
|
||||||
limit: props.pagination.noPaging ? (props.pagination.limit || 10) : (props.pagination.limit || 10) + 1,
|
limit: props.pagination.noPaging
|
||||||
}).then(res => {
|
? props.pagination.limit || 10
|
||||||
|
: (props.pagination.limit || 10) + 1,
|
||||||
|
})
|
||||||
|
.then(
|
||||||
|
(res) => {
|
||||||
for (let i = 0; i < res.length; i++) {
|
for (let i = 0; i < res.length; i++) {
|
||||||
const item = res[i];
|
const item = res[i];
|
||||||
if (props.pagination.reversed) {
|
if (props.pagination.reversed) {
|
||||||
|
@ -102,21 +164,30 @@
|
||||||
if (i === 3) item._shouldInsertAd_ = true;
|
if (i === 3) item._shouldInsertAd_ = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!props.pagination.noPaging && (res.length > (props.pagination.limit || 10))) {
|
if (
|
||||||
|
!props.pagination.noPaging &&
|
||||||
|
res.length > (props.pagination.limit || 10)
|
||||||
|
) {
|
||||||
res.pop();
|
res.pop();
|
||||||
items.value = props.pagination.reversed ? [...res].reverse() : res;
|
items.value = props.pagination.reversed
|
||||||
|
? [...res].reverse()
|
||||||
|
: res;
|
||||||
more.value = true;
|
more.value = true;
|
||||||
} else {
|
} else {
|
||||||
items.value = props.pagination.reversed ? [...res].reverse() : res;
|
items.value = props.pagination.reversed
|
||||||
|
? [...res].reverse()
|
||||||
|
: res;
|
||||||
more.value = false;
|
more.value = false;
|
||||||
}
|
}
|
||||||
offset.value = res.length;
|
offset.value = res.length;
|
||||||
error.value = false;
|
error.value = false;
|
||||||
fetching.value = false;
|
fetching.value = false;
|
||||||
}, err => {
|
},
|
||||||
|
(err) => {
|
||||||
error.value = true;
|
error.value = true;
|
||||||
fetching.value = false;
|
fetching.value = false;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const reload = (): void => {
|
const reload = (): void => {
|
||||||
|
@ -125,50 +196,76 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
const refresh = async (): void => {
|
const refresh = async (): void => {
|
||||||
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
const params = props.pagination.params
|
||||||
await os.api(props.pagination.endpoint, {
|
? isRef(props.pagination.params)
|
||||||
|
? props.pagination.params.value
|
||||||
|
: props.pagination.params
|
||||||
|
: {};
|
||||||
|
await os
|
||||||
|
.api(props.pagination.endpoint, {
|
||||||
...params,
|
...params,
|
||||||
limit: items.value.length + 1,
|
limit: items.value.length + 1,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
}).then(res => {
|
})
|
||||||
|
.then(
|
||||||
|
(res) => {
|
||||||
let ids = items.value.reduce((a, b) => {
|
let ids = items.value.reduce((a, b) => {
|
||||||
a[b.id] = true;
|
a[b.id] = true;
|
||||||
return a;
|
return a;
|
||||||
}, {} as { [id: string]: boolean; });
|
}, {} as { [id: string]: boolean });
|
||||||
|
|
||||||
for (let i = 0; i < res.length; i++) {
|
for (let i = 0; i < res.length; i++) {
|
||||||
const item = res[i];
|
const item = res[i];
|
||||||
if (!updateItem(item.id, old => item)) {
|
if (!updateItem(item.id, (old) => item)) {
|
||||||
append(item);
|
append(item);
|
||||||
}
|
}
|
||||||
delete ids[item.id];
|
delete ids[item.id];
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const id in ids) {
|
for (const id in ids) {
|
||||||
removeItem(i => i.id === id);
|
removeItem((i) => i.id === id);
|
||||||
}
|
}
|
||||||
}, err => {
|
},
|
||||||
|
(err) => {
|
||||||
error.value = true;
|
error.value = true;
|
||||||
fetching.value = false;
|
fetching.value = false;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchMore = async (): Promise<void> => {
|
const fetchMore = async (): Promise<void> => {
|
||||||
if (!more.value || fetching.value || moreFetching.value || items.value.length === 0) return;
|
if (
|
||||||
|
!more.value ||
|
||||||
|
fetching.value ||
|
||||||
|
moreFetching.value ||
|
||||||
|
items.value.length === 0
|
||||||
|
)
|
||||||
|
return;
|
||||||
moreFetching.value = true;
|
moreFetching.value = true;
|
||||||
backed.value = true;
|
backed.value = true;
|
||||||
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
const params = props.pagination.params
|
||||||
await os.api(props.pagination.endpoint, {
|
? isRef(props.pagination.params)
|
||||||
|
? props.pagination.params.value
|
||||||
|
: props.pagination.params
|
||||||
|
: {};
|
||||||
|
await os
|
||||||
|
.api(props.pagination.endpoint, {
|
||||||
...params,
|
...params,
|
||||||
limit: SECOND_FETCH_LIMIT + 1,
|
limit: SECOND_FETCH_LIMIT + 1,
|
||||||
...(props.pagination.offsetMode ? {
|
...(props.pagination.offsetMode
|
||||||
|
? {
|
||||||
offset: offset.value,
|
offset: offset.value,
|
||||||
} : props.pagination.reversed ? {
|
}
|
||||||
|
: props.pagination.reversed
|
||||||
|
? {
|
||||||
sinceId: items.value[0].id,
|
sinceId: items.value[0].id,
|
||||||
} : {
|
}
|
||||||
|
: {
|
||||||
untilId: items.value[items.value.length - 1].id,
|
untilId: items.value[items.value.length - 1].id,
|
||||||
}),
|
}),
|
||||||
}).then(res => {
|
})
|
||||||
|
.then(
|
||||||
|
(res) => {
|
||||||
for (let i = 0; i < res.length; i++) {
|
for (let i = 0; i < res.length; i++) {
|
||||||
const item = res[i];
|
const item = res[i];
|
||||||
if (props.pagination.reversed) {
|
if (props.pagination.reversed) {
|
||||||
|
@ -179,47 +276,76 @@
|
||||||
}
|
}
|
||||||
if (res.length > SECOND_FETCH_LIMIT) {
|
if (res.length > SECOND_FETCH_LIMIT) {
|
||||||
res.pop();
|
res.pop();
|
||||||
items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res);
|
items.value = props.pagination.reversed
|
||||||
|
? [...res].reverse().concat(items.value)
|
||||||
|
: items.value.concat(res);
|
||||||
more.value = true;
|
more.value = true;
|
||||||
} else {
|
} else {
|
||||||
items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res);
|
items.value = props.pagination.reversed
|
||||||
|
? [...res].reverse().concat(items.value)
|
||||||
|
: items.value.concat(res);
|
||||||
more.value = false;
|
more.value = false;
|
||||||
}
|
}
|
||||||
offset.value += res.length;
|
offset.value += res.length;
|
||||||
moreFetching.value = false;
|
moreFetching.value = false;
|
||||||
}, err => {
|
},
|
||||||
|
(err) => {
|
||||||
moreFetching.value = false;
|
moreFetching.value = false;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchMoreAhead = async (): Promise<void> => {
|
const fetchMoreAhead = async (): Promise<void> => {
|
||||||
if (!more.value || fetching.value || moreFetching.value || items.value.length === 0) return;
|
if (
|
||||||
|
!more.value ||
|
||||||
|
fetching.value ||
|
||||||
|
moreFetching.value ||
|
||||||
|
items.value.length === 0
|
||||||
|
)
|
||||||
|
return;
|
||||||
moreFetching.value = true;
|
moreFetching.value = true;
|
||||||
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
const params = props.pagination.params
|
||||||
await os.api(props.pagination.endpoint, {
|
? isRef(props.pagination.params)
|
||||||
|
? props.pagination.params.value
|
||||||
|
: props.pagination.params
|
||||||
|
: {};
|
||||||
|
await os
|
||||||
|
.api(props.pagination.endpoint, {
|
||||||
...params,
|
...params,
|
||||||
limit: SECOND_FETCH_LIMIT + 1,
|
limit: SECOND_FETCH_LIMIT + 1,
|
||||||
...(props.pagination.offsetMode ? {
|
...(props.pagination.offsetMode
|
||||||
|
? {
|
||||||
offset: offset.value,
|
offset: offset.value,
|
||||||
} : props.pagination.reversed ? {
|
}
|
||||||
|
: props.pagination.reversed
|
||||||
|
? {
|
||||||
untilId: items.value[0].id,
|
untilId: items.value[0].id,
|
||||||
} : {
|
}
|
||||||
|
: {
|
||||||
sinceId: items.value[items.value.length - 1].id,
|
sinceId: items.value[items.value.length - 1].id,
|
||||||
}),
|
}),
|
||||||
}).then(res => {
|
})
|
||||||
|
.then(
|
||||||
|
(res) => {
|
||||||
if (res.length > SECOND_FETCH_LIMIT) {
|
if (res.length > SECOND_FETCH_LIMIT) {
|
||||||
res.pop();
|
res.pop();
|
||||||
items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res);
|
items.value = props.pagination.reversed
|
||||||
|
? [...res].reverse().concat(items.value)
|
||||||
|
: items.value.concat(res);
|
||||||
more.value = true;
|
more.value = true;
|
||||||
} else {
|
} else {
|
||||||
items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res);
|
items.value = props.pagination.reversed
|
||||||
|
? [...res].reverse().concat(items.value)
|
||||||
|
: items.value.concat(res);
|
||||||
more.value = false;
|
more.value = false;
|
||||||
}
|
}
|
||||||
offset.value += res.length;
|
offset.value += res.length;
|
||||||
moreFetching.value = false;
|
moreFetching.value = false;
|
||||||
}, err => {
|
},
|
||||||
|
(err) => {
|
||||||
moreFetching.value = false;
|
moreFetching.value = false;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const prepend = (item: Item): void => {
|
const prepend = (item: Item): void => {
|
||||||
|
@ -232,7 +358,7 @@
|
||||||
const pos = getScrollPosition(rootEl.value);
|
const pos = getScrollPosition(rootEl.value);
|
||||||
const viewHeight = container.clientHeight;
|
const viewHeight = container.clientHeight;
|
||||||
const height = container.scrollHeight;
|
const height = container.scrollHeight;
|
||||||
const isBottom = (pos + viewHeight > height - 32);
|
const isBottom = pos + viewHeight > height - 32;
|
||||||
if (isBottom) {
|
if (isBottom) {
|
||||||
// オーバーフローしたら古いアイテムは捨てる
|
// オーバーフローしたら古いアイテムは捨てる
|
||||||
if (items.value.length >= props.displayLimit) {
|
if (items.value.length >= props.displayLimit) {
|
||||||
|
@ -255,7 +381,10 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isTop = isBackTop.value || (document.body.contains(rootEl.value) && isTopVisible(rootEl.value));
|
const isTop =
|
||||||
|
isBackTop.value ||
|
||||||
|
(document.body.contains(rootEl.value) &&
|
||||||
|
isTopVisible(rootEl.value));
|
||||||
|
|
||||||
if (isTop) {
|
if (isTop) {
|
||||||
// Prepend the item
|
// Prepend the item
|
||||||
|
@ -296,8 +425,8 @@
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateItem = (id: Item['id'], replacer: (old: Item) => Item): boolean => {
|
const updateItem = (id: Item["id"], replacer: (old: Item) => Item): boolean => {
|
||||||
const i = items.value.findIndex(item => item.id === id);
|
const i = items.value.findIndex((item) => item.id === id);
|
||||||
if (i === -1) {
|
if (i === -1) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -310,10 +439,14 @@
|
||||||
watch(props.pagination.params, init, { deep: true });
|
watch(props.pagination.params, init, { deep: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(queue, (a, b) => {
|
watch(
|
||||||
|
queue,
|
||||||
|
(a, b) => {
|
||||||
if (a.length === 0 && b.length === 0) return;
|
if (a.length === 0 && b.length === 0) return;
|
||||||
emit('queue', queue.value.length);
|
emit("queue", queue.value.length);
|
||||||
}, { deep: true });
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,47 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="tivcixzd" :class="{ done: closed || isVoted }">
|
<div class="tivcixzd" :class="{ done: closed || isVoted }">
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="(choice, i) in note.poll.choices" :key="i" :class="{ voted: choice.voted }" @click.stop="vote(i)">
|
<li
|
||||||
<div class="backdrop" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
|
v-for="(choice, i) in note.poll.choices"
|
||||||
|
:key="i"
|
||||||
|
:class="{ voted: choice.voted }"
|
||||||
|
@click.stop="vote(i)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="backdrop"
|
||||||
|
:style="{
|
||||||
|
width: `${
|
||||||
|
showResult ? (choice.votes / total) * 100 : 0
|
||||||
|
}%`,
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
<span>
|
<span>
|
||||||
<template v-if="choice.isVoted"><i class="ph-check ph-bold ph-lg"></i></template>
|
<template v-if="choice.isVoted"
|
||||||
<Mfm :text="choice.text" :plain="true" :custom-emojis="note.emojis"/>
|
><i class="ph-check ph-bold ph-lg"></i
|
||||||
<span v-if="showResult" class="votes">({{ i18n.t('_poll.votesCount', { n: choice.votes }) }})</span>
|
></template>
|
||||||
|
<Mfm
|
||||||
|
:text="choice.text"
|
||||||
|
:plain="true"
|
||||||
|
:custom-emojis="note.emojis"
|
||||||
|
/>
|
||||||
|
<span v-if="showResult" class="votes"
|
||||||
|
>({{
|
||||||
|
i18n.t("_poll.votesCount", { n: choice.votes })
|
||||||
|
}})</span
|
||||||
|
>
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p v-if="!readOnly">
|
<p v-if="!readOnly">
|
||||||
<span>{{ i18n.t('_poll.totalVotes', { n: total }) }}</span>
|
<span>{{ i18n.t("_poll.totalVotes", { n: total }) }}</span>
|
||||||
<span> · </span>
|
<span> · </span>
|
||||||
<a v-if="!closed && !isVoted" @click.stop="showResult = !showResult">{{ showResult ? i18n.ts._poll.vote : i18n.ts._poll.showResult }}</a>
|
<a
|
||||||
|
v-if="!closed && !isVoted"
|
||||||
|
@click.stop="showResult = !showResult"
|
||||||
|
>{{
|
||||||
|
showResult ? i18n.ts._poll.vote : i18n.ts._poll.showResult
|
||||||
|
}}</a
|
||||||
|
>
|
||||||
<span v-if="isVoted">{{ i18n.ts._poll.voted }}</span>
|
<span v-if="isVoted">{{ i18n.ts._poll.voted }}</span>
|
||||||
<span v-else-if="closed">{{ i18n.ts._poll.closed }}</span>
|
<span v-else-if="closed">{{ i18n.ts._poll.closed }}</span>
|
||||||
<span v-if="remaining > 0"> · {{ timer }}</span>
|
<span v-if="remaining > 0"> · {{ timer }}</span>
|
||||||
|
@ -22,13 +50,13 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onUnmounted, ref, toRef } from 'vue';
|
import { computed, onUnmounted, ref, toRef } from "vue";
|
||||||
import * as misskey from 'calckey-js';
|
import * as misskey from "calckey-js";
|
||||||
import { sum } from '@/scripts/array';
|
import { sum } from "@/scripts/array";
|
||||||
import { pleaseLogin } from '@/scripts/please-login';
|
import { pleaseLogin } from "@/scripts/please-login";
|
||||||
import * as os from '@/os';
|
import * as os from "@/os";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
import { useInterval } from '@/scripts/use-interval';
|
import { useInterval } from "@/scripts/use-interval";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
note: misskey.entities.Note;
|
note: misskey.entities.Note;
|
||||||
|
@ -37,25 +65,42 @@ const props = defineProps<{
|
||||||
|
|
||||||
const remaining = ref(-1);
|
const remaining = ref(-1);
|
||||||
|
|
||||||
const total = computed(() => sum(props.note.poll.choices.map(x => x.votes)));
|
const total = computed(() => sum(props.note.poll.choices.map((x) => x.votes)));
|
||||||
const closed = computed(() => remaining.value === 0);
|
const closed = computed(() => remaining.value === 0);
|
||||||
const isVoted = computed(() => !props.note.poll.multiple && props.note.poll.choices.some(c => c.isVoted));
|
const isVoted = computed(
|
||||||
const timer = computed(() => i18n.t(
|
() =>
|
||||||
remaining.value >= 86400 ? '_poll.remainingDays' :
|
!props.note.poll.multiple &&
|
||||||
remaining.value >= 3600 ? '_poll.remainingHours' :
|
props.note.poll.choices.some((c) => c.isVoted)
|
||||||
remaining.value >= 60 ? '_poll.remainingMinutes' : '_poll.remainingSeconds', {
|
);
|
||||||
|
const timer = computed(() =>
|
||||||
|
i18n.t(
|
||||||
|
remaining.value >= 86400
|
||||||
|
? "_poll.remainingDays"
|
||||||
|
: remaining.value >= 3600
|
||||||
|
? "_poll.remainingHours"
|
||||||
|
: remaining.value >= 60
|
||||||
|
? "_poll.remainingMinutes"
|
||||||
|
: "_poll.remainingSeconds",
|
||||||
|
{
|
||||||
s: Math.floor(remaining.value % 60),
|
s: Math.floor(remaining.value % 60),
|
||||||
m: Math.floor(remaining.value / 60) % 60,
|
m: Math.floor(remaining.value / 60) % 60,
|
||||||
h: Math.floor(remaining.value / 3600) % 24,
|
h: Math.floor(remaining.value / 3600) % 24,
|
||||||
d: Math.floor(remaining.value / 86400),
|
d: Math.floor(remaining.value / 86400),
|
||||||
}));
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
const showResult = ref(props.readOnly || isVoted.value);
|
const showResult = ref(props.readOnly || isVoted.value);
|
||||||
|
|
||||||
// 期限付きアンケート
|
// 期限付きアンケート
|
||||||
if (props.note.poll.expiresAt) {
|
if (props.note.poll.expiresAt) {
|
||||||
const tick = () => {
|
const tick = () => {
|
||||||
remaining.value = Math.floor(Math.max(new Date(props.note.poll.expiresAt).getTime() - Date.now(), 0) / 1000);
|
remaining.value = Math.floor(
|
||||||
|
Math.max(
|
||||||
|
new Date(props.note.poll.expiresAt).getTime() - Date.now(),
|
||||||
|
0
|
||||||
|
) / 1000
|
||||||
|
);
|
||||||
if (remaining.value === 0) {
|
if (remaining.value === 0) {
|
||||||
showResult.value = true;
|
showResult.value = true;
|
||||||
}
|
}
|
||||||
|
@ -73,12 +118,14 @@ const vote = async (id) => {
|
||||||
if (props.readOnly || closed.value || isVoted.value) return;
|
if (props.readOnly || closed.value || isVoted.value) return;
|
||||||
|
|
||||||
const { canceled } = await os.confirm({
|
const { canceled } = await os.confirm({
|
||||||
type: 'question',
|
type: "question",
|
||||||
text: i18n.t('voteConfirm', { choice: props.note.poll.choices[id].text }),
|
text: i18n.t("voteConfirm", {
|
||||||
|
choice: props.note.poll.choices[id].text,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
|
|
||||||
await os.api('notes/polls/vote', {
|
await os.api("notes/polls/vote", {
|
||||||
noteId: props.note.id,
|
noteId: props.note.id,
|
||||||
choice: id,
|
choice: id,
|
||||||
});
|
});
|
||||||
|
@ -111,7 +158,11 @@ const vote = async (id) => {
|
||||||
left: 0;
|
left: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
background: linear-gradient(90deg,var(--buttonGradateA),var(--buttonGradateB));
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
var(--buttonGradateA),
|
||||||
|
var(--buttonGradateB)
|
||||||
|
);
|
||||||
transition: width 1s ease;
|
transition: width 1s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,34 +1,53 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="zmdxowus">
|
<div class="zmdxowus">
|
||||||
<p v-if="choices.length < 2" class="caution">
|
<p v-if="choices.length < 2" class="caution">
|
||||||
<i class="ph-warning ph-bold ph-lg"></i>{{ i18n.ts._poll.noOnlyOneChoice }}
|
<i class="ph-warning ph-bold ph-lg"></i
|
||||||
|
>{{ i18n.ts._poll.noOnlyOneChoice }}
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="(choice, i) in choices" :key="i">
|
<li v-for="(choice, i) in choices" :key="i">
|
||||||
<MkInput class="input" small :model-value="choice" :placeholder="i18n.t('_poll.choiceN', { n: i + 1 })" @update:modelValue="onInput(i, $event)">
|
<MkInput
|
||||||
|
class="input"
|
||||||
|
small
|
||||||
|
:model-value="choice"
|
||||||
|
:placeholder="i18n.t('_poll.choiceN', { n: i + 1 })"
|
||||||
|
@update:modelValue="onInput(i, $event)"
|
||||||
|
>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<button class="_button" @click="remove(i)">
|
<button class="_button" @click="remove(i)">
|
||||||
<i class="ph-x ph-bold ph-lg"></i>
|
<i class="ph-x ph-bold ph-lg"></i>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<MkButton v-if="choices.length < 10" class="add" @click="add">{{ i18n.ts.add }}</MkButton>
|
<MkButton v-if="choices.length < 10" class="add" @click="add">{{
|
||||||
<MkButton v-else class="add" disabled>{{ i18n.ts._poll.noMore }}</MkButton>
|
i18n.ts.add
|
||||||
<MkSwitch v-model="multiple">{{ i18n.ts._poll.canMultipleVote }}</MkSwitch>
|
}}</MkButton>
|
||||||
|
<MkButton v-else class="add" disabled>{{
|
||||||
|
i18n.ts._poll.noMore
|
||||||
|
}}</MkButton>
|
||||||
|
<MkSwitch v-model="multiple">{{
|
||||||
|
i18n.ts._poll.canMultipleVote
|
||||||
|
}}</MkSwitch>
|
||||||
<section>
|
<section>
|
||||||
<div>
|
<div>
|
||||||
<MkSelect v-model="expiration" small>
|
<MkSelect v-model="expiration" small>
|
||||||
<template #label>{{ i18n.ts._poll.expiration }}</template>
|
<template #label>{{ i18n.ts._poll.expiration }}</template>
|
||||||
<option value="infinite">{{ i18n.ts._poll.infinite }}</option>
|
<option value="infinite">
|
||||||
|
{{ i18n.ts._poll.infinite }}
|
||||||
|
</option>
|
||||||
<option value="at">{{ i18n.ts._poll.at }}</option>
|
<option value="at">{{ i18n.ts._poll.at }}</option>
|
||||||
<option value="after">{{ i18n.ts._poll.after }}</option>
|
<option value="after">{{ i18n.ts._poll.after }}</option>
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<section v-if="expiration === 'at'">
|
<section v-if="expiration === 'at'">
|
||||||
<MkInput v-model="atDate" small type="date" class="input">
|
<MkInput v-model="atDate" small type="date" class="input">
|
||||||
<template #label>{{ i18n.ts._poll.deadlineDate }}</template>
|
<template #label>{{
|
||||||
|
i18n.ts._poll.deadlineDate
|
||||||
|
}}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkInput v-model="atTime" small type="time" class="input">
|
<MkInput v-model="atTime" small type="time" class="input">
|
||||||
<template #label>{{ i18n.ts._poll.deadlineTime }}</template>
|
<template #label>{{
|
||||||
|
i18n.ts._poll.deadlineTime
|
||||||
|
}}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
</section>
|
</section>
|
||||||
<section v-else-if="expiration === 'after'">
|
<section v-else-if="expiration === 'after'">
|
||||||
|
@ -36,8 +55,12 @@
|
||||||
<template #label>{{ i18n.ts._poll.duration }}</template>
|
<template #label>{{ i18n.ts._poll.duration }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkSelect v-model="unit" small>
|
<MkSelect v-model="unit" small>
|
||||||
<option value="second">{{ i18n.ts._time.second }}</option>
|
<option value="second">
|
||||||
<option value="minute">{{ i18n.ts._time.minute }}</option>
|
{{ i18n.ts._time.second }}
|
||||||
|
</option>
|
||||||
|
<option value="minute">
|
||||||
|
{{ i18n.ts._time.minute }}
|
||||||
|
</option>
|
||||||
<option value="hour">{{ i18n.ts._time.hour }}</option>
|
<option value="hour">{{ i18n.ts._time.hour }}</option>
|
||||||
<option value="day">{{ i18n.ts._time.day }}</option>
|
<option value="day">{{ i18n.ts._time.day }}</option>
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
|
@ -48,14 +71,14 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from "vue";
|
||||||
import MkInput from './form/input.vue';
|
import MkInput from "./form/input.vue";
|
||||||
import MkSelect from './form/select.vue';
|
import MkSelect from "./form/select.vue";
|
||||||
import MkSwitch from './form/switch.vue';
|
import MkSwitch from "./form/switch.vue";
|
||||||
import MkButton from './MkButton.vue';
|
import MkButton from "./MkButton.vue";
|
||||||
import { formatDateTimeString } from '@/scripts/format-time-string';
|
import { formatDateTimeString } from "@/scripts/format-time-string";
|
||||||
import { addTime } from '@/scripts/time';
|
import { addTime } from "@/scripts/time";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: {
|
modelValue: {
|
||||||
|
@ -66,30 +89,35 @@ const props = defineProps<{
|
||||||
};
|
};
|
||||||
}>();
|
}>();
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'update:modelValue', v: {
|
(
|
||||||
|
ev: "update:modelValue",
|
||||||
|
v: {
|
||||||
expiresAt: string;
|
expiresAt: string;
|
||||||
expiredAfter: number;
|
expiredAfter: number;
|
||||||
choices: string[];
|
choices: string[];
|
||||||
multiple: boolean;
|
multiple: boolean;
|
||||||
}): void;
|
}
|
||||||
|
): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const choices = ref(props.modelValue.choices);
|
const choices = ref(props.modelValue.choices);
|
||||||
const multiple = ref(props.modelValue.multiple);
|
const multiple = ref(props.modelValue.multiple);
|
||||||
const expiration = ref('infinite');
|
const expiration = ref("infinite");
|
||||||
const atDate = ref(formatDateTimeString(addTime(new Date(), 1, 'day'), 'yyyy-MM-dd'));
|
const atDate = ref(
|
||||||
const atTime = ref('00:00');
|
formatDateTimeString(addTime(new Date(), 1, "day"), "yyyy-MM-dd")
|
||||||
|
);
|
||||||
|
const atTime = ref("00:00");
|
||||||
const after = ref(0);
|
const after = ref(0);
|
||||||
const unit = ref('second');
|
const unit = ref("second");
|
||||||
|
|
||||||
if (props.modelValue.expiresAt) {
|
if (props.modelValue.expiresAt) {
|
||||||
expiration.value = 'at';
|
expiration.value = "at";
|
||||||
atDate.value = atTime.value = props.modelValue.expiresAt;
|
atDate.value = atTime.value = props.modelValue.expiresAt;
|
||||||
} else if (typeof props.modelValue.expiredAfter === 'number') {
|
} else if (typeof props.modelValue.expiredAfter === "number") {
|
||||||
expiration.value = 'after';
|
expiration.value = "after";
|
||||||
after.value = props.modelValue.expiredAfter / 1000;
|
after.value = props.modelValue.expiredAfter / 1000;
|
||||||
} else {
|
} else {
|
||||||
expiration.value = 'infinite';
|
expiration.value = "infinite";
|
||||||
}
|
}
|
||||||
|
|
||||||
function onInput(i, value) {
|
function onInput(i, value) {
|
||||||
|
@ -97,7 +125,7 @@ function onInput(i, value) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function add() {
|
function add() {
|
||||||
choices.value.push('');
|
choices.value.push("");
|
||||||
// TODO
|
// TODO
|
||||||
// nextTick(() => {
|
// nextTick(() => {
|
||||||
// (this.$refs.choices as any).childNodes[this.choices.length - 1].childNodes[0].focus();
|
// (this.$refs.choices as any).childNodes[this.choices.length - 1].childNodes[0].focus();
|
||||||
|
@ -116,30 +144,40 @@ function get() {
|
||||||
const calcAfter = () => {
|
const calcAfter = () => {
|
||||||
let base = parseInt(after.value);
|
let base = parseInt(after.value);
|
||||||
switch (unit.value) {
|
switch (unit.value) {
|
||||||
case 'day': base *= 24;
|
case "day":
|
||||||
|
base *= 24;
|
||||||
// fallthrough
|
// fallthrough
|
||||||
case 'hour': base *= 60;
|
case "hour":
|
||||||
|
base *= 60;
|
||||||
// fallthrough
|
// fallthrough
|
||||||
case 'minute': base *= 60;
|
case "minute":
|
||||||
|
base *= 60;
|
||||||
// fallthrough
|
// fallthrough
|
||||||
case 'second': return base *= 1000;
|
case "second":
|
||||||
default: return null;
|
return (base *= 1000);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
choices: choices.value,
|
choices: choices.value,
|
||||||
multiple: multiple.value,
|
multiple: multiple.value,
|
||||||
...(
|
...(expiration.value === "at"
|
||||||
expiration.value === 'at' ? { expiresAt: calcAt() } :
|
? { expiresAt: calcAt() }
|
||||||
expiration.value === 'after' ? { expiredAfter: calcAfter() } : {}
|
: expiration.value === "after"
|
||||||
),
|
? { expiredAfter: calcAfter() }
|
||||||
|
: {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
watch([choices, multiple, expiration, atDate, atTime, after, unit], () => emit('update:modelValue', get()), {
|
watch(
|
||||||
|
[choices, multiple, expiration, atDate, atTime, after, unit],
|
||||||
|
() => emit("update:modelValue", get()),
|
||||||
|
{
|
||||||
deep: true,
|
deep: true,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -1,25 +1,42 @@
|
||||||
<template>
|
<template>
|
||||||
<MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="modal.close()" @closed="emit('closed')">
|
<MkModal
|
||||||
<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :as-drawer="type === 'drawer'" class="sfhdhdhq" :class="{ drawer: type === 'drawer' }" @close="modal.close()"/>
|
ref="modal"
|
||||||
|
v-slot="{ type, maxHeight }"
|
||||||
|
:z-priority="'high'"
|
||||||
|
:src="src"
|
||||||
|
:transparent-bg="true"
|
||||||
|
@click="modal.close()"
|
||||||
|
@closed="emit('closed')"
|
||||||
|
>
|
||||||
|
<MkMenu
|
||||||
|
:items="items"
|
||||||
|
:align="align"
|
||||||
|
:width="width"
|
||||||
|
:max-height="maxHeight"
|
||||||
|
:as-drawer="type === 'drawer'"
|
||||||
|
class="sfhdhdhq"
|
||||||
|
:class="{ drawer: type === 'drawer' }"
|
||||||
|
@close="modal.close()"
|
||||||
|
/>
|
||||||
</MkModal>
|
</MkModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import {} from "vue";
|
||||||
import MkModal from './MkModal.vue';
|
import MkModal from "./MkModal.vue";
|
||||||
import MkMenu from './MkMenu.vue';
|
import MkMenu from "./MkMenu.vue";
|
||||||
import { MenuItem } from '@/types/menu';
|
import { MenuItem } from "@/types/menu";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
items: MenuItem[];
|
items: MenuItem[];
|
||||||
align?: 'center' | string;
|
align?: "center" | string;
|
||||||
width?: number;
|
width?: number;
|
||||||
viaKeyboard?: boolean;
|
viaKeyboard?: boolean;
|
||||||
src?: any;
|
src?: any;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'closed'): void;
|
(ev: "closed"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let modal = $ref<InstanceType<typeof MkModal>>();
|
let modal = $ref<InstanceType<typeof MkModal>>();
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,9 +1,25 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-show="files.length != 0" class="skeikyzd">
|
<div v-show="files.length != 0" class="skeikyzd">
|
||||||
<XDraggable v-model="_files" class="files" item-key="id" animation="150" delay="100" delay-on-touch-only="true">
|
<XDraggable
|
||||||
|
v-model="_files"
|
||||||
|
class="files"
|
||||||
|
item-key="id"
|
||||||
|
animation="150"
|
||||||
|
delay="100"
|
||||||
|
delay-on-touch-only="true"
|
||||||
|
>
|
||||||
<template #item="{ element }">
|
<template #item="{ element }">
|
||||||
<div class="file" @click="showFileMenu(element, $event)" @contextmenu.prevent="showFileMenu(element, $event)">
|
<div
|
||||||
<MkDriveFileThumbnail :data-id="element.id" class="thumbnail" :file="element" fit="cover"/>
|
class="file"
|
||||||
|
@click="showFileMenu(element, $event)"
|
||||||
|
@contextmenu.prevent="showFileMenu(element, $event)"
|
||||||
|
>
|
||||||
|
<MkDriveFileThumbnail
|
||||||
|
:data-id="element.id"
|
||||||
|
class="thumbnail"
|
||||||
|
:file="element"
|
||||||
|
fit="cover"
|
||||||
|
/>
|
||||||
<div v-if="element.isSensitive" class="sensitive">
|
<div v-if="element.isSensitive" class="sensitive">
|
||||||
<i class="ph-warning ph-bold ph-lg icon"></i>
|
<i class="ph-warning ph-bold ph-lg icon"></i>
|
||||||
</div>
|
</div>
|
||||||
|
@ -15,14 +31,16 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, defineAsyncComponent } from 'vue';
|
import { defineComponent, defineAsyncComponent } from "vue";
|
||||||
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
|
import MkDriveFileThumbnail from "@/components/MkDriveFileThumbnail.vue";
|
||||||
import * as os from '@/os';
|
import * as os from "@/os";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
XDraggable: defineAsyncComponent(() => import('vuedraggable').then(x => x.default)),
|
XDraggable: defineAsyncComponent(() =>
|
||||||
|
import("vuedraggable").then((x) => x.default)
|
||||||
|
),
|
||||||
MkDriveFileThumbnail,
|
MkDriveFileThumbnail,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -37,7 +55,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
emits: ['updated', 'detach', 'changeSensitive', 'changeName'],
|
emits: ["updated", "detach", "changeSensitive", "changeName"],
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -52,7 +70,7 @@ export default defineComponent({
|
||||||
return this.files;
|
return this.files;
|
||||||
},
|
},
|
||||||
set(value) {
|
set(value) {
|
||||||
this.$emit('updated', value);
|
this.$emit("updated", value);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -62,15 +80,15 @@ export default defineComponent({
|
||||||
if (this.detachMediaFn) {
|
if (this.detachMediaFn) {
|
||||||
this.detachMediaFn(id);
|
this.detachMediaFn(id);
|
||||||
} else {
|
} else {
|
||||||
this.$emit('detach', id);
|
this.$emit("detach", id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
toggleSensitive(file) {
|
toggleSensitive(file) {
|
||||||
os.api('drive/files/update', {
|
os.api("drive/files/update", {
|
||||||
fileId: file.id,
|
fileId: file.id,
|
||||||
isSensitive: !file.isSensitive,
|
isSensitive: !file.isSensitive,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
this.$emit('changeSensitive', file, !file.isSensitive);
|
this.$emit("changeSensitive", file, !file.isSensitive);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
async rename(file) {
|
async rename(file) {
|
||||||
|
@ -80,56 +98,86 @@ export default defineComponent({
|
||||||
allowEmpty: false,
|
allowEmpty: false,
|
||||||
});
|
});
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
os.api('drive/files/update', {
|
os.api("drive/files/update", {
|
||||||
fileId: file.id,
|
fileId: file.id,
|
||||||
name: result,
|
name: result,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
this.$emit('changeName', file, result);
|
this.$emit("changeName", file, result);
|
||||||
file.name = result;
|
file.name = result;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async describe(file) {
|
async describe(file) {
|
||||||
os.popup(defineAsyncComponent(() => import('@/components/MkMediaCaption.vue')), {
|
os.popup(
|
||||||
|
defineAsyncComponent(
|
||||||
|
() => import("@/components/MkMediaCaption.vue")
|
||||||
|
),
|
||||||
|
{
|
||||||
title: i18n.ts.describeFile,
|
title: i18n.ts.describeFile,
|
||||||
input: {
|
input: {
|
||||||
placeholder: i18n.ts.inputNewDescription,
|
placeholder: i18n.ts.inputNewDescription,
|
||||||
default: file.comment !== null ? file.comment : '',
|
default: file.comment !== null ? file.comment : "",
|
||||||
},
|
},
|
||||||
image: file,
|
image: file,
|
||||||
}, {
|
},
|
||||||
done: result => {
|
{
|
||||||
|
done: (result) => {
|
||||||
if (!result || result.canceled) return;
|
if (!result || result.canceled) return;
|
||||||
let comment = result.result.length === 0 ? null : result.result;
|
let comment =
|
||||||
os.api('drive/files/update', {
|
result.result.length === 0 ? null : result.result;
|
||||||
|
os.api("drive/files/update", {
|
||||||
fileId: file.id,
|
fileId: file.id,
|
||||||
comment: comment,
|
comment: comment,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
file.comment = comment;
|
file.comment = comment;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}, 'closed');
|
},
|
||||||
|
"closed"
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
showFileMenu(file, ev: MouseEvent) {
|
showFileMenu(file, ev: MouseEvent) {
|
||||||
if (this.menu) return;
|
if (this.menu) return;
|
||||||
this.menu = os.popupMenu([{
|
this.menu = os
|
||||||
|
.popupMenu(
|
||||||
|
[
|
||||||
|
{
|
||||||
text: i18n.ts.renameFile,
|
text: i18n.ts.renameFile,
|
||||||
icon: 'ph-cursor-text ph-bold ph-lg',
|
icon: "ph-cursor-text ph-bold ph-lg",
|
||||||
action: () => { this.rename(file); },
|
action: () => {
|
||||||
}, {
|
this.rename(file);
|
||||||
text: file.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive,
|
},
|
||||||
icon: file.isSensitive ? 'ph-eye-slash ph-bold ph-lg' : 'ph-eye ph-bold ph-lg',
|
},
|
||||||
action: () => { this.toggleSensitive(file); },
|
{
|
||||||
}, {
|
text: file.isSensitive
|
||||||
|
? i18n.ts.unmarkAsSensitive
|
||||||
|
: i18n.ts.markAsSensitive,
|
||||||
|
icon: file.isSensitive
|
||||||
|
? "ph-eye-slash ph-bold ph-lg"
|
||||||
|
: "ph-eye ph-bold ph-lg",
|
||||||
|
action: () => {
|
||||||
|
this.toggleSensitive(file);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
text: i18n.ts.describeFile,
|
text: i18n.ts.describeFile,
|
||||||
icon: 'ph-cursor-text ph-bold ph-lg',
|
icon: "ph-cursor-text ph-bold ph-lg",
|
||||||
action: () => { this.describe(file); },
|
action: () => {
|
||||||
}, {
|
this.describe(file);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
text: i18n.ts.attachCancel,
|
text: i18n.ts.attachCancel,
|
||||||
icon: 'ph-circle-wavy-warning ph-bold ph-lg',
|
icon: "ph-circle-wavy-warning ph-bold ph-lg",
|
||||||
action: () => { this.detachMedia(file.id); },
|
action: () => {
|
||||||
}], ev.currentTarget ?? ev.target).then(() => this.menu = null);
|
this.detachMedia(file.id);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
ev.currentTarget ?? ev.target
|
||||||
|
)
|
||||||
|
.then(() => (this.menu = null));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -172,7 +220,7 @@ export default defineComponent({
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
background: rgba(17, 17, 17, .7);
|
background: rgba(17, 17, 17, 0.7);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
|
||||||
> .icon {
|
> .icon {
|
||||||
|
|
|
@ -1,14 +1,28 @@
|
||||||
<template>
|
<template>
|
||||||
<MkModal ref="modal" :prefer-type="'dialog'" @click="modal.close()" @closed="onModalClosed()">
|
<MkModal
|
||||||
<MkPostForm ref="form" style="margin: 0 auto auto auto;" v-bind="props" autofocus freeze-after-posted @posted="onPosted" @cancel="modal.close()" @esc="modal.close()"/>
|
ref="modal"
|
||||||
|
:prefer-type="'dialog'"
|
||||||
|
@click="modal.close()"
|
||||||
|
@closed="onModalClosed()"
|
||||||
|
>
|
||||||
|
<MkPostForm
|
||||||
|
ref="form"
|
||||||
|
style="margin: 0 auto auto auto"
|
||||||
|
v-bind="props"
|
||||||
|
autofocus
|
||||||
|
freeze-after-posted
|
||||||
|
@posted="onPosted"
|
||||||
|
@cancel="modal.close()"
|
||||||
|
@esc="modal.close()"
|
||||||
|
/>
|
||||||
</MkModal>
|
</MkModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import {} from "vue";
|
||||||
import * as misskey from 'calckey-js';
|
import * as misskey from "calckey-js";
|
||||||
import MkModal from '@/components/MkModal.vue';
|
import MkModal from "@/components/MkModal.vue";
|
||||||
import MkPostForm from '@/components/MkPostForm.vue';
|
import MkPostForm from "@/components/MkPostForm.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
reply?: misskey.entities.Note;
|
reply?: misskey.entities.Note;
|
||||||
|
@ -28,7 +42,7 @@ const props = defineProps<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'closed'): void;
|
(ev: "closed"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let modal = $shallowRef<InstanceType<typeof MkModal>>();
|
let modal = $shallowRef<InstanceType<typeof MkModal>>();
|
||||||
|
@ -41,6 +55,6 @@ function onPosted() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function onModalClosed() {
|
function onModalClosed() {
|
||||||
emit('closed');
|
emit("closed");
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -14,7 +14,10 @@
|
||||||
{{ i18n.ts.subscribePushNotification }}
|
{{ i18n.ts.subscribePushNotification }}
|
||||||
</MkButton>
|
</MkButton>
|
||||||
<MkButton
|
<MkButton
|
||||||
v-else-if="!showOnlyToRegister && ($i ? pushRegistrationInServer : pushSubscription)"
|
v-else-if="
|
||||||
|
!showOnlyToRegister &&
|
||||||
|
($i ? pushRegistrationInServer : pushSubscription)
|
||||||
|
"
|
||||||
type="button"
|
type="button"
|
||||||
:primary="false"
|
:primary="false"
|
||||||
:gradate="gradate"
|
:gradate="gradate"
|
||||||
|
@ -27,20 +30,34 @@
|
||||||
>
|
>
|
||||||
{{ i18n.ts.unsubscribePushNotification }}
|
{{ i18n.ts.unsubscribePushNotification }}
|
||||||
</MkButton>
|
</MkButton>
|
||||||
<MkButton v-else-if="$i && pushRegistrationInServer" disabled :rounded="rounded" :inline="inline" :wait="wait" :full="full">
|
<MkButton
|
||||||
|
v-else-if="$i && pushRegistrationInServer"
|
||||||
|
disabled
|
||||||
|
:rounded="rounded"
|
||||||
|
:inline="inline"
|
||||||
|
:wait="wait"
|
||||||
|
:full="full"
|
||||||
|
>
|
||||||
{{ i18n.ts.pushNotificationAlreadySubscribed }}
|
{{ i18n.ts.pushNotificationAlreadySubscribed }}
|
||||||
</MkButton>
|
</MkButton>
|
||||||
<MkButton v-else-if="!supported" disabled :rounded="rounded" :inline="inline" :wait="wait" :full="full">
|
<MkButton
|
||||||
|
v-else-if="!supported"
|
||||||
|
disabled
|
||||||
|
:rounded="rounded"
|
||||||
|
:inline="inline"
|
||||||
|
:wait="wait"
|
||||||
|
:full="full"
|
||||||
|
>
|
||||||
{{ i18n.ts.pushNotificationNotSupported }}
|
{{ i18n.ts.pushNotificationNotSupported }}
|
||||||
</MkButton>
|
</MkButton>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { $i, getAccounts } from '@/account';
|
import { $i, getAccounts } from "@/account";
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from "@/components/MkButton.vue";
|
||||||
import { instance } from '@/instance';
|
import { instance } from "@/instance";
|
||||||
import { api, apiWithDialog, promiseDialog } from '@/os';
|
import { api, apiWithDialog, promiseDialog } from "@/os";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
primary?: boolean;
|
primary?: boolean;
|
||||||
|
@ -62,29 +79,47 @@ let registration = $ref<ServiceWorkerRegistration | undefined>();
|
||||||
let supported = $ref(false);
|
let supported = $ref(false);
|
||||||
// If this browser has already subscribed to push notification
|
// If this browser has already subscribed to push notification
|
||||||
let pushSubscription = $ref<PushSubscription | null>(null);
|
let pushSubscription = $ref<PushSubscription | null>(null);
|
||||||
let pushRegistrationInServer = $ref<{ state?: string; key?: string; userId: string; endpoint: string; sendReadMessage: boolean; } | undefined>();
|
let pushRegistrationInServer = $ref<
|
||||||
|
| {
|
||||||
|
state?: string;
|
||||||
|
key?: string;
|
||||||
|
userId: string;
|
||||||
|
endpoint: string;
|
||||||
|
sendReadMessage: boolean;
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
|
>();
|
||||||
|
|
||||||
function subscribe() {
|
function subscribe() {
|
||||||
if (!registration || !supported || !instance.swPublickey) return;
|
if (!registration || !supported || !instance.swPublickey) return;
|
||||||
|
|
||||||
// SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters
|
// SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters
|
||||||
return promiseDialog(registration.pushManager.subscribe({
|
return promiseDialog(
|
||||||
|
registration.pushManager
|
||||||
|
.subscribe({
|
||||||
userVisibleOnly: true,
|
userVisibleOnly: true,
|
||||||
applicationServerKey: urlBase64ToUint8Array(instance.swPublickey),
|
applicationServerKey: urlBase64ToUint8Array(
|
||||||
|
instance.swPublickey
|
||||||
|
),
|
||||||
})
|
})
|
||||||
.then(async subscription => {
|
.then(
|
||||||
|
async (subscription) => {
|
||||||
pushSubscription = subscription;
|
pushSubscription = subscription;
|
||||||
|
|
||||||
// Register
|
// Register
|
||||||
pushRegistrationInServer = await api('sw/register', {
|
pushRegistrationInServer = await api("sw/register", {
|
||||||
endpoint: subscription.endpoint,
|
endpoint: subscription.endpoint,
|
||||||
auth: encode(subscription.getKey('auth')),
|
auth: encode(subscription.getKey("auth")),
|
||||||
publickey: encode(subscription.getKey('p256dh')),
|
publickey: encode(subscription.getKey("p256dh")),
|
||||||
});
|
});
|
||||||
}, async err => { // When subscribe failed
|
},
|
||||||
|
async (err) => {
|
||||||
|
// When subscribe failed
|
||||||
// 通知が許可されていなかったとき
|
// 通知が許可されていなかったとき
|
||||||
if (err?.name === 'NotAllowedError') {
|
if (err?.name === "NotAllowedError") {
|
||||||
console.info('User denied the notification permission request.');
|
console.info(
|
||||||
|
"User denied the notification permission request."
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,7 +128,11 @@ function subscribe() {
|
||||||
// そのサブスクリプションを解除しておく
|
// そのサブスクリプションを解除しておく
|
||||||
// (これは実行されなさそうだけど、おまじない的に古い実装から残してある)
|
// (これは実行されなさそうだけど、おまじない的に古い実装から残してある)
|
||||||
await unsubscribe();
|
await unsubscribe();
|
||||||
}), null, null);
|
}
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function unsubscribe() {
|
async function unsubscribe() {
|
||||||
|
@ -105,13 +144,13 @@ async function unsubscribe() {
|
||||||
pushRegistrationInServer = undefined;
|
pushRegistrationInServer = undefined;
|
||||||
|
|
||||||
if ($i && accounts.length >= 2) {
|
if ($i && accounts.length >= 2) {
|
||||||
apiWithDialog('sw/unregister', {
|
apiWithDialog("sw/unregister", {
|
||||||
i: $i.token,
|
i: $i.token,
|
||||||
endpoint,
|
endpoint,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
pushSubscription.unsubscribe();
|
pushSubscription.unsubscribe();
|
||||||
apiWithDialog('sw/unregister', {
|
apiWithDialog("sw/unregister", {
|
||||||
endpoint,
|
endpoint,
|
||||||
});
|
});
|
||||||
pushSubscription = null;
|
pushSubscription = null;
|
||||||
|
@ -127,10 +166,10 @@ function encode(buffer: ArrayBuffer | null) {
|
||||||
* @param base64String base64 string
|
* @param base64String base64 string
|
||||||
*/
|
*/
|
||||||
function urlBase64ToUint8Array(base64String: string): Uint8Array {
|
function urlBase64ToUint8Array(base64String: string): Uint8Array {
|
||||||
const padding = '='.repeat((4 - base64String.length % 4) % 4);
|
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
|
||||||
const base64 = (base64String + padding)
|
const base64 = (base64String + padding)
|
||||||
.replace(/-/g, '+')
|
.replace(/-/g, "+")
|
||||||
.replace(/_/g, '/');
|
.replace(/_/g, "/");
|
||||||
|
|
||||||
const rawData = window.atob(base64);
|
const rawData = window.atob(base64);
|
||||||
const outputArray = new Uint8Array(rawData.length);
|
const outputArray = new Uint8Array(rawData.length);
|
||||||
|
@ -144,16 +183,16 @@ function urlBase64ToUint8Array(base64String: string): Uint8Array {
|
||||||
if (navigator.serviceWorker == null) {
|
if (navigator.serviceWorker == null) {
|
||||||
// TODO: よしなに?
|
// TODO: よしなに?
|
||||||
} else {
|
} else {
|
||||||
navigator.serviceWorker.ready.then(async swr => {
|
navigator.serviceWorker.ready.then(async (swr) => {
|
||||||
registration = swr;
|
registration = swr;
|
||||||
|
|
||||||
pushSubscription = await registration.pushManager.getSubscription();
|
pushSubscription = await registration.pushManager.getSubscription();
|
||||||
|
|
||||||
if (instance.swPublickey && ('PushManager' in window) && $i && $i.token) {
|
if (instance.swPublickey && "PushManager" in window && $i && $i.token) {
|
||||||
supported = true;
|
supported = true;
|
||||||
|
|
||||||
if (pushSubscription) {
|
if (pushSubscription) {
|
||||||
const res = await api('sw/show-registration', {
|
const res = await api("sw/show-registration", {
|
||||||
endpoint: pushSubscription.endpoint,
|
endpoint: pushSubscription.endpoint,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -10,18 +10,22 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from "vue";
|
||||||
import type { Note } from 'calckey-js/built/entities';
|
import type { Note } from "calckey-js/built/entities";
|
||||||
import { pleaseLogin } from '@/scripts/please-login';
|
import { pleaseLogin } from "@/scripts/please-login";
|
||||||
import * as os from '@/os';
|
import * as os from "@/os";
|
||||||
import { $i } from '@/account';
|
import { $i } from "@/account";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
note: Note;
|
note: Note;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const canRenote = computed(() => ['public', 'home'].includes(props.note.visibility) || props.note.userId === $i?.id);
|
const canRenote = computed(
|
||||||
|
() =>
|
||||||
|
["public", "home"].includes(props.note.visibility) ||
|
||||||
|
props.note.userId === $i?.id
|
||||||
|
);
|
||||||
|
|
||||||
function quote(): void {
|
function quote(): void {
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
|
|
|
@ -11,21 +11,47 @@
|
||||||
<MkSpacer :margin-min="20" :margin-max="28">
|
<MkSpacer :margin-min="20" :margin-max="28">
|
||||||
<div v-if="note" class="_gaps">
|
<div v-if="note" class="_gaps">
|
||||||
<div v-if="reactions.length === 0" class="_fullinfo">
|
<div v-if="reactions.length === 0" class="_fullinfo">
|
||||||
<img src="/static-assets/badges/info.png" class="_ghost" alt="Info"/>
|
<img
|
||||||
|
src="/static-assets/badges/info.png"
|
||||||
|
class="_ghost"
|
||||||
|
alt="Info"
|
||||||
|
/>
|
||||||
<div>{{ i18n.ts.nothing }}</div>
|
<div>{{ i18n.ts.nothing }}</div>
|
||||||
</div>
|
</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div :class="$style.tabs">
|
<div :class="$style.tabs">
|
||||||
<button v-for="reaction in reactions" :key="reaction" :class="[$style.tab, { [$style.tabActive]: tab === reaction }]" class="_button" @click="tab = reaction">
|
<button
|
||||||
|
v-for="reaction in reactions"
|
||||||
|
:key="reaction"
|
||||||
|
:class="[
|
||||||
|
$style.tab,
|
||||||
|
{ [$style.tabActive]: tab === reaction },
|
||||||
|
]"
|
||||||
|
class="_button"
|
||||||
|
@click="tab = reaction"
|
||||||
|
>
|
||||||
<MkReactionIcon
|
<MkReactionIcon
|
||||||
ref="reactionRef"
|
ref="reactionRef"
|
||||||
:reaction="reaction ? reaction.replace(/^:(\w+):$/, ':$1@.:') : reaction"
|
:reaction="
|
||||||
|
reaction
|
||||||
|
? reaction.replace(
|
||||||
|
/^:(\w+):$/,
|
||||||
|
':$1@.:'
|
||||||
|
)
|
||||||
|
: reaction
|
||||||
|
"
|
||||||
:custom-emojis="note.emojis"
|
:custom-emojis="note.emojis"
|
||||||
/>
|
/>
|
||||||
<span style="margin-left: 4px;">{{ note.reactions[reaction] }}</span>
|
<span style="margin-left: 4px">{{
|
||||||
|
note.reactions[reaction]
|
||||||
|
}}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<MkA v-for="user in users" :key="user.id" :to="userPage(user)">
|
<MkA
|
||||||
|
v-for="user in users"
|
||||||
|
:key="user.id"
|
||||||
|
:to="userPage(user)"
|
||||||
|
>
|
||||||
<MkUserCardMini :user="user" :with-chart="false" />
|
<MkUserCardMini :user="user" :with-chart="false" />
|
||||||
</MkA>
|
</MkA>
|
||||||
</template>
|
</template>
|
||||||
|
@ -38,21 +64,21 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, watch } from 'vue';
|
import { onMounted, watch } from "vue";
|
||||||
import * as misskey from 'calckey-js';
|
import * as misskey from "calckey-js";
|
||||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
import MkModalWindow from "@/components/MkModalWindow.vue";
|
||||||
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
import MkReactionIcon from "@/components/MkReactionIcon.vue";
|
||||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
import MkUserCardMini from "@/components/MkUserCardMini.vue";
|
||||||
import { userPage } from '@/filters/user';
|
import { userPage } from "@/filters/user";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
import * as os from '@/os';
|
import * as os from "@/os";
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'closed'): void,
|
(ev: "closed"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
noteId: misskey.entities.Note['id'];
|
noteId: misskey.entities.Note["id"];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
|
const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||||
|
@ -63,17 +89,17 @@ let reactions = $ref<string[]>();
|
||||||
let users = $ref();
|
let users = $ref();
|
||||||
|
|
||||||
watch($$(tab), async () => {
|
watch($$(tab), async () => {
|
||||||
const res = await os.api('notes/reactions', {
|
const res = await os.api("notes/reactions", {
|
||||||
noteId: props.noteId,
|
noteId: props.noteId,
|
||||||
type: tab,
|
type: tab,
|
||||||
limit: 30,
|
limit: 30,
|
||||||
});
|
});
|
||||||
|
|
||||||
users = res.map(x => x.user);
|
users = res.map((x) => x.user);
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
os.api('notes/show', {
|
os.api("notes/show", {
|
||||||
noteId: props.noteId,
|
noteId: props.noteId,
|
||||||
}).then((res) => {
|
}).then((res) => {
|
||||||
reactions = Object.keys(res.reactions);
|
reactions = Object.keys(res.reactions);
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<MkEmoji :emoji="reaction" :custom-emojis="customEmojis || []" :is-reaction="true" :normal="true" :no-style="noStyle"/>
|
<MkEmoji
|
||||||
|
:emoji="reaction"
|
||||||
|
:custom-emojis="customEmojis || []"
|
||||||
|
:is-reaction="true"
|
||||||
|
:normal="true"
|
||||||
|
:no-style="noStyle"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import {} from "vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
reaction: string;
|
reaction: string;
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue