merge: upstream (1)
This commit is contained in:
commit
db012fc8c3
258 changed files with 18802 additions and 7557 deletions
BIN
packages/frontend/assets/reversi/logo.png
Normal file
BIN
packages/frontend/assets/reversi/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 94 KiB |
BIN
packages/frontend/assets/reversi/lose.mp3
Normal file
BIN
packages/frontend/assets/reversi/lose.mp3
Normal file
Binary file not shown.
BIN
packages/frontend/assets/reversi/matched.mp3
Normal file
BIN
packages/frontend/assets/reversi/matched.mp3
Normal file
Binary file not shown.
BIN
packages/frontend/assets/reversi/put.mp3
Normal file
BIN
packages/frontend/assets/reversi/put.mp3
Normal file
Binary file not shown.
BIN
packages/frontend/assets/reversi/stone_b.png
Normal file
BIN
packages/frontend/assets/reversi/stone_b.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
BIN
packages/frontend/assets/reversi/stone_w.png
Normal file
BIN
packages/frontend/assets/reversi/stone_w.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
BIN
packages/frontend/assets/reversi/win.mp3
Normal file
BIN
packages/frontend/assets/reversi/win.mp3
Normal file
Binary file not shown.
|
@ -40,8 +40,9 @@
|
|||
"chartjs-chart-matrix": "2.0.1",
|
||||
"chartjs-plugin-gradient": "0.6.1",
|
||||
"chartjs-plugin-zoom": "2.0.1",
|
||||
"chromatic": "10.1.0",
|
||||
"chromatic": "10.3.1",
|
||||
"compare-versions": "6.1.0",
|
||||
"crc-32": "^1.2.2",
|
||||
"cropperjs": "2.0.0-beta.4",
|
||||
"date-fns": "2.30.0",
|
||||
"escape-regexp": "0.0.1",
|
||||
|
@ -54,16 +55,17 @@
|
|||
"katex": "0.16.9",
|
||||
"matter-js": "0.19.0",
|
||||
"misskey-js": "workspace:*",
|
||||
"misskey-reversi": "workspace:*",
|
||||
"misskey-bubble-game": "workspace:*",
|
||||
"photoswipe": "5.4.3",
|
||||
"punycode": "2.3.1",
|
||||
"rollup": "4.9.1",
|
||||
"rollup": "4.9.6",
|
||||
"sanitize-html": "2.11.0",
|
||||
"sass": "1.69.5",
|
||||
"seedrandom": "^3.0.5",
|
||||
"sass": "1.70.0",
|
||||
"shiki": "0.14.7",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"textarea-caret": "3.1.0",
|
||||
"three": "0.159.0",
|
||||
"three": "0.160.0",
|
||||
"throttle-debounce": "5.0.0",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tsc-alias": "1.8.8",
|
||||
|
@ -71,70 +73,70 @@
|
|||
"typescript": "5.3.3",
|
||||
"uuid": "9.0.1",
|
||||
"v-code-diff": "1.7.2",
|
||||
"vite": "5.0.10",
|
||||
"vue": "3.4.3",
|
||||
"vite": "5.0.12",
|
||||
"vue": "3.4.15",
|
||||
"vuedraggable": "next"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@misskey-dev/eslint-plugin": "^1.0.0",
|
||||
"@misskey-dev/summaly": "^5.0.3",
|
||||
"@storybook/addon-actions": "7.6.5",
|
||||
"@storybook/addon-essentials": "7.6.5",
|
||||
"@storybook/addon-interactions": "7.6.5",
|
||||
"@storybook/addon-links": "7.6.5",
|
||||
"@storybook/addon-storysource": "7.6.5",
|
||||
"@storybook/addons": "7.6.5",
|
||||
"@storybook/blocks": "7.6.5",
|
||||
"@storybook/core-events": "7.6.5",
|
||||
"@storybook/addon-actions": "7.6.10",
|
||||
"@storybook/addon-essentials": "7.6.10",
|
||||
"@storybook/addon-interactions": "7.6.10",
|
||||
"@storybook/addon-links": "7.6.10",
|
||||
"@storybook/addon-storysource": "7.6.10",
|
||||
"@storybook/addons": "7.6.10",
|
||||
"@storybook/blocks": "7.6.10",
|
||||
"@storybook/core-events": "7.6.10",
|
||||
"@storybook/jest": "0.2.3",
|
||||
"@storybook/manager-api": "7.6.5",
|
||||
"@storybook/preview-api": "7.6.5",
|
||||
"@storybook/react": "7.6.5",
|
||||
"@storybook/react-vite": "7.6.5",
|
||||
"@storybook/manager-api": "7.6.10",
|
||||
"@storybook/preview-api": "7.6.10",
|
||||
"@storybook/react": "7.6.10",
|
||||
"@storybook/react-vite": "7.6.10",
|
||||
"@storybook/testing-library": "0.2.2",
|
||||
"@storybook/theming": "7.6.5",
|
||||
"@storybook/types": "7.6.5",
|
||||
"@storybook/vue3": "7.6.5",
|
||||
"@storybook/vue3-vite": "7.6.5",
|
||||
"@storybook/theming": "7.6.10",
|
||||
"@storybook/types": "7.6.10",
|
||||
"@storybook/vue3": "7.6.10",
|
||||
"@storybook/vue3-vite": "7.6.10",
|
||||
"@testing-library/vue": "8.0.1",
|
||||
"@types/escape-regexp": "0.0.3",
|
||||
"@types/estree": "1.0.5",
|
||||
"@types/matter-js": "0.19.5",
|
||||
"@types/matter-js": "0.19.6",
|
||||
"@types/micromatch": "4.0.6",
|
||||
"@types/node": "20.10.5",
|
||||
"@types/node": "20.11.5",
|
||||
"@types/punycode": "2.1.3",
|
||||
"@types/sanitize-html": "2.9.5",
|
||||
"@types/throttle-debounce": "5.0.2",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@types/uuid": "9.0.7",
|
||||
"@types/ws": "8.5.10",
|
||||
"@typescript-eslint/eslint-plugin": "6.14.0",
|
||||
"@typescript-eslint/parser": "6.14.0",
|
||||
"@vitest/coverage-v8": "0.34.6",
|
||||
"@vue/runtime-core": "3.4.3",
|
||||
"acorn": "8.11.2",
|
||||
"@typescript-eslint/eslint-plugin": "6.19.0",
|
||||
"@typescript-eslint/parser": "6.19.0",
|
||||
"@vitest/coverage-v8": "1.2.1",
|
||||
"@vue/runtime-core": "3.4.15",
|
||||
"acorn": "8.11.3",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "13.6.1",
|
||||
"cypress": "13.6.3",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-vue": "9.19.2",
|
||||
"eslint-plugin-vue": "9.20.1",
|
||||
"fast-glob": "3.3.2",
|
||||
"happy-dom": "10.0.3",
|
||||
"intersection-observer": "0.12.2",
|
||||
"micromatch": "4.0.5",
|
||||
"msw": "1.3.2",
|
||||
"msw": "2.1.2",
|
||||
"msw-storybook-addon": "1.10.0",
|
||||
"nodemon": "3.0.2",
|
||||
"prettier": "3.1.1",
|
||||
"nodemon": "3.0.3",
|
||||
"prettier": "3.2.4",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"start-server-and-test": "2.0.3",
|
||||
"storybook": "7.6.5",
|
||||
"storybook": "7.6.10",
|
||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||
"vite-plugin-turbosnap": "1.0.3",
|
||||
"vitest": "0.34.6",
|
||||
"vitest": "1.2.1",
|
||||
"vitest-fetch-mock": "0.2.2",
|
||||
"vue-eslint-parser": "9.3.2",
|
||||
"vue-eslint-parser": "9.4.0",
|
||||
"vue-tsc": "1.8.27"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -75,9 +75,18 @@ export async function mainBoot() {
|
|||
|
||||
if (defaultStore.state.enableSeasonalScreenEffect) {
|
||||
const month = new Date().getMonth() + 1;
|
||||
if (month === 12 || month === 1) {
|
||||
const SnowfallEffect = (await import('@/scripts/snowfall-effect.js')).SnowfallEffect;
|
||||
new SnowfallEffect().render();
|
||||
if (defaultStore.state.hemisphere === 'S') {
|
||||
// ▼南半球
|
||||
if (month === 7 || month === 8) {
|
||||
const SnowfallEffect = (await import('@/scripts/snowfall-effect.js')).SnowfallEffect;
|
||||
new SnowfallEffect().render();
|
||||
}
|
||||
} else {
|
||||
// ▼北半球
|
||||
if (month === 12 || month === 1) {
|
||||
const SnowfallEffect = (await import('@/scripts/snowfall-effect.js')).SnowfallEffect;
|
||||
new SnowfallEffect().render();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -203,7 +212,7 @@ export async function mainBoot() {
|
|||
const lastUsedDate = parseInt(lastUsed, 10);
|
||||
// 二時間以上前なら
|
||||
if (Date.now() - lastUsedDate > 1000 * 60 * 60 * 2) {
|
||||
toast(i18n.t('welcomeBackWithName', {
|
||||
toast(i18n.tsx.welcomeBackWithName({
|
||||
name: $i.name || $i.username,
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ async function ok() {
|
|||
const confirm = await os.confirm({
|
||||
type: 'question',
|
||||
title: i18n.ts._announcement.readConfirmTitle,
|
||||
text: i18n.t('_announcement.readConfirmText', { title: props.announcement.title }),
|
||||
text: i18n.tsx._announcement.readConfirmText({ title: props.announcement.title }),
|
||||
});
|
||||
if (confirm.canceled) return;
|
||||
}
|
||||
|
|
|
@ -35,6 +35,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<span>{{ tag }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
<ol v-else-if="mfmParams.length > 0" ref="suggests" :class="$style.list">
|
||||
<li v-for="param in mfmParams" tabindex="-1" :class="$style.item" @click="complete(type, q.params.toSpliced(-1, 1, param).join(','))" @keydown="onKeydown">
|
||||
<span>{{ param }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -51,7 +56,7 @@ import { emojilist, getEmojiName } from '@/scripts/emojilist.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { customEmojis } from '@/custom-emojis.js';
|
||||
import { MFM_TAGS } from '@/const.js';
|
||||
import { MFM_TAGS, MFM_PARAMS } from '@/const.js';
|
||||
|
||||
type EmojiDef = {
|
||||
emoji: string;
|
||||
|
@ -130,7 +135,7 @@ export default {
|
|||
<script lang="ts" setup>
|
||||
const props = defineProps<{
|
||||
type: string;
|
||||
q: string | null;
|
||||
q: any;
|
||||
textarea: HTMLTextAreaElement;
|
||||
close: () => void;
|
||||
x: number;
|
||||
|
@ -151,6 +156,7 @@ const hashtags = ref<any[]>([]);
|
|||
const emojis = ref<(EmojiDef)[]>([]);
|
||||
const items = ref<Element[] | HTMLCollection>([]);
|
||||
const mfmTags = ref<string[]>([]);
|
||||
const mfmParams = ref<string[]>([]);
|
||||
const select = ref(-1);
|
||||
const zIndex = os.claimZIndex('high');
|
||||
|
||||
|
@ -251,6 +257,13 @@ function exec() {
|
|||
}
|
||||
|
||||
mfmTags.value = MFM_TAGS.filter(tag => tag.startsWith(props.q ?? ''));
|
||||
} else if (props.type === 'mfmParam') {
|
||||
if (props.q.params.at(-1) === '') {
|
||||
mfmParams.value = MFM_PARAMS[props.q.tag] ?? [];
|
||||
return;
|
||||
}
|
||||
|
||||
mfmParams.value = MFM_PARAMS[props.q.tag].filter(param => param.startsWith(props.q.params.at(-1) ?? ''));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,48 +4,64 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<Suspense>
|
||||
<template #fallback>
|
||||
<MkLoading v-if="!inline ?? true"/>
|
||||
</template>
|
||||
<code v-if="inline" :class="$style.codeInlineRoot">{{ code }}</code>
|
||||
<XCode v-else-if="show && lang" :code="code" :lang="lang"/>
|
||||
<pre v-else-if="show" :class="$style.codeBlockFallbackRoot"><code :class="$style.codeBlockFallbackCode">{{ code }}</code></pre>
|
||||
<button v-else :class="$style.codePlaceholderRoot" @click="show = true">
|
||||
<div :class="$style.codePlaceholderContainer">
|
||||
<div><i class="ph-code ph-bold ph-lg"></i> {{ i18n.ts.code }}</div>
|
||||
<div>{{ i18n.ts.clickToShow }}</div>
|
||||
</div>
|
||||
<div :class="$style.codeBlockRoot">
|
||||
<button :class="$style.codeBlockCopyButton" class="_button" @click="copy">
|
||||
<i class="ph-copy ph-bold ph-lg"></i>
|
||||
</button>
|
||||
</Suspense>
|
||||
<Suspense>
|
||||
<template #fallback>
|
||||
<MkLoading />
|
||||
</template>
|
||||
<XCode v-if="show && lang" :code="code" :lang="lang"/>
|
||||
<pre v-else-if="show" :class="$style.codeBlockFallbackRoot"><code :class="$style.codeBlockFallbackCode">{{ code }}</code></pre>
|
||||
<button v-else :class="$style.codePlaceholderRoot" @click="show = true">
|
||||
<div :class="$style.codePlaceholderContainer">
|
||||
<div><i class="ph-code ph-bold ph-lg"></i> {{ i18n.ts.code }}</div>
|
||||
<div>{{ i18n.ts.clickToShow }}</div>
|
||||
</div>
|
||||
</button>
|
||||
</Suspense>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, ref } from 'vue';
|
||||
import * as os from '@/os.js';
|
||||
import MkLoading from '@/components/global/MkLoading.vue';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
code: string;
|
||||
lang?: string;
|
||||
inline?: boolean;
|
||||
}>();
|
||||
|
||||
const show = ref(!defaultStore.state.dataSaver.code);
|
||||
|
||||
const XCode = defineAsyncComponent(() => import('@/components/MkCode.core.vue'));
|
||||
|
||||
function copy() {
|
||||
copyToClipboard(props.code);
|
||||
os.success();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.codeInlineRoot {
|
||||
display: inline-block;
|
||||
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
|
||||
overflow-wrap: anywhere;
|
||||
.codeBlockRoot {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.codeBlockCopyButton {
|
||||
color: #D4D4D4;
|
||||
background: #1E1E1E;
|
||||
padding: .1em;
|
||||
border-radius: .3em;
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
opacity: 0.5;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.codeBlockFallbackRoot {
|
||||
|
|
26
packages/frontend/src/components/MkCodeInline.vue
Normal file
26
packages/frontend/src/components/MkCodeInline.vue
Normal file
|
@ -0,0 +1,26 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<code :class="$style.root">{{ code }}</code>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps<{
|
||||
code: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.root {
|
||||
display: inline-block;
|
||||
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
|
||||
overflow-wrap: anywhere;
|
||||
color: #D4D4D4;
|
||||
background: #1E1E1E;
|
||||
padding: .1em;
|
||||
border-radius: .3em;
|
||||
}
|
||||
</style>
|
|
@ -10,9 +10,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkSpacer>
|
||||
<div style="display: flex; flex-direction: column; gap: 1em;">
|
||||
<div :class="$style.emojiImgWrapper">
|
||||
<MkCustomEmoji :name="emoji.name" :normal="true" style="height: 100%;"></MkCustomEmoji>
|
||||
<MkCustomEmoji :name="emoji.name" :normal="true" :useOriginalSize="true" style="height: 100%;"></MkCustomEmoji>
|
||||
</div>
|
||||
<MkKeyValue>
|
||||
<MkKeyValue :copy="`:${emoji.name}:`">
|
||||
<template #key>{{ i18n.ts.name }}</template>
|
||||
<template #value>{{ emoji.name }}</template>
|
||||
</MkKeyValue>
|
||||
|
@ -41,12 +41,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkKeyValue>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.license }}</template>
|
||||
<template #value>{{ emoji.license ?? i18n.ts.none }}</template>
|
||||
<template #value><Mfm :text="emoji.license ?? i18n.ts.none" /></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue :copy="emoji.url">
|
||||
<template #key>{{ i18n.ts.emojiUrl }}</template>
|
||||
<template #value>
|
||||
<a :href="emoji.url" target="_blank">{{ emoji.url }}</a>
|
||||
<MkLink :url="emoji.url" target="_blank">{{ emoji.url }}</MkLink>
|
||||
</template>
|
||||
</MkKeyValue>
|
||||
</div>
|
||||
|
@ -61,6 +61,7 @@ import { defineProps, shallowRef } from 'vue';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import MkLink from './MkLink.vue';
|
||||
const props = defineProps<{
|
||||
emoji: Misskey.entities.EmojiDetailed,
|
||||
}>();
|
||||
|
@ -94,6 +95,7 @@ const cancel = () => {
|
|||
|
||||
.alias {
|
||||
display: inline-block;
|
||||
word-break: break-all;
|
||||
padding: 3px 10px;
|
||||
background-color: var(--X5);
|
||||
border: solid 1px var(--divider);
|
||||
|
|
|
@ -41,9 +41,9 @@ const emit = defineEmits<{
|
|||
|
||||
const label = computed(() => {
|
||||
return concat([
|
||||
props.text ? [i18n.t('_cw.chars', { count: props.text.length })] : [],
|
||||
props.text ? [i18n.tsx._cw.chars({ count: props.text.length })] : [],
|
||||
props.renote ? [i18n.ts.quote] : [],
|
||||
props.files.length !== 0 ? [i18n.t('_cw.files', { count: props.files.length })] : [],
|
||||
props.files.length !== 0 ? [i18n.tsx._cw.files({ count: props.files.length })] : [],
|
||||
props.poll != null ? [i18n.ts.poll] : [],
|
||||
] as string[][]).join(' / ');
|
||||
});
|
||||
|
|
|
@ -46,7 +46,7 @@ export default defineComponent({
|
|||
function getDateText(time: string) {
|
||||
const date = new Date(time).getDate();
|
||||
const month = new Date(time).getMonth() + 1;
|
||||
return i18n.t('monthAndDay', {
|
||||
return i18n.tsx.monthAndDay({
|
||||
month: month.toString(),
|
||||
day: date.toString(),
|
||||
});
|
||||
|
|
|
@ -30,8 +30,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder || undefined" :autocomplete="input.autocomplete" @keydown="onInputKeydown">
|
||||
<template v-if="input.type === 'password'" #prefix><i class="ph-lock ph-bold ph-lg"></i></template>
|
||||
<template #caption>
|
||||
<span v-if="okButtonDisabledReason === 'charactersExceeded'" v-text="i18n.t('_dialog.charactersExceeded', { current: (inputValue as string).length, max: input.maxLength ?? 'NaN' })"/>
|
||||
<span v-else-if="okButtonDisabledReason === 'charactersBelow'" v-text="i18n.t('_dialog.charactersBelow', { current: (inputValue as string).length, min: input.minLength ?? 'NaN' })"/>
|
||||
<span v-if="okButtonDisabledReason === 'charactersExceeded'" v-text="i18n.tsx._dialog.charactersExceeded({ current: (inputValue as string).length, max: input.maxLength ?? 'NaN' })"/>
|
||||
<span v-else-if="okButtonDisabledReason === 'charactersBelow'" v-text="i18n.tsx._dialog.charactersBelow({ current: (inputValue as string).length, min: input.minLength ?? 'NaN' })"/>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkSelect v-if="select" v-model="selectedValue" autofocus>
|
||||
|
|
|
@ -82,8 +82,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkButton v-show="moreFiles" ref="loadMoreFiles" @click="fetchMoreFiles">{{ i18n.ts.loadMore }}</MkButton>
|
||||
</div>
|
||||
<div v-if="files.length == 0 && folders.length == 0 && !fetching" :class="$style.empty">
|
||||
<div v-if="draghover">{{ i18n.t('empty-draghover') }}</div>
|
||||
<div v-if="!draghover && folder == null"><strong>{{ i18n.ts.emptyDrive }}</strong><br/>{{ i18n.t('empty-drive-description') }}</div>
|
||||
<div v-if="draghover">{{ i18n.ts['empty-draghover'] }}</div>
|
||||
<div v-if="!draghover && folder == null"><strong>{{ i18n.ts.emptyDrive }}</strong><br/>{{ i18n.ts['empty-drive-description'] }}</div>
|
||||
<div v-if="!draghover && folder != null">{{ i18n.ts.emptyFolder }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -84,7 +84,7 @@ async function onClick() {
|
|||
if (isFollowing.value) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('unfollowConfirm', { name: props.user.name || props.user.username }),
|
||||
text: i18n.tsx.unfollowConfirm({ name: props.user.name || props.user.username }),
|
||||
});
|
||||
|
||||
if (canceled) return;
|
||||
|
|
211
packages/frontend/src/components/MkHorizontalSwipe.vue
Normal file
211
packages/frontend/src/components/MkHorizontalSwipe.vue
Normal file
|
@ -0,0 +1,211 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="rootEl"
|
||||
:class="[$style.transitionRoot]"
|
||||
@touchstart.passive="touchStart"
|
||||
@touchmove.passive="touchMove"
|
||||
@touchend.passive="touchEnd"
|
||||
>
|
||||
<Transition
|
||||
:class="[$style.transitionChildren, { [$style.swiping]: isSwipingForClass }]"
|
||||
:enterActiveClass="$style.swipeAnimation_enterActive"
|
||||
:leaveActiveClass="$style.swipeAnimation_leaveActive"
|
||||
:enterFromClass="transitionName === 'swipeAnimationLeft' ? $style.swipeAnimationLeft_enterFrom : $style.swipeAnimationRight_enterFrom"
|
||||
:leaveToClass="transitionName === 'swipeAnimationLeft' ? $style.swipeAnimationLeft_leaveTo : $style.swipeAnimationRight_leaveTo"
|
||||
:style="`--swipe: ${pullDistance}px;`"
|
||||
>
|
||||
<!-- 【注意】slot内の最上位要素に動的にkeyを設定すること -->
|
||||
<!-- 各最上位要素にユニークなkeyの指定がないとTransitionがうまく動きません -->
|
||||
<slot></slot>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, shallowRef, computed, nextTick, watch } from 'vue';
|
||||
import type { Tab } from '@/components/global/MkPageHeader.tabs.vue';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
const rootEl = shallowRef<HTMLDivElement>();
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const tabModel = defineModel<string>('tab');
|
||||
|
||||
const props = defineProps<{
|
||||
tabs: Tab[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'swiped', newKey: string, direction: 'left' | 'right'): void;
|
||||
}>();
|
||||
|
||||
// ▼ しきい値 ▼ //
|
||||
|
||||
// スワイプと判定される最小の距離
|
||||
const MIN_SWIPE_DISTANCE = 50;
|
||||
|
||||
// スワイプ時の動作を発火する最小の距離
|
||||
const SWIPE_DISTANCE_THRESHOLD = 125;
|
||||
|
||||
// スワイプを中断するY方向の移動距離
|
||||
const SWIPE_ABORT_Y_THRESHOLD = 75;
|
||||
|
||||
// スワイプできる最大の距離
|
||||
const MAX_SWIPE_DISTANCE = 150;
|
||||
|
||||
// ▲ しきい値 ▲ //
|
||||
|
||||
let startScreenX: number | null = null;
|
||||
let startScreenY: number | null = null;
|
||||
|
||||
const currentTabIndex = computed(() => props.tabs.findIndex(tab => tab.key === tabModel.value));
|
||||
|
||||
const pullDistance = ref(0);
|
||||
const isSwiping = ref(false);
|
||||
const isSwipingForClass = ref(false);
|
||||
let swipeAborted = false;
|
||||
|
||||
function touchStart(event: TouchEvent) {
|
||||
if (!defaultStore.reactiveState.enableHorizontalSwipe.value) return;
|
||||
|
||||
if (event.touches.length !== 1) return;
|
||||
|
||||
startScreenX = event.touches[0].screenX;
|
||||
startScreenY = event.touches[0].screenY;
|
||||
}
|
||||
|
||||
function touchMove(event: TouchEvent) {
|
||||
if (!defaultStore.reactiveState.enableHorizontalSwipe.value) return;
|
||||
|
||||
if (event.touches.length !== 1) return;
|
||||
|
||||
if (startScreenX == null || startScreenY == null) return;
|
||||
|
||||
if (swipeAborted) return;
|
||||
|
||||
let distanceX = event.touches[0].screenX - startScreenX;
|
||||
let distanceY = event.touches[0].screenY - startScreenY;
|
||||
|
||||
if (Math.abs(distanceY) > SWIPE_ABORT_Y_THRESHOLD) {
|
||||
swipeAborted = true;
|
||||
|
||||
pullDistance.value = 0;
|
||||
isSwiping.value = false;
|
||||
setTimeout(() => {
|
||||
isSwipingForClass.value = false;
|
||||
}, 400);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (Math.abs(distanceX) < MIN_SWIPE_DISTANCE) return;
|
||||
if (Math.abs(distanceX) > MAX_SWIPE_DISTANCE) return;
|
||||
|
||||
if (currentTabIndex.value === 0 || props.tabs[currentTabIndex.value - 1].onClick) {
|
||||
distanceX = Math.min(distanceX, 0);
|
||||
}
|
||||
if (currentTabIndex.value === props.tabs.length - 1 || props.tabs[currentTabIndex.value + 1].onClick) {
|
||||
distanceX = Math.max(distanceX, 0);
|
||||
}
|
||||
if (distanceX === 0) return;
|
||||
|
||||
isSwiping.value = true;
|
||||
isSwipingForClass.value = true;
|
||||
nextTick(() => {
|
||||
// グリッチを控えるため、1.5px以上の差がないと更新しない
|
||||
if (Math.abs(distanceX - pullDistance.value) < 1.5) return;
|
||||
pullDistance.value = distanceX;
|
||||
});
|
||||
}
|
||||
|
||||
function touchEnd(event: TouchEvent) {
|
||||
if (swipeAborted) {
|
||||
swipeAborted = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!defaultStore.reactiveState.enableHorizontalSwipe.value) return;
|
||||
|
||||
if (event.touches.length !== 0) return;
|
||||
|
||||
if (startScreenX == null) return;
|
||||
|
||||
if (!isSwiping.value) return;
|
||||
|
||||
const distance = event.changedTouches[0].screenX - startScreenX;
|
||||
|
||||
if (Math.abs(distance) > SWIPE_DISTANCE_THRESHOLD) {
|
||||
if (distance > 0) {
|
||||
if (props.tabs[currentTabIndex.value - 1] && !props.tabs[currentTabIndex.value - 1].onClick) {
|
||||
tabModel.value = props.tabs[currentTabIndex.value - 1].key;
|
||||
emit('swiped', props.tabs[currentTabIndex.value - 1].key, 'right');
|
||||
}
|
||||
} else {
|
||||
if (props.tabs[currentTabIndex.value + 1] && !props.tabs[currentTabIndex.value + 1].onClick) {
|
||||
tabModel.value = props.tabs[currentTabIndex.value + 1].key;
|
||||
emit('swiped', props.tabs[currentTabIndex.value + 1].key, 'left');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pullDistance.value = 0;
|
||||
isSwiping.value = false;
|
||||
window.setTimeout(() => {
|
||||
isSwipingForClass.value = false;
|
||||
}, 400);
|
||||
}
|
||||
|
||||
const transitionName = ref<'swipeAnimationLeft' | 'swipeAnimationRight' | undefined>(undefined);
|
||||
|
||||
watch(tabModel, (newTab, oldTab) => {
|
||||
const newIndex = props.tabs.findIndex(tab => tab.key === newTab);
|
||||
const oldIndex = props.tabs.findIndex(tab => tab.key === oldTab);
|
||||
|
||||
if (oldIndex >= 0 && newIndex && oldIndex < newIndex) {
|
||||
transitionName.value = 'swipeAnimationLeft';
|
||||
} else {
|
||||
transitionName.value = 'swipeAnimationRight';
|
||||
}
|
||||
|
||||
window.setTimeout(() => {
|
||||
transitionName.value = undefined;
|
||||
}, 400);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.transitionRoot {
|
||||
display: grid;
|
||||
grid-template-columns: 100%;
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
.transitionChildren {
|
||||
grid-area: 1 / 1 / 2 / 2;
|
||||
transform: translateX(var(--swipe));
|
||||
|
||||
&.swipeAnimation_enterActive,
|
||||
&.swipeAnimation_leaveActive {
|
||||
transition: transform .3s cubic-bezier(0.65, 0.05, 0.36, 1);
|
||||
}
|
||||
|
||||
&.swipeAnimationRight_leaveTo,
|
||||
&.swipeAnimationLeft_enterFrom {
|
||||
transform: translateX(calc(100% + 24px));
|
||||
}
|
||||
|
||||
&.swipeAnimationRight_enterFrom,
|
||||
&.swipeAnimationLeft_leaveTo {
|
||||
transform: translateX(calc(-100% - 24px));
|
||||
}
|
||||
}
|
||||
|
||||
.swiping {
|
||||
transition: transform .2s ease-out;
|
||||
}
|
||||
</style>
|
|
@ -13,8 +13,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
>
|
||||
<button v-if="hide" :class="$style.hidden" @click="hide = false">
|
||||
<div :class="$style.hiddenTextWrapper">
|
||||
<b v-if="audio.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.audio}${audio.size ? ' ' + bytes(audio.size) : ''})` : '' }}</b>
|
||||
<b v-else style="display: block;"><i class="ti ti-music"></i> {{ defaultStore.state.dataSaver.media && audio.size ? bytes(audio.size) : i18n.ts.audio }}</b>
|
||||
<b v-if="audio.isSensitive" style="display: block;"><i class="ph-eye-slash ph-bold ph-lg"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.audio}${audio.size ? ' ' + bytes(audio.size) : ''})` : '' }}</b>
|
||||
<b v-else style="display: block;"><i class="ph-music-notes ph-bold ph-lg"></i> {{ defaultStore.state.dataSaver.media && audio.size ? bytes(audio.size) : i18n.ts.audio }}</b>
|
||||
<span style="display: block;">{{ i18n.ts.clickToShow }}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
@ -22,26 +22,25 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<audio
|
||||
ref="audioEl"
|
||||
preload="metadata"
|
||||
:class="$style.audio"
|
||||
>
|
||||
<source :src="audio.url">
|
||||
</audio>
|
||||
<div :class="[$style.controlsChild, $style.controlsLeft]">
|
||||
<button class="_button" :class="$style.controlButton" @click="togglePlayPause">
|
||||
<i v-if="isPlaying" class="ti ti-player-pause-filled"></i>
|
||||
<i v-else class="ti ti-player-play-filled"></i>
|
||||
<i v-if="isPlaying" class="ph-pause ph-bold ph-lg"></i>
|
||||
<i v-else class="ph-play ph-bold ph-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div :class="[$style.controlsChild, $style.controlsRight]">
|
||||
<button class="_button" :class="$style.controlButton" @click="showMenu">
|
||||
<i class="ti ti-settings"></i>
|
||||
<i class="ph-gear ph-bold ph-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div :class="[$style.controlsChild, $style.controlsTime]">{{ hms(elapsedTimeMs) }}</div>
|
||||
<div :class="[$style.controlsChild, $style.controlsVolume]">
|
||||
<button class="_button" :class="$style.controlButton" @click="toggleMute">
|
||||
<i v-if="volume === 0" class="ti ti-volume-3"></i>
|
||||
<i v-else class="ti ti-volume"></i>
|
||||
<i v-if="volume === 0" class="ph-speaker-x ph-bold ph-lg"></i>
|
||||
<i v-else class="ph-speaker-high ph-bold ph-lg"></i>
|
||||
</button>
|
||||
<MkMediaRange
|
||||
v-model="volume"
|
||||
|
@ -88,7 +87,7 @@ function showMenu(ev: MouseEvent) {
|
|||
// TODO: 再生キューに追加
|
||||
{
|
||||
text: i18n.ts.hide,
|
||||
icon: 'ti ti-eye-off',
|
||||
icon: 'ph-eye-closed ph-bold ph-lg',
|
||||
action: () => {
|
||||
hide.value = true;
|
||||
},
|
||||
|
@ -100,7 +99,7 @@ function showMenu(ev: MouseEvent) {
|
|||
type: 'divider',
|
||||
}, {
|
||||
text: props.audio.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive,
|
||||
icon: props.audio.isSensitive ? 'ti ti-eye' : 'ti ti-eye-exclamation',
|
||||
icon: props.audio.isSensitive ? 'ph-eye ph-bold ph-lg' : 'ph-eye-slash ph-bold ph-lg',
|
||||
danger: true,
|
||||
action: () => toggleSensitive(props.audio),
|
||||
});
|
||||
|
@ -138,7 +137,7 @@ const rangePercent = computed({
|
|||
audioEl.value.currentTime = to * durationMs.value / 1000;
|
||||
},
|
||||
});
|
||||
const volume = ref(.5);
|
||||
const volume = ref(.25);
|
||||
const bufferedEnd = ref(0);
|
||||
const bufferedDataRatio = computed(() => {
|
||||
if (!audioEl.value) return 0;
|
||||
|
@ -161,7 +160,7 @@ function togglePlayPause() {
|
|||
|
||||
function toggleMute() {
|
||||
if (volume.value === 0) {
|
||||
volume.value = .5;
|
||||
volume.value = .25;
|
||||
} else {
|
||||
volume.value = 0;
|
||||
}
|
||||
|
@ -207,7 +206,7 @@ function init() {
|
|||
isActuallyPlaying.value = false;
|
||||
isPlaying.value = false;
|
||||
});
|
||||
|
||||
|
||||
durationMs.value = audioEl.value.duration * 1000;
|
||||
audioEl.value.addEventListener('durationchange', () => {
|
||||
if (audioEl.value) {
|
||||
|
|
|
@ -5,9 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<!-- Media系専用のinput range -->
|
||||
<template>
|
||||
<div :class="$style.controlsSeekbar" :style="sliderBgWhite ? '--sliderBg: rgba(255,255,255,.25);' : '--sliderBg: var(--scrollbarHandle);'">
|
||||
<progress v-if="buffer !== undefined" :class="$style.buffer" :value="isNaN(buffer) ? 0 : buffer" min="0" max="1">{{ Math.round(buffer * 100) }}% buffered</progress>
|
||||
<input v-model="model" :class="$style.seek" :style="`--value: ${modelValue * 100}%;`" type="range" min="0" max="1" step="any" @change="emit('dragEnded', modelValue)"/>
|
||||
<div :style="sliderBgWhite ? '--sliderBg: rgba(255,255,255,.25);' : '--sliderBg: var(--scrollbarHandle);'">
|
||||
<div :class="$style.controlsSeekbar">
|
||||
<progress v-if="buffer !== undefined" :class="$style.buffer" :value="isNaN(buffer) ? 0 : buffer" min="0" max="1">{{ Math.round(buffer * 100) }}% buffered</progress>
|
||||
<input v-model="model" :class="$style.seek" :style="`--value: ${modelValue * 100}%;`" type="range" min="0" max="1" step="any" @change="emit('dragEnded', modelValue)"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-else-if="!isActuallyPlaying" :class="$style.videoLoading">
|
||||
<MkLoading/>
|
||||
</div>
|
||||
<i class="ti ti-eye-off" :class="$style.hide" @click="hide = true"></i>
|
||||
<i class="ph-eye-closed ph-bold ph-lg" :class="$style.hide" @click="hide = true"></i>
|
||||
<div :class="$style.indicators">
|
||||
<div v-if="video.comment" :class="$style.indicator">ALT</div>
|
||||
<div v-if="video.isSensitive" :class="$style.indicator" style="color: var(--warn);" :title="i18n.ts.sensitive"><i class="ph-warning ph-bold ph-lg"></i></div>
|
||||
|
@ -113,7 +113,7 @@ function showMenu(ev: MouseEvent) {
|
|||
// TODO: 再生キューに追加
|
||||
{
|
||||
text: i18n.ts.hide,
|
||||
icon: 'ti ti-eye-off',
|
||||
icon: 'ph-eye-closed ph-bold ph-lg',
|
||||
action: () => {
|
||||
hide.value = true;
|
||||
},
|
||||
|
@ -125,7 +125,7 @@ function showMenu(ev: MouseEvent) {
|
|||
type: 'divider',
|
||||
}, {
|
||||
text: props.video.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive,
|
||||
icon: props.video.isSensitive ? 'ti ti-eye' : 'ti ti-eye-exclamation',
|
||||
icon: props.video.isSensitive ? 'ph-eye ph-bold ph-lg' : 'ph-eye-slash ph-bold ph-lg',
|
||||
danger: true,
|
||||
action: () => toggleSensitive(props.video),
|
||||
});
|
||||
|
@ -176,7 +176,7 @@ const rangePercent = computed({
|
|||
videoEl.value.currentTime = to * durationMs.value / 1000;
|
||||
},
|
||||
});
|
||||
const volume = ref(.5);
|
||||
const volume = ref(.25);
|
||||
const bufferedEnd = ref(0);
|
||||
const bufferedDataRatio = computed(() => {
|
||||
if (!videoEl.value) return 0;
|
||||
|
@ -236,7 +236,7 @@ function toggleFullscreen() {
|
|||
|
||||
function toggleMute() {
|
||||
if (volume.value === 0) {
|
||||
volume.value = .5;
|
||||
volume.value = .25;
|
||||
} else {
|
||||
volume.value = 0;
|
||||
}
|
||||
|
@ -535,6 +535,9 @@ onDeactivated(() => {
|
|||
|
||||
.seekbarRoot {
|
||||
grid-area: seekbar;
|
||||
/* ▼シークバー操作をやりやすくするためにクリックイベントが伝播されないエリアを拡張する */
|
||||
margin: -10px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
@container (min-width: 500px) {
|
||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<div
|
||||
v-if="!hardMuted && !muted"
|
||||
v-if="!hardMuted && muted === false"
|
||||
v-show="!isDeleted"
|
||||
ref="el"
|
||||
v-hotkey="keymap"
|
||||
|
@ -75,7 +75,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-if="translating || translation" :class="$style.translation">
|
||||
<MkLoading v-if="translating" mini/>
|
||||
<div v-else>
|
||||
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
||||
<b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b>
|
||||
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -153,7 +153,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</article>
|
||||
</div>
|
||||
<div v-else-if="!hardMuted" :class="$style.muted" @click="muted = false">
|
||||
<I18n :src="i18n.ts.userSaysSomething" tag="small">
|
||||
<I18n v-if="muted === 'sensitiveMute'" :src="i18n.ts.userSaysSomethingSensitive" tag="small">
|
||||
<template #name>
|
||||
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
|
||||
<MkUserName :user="appearNote.user"/>
|
||||
</MkA>
|
||||
</template>
|
||||
</I18n>
|
||||
<I18n v-else :src="i18n.ts.userSaysSomething" tag="small">
|
||||
<template #name>
|
||||
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
|
||||
<MkUserName :user="appearNote.user"/>
|
||||
|
@ -228,6 +235,7 @@ const emit = defineEmits<{
|
|||
|
||||
const router = useRouter();
|
||||
|
||||
const inTimeline = inject<boolean>('inTimeline', false);
|
||||
const inChannel = inject('inChannel', null);
|
||||
const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null);
|
||||
|
||||
|
@ -289,7 +297,7 @@ const collapsed = defaultStore.state.expandLongNote && appearNote.value.cw == nu
|
|||
const isDeleted = ref(false);
|
||||
const renoted = ref(false);
|
||||
const muted = ref(checkMute(appearNote.value, $i?.mutedWords));
|
||||
const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, $i?.hardMutedWords));
|
||||
const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, $i?.hardMutedWords, true));
|
||||
const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
|
||||
const translating = ref(false);
|
||||
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
|
||||
|
@ -299,12 +307,20 @@ const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.
|
|||
const animated = computed(() => parsed.value ? checkAnimationFromMfm(parsed.value) : null);
|
||||
const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false);
|
||||
|
||||
function checkMute(note: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null): boolean {
|
||||
/* Overload FunctionにLintが対応していないのでコメントアウト
|
||||
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: true): boolean;
|
||||
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): boolean | 'sensitiveMute';
|
||||
*/
|
||||
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): boolean | 'sensitiveMute' {
|
||||
if (mutedWords == null) return false;
|
||||
|
||||
if (checkWordMute(note, $i, mutedWords)) return true;
|
||||
if (note.reply && checkWordMute(note.reply, $i, mutedWords)) return true;
|
||||
if (note.renote && checkWordMute(note.renote, $i, mutedWords)) return true;
|
||||
if (checkWordMute(noteToCheck, $i, mutedWords)) return true;
|
||||
if (noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords)) return true;
|
||||
if (noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords)) return true;
|
||||
|
||||
if (checkOnly) return false;
|
||||
|
||||
if (inTimeline && !defaultStore.state.tl.filter.withSensitive && noteToCheck.files?.some((v) => v.isSensitive)) return 'sensitiveMute';
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -89,7 +89,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-if="translating || translation" :class="$style.translation">
|
||||
<MkLoading v-if="translating" mini/>
|
||||
<div v-else>
|
||||
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
||||
<b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b>
|
||||
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -56,8 +56,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
|
||||
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
|
||||
<MkA v-else-if="notification.user" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
|
||||
<span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.t('_notification.reactedBySomeUsers', { n: notification.reactions.length }) }}</span>
|
||||
<span v-else-if="notification.type === 'renote:grouped'">{{ i18n.t('_notification.renotedBySomeUsers', { n: notification.users.length }) }}</span>
|
||||
<span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: notification.reactions.length }) }}</span>
|
||||
<span v-else-if="notification.type === 'renote:grouped'">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span>
|
||||
<span v-else>{{ notification.header }}</span>
|
||||
<MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/>
|
||||
</header>
|
||||
|
|
|
@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkButton inline @click="disableAll">{{ i18n.ts.disableAll }}</MkButton>
|
||||
<MkButton inline @click="enableAll">{{ i18n.ts.enableAll }}</MkButton>
|
||||
</div>
|
||||
<MkSwitch v-for="ntype in notificationTypes" :key="ntype" v-model="typesMap[ntype].value">{{ i18n.t(`_notification._types.${ntype}`) }}</MkSwitch>
|
||||
<MkSwitch v-for="ntype in notificationTypes" :key="ntype" v-model="typesMap[ntype].value">{{ i18n.ts._notification._types[ntype] }}</MkSwitch>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkModalWindow>
|
||||
|
|
|
@ -11,12 +11,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<span :class="$style.fg">
|
||||
<template v-if="choice.isVoted"><i class="ph-check ph-bold ph-lg" style="margin-right: 4px; color: var(--accent);"></i></template>
|
||||
<Mfm :text="choice.text" :plain="true"/>
|
||||
<span v-if="showResult" style="margin-left: 4px; opacity: 0.7;">({{ i18n.t('_poll.votesCount', { n: choice.votes }) }})</span>
|
||||
<span v-if="showResult" style="margin-left: 4px; opacity: 0.7;">({{ i18n.tsx._poll.votesCount({ n: choice.votes }) }})</span>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-if="!readOnly" :class="$style.info">
|
||||
<span>{{ i18n.t('_poll.totalVotes', { n: total }) }}</span>
|
||||
<span>{{ i18n.tsx._poll.totalVotes({ n: total }) }}</span>
|
||||
<span v-if="note.poll.multiple"> · </span>
|
||||
<span v-if="note.poll.multiple" style="color: var(--accent); font-weight: bolder;">{{ i18n.ts._poll.multiple }}</span>
|
||||
<span> · </span>
|
||||
|
@ -51,10 +51,11 @@ const total = computed(() => sum(props.note.poll.choices.map(x => x.votes)));
|
|||
const closed = computed(() => remaining.value === 0);
|
||||
const isLocal = computed(() => !props.note.uri);
|
||||
const isVoted = computed(() => !props.note.poll.multiple && props.note.poll.choices.some(c => c.isVoted));
|
||||
const timer = computed(() => i18n.t(
|
||||
remaining.value >= 86400 ? '_poll.remainingDays' :
|
||||
remaining.value >= 3600 ? '_poll.remainingHours' :
|
||||
remaining.value >= 60 ? '_poll.remainingMinutes' : '_poll.remainingSeconds', {
|
||||
const timer = computed(() => i18n.tsx._poll[
|
||||
remaining.value >= 86400 ? 'remainingDays' :
|
||||
remaining.value >= 3600 ? 'remainingHours' :
|
||||
remaining.value >= 60 ? 'remainingMinutes' : 'remainingSeconds'
|
||||
]({
|
||||
s: Math.floor(remaining.value % 60),
|
||||
m: Math.floor(remaining.value / 60) % 60,
|
||||
h: Math.floor(remaining.value / 3600) % 24,
|
||||
|
@ -85,13 +86,13 @@ const vote = async (id) => {
|
|||
if (!props.note.poll.multiple) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'question',
|
||||
text: i18n.t('voteConfirm', { choice: props.note.poll.choices[id].text }),
|
||||
text: i18n.tsx.voteConfirm({ choice: props.note.poll.choices[id].text }),
|
||||
});
|
||||
if (canceled) return;
|
||||
} else {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'question',
|
||||
text: i18n.t('voteConfirmMulti', { choice: props.note.poll.choices[id].text }),
|
||||
text: i18n.tsx.voteConfirmMulti({ choice: props.note.poll.choices[id].text }),
|
||||
});
|
||||
if (canceled) return;
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</p>
|
||||
<ul>
|
||||
<li v-for="(choice, i) in choices" :key="i">
|
||||
<MkInput class="input" small :modelValue="choice" :placeholder="i18n.t('_poll.choiceN', { n: i + 1 })" @update:modelValue="onInput(i, $event)">
|
||||
<MkInput class="input" small :modelValue="choice" :placeholder="i18n.tsx._poll.choiceN({ n: i + 1 })" @update:modelValue="onInput(i, $event)">
|
||||
</MkInput>
|
||||
<button class="_button" @click="remove(i)">
|
||||
<i class="ph-x ph-bold ph-lg"></i>
|
||||
|
|
|
@ -56,6 +56,23 @@ function detachMedia(id: string) {
|
|||
}
|
||||
}
|
||||
|
||||
async function detachAndDeleteMedia(file: Misskey.entities.DriveFile) {
|
||||
if (mock) return;
|
||||
|
||||
detachMedia(file.id);
|
||||
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('driveFileDeleteConfirm', { name: file.name }),
|
||||
});
|
||||
|
||||
if (canceled) return;
|
||||
|
||||
os.apiWithDialog('drive/files/delete', {
|
||||
fileId: file.id,
|
||||
});
|
||||
}
|
||||
|
||||
function toggleSensitive(file) {
|
||||
if (mock) {
|
||||
emit('changeSensitive', file, !file.isSensitive);
|
||||
|
@ -138,6 +155,13 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent): void {
|
|||
text: i18n.ts.attachCancel,
|
||||
icon: 'ph-x-circle ph-bold ph-lg',
|
||||
action: () => { detachMedia(file.id); },
|
||||
}, {
|
||||
type: 'divider',
|
||||
}, {
|
||||
text: i18n.ts.deleteFile,
|
||||
icon: 'ph-trash ph-bold ph-lg',
|
||||
danger: true,
|
||||
action: () => { detachAndDeleteMedia(file); },
|
||||
}], ev.currentTarget ?? ev.target).then(() => menuShowing = false);
|
||||
menuShowing = true;
|
||||
}
|
||||
|
|
|
@ -18,6 +18,9 @@ export default defineComponent({
|
|||
watch(value, () => {
|
||||
context.emit('update:modelValue', value.value);
|
||||
});
|
||||
watch(() => props.modelValue, v => {
|
||||
value.value = v;
|
||||
});
|
||||
if (!context.slots.default) return null;
|
||||
let options = context.slots.default();
|
||||
const label = context.slots.label && context.slots.label();
|
||||
|
|
|
@ -105,7 +105,7 @@ async function menu(ev) {
|
|||
if (!props.reaction.includes(":")) return;
|
||||
os.popupMenu([{
|
||||
text: i18n.ts.info,
|
||||
icon: 'ti ti-info-circle',
|
||||
icon: 'ph-info ph-bold ph-lg',
|
||||
action: async () => {
|
||||
os.popup(MkCustomEmojiDetailedDialog, {
|
||||
emoji: await misskeyApiGet('emoji', {
|
||||
|
|
|
@ -52,7 +52,7 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'change', _ev: KeyboardEvent): void;
|
||||
(ev: 'changeByUser'): void;
|
||||
(ev: 'update:modelValue', value: string | null): void;
|
||||
}>();
|
||||
|
||||
|
@ -77,7 +77,6 @@ const height =
|
|||
const focus = () => inputEl.value.focus();
|
||||
const onInput = (ev) => {
|
||||
changed.value = true;
|
||||
emit('change', ev);
|
||||
};
|
||||
|
||||
const updated = () => {
|
||||
|
@ -136,6 +135,7 @@ function show(ev: MouseEvent) {
|
|||
active: computed(() => v.value === option.props.value),
|
||||
action: () => {
|
||||
v.value = option.props.value;
|
||||
emit('changeByUser', v.value);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -269,7 +269,7 @@ async function onSubmit(): Promise<void> {
|
|||
os.alert({
|
||||
type: 'success',
|
||||
title: i18n.ts._signup.almostThere,
|
||||
text: i18n.t('_signup.emailSent', { email: email.value }),
|
||||
text: i18n.tsx._signup.emailSent({ email: email.value }),
|
||||
});
|
||||
emit('signupEmailPending');
|
||||
} else if (instance.approvalRequiredForSignup) {
|
||||
|
|
|
@ -105,7 +105,7 @@ async function updateAgreeServerRules(v: boolean) {
|
|||
const confirm = await os.confirm({
|
||||
type: 'question',
|
||||
title: i18n.ts.doYouAgree,
|
||||
text: i18n.t('iHaveReadXCarefullyAndAgree', { x: i18n.ts.serverRules }),
|
||||
text: i18n.tsx.iHaveReadXCarefullyAndAgree({ x: i18n.ts.serverRules }),
|
||||
});
|
||||
if (confirm.canceled) return;
|
||||
agreeServerRules.value = true;
|
||||
|
@ -119,7 +119,7 @@ async function updateAgreeTosAndPrivacyPolicy(v: boolean) {
|
|||
const confirm = await os.confirm({
|
||||
type: 'question',
|
||||
title: i18n.ts.doYouAgree,
|
||||
text: i18n.t('iHaveReadXCarefullyAndAgree', {
|
||||
text: i18n.tsx.iHaveReadXCarefullyAndAgree({
|
||||
x: tosPrivacyPolicyLabel.value,
|
||||
}),
|
||||
});
|
||||
|
@ -135,7 +135,7 @@ async function updateAgreeNote(v: boolean) {
|
|||
const confirm = await os.confirm({
|
||||
type: 'question',
|
||||
title: i18n.ts.doYouAgree,
|
||||
text: i18n.t('iHaveReadXCarefullyAndAgree', { x: i18n.ts.basicNotesBeforeCreateAccount }),
|
||||
text: i18n.tsx.iHaveReadXCarefullyAndAgree({ x: i18n.ts.basicNotesBeforeCreateAccount }),
|
||||
});
|
||||
if (confirm.canceled) return;
|
||||
agreeNote.value = true;
|
||||
|
|
|
@ -15,14 +15,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-if="note.text && translating || note.text && translation" :class="$style.translation">
|
||||
<MkLoading v-if="translating" mini/>
|
||||
<div v-else>
|
||||
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
||||
<b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b>
|
||||
<Mfm :text="translation.text" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/>
|
||||
</div>
|
||||
</div>
|
||||
<MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`" @click.stop>RN: ...</MkA>
|
||||
</div>
|
||||
<details v-if="note.files.length > 0" :open="!defaultStore.state.collapseFiles && !hideFiles">
|
||||
<summary>({{ i18n.t('withNFiles', { n: note.files.length }) }})</summary>
|
||||
<summary>({{ i18n.tsx.withNFiles({ n: note.files.length }) }})</summary>
|
||||
<MkMediaList :mediaList="note.files"/>
|
||||
</details>
|
||||
<details v-if="note.poll">
|
||||
|
|
|
@ -51,6 +51,7 @@ const emit = defineEmits<{
|
|||
(ev: 'queue', count: number): void;
|
||||
}>();
|
||||
|
||||
provide('inTimeline', true);
|
||||
provide('inChannel', computed(() => props.src === 'channel'));
|
||||
|
||||
type TimelineQueryType = {
|
||||
|
|
|
@ -33,12 +33,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkButton inline @click="enableAll">{{ i18n.ts.enableAll }}</MkButton>
|
||||
</div>
|
||||
<div class="_gaps_s">
|
||||
<MkSwitch v-for="kind in Object.keys(permissionSwitches)" :key="kind" v-model="permissionSwitches[kind]">{{ i18n.t(`_permissions.${kind}`) }}</MkSwitch>
|
||||
<MkSwitch v-for="kind in Object.keys(permissionSwitches)" :key="kind" v-model="permissionSwitches[kind]">{{ i18n.ts._permissions[kind] }}</MkSwitch>
|
||||
</div>
|
||||
<div v-if="iAmAdmin" :class="$style.adminPermissions">
|
||||
<div :class="$style.adminPermissionsHeader"><b>{{ i18n.ts.adminPermission }}</b></div>
|
||||
<div class="_gaps_s">
|
||||
<MkSwitch v-for="kind in Object.keys(permissionSwitchesForAdmin)" :key="kind" v-model="permissionSwitchesForAdmin[kind]">{{ i18n.t(`_permissions.${kind}`) }}</MkSwitch>
|
||||
<MkSwitch v-for="kind in Object.keys(permissionSwitchesForAdmin)" :key="kind" v-model="permissionSwitchesForAdmin[kind]">{{ i18n.ts._permissions[kind] }}</MkSwitch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -133,7 +133,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<a href="https://misskey-hub.net/docs/for-users/" target="_blank" class="_link">{{ i18n.ts.help }}</a>
|
||||
</template>
|
||||
</I18n>
|
||||
<div>{{ i18n.t('_initialAccountSetting.haveFun', { name: instance.name ?? host }) }}</div>
|
||||
<div>{{ i18n.tsx._initialAccountSetting.haveFun({ name: instance.name ?? host }) }}</div>
|
||||
<div class="_buttonsCenter" style="margin-top: 16px;">
|
||||
<MkButton v-if="initialPage !== 4" rounded @click="page--"><i class="ph-arrow-left ph-bold pg-lg"></i> {{ i18n.ts.goBack }}</MkButton>
|
||||
<MkButton rounded primary gradate @click="close(false)">{{ i18n.ts.close }}</MkButton>
|
||||
|
|
|
@ -83,7 +83,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, onUnmounted, ref } from 'vue';
|
||||
import { defineAsyncComponent, onDeactivated, onUnmounted, ref } from 'vue';
|
||||
import type { summaly } from '@misskey-dev/summaly';
|
||||
import { url as local } from '@/config.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
@ -131,6 +131,10 @@ const embedId = `embed${Math.random().toString().replace(/\D/, '')}`;
|
|||
const tweetHeight = ref(150);
|
||||
const unknownUrl = ref(false);
|
||||
|
||||
onDeactivated(() => {
|
||||
playerEnabled.value = false;
|
||||
});
|
||||
|
||||
const requestUrl = new URL(props.url);
|
||||
if (!['http:', 'https:'].includes(requestUrl.protocol)) throw new Error('invalid url');
|
||||
|
||||
|
|
|
@ -118,7 +118,7 @@ async function done() {
|
|||
async function del() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('removeAreYouSure', { x: title.value }),
|
||||
text: i18n.tsx.removeAreYouSure({ x: title.value }),
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
|
|
|
@ -85,7 +85,7 @@ const recentUsers = ref<Misskey.entities.UserDetailed[]>([]);
|
|||
const selected = ref<Misskey.entities.UserDetailed | null>(null);
|
||||
const dialogEl = ref();
|
||||
|
||||
const search = () => {
|
||||
function search() {
|
||||
if (username.value === '' && host.value === '') {
|
||||
users.value = [];
|
||||
return;
|
||||
|
@ -98,9 +98,9 @@ const search = () => {
|
|||
}).then(_users => {
|
||||
users.value = _users;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const ok = () => {
|
||||
function ok() {
|
||||
if (selected.value == null) return;
|
||||
emit('ok', selected.value);
|
||||
dialogEl.value.close();
|
||||
|
@ -110,12 +110,12 @@ const ok = () => {
|
|||
recents = recents.filter(x => x !== selected.value.id);
|
||||
recents.unshift(selected.value.id);
|
||||
defaultStore.set('recentlyUsedUsers', recents.splice(0, 16));
|
||||
};
|
||||
}
|
||||
|
||||
const cancel = () => {
|
||||
function cancel() {
|
||||
emit('cancel');
|
||||
dialogEl.value.close();
|
||||
};
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
misskeyApi('users/show', {
|
||||
|
|
|
@ -68,7 +68,7 @@ function setAvatar(ev) {
|
|||
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'question',
|
||||
text: i18n.t('cropImageAsk'),
|
||||
text: i18n.ts.cropImageAsk,
|
||||
okText: i18n.ts.cropYes,
|
||||
cancelText: i18n.ts.cropNo,
|
||||
});
|
||||
|
|
|
@ -93,7 +93,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div class="_gaps" style="text-align: center;">
|
||||
<i class="ph-bell-ringing ph-bold ph-lg" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
|
||||
<div style="font-size: 120%;">{{ i18n.ts.pushNotification }}</div>
|
||||
<div style="padding: 0 16px;">{{ i18n.t('_initialAccountSetting.pushNotificationDescription', { name: instance.name ?? host }) }}</div>
|
||||
<div style="padding: 0 16px;">{{ i18n.tsx._initialAccountSetting.pushNotificationDescription({ name: instance.name ?? host }) }}</div>
|
||||
<MkPushNotificationAllowButton primary showOnlyToRegister style="margin: 0 auto;"/>
|
||||
<div class="_buttonsCenter" style="margin-top: 16px;">
|
||||
<MkButton rounded data-cy-user-setup-back @click="page--"><i class="ph-arrow-left ph-bold ph-lg"></i> {{ i18n.ts.goBack }}</MkButton>
|
||||
|
@ -110,7 +110,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div class="_gaps" style="text-align: center;">
|
||||
<i class="ph-check ph-bold ph-lg" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
|
||||
<div style="font-size: 120%;">{{ i18n.ts._initialAccountSetting.initialAccountSettingCompleted }}</div>
|
||||
<div>{{ i18n.t('_initialAccountSetting.youCanContinueTutorial', { name: instance.name ?? host }) }}</div>
|
||||
<div>{{ i18n.tsx._initialAccountSetting.youCanContinueTutorial({ name: instance.name ?? host }) }}</div>
|
||||
<div class="_buttonsCenter" style="margin-top: 16px;">
|
||||
<MkButton rounded primary gradate data-cy-user-setup-continue @click="launchTutorial()">{{ i18n.ts._initialAccountSetting.startTutorial }} <i class="ph-arrow-right ph-bold ph-lg"></i></MkButton>
|
||||
</div>
|
||||
|
|
|
@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<header :class="$style.editHeader">
|
||||
<MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--margin)" data-cy-widget-select>
|
||||
<template #label>{{ i18n.ts.selectWidget }}</template>
|
||||
<option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.t(`_widgets.${widget}`) }}</option>
|
||||
<option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.ts._widgets[widget] }}</option>
|
||||
</MkSelect>
|
||||
<MkButton inline primary data-cy-widget-add @click="addWidget"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.add }}</MkButton>
|
||||
<MkButton inline @click="$emit('exit')">{{ i18n.ts.close }}</MkButton>
|
||||
|
@ -116,7 +116,7 @@ function onContextmenu(widget: Widget, ev: MouseEvent) {
|
|||
|
||||
os.contextMenu([{
|
||||
type: 'label',
|
||||
text: i18n.t(`_widgets.${widget.name}`),
|
||||
text: i18n.ts._widgets[widget.name],
|
||||
}, {
|
||||
icon: 'ph-gear ph-bold ph-lg',
|
||||
text: i18n.ts.settings,
|
||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<div
|
||||
v-if="!hardMuted && !muted"
|
||||
v-if="!hardMuted && muted === false"
|
||||
v-show="!isDeleted"
|
||||
ref="el"
|
||||
v-hotkey="keymap"
|
||||
|
@ -77,7 +77,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-if="translating || translation" :class="$style.translation">
|
||||
<MkLoading v-if="translating" mini/>
|
||||
<div v-else>
|
||||
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
||||
<b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b>
|
||||
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -155,7 +155,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</article>
|
||||
</div>
|
||||
<div v-else-if="!hardMuted" :class="$style.muted" @click="muted = false">
|
||||
<I18n :src="i18n.ts.userSaysSomething" tag="small">
|
||||
<I18n v-if="muted === 'sensitiveMute'" :src="i18n.ts.userSaysSomethingSensitive" tag="small">
|
||||
<template #name>
|
||||
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
|
||||
<MkUserName :user="appearNote.user"/>
|
||||
</MkA>
|
||||
</template>
|
||||
</I18n>
|
||||
<I18n v-else :src="i18n.ts.userSaysSomething" tag="small">
|
||||
<template #name>
|
||||
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
|
||||
<MkUserName :user="appearNote.user"/>
|
||||
|
@ -229,6 +236,7 @@ const emit = defineEmits<{
|
|||
|
||||
const router = useRouter();
|
||||
|
||||
const inTimeline = inject<boolean>('inTimeline', false);
|
||||
const inChannel = inject('inChannel', null);
|
||||
const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null);
|
||||
|
||||
|
@ -290,7 +298,7 @@ const collapsed = defaultStore.state.expandLongNote && appearNote.value.cw == nu
|
|||
const isDeleted = ref(false);
|
||||
const renoted = ref(false);
|
||||
const muted = ref(checkMute(appearNote.value, $i?.mutedWords));
|
||||
const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, $i?.hardMutedWords));
|
||||
const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, $i?.hardMutedWords, true));
|
||||
const translation = ref<any>(null);
|
||||
const translating = ref(false);
|
||||
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
|
||||
|
@ -300,12 +308,20 @@ const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.
|
|||
const animated = computed(() => parsed.value ? checkAnimationFromMfm(parsed.value) : null);
|
||||
const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false);
|
||||
|
||||
function checkMute(note: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null): boolean {
|
||||
/* Overload FunctionにLintが対応していないのでコメントアウト
|
||||
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: true): boolean;
|
||||
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): boolean | 'sensitiveMute';
|
||||
*/
|
||||
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): boolean | 'sensitiveMute' {
|
||||
if (mutedWords == null) return false;
|
||||
|
||||
if (checkWordMute(note, $i, mutedWords)) return true;
|
||||
if (note.reply && checkWordMute(note.reply, $i, mutedWords)) return true;
|
||||
if (note.renote && checkWordMute(note.renote, $i, mutedWords)) return true;
|
||||
if (checkWordMute(noteToCheck, $i, mutedWords)) return true;
|
||||
if (noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords)) return true;
|
||||
if (noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords)) return true;
|
||||
|
||||
if (checkOnly) return false;
|
||||
|
||||
if (inTimeline && !defaultStore.state.tl.filter.withSensitive && noteToCheck.files?.some((v) => v.isSensitive)) return 'sensitiveMute';
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -97,7 +97,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-if="translating || translation" :class="$style.translation">
|
||||
<MkLoading v-if="translating" mini/>
|
||||
<div v-else>
|
||||
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
||||
<b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b>
|
||||
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
46
packages/frontend/src/components/global/I18n.vue
Normal file
46
packages/frontend/src/components/global/I18n.vue
Normal file
|
@ -0,0 +1,46 @@
|
|||
<template>
|
||||
<render/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" generic="T extends string | ParameterizedString">
|
||||
import { computed, h } from 'vue';
|
||||
import type { ParameterizedString } from '../../../../../locales/index.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
src: T;
|
||||
tag?: string;
|
||||
// eslint-disable-next-line vue/require-default-prop
|
||||
textTag?: string;
|
||||
}>(), {
|
||||
tag: 'span',
|
||||
});
|
||||
|
||||
const slots = defineSlots<T extends ParameterizedString<infer R> ? { [K in R]: () => unknown } : NonNullable<unknown>>();
|
||||
|
||||
const parsed = computed(() => {
|
||||
let str = props.src as string;
|
||||
const value: (string | { arg: string; })[] = [];
|
||||
for (;;) {
|
||||
const nextBracketOpen = str.indexOf('{');
|
||||
const nextBracketClose = str.indexOf('}');
|
||||
|
||||
if (nextBracketOpen === -1) {
|
||||
value.push(str);
|
||||
break;
|
||||
} else {
|
||||
if (nextBracketOpen > 0) value.push(str.substring(0, nextBracketOpen));
|
||||
value.push({
|
||||
arg: str.substring(nextBracketOpen + 1, nextBracketClose),
|
||||
});
|
||||
}
|
||||
|
||||
str = str.substring(nextBracketClose + 1);
|
||||
}
|
||||
|
||||
return value;
|
||||
});
|
||||
|
||||
const render = () => {
|
||||
return h(props.tag, parsed.value.map(x => typeof x === 'string' ? (props.textTag ? h(props.textTag, x) : x) : slots[x.arg]()));
|
||||
};
|
||||
</script>
|
|
@ -97,7 +97,7 @@ function onClick(ev: MouseEvent) {
|
|||
},
|
||||
}] : []), {
|
||||
text: i18n.ts.info,
|
||||
icon: 'ti ti-info-circle',
|
||||
icon: 'ph-info ph-bold ph-lg',
|
||||
action: async () => {
|
||||
os.popup(MkCustomEmojiDetailedDialog, {
|
||||
emoji: await misskeyApiGet('emoji', {
|
||||
|
|
|
@ -13,6 +13,7 @@ import MkMention from '@/components/MkMention.vue';
|
|||
import MkEmoji from '@/components/global/MkEmoji.vue';
|
||||
import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue';
|
||||
import MkCode from '@/components/MkCode.vue';
|
||||
import MkCodeInline from '@/components/MkCodeInline.vue';
|
||||
import MkGoogle from '@/components/MkGoogle.vue';
|
||||
import MkSparkle from '@/components/MkSparkle.vue';
|
||||
import MkA from '@/components/global/MkA.vue';
|
||||
|
@ -269,7 +270,7 @@ export default function(props: MfmProps, context: SetupContext<MfmEvents>) {
|
|||
) b_style = 'solid';
|
||||
const width = parseFloat(token.props.args.width ?? '1');
|
||||
const radius = parseFloat(token.props.args.radius ?? '0');
|
||||
style = `border: ${width}px ${b_style} ${color}; border-radius: ${radius}px`;
|
||||
style = `border: ${width}px ${b_style} ${color}; border-radius: ${radius}px;${token.props.args.noclip ? '' : ' overflow: clip;'}`;
|
||||
break;
|
||||
}
|
||||
case 'ruby': {
|
||||
|
@ -376,10 +377,9 @@ export default function(props: MfmProps, context: SetupContext<MfmEvents>) {
|
|||
}
|
||||
|
||||
case 'inlineCode': {
|
||||
return [h(MkCode, {
|
||||
return [h(MkCodeInline, {
|
||||
key: Math.random(),
|
||||
code: token.props.code,
|
||||
inline: true,
|
||||
})];
|
||||
}
|
||||
|
||||
|
|
|
@ -123,7 +123,7 @@ export const DetailNow = {
|
|||
export const RelativeOneHourAgo = {
|
||||
...Empty,
|
||||
async play({ canvasElement }) {
|
||||
await expect(canvasElement).toHaveTextContent(i18n.t('_ago.hoursAgo', { n: 1 }));
|
||||
await expect(canvasElement).toHaveTextContent(i18n.tsx._ago.hoursAgo({ n: 1 }));
|
||||
},
|
||||
args: {
|
||||
...Empty.args,
|
||||
|
@ -162,7 +162,7 @@ export const DetailOneHourAgo = {
|
|||
export const RelativeOneDayAgo = {
|
||||
...Empty,
|
||||
async play({ canvasElement }) {
|
||||
await expect(canvasElement).toHaveTextContent(i18n.t('_ago.daysAgo', { n: 1 }));
|
||||
await expect(canvasElement).toHaveTextContent(i18n.tsx._ago.daysAgo({ n: 1 }));
|
||||
},
|
||||
args: {
|
||||
...Empty.args,
|
||||
|
@ -201,7 +201,7 @@ export const DetailOneDayAgo = {
|
|||
export const RelativeOneWeekAgo = {
|
||||
...Empty,
|
||||
async play({ canvasElement }) {
|
||||
await expect(canvasElement).toHaveTextContent(i18n.t('_ago.weeksAgo', { n: 1 }));
|
||||
await expect(canvasElement).toHaveTextContent(i18n.tsx._ago.weeksAgo({ n: 1 }));
|
||||
},
|
||||
args: {
|
||||
...Empty.args,
|
||||
|
@ -240,7 +240,7 @@ export const DetailOneWeekAgo = {
|
|||
export const RelativeOneMonthAgo = {
|
||||
...Empty,
|
||||
async play({ canvasElement }) {
|
||||
await expect(canvasElement).toHaveTextContent(i18n.t('_ago.monthsAgo', { n: 1 }));
|
||||
await expect(canvasElement).toHaveTextContent(i18n.tsx._ago.monthsAgo({ n: 1 }));
|
||||
},
|
||||
args: {
|
||||
...Empty.args,
|
||||
|
@ -279,7 +279,7 @@ export const DetailOneMonthAgo = {
|
|||
export const RelativeOneYearAgo = {
|
||||
...Empty,
|
||||
async play({ canvasElement }) {
|
||||
await expect(canvasElement).toHaveTextContent(i18n.t('_ago.yearsAgo', { n: 1 }));
|
||||
await expect(canvasElement).toHaveTextContent(i18n.tsx._ago.yearsAgo({ n: 1 }));
|
||||
},
|
||||
args: {
|
||||
...Empty.args,
|
||||
|
|
|
@ -55,21 +55,21 @@ const relative = computed<string>(() => {
|
|||
if (invalid) return i18n.ts._ago.invalid;
|
||||
|
||||
return (
|
||||
ago.value >= 31536000 ? i18n.t('_ago.yearsAgo', { n: Math.round(ago.value / 31536000).toString() }) :
|
||||
ago.value >= 2592000 ? i18n.t('_ago.monthsAgo', { n: Math.round(ago.value / 2592000).toString() }) :
|
||||
ago.value >= 604800 ? i18n.t('_ago.weeksAgo', { n: Math.round(ago.value / 604800).toString() }) :
|
||||
ago.value >= 86400 ? i18n.t('_ago.daysAgo', { n: Math.round(ago.value / 86400).toString() }) :
|
||||
ago.value >= 3600 ? i18n.t('_ago.hoursAgo', { n: Math.round(ago.value / 3600).toString() }) :
|
||||
ago.value >= 60 ? i18n.t('_ago.minutesAgo', { n: (~~(ago.value / 60)).toString() }) :
|
||||
ago.value >= 10 ? i18n.t('_ago.secondsAgo', { n: (~~(ago.value % 60)).toString() }) :
|
||||
ago.value >= 31536000 ? i18n.tsx._ago.yearsAgo({ n: Math.round(ago.value / 31536000).toString() }) :
|
||||
ago.value >= 2592000 ? i18n.tsx._ago.monthsAgo({ n: Math.round(ago.value / 2592000).toString() }) :
|
||||
ago.value >= 604800 ? i18n.tsx._ago.weeksAgo({ n: Math.round(ago.value / 604800).toString() }) :
|
||||
ago.value >= 86400 ? i18n.tsx._ago.daysAgo({ n: Math.round(ago.value / 86400).toString() }) :
|
||||
ago.value >= 3600 ? i18n.tsx._ago.hoursAgo({ n: Math.round(ago.value / 3600).toString() }) :
|
||||
ago.value >= 60 ? i18n.tsx._ago.minutesAgo({ n: (~~(ago.value / 60)).toString() }) :
|
||||
ago.value >= 10 ? i18n.tsx._ago.secondsAgo({ n: (~~(ago.value % 60)).toString() }) :
|
||||
ago.value >= -3 ? i18n.ts._ago.justNow :
|
||||
ago.value < -31536000 ? i18n.t('_timeIn.years', { n: Math.round(-ago.value / 31536000).toString() }) :
|
||||
ago.value < -2592000 ? i18n.t('_timeIn.months', { n: Math.round(-ago.value / 2592000).toString() }) :
|
||||
ago.value < -604800 ? i18n.t('_timeIn.weeks', { n: Math.round(-ago.value / 604800).toString() }) :
|
||||
ago.value < -86400 ? i18n.t('_timeIn.days', { n: Math.round(-ago.value / 86400).toString() }) :
|
||||
ago.value < -3600 ? i18n.t('_timeIn.hours', { n: Math.round(-ago.value / 3600).toString() }) :
|
||||
ago.value < -60 ? i18n.t('_timeIn.minutes', { n: (~~(-ago.value / 60)).toString() }) :
|
||||
i18n.t('_timeIn.seconds', { n: (~~(-ago.value % 60)).toString() })
|
||||
ago.value < -31536000 ? i18n.tsx._timeIn.years({ n: Math.round(-ago.value / 31536000).toString() }) :
|
||||
ago.value < -2592000 ? i18n.tsx._timeIn.months({ n: Math.round(-ago.value / 2592000).toString() }) :
|
||||
ago.value < -604800 ? i18n.tsx._timeIn.weeks({ n: Math.round(-ago.value / 604800).toString() }) :
|
||||
ago.value < -86400 ? i18n.tsx._timeIn.days({ n: Math.round(-ago.value / 86400).toString() }) :
|
||||
ago.value < -3600 ? i18n.tsx._timeIn.hours({ n: Math.round(-ago.value / 3600).toString() }) :
|
||||
ago.value < -60 ? i18n.tsx._timeIn.minutes({ n: (~~(-ago.value / 60)).toString() }) :
|
||||
i18n.tsx._timeIn.seconds({ n: (~~(-ago.value % 60)).toString() })
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
export default function(props: { src: string; tag?: string; textTag?: string; }, { slots }) {
|
||||
let str = props.src;
|
||||
const parsed = [] as (string | { arg: string; })[];
|
||||
while (true) {
|
||||
const nextBracketOpen = str.indexOf('{');
|
||||
const nextBracketClose = str.indexOf('}');
|
||||
|
||||
if (nextBracketOpen === -1) {
|
||||
parsed.push(str);
|
||||
break;
|
||||
} else {
|
||||
if (nextBracketOpen > 0) parsed.push(str.substring(0, nextBracketOpen));
|
||||
parsed.push({
|
||||
arg: str.substring(nextBracketOpen + 1, nextBracketClose),
|
||||
});
|
||||
}
|
||||
|
||||
str = str.substring(nextBracketClose + 1);
|
||||
}
|
||||
|
||||
return h(props.tag ?? 'span', parsed.map(x => typeof x === 'string' ? (props.textTag ? h(props.textTag, x) : x) : slots[x.arg]()));
|
||||
}
|
|
@ -16,7 +16,7 @@ import MkUserName from './global/MkUserName.vue';
|
|||
import MkEllipsis from './global/MkEllipsis.vue';
|
||||
import MkTime from './global/MkTime.vue';
|
||||
import MkUrl from './global/MkUrl.vue';
|
||||
import I18n from './global/i18n.js';
|
||||
import I18n from './global/I18n.vue';
|
||||
import RouterView from './global/RouterView.vue';
|
||||
import MkLoading from './global/MkLoading.vue';
|
||||
import MkError from './global/MkError.vue';
|
||||
|
|
|
@ -162,3 +162,27 @@ export const DEFAULT_NOT_FOUND_IMAGE_URL = 'https://launcher.moe/missingpage.web
|
|||
export const DEFAULT_INFO_IMAGE_URL = 'https://launcher.moe/nothinghere.png';
|
||||
|
||||
export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'scale', 'position', 'fg', 'bg', 'border', 'font', 'blur', 'rainbow', 'sparkle', 'rotate', 'ruby', 'unixtime'];
|
||||
export const MFM_PARAMS: Record<typeof MFM_TAGS[number], string[]> = {
|
||||
tada: ['speed=', 'delay='],
|
||||
jelly: ['speed=', 'delay='],
|
||||
twitch: ['speed=', 'delay='],
|
||||
shake: ['speed=', 'delay='],
|
||||
spin: ['speed=', 'delay=', 'left', 'alternate', 'x', 'y'],
|
||||
jump: ['speed=', 'delay='],
|
||||
bounce: ['speed=', 'delay='],
|
||||
flip: ['h', 'v'],
|
||||
x2: [],
|
||||
x3: [],
|
||||
x4: [],
|
||||
scale: ['x=', 'y='],
|
||||
position: ['x=', 'y='],
|
||||
fg: ['color='],
|
||||
bg: ['color='],
|
||||
border: ['width=', 'style=', 'color=', 'radius=', 'noclip'],
|
||||
font: ['serif', 'monospace', 'cursive', 'fantasy', 'emoji', 'math'],
|
||||
blur: [],
|
||||
rainbow: ['speed=', 'delay='],
|
||||
rotate: ['deg='],
|
||||
ruby: [],
|
||||
unixtime: [],
|
||||
};
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
export function hms(ms: number, options: {
|
||||
export function hms(ms: number, options?: {
|
||||
textFormat?: 'colon' | 'locale';
|
||||
enableSeconds?: boolean;
|
||||
enableMs?: boolean;
|
||||
|
|
|
@ -15,6 +15,7 @@ const page = (loader: AsyncComponentLoader<any>) => defineAsyncComponent({
|
|||
loadingComponent: MkLoading,
|
||||
errorComponent: MkError,
|
||||
});
|
||||
|
||||
const routes = [{
|
||||
path: '/@:initUser/pages/:initPageName/view-source',
|
||||
component: page(() => import('@/pages/page-editor/page-editor.vue')),
|
||||
|
@ -332,7 +333,12 @@ const routes = [{
|
|||
component: page(() => import('@/pages/registry.vue')),
|
||||
}, {
|
||||
path: '/install-extentions',
|
||||
component: page(() => import('@/pages/install-extentions.vue')),
|
||||
// Note: This path is kept for compatibility. It may be deleted.
|
||||
component: page(() => import('@/pages/install-extensions.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/install-extensions',
|
||||
component: page(() => import('@/pages/install-extensions.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/admin/user/:userId',
|
||||
|
@ -527,18 +533,26 @@ const routes = [{
|
|||
path: '/timeline/antenna/:antennaId',
|
||||
component: page(() => import('@/pages/antenna-timeline.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/games',
|
||||
component: page(() => import('@/pages/games.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/clicker',
|
||||
component: page(() => import('@/pages/clicker.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/games',
|
||||
component: page(() => import('@/pages/games.vue')),
|
||||
loginRequired: false,
|
||||
}, {
|
||||
path: '/bubble-game',
|
||||
component: page(() => import('@/pages/drop-and-fusion.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/reversi',
|
||||
component: page(() => import('@/pages/reversi/index.vue')),
|
||||
loginRequired: false,
|
||||
}, {
|
||||
path: '/reversi/g/:gameId',
|
||||
component: page(() => import('@/pages/reversi/game.vue')),
|
||||
loginRequired: false,
|
||||
}, {
|
||||
path: '/timeline',
|
||||
component: page(() => import('@/pages/timeline.vue')),
|
||||
|
|
|
@ -10,6 +10,7 @@ import { I18n } from '@/scripts/i18n.js';
|
|||
|
||||
export const i18n = markRaw(new I18n<Locale>(locale));
|
||||
|
||||
export function updateI18n(newLocale) {
|
||||
i18n.ts = newLocale;
|
||||
export function updateI18n(newLocale: Locale) {
|
||||
// @ts-expect-error -- private field
|
||||
i18n.locale = newLocale;
|
||||
}
|
||||
|
|
|
@ -22,7 +22,8 @@
|
|||
style-src 'self' 'unsafe-inline';
|
||||
img-src 'self' data: blob: www.google.com xn--931a.moe localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
|
||||
media-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
|
||||
connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://newassets.hcaptcha.com;"
|
||||
connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://newassets.hcaptcha.com;
|
||||
frame-src *;"
|
||||
/>
|
||||
<meta property="og:site_name" content="[DEV BUILD] Misskey" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
|
|
@ -119,8 +119,8 @@ export const navbarItemDef = reactive({
|
|||
to: '/my/achievements',
|
||||
},
|
||||
games: {
|
||||
title: 'Sharkey Games',
|
||||
icon: 'ph-gane-controller ph-bold ph-lg',
|
||||
title: 'Misskey Games',
|
||||
icon: 'ph-game-controller ph-bold ph-lg',
|
||||
to: '/games',
|
||||
},
|
||||
ui: {
|
||||
|
|
|
@ -419,7 +419,7 @@ export function form(title, form) {
|
|||
});
|
||||
}
|
||||
|
||||
export async function selectUser(opts: { includeSelf?: boolean } = {}) {
|
||||
export async function selectUser(opts: { includeSelf?: boolean } = {}): Promise<Misskey.entities.UserLite> {
|
||||
return new Promise((resolve, reject) => {
|
||||
popup(defineAsyncComponent(() => import('@/components/MkUserSelectDialog.vue')), {
|
||||
includeSelf: opts.includeSelf,
|
||||
|
|
|
@ -6,99 +6,101 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer v-if="tab === 'overview'" :contentMax="600" :marginMin="20">
|
||||
<div class="_gaps_m">
|
||||
<div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }">
|
||||
<div style="overflow: clip;">
|
||||
<img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" alt="" :class="$style.bannerIcon"/>
|
||||
<div :class="$style.bannerName">
|
||||
<b>{{ instance.name ?? host }}</b>
|
||||
<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
|
||||
<MkSpacer v-if="tab === 'overview'" :contentMax="600" :marginMin="20">
|
||||
<div class="_gaps_m">
|
||||
<div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }">
|
||||
<div style="overflow: clip;">
|
||||
<img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" alt="" :class="$style.bannerIcon"/>
|
||||
<div :class="$style.bannerName">
|
||||
<b>{{ instance.name ?? host }}</b>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.description }}</template>
|
||||
<template #value><div v-html="instance.description"></div></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.description }}</template>
|
||||
<template #value><div v-html="instance.description"></div></template>
|
||||
</MkKeyValue>
|
||||
|
||||
<FormSection>
|
||||
<div class="_gaps_m">
|
||||
<MkKeyValue :copy="version">
|
||||
<template #key>Sharkey</template>
|
||||
<template #value>{{ version }}</template>
|
||||
</MkKeyValue>
|
||||
<div v-html="i18n.t('poweredByMisskeyDescription', { name: instance.name ?? host })">
|
||||
</div>
|
||||
<FormLink to="/about-sharkey">{{ i18n.ts.aboutMisskey }}</FormLink>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<div class="_gaps_m">
|
||||
<FormSplit>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.administrator }}</template>
|
||||
<template #value>{{ instance.maintainerName }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.contact }}</template>
|
||||
<template #value>{{ instance.maintainerEmail }}</template>
|
||||
</MkKeyValue>
|
||||
</FormSplit>
|
||||
<FormLink v-if="instance.impressumUrl" :to="instance.impressumUrl" external>{{ i18n.ts.impressum }}</FormLink>
|
||||
<div class="_gaps_s">
|
||||
<MkFolder v-if="instance.serverRules.length > 0">
|
||||
<template #label>{{ i18n.ts.serverRules }}</template>
|
||||
|
||||
<ol class="_gaps_s" :class="$style.rules">
|
||||
<li v-for="item, index in instance.serverRules" :key="index" :class="$style.rule"><div :class="$style.ruleText" v-html="item"></div></li>
|
||||
</ol>
|
||||
</MkFolder>
|
||||
<FormLink v-if="instance.tosUrl" :to="instance.tosUrl" external>{{ i18n.ts.termsOfService }}</FormLink>
|
||||
<FormLink v-if="instance.privacyPolicyUrl" :to="instance.privacyPolicyUrl" external>{{ i18n.ts.privacyPolicy }}</FormLink>
|
||||
<FormLink v-if="instance.donationUrl" :to="instance.donationUrl" external>{{ i18n.ts.donation }}</FormLink>
|
||||
</div>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
<FormSuspense :p="initStats">
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts.statistics }}</template>
|
||||
<FormSplit>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.users }}</template>
|
||||
<template #value>{{ number(stats.originalUsersCount) }}</template>
|
||||
<div class="_gaps_m">
|
||||
<MkKeyValue :copy="version">
|
||||
<template #key>Sharkey</template>
|
||||
<template #value>{{ version }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.notes }}</template>
|
||||
<template #value>{{ number(stats.originalNotesCount) }}</template>
|
||||
</MkKeyValue>
|
||||
</FormSplit>
|
||||
<div v-html="i18n.tsx.poweredByMisskeyDescription({ name: instance.name ?? host })">
|
||||
</div>
|
||||
<FormLink to="/about-sharkey">{{ i18n.ts.aboutMisskey }}</FormLink>
|
||||
</div>
|
||||
</FormSection>
|
||||
</FormSuspense>
|
||||
|
||||
<FormSection>
|
||||
<template #label>Well-known resources</template>
|
||||
<div class="_gaps_s">
|
||||
<FormLink :to="`/.well-known/host-meta`" external>host-meta</FormLink>
|
||||
<FormLink :to="`/.well-known/host-meta.json`" external>host-meta.json</FormLink>
|
||||
<FormLink :to="`/.well-known/nodeinfo`" external>nodeinfo</FormLink>
|
||||
<FormLink :to="`/robots.txt`" external>robots.txt</FormLink>
|
||||
<FormLink :to="`/manifest.json`" external>manifest.json</FormLink>
|
||||
</div>
|
||||
</FormSection>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
<MkSpacer v-else-if="tab === 'emojis'" :contentMax="1000" :marginMin="20">
|
||||
<XEmojis/>
|
||||
</MkSpacer>
|
||||
<MkSpacer v-else-if="tab === 'federation'" :contentMax="1000" :marginMin="20">
|
||||
<XFederation/>
|
||||
</MkSpacer>
|
||||
<MkSpacer v-else-if="tab === 'charts'" :contentMax="1000" :marginMin="20">
|
||||
<MkInstanceStats/>
|
||||
</MkSpacer>
|
||||
<FormSection>
|
||||
<div class="_gaps_m">
|
||||
<FormSplit>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.administrator }}</template>
|
||||
<template #value>{{ instance.maintainerName }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.contact }}</template>
|
||||
<template #value>{{ instance.maintainerEmail }}</template>
|
||||
</MkKeyValue>
|
||||
</FormSplit>
|
||||
<FormLink v-if="instance.impressumUrl" :to="instance.impressumUrl" external>{{ i18n.ts.impressum }}</FormLink>
|
||||
<div class="_gaps_s">
|
||||
<MkFolder v-if="instance.serverRules.length > 0">
|
||||
<template #label>{{ i18n.ts.serverRules }}</template>
|
||||
|
||||
<ol class="_gaps_s" :class="$style.rules">
|
||||
<li v-for="item, index in instance.serverRules" :key="index" :class="$style.rule"><div :class="$style.ruleText" v-html="item"></div></li>
|
||||
</ol>
|
||||
</MkFolder>
|
||||
<FormLink v-if="instance.tosUrl" :to="instance.tosUrl" external>{{ i18n.ts.termsOfService }}</FormLink>
|
||||
<FormLink v-if="instance.privacyPolicyUrl" :to="instance.privacyPolicyUrl" external>{{ i18n.ts.privacyPolicy }}</FormLink>
|
||||
<FormLink v-if="instance.donationUrl" :to="instance.donationUrl" external>{{ i18n.ts.donation }}</FormLink>
|
||||
</div>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
<FormSuspense :p="initStats">
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts.statistics }}</template>
|
||||
<FormSplit>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.users }}</template>
|
||||
<template #value>{{ number(stats.originalUsersCount) }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.notes }}</template>
|
||||
<template #value>{{ number(stats.originalNotesCount) }}</template>
|
||||
</MkKeyValue>
|
||||
</FormSplit>
|
||||
</FormSection>
|
||||
</FormSuspense>
|
||||
|
||||
<FormSection>
|
||||
<template #label>Well-known resources</template>
|
||||
<div class="_gaps_s">
|
||||
<FormLink :to="`/.well-known/host-meta`" external>host-meta</FormLink>
|
||||
<FormLink :to="`/.well-known/host-meta.json`" external>host-meta.json</FormLink>
|
||||
<FormLink :to="`/.well-known/nodeinfo`" external>nodeinfo</FormLink>
|
||||
<FormLink :to="`/robots.txt`" external>robots.txt</FormLink>
|
||||
<FormLink :to="`/manifest.json`" external>manifest.json</FormLink>
|
||||
</div>
|
||||
</FormSection>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
<MkSpacer v-else-if="tab === 'emojis'" :contentMax="1000" :marginMin="20">
|
||||
<XEmojis/>
|
||||
</MkSpacer>
|
||||
<MkSpacer v-else-if="tab === 'federation'" :contentMax="1000" :marginMin="20">
|
||||
<XFederation/>
|
||||
</MkSpacer>
|
||||
<MkSpacer v-else-if="tab === 'charts'" :contentMax="1000" :marginMin="20">
|
||||
<MkInstanceStats/>
|
||||
</MkSpacer>
|
||||
</MkHorizontalSwipe>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
|
@ -115,6 +117,7 @@ import FormSplit from '@/components/form/split.vue';
|
|||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import MkInstanceStats from '@/components/MkInstanceStats.vue';
|
||||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import number from '@/filters/number.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
|
|
@ -104,7 +104,7 @@ fetch();
|
|||
async function del() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('removeAreYouSure', { x: file.value.name }),
|
||||
text: i18n.tsx.removeAreYouSure({ x: file.value.name }),
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
|
|
|
@ -169,9 +169,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkSelect>
|
||||
</div>
|
||||
<div class="charts">
|
||||
<div class="label">{{ i18n.t('recentNHours', { n: 90 }) }}</div>
|
||||
<div class="label">{{ i18n.tsx.recentNHours({ n: 90 }) }}</div>
|
||||
<MkChart class="chart" :src="chartSrc" span="hour" :limit="90" :args="{ user, withoutAll: true }" :detailed="true"></MkChart>
|
||||
<div class="label">{{ i18n.t('recentNDays', { n: 90 }) }}</div>
|
||||
<div class="label">{{ i18n.tsx.recentNDays({ n: 90 }) }}</div>
|
||||
<MkChart class="chart" :src="chartSrc" span="day" :limit="90" :args="{ user, withoutAll: true }" :detailed="true"></MkChart>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -297,7 +297,7 @@ async function resetPassword() {
|
|||
});
|
||||
os.alert({
|
||||
type: 'success',
|
||||
text: i18n.t('newPasswordIs', { password }),
|
||||
text: i18n.tsx.newPasswordIs({ password }),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -406,7 +406,7 @@ async function deleteAccount() {
|
|||
if (confirm.canceled) return;
|
||||
|
||||
const typed = await os.inputText({
|
||||
text: i18n.t('typeToConfirm', { x: user.value?.username }),
|
||||
text: i18n.tsx.typeToConfirm({ x: user.value?.username }),
|
||||
});
|
||||
if (typed.canceled) return;
|
||||
|
||||
|
|
|
@ -160,7 +160,7 @@ function add() {
|
|||
function remove(ad) {
|
||||
os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('removeAreYouSure', { x: ad.url }),
|
||||
text: i18n.tsx.removeAreYouSure({ x: ad.url }),
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
ads.value = ads.value.filter(x => x !== ad);
|
||||
|
|
|
@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkSwitch v-model="announcement.needConfirmationToRead" :helpText="i18n.ts._announcement.needConfirmationToReadDescription">
|
||||
{{ i18n.ts._announcement.needConfirmationToRead }}
|
||||
</MkSwitch>
|
||||
<p v-if="announcement.reads">{{ i18n.t('nUsersRead', { n: announcement.reads }) }}</p>
|
||||
<p v-if="announcement.reads">{{ i18n.tsx.nUsersRead({ n: announcement.reads }) }}</p>
|
||||
<div class="buttons _buttons">
|
||||
<MkButton class="button" inline primary @click="save(announcement)"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
|
||||
<MkButton v-if="announcement.id != null" class="button" inline @click="archive(announcement)"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts._announcement.end }} ({{ i18n.ts.archive }})</MkButton>
|
||||
|
@ -109,7 +109,7 @@ function add() {
|
|||
function del(announcement) {
|
||||
os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('deleteAreYouSure', { x: announcement.title }),
|
||||
text: i18n.tsx.deleteAreYouSure({ x: announcement.title }),
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
announcements.value = announcements.value.filter(x => x !== announcement);
|
||||
|
|
|
@ -19,10 +19,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #prefix><i class="ph-link ph-bold ph-lg"></i></template>
|
||||
<template #label>{{ i18n.ts._serverSettings.iconUrl }} (App/192px)</template>
|
||||
<template #caption>
|
||||
<div>{{ i18n.t('_serverSettings.appIconDescription', { host: instance.name ?? host }) }}</div>
|
||||
<div>{{ i18n.tsx._serverSettings.appIconDescription({ host: instance.name ?? host }) }}</div>
|
||||
<div>({{ i18n.ts._serverSettings.appIconUsageExample }})</div>
|
||||
<div>{{ i18n.ts._serverSettings.appIconStyleRecommendation }}</div>
|
||||
<div><strong>{{ i18n.t('_serverSettings.appIconResolutionMustBe', { resolution: '192x192px' }) }}</strong></div>
|
||||
<div><strong>{{ i18n.tsx._serverSettings.appIconResolutionMustBe({ resolution: '192x192px' }) }}</strong></div>
|
||||
</template>
|
||||
</MkInput>
|
||||
|
||||
|
@ -30,10 +30,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #prefix><i class="ph-link ph-bold ph-lg"></i></template>
|
||||
<template #label>{{ i18n.ts._serverSettings.iconUrl }} (App/512px)</template>
|
||||
<template #caption>
|
||||
<div>{{ i18n.t('_serverSettings.appIconDescription', { host: instance.name ?? host }) }}</div>
|
||||
<div>{{ i18n.tsx._serverSettings.appIconDescription({ host: instance.name ?? host }) }}</div>
|
||||
<div>({{ i18n.ts._serverSettings.appIconUsageExample }})</div>
|
||||
<div>{{ i18n.ts._serverSettings.appIconStyleRecommendation }}</div>
|
||||
<div><strong>{{ i18n.t('_serverSettings.appIconResolutionMustBe', { resolution: '512x512px' }) }}</strong></div>
|
||||
<div><strong>{{ i18n.tsx._serverSettings.appIconResolutionMustBe({ resolution: '512x512px' }) }}</strong></div>
|
||||
</template>
|
||||
</MkInput>
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<i v-if="relay.status === 'accepted'" class="ph-check ph-bold ph-lg" :class="$style.icon" style="color: var(--success);"></i>
|
||||
<i v-else-if="relay.status === 'rejected'" class="ph-prohibit ph-bold ph-lg" :class="$style.icon" style="color: var(--error);"></i>
|
||||
<i v-else class="ph-clock ph-bold ph-lg" :class="$style.icon"></i>
|
||||
<span>{{ i18n.t(`_relayStatus.${relay.status}`) }}</span>
|
||||
<span>{{ i18n.ts._relayStatus[relay.status] }}</span>
|
||||
</div>
|
||||
<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.remove }}</MkButton>
|
||||
</div>
|
||||
|
|
|
@ -104,7 +104,7 @@ function edit() {
|
|||
async function del() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('deleteAreYouSure', { x: role.name }),
|
||||
text: i18n.tsx.deleteAreYouSure({ x: role.name }),
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
|
|
|
@ -28,27 +28,28 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<div class="_gaps_m">
|
||||
<span>{{ i18n.ts.activeEmailValidationDescription }}</span>
|
||||
<MkSwitch v-model="enableActiveEmailValidation" @update:modelValue="save">
|
||||
<MkSwitch v-model="enableActiveEmailValidation">
|
||||
<template #label>Enable</template>
|
||||
</MkSwitch>
|
||||
<MkSwitch v-model="enableVerifymailApi" @update:modelValue="save">
|
||||
<MkSwitch v-model="enableVerifymailApi">
|
||||
<template #label>Use Verifymail.io API</template>
|
||||
</MkSwitch>
|
||||
<MkInput v-model="verifymailAuthKey" @update:modelValue="save">
|
||||
<MkInput v-model="verifymailAuthKey">
|
||||
<template #prefix><i class="ph-key ph-bold ph-lg"></i></template>
|
||||
<template #label>Verifymail.io API Auth Key</template>
|
||||
</MkInput>
|
||||
<MkSwitch v-model="enableTruemailApi" @update:modelValue="save">
|
||||
<MkSwitch v-model="enableTruemailApi">
|
||||
<template #label>Use TrueMail API</template>
|
||||
</MkSwitch>
|
||||
<MkInput v-model="truemailInstance" @update:modelValue="save">
|
||||
<MkInput v-model="truemailInstance">
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>TrueMail API Instance</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="truemailAuthKey" @update:modelValue="save">
|
||||
<MkInput v-model="truemailAuthKey">
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>TrueMail API Auth Key</template>
|
||||
</MkInput>
|
||||
<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
|
@ -136,7 +137,10 @@ async function init() {
|
|||
enableActiveEmailValidation.value = meta.enableActiveEmailValidation;
|
||||
enableVerifymailApi.value = meta.enableVerifymailApi;
|
||||
verifymailAuthKey.value = meta.verifymailAuthKey;
|
||||
bannedEmailDomains.value = meta.bannedEmailDomains.join('\n');
|
||||
enableTruemailApi.value = meta.enableTruemailApi;
|
||||
truemailInstance.value = meta.truemailInstance;
|
||||
truemailAuthKey.value = meta.truemailAuthKey;
|
||||
bannedEmailDomains.value = meta.bannedEmailDomains?.join('\n') || "";
|
||||
}
|
||||
|
||||
function save() {
|
||||
|
|
|
@ -7,34 +7,36 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="800">
|
||||
<div class="_gaps">
|
||||
<MkInfo v-if="$i && $i.hasUnreadAnnouncement && tab === 'current'" warn>{{ i18n.ts.youHaveUnreadAnnouncements }}</MkInfo>
|
||||
<MkPagination ref="paginationEl" :key="tab" v-slot="{items}" :pagination="tab === 'current' ? paginationCurrent : paginationPast" class="_gaps">
|
||||
<section v-for="announcement in items" :key="announcement.id" class="_panel" :class="$style.announcement">
|
||||
<div v-if="announcement.forYou" :class="$style.forYou"><i class="ph-push-pin ph-bold ph-lg"></i> {{ i18n.ts.forYou }}</div>
|
||||
<div :class="$style.header">
|
||||
<span v-if="$i && !announcement.silence && !announcement.isRead" style="margin-right: 0.5em;">🆕</span>
|
||||
<span style="margin-right: 0.5em;">
|
||||
<i v-if="announcement.icon === 'info'" class="ph-info ph-bold ph-lg"></i>
|
||||
<i v-else-if="announcement.icon === 'warning'" class="ph-warning ph-bold ph-lg" style="color: var(--warn);"></i>
|
||||
<i v-else-if="announcement.icon === 'error'" class="ph-x-circle ph-bold ph-lg" style="color: var(--error);"></i>
|
||||
<i v-else-if="announcement.icon === 'success'" class="ph-check ph-bold ph-lg" style="color: var(--success);"></i>
|
||||
</span>
|
||||
<span>{{ announcement.title }}</span>
|
||||
</div>
|
||||
<div :class="$style.content">
|
||||
<Mfm :text="announcement.text"/>
|
||||
<img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
|
||||
<div style="opacity: 0.7; font-size: 85%;">
|
||||
<MkTime :time="announcement.updatedAt ?? announcement.createdAt" mode="detail"/>
|
||||
<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
|
||||
<div :key="tab" class="_gaps">
|
||||
<MkInfo v-if="$i && $i.hasUnreadAnnouncement && tab === 'current'" warn>{{ i18n.ts.youHaveUnreadAnnouncements }}</MkInfo>
|
||||
<MkPagination ref="paginationEl" :key="tab" v-slot="{items}" :pagination="tab === 'current' ? paginationCurrent : paginationPast" class="_gaps">
|
||||
<section v-for="announcement in items" :key="announcement.id" class="_panel" :class="$style.announcement">
|
||||
<div v-if="announcement.forYou" :class="$style.forYou"><i class="ph-push-pin ph-bold ph-lg"></i> {{ i18n.ts.forYou }}</div>
|
||||
<div :class="$style.header">
|
||||
<span v-if="$i && !announcement.silence && !announcement.isRead" style="margin-right: 0.5em;">🆕</span>
|
||||
<span style="margin-right: 0.5em;">
|
||||
<i v-if="announcement.icon === 'info'" class="ph-info ph-bold ph-lg"></i>
|
||||
<i v-else-if="announcement.icon === 'warning'" class="ph-warning ph-bold ph-lg" style="color: var(--warn);"></i>
|
||||
<i v-else-if="announcement.icon === 'error'" class="ph-x-circle ph-bold ph-lg" style="color: var(--error);"></i>
|
||||
<i v-else-if="announcement.icon === 'success'" class="ph-check ph-bold ph-lg" style="color: var(--success);"></i>
|
||||
</span>
|
||||
<span>{{ announcement.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="tab !== 'past' && $i && !announcement.silence && !announcement.isRead" :class="$style.footer">
|
||||
<MkButton primary @click="read(announcement)"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.gotIt }}</MkButton>
|
||||
</div>
|
||||
</section>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<div :class="$style.content">
|
||||
<Mfm :text="announcement.text"/>
|
||||
<img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
|
||||
<div style="opacity: 0.7; font-size: 85%;">
|
||||
<MkTime :time="announcement.updatedAt ?? announcement.createdAt" mode="detail"/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="tab !== 'past' && $i && !announcement.silence && !announcement.isRead" :class="$style.footer">
|
||||
<MkButton primary @click="read(announcement)"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.gotIt }}</MkButton>
|
||||
</div>
|
||||
</section>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkHorizontalSwipe>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
@ -44,6 +46,7 @@ import { ref, computed } from 'vue';
|
|||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
@ -75,7 +78,7 @@ async function read(announcement) {
|
|||
const confirm = await os.confirm({
|
||||
type: 'question',
|
||||
title: i18n.ts._announcement.readConfirmTitle,
|
||||
text: i18n.t('_announcement.readConfirmText', { title: announcement.title }),
|
||||
text: i18n.tsx._announcement.readConfirmText({ title: announcement.title }),
|
||||
});
|
||||
if (confirm.canceled) return;
|
||||
}
|
||||
|
|
|
@ -6,12 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<section>
|
||||
<div v-if="app.permission.length > 0">
|
||||
<p>{{ i18n.t('_auth.permission', { name }) }}</p>
|
||||
<p>{{ i18n.tsx._auth.permission({ name }) }}</p>
|
||||
<ul>
|
||||
<li v-for="p in app.permission" :key="p">{{ i18n.t(`_permissions.${p}`) }}</li>
|
||||
<li v-for="p in app.permission" :key="p">{{ i18n.ts._permissions[p] }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>{{ i18n.t('_auth.shareAccess', { name: `${name} (${app.id})` }) }}</div>
|
||||
<div>{{ i18n.tsx._auth.shareAccess({ name: `${name} (${app.id})` }) }}</div>
|
||||
<div :class="$style.buttons">
|
||||
<MkButton inline @click="cancel">{{ i18n.ts.cancel }}</MkButton>
|
||||
<MkButton inline primary @click="accept">{{ i18n.ts.accept }}</MkButton>
|
||||
|
|
|
@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<h1>{{ i18n.ts._auth.denied }}</h1>
|
||||
</div>
|
||||
<div v-if="state == 'accepted' && session">
|
||||
<h1>{{ session.app.isAuthorized ? i18n.t('already-authorized') : i18n.ts.allowed }}</h1>
|
||||
<h1>{{ session.app.isAuthorized ? i18n.ts['already-authorized'] : i18n.ts.allowed }}</h1>
|
||||
<p v-if="session.app.callbackUrl">
|
||||
{{ i18n.ts._auth.callback }}
|
||||
<MkEllipsis/>
|
||||
|
|
|
@ -60,7 +60,7 @@ function add() {
|
|||
function del(avatarDecoration) {
|
||||
os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('deleteAreYouSure', { x: avatarDecoration.name }),
|
||||
text: i18n.tsx.deleteAreYouSure({ x: avatarDecoration.name }),
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
avatarDecorations.value = avatarDecorations.value.filter(x => x !== avatarDecoration);
|
||||
|
|
|
@ -174,7 +174,7 @@ function save() {
|
|||
async function archive() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
title: i18n.t('channelArchiveConfirmTitle', { name: name.value }),
|
||||
title: i18n.tsx.channelArchiveConfirmTitle({ name: name.value }),
|
||||
text: i18n.ts.channelArchiveConfirmDescription,
|
||||
});
|
||||
|
||||
|
|
|
@ -7,53 +7,55 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="700" :class="$style.main">
|
||||
<div v-if="channel && tab === 'overview'" class="_gaps">
|
||||
<div class="_panel" :class="$style.bannerContainer">
|
||||
<XChannelFollowButton :channel="channel" :full="true" :class="$style.subscribe"/>
|
||||
<MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" asLike class="button" rounded primary :class="$style.favorite" @click="unfavorite()"><i class="ph-star ph-bold ph-lg"></i></MkButton>
|
||||
<MkButton v-else v-tooltip="i18n.ts.favorite" asLike class="button" rounded :class="$style.favorite" @click="favorite()"><i class="ph-star ph-bold ph-lg"></i></MkButton>
|
||||
<div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : undefined }" :class="$style.banner">
|
||||
<div :class="$style.bannerStatus">
|
||||
<div><i class="ph-users ph-bold ph-lg ti-fw"></i><I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div>
|
||||
<div><i class="ph-pencil ph-bold ph-lg ti-fw"></i><I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></I18n></div>
|
||||
<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
|
||||
<div v-if="channel && tab === 'overview'" key="overview" class="_gaps">
|
||||
<div class="_panel" :class="$style.bannerContainer">
|
||||
<XChannelFollowButton :channel="channel" :full="true" :class="$style.subscribe"/>
|
||||
<MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" asLike class="button" rounded primary :class="$style.favorite" @click="unfavorite()"><i class="ph-star ph-bold ph-lg"></i></MkButton>
|
||||
<MkButton v-else v-tooltip="i18n.ts.favorite" asLike class="button" rounded :class="$style.favorite" @click="favorite()"><i class="ph-star ph-bold ph-lg"></i></MkButton>
|
||||
<div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : undefined }" :class="$style.banner">
|
||||
<div :class="$style.bannerStatus">
|
||||
<div><i class="ph-users ph-bold ph-lg"></i><I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div>
|
||||
<div><i class="ph-pencil ph-bold ph-lg"></i><I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></I18n></div>
|
||||
</div>
|
||||
<div v-if="channel.isSensitive" :class="$style.sensitiveIndicator">{{ i18n.ts.sensitive }}</div>
|
||||
<div :class="$style.bannerFade"></div>
|
||||
</div>
|
||||
<div v-if="channel.description" :class="$style.description">
|
||||
<Mfm :text="channel.description" :isNote="false"/>
|
||||
</div>
|
||||
<div v-if="channel.isSensitive" :class="$style.sensitiveIndicator">{{ i18n.ts.sensitive }}</div>
|
||||
<div :class="$style.bannerFade"></div>
|
||||
</div>
|
||||
<div v-if="channel.description" :class="$style.description">
|
||||
<Mfm :text="channel.description" :isNote="false"/>
|
||||
|
||||
<MkFoldableSection>
|
||||
<template #header><i class="ph-push-pin ph-bold ph-lg" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedNotes }}</template>
|
||||
<div v-if="channel.pinnedNotes && channel.pinnedNotes.length > 0" class="_gaps">
|
||||
<MkNote v-for="note in channel.pinnedNotes" :key="note.id" class="_panel" :note="note"/>
|
||||
</div>
|
||||
</MkFoldableSection>
|
||||
</div>
|
||||
<div v-if="channel && tab === 'timeline'" key="timeline" class="_gaps">
|
||||
<MkInfo v-if="channel.isArchived" warn>{{ i18n.ts.thisChannelArchived }}</MkInfo>
|
||||
|
||||
<!-- スマホ・タブレットの場合、キーボードが表示されると投稿が見づらくなるので、デスクトップ場合のみ自動でフォーカスを当てる -->
|
||||
<MkPostForm v-if="$i && defaultStore.reactiveState.showFixedPostFormInChannel.value" :channel="channel" class="post-form _panel" fixed :autofocus="deviceKind === 'desktop'"/>
|
||||
|
||||
<MkTimeline :key="channelId" src="channel" :channel="channelId" @before="before" @after="after" @note="miLocalStorage.setItemAsJson(`channelLastReadedAt:${channel.id}`, Date.now())"/>
|
||||
</div>
|
||||
<div v-else-if="tab === 'featured'" key="featured">
|
||||
<MkNotes :pagination="featuredPagination"/>
|
||||
</div>
|
||||
<div v-else-if="tab === 'search'" key="search">
|
||||
<div class="_gaps">
|
||||
<div>
|
||||
<MkInput v-model="searchQuery" @enter="search()">
|
||||
<template #prefix><i class="ph-magnifying-glass ph-bold ph-lg"></i></template>
|
||||
</MkInput>
|
||||
<MkButton primary rounded style="margin-top: 8px;" @click="search()">{{ i18n.ts.search }}</MkButton>
|
||||
</div>
|
||||
<MkNotes v-if="searchPagination" :key="searchKey" :pagination="searchPagination"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MkFoldableSection>
|
||||
<template #header><i class="ph-push-pin ph-bold ph-lg ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedNotes }}</template>
|
||||
<div v-if="channel.pinnedNotes && channel.pinnedNotes.length > 0" class="_gaps">
|
||||
<MkNote v-for="note in channel.pinnedNotes" :key="note.id" class="_panel" :note="note"/>
|
||||
</div>
|
||||
</MkFoldableSection>
|
||||
</div>
|
||||
<div v-if="channel && tab === 'timeline'" class="_gaps">
|
||||
<MkInfo v-if="channel.isArchived" warn>{{ i18n.ts.thisChannelArchived }}</MkInfo>
|
||||
|
||||
<!-- スマホ・タブレットの場合、キーボードが表示されると投稿が見づらくなるので、デスクトップ場合のみ自動でフォーカスを当てる -->
|
||||
<MkPostForm v-if="$i && defaultStore.reactiveState.showFixedPostFormInChannel.value" :channel="channel" class="post-form _panel" fixed :autofocus="deviceKind === 'desktop'"/>
|
||||
|
||||
<MkTimeline :key="channelId" src="channel" :channel="channelId" @before="before" @after="after" @note="miLocalStorage.setItemAsJson(`channelLastReadedAt:${channel.id}`, Date.now())"/>
|
||||
</div>
|
||||
<div v-else-if="tab === 'featured'">
|
||||
<MkNotes :pagination="featuredPagination"/>
|
||||
</div>
|
||||
<div v-else-if="tab === 'search'">
|
||||
<div class="_gaps">
|
||||
<div>
|
||||
<MkInput v-model="searchQuery" @enter="search()">
|
||||
<template #prefix><i class="ph-magnifying-glass ph-bold ph-lg"></i></template>
|
||||
</MkInput>
|
||||
<MkButton primary rounded style="margin-top: 8px;" @click="search()">{{ i18n.ts.search }}</MkButton>
|
||||
</div>
|
||||
<MkNotes v-if="searchPagination" :key="searchKey" :pagination="searchPagination"/>
|
||||
</div>
|
||||
</div>
|
||||
</MkHorizontalSwipe>
|
||||
</MkSpacer>
|
||||
<template #footer>
|
||||
<div :class="$style.footer">
|
||||
|
@ -87,6 +89,7 @@ import { defaultStore } from '@/store.js';
|
|||
import MkNote from '@/components/MkNote.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
||||
import { PageHeaderItem } from '@/types/page-header.js';
|
||||
import { isSupportShare } from '@/scripts/navigator.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
|
@ -100,6 +103,7 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
const tab = ref('overview');
|
||||
|
||||
const channel = ref<Misskey.entities.Channel | null>(null);
|
||||
const favorited = ref(false);
|
||||
const searchQuery = ref('');
|
||||
|
@ -268,6 +272,7 @@ definePageMetadata(computed(() => channel.value ? {
|
|||
.footer {
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
background: var(--acrylicBg);
|
||||
border-top: solid 0.5px var(--divider);
|
||||
}
|
||||
|
||||
|
|
|
@ -7,44 +7,46 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="700">
|
||||
<div v-if="tab === 'search'">
|
||||
<div class="_gaps">
|
||||
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter="search">
|
||||
<template #prefix><i class="ph-magnifying-glass ph-bold ph-lg"></i></template>
|
||||
</MkInput>
|
||||
<MkRadios v-model="searchType" @update:modelValue="search()">
|
||||
<option value="nameAndDescription">{{ i18n.ts._channel.nameAndDescription }}</option>
|
||||
<option value="nameOnly">{{ i18n.ts._channel.nameOnly }}</option>
|
||||
</MkRadios>
|
||||
<MkButton large primary gradate rounded @click="search">{{ i18n.ts.search }}</MkButton>
|
||||
</div>
|
||||
<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
|
||||
<div v-if="tab === 'search'" key="search">
|
||||
<div class="_gaps">
|
||||
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter="search">
|
||||
<template #prefix><i class="ph-magnifying-glass ph-bold ph-lg"></i></template>
|
||||
</MkInput>
|
||||
<MkRadios v-model="searchType" @update:modelValue="search()">
|
||||
<option value="nameAndDescription">{{ i18n.ts._channel.nameAndDescription }}</option>
|
||||
<option value="nameOnly">{{ i18n.ts._channel.nameOnly }}</option>
|
||||
</MkRadios>
|
||||
<MkButton large primary gradate rounded @click="search">{{ i18n.ts.search }}</MkButton>
|
||||
</div>
|
||||
|
||||
<MkFoldableSection v-if="channelPagination">
|
||||
<template #header>{{ i18n.ts.searchResult }}</template>
|
||||
<MkChannelList :key="key" :pagination="channelPagination"/>
|
||||
</MkFoldableSection>
|
||||
</div>
|
||||
<div v-if="tab === 'featured'">
|
||||
<MkPagination v-slot="{items}" :pagination="featuredPagination">
|
||||
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<div v-else-if="tab === 'favorites'">
|
||||
<MkPagination v-slot="{items}" :pagination="favoritesPagination">
|
||||
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<div v-else-if="tab === 'following'">
|
||||
<MkPagination v-slot="{items}" :pagination="followingPagination">
|
||||
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<div v-else-if="tab === 'owned'">
|
||||
<MkButton class="new" @click="create()"><i class="ph-plus ph-bold ph-lg"></i></MkButton>
|
||||
<MkPagination v-slot="{items}" :pagination="ownedPagination">
|
||||
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<MkFoldableSection v-if="channelPagination">
|
||||
<template #header>{{ i18n.ts.searchResult }}</template>
|
||||
<MkChannelList :key="key" :pagination="channelPagination"/>
|
||||
</MkFoldableSection>
|
||||
</div>
|
||||
<div v-if="tab === 'featured'" key="featured">
|
||||
<MkPagination v-slot="{items}" :pagination="featuredPagination">
|
||||
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<div v-else-if="tab === 'favorites'" key="favorites">
|
||||
<MkPagination v-slot="{items}" :pagination="favoritesPagination">
|
||||
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<div v-else-if="tab === 'following'" key="following">
|
||||
<MkPagination v-slot="{items}" :pagination="followingPagination">
|
||||
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<div v-else-if="tab === 'owned'" key="owned">
|
||||
<MkButton class="new" @click="create()"><i class="ph-plus ph-bold ph-lg"></i></MkButton>
|
||||
<MkPagination v-slot="{items}" :pagination="ownedPagination">
|
||||
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkHorizontalSwipe>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
@ -58,6 +60,7 @@ import MkInput from '@/components/MkInput.vue';
|
|||
import MkRadios from '@/components/MkRadios.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { useRouter } from '@/global/router/supplier.js';
|
||||
|
|
|
@ -145,7 +145,7 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{
|
|||
handler: async (): Promise<void> => {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('deleteAreYouSure', { x: clip.value.name }),
|
||||
text: i18n.tsx.deleteAreYouSure({ x: clip.value.name }),
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
|
|
|
@ -180,7 +180,7 @@ async function deleteFile() {
|
|||
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('driveFileDeleteConfirm', { name: file.value.name }),
|
||||
text: i18n.tsx.driveFileDeleteConfirm({ name: file.value.name }),
|
||||
});
|
||||
|
||||
if (canceled) return;
|
||||
|
|
|
@ -9,13 +9,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/>
|
||||
</template>
|
||||
|
||||
<MkSpacer v-if="tab === 'info'" :contentMax="800">
|
||||
<XFileInfo :fileId="fileId"/>
|
||||
</MkSpacer>
|
||||
<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
|
||||
<MkSpacer v-if="tab === 'info'" key="info" :contentMax="800">
|
||||
<XFileInfo :fileId="fileId"/>
|
||||
</MkSpacer>
|
||||
|
||||
<MkSpacer v-else-if="tab === 'notes'" :contentMax="800">
|
||||
<XNotes :fileId="fileId"/>
|
||||
</MkSpacer>
|
||||
<MkSpacer v-else-if="tab === 'notes'" key="notes" :contentMax="800">
|
||||
<XNotes :fileId="fileId"/>
|
||||
</MkSpacer>
|
||||
</MkHorizontalSwipe>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
|
@ -23,6 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { computed, ref, defineAsyncComponent } from 'vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
fileId: string;
|
||||
|
|
|
@ -96,7 +96,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-if="gameMode === 'sweets'"><b>おにぎり<MkNumber :value="score / 130"/>個分</b></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="replaying" :class="$style.replayIndicator"><span :class="$style.replayIndicatorText"><i class="ti ti-player-play"></i> {{ i18n.ts.replaying }}</span></div>
|
||||
<div v-if="replaying" :class="$style.replayIndicator"><span :class="$style.replayIndicatorText"><i class="ph-play ph-bold ph-lg"></i> {{ i18n.ts.replaying }}</span></div>
|
||||
</div>
|
||||
|
||||
<div v-if="replaying" :class="$style.frame">
|
||||
|
@ -107,9 +107,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
<div :class="$style.frameInner">
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton @click="endReplay"><i class="ti ti-player-stop"></i> END</MkButton>
|
||||
<MkButton :primary="replayPlaybackRate === 4" @click="replayPlaybackRate = replayPlaybackRate === 4 ? 1 : 4"><i class="ti ti-player-track-next"></i> x4</MkButton>
|
||||
<MkButton :primary="replayPlaybackRate === 16" @click="replayPlaybackRate = replayPlaybackRate === 16 ? 1 : 16"><i class="ti ti-player-track-next"></i> x16</MkButton>
|
||||
<MkButton @click="endReplay"><i class="ph-stop ph-bold ph-lg"></i> END</MkButton>
|
||||
<MkButton :primary="replayPlaybackRate === 4" @click="replayPlaybackRate = replayPlaybackRate === 4 ? 1 : 4"><i class="ph-skip-forward ph-bold ph-lg"></i> x4</MkButton>
|
||||
<MkButton :primary="replayPlaybackRate === 16" @click="replayPlaybackRate = replayPlaybackRate === 16 ? 1 : 16"><i class="ph-skip-forward ph-bold ph-lg"></i> x16</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -135,7 +135,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
<div :class="[$style.frame]" style="margin-left: auto;">
|
||||
<div :class="$style.frameInner" style="text-align: center;">
|
||||
<div @click="showConfig = !showConfig"><i class="ti ti-settings"></i></div>
|
||||
<div @click="showConfig = !showConfig"><i class="ph-gear ph-bold ph-lg"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -159,7 +159,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div>
|
||||
<div v-for="(mono, i) in game.monoDefinitions.sort((a, b) => a.level - b.level)" :key="mono.id" style="display: inline-block;">
|
||||
<img :src="getTextureImageUrl(mono)" style="width: 32px; vertical-align: bottom;"/>
|
||||
<div v-if="i < game.monoDefinitions.length - 1" style="display: inline-block; margin-left: 4px; vertical-align: bottom;"><i class="ti ti-arrow-big-right"></i></div>
|
||||
<div v-if="i < game.monoDefinitions.length - 1" style="display: inline-block; margin-left: 4px; vertical-align: bottom;"><i class="ph-arrow-fat-right ph-bold ph-lg"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -180,6 +180,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { computed, onDeactivated, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue';
|
||||
import * as Matter from 'matter-js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { DropAndFusionGame, Mono } from 'misskey-bubble-game';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||
import * as os from '@/os.js';
|
||||
|
@ -193,7 +194,6 @@ import { i18n } from '@/i18n.js';
|
|||
import { useInterval } from '@/scripts/use-interval.js';
|
||||
import { apiUrl } from '@/config.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { DropAndFusionGame, Mono } from '@/scripts/drop-and-fusion-engine.js';
|
||||
import * as sound from '@/scripts/sound.js';
|
||||
import MkRange from '@/components/MkRange.vue';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
|
@ -496,7 +496,7 @@ const SWEETS_MONOS: FrontendMonoDefinition[] = [{
|
|||
}];
|
||||
|
||||
const props = defineProps<{
|
||||
gameMode: 'normal' | 'square' | 'yen' | 'sweets';
|
||||
gameMode: 'normal' | 'square' | 'yen' | 'sweets' | 'space';
|
||||
mute: boolean;
|
||||
}>();
|
||||
|
||||
|
@ -509,6 +509,7 @@ const monoDefinitions = computed(() => {
|
|||
props.gameMode === 'square' ? SQUARE_MONOS :
|
||||
props.gameMode === 'yen' ? YEN_MONOS :
|
||||
props.gameMode === 'sweets' ? SWEETS_MONOS :
|
||||
props.gameMode === 'space' ? NORAML_MONOS :
|
||||
[] as never;
|
||||
});
|
||||
|
||||
|
@ -1206,7 +1207,7 @@ onDeactivated(() => {
|
|||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.bubbleGame,
|
||||
icon: 'ti ti-apple',
|
||||
icon: 'ph-orange-slice ph-bold ph-lg',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<option value="square">SQUARE</option>
|
||||
<option value="yen">YEN</option>
|
||||
<option value="sweets">SWEETS</option>
|
||||
<!--<option value="space">SPACE</option>-->
|
||||
</MkSelect>
|
||||
<MkButton primary gradate large rounded inline @click="start">{{ i18n.ts.start }}</MkButton>
|
||||
</div>
|
||||
|
@ -44,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div :class="$style.frame">
|
||||
<div :class="$style.frameInner">
|
||||
<div class="_gaps_s" style="padding: 16px;">
|
||||
<div><b>{{ i18n.t('lastNDays', { n: 7 }) }} {{ i18n.ts.ranking }}</b> ({{ gameMode }})</div>
|
||||
<div><b>{{ i18n.tsx.lastNDays({ n: 7 }) }} {{ i18n.ts.ranking }}</b> ({{ gameMode }})</div>
|
||||
<div v-if="ranking" class="_gaps_s">
|
||||
<div v-for="r in ranking" :key="r.id" :class="$style.rankingRecord">
|
||||
<MkAvatar :link="true" style="width: 24px; height: 24px; margin-right: 4px;" :user="r.user"/>
|
||||
|
@ -94,7 +95,7 @@ import MkSelect from '@/components/MkSelect.vue';
|
|||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import { misskeyApiGet } from '@/scripts/misskey-api.js';
|
||||
|
||||
const gameMode = ref<'normal' | 'square' | 'yen' | 'sweets'>('normal');
|
||||
const gameMode = ref<'normal' | 'square' | 'yen' | 'sweets' | 'space'>('normal');
|
||||
const gameStarted = ref(false);
|
||||
const mute = ref(false);
|
||||
const ranking = ref(null);
|
||||
|
@ -108,6 +109,7 @@ function getScoreUnit(gameMode: string) {
|
|||
gameMode === 'square' ? 'pt' :
|
||||
gameMode === 'yen' ? '円' :
|
||||
gameMode === 'sweets' ? 'kcal' :
|
||||
gameMode === 'space' ? 'pt' :
|
||||
'' as never;
|
||||
}
|
||||
|
||||
|
@ -121,7 +123,7 @@ function onGameEnd() {
|
|||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.bubbleGame,
|
||||
icon: 'ti ti-apple',
|
||||
icon: 'ti ti-device-gamepad',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -4,10 +4,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkModalWindow
|
||||
ref="dialog"
|
||||
:width="400"
|
||||
@close="dialog.close()"
|
||||
<MkWindow
|
||||
ref="windowEl"
|
||||
:initialWidth="400"
|
||||
:initialHeight="500"
|
||||
:canResize="false"
|
||||
@close="windowEl.close()"
|
||||
@closed="$emit('closed')"
|
||||
>
|
||||
<template v-if="emoji" #header>:{{ emoji.name }}:</template>
|
||||
|
@ -44,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
{{ i18n.ts.setMultipleBySeparatingWithSpace }}
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="license">
|
||||
<MkInput v-model="license" :mfmAutocomplete="true">
|
||||
<template #label>{{ i18n.ts.license }}</template>
|
||||
</MkInput>
|
||||
<MkFolder>
|
||||
|
@ -73,13 +75,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkButton primary rounded style="margin: 0 auto;" @click="done"><i class="ph-check ph-bold ph-lg"></i> {{ props.emoji ? i18n.ts.update : i18n.ts.create }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkModalWindow>
|
||||
</MkWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import MkWindow from '@/components/MkWindow.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
|
@ -96,7 +98,7 @@ const props = defineProps<{
|
|||
emoji?: any,
|
||||
}>();
|
||||
|
||||
const dialog = ref<InstanceType<typeof MkModalWindow> | null>(null);
|
||||
const windowEl = ref<InstanceType<typeof MkWindow> | null>(null);
|
||||
const name = ref<string>(props.emoji ? props.emoji.name : '');
|
||||
const category = ref<string>(props.emoji ? props.emoji.category : '');
|
||||
const aliases = ref<string>(props.emoji ? props.emoji.aliases.join(' ') : '');
|
||||
|
@ -170,7 +172,7 @@ async function done() {
|
|||
},
|
||||
});
|
||||
|
||||
dialog.value.close();
|
||||
windowEl.value.close();
|
||||
} else {
|
||||
const created = await os.apiWithDialog('admin/emoji/add', params);
|
||||
|
||||
|
@ -178,14 +180,14 @@ async function done() {
|
|||
created: created,
|
||||
});
|
||||
|
||||
dialog.value.close();
|
||||
windowEl.value.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function del() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('removeAreYouSure', { x: name.value }),
|
||||
text: i18n.tsx.removeAreYouSure({ x: name.value }),
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
|
@ -195,7 +197,7 @@ async function del() {
|
|||
emit('done', {
|
||||
deleted: true,
|
||||
});
|
||||
dialog.value.close();
|
||||
windowEl.value.close();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -6,17 +6,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<div>
|
||||
<div v-if="tab === 'featured'">
|
||||
<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
|
||||
<div v-if="tab === 'featured'" key="featured">
|
||||
<XFeatured/>
|
||||
</div>
|
||||
<div v-else-if="tab === 'users'">
|
||||
<div v-else-if="tab === 'users'" key="users">
|
||||
<XUsers/>
|
||||
</div>
|
||||
<div v-else-if="tab === 'roles'">
|
||||
<div v-else-if="tab === 'roles'" key="roles">
|
||||
<XRoles/>
|
||||
</div>
|
||||
</div>
|
||||
</MkHorizontalSwipe>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
|
@ -26,6 +26,7 @@ import XFeatured from './explore.featured.vue';
|
|||
import XUsers from './explore.users.vue';
|
||||
import XRoles from './explore.roles.vue';
|
||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
|
|
|
@ -438,7 +438,7 @@ function show() {
|
|||
async function del() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('deleteAreYouSure', { x: flash.value.title }),
|
||||
text: i18n.tsx.deleteAreYouSure({ x: flash.value.title }),
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
|
|
|
@ -7,32 +7,34 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="700">
|
||||
<div v-if="tab === 'featured'">
|
||||
<MkPagination v-slot="{items}" :pagination="featuredFlashsPagination">
|
||||
<div class="_gaps_s">
|
||||
<MkFlashPreview v-for="flash in items" :key="flash.id" :flash="flash"/>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</div>
|
||||
|
||||
<div v-else-if="tab === 'my'">
|
||||
<div class="_gaps">
|
||||
<MkButton gradate rounded style="margin: 0 auto;" @click="create()"><i class="ph-plus ph-bold ph-lg"></i></MkButton>
|
||||
<MkPagination v-slot="{items}" :pagination="myFlashsPagination">
|
||||
<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
|
||||
<div v-if="tab === 'featured'" key="featured">
|
||||
<MkPagination v-slot="{items}" :pagination="featuredFlashsPagination">
|
||||
<div class="_gaps_s">
|
||||
<MkFlashPreview v-for="flash in items" :key="flash.id" :flash="flash"/>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="tab === 'liked'">
|
||||
<MkPagination v-slot="{items}" :pagination="likedFlashsPagination">
|
||||
<div class="_gaps_s">
|
||||
<MkFlashPreview v-for="like in items" :key="like.flash.id" :flash="like.flash"/>
|
||||
<div v-else-if="tab === 'my'" key="my">
|
||||
<div class="_gaps">
|
||||
<MkButton gradate rounded style="margin: 0 auto;" @click="create()"><i class="ph-plus ph-bold ph-lg"></i></MkButton>
|
||||
<MkPagination v-slot="{items}" :pagination="myFlashsPagination">
|
||||
<div class="_gaps_s">
|
||||
<MkFlashPreview v-for="flash in items" :key="flash.id" :flash="flash"/>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="tab === 'liked'" key="liked">
|
||||
<MkPagination v-slot="{items}" :pagination="likedFlashsPagination">
|
||||
<div class="_gaps_s">
|
||||
<MkFlashPreview v-for="like in items" :key="like.flash.id" :flash="like.flash"/>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkHorizontalSwipe>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
@ -42,6 +44,7 @@ import { computed, ref } from 'vue';
|
|||
import MkFlashPreview from '@/components/MkFlashPreview.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { useRouter } from '@/global/router/supplier.js';
|
||||
|
|
|
@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #icon><i class="ph-code ph-bold ph-lg"></i></template>
|
||||
<template #label>{{ i18n.ts._play.viewSource }}</template>
|
||||
|
||||
<MkCode :code="flash.script" lang="is" :inline="false" class="_monospace"/>
|
||||
<MkCode :code="flash.script" lang="is" class="_monospace"/>
|
||||
</MkFolder>
|
||||
<div :class="$style.footer">
|
||||
<Mfm :text="`By @${flash.user.username}`"/>
|
||||
|
|
|
@ -20,7 +20,7 @@ import { mainRouter } from '@/global/router/main.js';
|
|||
async function follow(user): Promise<void> {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'question',
|
||||
text: i18n.t('followConfirm', { name: user.name || user.username }),
|
||||
text: i18n.tsx.followConfirm({ name: user.name || user.username }),
|
||||
});
|
||||
|
||||
if (canceled) {
|
||||
|
|
|
@ -7,8 +7,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="1400">
|
||||
<div class="_root">
|
||||
<div v-if="tab === 'explore'">
|
||||
<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
|
||||
<div v-if="tab === 'explore'" key="explore">
|
||||
<MkFoldableSection class="_margin">
|
||||
<template #header><i class="ph-clock ph-bold ph-lg"></i>{{ i18n.ts.recentPosts }}</template>
|
||||
<MkPagination v-slot="{items}" :pagination="recentPostsPagination" :disableAutoLoad="true">
|
||||
|
@ -26,14 +26,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkPagination>
|
||||
</MkFoldableSection>
|
||||
</div>
|
||||
<div v-else-if="tab === 'liked'">
|
||||
<div v-else-if="tab === 'liked'" key="liked">
|
||||
<MkPagination v-slot="{items}" :pagination="likedPostsPagination">
|
||||
<div :class="$style.items">
|
||||
<MkGalleryPostPreview v-for="like in items" :key="like.id" :post="like.post" class="post"/>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<div v-else-if="tab === 'my'">
|
||||
<div v-else-if="tab === 'my'" key="my">
|
||||
<MkA to="/gallery/new" class="_link" style="margin: 16px;"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.postToGallery }}</MkA>
|
||||
<MkPagination v-slot="{items}" :pagination="myPostsPagination">
|
||||
<div :class="$style.items">
|
||||
|
@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</div>
|
||||
</MkHorizontalSwipe>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
@ -51,6 +51,7 @@ import { watch, ref, computed } from 'vue';
|
|||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue';
|
||||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { useRouter } from '@/global/router/supplier.js';
|
||||
|
|
|
@ -7,10 +7,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader/></template>
|
||||
<MkSpacer :contentMax="800">
|
||||
<div class="_panel">
|
||||
<MkA to="/bubble-game">
|
||||
<img src="/client-assets/drop-and-fusion/logo.png" style="display: block; max-width: 100%; max-height: 200px; margin: auto;"/>
|
||||
</MkA>
|
||||
<div class="_gaps">
|
||||
<div class="_panel">
|
||||
<MkA to="/bubble-game">
|
||||
<img src="/client-assets/drop-and-fusion/logo.png" style="display: block; max-width: 100%; max-height: 200px; margin: auto;"/>
|
||||
</MkA>
|
||||
</div>
|
||||
<div class="_panel">
|
||||
<MkA to="/reversi">
|
||||
<img src="/client-assets/reversi/logo.png" style="display: block; max-width: 100%; max-height: 200px; margin: auto;"/>
|
||||
</MkA>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
|
@ -22,6 +29,6 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
|
|||
|
||||
definePageMetadata({
|
||||
title: 'Misskey Games',
|
||||
icon: 'ti ti-device-gamepad',
|
||||
icon: 'ph-game-controller ph-bold ph-lg',
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -7,112 +7,114 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer v-if="instance" :contentMax="600" :marginMin="16" :marginMax="32">
|
||||
<div v-if="tab === 'overview'" class="_gaps_m">
|
||||
<div class="fnfelxur">
|
||||
<img :src="faviconUrl" alt="" class="icon"/>
|
||||
<span class="name">{{ instance.name || `(${i18n.ts.unknown})` }}</span>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 1em;">
|
||||
<MkKeyValue :copy="host" oneline>
|
||||
<template #key>Host</template>
|
||||
<template #value><span class="_monospace"><MkLink :url="`https://${host}`">{{ host }}</MkLink></span></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline>
|
||||
<template #key>{{ i18n.ts.software }}</template>
|
||||
<template #value><span class="_monospace">{{ instance.softwareName || `(${i18n.ts.unknown})` }} / {{ instance.softwareVersion || `(${i18n.ts.unknown})` }}</span></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline>
|
||||
<template #key>{{ i18n.ts.administrator }}</template>
|
||||
<template #value>{{ instance.maintainerName || `(${i18n.ts.unknown})` }} ({{ instance.maintainerEmail || `(${i18n.ts.unknown})` }})</template>
|
||||
</MkKeyValue>
|
||||
</div>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.description }}</template>
|
||||
<template #value>{{ instance.description }}</template>
|
||||
</MkKeyValue>
|
||||
|
||||
<FormSection v-if="iAmModerator">
|
||||
<template #label>Moderation</template>
|
||||
<div class="_gaps_s">
|
||||
<MkSwitch v-model="suspended" :disabled="!instance" @update:modelValue="toggleSuspend">{{ i18n.ts.stopActivityDelivery }}</MkSwitch>
|
||||
<MkSwitch v-model="isBlocked" :disabled="!meta || !instance" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch>
|
||||
<MkSwitch v-model="isSilenced" :disabled="!meta || !instance" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch>
|
||||
<MkSwitch v-model="isNSFW" :disabled="!instance" @update:modelValue="toggleNSFW">Mark as NSFW</MkSwitch>
|
||||
<MkButton @click="refreshMetadata"><i class="ph-arrows-counter-clockwise ph-bold ph-lg"></i> Refresh metadata</MkButton>
|
||||
<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
|
||||
<div v-if="tab === 'overview'" key="overview" class="_gaps_m">
|
||||
<div class="fnfelxur">
|
||||
<img :src="faviconUrl" alt="" class="icon"/>
|
||||
<span class="name">{{ instance.name || `(${i18n.ts.unknown})` }}</span>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>{{ i18n.ts.registeredAt }}</template>
|
||||
<template #value><MkTime mode="detail" :time="instance.firstRetrievedAt"/></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>{{ i18n.ts.updatedAt }}</template>
|
||||
<template #value><MkTime mode="detail" :time="instance.infoUpdatedAt"/></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>{{ i18n.ts.latestRequestReceivedAt }}</template>
|
||||
<template #value><MkTime v-if="instance.latestRequestReceivedAt" mode="detail" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></template>
|
||||
</MkKeyValue>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>Following (Pub)</template>
|
||||
<template #value>{{ number(instance.followingCount) }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>Followers (Sub)</template>
|
||||
<template #value>{{ number(instance.followersCount) }}</template>
|
||||
</MkKeyValue>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<template #label>Well-known resources</template>
|
||||
<FormLink :to="`https://${host}/.well-known/host-meta`" external style="margin-bottom: 8px;">host-meta</FormLink>
|
||||
<FormLink :to="`https://${host}/.well-known/host-meta.json`" external style="margin-bottom: 8px;">host-meta.json</FormLink>
|
||||
<FormLink :to="`https://${host}/.well-known/nodeinfo`" external style="margin-bottom: 8px;">nodeinfo</FormLink>
|
||||
<FormLink :to="`https://${host}/robots.txt`" external style="margin-bottom: 8px;">robots.txt</FormLink>
|
||||
<FormLink :to="`https://${host}/manifest.json`" external style="margin-bottom: 8px;">manifest.json</FormLink>
|
||||
</FormSection>
|
||||
</div>
|
||||
<div v-else-if="tab === 'chart'" class="_gaps_m">
|
||||
<div class="cmhjzshl">
|
||||
<div class="selects">
|
||||
<MkSelect v-model="chartSrc" style="margin: 0 10px 0 0; flex: 1;">
|
||||
<option value="instance-requests">{{ i18n.ts._instanceCharts.requests }}</option>
|
||||
<option value="instance-users">{{ i18n.ts._instanceCharts.users }}</option>
|
||||
<option value="instance-users-total">{{ i18n.ts._instanceCharts.usersTotal }}</option>
|
||||
<option value="instance-notes">{{ i18n.ts._instanceCharts.notes }}</option>
|
||||
<option value="instance-notes-total">{{ i18n.ts._instanceCharts.notesTotal }}</option>
|
||||
<option value="instance-ff">{{ i18n.ts._instanceCharts.ff }}</option>
|
||||
<option value="instance-ff-total">{{ i18n.ts._instanceCharts.ffTotal }}</option>
|
||||
<option value="instance-drive-usage">{{ i18n.ts._instanceCharts.cacheSize }}</option>
|
||||
<option value="instance-drive-usage-total">{{ i18n.ts._instanceCharts.cacheSizeTotal }}</option>
|
||||
<option value="instance-drive-files">{{ i18n.ts._instanceCharts.files }}</option>
|
||||
<option value="instance-drive-files-total">{{ i18n.ts._instanceCharts.filesTotal }}</option>
|
||||
</MkSelect>
|
||||
<div style="display: flex; flex-direction: column; gap: 1em;">
|
||||
<MkKeyValue :copy="host" oneline>
|
||||
<template #key>Host</template>
|
||||
<template #value><span class="_monospace"><MkLink :url="`https://${host}`">{{ host }}</MkLink></span></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline>
|
||||
<template #key>{{ i18n.ts.software }}</template>
|
||||
<template #value><span class="_monospace">{{ instance.softwareName || `(${i18n.ts.unknown})` }} / {{ instance.softwareVersion || `(${i18n.ts.unknown})` }}</span></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline>
|
||||
<template #key>{{ i18n.ts.administrator }}</template>
|
||||
<template #value>{{ instance.maintainerName || `(${i18n.ts.unknown})` }} ({{ instance.maintainerEmail || `(${i18n.ts.unknown})` }})</template>
|
||||
</MkKeyValue>
|
||||
</div>
|
||||
<div class="charts">
|
||||
<div class="label">{{ i18n.t('recentNHours', { n: 90 }) }}</div>
|
||||
<MkChart class="chart" :src="chartSrc" span="hour" :limit="90" :args="{ host: host }" :detailed="true"></MkChart>
|
||||
<div class="label">{{ i18n.t('recentNDays', { n: 90 }) }}</div>
|
||||
<MkChart class="chart" :src="chartSrc" span="day" :limit="90" :args="{ host: host }" :detailed="true"></MkChart>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.description }}</template>
|
||||
<template #value>{{ instance.description }}</template>
|
||||
</MkKeyValue>
|
||||
|
||||
<FormSection v-if="iAmModerator">
|
||||
<template #label>Moderation</template>
|
||||
<div class="_gaps_s">
|
||||
<MkSwitch v-model="suspended" :disabled="!instance" @update:modelValue="toggleSuspend">{{ i18n.ts.stopActivityDelivery }}</MkSwitch>
|
||||
<MkSwitch v-model="isBlocked" :disabled="!meta || !instance" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch>
|
||||
<MkSwitch v-model="isSilenced" :disabled="!meta || !instance" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch>
|
||||
<MkSwitch v-model="isNSFW" :disabled="!instance" @update:modelValue="toggleNSFW">Mark as NSFW</MkSwitch>
|
||||
<MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>{{ i18n.ts.registeredAt }}</template>
|
||||
<template #value><MkTime mode="detail" :time="instance.firstRetrievedAt"/></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>{{ i18n.ts.updatedAt }}</template>
|
||||
<template #value><MkTime mode="detail" :time="instance.infoUpdatedAt"/></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>{{ i18n.ts.latestRequestReceivedAt }}</template>
|
||||
<template #value><MkTime v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></template>
|
||||
</MkKeyValue>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>Following (Pub)</template>
|
||||
<template #value>{{ number(instance.followingCount) }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>Followers (Sub)</template>
|
||||
<template #value>{{ number(instance.followersCount) }}</template>
|
||||
</MkKeyValue>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<template #label>Well-known resources</template>
|
||||
<FormLink :to="`https://${host}/.well-known/host-meta`" external style="margin-bottom: 8px;">host-meta</FormLink>
|
||||
<FormLink :to="`https://${host}/.well-known/host-meta.json`" external style="margin-bottom: 8px;">host-meta.json</FormLink>
|
||||
<FormLink :to="`https://${host}/.well-known/nodeinfo`" external style="margin-bottom: 8px;">nodeinfo</FormLink>
|
||||
<FormLink :to="`https://${host}/robots.txt`" external style="margin-bottom: 8px;">robots.txt</FormLink>
|
||||
<FormLink :to="`https://${host}/manifest.json`" external style="margin-bottom: 8px;">manifest.json</FormLink>
|
||||
</FormSection>
|
||||
</div>
|
||||
<div v-else-if="tab === 'chart'" key="chart" class="_gaps_m">
|
||||
<div class="cmhjzshl">
|
||||
<div class="selects">
|
||||
<MkSelect v-model="chartSrc" style="margin: 0 10px 0 0; flex: 1;">
|
||||
<option value="instance-requests">{{ i18n.ts._instanceCharts.requests }}</option>
|
||||
<option value="instance-users">{{ i18n.ts._instanceCharts.users }}</option>
|
||||
<option value="instance-users-total">{{ i18n.ts._instanceCharts.usersTotal }}</option>
|
||||
<option value="instance-notes">{{ i18n.ts._instanceCharts.notes }}</option>
|
||||
<option value="instance-notes-total">{{ i18n.ts._instanceCharts.notesTotal }}</option>
|
||||
<option value="instance-ff">{{ i18n.ts._instanceCharts.ff }}</option>
|
||||
<option value="instance-ff-total">{{ i18n.ts._instanceCharts.ffTotal }}</option>
|
||||
<option value="instance-drive-usage">{{ i18n.ts._instanceCharts.cacheSize }}</option>
|
||||
<option value="instance-drive-usage-total">{{ i18n.ts._instanceCharts.cacheSizeTotal }}</option>
|
||||
<option value="instance-drive-files">{{ i18n.ts._instanceCharts.files }}</option>
|
||||
<option value="instance-drive-files-total">{{ i18n.ts._instanceCharts.filesTotal }}</option>
|
||||
</MkSelect>
|
||||
</div>
|
||||
<div class="charts">
|
||||
<div class="label">{{ i18n.tsx.recentNHours({ n: 90 }) }}</div>
|
||||
<MkChart class="chart" :src="chartSrc" span="hour" :limit="90" :args="{ host: host }" :detailed="true"></MkChart>
|
||||
<div class="label">{{ i18n.tsx.recentNDays({ n: 90 }) }}</div>
|
||||
<MkChart class="chart" :src="chartSrc" span="day" :limit="90" :args="{ host: host }" :detailed="true"></MkChart>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="tab === 'users'" class="_gaps_m">
|
||||
<MkPagination v-slot="{items}" :pagination="usersPagination" style="display: grid; grid-template-columns: repeat(auto-fill,minmax(270px,1fr)); grid-gap: 12px;">
|
||||
<MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${dateString(user.updatedAt)}`" class="user" :to="`/admin/user/${user.id}`">
|
||||
<MkUserCardMini :user="user"/>
|
||||
</MkA>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<div v-else-if="tab === 'raw'" class="_gaps_m">
|
||||
<MkObjectView tall :value="instance">
|
||||
</MkObjectView>
|
||||
</div>
|
||||
<div v-else-if="tab === 'users'" key="users" class="_gaps_m">
|
||||
<MkPagination v-slot="{items}" :pagination="usersPagination" style="display: grid; grid-template-columns: repeat(auto-fill,minmax(270px,1fr)); grid-gap: 12px;">
|
||||
<MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${dateString(user.updatedAt)}`" class="user" :to="`/admin/user/${user.id}`">
|
||||
<MkUserCardMini :user="user"/>
|
||||
</MkA>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<div v-else-if="tab === 'raw'" key="raw" class="_gaps_m">
|
||||
<MkObjectView tall :value="instance">
|
||||
</MkObjectView>
|
||||
</div>
|
||||
</MkHorizontalSwipe>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
@ -137,6 +139,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
||||
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
|
||||
import { dateString } from '@/filters/date.js';
|
||||
|
||||
|
@ -145,6 +148,7 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
const tab = ref('overview');
|
||||
|
||||
const chartSrc = ref('instance-requests');
|
||||
const meta = ref<Misskey.entities.AdminMetaResponse | null>(null);
|
||||
const instance = ref<Misskey.entities.FederationInstance | null>(null);
|
||||
|
|
|
@ -19,9 +19,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MKSpacer>
|
||||
<MkSpacer v-else :contentMax="800">
|
||||
<div class="_gaps_m" style="text-align: center;">
|
||||
<div v-if="resetCycle && inviteLimit">{{ i18n.t('inviteLimitResetCycle', { time: resetCycle, limit: inviteLimit }) }}</div>
|
||||
<div v-if="resetCycle && inviteLimit">{{ i18n.tsx.inviteLimitResetCycle({ time: resetCycle, limit: inviteLimit }) }}</div>
|
||||
<MkButton inline primary rounded :disabled="currentInviteLimit !== null && currentInviteLimit <= 0" @click="create"><i class="ph-user-plus ph-bold ph-lg"></i> {{ i18n.ts.createInviteCode }}</MkButton>
|
||||
<div v-if="currentInviteLimit !== null">{{ i18n.t('createLimitRemaining', { limit: currentInviteLimit }) }}</div>
|
||||
<div v-if="currentInviteLimit !== null">{{ i18n.tsx.createLimitRemaining({ limit: currentInviteLimit }) }}</div>
|
||||
|
||||
<MkPagination ref="pagingComponent" :pagination="pagination">
|
||||
<template #default="{ items }">
|
||||
|
|
|
@ -20,13 +20,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
<div v-else>
|
||||
<div v-if="_permissions.length > 0">
|
||||
<p v-if="name">{{ i18n.t('_auth.permission', { name }) }}</p>
|
||||
<p v-if="name">{{ i18n.tsx._auth.permission({ name }) }}</p>
|
||||
<p v-else>{{ i18n.ts._auth.permissionAsk }}</p>
|
||||
<ul>
|
||||
<li v-for="p in _permissions" :key="p">{{ i18n.t(`_permissions.${p}`) }}</li>
|
||||
<li v-for="p in _permissions" :key="p">{{ i18n.ts._permissions[p] }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="name">{{ i18n.t('_auth.shareAccess', { name }) }}</div>
|
||||
<div v-if="name">{{ i18n.tsx._auth.shareAccess({ name }) }}</div>
|
||||
<div v-else>{{ i18n.ts._auth.shareAccessAsk }}</div>
|
||||
<div :class="$style.buttons">
|
||||
<MkButton inline @click="deny">{{ i18n.ts.cancel }}</MkButton>
|
||||
|
|
|
@ -116,7 +116,7 @@ async function saveAntenna() {
|
|||
async function deleteAntenna() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('removeAreYouSure', { x: props.antenna.name }),
|
||||
text: i18n.tsx.removeAreYouSure({ x: props.antenna.name }),
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
|
|
|
@ -7,20 +7,22 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="700">
|
||||
<div v-if="tab === 'my'" class="_gaps">
|
||||
<MkButton primary rounded class="add" @click="create"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.add }}</MkButton>
|
||||
<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
|
||||
<div v-if="tab === 'my'" key="my" class="_gaps">
|
||||
<MkButton primary rounded class="add" @click="create"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.add }}</MkButton>
|
||||
|
||||
<MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination" class="_gaps">
|
||||
<MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`">
|
||||
<MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination" class="_gaps">
|
||||
<MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`">
|
||||
<MkClipPreview :clip="item"/>
|
||||
</MkA>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<div v-else-if="tab === 'favorites'" key="favorites" class="_gaps">
|
||||
<MkA v-for="item in favorites" :key="item.id" :to="`/clips/${item.id}`">
|
||||
<MkClipPreview :clip="item"/>
|
||||
</MkA>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<div v-else-if="tab === 'favorites'" class="_gaps">
|
||||
<MkA v-for="item in favorites" :key="item.id" :to="`/clips/${item.id}`">
|
||||
<MkClipPreview :clip="item"/>
|
||||
</MkA>
|
||||
</div>
|
||||
</div>
|
||||
</MkHorizontalSwipe>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
@ -36,6 +38,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { clipsCache } from '@/cache.js';
|
||||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
||||
|
||||
const pagination = {
|
||||
endpoint: 'clips/list' as const,
|
||||
|
@ -44,6 +47,7 @@ const pagination = {
|
|||
};
|
||||
|
||||
const tab = ref('my');
|
||||
|
||||
const favorites = ref<Misskey.entities.Clip[] | null>(null);
|
||||
|
||||
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
|
||||
|
|
|
@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<div v-if="items.length > 0" class="_gaps">
|
||||
<MkA v-for="list in items" :key="list.id" class="_panel" :class="$style.list" :to="`/my/lists/${ list.id }`">
|
||||
<div style="margin-bottom: 4px;">{{ list.name }} <span :class="$style.nUsers">({{ i18n.t('nUsers', { n: `${list.userIds.length}/${$i.policies['userEachUserListsLimit']}` }) }})</span></div>
|
||||
<div style="margin-bottom: 4px;">{{ list.name }} <span :class="$style.nUsers">({{ i18n.tsx.nUsers({ n: `${list.userIds.length}/${$i.policies['userEachUserListsLimit']}` }) }})</span></div>
|
||||
<MkAvatars :userIds="list.userIds" :limit="10"/>
|
||||
</MkA>
|
||||
</div>
|
||||
|
|
|
@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<MkFolder defaultOpen>
|
||||
<template #label>{{ i18n.ts.members }}</template>
|
||||
<template #caption>{{ i18n.t('nUsers', { n: `${list.userIds.length}/${$i.policies['userEachUserListsLimit']}` }) }}</template>
|
||||
<template #caption>{{ i18n.tsx.nUsers({ n: `${list.userIds.length}/${$i.policies['userEachUserListsLimit']}` }) }}</template>
|
||||
|
||||
<div class="_gaps_s">
|
||||
<MkButton rounded primary style="margin: 0 auto;" @click="addUser()">{{ i18n.ts.addUser }}</MkButton>
|
||||
|
@ -155,7 +155,7 @@ async function deleteList() {
|
|||
if (!list.value) return;
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('removeAreYouSure', { x: list.value.name }),
|
||||
text: i18n.tsx.removeAreYouSure({ x: list.value.name }),
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
|
|
|
@ -11,11 +11,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<Transition :name="defaultStore.state.animation ? 'fade' : ''" mode="out-in">
|
||||
<div v-if="note">
|
||||
<div v-if="showNext" class="_margin">
|
||||
<MkNotes class="" :pagination="nextPagination" :noGap="true" :disableAutoLoad="true"/>
|
||||
<MkNotes class="" :pagination="showNext === 'channel' ? nextChannelPagination : nextUserPagination" :noGap="true" :disableAutoLoad="true"/>
|
||||
</div>
|
||||
|
||||
<div class="_margin">
|
||||
<MkButton v-if="!showNext" :class="$style.loadNext" @click="showNext = true"><i class="ph-caret-up ph-bold ph-lg"></i></MkButton>
|
||||
<div v-if="!showNext" class="_buttons" :class="$style.loadNext">
|
||||
<MkButton v-if="note.channelId" rounded :class="$style.loadButton" @click="showNext = 'channel'"><i class="ph-caret-up ph-bold ph-lg"></i> <i class="ph-television-simple ph-bold ph-lg"></i></MkButton>
|
||||
<MkButton rounded :class="$style.loadButton" @click="showNext = 'user'"><i class="ph-caret-up ph-bold ph-lg"></i> <i class="ph-user ph-bold ph-lg"></i></MkButton>
|
||||
</div>
|
||||
<div v-if="defaultStore.state.noteDesign === 'misskey'" class="_margin _gaps_s">
|
||||
<MkRemoteCaution v-if="note.user.host != null" :href="note.url ?? note.uri"/>
|
||||
<MkNoteDetailed :key="note.id" v-model:note="note" :class="$style.note" :expandAllCws="expandAllCws"/>
|
||||
|
@ -32,11 +35,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkA>
|
||||
</div>
|
||||
</div>
|
||||
<MkButton v-if="!showPrev" :class="$style.loadPrev" @click="showPrev = true"><i class="ph-caret-down ph-bold ph-lg"></i></MkButton>
|
||||
<div v-if="!showPrev" class="_buttons" :class="$style.loadPrev">
|
||||
<MkButton v-if="note.channelId" rounded :class="$style.loadButton" @click="showPrev = 'channel'"><i class="ph-caret-down ph-bold ph-lg"></i> <i class="ph-television-simple ph-bold ph-lg"></i></MkButton>
|
||||
<MkButton rounded :class="$style.loadButton" @click="showPrev = 'user'"><i class="ph-caret-down ph-bold ph-lg"></i> <i class="ph-user ph-bold ph-lg"></i></MkButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showPrev" class="_margin">
|
||||
<MkNotes class="" :pagination="prevPagination" :noGap="true"/>
|
||||
<MkNotes class="" :pagination="showPrev === 'channel' ? prevChannelPagination : prevUserPagination" :noGap="true"/>
|
||||
</div>
|
||||
</div>
|
||||
<MkError v-else-if="error" @retry="fetchNote()"/>
|
||||
|
@ -50,6 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { computed, watch, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import type { Paging } from '@/components/MkPagination.vue';
|
||||
import MkNoteDetailed from '@/components/MkNoteDetailed.vue';
|
||||
import MkNotes from '@/components/MkNotes.vue';
|
||||
import SkNoteDetailed from '@/components/SkNoteDetailed.vue';
|
||||
|
@ -68,28 +75,47 @@ const props = defineProps<{
|
|||
|
||||
const note = ref<null | Misskey.entities.Note>();
|
||||
const clips = ref<Misskey.entities.Clip[]>();
|
||||
const showPrev = ref(false);
|
||||
const showNext = ref(false);
|
||||
const showPrev = ref<'user' | 'channel' | false>(false);
|
||||
const showNext = ref<'user' | 'channel' | false>(false);
|
||||
const expandAllCws = ref(false);
|
||||
const error = ref();
|
||||
|
||||
const prevPagination = {
|
||||
endpoint: 'users/notes' as const,
|
||||
const prevUserPagination: Paging = {
|
||||
endpoint: 'users/notes',
|
||||
limit: 10,
|
||||
params: computed(() => note.value ? ({
|
||||
userId: note.value.userId,
|
||||
untilId: note.value.id,
|
||||
}) : null),
|
||||
}) : undefined),
|
||||
};
|
||||
|
||||
const nextPagination = {
|
||||
const nextUserPagination: Paging = {
|
||||
reversed: true,
|
||||
endpoint: 'users/notes' as const,
|
||||
endpoint: 'users/notes',
|
||||
limit: 10,
|
||||
params: computed(() => note.value ? ({
|
||||
userId: note.value.userId,
|
||||
sinceId: note.value.id,
|
||||
}) : null),
|
||||
}) : undefined),
|
||||
};
|
||||
|
||||
const prevChannelPagination: Paging = {
|
||||
endpoint: 'channels/timeline',
|
||||
limit: 10,
|
||||
params: computed(() => note.value ? ({
|
||||
channelId: note.value.channelId,
|
||||
untilId: note.value.id,
|
||||
}) : undefined),
|
||||
};
|
||||
|
||||
const nextChannelPagination: Paging = {
|
||||
reversed: true,
|
||||
endpoint: 'channels/timeline',
|
||||
limit: 10,
|
||||
params: computed(() => note.value ? ({
|
||||
channelId: note.value.channelId,
|
||||
sinceId: note.value.id,
|
||||
}) : undefined),
|
||||
};
|
||||
|
||||
function fetchNote() {
|
||||
|
@ -133,7 +159,7 @@ definePageMetadata(computed(() => note.value ? {
|
|||
avatar: note.value.user,
|
||||
path: `/notes/${note.value.id}`,
|
||||
share: {
|
||||
title: i18n.t('noteOf', { user: note.value.user.name }),
|
||||
title: i18n.tsx.noteOf({ user: note.value.user.name }),
|
||||
text: note.value.text,
|
||||
},
|
||||
} : null));
|
||||
|
@ -151,9 +177,7 @@ definePageMetadata(computed(() => note.value ? {
|
|||
|
||||
.loadNext,
|
||||
.loadPrev {
|
||||
min-width: 0;
|
||||
margin: 0 auto;
|
||||
border-radius: var(--radius-ellipse);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.loadNext {
|
||||
|
@ -164,6 +188,10 @@ definePageMetadata(computed(() => note.value ? {
|
|||
margin-top: var(--margin);
|
||||
}
|
||||
|
||||
.loadButton {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.note {
|
||||
border-radius: var(--radius);
|
||||
background: var(--panel);
|
||||
|
|
|
@ -7,15 +7,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="800">
|
||||
<div v-if="tab === 'all'">
|
||||
<XNotifications class="notifications" :excludeTypes="excludeTypes"/>
|
||||
</div>
|
||||
<div v-else-if="tab === 'mentions'">
|
||||
<MkNotes :pagination="mentionsPagination"/>
|
||||
</div>
|
||||
<div v-else-if="tab === 'directNotes'">
|
||||
<MkNotes :pagination="directNotesPagination"/>
|
||||
</div>
|
||||
<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
|
||||
<div v-if="tab === 'all'" key="all">
|
||||
<XNotifications :class="$style.notifications" :excludeTypes="excludeTypes"/>
|
||||
</div>
|
||||
<div v-else-if="tab === 'mentions'" key="mention">
|
||||
<MkNotes :pagination="mentionsPagination"/>
|
||||
</div>
|
||||
<div v-else-if="tab === 'directNotes'" key="directNotes">
|
||||
<MkNotes :pagination="directNotesPagination"/>
|
||||
</div>
|
||||
</MkHorizontalSwipe>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
@ -24,6 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { computed, ref } from 'vue';
|
||||
import XNotifications from '@/components/MkNotifications.vue';
|
||||
import MkNotes from '@/components/MkNotes.vue';
|
||||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
|
@ -48,7 +51,7 @@ const directNotesPagination = {
|
|||
|
||||
function setFilter(ev) {
|
||||
const typeItems = notificationTypes.map(t => ({
|
||||
text: i18n.t(`_notification._types.${t}`),
|
||||
text: i18n.ts._notification._types[t],
|
||||
active: includeTypes.value && includeTypes.value.includes(t),
|
||||
action: () => {
|
||||
includeTypes.value = [t];
|
||||
|
@ -96,3 +99,10 @@ definePageMetadata(computed(() => ({
|
|||
icon: 'ph-bell ph-bold ph-lg',
|
||||
})));
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.notifications {
|
||||
border-radius: var(--radius);
|
||||
overflow: clip;
|
||||
}
|
||||
</style>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue