diff options
| author | Marie <marie@kaifa.ch> | 2024-01-21 13:11:23 +0100 |
|---|---|---|
| committer | Marie <marie@kaifa.ch> | 2024-01-21 13:11:23 +0100 |
| commit | db012fc8c3c88f8676b8a0c13347d0257df85eb2 (patch) | |
| tree | e9118b604b9ea027369cd327837bb59843de6f83 /packages/frontend/src/components | |
| parent | merge: fix make sure that signToActivityPubGet defaults to true (#352) (diff) | |
| parent | enhance(frontend): 季節に応じた画面の演出を南半球に対応さ... (diff) | |
| download | sharkey-db012fc8c3c88f8676b8a0c13347d0257df85eb2.tar.gz sharkey-db012fc8c3c88f8676b8a0c13347d0257df85eb2.tar.bz2 sharkey-db012fc8c3c88f8676b8a0c13347d0257df85eb2.zip | |
merge: upstream (1)
Diffstat (limited to 'packages/frontend/src/components')
45 files changed, 520 insertions, 166 deletions
diff --git a/packages/frontend/src/components/MkAnnouncementDialog.vue b/packages/frontend/src/components/MkAnnouncementDialog.vue index eca5daf1c1..8f550cf255 100644 --- a/packages/frontend/src/components/MkAnnouncementDialog.vue +++ b/packages/frontend/src/components/MkAnnouncementDialog.vue @@ -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; } diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue index 181594c9f8..976ba190ff 100644 --- a/packages/frontend/src/components/MkAutocomplete.vue +++ b/packages/frontend/src/components/MkAutocomplete.vue @@ -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) ?? '')); } } diff --git a/packages/frontend/src/components/MkCode.vue b/packages/frontend/src/components/MkCode.vue index e0973b676a..05b5fe8da1 100644 --- a/packages/frontend/src/components/MkCode.vue +++ b/packages/frontend/src/components/MkCode.vue @@ -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 { diff --git a/packages/frontend/src/components/MkCodeInline.vue b/packages/frontend/src/components/MkCodeInline.vue new file mode 100644 index 0000000000..5340c1fd5f --- /dev/null +++ b/packages/frontend/src/components/MkCodeInline.vue @@ -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> diff --git a/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue index c53bbca37c..1952369b6d 100644 --- a/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue +++ b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue @@ -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); diff --git a/packages/frontend/src/components/MkCwButton.vue b/packages/frontend/src/components/MkCwButton.vue index 4a6d2dfba2..ca19a2122d 100644 --- a/packages/frontend/src/components/MkCwButton.vue +++ b/packages/frontend/src/components/MkCwButton.vue @@ -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(' / '); }); diff --git a/packages/frontend/src/components/MkDateSeparatedList.vue b/packages/frontend/src/components/MkDateSeparatedList.vue index b45aef45ff..f6bd85bd1d 100644 --- a/packages/frontend/src/components/MkDateSeparatedList.vue +++ b/packages/frontend/src/components/MkDateSeparatedList.vue @@ -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(), }); diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue index 2c0f6a4d78..83f5041a46 100644 --- a/packages/frontend/src/components/MkDialog.vue +++ b/packages/frontend/src/components/MkDialog.vue @@ -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> diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index 2fa5c1a0f8..e68b69c140 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -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> diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue index 1aae460117..d442275c78 100644 --- a/packages/frontend/src/components/MkFollowButton.vue +++ b/packages/frontend/src/components/MkFollowButton.vue @@ -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; diff --git a/packages/frontend/src/components/MkHorizontalSwipe.vue b/packages/frontend/src/components/MkHorizontalSwipe.vue new file mode 100644 index 0000000000..55bb4b13b0 --- /dev/null +++ b/packages/frontend/src/components/MkHorizontalSwipe.vue @@ -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> diff --git a/packages/frontend/src/components/MkMediaAudio.vue b/packages/frontend/src/components/MkMediaAudio.vue index 75b31b9a49..39352014b0 100644 --- a/packages/frontend/src/components/MkMediaAudio.vue +++ b/packages/frontend/src/components/MkMediaAudio.vue @@ -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) { diff --git a/packages/frontend/src/components/MkMediaRange.vue b/packages/frontend/src/components/MkMediaRange.vue index e6303a5c41..a150ae9843 100644 --- a/packages/frontend/src/components/MkMediaRange.vue +++ b/packages/frontend/src/components/MkMediaRange.vue @@ -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> diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue index cdb810e99c..91a5ddcbe7 100644 --- a/packages/frontend/src/components/MkMediaVideo.vue +++ b/packages/frontend/src/components/MkMediaVideo.vue @@ -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) { diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 182ef7661e..e16d3c3c0d 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -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; } diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 74fb476f1a..0d972c9170 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -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> diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue index 455a7c9429..0018924804 100644 --- a/packages/frontend/src/components/MkNotification.vue +++ b/packages/frontend/src/components/MkNotification.vue @@ -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> diff --git a/packages/frontend/src/components/MkNotificationSelectWindow.vue b/packages/frontend/src/components/MkNotificationSelectWindow.vue index 6725776f43..0e77d2a6aa 100644 --- a/packages/frontend/src/components/MkNotificationSelectWindow.vue +++ b/packages/frontend/src/components/MkNotificationSelectWindow.vue @@ -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> diff --git a/packages/frontend/src/components/MkPoll.vue b/packages/frontend/src/components/MkPoll.vue index 1322cfdcb6..3779fa47ff 100644 --- a/packages/frontend/src/components/MkPoll.vue +++ b/packages/frontend/src/components/MkPoll.vue @@ -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; } diff --git a/packages/frontend/src/components/MkPollEditor.vue b/packages/frontend/src/components/MkPollEditor.vue index f46779a632..47557e1225 100644 --- a/packages/frontend/src/components/MkPollEditor.vue +++ b/packages/frontend/src/components/MkPollEditor.vue @@ -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> diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue index 82ed809650..c94eaeef42 100644 --- a/packages/frontend/src/components/MkPostFormAttaches.vue +++ b/packages/frontend/src/components/MkPostFormAttaches.vue @@ -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; } diff --git a/packages/frontend/src/components/MkRadios.vue b/packages/frontend/src/components/MkRadios.vue index d9178f3362..22e7ed1ef7 100644 --- a/packages/frontend/src/components/MkRadios.vue +++ b/packages/frontend/src/components/MkRadios.vue @@ -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(); diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue index 3dd8259e05..afb7d9bd46 100644 --- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue +++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue @@ -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', { diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue index 665ae2b813..38c6bf8449 100644 --- a/packages/frontend/src/components/MkSelect.vue +++ b/packages/frontend/src/components/MkSelect.vue @@ -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); }, }); }; diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue index 98eeaed066..4fba0f184b 100644 --- a/packages/frontend/src/components/MkSignupDialog.form.vue +++ b/packages/frontend/src/components/MkSignupDialog.form.vue @@ -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) { diff --git a/packages/frontend/src/components/MkSignupDialog.rules.vue b/packages/frontend/src/components/MkSignupDialog.rules.vue index 2c30ccc7d0..fa4ce648b1 100644 --- a/packages/frontend/src/components/MkSignupDialog.rules.vue +++ b/packages/frontend/src/components/MkSignupDialog.rules.vue @@ -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; diff --git a/packages/frontend/src/components/MkSubNoteContent.vue b/packages/frontend/src/components/MkSubNoteContent.vue index 1ad213912f..904b0f7943 100644 --- a/packages/frontend/src/components/MkSubNoteContent.vue +++ b/packages/frontend/src/components/MkSubNoteContent.vue @@ -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"> diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue index 572d6edcdd..4b61f4ec17 100644 --- a/packages/frontend/src/components/MkTimeline.vue +++ b/packages/frontend/src/components/MkTimeline.vue @@ -51,6 +51,7 @@ const emit = defineEmits<{ (ev: 'queue', count: number): void; }>(); +provide('inTimeline', true); provide('inChannel', computed(() => props.src === 'channel')); type TimelineQueryType = { diff --git a/packages/frontend/src/components/MkTokenGenerateWindow.vue b/packages/frontend/src/components/MkTokenGenerateWindow.vue index a42767e1b6..28f2f29b7d 100644 --- a/packages/frontend/src/components/MkTokenGenerateWindow.vue +++ b/packages/frontend/src/components/MkTokenGenerateWindow.vue @@ -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> diff --git a/packages/frontend/src/components/MkTutorialDialog.vue b/packages/frontend/src/components/MkTutorialDialog.vue index a734f93ec9..53abf7620a 100644 --- a/packages/frontend/src/components/MkTutorialDialog.vue +++ b/packages/frontend/src/components/MkTutorialDialog.vue @@ -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> diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue index 81d0acb8fa..440dcd84fa 100644 --- a/packages/frontend/src/components/MkUrlPreview.vue +++ b/packages/frontend/src/components/MkUrlPreview.vue @@ -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'); diff --git a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue index 17ede3e620..788c373ccf 100644 --- a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue +++ b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue @@ -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; diff --git a/packages/frontend/src/components/MkUserSelectDialog.vue b/packages/frontend/src/components/MkUserSelectDialog.vue index f4aa06950d..ad11ba1940 100644 --- a/packages/frontend/src/components/MkUserSelectDialog.vue +++ b/packages/frontend/src/components/MkUserSelectDialog.vue @@ -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', { diff --git a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue index 37aa677b44..f082833838 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue @@ -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, }); diff --git a/packages/frontend/src/components/MkUserSetupDialog.vue b/packages/frontend/src/components/MkUserSetupDialog.vue index be945c1066..ddc4f188ca 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.vue @@ -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> diff --git a/packages/frontend/src/components/MkWidgets.vue b/packages/frontend/src/components/MkWidgets.vue index bc1f33c43e..100683cc54 100644 --- a/packages/frontend/src/components/MkWidgets.vue +++ b/packages/frontend/src/components/MkWidgets.vue @@ -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, diff --git a/packages/frontend/src/components/SkNote.vue b/packages/frontend/src/components/SkNote.vue index 15192405f5..c9ac4c11c0 100644 --- a/packages/frontend/src/components/SkNote.vue +++ b/packages/frontend/src/components/SkNote.vue @@ -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; } diff --git a/packages/frontend/src/components/SkNoteDetailed.vue b/packages/frontend/src/components/SkNoteDetailed.vue index 014c655bb8..f3a835b0ee 100644 --- a/packages/frontend/src/components/SkNoteDetailed.vue +++ b/packages/frontend/src/components/SkNoteDetailed.vue @@ -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> diff --git a/packages/frontend/src/components/global/I18n.vue b/packages/frontend/src/components/global/I18n.vue new file mode 100644 index 0000000000..162aa2bcf8 --- /dev/null +++ b/packages/frontend/src/components/global/I18n.vue @@ -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> diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue index 2a4d02a7be..5ba2ffb8b9 100644 --- a/packages/frontend/src/components/global/MkCustomEmoji.vue +++ b/packages/frontend/src/components/global/MkCustomEmoji.vue @@ -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', { diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts index cd55362923..748b4f476e 100644 --- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts +++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts @@ -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, })]; } diff --git a/packages/frontend/src/components/global/MkTime.stories.impl.ts b/packages/frontend/src/components/global/MkTime.stories.impl.ts index 0eeefa4859..0e7f6a9bdf 100644 --- a/packages/frontend/src/components/global/MkTime.stories.impl.ts +++ b/packages/frontend/src/components/global/MkTime.stories.impl.ts @@ -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, diff --git a/packages/frontend/src/components/global/MkTime.vue b/packages/frontend/src/components/global/MkTime.vue index e11db9dc31..2b0bf246ad 100644 --- a/packages/frontend/src/components/global/MkTime.vue +++ b/packages/frontend/src/components/global/MkTime.vue @@ -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() }) ); }); diff --git a/packages/frontend/src/components/global/i18n.ts b/packages/frontend/src/components/global/i18n.ts deleted file mode 100644 index 2f4d7edabd..0000000000 --- a/packages/frontend/src/components/global/i18n.ts +++ /dev/null @@ -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]())); -} diff --git a/packages/frontend/src/components/index.ts b/packages/frontend/src/components/index.ts index a3e13c3a50..f3b476b15c 100644 --- a/packages/frontend/src/components/index.ts +++ b/packages/frontend/src/components/index.ts @@ -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'; |