diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2022-02-02 00:40:00 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2022-02-02 00:40:00 +0900 |
| commit | 9c2f5ee0413ccab907f11e7e94c158eaf77ca020 (patch) | |
| tree | 66ad5e26293fc886406b2ff1e6c8a2ca3fa5ac04 /packages/client/src | |
| parent | Merge branch 'develop' (diff) | |
| parent | 12.103.0 (diff) | |
| download | misskey-9c2f5ee0413ccab907f11e7e94c158eaf77ca020.tar.gz misskey-9c2f5ee0413ccab907f11e7e94c158eaf77ca020.tar.bz2 misskey-9c2f5ee0413ccab907f11e7e94c158eaf77ca020.zip | |
Merge branch 'develop'
Diffstat (limited to 'packages/client/src')
116 files changed, 1417 insertions, 1454 deletions
diff --git a/packages/client/src/account.ts b/packages/client/src/account.ts index 5a935e1dc7..4aeceeccab 100644 --- a/packages/client/src/account.ts +++ b/packages/client/src/account.ts @@ -192,31 +192,31 @@ export async function openAccountMenu(opts: { if (opts.withExtraOperation) { popupMenu([...[{ type: 'link', - text: i18n.locale.profile, + text: i18n.ts.profile, to: `/@${ $i.username }`, avatar: $i, }, null, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, { icon: 'fas fa-plus', - text: i18n.locale.addAccount, + text: i18n.ts.addAccount, action: () => { popupMenu([{ - text: i18n.locale.existingAccount, + text: i18n.ts.existingAccount, action: () => { showSigninDialog(); }, }, { - text: i18n.locale.createAccount, + text: i18n.ts.createAccount, action: () => { createAccount(); }, - }], ev.currentTarget || ev.target); + }], ev.currentTarget ?? ev.target); }, }, { type: 'link', icon: 'fas fa-users', - text: i18n.locale.manageAccounts, + text: i18n.ts.manageAccounts, to: `/settings/accounts`, - }]], ev.currentTarget || ev.target, { + }]], ev.currentTarget ?? ev.target, { align: 'left' }); } else { - popupMenu([...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises], ev.currentTarget || ev.target, { + popupMenu([...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises], ev.currentTarget ?? ev.target, { align: 'left' }); } diff --git a/packages/client/src/components/abuse-report-window.vue b/packages/client/src/components/abuse-report-window.vue index cd04f62bca..f2cb369802 100644 --- a/packages/client/src/components/abuse-report-window.vue +++ b/packages/client/src/components/abuse-report-window.vue @@ -2,7 +2,7 @@ <XWindow ref="window" :initial-width="400" :initial-height="500" :can-resize="true" @closed="emit('closed')"> <template #header> <i class="fas fa-exclamation-circle" style="margin-right: 0.5em;"></i> - <I18n :src="i18n.locale.reportAbuseOf" tag="span"> + <I18n :src="i18n.ts.reportAbuseOf" tag="span"> <template #name> <b><MkAcct :user="user"/></b> </template> @@ -11,12 +11,12 @@ <div class="dpvffvvy _monolithic_"> <div class="_section"> <MkTextarea v-model="comment"> - <template #label>{{ i18n.locale.details }}</template> - <template #caption>{{ i18n.locale.fillAbuseReportDescription }}</template> + <template #label>{{ i18n.ts.details }}</template> + <template #caption>{{ i18n.ts.fillAbuseReportDescription }}</template> </MkTextarea> </div> <div class="_section"> - <MkButton primary full :disabled="comment.length === 0" @click="send">{{ i18n.locale.send }}</MkButton> + <MkButton primary full :disabled="comment.length === 0" @click="send">{{ i18n.ts.send }}</MkButton> </div> </div> </XWindow> @@ -50,7 +50,7 @@ function send() { }, undefined).then(res => { os.alert({ type: 'success', - text: i18n.locale.abuseReported + text: i18n.ts.abuseReported }); window.value?.close(); emit('closed'); diff --git a/packages/client/src/components/autocomplete.vue b/packages/client/src/components/autocomplete.vue index 7ba83b7cb1..91a50ffa59 100644 --- a/packages/client/src/components/autocomplete.vue +++ b/packages/client/src/components/autocomplete.vue @@ -8,7 +8,7 @@ </span> <span class="username">@{{ acct(user) }}</span> </li> - <li tabindex="-1" class="choose" @click="chooseUser()" @keydown="onKeydown">{{ i18n.locale.selectUser }}</li> + <li tabindex="-1" class="choose" @click="chooseUser()" @keydown="onKeydown">{{ i18n.ts.selectUser }}</li> </ol> <ol v-else-if="hashtags.length > 0" ref="suggests" class="hashtags"> <li v-for="hashtag in hashtags" tabindex="-1" @click="complete(type, hashtag)" @keydown="onKeydown"> diff --git a/packages/client/src/components/captcha.vue b/packages/client/src/components/captcha.vue index 307fc312bc..963ae25f8e 100644 --- a/packages/client/src/components/captcha.vue +++ b/packages/client/src/components/captcha.vue @@ -1,6 +1,6 @@ <template> <div> - <span v-if="!available">{{ i18n.locale.waiting }}<MkEllipsis/></span> + <span v-if="!available">{{ i18n.ts.waiting }}<MkEllipsis/></span> <div ref="captchaEl"></div> </div> </template> diff --git a/packages/client/src/components/channel-follow-button.vue b/packages/client/src/components/channel-follow-button.vue index 0ad5384cd5..7bbf5ae663 100644 --- a/packages/client/src/components/channel-follow-button.vue +++ b/packages/client/src/components/channel-follow-button.vue @@ -6,14 +6,14 @@ > <template v-if="!wait"> <template v-if="isFollowing"> - <span v-if="full">{{ i18n.locale.unfollow }}</span><i class="fas fa-minus"></i> + <span v-if="full">{{ i18n.ts.unfollow }}</span><i class="fas fa-minus"></i> </template> <template v-else> - <span v-if="full">{{ i18n.locale.follow }}</span><i class="fas fa-plus"></i> + <span v-if="full">{{ i18n.ts.follow }}</span><i class="fas fa-plus"></i> </template> </template> <template v-else> - <span v-if="full">{{ i18n.locale.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i> + <span v-if="full">{{ i18n.ts.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i> </template> </button> </template> diff --git a/packages/client/src/components/channel-preview.vue b/packages/client/src/components/channel-preview.vue index 8d135a192f..dd3794a657 100644 --- a/packages/client/src/components/channel-preview.vue +++ b/packages/client/src/components/channel-preview.vue @@ -6,7 +6,7 @@ <div class="status"> <div> <i class="fas fa-users fa-fw"></i> - <I18n :src="i18n.locale._channel.usersCount" tag="span" style="margin-left: 4px;"> + <I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;"> <template #n> <b>{{ channel.usersCount }}</b> </template> @@ -14,7 +14,7 @@ </div> <div> <i class="fas fa-pencil-alt fa-fw"></i> - <I18n :src="i18n.locale._channel.notesCount" tag="span" style="margin-left: 4px;"> + <I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;"> <template #n> <b>{{ channel.notesCount }}</b> </template> @@ -27,7 +27,7 @@ </article> <footer> <span v-if="channel.lastNotedAt"> - {{ i18n.locale.updatedAt }}: <MkTime :time="channel.lastNotedAt"/> + {{ i18n.ts.updatedAt }}: <MkTime :time="channel.lastNotedAt"/> </span> </footer> </MkA> diff --git a/packages/client/src/components/chart-tooltip.vue b/packages/client/src/components/chart-tooltip.vue new file mode 100644 index 0000000000..b080eaf2b4 --- /dev/null +++ b/packages/client/src/components/chart-tooltip.vue @@ -0,0 +1,51 @@ +<template> +<MkTooltip ref="tooltip" :showing="showing" :x="x" :y="y" :max-width="340" @closed="emit('closed')"> + <div v-if="title" class="qpcyisrl"> + <div class="title">{{ title }}</div> + <div v-for="x in series" class="series"> + <span class="color" :style="{ background: x.backgroundColor, borderColor: x.borderColor }"></span> + <span>{{ x.text }}</span> + </div> + </div> +</MkTooltip> +</template> + +<script lang="ts" setup> +import { } from 'vue'; +import MkTooltip from './ui/tooltip.vue'; + +const props = defineProps<{ + showing: boolean; + x: number; + y: number; + title: string; + series: { + backgroundColor: string; + borderColor: string; + text: string; + }[]; +}>(); + +const emit = defineEmits<{ + (ev: 'closed'): void; +}>(); +</script> + +<style lang="scss" scoped> +.qpcyisrl { + > .title { + margin-bottom: 4px; + } + + > .series { + > .color { + display: inline-block; + width: 8px; + height: 8px; + border-width: 1px; + border-style: solid; + margin-right: 8px; + } + } +} +</style> diff --git a/packages/client/src/components/chart.vue b/packages/client/src/components/chart.vue index 1959271f5d..3e46c51b47 100644 --- a/packages/client/src/components/chart.vue +++ b/packages/client/src/components/chart.vue @@ -8,7 +8,7 @@ </template> <script lang="ts"> -import { defineComponent, onMounted, ref, watch, PropType } from 'vue'; +import { defineComponent, onMounted, ref, watch, PropType, onUnmounted, shallowRef } from 'vue'; import { Chart, ArcElement, @@ -31,6 +31,7 @@ import { enUS } from 'date-fns/locale'; import zoomPlugin from 'chartjs-plugin-zoom'; import * as os from '@/os'; import { defaultStore } from '@/store'; +import MkChartTooltip from '@/components/chart-tooltip.vue'; Chart.register( ArcElement, @@ -137,12 +138,50 @@ export default defineComponent({ })); }; + const tooltipShowing = ref(false); + const tooltipX = ref(0); + const tooltipY = ref(0); + const tooltipTitle = ref(null); + const tooltipSeries = ref(null); + let disposeTooltipComponent; + + os.popup(MkChartTooltip, { + showing: tooltipShowing, + x: tooltipX, + y: tooltipY, + title: tooltipTitle, + series: tooltipSeries, + }, {}).then(({ dispose }) => { + disposeTooltipComponent = dispose; + }); + + function externalTooltipHandler(context) { + if (context.tooltip.opacity === 0) { + tooltipShowing.value = false; + return; + } + + tooltipTitle.value = context.tooltip.title[0]; + tooltipSeries.value = context.tooltip.body.map((b, i) => ({ + backgroundColor: context.tooltip.labelColors[i].backgroundColor, + borderColor: context.tooltip.labelColors[i].borderColor, + text: b.lines[0], + })); + + const rect = context.chart.canvas.getBoundingClientRect(); + + tooltipShowing.value = true; + tooltipX.value = rect.left + window.pageXOffset + context.tooltip.caretX; + tooltipY.value = rect.top + window.pageYOffset + context.tooltip.caretY; + } + const render = () => { if (chartInstance) { chartInstance.destroy(); } const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'; + const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; // フォントカラー Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg'); @@ -221,10 +260,12 @@ export default defineComponent({ }, }, tooltip: { + enabled: false, mode: 'index', animation: { duration: 0, }, + external: externalTooltipHandler, }, zoom: { pan: { @@ -255,6 +296,27 @@ export default defineComponent({ }, }, }, + plugins: [{ + id: 'vLine', + beforeDraw(chart, args, options) { + if (chart.tooltip._active && chart.tooltip._active.length) { + const activePoint = chart.tooltip._active[0]; + const ctx = chart.ctx; + const x = activePoint.element.x; + const topY = chart.scales.y.top; + const bottomY = chart.scales.y.bottom; + + ctx.save(); + ctx.beginPath(); + ctx.moveTo(x, bottomY); + ctx.lineTo(x, topY); + ctx.lineWidth = 1; + ctx.strokeStyle = vLineColor; + ctx.stroke(); + ctx.restore(); + } + } + }] }); }; @@ -662,6 +724,10 @@ export default defineComponent({ fetchAndRender(); }); + onUnmounted(() => { + if (disposeTooltipComponent) disposeTooltipComponent(); + }); + return { chartEl, fetching, diff --git a/packages/client/src/components/cw-button.vue b/packages/client/src/components/cw-button.vue index ccfd11462a..e7c9aabe4e 100644 --- a/packages/client/src/components/cw-button.vue +++ b/packages/client/src/components/cw-button.vue @@ -1,6 +1,6 @@ <template> <button class="nrvgflfu _button" @click="toggle"> - <b>{{ modelValue ? i18n.locale._cw.hide : i18n.locale._cw.show }}</b> + <b>{{ modelValue ? i18n.ts._cw.hide : i18n.ts._cw.show }}</b> <span v-if="!modelValue">{{ label }}</span> </button> </template> @@ -25,7 +25,7 @@ const label = computed(() => { return concat([ props.note.text ? [i18n.t('_cw.chars', { count: length(props.note.text) })] : [], props.note.files && props.note.files.length !== 0 ? [i18n.t('_cw.files', { count: props.note.files.length }) ] : [], - props.note.poll != null ? [i18n.locale.poll] : [] + props.note.poll != null ? [i18n.ts.poll] : [] ] as string[][]).join(' / '); }); diff --git a/packages/client/src/components/dialog.vue b/packages/client/src/components/dialog.vue index b6b649cde9..3e106a4f0c 100644 --- a/packages/client/src/components/dialog.vue +++ b/packages/client/src/components/dialog.vue @@ -28,8 +28,8 @@ </template> </MkSelect> <div v-if="(showOkButton || showCancelButton) && !actions" class="buttons"> - <MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" @click="ok">{{ (showCancelButton || input || select) ? i18n.locale.ok : i18n.locale.gotIt }}</MkButton> - <MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ i18n.locale.cancel }}</MkButton> + <MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" @click="ok">{{ (showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt }}</MkButton> + <MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ i18n.ts.cancel }}</MkButton> </div> <div v-if="actions" class="buttons"> <MkButton v-for="action in actions" :key="action.text" inline :primary="action.primary" @click="() => { action.callback(); close(); }">{{ action.text }}</MkButton> diff --git a/packages/client/src/components/drive-select-dialog.vue b/packages/client/src/components/drive-select-dialog.vue index 6d84511277..f6c59457d1 100644 --- a/packages/client/src/components/drive-select-dialog.vue +++ b/packages/client/src/components/drive-select-dialog.vue @@ -10,7 +10,7 @@ @closed="emit('closed')" > <template #header> - {{ multiple ? ((type === 'file') ? i18n.locale.selectFiles : i18n.locale.selectFolders) : ((type === 'file') ? i18n.locale.selectFile : i18n.locale.selectFolder) }} + {{ multiple ? ((type === 'file') ? i18n.ts.selectFiles : i18n.ts.selectFolders) : ((type === 'file') ? i18n.ts.selectFile : i18n.ts.selectFolder) }} <span v-if="selected.length > 0" style="margin-left: 8px; opacity: 0.5;">({{ number(selected.length) }})</span> </template> <XDrive :multiple="multiple" :select="type" @changeSelection="onChangeSelection" @selected="ok()"/> diff --git a/packages/client/src/components/drive-window.vue b/packages/client/src/components/drive-window.vue index 8b60bf7794..d08c5fb674 100644 --- a/packages/client/src/components/drive-window.vue +++ b/packages/client/src/components/drive-window.vue @@ -6,7 +6,7 @@ @closed="emit('closed')" > <template #header> - {{ i18n.locale.drive }} + {{ i18n.ts.drive }} </template> <XDrive :initial-folder="initialFolder"/> </XWindow> diff --git a/packages/client/src/components/drive.file.vue b/packages/client/src/components/drive.file.vue index fd6a813838..262eae0de1 100644 --- a/packages/client/src/components/drive.file.vue +++ b/packages/client/src/components/drive.file.vue @@ -10,15 +10,15 @@ > <div v-if="$i?.avatarId == file.id" class="label"> <img src="/client-assets/label.svg"/> - <p>{{ i18n.locale.avatar }}</p> + <p>{{ i18n.ts.avatar }}</p> </div> <div v-if="$i?.bannerId == file.id" class="label"> <img src="/client-assets/label.svg"/> - <p>{{ i18n.locale.banner }}</p> + <p>{{ i18n.ts.banner }}</p> </div> <div v-if="file.isSensitive" class="label red"> <img src="/client-assets/label-red.svg"/> - <p>{{ i18n.locale.nsfw }}</p> + <p>{{ i18n.ts.nsfw }}</p> </div> <MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/> @@ -61,30 +61,30 @@ const title = computed(() => `${props.file.name}\n${props.file.type} ${bytes(pro function getMenu() { return [{ - text: i18n.locale.rename, + text: i18n.ts.rename, icon: 'fas fa-i-cursor', action: rename }, { - text: props.file.isSensitive ? i18n.locale.unmarkAsSensitive : i18n.locale.markAsSensitive, + text: props.file.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive, icon: props.file.isSensitive ? 'fas fa-eye' : 'fas fa-eye-slash', action: toggleSensitive }, { - text: i18n.locale.describeFile, + text: i18n.ts.describeFile, icon: 'fas fa-i-cursor', action: describe }, null, { - text: i18n.locale.copyUrl, + text: i18n.ts.copyUrl, icon: 'fas fa-link', action: copyUrl }, { type: 'a', href: props.file.url, target: '_blank', - text: i18n.locale.download, + text: i18n.ts.download, icon: 'fas fa-download', download: props.file.name }, null, { - text: i18n.locale.delete, + text: i18n.ts.delete, icon: 'fas fa-trash-alt', danger: true, action: deleteFile @@ -95,7 +95,7 @@ function onClick(ev: MouseEvent) { if (props.selectMode) { emit('chosen', props.file); } else { - os.popupMenu(getMenu(), (ev.currentTarget || ev.target || undefined) as HTMLElement | undefined); + os.popupMenu(getMenu(), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined); } } @@ -120,8 +120,8 @@ function onDragend() { function rename() { os.inputText({ - title: i18n.locale.renameFile, - placeholder: i18n.locale.inputNewFileName, + title: i18n.ts.renameFile, + placeholder: i18n.ts.inputNewFileName, default: props.file.name, }).then(({ canceled, result: name }) => { if (canceled) return; @@ -134,9 +134,9 @@ function rename() { function describe() { os.popup(import('@/components/media-caption.vue'), { - title: i18n.locale.describeFile, + title: i18n.ts.describeFile, input: { - placeholder: i18n.locale.inputNewDescription, + placeholder: i18n.ts.inputNewDescription, default: props.file.comment !== null ? props.file.comment : '', }, image: props.file diff --git a/packages/client/src/components/drive.folder.vue b/packages/client/src/components/drive.folder.vue index 20a6343cfe..57621bf097 100644 --- a/packages/client/src/components/drive.folder.vue +++ b/packages/client/src/components/drive.folder.vue @@ -20,7 +20,7 @@ {{ folder.name }} </p> <p v-if="defaultStore.state.uploadFolder == folder.id" class="upload"> - {{ i18n.locale.uploadFolder }} + {{ i18n.ts.uploadFolder }} </p> <button v-if="selectMode" class="checkbox _button" :class="{ checked: isSelected }" @click.prevent.stop="checkboxClicked"></button> </div> @@ -146,14 +146,14 @@ function onDrop(ev: DragEvent) { switch (err) { case 'detected-circular-definition': os.alert({ - title: i18n.locale.unableToProcess, - text: i18n.locale.circularReferenceFolder + title: i18n.ts.unableToProcess, + text: i18n.ts.circularReferenceFolder }); break; default: os.alert({ type: 'error', - text: i18n.locale.somethingHappened + text: i18n.ts.somethingHappened }); } }); @@ -184,8 +184,8 @@ function go() { function rename() { os.inputText({ - title: i18n.locale.renameFolder, - placeholder: i18n.locale.inputNewFolderName, + title: i18n.ts.renameFolder, + placeholder: i18n.ts.inputNewFolderName, default: props.folder.name }).then(({ canceled, result: name }) => { if (canceled) return; @@ -208,14 +208,14 @@ function deleteFolder() { case 'b0fc8a17-963c-405d-bfbc-859a487295e1': os.alert({ type: 'error', - title: i18n.locale.unableToDelete, - text: i18n.locale.hasChildFilesOrFolders + title: i18n.ts.unableToDelete, + text: i18n.ts.hasChildFilesOrFolders }); break; default: os.alert({ type: 'error', - text: i18n.locale.unableToDelete + text: i18n.ts.unableToDelete }); } }); @@ -227,7 +227,7 @@ function setAsUploadFolder() { function onContextmenu(ev: MouseEvent) { os.contextMenu([{ - text: i18n.locale.openInWindow, + text: i18n.ts.openInWindow, icon: 'fas fa-window-restore', action: () => { os.popup(import('./drive-window.vue'), { @@ -236,11 +236,11 @@ function onContextmenu(ev: MouseEvent) { }, 'closed'); } }, null, { - text: i18n.locale.rename, + text: i18n.ts.rename, icon: 'fas fa-i-cursor', action: rename, }, null, { - text: i18n.locale.delete, + text: i18n.ts.delete, icon: 'fas fa-trash-alt', danger: true, action: deleteFolder, diff --git a/packages/client/src/components/drive.nav-folder.vue b/packages/client/src/components/drive.nav-folder.vue index 7c35c5d3da..67223267c1 100644 --- a/packages/client/src/components/drive.nav-folder.vue +++ b/packages/client/src/components/drive.nav-folder.vue @@ -8,7 +8,7 @@ @drop.stop="onDrop" > <i v-if="folder == null" class="fas fa-cloud"></i> - <span>{{ folder == null ? i18n.locale.drive : folder.name }}</span> + <span>{{ folder == null ? i18n.ts.drive : folder.name }}</span> </div> </template> diff --git a/packages/client/src/components/drive.vue b/packages/client/src/components/drive.vue index e27b0a5fbb..e044c67523 100644 --- a/packages/client/src/components/drive.vue +++ b/packages/client/src/components/drive.vue @@ -54,7 +54,7 @@ /> <!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid --> <div v-for="(n, i) in 16" :key="i" class="padding"></div> - <MkButton v-if="moreFolders" ref="moreFolders">{{ i18n.locale.loadMore }}</MkButton> + <MkButton v-if="moreFolders" ref="moreFolders">{{ i18n.ts.loadMore }}</MkButton> </div> <div v-show="files.length > 0" ref="filesContainer" class="files"> <XFile @@ -71,12 +71,12 @@ /> <!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid --> <div v-for="(n, i) in 16" :key="i" class="padding"></div> - <MkButton v-show="moreFiles" ref="loadMoreFiles" @click="fetchMoreFiles">{{ i18n.locale.loadMore }}</MkButton> + <MkButton v-show="moreFiles" ref="loadMoreFiles" @click="fetchMoreFiles">{{ i18n.ts.loadMore }}</MkButton> </div> <div v-if="files.length == 0 && folders.length == 0 && !fetching" class="empty"> <p v-if="draghover">{{ i18n.t('empty-draghover') }}</p> - <p v-if="!draghover && folder == null"><strong>{{ i18n.locale.emptyDrive }}</strong><br/>{{ i18n.t('empty-drive-description') }}</p> - <p v-if="!draghover && folder != null">{{ i18n.locale.emptyFolder }}</p> + <p v-if="!draghover && folder == null"><strong>{{ i18n.ts.emptyDrive }}</strong><br/>{{ i18n.t('empty-drive-description') }}</p> + <p v-if="!draghover && folder != null">{{ i18n.ts.emptyFolder }}</p> </div> </div> <MkLoading v-if="fetching"/> @@ -253,14 +253,14 @@ function onDrop(e: DragEvent): any { switch (err) { case 'detected-circular-definition': os.alert({ - title: i18n.locale.unableToProcess, - text: i18n.locale.circularReferenceFolder + title: i18n.ts.unableToProcess, + text: i18n.ts.circularReferenceFolder }); break; default: os.alert({ type: 'error', - text: i18n.locale.somethingHappened + text: i18n.ts.somethingHappened }); } }); @@ -274,9 +274,9 @@ function selectLocalFile() { function urlUpload() { os.inputText({ - title: i18n.locale.uploadFromUrl, + title: i18n.ts.uploadFromUrl, type: 'url', - placeholder: i18n.locale.uploadFromUrlDescription + placeholder: i18n.ts.uploadFromUrlDescription }).then(({ canceled, result: url }) => { if (canceled || !url) return; os.api('drive/files/upload-from-url', { @@ -285,16 +285,16 @@ function urlUpload() { }); os.alert({ - title: i18n.locale.uploadFromUrlRequested, - text: i18n.locale.uploadFromUrlMayTakeTime + title: i18n.ts.uploadFromUrlRequested, + text: i18n.ts.uploadFromUrlMayTakeTime }); }); } function createFolder() { os.inputText({ - title: i18n.locale.createFolder, - placeholder: i18n.locale.folderName + title: i18n.ts.createFolder, + placeholder: i18n.ts.folderName }).then(({ canceled, result: name }) => { if (canceled) return; os.api('drive/folders/create', { @@ -308,8 +308,8 @@ function createFolder() { function renameFolder(folderToRename: Misskey.entities.DriveFolder) { os.inputText({ - title: i18n.locale.renameFolder, - placeholder: i18n.locale.inputNewFolderName, + title: i18n.ts.renameFolder, + placeholder: i18n.ts.inputNewFolderName, default: folderToRename.name }).then(({ canceled, result: name }) => { if (canceled) return; @@ -334,14 +334,14 @@ function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) { case 'b0fc8a17-963c-405d-bfbc-859a487295e1': os.alert({ type: 'error', - title: i18n.locale.unableToDelete, - text: i18n.locale.hasChildFilesOrFolders + title: i18n.ts.unableToDelete, + text: i18n.ts.hasChildFilesOrFolders }); break; default: os.alert({ type: 'error', - text: i18n.locale.unableToDelete + text: i18n.ts.unableToDelete }); } }); @@ -562,36 +562,36 @@ function fetchMoreFiles() { function getMenu() { return [{ - text: i18n.locale.addFile, + text: i18n.ts.addFile, type: 'label' }, { - text: i18n.locale.upload, + text: i18n.ts.upload, icon: 'fas fa-upload', action: () => { selectLocalFile(); } }, { - text: i18n.locale.fromUrl, + text: i18n.ts.fromUrl, icon: 'fas fa-link', action: () => { urlUpload(); } }, null, { - text: folder.value ? folder.value.name : i18n.locale.drive, + text: folder.value ? folder.value.name : i18n.ts.drive, type: 'label' }, folder.value ? { - text: i18n.locale.renameFolder, + text: i18n.ts.renameFolder, icon: 'fas fa-i-cursor', action: () => { renameFolder(folder.value); } } : undefined, folder.value ? { - text: i18n.locale.deleteFolder, + text: i18n.ts.deleteFolder, icon: 'fas fa-trash-alt', action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); } } : undefined, { - text: i18n.locale.createFolder, + text: i18n.ts.createFolder, icon: 'fas fa-folder-plus', action: () => { createFolder(); } }]; } function showMenu(ev: MouseEvent) { - os.popupMenu(getMenu(), (ev.currentTarget || ev.target || undefined) as HTMLElement | undefined); + os.popupMenu(getMenu(), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined); } function onContextmenu(ev: MouseEvent) { diff --git a/packages/client/src/components/emoji-picker-dialog.vue b/packages/client/src/components/emoji-picker-dialog.vue index f06a24636c..2c0b2e9a8b 100644 --- a/packages/client/src/components/emoji-picker-dialog.vue +++ b/packages/client/src/components/emoji-picker-dialog.vue @@ -32,20 +32,20 @@ import MkEmojiPicker from '@/components/emoji-picker.vue'; import { defaultStore } from '@/store'; withDefaults(defineProps<{ - manualShowing?: boolean; + manualShowing?: boolean | null; src?: HTMLElement; showPinned?: boolean; asReactionPicker?: boolean; }>(), { - manualShowing: false, + manualShowing: null, showPinned: true, asReactionPicker: false, }); const emit = defineEmits<{ - (e: 'done', v: any): void; - (e: 'close'): void; - (e: 'closed'): void; + (ev: 'done', v: any): void; + (ev: 'close'): void; + (ev: 'closed'): void; }>(); const modal = ref<InstanceType<typeof MkModal>>(); diff --git a/packages/client/src/components/emoji-picker.vue b/packages/client/src/components/emoji-picker.vue index 96670fa58c..6999ad6517 100644 --- a/packages/client/src/components/emoji-picker.vue +++ b/packages/client/src/components/emoji-picker.vue @@ -1,6 +1,6 @@ <template> <div class="omfetrab" :class="['w' + width, 'h' + height, { big, asDrawer }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }"> - <input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.locale.search" @paste.stop="paste" @keyup.enter="done()"> + <input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" @paste.stop="paste" @keyup.enter="done()"> <div ref="emojis" class="emojis"> <section class="result"> <div v-if="searchResultCustom.length > 0"> @@ -43,7 +43,7 @@ </section> <section> - <header class="_acrylic"><i class="far fa-clock fa-fw"></i> {{ i18n.locale.recentUsed }}</header> + <header class="_acrylic"><i class="far fa-clock fa-fw"></i> {{ i18n.ts.recentUsed }}</header> <div> <button v-for="emoji in recentlyUsedEmojis" :key="emoji" @@ -56,11 +56,11 @@ </section> </div> <div> - <header class="_acrylic">{{ i18n.locale.customEmojis }}</header> - <XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':')" @chosen="chosen">{{ category || i18n.locale.other }}</XSection> + <header class="_acrylic">{{ i18n.ts.customEmojis }}</header> + <XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':')" @chosen="chosen">{{ category || i18n.ts.other }}</XSection> </div> <div> - <header class="_acrylic">{{ i18n.locale.emoji }}</header> + <header class="_acrylic">{{ i18n.ts.emoji }}</header> <XSection v-for="category in categories" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)" @chosen="chosen">{{ category }}</XSection> </div> </div> @@ -280,7 +280,7 @@ function getKey(emoji: string | Misskey.entities.CustomEmoji | UnicodeEmojiDef): } function chosen(emoji: any, ev?: MouseEvent) { - const el = ev && (ev.currentTarget || ev.target) as HTMLElement | null | undefined; + const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined; if (el) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); diff --git a/packages/client/src/components/follow-button.vue b/packages/client/src/components/follow-button.vue index 345edb6441..93c9e891c1 100644 --- a/packages/client/src/components/follow-button.vue +++ b/packages/client/src/components/follow-button.vue @@ -6,23 +6,23 @@ > <template v-if="!wait"> <template v-if="hasPendingFollowRequestFromYou && user.isLocked"> - <span v-if="full">{{ i18n.locale.followRequestPending }}</span><i class="fas fa-hourglass-half"></i> + <span v-if="full">{{ i18n.ts.followRequestPending }}</span><i class="fas fa-hourglass-half"></i> </template> <template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked"> <!-- つまりリモートフォローの場合。 --> - <span v-if="full">{{ i18n.locale.processing }}</span><i class="fas fa-spinner fa-pulse"></i> + <span v-if="full">{{ i18n.ts.processing }}</span><i class="fas fa-spinner fa-pulse"></i> </template> <template v-else-if="isFollowing"> - <span v-if="full">{{ i18n.locale.unfollow }}</span><i class="fas fa-minus"></i> + <span v-if="full">{{ i18n.ts.unfollow }}</span><i class="fas fa-minus"></i> </template> <template v-else-if="!isFollowing && user.isLocked"> - <span v-if="full">{{ i18n.locale.followRequest }}</span><i class="fas fa-plus"></i> + <span v-if="full">{{ i18n.ts.followRequest }}</span><i class="fas fa-plus"></i> </template> <template v-else-if="!isFollowing && !user.isLocked"> - <span v-if="full">{{ i18n.locale.follow }}</span><i class="fas fa-plus"></i> + <span v-if="full">{{ i18n.ts.follow }}</span><i class="fas fa-plus"></i> </template> </template> <template v-else> - <span v-if="full">{{ i18n.locale.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i> + <span v-if="full">{{ i18n.ts.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i> </template> </button> </template> diff --git a/packages/client/src/components/forgot-password.vue b/packages/client/src/components/forgot-password.vue index c74e1ac75e..46cbf6bd70 100644 --- a/packages/client/src/components/forgot-password.vue +++ b/packages/client/src/components/forgot-password.vue @@ -5,28 +5,28 @@ @close="dialog.close()" @closed="emit('closed')" > - <template #header>{{ i18n.locale.forgotPassword }}</template> + <template #header>{{ i18n.ts.forgotPassword }}</template> <form v-if="instance.enableEmail" class="bafeceda" @submit.prevent="onSubmit"> <div class="main _formRoot"> <MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required> - <template #label>{{ i18n.locale.username }}</template> + <template #label>{{ i18n.ts.username }}</template> <template #prefix>@</template> </MkInput> <MkInput v-model="email" class="_formBlock" type="email" spellcheck="false" required> - <template #label>{{ i18n.locale.emailAddress }}</template> - <template #caption>{{ i18n.locale._forgotPassword.enterEmail }}</template> + <template #label>{{ i18n.ts.emailAddress }}</template> + <template #caption>{{ i18n.ts._forgotPassword.enterEmail }}</template> </MkInput> - <MkButton class="_formBlock" type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ i18n.locale.send }}</MkButton> + <MkButton class="_formBlock" type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ i18n.ts.send }}</MkButton> </div> <div class="sub"> - <MkA to="/about" class="_link">{{ i18n.locale._forgotPassword.ifNoEmail }}</MkA> + <MkA to="/about" class="_link">{{ i18n.ts._forgotPassword.ifNoEmail }}</MkA> </div> </form> <div v-else class="bafecedb"> - {{ i18n.locale._forgotPassword.contactAdmin }} + {{ i18n.ts._forgotPassword.contactAdmin }} </div> </XModalWindow> </template> diff --git a/packages/client/src/components/form/range.vue b/packages/client/src/components/form/range.vue index 3e02cacb9b..a82348d317 100644 --- a/packages/client/src/components/form/range.vue +++ b/packages/client/src/components/form/range.vue @@ -117,7 +117,7 @@ export default defineComponent({ text: computed(() => { return props.textConverter(finalValue.value); }), - source: thumbEl, + targetElement: thumbEl, }, {}, 'closed'); const style = document.createElement('style'); diff --git a/packages/client/src/components/form/switch.vue b/packages/client/src/components/form/switch.vue index f8a07b4caa..b5a30d635c 100644 --- a/packages/client/src/components/form/switch.vue +++ b/packages/client/src/components/form/switch.vue @@ -20,45 +20,33 @@ </div> </template> -<script lang="ts"> -import { defineComponent, ref, toRefs } from 'vue'; +<script lang="ts" setup> +import { toRefs, Ref } from 'vue'; import * as os from '@/os'; import Ripple from '@/components/ripple.vue'; -export default defineComponent({ - props: { - modelValue: { - type: Boolean, - default: false - }, - disabled: { - type: Boolean, - default: false - } - }, +const props = defineProps<{ + modelValue: boolean | Ref<boolean>; + disabled?: boolean; +}>(); - setup(props, context) { - const button = ref<HTMLElement>(); - const checked = toRefs(props).modelValue; - const toggle = () => { - if (props.disabled) return; - context.emit('update:modelValue', !checked.value); +const emit = defineEmits<{ + (e: 'update:modelValue', v: boolean): void; +}>(); - if (!checked.value) { - const rect = button.value.getBoundingClientRect(); - const x = rect.left + (button.value.offsetWidth / 2); - const y = rect.top + (button.value.offsetHeight / 2); - os.popup(Ripple, { x, y, particle: false }, {}, 'end'); - } - }; +let button = $ref<HTMLElement>(); +const checked = toRefs(props).modelValue; +const toggle = () => { + if (props.disabled) return; + emit('update:modelValue', !checked.value); - return { - button, - checked, - toggle, - }; - }, -}); + if (!checked.value) { + const rect = button.getBoundingClientRect(); + const x = rect.left + (button.offsetWidth / 2); + const y = rect.top + (button.offsetHeight / 2); + os.popup(Ripple, { x, y, particle: false }, {}, 'end'); + } +}; </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/global/a.vue b/packages/client/src/components/global/a.vue index cf7385ca22..52fef50f9b 100644 --- a/packages/client/src/components/global/a.vue +++ b/packages/client/src/components/global/a.vue @@ -23,8 +23,9 @@ const props = withDefaults(defineProps<{ behavior: null, }); -const navHook = inject('navHook', null); -const sideViewHook = inject('sideViewHook', null); +type Navigate = (path: string, record?: boolean) => void; +const navHook = inject<null | Navigate>('navHook', null); +const sideViewHook = inject<null | Navigate>('sideViewHook', null); const active = $computed(() => { if (props.activeClass == null) return false; @@ -43,31 +44,31 @@ function onContextmenu(ev) { text: props.to, }, { icon: 'fas fa-window-maximize', - text: i18n.locale.openInWindow, + text: i18n.ts.openInWindow, action: () => { os.pageWindow(props.to); } }, sideViewHook ? { icon: 'fas fa-columns', - text: i18n.locale.openInSideView, + text: i18n.ts.openInSideView, action: () => { sideViewHook(props.to); } } : undefined, { icon: 'fas fa-expand-alt', - text: i18n.locale.showInPage, + text: i18n.ts.showInPage, action: () => { router.push(props.to); } }, null, { icon: 'fas fa-external-link-alt', - text: i18n.locale.openInNewTab, + text: i18n.ts.openInNewTab, action: () => { window.open(props.to, '_blank'); } }, { icon: 'fas fa-link', - text: i18n.locale.copyLink, + text: i18n.ts.copyLink, action: () => { copyToClipboard(`${url}${props.to}`); } diff --git a/packages/client/src/components/global/header.vue b/packages/client/src/components/global/header.vue index a241ece407..e558614c12 100644 --- a/packages/client/src/components/global/header.vue +++ b/packages/client/src/components/global/header.vue @@ -104,7 +104,7 @@ export default defineComponent({ if (props.info.share) { if (menu.length > 0) menu.push(null); menu.push({ - text: i18n.locale.share, + text: i18n.ts.share, icon: 'fas fa-share-alt', action: share }); @@ -113,7 +113,7 @@ export default defineComponent({ if (menu.length > 0) menu.push(null); menu = menu.concat(props.menu); } - popupMenu(menu, ev.currentTarget || ev.target); + popupMenu(menu, ev.currentTarget ?? ev.target); }; const showTabsPopup = (ev: MouseEvent) => { @@ -126,7 +126,7 @@ export default defineComponent({ icon: tab.icon, action: tab.onClick, })); - popupMenu(menu, ev.currentTarget || ev.target); + popupMenu(menu, ev.currentTarget ?? ev.target); }; const preventDrag = (ev: TouchEvent) => { diff --git a/packages/client/src/components/global/time.vue b/packages/client/src/components/global/time.vue index d2788264c5..5748d9de61 100644 --- a/packages/client/src/components/global/time.vue +++ b/packages/client/src/components/global/time.vue @@ -24,16 +24,16 @@ let now = $ref(new Date()); const relative = $computed(() => { const ago = (now.getTime() - _time.getTime()) / 1000/*ms*/; return ( - ago >= 31536000 ? i18n.t('_ago.yearsAgo', { n: (~~(ago / 31536000)).toString() }) : - ago >= 2592000 ? i18n.t('_ago.monthsAgo', { n: (~~(ago / 2592000)).toString() }) : - ago >= 604800 ? i18n.t('_ago.weeksAgo', { n: (~~(ago / 604800)).toString() }) : - ago >= 86400 ? i18n.t('_ago.daysAgo', { n: (~~(ago / 86400)).toString() }) : - ago >= 3600 ? i18n.t('_ago.hoursAgo', { n: (~~(ago / 3600)).toString() }) : + ago >= 31536000 ? i18n.t('_ago.yearsAgo', { n: Math.round(ago / 31536000).toString() }) : + ago >= 2592000 ? i18n.t('_ago.monthsAgo', { n: Math.round(ago / 2592000).toString() }) : + ago >= 604800 ? i18n.t('_ago.weeksAgo', { n: Math.round(ago / 604800).toString() }) : + ago >= 86400 ? i18n.t('_ago.daysAgo', { n: Math.round(ago / 86400).toString() }) : + ago >= 3600 ? i18n.t('_ago.hoursAgo', { n: Math.round(ago / 3600).toString() }) : ago >= 60 ? i18n.t('_ago.minutesAgo', { n: (~~(ago / 60)).toString() }) : ago >= 10 ? i18n.t('_ago.secondsAgo', { n: (~~(ago % 60)).toString() }) : - ago >= -1 ? i18n.locale._ago.justNow : - ago < -1 ? i18n.locale._ago.future : - i18n.locale._ago.unknown); + ago >= -1 ? i18n.ts._ago.justNow : + ago < -1 ? i18n.ts._ago.future : + i18n.ts._ago.unknown); }); function tick() { diff --git a/packages/client/src/components/media-image.vue b/packages/client/src/components/media-image.vue index 3e2cabae0a..43639f6771 100644 --- a/packages/client/src/components/media-image.vue +++ b/packages/client/src/components/media-image.vue @@ -20,52 +20,32 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { watch } from 'vue'; +import * as misskey from 'misskey-js'; import { getStaticImageUrl } from '@/scripts/get-static-image-url'; import ImgWithBlurhash from '@/components/img-with-blurhash.vue'; -import * as os from '@/os'; +import { defaultStore } from '@/store'; -export default defineComponent({ - components: { - ImgWithBlurhash - }, - props: { - image: { - type: Object, - required: true - }, - raw: { - default: false - } - }, - data() { - return { - hide: true, - }; - }, - computed: { - url(): any { - let url = this.$store.state.disableShowingAnimatedImages - ? getStaticImageUrl(this.image.thumbnailUrl) - : this.image.thumbnailUrl; +const props = defineProps<{ + image: misskey.entities.DriveFile; + raw?: boolean; +}>(); - if (this.raw || this.$store.state.loadRawImages) { - url = this.image.url; - } +let hide = $ref(true); - return url; - } - }, - created() { - // Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする - this.$watch('image', () => { - this.hide = (this.$store.state.nsfw === 'force') ? true : this.image.isSensitive && (this.$store.state.nsfw !== 'ignore'); - }, { - deep: true, - immediate: true, - }); - }, +const url = (props.raw || defaultStore.state.loadRawImages) + ? props.image.url + : defaultStore.state.disableShowingAnimatedImages + ? getStaticImageUrl(props.image.thumbnailUrl) + : props.image.thumbnailUrl; + +// Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする +watch(() => props.image, () => { + hide = (defaultStore.state.nsfw === 'force') ? true : props.image.isSensitive && (defaultStore.state.nsfw !== 'ignore'); +}, { + deep: true, + immediate: true, }); </script> diff --git a/packages/client/src/components/media-list.vue b/packages/client/src/components/media-list.vue index efcbb12922..532627edbd 100644 --- a/packages/client/src/components/media-list.vue +++ b/packages/client/src/components/media-list.vue @@ -12,8 +12,8 @@ </div> </template> -<script lang="ts"> -import { defineComponent, onMounted, PropType, ref } from 'vue'; +<script lang="ts" setup> +import { onMounted, ref } from 'vue'; import * as misskey from 'misskey-js'; import PhotoSwipeLightbox from 'photoswipe/dist/photoswipe-lightbox.esm.js'; import PhotoSwipe from 'photoswipe/dist/photoswipe.esm.js'; @@ -25,98 +25,80 @@ import * as os from '@/os'; import { FILE_TYPE_BROWSERSAFE } from '@/const'; import { defaultStore } from '@/store'; -export default defineComponent({ - components: { - XBanner, - XImage, - XVideo, - }, - props: { - mediaList: { - type: Array as PropType<misskey.entities.DriveFile[]>, - required: true, - }, - raw: { - default: false - }, - }, - setup(props) { - const gallery = ref(null); +const props = defineProps<{ + mediaList: misskey.entities.DriveFile[]; + raw?: boolean; +}>(); - onMounted(() => { - const lightbox = new PhotoSwipeLightbox({ - dataSource: props.mediaList - .filter(media => { - if (media.type === 'image/svg+xml') return true; // svgのwebpublicはpngなのでtrue - return media.type.startsWith('image') && FILE_TYPE_BROWSERSAFE.includes(media.type); - }) - .map(media => { - const item = { - src: media.url, - w: media.properties.width, - h: media.properties.height, - alt: media.name, - }; - if (media.properties.orientation != null && media.properties.orientation >= 5) { - [item.w, item.h] = [item.h, item.w]; - } - return item; - }), - gallery: gallery.value, - children: '.image', - thumbSelector: '.image', - loop: false, - padding: window.innerWidth > 500 ? { - top: 32, - bottom: 32, - left: 32, - right: 32, - } : { - top: 0, - bottom: 0, - left: 0, - right: 0, - }, - imageClickAction: 'close', - tapAction: 'toggle-controls', - pswpModule: PhotoSwipe, - }); +const gallery = ref(null); +const pswpZIndex = os.claimZIndex('middle'); - lightbox.on('itemData', (e) => { - const { itemData } = e; - - // element is children - const { element } = itemData; +onMounted(() => { + const lightbox = new PhotoSwipeLightbox({ + dataSource: props.mediaList + .filter(media => { + if (media.type === 'image/svg+xml') return true; // svgのwebpublicはpngなのでtrue + return media.type.startsWith('image') && FILE_TYPE_BROWSERSAFE.includes(media.type); + }) + .map(media => { + const item = { + src: media.url, + w: media.properties.width, + h: media.properties.height, + alt: media.name, + }; + if (media.properties.orientation != null && media.properties.orientation >= 5) { + [item.w, item.h] = [item.h, item.w]; + } + return item; + }), + gallery: gallery.value, + children: '.image', + thumbSelector: '.image', + loop: false, + padding: window.innerWidth > 500 ? { + top: 32, + bottom: 32, + left: 32, + right: 32, + } : { + top: 0, + bottom: 0, + left: 0, + right: 0, + }, + imageClickAction: 'close', + tapAction: 'toggle-controls', + pswpModule: PhotoSwipe, + }); - const id = element.dataset.id; - const file = props.mediaList.find(media => media.id === id); + lightbox.on('itemData', (ev) => { + const { itemData } = ev; - itemData.src = file.url; - itemData.w = Number(file.properties.width); - itemData.h = Number(file.properties.height); - if (file.properties.orientation != null && file.properties.orientation >= 5) { - [itemData.w, itemData.h] = [itemData.h, itemData.w]; - } - itemData.msrc = file.thumbnailUrl; - itemData.thumbCropped = true; - }); + // element is children + const { element } = itemData; - lightbox.init(); - }); + const id = element.dataset.id; + const file = props.mediaList.find(media => media.id === id); - const previewable = (file: misskey.entities.DriveFile): boolean => { - if (file.type === 'image/svg+xml') return true; // svgのwebpublic/thumbnailはpngなのでtrue - // FILE_TYPE_BROWSERSAFEに適合しないものはブラウザで表示するのに不適切 - return (file.type.startsWith('video') || file.type.startsWith('image')) && FILE_TYPE_BROWSERSAFE.includes(file.type); - }; + itemData.src = file.url; + itemData.w = Number(file.properties.width); + itemData.h = Number(file.properties.height); + if (file.properties.orientation != null && file.properties.orientation >= 5) { + [itemData.w, itemData.h] = [itemData.h, itemData.w]; + } + itemData.msrc = file.thumbnailUrl; + itemData.thumbCropped = true; + }); - return { - previewable, - gallery, - pswpZIndex: os.claimZIndex('middle'), - }; - }, + lightbox.init(); }); + +const previewable = (file: misskey.entities.DriveFile): boolean => { + if (file.type === 'image/svg+xml') return true; // svgのwebpublic/thumbnailはpngなのでtrue + // FILE_TYPE_BROWSERSAFEに適合しないものはブラウザで表示するのに不適切 + return (file.type.startsWith('video') || file.type.startsWith('image')) && FILE_TYPE_BROWSERSAFE.includes(file.type); +}; </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/note-detailed.vue b/packages/client/src/components/note-detailed.vue index a3b30f726e..5fc3a0f334 100644 --- a/packages/client/src/components/note-detailed.vue +++ b/packages/client/src/components/note-detailed.vue @@ -250,7 +250,7 @@ function menu(viaKeyboard = false): void { function showRenoteMenu(viaKeyboard = false): void { if (!isMyRenote) return; os.popupMenu([{ - text: i18n.locale.unrenote, + text: i18n.ts.unrenote, icon: 'fas fa-trash-alt', danger: true, action: () => { diff --git a/packages/client/src/components/note.vue b/packages/client/src/components/note.vue index fc89c2777b..6c596fb60d 100644 --- a/packages/client/src/components/note.vue +++ b/packages/client/src/components/note.vue @@ -10,13 +10,13 @@ :class="{ renote: isRenote }" > <MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/> - <div v-if="pinned" class="info"><i class="fas fa-thumbtack"></i> {{ i18n.locale.pinnedNote }}</div> - <div v-if="appearNote._prId_" class="info"><i class="fas fa-bullhorn"></i> {{ i18n.locale.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.locale.hideThisNote }} <i class="fas fa-times"></i></button></div> - <div v-if="appearNote._featuredId_" class="info"><i class="fas fa-bolt"></i> {{ i18n.locale.featured }}</div> + <div v-if="pinned" class="info"><i class="fas fa-thumbtack"></i> {{ i18n.ts.pinnedNote }}</div> + <div v-if="appearNote._prId_" class="info"><i class="fas fa-bullhorn"></i> {{ i18n.ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.ts.hideThisNote }} <i class="fas fa-times"></i></button></div> + <div v-if="appearNote._featuredId_" class="info"><i class="fas fa-bolt"></i> {{ i18n.ts.featured }}</div> <div v-if="isRenote" class="renote"> <MkAvatar class="avatar" :user="note.user"/> <i class="fas fa-retweet"></i> - <I18n :src="i18n.locale.renotedBy" tag="span"> + <I18n :src="i18n.ts.renotedBy" tag="span"> <template #user> <MkA v-user-preview="note.userId" class="name" :to="userPage(note.user)"> <MkUserName :user="note.user"/> @@ -48,7 +48,7 @@ </p> <div v-show="appearNote.cw == null || showContent" class="content" :class="{ collapsed }"> <div class="text"> - <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.locale.private }})</span> + <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> <MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i class="fas fa-reply"></i></MkA> <Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/> <a v-if="appearNote.renote != null" class="rp">RN:</a> @@ -67,7 +67,7 @@ <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" class="url-preview"/> <div v-if="appearNote.renote" class="renote"><XNoteSimple :note="appearNote.renote"/></div> <button v-if="collapsed" class="fade _button" @click="collapsed = false"> - <span>{{ i18n.locale.showMore }}</span> + <span>{{ i18n.ts.showMore }}</span> </button> </div> <MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="fas fa-satellite-dish"></i> {{ appearNote.channel.name }}</MkA> @@ -94,7 +94,7 @@ </article> </div> <div v-else class="muted" @click="muted = false"> - <I18n :src="i18n.locale.userSaysSomething" tag="small"> + <I18n :src="i18n.ts.userSaysSomething" tag="small"> <template #name> <MkA v-user-preview="appearNote.userId" class="name" :to="userPage(appearNote.user)"> <MkUserName :user="appearNote.user"/> @@ -238,7 +238,7 @@ function menu(viaKeyboard = false): void { function showRenoteMenu(viaKeyboard = false): void { if (!isMyRenote) return; os.popupMenu([{ - text: i18n.locale.unrenote, + text: i18n.ts.unrenote, icon: 'fas fa-trash-alt', danger: true, action: () => { diff --git a/packages/client/src/components/notification.vue b/packages/client/src/components/notification.vue index 5659c899be..d855f81f8a 100644 --- a/packages/client/src/components/notification.vue +++ b/packages/client/src/components/notification.vue @@ -153,7 +153,7 @@ export default defineComponent({ showing, reaction: props.notification.reaction ? props.notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : props.notification.reaction, emojis: props.notification.note.emojis, - source: reactionRef.value.$el, + targetElement: reactionRef.value.$el, }, {}, 'closed'); }); diff --git a/packages/client/src/components/page-window.vue b/packages/client/src/components/page-window.vue index ec7451d5aa..7455236bad 100644 --- a/packages/client/src/components/page-window.vue +++ b/packages/client/src/components/page-window.vue @@ -160,7 +160,7 @@ export default defineComponent({ action: () => { copyToClipboard(this.url); } - }], ev.currentTarget || ev.target); + }], ev.currentTarget ?? ev.target); }, back() { diff --git a/packages/client/src/components/post-form-attaches.vue b/packages/client/src/components/post-form-attaches.vue index 0c8181b481..9dd69a0ee5 100644 --- a/packages/client/src/components/post-form-attaches.vue +++ b/packages/client/src/components/post-form-attaches.vue @@ -127,7 +127,7 @@ export default defineComponent({ text: this.$ts.attachCancel, icon: 'fas fa-times-circle', action: () => { this.detachMedia(file.id) } - }], ev.currentTarget || ev.target).then(() => this.menu = null); + }], ev.currentTarget ?? ev.target).then(() => this.menu = null); } } }); diff --git a/packages/client/src/components/post-form.vue b/packages/client/src/components/post-form.vue index 2eda97e14d..535218ecf9 100644 --- a/packages/client/src/components/post-form.vue +++ b/packages/client/src/components/post-form.vue @@ -8,28 +8,28 @@ > <header> <button v-if="!fixed" class="cancel _button" @click="cancel"><i class="fas fa-times"></i></button> - <button v-click-anime v-tooltip="i18n.locale.switchAccount" class="account _button" @click="openAccountMenu"> + <button v-click-anime v-tooltip="i18n.ts.switchAccount" class="account _button" @click="openAccountMenu"> <MkAvatar :user="postAccount ?? $i" class="avatar"/> </button> <div> <span class="text-count" :class="{ over: textLength > maxTextLength }">{{ maxTextLength - textLength }}</span> <span v-if="localOnly" class="local-only"><i class="fas fa-biohazard"></i></span> - <button ref="visibilityButton" v-tooltip="i18n.locale.visibility" class="_button visibility" :disabled="channel != null" @click="setVisibility"> + <button ref="visibilityButton" v-tooltip="i18n.ts.visibility" class="_button visibility" :disabled="channel != null" @click="setVisibility"> <span v-if="visibility === 'public'"><i class="fas fa-globe"></i></span> <span v-if="visibility === 'home'"><i class="fas fa-home"></i></span> <span v-if="visibility === 'followers'"><i class="fas fa-unlock"></i></span> <span v-if="visibility === 'specified'"><i class="fas fa-envelope"></i></span> </button> - <button v-tooltip="i18n.locale.previewNoteText" class="_button preview" :class="{ active: showPreview }" @click="showPreview = !showPreview"><i class="fas fa-file-code"></i></button> + <button v-tooltip="i18n.ts.previewNoteText" class="_button preview" :class="{ active: showPreview }" @click="showPreview = !showPreview"><i class="fas fa-file-code"></i></button> <button class="submit _buttonGradate" :disabled="!canPost" data-cy-open-post-form-submit @click="post">{{ submitText }}<i :class="reply ? 'fas fa-reply' : renote ? 'fas fa-quote-right' : 'fas fa-paper-plane'"></i></button> </div> </header> <div class="form" :class="{ fixed }"> <XNoteSimple v-if="reply" class="preview" :note="reply"/> <XNoteSimple v-if="renote" class="preview" :note="renote"/> - <div v-if="quoteId" class="with-quote"><i class="fas fa-quote-left"></i> {{ i18n.locale.quoteAttached }}<button @click="quoteId = null"><i class="fas fa-times"></i></button></div> + <div v-if="quoteId" class="with-quote"><i class="fas fa-quote-left"></i> {{ i18n.ts.quoteAttached }}<button @click="quoteId = null"><i class="fas fa-times"></i></button></div> <div v-if="visibility === 'specified'" class="to-specified"> - <span style="margin-right: 8px;">{{ i18n.locale.recipient }}</span> + <span style="margin-right: 8px;">{{ i18n.ts.recipient }}</span> <div class="visibleUsers"> <span v-for="u in visibleUsers" :key="u.id"> <MkAcct :user="u"/> @@ -38,21 +38,21 @@ <button class="_buttonPrimary" @click="addVisibleUser"><i class="fas fa-plus fa-fw"></i></button> </div> </div> - <MkInfo v-if="hasNotSpecifiedMentions" warn class="hasNotSpecifiedMentions">{{ i18n.locale.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.locale.add }}</button></MkInfo> - <input v-show="useCw" ref="cwInputEl" v-model="cw" class="cw" :placeholder="i18n.locale.annotation" @keydown="onKeydown"> + <MkInfo v-if="hasNotSpecifiedMentions" warn class="hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo> + <input v-show="useCw" ref="cwInputEl" v-model="cw" class="cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown"> <textarea ref="textareaEl" v-model="text" class="text" :class="{ withCw: useCw }" :disabled="posting" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/> - <input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" class="hashtags" :placeholder="i18n.locale.hashtags" list="hashtags"> + <input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" class="hashtags" :placeholder="i18n.ts.hashtags" list="hashtags"> <XPostFormAttaches class="attaches" :files="files" @updated="updateFiles" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/> <XPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/> <XNotePreview v-if="showPreview" class="preview" :text="text"/> <footer> - <button v-tooltip="i18n.locale.attachFile" class="_button" @click="chooseFileFrom"><i class="fas fa-photo-video"></i></button> - <button v-tooltip="i18n.locale.poll" class="_button" :class="{ active: poll }" @click="togglePoll"><i class="fas fa-poll-h"></i></button> - <button v-tooltip="i18n.locale.useCw" class="_button" :class="{ active: useCw }" @click="useCw = !useCw"><i class="fas fa-eye-slash"></i></button> - <button v-tooltip="i18n.locale.mention" class="_button" @click="insertMention"><i class="fas fa-at"></i></button> - <button v-tooltip="i18n.locale.hashtags" class="_button" :class="{ active: withHashtags }" @click="withHashtags = !withHashtags"><i class="fas fa-hashtag"></i></button> - <button v-tooltip="i18n.locale.emoji" class="_button" @click="insertEmoji"><i class="fas fa-laugh-squint"></i></button> - <button v-if="postFormActions.length > 0" v-tooltip="i18n.locale.plugin" class="_button" @click="showActions"><i class="fas fa-plug"></i></button> + <button v-tooltip="i18n.ts.attachFile" class="_button" @click="chooseFileFrom"><i class="fas fa-photo-video"></i></button> + <button v-tooltip="i18n.ts.poll" class="_button" :class="{ active: poll }" @click="togglePoll"><i class="fas fa-poll-h"></i></button> + <button v-tooltip="i18n.ts.useCw" class="_button" :class="{ active: useCw }" @click="useCw = !useCw"><i class="fas fa-eye-slash"></i></button> + <button v-tooltip="i18n.ts.mention" class="_button" @click="insertMention"><i class="fas fa-at"></i></button> + <button v-tooltip="i18n.ts.hashtags" class="_button" :class="{ active: withHashtags }" @click="withHashtags = !withHashtags"><i class="fas fa-hashtag"></i></button> + <button v-tooltip="i18n.ts.emoji" class="_button" @click="insertEmoji"><i class="fas fa-laugh-squint"></i></button> + <button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugin" class="_button" @click="showActions"><i class="fas fa-plug"></i></button> </footer> <datalist id="hashtags"> <option v-for="hashtag in recentHashtags" :key="hashtag" :value="hashtag"/> @@ -135,7 +135,10 @@ let showPreview = $ref(false); let cw = $ref<string | null>(null); let localOnly = $ref<boolean>(props.initialLocalOnly ?? defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly); let visibility = $ref(props.initialVisibility ?? (defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility) as typeof misskey.noteVisibilities[number]); -let visibleUsers = $ref(props.initialVisibleUsers ?? []); +let visibleUsers = $ref([]); +if (props.initialVisibleUsers) { + props.initialVisibleUsers.forEach(pushVisibleUser); +} let autocomplete = $ref(null); let draghover = $ref(false); let quoteId = $ref(null); @@ -165,19 +168,19 @@ const draftKey = $computed((): string => { const placeholder = $computed((): string => { if (props.renote) { - return i18n.locale._postForm.quotePlaceholder; + return i18n.ts._postForm.quotePlaceholder; } else if (props.reply) { - return i18n.locale._postForm.replyPlaceholder; + return i18n.ts._postForm.replyPlaceholder; } else if (props.channel) { - return i18n.locale._postForm.channelPlaceholder; + return i18n.ts._postForm.channelPlaceholder; } else { const xs = [ - i18n.locale._postForm._placeholders.a, - i18n.locale._postForm._placeholders.b, - i18n.locale._postForm._placeholders.c, - i18n.locale._postForm._placeholders.d, - i18n.locale._postForm._placeholders.e, - i18n.locale._postForm._placeholders.f + i18n.ts._postForm._placeholders.a, + i18n.ts._postForm._placeholders.b, + i18n.ts._postForm._placeholders.c, + i18n.ts._postForm._placeholders.d, + i18n.ts._postForm._placeholders.e, + i18n.ts._postForm._placeholders.f ]; return xs[Math.floor(Math.random() * xs.length)]; } @@ -185,10 +188,10 @@ const placeholder = $computed((): string => { const submitText = $computed((): string => { return props.renote - ? i18n.locale.quote + ? i18n.ts.quote : props.reply - ? i18n.locale.reply - : i18n.locale.note; + ? i18n.ts.reply + : i18n.ts.note; }); const textLength = $computed((): number => { @@ -262,12 +265,12 @@ if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visib os.api('users/show', { userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply.userId) }).then(users => { - visibleUsers.push(...users); + users.forEach(pushVisibleUser); }); if (props.reply.userId !== $i.id) { os.api('users/show', { userId: props.reply.userId }).then(user => { - visibleUsers.push(user); + pushVisibleUser(user); }); } } @@ -275,7 +278,7 @@ if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visib if (props.specified) { visibility = 'specified'; - visibleUsers.push(props.specified); + pushVisibleUser(props.specified); } // keep cw when reply @@ -342,7 +345,7 @@ function focus() { } function chooseFileFrom(ev) { - selectFiles(ev.currentTarget || ev.target, i18n.locale.attachFile).then(files_ => { + selectFiles(ev.currentTarget ?? ev.target, i18n.ts.attachFile).then(files_ => { for (const file of files_) { files.push(file); } @@ -397,9 +400,15 @@ function setVisibility() { }, 'closed'); } +function pushVisibleUser(user) { + if (!visibleUsers.some(u => u.username === user.username && u.host === user.host)) { + visibleUsers.push(user); + } +} + function addVisibleUser() { os.selectUser().then(user => { - visibleUsers.push(user); + pushVisibleUser(user); }); } @@ -447,7 +456,7 @@ async function onPaste(e: ClipboardEvent) { os.confirm({ type: 'info', - text: i18n.locale.quoteQuestion, + text: i18n.ts.quoteQuestion, }).then(({ canceled }) => { if (canceled) { insertTextAtCursor(textareaEl, paste); @@ -540,8 +549,8 @@ async function post() { }; if (withHashtags && hashtags && hashtags.trim() !== '') { - const hashtags = hashtags.trim().split(' ').map(x => x.startsWith('#') ? x : '#' + x).join(' '); - data.text = data.text ? `${data.text} ${hashtags}` : hashtags; + const hashtags_ = hashtags.trim().split(' ').map(x => x.startsWith('#') ? x : '#' + x).join(' '); + data.text = data.text ? `${data.text} ${hashtags_}` : hashtags_; } // plugin @@ -565,9 +574,9 @@ async function post() { deleteDraft(); emit('posted'); if (data.text && data.text != '') { - const hashtags = mfm.parse(data.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag); + const hashtags_ = mfm.parse(data.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag); const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[]; - localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history)))); + localStorage.setItem('hashtags', JSON.stringify(unique(hashtags_.concat(history)))); } posting = false; postAccount = null; @@ -592,7 +601,7 @@ function insertMention() { } async function insertEmoji(ev: MouseEvent) { - os.openEmojiPicker(ev.currentTarget || ev.target, {}, textareaEl); + os.openEmojiPicker(ev.currentTarget ?? ev.target, {}, textareaEl); } function showActions(ev) { @@ -605,7 +614,7 @@ function showActions(ev) { if (key === 'text') { text = value; } }); } - })), ev.currentTarget || ev.target); + })), ev.currentTarget ?? ev.target); } let postAccount = $ref<misskey.entities.UserDetailed | null>(null); diff --git a/packages/client/src/components/reaction-tooltip.vue b/packages/client/src/components/reaction-tooltip.vue index 1b2a024e21..b53061df48 100644 --- a/packages/client/src/components/reaction-tooltip.vue +++ b/packages/client/src/components/reaction-tooltip.vue @@ -1,5 +1,5 @@ <template> -<MkTooltip ref="tooltip" :source="source" :max-width="340" @closed="emit('closed')"> +<MkTooltip ref="tooltip" :target-element="targetElement" :max-width="340" @closed="emit('closed')"> <div class="beeadbfb"> <XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/> <div class="name">{{ reaction.replace('@.', '') }}</div> @@ -15,11 +15,11 @@ import XReactionIcon from './reaction-icon.vue'; const props = defineProps<{ reaction: string; emojis: any[]; // TODO - source: any; // TODO + targetElement: HTMLElement; }>(); const emit = defineEmits<{ - (e: 'closed'): void; + (ev: 'closed'): void; }>(); </script> diff --git a/packages/client/src/components/reactions-viewer.details.vue b/packages/client/src/components/reactions-viewer.details.vue index 8cec8dfa2f..eb889c4888 100644 --- a/packages/client/src/components/reactions-viewer.details.vue +++ b/packages/client/src/components/reactions-viewer.details.vue @@ -1,5 +1,5 @@ <template> -<MkTooltip ref="tooltip" :source="source" :max-width="340" @closed="emit('closed')"> +<MkTooltip ref="tooltip" :target-element="targetElement" :max-width="340" @closed="emit('closed')"> <div class="bqxuuuey"> <div class="reaction"> <XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/> @@ -26,11 +26,11 @@ const props = defineProps<{ users: any[]; // TODO count: number; emojis: any[]; // TODO - source: any; // TODO + targetElement: HTMLElement; }>(); const emit = defineEmits<{ - (e: 'closed'): void; + (ev: 'closed'): void; }>(); </script> diff --git a/packages/client/src/components/renote-button.vue b/packages/client/src/components/renote-button.vue index 446686de10..c1c0d285e1 100644 --- a/packages/client/src/components/renote-button.vue +++ b/packages/client/src/components/renote-button.vue @@ -59,7 +59,7 @@ export default defineComponent({ const renote = (viaKeyboard = false) => { pleaseLogin(); os.popupMenu([{ - text: i18n.locale.renote, + text: i18n.ts.renote, icon: 'fas fa-retweet', action: () => { os.api('notes/create', { @@ -67,7 +67,7 @@ export default defineComponent({ }); } }, { - text: i18n.locale.quote, + text: i18n.ts.quote, icon: 'fas fa-quote-right', action: () => { os.post({ diff --git a/packages/client/src/components/renote.details.vue b/packages/client/src/components/renote.details.vue index cdbc71bdce..2df19bcd3f 100644 --- a/packages/client/src/components/renote.details.vue +++ b/packages/client/src/components/renote.details.vue @@ -1,5 +1,5 @@ <template> -<MkTooltip ref="tooltip" :source="source" :max-width="250" @closed="emit('closed')"> +<MkTooltip ref="tooltip" :target-element="targetElement" :max-width="250" @closed="emit('closed')"> <div class="beaffaef"> <div v-for="u in users" :key="u.id" class="user"> <MkAvatar class="avatar" :user="u"/> @@ -17,11 +17,11 @@ import MkTooltip from './ui/tooltip.vue'; const props = defineProps<{ users: any[]; // TODO count: number; - source: any; // TODO + targetElement: HTMLElement; }>(); const emit = defineEmits<{ - (e: 'closed'): void; + (ev: 'closed'): void; }>(); </script> diff --git a/packages/client/src/components/sample.vue b/packages/client/src/components/sample.vue index 03ad6a9838..65249ff7e9 100644 --- a/packages/client/src/components/sample.vue +++ b/packages/client/src/components/sample.vue @@ -109,7 +109,7 @@ export default defineComponent({ text: 'Delete some bananas', danger: true, action: () => {}, - }], ev.currentTarget || ev.target); + }], ev.currentTarget ?? ev.target); }, } }); diff --git a/packages/client/src/components/ui/context-menu.vue b/packages/client/src/components/ui/context-menu.vue index 85606bf6d5..f491b43b46 100644 --- a/packages/client/src/components/ui/context-menu.vue +++ b/packages/client/src/components/ui/context-menu.vue @@ -1,88 +1,71 @@ <template> <transition :name="$store.state.animation ? 'fade' : ''" appear> - <div class="nvlagfpb" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}"> + <div ref="rootEl" class="nvlagfpb" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}"> <MkMenu :items="items" class="_popup _shadow" :align="'left'" @close="$emit('closed')"/> </div> </transition> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { onMounted, onBeforeUnmount } from 'vue'; import contains from '@/scripts/contains'; import MkMenu from './menu.vue'; +import { MenuItem } from './types/menu.vue'; import * as os from '@/os'; -export default defineComponent({ - components: { - MkMenu, - }, - props: { - items: { - type: Array, - required: true - }, - ev: { - required: true - }, - viaKeyboard: { - type: Boolean, - required: false - }, - }, - emits: ['closed'], - data() { - return { - zIndex: os.claimZIndex('high'), - }; - }, - computed: { - keymap(): any { - return { - 'esc': () => this.$emit('closed'), - }; - }, - }, - mounted() { - let left = this.ev.pageX + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1 - let top = this.ev.pageY + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1 +const props = defineProps<{ + items: MenuItem[]; + ev: MouseEvent; +}>(); - const width = this.$el.offsetWidth; - const height = this.$el.offsetHeight; +const emit = defineEmits<{ + (e: 'closed'): void; +}>(); - if (left + width - window.pageXOffset > window.innerWidth) { - left = window.innerWidth - width + window.pageXOffset; - } +let rootEl = $ref<HTMLDivElement>(); - if (top + height - window.pageYOffset > window.innerHeight) { - top = window.innerHeight - height + window.pageYOffset; - } +let zIndex = $ref<number>(os.claimZIndex('high')); - if (top < 0) { - top = 0; - } +onMounted(() => { + let left = props.ev.pageX + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1 + let top = props.ev.pageY + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1 - if (left < 0) { - left = 0; - } + const width = rootEl.offsetWidth; + const height = rootEl.offsetHeight; - this.$el.style.top = top + 'px'; - this.$el.style.left = left + 'px'; + if (left + width - window.pageXOffset > window.innerWidth) { + left = window.innerWidth - width + window.pageXOffset; + } + + if (top + height - window.pageYOffset > window.innerHeight) { + top = window.innerHeight - height + window.pageYOffset; + } - for (const el of Array.from(document.querySelectorAll('body *'))) { - el.addEventListener('mousedown', this.onMousedown); - } - }, - beforeUnmount() { - for (const el of Array.from(document.querySelectorAll('body *'))) { - el.removeEventListener('mousedown', this.onMousedown); - } - }, - methods: { - onMousedown(e) { - if (!contains(this.$el, e.target) && (this.$el != e.target)) this.$emit('closed'); - }, + if (top < 0) { + top = 0; + } + + if (left < 0) { + left = 0; + } + + rootEl.style.top = `${top}px`; + rootEl.style.left = `${left}px`; + + for (const el of Array.from(document.querySelectorAll('body *'))) { + el.addEventListener('mousedown', onMousedown); + } +}); + +onBeforeUnmount(() => { + for (const el of Array.from(document.querySelectorAll('body *'))) { + el.removeEventListener('mousedown', onMousedown); } }); + +function onMousedown(e: Event) { + if (!contains(rootEl, e.target) && (rootEl != e.target)) emit('closed'); +} </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/ui/menu.vue b/packages/client/src/components/ui/menu.vue index 41165c8d33..a93cc8cda8 100644 --- a/packages/client/src/components/ui/menu.vue +++ b/packages/client/src/components/ui/menu.vue @@ -1,8 +1,8 @@ <template> -<div ref="items" v-hotkey="keymap" +<div ref="itemsEl" v-hotkey="keymap" class="rrevdjwt" :class="{ center: align === 'center', asDrawer }" - :style="{ width: (width && !asDrawer) ? width + 'px' : null, maxHeight: maxHeight ? maxHeight + 'px' : null }" + :style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }" @contextmenu.self="e => e.preventDefault()" > <template v-for="(item, i) in items2"> @@ -28,6 +28,9 @@ <MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/> <span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span> </button> + <span v-else-if="item.type === 'switch'" :tabindex="i" class="item"> + <FormSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</FormSwitch> + </span> <button v-else :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)"> <i v-if="item.icon" class="fa-fw" :class="item.icon"></i> <MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/> @@ -41,114 +44,78 @@ </div> </template> -<script lang="ts"> -import { defineComponent, ref, unref } from 'vue'; +<script lang="ts" setup> +import { nextTick, onMounted, watch } from 'vue'; import { focusPrev, focusNext } from '@/scripts/focus'; -import contains from '@/scripts/contains'; +import FormSwitch from '@/components/form/switch.vue'; +import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from '@/types/menu'; -export default defineComponent({ - props: { - items: { - type: Array, - required: true - }, - viaKeyboard: { - type: Boolean, - required: false - }, - asDrawer: { - type: Boolean, - required: false - }, - align: { - type: String, - requried: false - }, - width: { - type: Number, - required: false - }, - maxHeight: { - type: Number, - required: false - }, - }, - emits: ['close'], - data() { - return { - items2: [], - }; - }, - computed: { - keymap(): any { - return { - 'up|k|shift+tab': this.focusUp, - 'down|j|tab': this.focusDown, - 'esc': this.close, - }; - }, - }, - watch: { - items: { - handler() { - const items = ref(unref(this.items).filter(item => item !== undefined)); +const props = defineProps<{ + items: MenuItem[]; + viaKeyboard?: boolean; + asDrawer?: boolean; + align?: 'center' | string; + width?: number; + maxHeight?: number; +}>(); - for (let i = 0; i < items.value.length; i++) { - const item = items.value[i]; - - if (item && item.then) { // if item is Promise - items.value[i] = { type: 'pending' }; - item.then(actualItem => { - items.value[i] = actualItem; - }); - } - } +const emit = defineEmits<{ + (e: 'close'): void; +}>(); - this.items2 = items; - }, - immediate: true - } - }, - mounted() { - if (this.viaKeyboard) { - this.$nextTick(() => { - focusNext(this.$refs.items.children[0], true, false); +let itemsEl = $ref<HTMLDivElement>(); + +let items2: InnerMenuItem[] = $ref([]); + +let keymap = $computed(() => ({ + 'up|k|shift+tab': focusUp, + 'down|j|tab': focusDown, + 'esc': close, +})); + +watch(() => props.items, () => { + const items: (MenuItem | MenuPending)[] = [...props.items].filter(item => item !== undefined); + + for (let i = 0; i < items.length; i++) { + const item = items[i]; + + if (item && 'then' in item) { // if item is Promise + items[i] = { type: 'pending' }; + item.then(actualItem => { + items2[i] = actualItem; }); } + } - if (this.contextmenuEvent) { - this.$el.style.top = this.contextmenuEvent.pageY + 'px'; - this.$el.style.left = this.contextmenuEvent.pageX + 'px'; + items2 = items as InnerMenuItem[]; +}, { + immediate: true, +}); - for (const el of Array.from(document.querySelectorAll('body *'))) { - el.addEventListener('mousedown', this.onMousedown); - } - } - }, - beforeUnmount() { - for (const el of Array.from(document.querySelectorAll('body *'))) { - el.removeEventListener('mousedown', this.onMousedown); - } - }, - methods: { - clicked(fn, ev) { - fn(ev); - this.close(); - }, - close() { - this.$emit('close'); - }, - focusUp() { - focusPrev(document.activeElement); - }, - focusDown() { - focusNext(document.activeElement); - }, - onMousedown(e) { - if (!contains(this.$el, e.target) && (this.$el != e.target)) this.close(); - }, +onMounted(() => { + if (props.viaKeyboard) { + nextTick(() => { + focusNext(itemsEl.children[0], true, false); + }); } }); + +function clicked(fn: MenuAction, ev: MouseEvent) { + fn(ev); + close(); +} + +function close() { + emit('close'); +} + +function focusUp() { + focusPrev(document.activeElement); +} + +function focusDown() { + focusNext(document.activeElement); +} </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/ui/modal.vue b/packages/client/src/components/ui/modal.vue index c691c8c6d0..3c3bb5c226 100644 --- a/packages/client/src/components/ui/modal.vue +++ b/packages/client/src/components/ui/modal.vue @@ -1,5 +1,5 @@ <template> -<transition :name="$store.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? 200 : 0" appear @after-leave="$emit('closed')" @enter="$emit('opening')" @after-enter="childRendered"> +<transition :name="$store.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? 200 : 0" appear @after-leave="emit('closed')" @enter="emit('opening')" @after-enter="childRendered"> <div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }"> <div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div> <div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick"> @@ -9,8 +9,8 @@ </transition> </template> -<script lang="ts"> -import { defineComponent, nextTick, onMounted, computed, PropType, ref, watch } from 'vue'; +<script lang="ts" setup> +import { nextTick, onMounted, computed, ref, watch, provide } from 'vue'; import * as os from '@/os'; import { isTouchUsing } from '@/scripts/touch'; import { defaultStore } from '@/store'; @@ -25,234 +25,206 @@ function getFixedContainer(el: Element | null): Element | null { } } -export default defineComponent({ - provide: { - modal: true - }, +type ModalTypes = 'popup' | 'dialog' | 'dialog:top' | 'drawer'; - props: { - manualShowing: { - type: Boolean, - required: false, - default: null, - }, - srcCenter: { - type: Boolean, - required: false - }, - src: { - type: Object as PropType<HTMLElement>, - required: false, - default: null, - }, - preferType: { - required: false, - type: String, - default: 'auto', - }, - zPriority: { - type: String as PropType<'low' | 'middle' | 'high'>, - required: false, - default: 'low', - }, - noOverlap: { - type: Boolean, - required: false, - default: true, - }, - transparentBg: { - type: Boolean, - required: false, - default: false, - }, - }, +const props = withDefaults(defineProps<{ + manualShowing?: boolean | null; + srcCenter?: boolean; + src?: HTMLElement; + preferType?: ModalTypes | 'auto'; + zPriority?: 'low' | 'middle' | 'high'; + noOverlap?: boolean; + transparentBg?: boolean; +}>(), { + manualShowing: null, + src: null, + preferType: 'auto', + zPriority: 'low', + noOverlap: true, + transparentBg: false, +}); - emits: ['opening', 'click', 'esc', 'close', 'closed'], +const emit = defineEmits<{ + (ev: 'opening'): void; + (ev: 'click'): void; + (ev: 'esc'): void; + (ev: 'close'): void; + (ev: 'closed'): void; +}>(); - setup(props, context) { - const maxHeight = ref<number>(); - const fixed = ref(false); - const transformOrigin = ref('center'); - const showing = ref(true); - const content = ref<HTMLElement>(); - const zIndex = os.claimZIndex(props.zPriority); - const type = computed(() => { - if (props.preferType === 'auto') { - if (!defaultStore.state.disableDrawer && isTouchUsing && window.innerWidth < 500 && window.innerHeight < 1000) { - return 'drawer'; - } else { - return props.src != null ? 'popup' : 'dialog'; - } - } else { - return props.preferType; - } - }); - - let contentClicking = false; +provide('modal', true); - const close = () => { - // eslint-disable-next-line vue/no-mutating-props - if (props.src) props.src.style.pointerEvents = 'auto'; - showing.value = false; - context.emit('close'); - }; +const maxHeight = ref<number>(); +const fixed = ref(false); +const transformOrigin = ref('center'); +const showing = ref(true); +const content = ref<HTMLElement>(); +const zIndex = os.claimZIndex(props.zPriority); +const type = computed(() => { + if (props.preferType === 'auto') { + if (!defaultStore.state.disableDrawer && isTouchUsing && window.innerWidth < 500 && window.innerHeight < 1000) { + return 'drawer'; + } else { + return props.src != null ? 'popup' : 'dialog'; + } + } else { + return props.preferType!; + } +}); - const onBgClick = () => { - if (contentClicking) return; - context.emit('click'); - }; +let contentClicking = false; - if (type.value === 'drawer') { - maxHeight.value = window.innerHeight / 2; - } +const close = () => { + // eslint-disable-next-line vue/no-mutating-props + if (props.src) props.src.style.pointerEvents = 'auto'; + showing.value = false; + emit('close'); +}; - const keymap = { - 'esc': () => context.emit('esc'), - }; +const onBgClick = () => { + if (contentClicking) return; + emit('click'); +}; - const MARGIN = 16; +if (type.value === 'drawer') { + maxHeight.value = window.innerHeight / 2; +} - const align = () => { - if (props.src == null) return; - if (type.value === 'drawer') return; +const keymap = { + 'esc': () => emit('esc'), +}; - const popover = content.value!; +const MARGIN = 16; - if (popover == null) return; +const align = () => { + if (props.src == null) return; + if (type.value === 'drawer') return; - const rect = props.src.getBoundingClientRect(); - - const width = popover.offsetWidth; - const height = popover.offsetHeight; + const popover = content.value!; - let left; - let top; + if (popover == null) return; - if (props.srcCenter) { - const x = rect.left + (fixed.value ? 0 : window.pageXOffset) + (props.src.offsetWidth / 2); - const y = rect.top + (fixed.value ? 0 : window.pageYOffset) + (props.src.offsetHeight / 2); - left = (x - (width / 2)); - top = (y - (height / 2)); - } else { - const x = rect.left + (fixed.value ? 0 : window.pageXOffset) + (props.src.offsetWidth / 2); - const y = rect.top + (fixed.value ? 0 : window.pageYOffset) + props.src.offsetHeight; - left = (x - (width / 2)); - top = y; - } + const rect = props.src.getBoundingClientRect(); + + const width = popover.offsetWidth; + const height = popover.offsetHeight; - if (fixed.value) { - // 画面から横にはみ出る場合 - if (left + width > window.innerWidth) { - left = window.innerWidth - width; - } + let left; + let top; - // 画面から縦にはみ出る場合 - if (top + height > (window.innerHeight - MARGIN)) { - if (props.noOverlap) { - const underSpace = (window.innerHeight - MARGIN) - top; - const upperSpace = (rect.top - MARGIN); - if (underSpace >= (upperSpace / 3)) { - maxHeight.value = underSpace; - } else { - maxHeight.value = upperSpace; - top = (upperSpace + MARGIN) - height; - } - } else { - top = (window.innerHeight - MARGIN) - height; - } + if (props.srcCenter) { + const x = rect.left + (fixed.value ? 0 : window.pageXOffset) + (props.src.offsetWidth / 2); + const y = rect.top + (fixed.value ? 0 : window.pageYOffset) + (props.src.offsetHeight / 2); + left = (x - (width / 2)); + top = (y - (height / 2)); + } else { + const x = rect.left + (fixed.value ? 0 : window.pageXOffset) + (props.src.offsetWidth / 2); + const y = rect.top + (fixed.value ? 0 : window.pageYOffset) + props.src.offsetHeight; + left = (x - (width / 2)); + top = y; + } + + if (fixed.value) { + // 画面から横にはみ出る場合 + if (left + width > window.innerWidth) { + left = window.innerWidth - width; + } + + // 画面から縦にはみ出る場合 + if (top + height > (window.innerHeight - MARGIN)) { + if (props.noOverlap) { + const underSpace = (window.innerHeight - MARGIN) - top; + const upperSpace = (rect.top - MARGIN); + if (underSpace >= (upperSpace / 3)) { + maxHeight.value = underSpace; + } else { + maxHeight.value = upperSpace; + top = (upperSpace + MARGIN) - height; } } else { - // 画面から横にはみ出る場合 - if (left + width - window.pageXOffset > window.innerWidth) { - left = window.innerWidth - width + window.pageXOffset - 1; - } + top = (window.innerHeight - MARGIN) - height; + } + } + } else { + // 画面から横にはみ出る場合 + if (left + width - window.pageXOffset > window.innerWidth) { + left = window.innerWidth - width + window.pageXOffset - 1; + } - // 画面から縦にはみ出る場合 - if (top + height - window.pageYOffset > (window.innerHeight - MARGIN)) { - if (props.noOverlap) { - const underSpace = (window.innerHeight - MARGIN) - (top - window.pageYOffset); - const upperSpace = (rect.top - MARGIN); - if (underSpace >= (upperSpace / 3)) { - maxHeight.value = underSpace; - } else { - maxHeight.value = upperSpace; - top = window.pageYOffset + ((upperSpace + MARGIN) - height); - } - } else { - top = (window.innerHeight - MARGIN) - height + window.pageYOffset - 1; - } + // 画面から縦にはみ出る場合 + if (top + height - window.pageYOffset > (window.innerHeight - MARGIN)) { + if (props.noOverlap) { + const underSpace = (window.innerHeight - MARGIN) - (top - window.pageYOffset); + const upperSpace = (rect.top - MARGIN); + if (underSpace >= (upperSpace / 3)) { + maxHeight.value = underSpace; + } else { + maxHeight.value = upperSpace; + top = window.pageYOffset + ((upperSpace + MARGIN) - height); } + } else { + top = (window.innerHeight - MARGIN) - height + window.pageYOffset - 1; } + } + } - if (top < 0) { - top = MARGIN; - } + if (top < 0) { + top = MARGIN; + } - if (left < 0) { - left = 0; - } + if (left < 0) { + left = 0; + } - if (top > rect.top + (fixed.value ? 0 : window.pageYOffset)) { - transformOrigin.value = 'center top'; - } else if ((top + height) <= rect.top + (fixed.value ? 0 : window.pageYOffset)) { - transformOrigin.value = 'center bottom'; - } else { - transformOrigin.value = 'center'; - } + if (top > rect.top + (fixed.value ? 0 : window.pageYOffset)) { + transformOrigin.value = 'center top'; + } else if ((top + height) <= rect.top + (fixed.value ? 0 : window.pageYOffset)) { + transformOrigin.value = 'center bottom'; + } else { + transformOrigin.value = 'center'; + } - popover.style.left = left + 'px'; - popover.style.top = top + 'px'; - }; + popover.style.left = left + 'px'; + popover.style.top = top + 'px'; +}; - const childRendered = () => { - // モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する - const el = content.value!.children[0]; - el.addEventListener('mousedown', e => { - contentClicking = true; - window.addEventListener('mouseup', e => { - // click イベントより先に mouseup イベントが発生するかもしれないのでちょっと待つ - window.setTimeout(() => { - contentClicking = false; - }, 100); - }, { passive: true, once: true }); - }, { passive: true }); - }; +const childRendered = () => { + // モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する + const el = content.value!.children[0]; + el.addEventListener('mousedown', ev => { + contentClicking = true; + window.addEventListener('mouseup', ev => { + // click イベントより先に mouseup イベントが発生するかもしれないのでちょっと待つ + window.setTimeout(() => { + contentClicking = false; + }, 100); + }, { passive: true, once: true }); + }, { passive: true }); +}; - onMounted(() => { - watch(() => props.src, async () => { - if (props.src) { - // eslint-disable-next-line vue/no-mutating-props - props.src.style.pointerEvents = 'none'; - } - fixed.value = (type.value === 'drawer') || (getFixedContainer(props.src) != null); +onMounted(() => { + watch(() => props.src, async () => { + if (props.src) { + // eslint-disable-next-line vue/no-mutating-props + props.src.style.pointerEvents = 'none'; + } + fixed.value = (type.value === 'drawer') || (getFixedContainer(props.src) != null); - await nextTick() - - align(); - }, { immediate: true, }); + await nextTick() + + align(); + }, { immediate: true, }); - nextTick(() => { - const popover = content.value; - new ResizeObserver((entries, observer) => { - align(); - }).observe(popover!); - }); - }); + nextTick(() => { + const popover = content.value; + new ResizeObserver((entries, observer) => { + align(); + }).observe(popover!); + }); +}); - return { - showing, - type, - fixed, - content, - transformOrigin, - maxHeight, - close, - zIndex, - keymap, - onBgClick, - childRendered, - }; - }, +defineExpose({ + close, }); </script> diff --git a/packages/client/src/components/ui/popup-menu.vue b/packages/client/src/components/ui/popup-menu.vue index 8ffc4ad195..8d6c1b5695 100644 --- a/packages/client/src/components/ui/popup-menu.vue +++ b/packages/client/src/components/ui/popup-menu.vue @@ -1,44 +1,28 @@ <template> -<MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="$refs.modal.close()" @closed="$emit('closed')"> - <MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :as-drawer="type === 'drawer'" class="sfhdhdhq _popup _shadow" :class="{ drawer: type === 'drawer' }" @close="$refs.modal.close()"/> +<MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="modal.close()" @closed="emit('closed')"> + <MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :as-drawer="type === 'drawer'" class="sfhdhdhq _popup _shadow" :class="{ drawer: type === 'drawer' }" @close="modal.close()"/> </MkModal> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import MkModal from './modal.vue'; import MkMenu from './menu.vue'; +import { MenuItem } from '@/types/menu'; -export default defineComponent({ - components: { - MkModal, - MkMenu, - }, +defineProps<{ + items: MenuItem[]; + align?: 'center' | string; + width?: number; + viaKeyboard?: boolean; + src?: any; +}>(); - props: { - items: { - type: Array, - required: true - }, - align: { - type: String, - required: false - }, - width: { - type: Number, - required: false - }, - viaKeyboard: { - type: Boolean, - required: false - }, - src: { - required: false - }, - }, +const emit = defineEmits<{ + (e: 'closed'): void; +}>(); - emits: ['close', 'closed'], -}); +let modal = $ref<InstanceType<typeof MkModal>>(); </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/ui/tooltip.vue b/packages/client/src/components/ui/tooltip.vue index 394b068352..1892877cc1 100644 --- a/packages/client/src/components/ui/tooltip.vue +++ b/packages/client/src/components/ui/tooltip.vue @@ -1,99 +1,96 @@ <template> -<transition :name="$store.state.animation ? 'tooltip' : ''" appear @after-leave="$emit('closed')"> +<transition :name="$store.state.animation ? 'tooltip' : ''" appear @after-leave="emit('closed')"> <div v-show="showing" ref="el" class="buebdbiu _acrylic _shadow" :style="{ zIndex, maxWidth: maxWidth + 'px' }"> <slot>{{ text }}</slot> </div> </transition> </template> -<script lang="ts"> -import { defineComponent, nextTick, onMounted, onUnmounted, ref } from 'vue'; +<script lang="ts" setup> +import { nextTick, onMounted, onUnmounted, ref } from 'vue'; import * as os from '@/os'; -export default defineComponent({ - props: { - showing: { - type: Boolean, - required: true, - }, - source: { - required: true, - }, - text: { - type: String, - required: false - }, - maxWidth: { - type: Number, - required: false, - default: 250, - }, - }, +const props = withDefaults(defineProps<{ + showing: boolean; + targetElement?: HTMLElement; + x?: number; + y?: number; + text?: string; + maxWidth?: number; +}>(), { + maxWidth: 250, +}); - emits: ['closed'], +const emit = defineEmits<{ + (ev: 'closed'): void; +}>(); - setup(props, context) { - const el = ref<HTMLElement>(); - const zIndex = os.claimZIndex('high'); +const el = ref<HTMLElement>(); +const zIndex = os.claimZIndex('high'); - const setPosition = () => { - if (el.value == null) return; +const setPosition = () => { + if (el.value == null) return; - const rect = props.source.getBoundingClientRect(); + const contentWidth = el.value.offsetWidth; + const contentHeight = el.value.offsetHeight; - const contentWidth = el.value.offsetWidth; - const contentHeight = el.value.offsetHeight; + let left: number; + let top: number; - let left = rect.left + window.pageXOffset + (props.source.offsetWidth / 2); - let top = rect.top + window.pageYOffset - contentHeight; + let rect: DOMRect; - left -= (el.value.offsetWidth / 2); + if (props.targetElement) { + rect = props.targetElement.getBoundingClientRect(); - if (left + contentWidth - window.pageXOffset > window.innerWidth) { - left = window.innerWidth - contentWidth + window.pageXOffset - 1; - } + left = rect.left + window.pageXOffset + (props.targetElement.offsetWidth / 2); + top = rect.top + window.pageYOffset - contentHeight; - if (top - window.pageYOffset < 0) { - top = rect.top + window.pageYOffset + props.source.offsetHeight; - el.value.style.transformOrigin = 'center top'; - } + el.value.style.transformOrigin = 'center bottom'; + } else { + left = props.x; + top = props.y - contentHeight; + } - el.value.style.left = left + 'px'; - el.value.style.top = top + 'px'; - }; + left -= (el.value.offsetWidth / 2); - onMounted(() => { - nextTick(() => { - if (props.source == null) { - context.emit('closed'); - return; - } + if (left + contentWidth - window.pageXOffset > window.innerWidth) { + left = window.innerWidth - contentWidth + window.pageXOffset - 1; + } - setPosition(); + // ツールチップを上に向かって表示するスペースがなければ下に向かって出す + if (top - window.pageYOffset < 0) { + if (props.targetElement) { + top = rect.top + window.pageYOffset + props.targetElement.offsetHeight; + el.value.style.transformOrigin = 'center top'; + } else { + top = props.y; + } + } - let loopHandler; + el.value.style.left = left + 'px'; + el.value.style.top = top + 'px'; +}; - const loop = () => { - loopHandler = window.requestAnimationFrame(() => { - setPosition(); - loop(); - }); - }; +onMounted(() => { + nextTick(() => { + setPosition(); - loop(); + let loopHandler; - onUnmounted(() => { - window.cancelAnimationFrame(loopHandler); - }); + const loop = () => { + loopHandler = window.requestAnimationFrame(() => { + setPosition(); + loop(); }); - }); - - return { - el, - zIndex, }; - }, -}) + + loop(); + + onUnmounted(() => { + window.cancelAnimationFrame(loopHandler); + }); + }); +}); </script> <style lang="scss" scoped> @@ -118,6 +115,6 @@ export default defineComponent({ border-radius: 4px; border: solid 0.5px var(--divider); pointer-events: none; - transform-origin: center bottom; + transform-origin: center center; } </style> diff --git a/packages/client/src/components/user-online-indicator.vue b/packages/client/src/components/user-online-indicator.vue index a87b0aeff5..a4f6f80383 100644 --- a/packages/client/src/components/user-online-indicator.vue +++ b/packages/client/src/components/user-online-indicator.vue @@ -13,10 +13,10 @@ const props = defineProps<{ const text = $computed(() => { switch (props.user.onlineStatus) { - case 'online': return i18n.locale.online; - case 'active': return i18n.locale.active; - case 'offline': return i18n.locale.offline; - case 'unknown': return i18n.locale.unknown; + case 'online': return i18n.ts.online; + case 'active': return i18n.ts.active; + case 'offline': return i18n.ts.offline; + case 'unknown': return i18n.ts.unknown; } }); </script> diff --git a/packages/client/src/directives/tooltip.ts b/packages/client/src/directives/tooltip.ts index fffde14874..dd715227a4 100644 --- a/packages/client/src/directives/tooltip.ts +++ b/packages/client/src/directives/tooltip.ts @@ -48,7 +48,7 @@ export default { popup(import('@/components/ui/tooltip.vue'), { showing, text: self.text, - source: el + targetElement: el, }, {}, 'closed'); self._close = () => { @@ -56,8 +56,8 @@ export default { }; }; - el.addEventListener('selectstart', e => { - e.preventDefault(); + el.addEventListener('selectstart', ev => { + ev.preventDefault(); }); el.addEventListener(start, () => { diff --git a/packages/client/src/init.ts b/packages/client/src/init.ts index af70aec70a..81e41febd1 100644 --- a/packages/client/src/init.ts +++ b/packages/client/src/init.ts @@ -185,7 +185,7 @@ app.config.globalProperties = { $store: defaultStore, $instance: instance, $t: i18n.t, - $ts: i18n.locale, + $ts: i18n.ts, }; app.use(router); @@ -299,8 +299,8 @@ stream.on('_disconnected_', async () => { reloadDialogShowing = true; const { canceled } = await confirm({ type: 'warning', - title: i18n.locale.disconnectedFromServer, - text: i18n.locale.reloadConfirm, + title: i18n.ts.disconnectedFromServer, + text: i18n.ts.reloadConfirm, }); reloadDialogShowing = false; if (!canceled) { @@ -324,7 +324,7 @@ if ($i) { if ($i.isDeleted) { alert({ type: 'warning', - text: i18n.locale.accountDeletionInProgress, + text: i18n.ts.accountDeletionInProgress, }); } diff --git a/packages/client/src/menu.ts b/packages/client/src/menu.ts index 184779f21f..ebc7898101 100644 --- a/packages/client/src/menu.ts +++ b/packages/client/src/menu.ts @@ -73,12 +73,12 @@ export const menuDef = reactive({ })), null, { type: 'link', to: '/my/lists', - text: i18n.locale.manageLists, + text: i18n.ts.manageLists, icon: 'fas fa-cog', }]; items.value = _items; }); - os.popupMenu(items, ev.currentTarget || ev.target); + os.popupMenu(items, ev.currentTarget ?? ev.target); }, }, groups: { @@ -104,12 +104,12 @@ export const menuDef = reactive({ })), null, { type: 'link', to: '/my/antennas', - text: i18n.locale.manageAntennas, + text: i18n.ts.manageAntennas, icon: 'fas fa-cog', }]; items.value = _items; }); - os.popupMenu(items, ev.currentTarget || ev.target); + os.popupMenu(items, ev.currentTarget ?? ev.target); }, }, mentions: { @@ -173,34 +173,34 @@ export const menuDef = reactive({ icon: 'fas fa-columns', action: (ev) => { os.popupMenu([{ - text: i18n.locale.default, + text: i18n.ts.default, active: ui === 'default' || ui === null, action: () => { localStorage.setItem('ui', 'default'); unisonReload(); } }, { - text: i18n.locale.deck, + text: i18n.ts.deck, active: ui === 'deck', action: () => { localStorage.setItem('ui', 'deck'); unisonReload(); } }, { - text: i18n.locale.classic, + text: i18n.ts.classic, active: ui === 'classic', action: () => { localStorage.setItem('ui', 'classic'); unisonReload(); } }, /*{ - text: i18n.locale.desktop + ' (β)', + text: i18n.ts.desktop + ' (β)', active: ui === 'desktop', action: () => { localStorage.setItem('ui', 'desktop'); unisonReload(); } - }*/], ev.currentTarget || ev.target); + }*/], ev.currentTarget ?? ev.target); }, }, }); diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts index c16ea717ad..95b4e87a1f 100644 --- a/packages/client/src/os.ts +++ b/packages/client/src/os.ts @@ -7,8 +7,10 @@ import * as Misskey from 'misskey-js'; import { apiUrl, url } from '@/config'; import MkPostFormDialog from '@/components/post-form-dialog.vue'; import MkWaitingDialog from '@/components/waiting-dialog.vue'; +import { MenuItem } from '@/types/menu'; import { resolve } from '@/router'; import { $i } from '@/account'; +import { defaultStore } from '@/store'; export const pendingApiRequestsCount = ref(0); @@ -403,7 +405,7 @@ export async function selectDriveFolder(multiple: boolean) { }); } -export async function pickEmoji(src?: HTMLElement, opts) { +export async function pickEmoji(src: HTMLElement | null, opts) { return new Promise((resolve, reject) => { popup(import('@/components/emoji-picker-dialog.vue'), { src, @@ -470,7 +472,7 @@ export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea: }); } -export function popupMenu(items: any[] | Ref<any[]>, src?: HTMLElement, options?: { +export function popupMenu(items: MenuItem[] | Ref<MenuItem[]>, src?: HTMLElement, options?: { align?: string; width?: number; viaKeyboard?: boolean; @@ -494,7 +496,7 @@ export function popupMenu(items: any[] | Ref<any[]>, src?: HTMLElement, options? }); } -export function contextMenu(items: any[], ev: MouseEvent) { +export function contextMenu(items: MenuItem[] | Ref<MenuItem[]>, ev: MouseEvent) { ev.preventDefault(); return new Promise((resolve, reject) => { let dispose; @@ -541,7 +543,7 @@ export const uploads = ref<{ img: string; }[]>([]); -export function upload(file: File, folder?: any, name?: string): Promise<Misskey.entities.DriveFile> { +export function upload(file: File, folder?: any, name?: string, keepOriginal: boolean = defaultStore.state.keepOriginalUploading): Promise<Misskey.entities.DriveFile> { if (folder && typeof folder == 'object') folder = folder.id; return new Promise((resolve, reject) => { @@ -559,6 +561,8 @@ export function upload(file: File, folder?: any, name?: string): Promise<Misskey uploads.value.push(ctx); + console.log(keepOriginal); + const data = new FormData(); data.append('i', $i.token); data.append('force', 'true'); diff --git a/packages/client/src/pages/_error_.vue b/packages/client/src/pages/_error_.vue index 7540995707..4cfe2e255c 100644 --- a/packages/client/src/pages/_error_.vue +++ b/packages/client/src/pages/_error_.vue @@ -3,15 +3,15 @@ <transition :name="$store.state.animation ? 'zoom' : ''" appear> <div v-show="loaded" class="mjndxjch"> <img src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/> - <p><b><i class="fas fa-exclamation-triangle"></i> {{ i18n.locale.pageLoadError }}</b></p> - <p v-if="meta && (version === meta.version)">{{ i18n.locale.pageLoadErrorDescription }}</p> - <p v-else-if="serverIsDead">{{ i18n.locale.serverIsDead }}</p> + <p><b><i class="fas fa-exclamation-triangle"></i> {{ i18n.ts.pageLoadError }}</b></p> + <p v-if="meta && (version === meta.version)">{{ i18n.ts.pageLoadErrorDescription }}</p> + <p v-else-if="serverIsDead">{{ i18n.ts.serverIsDead }}</p> <template v-else> - <p>{{ i18n.locale.newVersionOfClientAvailable }}</p> - <p>{{ i18n.locale.youShouldUpgradeClient }}</p> - <MkButton class="button primary" @click="reload">{{ i18n.locale.reload }}</MkButton> + <p>{{ i18n.ts.newVersionOfClientAvailable }}</p> + <p>{{ i18n.ts.youShouldUpgradeClient }}</p> + <MkButton class="button primary" @click="reload">{{ i18n.ts.reload }}</MkButton> </template> - <p><MkA to="/docs/general/troubleshooting" class="_link">{{ i18n.locale.troubleshooting }}</MkA></p> + <p><MkA to="/docs/general/troubleshooting" class="_link">{{ i18n.ts.troubleshooting }}</MkA></p> <p v-if="error" class="error">ERROR: {{ error }}</p> </div> </transition> @@ -54,7 +54,7 @@ function reload() { defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale.error, + title: i18n.ts.error, icon: 'fas fa-exclamation-triangle', }, }); diff --git a/packages/client/src/pages/about-misskey.vue b/packages/client/src/pages/about-misskey.vue index f887e29cc0..0ffb6b9e1d 100644 --- a/packages/client/src/pages/about-misskey.vue +++ b/packages/client/src/pages/about-misskey.vue @@ -10,7 +10,7 @@ <span v-for="emoji in easterEggEmojis" :key="emoji.id" class="emoji" :data-physics-x="emoji.left" :data-physics-y="emoji.top" :class="{ _physics_circle_: !emoji.emoji.startsWith(':') }"><MkEmoji class="emoji" :emoji="emoji.emoji" :custom-emojis="$instance.emojis" :is-reaction="false" :normal="true" :no-style="true"/></span> </div> <div class="_formBlock" style="text-align: center;"> - {{ i18n.locale._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/misskey.html" target="_blank" class="_link">{{ i18n.locale.learnMore }}</a> + {{ i18n.ts._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/misskey.html" target="_blank" class="_link">{{ i18n.ts.learnMore }}</a> </div> <div class="_formBlock" style="text-align: center;"> <MkButton primary rounded inline @click="iLoveMisskey">I <Mfm text="$[jelly ❤]"/> #Misskey</MkButton> @@ -19,23 +19,23 @@ <div class="_formLinks"> <FormLink to="https://github.com/misskey-dev/misskey" external> <template #icon><i class="fas fa-code"></i></template> - {{ i18n.locale._aboutMisskey.source }} + {{ i18n.ts._aboutMisskey.source }} <template #suffix>GitHub</template> </FormLink> <FormLink to="https://crowdin.com/project/misskey" external> <template #icon><i class="fas fa-language"></i></template> - {{ i18n.locale._aboutMisskey.translation }} + {{ i18n.ts._aboutMisskey.translation }} <template #suffix>Crowdin</template> </FormLink> <FormLink to="https://www.patreon.com/syuilo" external> <template #icon><i class="fas fa-hand-holding-medical"></i></template> - {{ i18n.locale._aboutMisskey.donate }} + {{ i18n.ts._aboutMisskey.donate }} <template #suffix>Patreon</template> </FormLink> </div> </FormSection> <FormSection> - <template #label>{{ i18n.locale._aboutMisskey.contributors }}</template> + <template #label>{{ i18n.ts._aboutMisskey.contributors }}</template> <div class="_formLinks"> <FormLink to="https://github.com/syuilo" external>@syuilo</FormLink> <FormLink to="https://github.com/AyaMorisawa" external>@AyaMorisawa</FormLink> @@ -47,12 +47,12 @@ <FormLink to="https://github.com/u1-liquid" external>@u1-liquid</FormLink> <FormLink to="https://github.com/marihachi" external>@marihachi</FormLink> </div> - <template #caption><MkLink url="https://github.com/misskey-dev/misskey/graphs/contributors">{{ i18n.locale._aboutMisskey.allContributors }}</MkLink></template> + <template #caption><MkLink url="https://github.com/misskey-dev/misskey/graphs/contributors">{{ i18n.ts._aboutMisskey.allContributors }}</MkLink></template> </FormSection> <FormSection> - <template #label><Mfm text="$[jelly ❤]"/> {{ i18n.locale._aboutMisskey.patrons }}</template> + <template #label><Mfm text="$[jelly ❤]"/> {{ i18n.ts._aboutMisskey.patrons }}</template> <div v-for="patron in patrons" :key="patron">{{ patron }}</div> - <template #caption>{{ i18n.locale._aboutMisskey.morePatrons }}</template> + <template #caption>{{ i18n.ts._aboutMisskey.morePatrons }}</template> </FormSection> </div> </MkSpacer> @@ -194,7 +194,7 @@ onBeforeUnmount(() => { defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale.aboutMisskey, + title: i18n.ts.aboutMisskey, icon: null, bg: 'var(--bg)', }, diff --git a/packages/client/src/pages/about.vue b/packages/client/src/pages/about.vue index a5984c548d..d5bab4baf8 100644 --- a/packages/client/src/pages/about.vue +++ b/packages/client/src/pages/about.vue @@ -90,7 +90,7 @@ const initStats = () => os.api('stats', { defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale.instanceInfo, + title: i18n.ts.instanceInfo, icon: 'fas fa-info-circle', bg: 'var(--bg)', }, diff --git a/packages/client/src/pages/admin/emojis.vue b/packages/client/src/pages/admin/emojis.vue index 5b1dfe565a..a080ee9c23 100644 --- a/packages/client/src/pages/admin/emojis.vue +++ b/packages/client/src/pages/admin/emojis.vue @@ -118,7 +118,7 @@ const toggleSelect = (emoji) => { }; const add = async (ev: MouseEvent) => { - const files = await selectFiles(ev.currentTarget || ev.target, null); + const files = await selectFiles(ev.currentTarget ?? ev.target, null); const promise = Promise.all(files.map(file => os.api('admin/emoji/add', { fileId: file.id, @@ -157,23 +157,23 @@ const remoteMenu = (emoji, ev: MouseEvent) => { type: 'label', text: ':' + emoji.name + ':', }, { - text: i18n.locale.import, + text: i18n.ts.import, icon: 'fas fa-plus', action: () => { im(emoji) } - }], ev.currentTarget || ev.target); + }], ev.currentTarget ?? ev.target); }; const menu = (ev: MouseEvent) => { os.popupMenu([{ icon: 'fas fa-download', - text: i18n.locale.export, + text: i18n.ts.export, action: async () => { os.api('export-custom-emojis', { }) .then(() => { os.alert({ type: 'info', - text: i18n.locale.exportRequested, + text: i18n.ts.exportRequested, }); }).catch((e) => { os.alert({ @@ -184,16 +184,16 @@ const menu = (ev: MouseEvent) => { } }, { icon: 'fas fa-upload', - text: i18n.locale.import, + text: i18n.ts.import, action: async () => { - const file = await selectFile(ev.currentTarget || ev.target); + const file = await selectFile(ev.currentTarget ?? ev.target); os.api('admin/emoji/import-zip', { fileId: file.id, }) .then(() => { os.alert({ type: 'info', - text: i18n.locale.importRequested, + text: i18n.ts.importRequested, }); }).catch((e) => { os.alert({ @@ -202,7 +202,7 @@ const menu = (ev: MouseEvent) => { }); }); } - }], ev.currentTarget || ev.target); + }], ev.currentTarget ?? ev.target); }; const setCategoryBulk = async () => { @@ -256,7 +256,7 @@ const setTagBulk = async () => { const delBulk = async () => { const { canceled } = await os.confirm({ type: 'warning', - text: i18n.locale.deleteConfirm, + text: i18n.ts.deleteConfirm, }); if (canceled) return; await os.apiWithDialog('admin/emoji/delete-bulk', { @@ -267,13 +267,13 @@ const delBulk = async () => { defineExpose({ [symbols.PAGE_INFO]: computed(() => ({ - title: i18n.locale.customEmojis, + title: i18n.ts.customEmojis, icon: 'fas fa-laugh', bg: 'var(--bg)', actions: [{ asFullButton: true, icon: 'fas fa-plus', - text: i18n.locale.addEmoji, + text: i18n.ts.addEmoji, handler: add, }, { icon: 'fas fa-ellipsis-h', @@ -281,11 +281,11 @@ defineExpose({ }], tabs: [{ active: tab.value === 'local', - title: i18n.locale.local, + title: i18n.ts.local, onClick: () => { tab.value = 'local'; }, }, { active: tab.value === 'remote', - title: i18n.locale.remote, + title: i18n.ts.remote, onClick: () => { tab.value = 'remote'; }, },] })), diff --git a/packages/client/src/pages/admin/index.vue b/packages/client/src/pages/admin/index.vue index 350e7defc6..6b11650f48 100644 --- a/packages/client/src/pages/admin/index.vue +++ b/packages/client/src/pages/admin/index.vue @@ -55,7 +55,7 @@ export default defineComponent({ setup(props, context) { const indexInfo = { - title: i18n.locale.controlPanel, + title: i18n.ts.controlPanel, icon: 'fas fa-cog', bg: 'var(--bg)', hideHeader: true, @@ -91,119 +91,119 @@ export default defineComponent({ }); const menuDef = computed(() => [{ - title: i18n.locale.quickAction, + title: i18n.ts.quickAction, items: [{ type: 'button', icon: 'fas fa-search', - text: i18n.locale.lookup, + text: i18n.ts.lookup, action: lookup, }, ...(instance.disableRegistration ? [{ type: 'button', icon: 'fas fa-user', - text: i18n.locale.invite, + text: i18n.ts.invite, action: invite, }] : [])], }, { - title: i18n.locale.administration, + title: i18n.ts.administration, items: [{ icon: 'fas fa-tachometer-alt', - text: i18n.locale.dashboard, + text: i18n.ts.dashboard, to: '/admin/overview', active: page.value === 'overview', }, { icon: 'fas fa-users', - text: i18n.locale.users, + text: i18n.ts.users, to: '/admin/users', active: page.value === 'users', }, { icon: 'fas fa-laugh', - text: i18n.locale.customEmojis, + text: i18n.ts.customEmojis, to: '/admin/emojis', active: page.value === 'emojis', }, { icon: 'fas fa-globe', - text: i18n.locale.federation, + text: i18n.ts.federation, to: '/admin/federation', active: page.value === 'federation', }, { icon: 'fas fa-clipboard-list', - text: i18n.locale.jobQueue, + text: i18n.ts.jobQueue, to: '/admin/queue', active: page.value === 'queue', }, { icon: 'fas fa-cloud', - text: i18n.locale.files, + text: i18n.ts.files, to: '/admin/files', active: page.value === 'files', }, { icon: 'fas fa-broadcast-tower', - text: i18n.locale.announcements, + text: i18n.ts.announcements, to: '/admin/announcements', active: page.value === 'announcements', }, { icon: 'fas fa-audio-description', - text: i18n.locale.ads, + text: i18n.ts.ads, to: '/admin/ads', active: page.value === 'ads', }, { icon: 'fas fa-exclamation-circle', - text: i18n.locale.abuseReports, + text: i18n.ts.abuseReports, to: '/admin/abuses', active: page.value === 'abuses', }], }, { - title: i18n.locale.settings, + title: i18n.ts.settings, items: [{ icon: 'fas fa-cog', - text: i18n.locale.general, + text: i18n.ts.general, to: '/admin/settings', active: page.value === 'settings', }, { icon: 'fas fa-envelope', - text: i18n.locale.emailServer, + text: i18n.ts.emailServer, to: '/admin/email-settings', active: page.value === 'email-settings', }, { icon: 'fas fa-cloud', - text: i18n.locale.objectStorage, + text: i18n.ts.objectStorage, to: '/admin/object-storage', active: page.value === 'object-storage', }, { icon: 'fas fa-lock', - text: i18n.locale.security, + text: i18n.ts.security, to: '/admin/security', active: page.value === 'security', }, { icon: 'fas fa-globe', - text: i18n.locale.relays, + text: i18n.ts.relays, to: '/admin/relays', active: page.value === 'relays', }, { icon: 'fas fa-share-alt', - text: i18n.locale.integration, + text: i18n.ts.integration, to: '/admin/integrations', active: page.value === 'integrations', }, { icon: 'fas fa-ban', - text: i18n.locale.instanceBlocking, + text: i18n.ts.instanceBlocking, to: '/admin/instance-block', active: page.value === 'instance-block', }, { icon: 'fas fa-ghost', - text: i18n.locale.proxyAccount, + text: i18n.ts.proxyAccount, to: '/admin/proxy-account', active: page.value === 'proxy-account', }, { icon: 'fas fa-cogs', - text: i18n.locale.other, + text: i18n.ts.other, to: '/admin/other-settings', active: page.value === 'other-settings', }], }, { - title: i18n.locale.info, + title: i18n.ts.info, items: [{ icon: 'fas fa-database', - text: i18n.locale.database, + text: i18n.ts.database, to: '/admin/database', active: page.value === 'database', }], @@ -275,37 +275,37 @@ export default defineComponent({ const lookup = (ev) => { os.popupMenu([{ - text: i18n.locale.user, + text: i18n.ts.user, icon: 'fas fa-user', action: () => { lookupUser(); } }, { - text: i18n.locale.note, + text: i18n.ts.note, icon: 'fas fa-pencil-alt', action: () => { alert('TODO'); } }, { - text: i18n.locale.file, + text: i18n.ts.file, icon: 'fas fa-cloud', action: () => { alert('TODO'); } }, { - text: i18n.locale.instance, + text: i18n.ts.instance, icon: 'fas fa-globe', action: () => { alert('TODO'); } - }], ev.currentTarget || ev.target); + }], ev.currentTarget ?? ev.target); }; return { [symbols.PAGE_INFO]: INFO, menuDef, header: { - title: i18n.locale.controlPanel, + title: i18n.ts.controlPanel, }, noMaintainerInformation, noBotProtection, diff --git a/packages/client/src/pages/channel-editor.vue b/packages/client/src/pages/channel-editor.vue index 58c644be62..3818c7481a 100644 --- a/packages/client/src/pages/channel-editor.vue +++ b/packages/client/src/pages/channel-editor.vue @@ -112,7 +112,7 @@ export default defineComponent({ }, setBannerImage(e) { - selectFile(e.currentTarget || e.target, null).then(file => { + selectFile(e.currentTarget ?? e.target, null).then(file => { this.bannerId = file.id; }); }, diff --git a/packages/client/src/pages/clip.vue b/packages/client/src/pages/clip.vue index 6b49221d32..c999f1bfc9 100644 --- a/packages/client/src/pages/clip.vue +++ b/packages/client/src/pages/clip.vue @@ -127,7 +127,7 @@ export default defineComponent({ clipId: this.clip.id, }); } - } : undefined], ev.currentTarget || ev.target); + } : undefined], ev.currentTarget ?? ev.target); } } }); diff --git a/packages/client/src/pages/drive.vue b/packages/client/src/pages/drive.vue index 1e17bea0cc..68777bb083 100644 --- a/packages/client/src/pages/drive.vue +++ b/packages/client/src/pages/drive.vue @@ -15,7 +15,7 @@ let folder = $ref(null); defineExpose({ [symbols.PAGE_INFO]: computed(() => ({ - title: folder ? folder.name : i18n.locale.drive, + title: folder ? folder.name : i18n.ts.drive, icon: 'fas fa-cloud', bg: 'var(--bg)', hideHeader: true, diff --git a/packages/client/src/pages/emojis.emoji.vue b/packages/client/src/pages/emojis.emoji.vue index 83539ce7a3..b2801694db 100644 --- a/packages/client/src/pages/emojis.emoji.vue +++ b/packages/client/src/pages/emojis.emoji.vue @@ -23,13 +23,13 @@ function menu(ev) { type: 'label', text: ':' + props.emoji.name + ':', }, { - text: i18n.locale.copy, + text: i18n.ts.copy, icon: 'fas fa-copy', action: () => { copyToClipboard(`:${props.emoji.name}:`); os.success(); } - }], ev.currentTarget || ev.target); + }], ev.currentTarget ?? ev.target); } </script> diff --git a/packages/client/src/pages/emojis.vue b/packages/client/src/pages/emojis.vue index 6577f5abd9..886b5f7119 100644 --- a/packages/client/src/pages/emojis.vue +++ b/packages/client/src/pages/emojis.vue @@ -16,14 +16,14 @@ const tab = ref('category'); function menu(ev) { os.popupMenu([{ icon: 'fas fa-download', - text: i18n.locale.export, + text: i18n.ts.export, action: async () => { os.api('export-custom-emojis', { }) .then(() => { os.alert({ type: 'info', - text: i18n.locale.exportRequested, + text: i18n.ts.exportRequested, }); }).catch((e) => { os.alert({ @@ -32,12 +32,12 @@ function menu(ev) { }); }); } - }], ev.currentTarget || ev.target); + }], ev.currentTarget ?? ev.target); } defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale.customEmojis, + title: i18n.ts.customEmojis, icon: 'fas fa-laugh', bg: 'var(--bg)', actions: [{ diff --git a/packages/client/src/pages/favorites.vue b/packages/client/src/pages/favorites.vue index 8965b30d60..b4f6ff35bc 100644 --- a/packages/client/src/pages/favorites.vue +++ b/packages/client/src/pages/favorites.vue @@ -34,7 +34,7 @@ const pagingComponent = ref<InstanceType<typeof MkPagination>>(); defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale.favorites, + title: i18n.ts.favorites, icon: 'fas fa-star', bg: 'var(--bg)', }, diff --git a/packages/client/src/pages/featured.vue b/packages/client/src/pages/featured.vue index 725c70f0f7..14fe0cb740 100644 --- a/packages/client/src/pages/featured.vue +++ b/packages/client/src/pages/featured.vue @@ -17,7 +17,7 @@ const pagination = { defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale.featured, + title: i18n.ts.featured, icon: 'fas fa-fire-alt', bg: 'var(--bg)', }, diff --git a/packages/client/src/pages/federation.vue b/packages/client/src/pages/federation.vue index 6a4a28b6b4..3c5050cdb8 100644 --- a/packages/client/src/pages/federation.vue +++ b/packages/client/src/pages/federation.vue @@ -115,7 +115,7 @@ const pagination = { offsetMode: true, params: computed(() => ({ sort: sort, - host: host != '' ? host : null, + host: host !== '' ? host : null, ...( state === 'federating' ? { federating: true } : state === 'subscribing' ? { subscribing: true } : @@ -135,7 +135,7 @@ function getStatus(instance) { defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale.federation, + title: i18n.ts.federation, icon: 'fas fa-globe', bg: 'var(--bg)', }, @@ -157,11 +157,10 @@ defineExpose({ > .instance { padding: 16px; - border: solid 1px var(--divider); - border-radius: 6px; + background: var(--panel); + border-radius: 8px; &:hover { - border: solid 1px var(--accent); text-decoration: none; } diff --git a/packages/client/src/pages/follow-requests.vue b/packages/client/src/pages/follow-requests.vue index 764daa0d3e..6adc1a404b 100644 --- a/packages/client/src/pages/follow-requests.vue +++ b/packages/client/src/pages/follow-requests.vue @@ -60,7 +60,7 @@ function reject(user) { defineExpose({ [symbols.PAGE_INFO]: computed(() => ({ - title: i18n.locale.followRequests, + title: i18n.ts.followRequests, icon: 'fas fa-user-clock', bg: 'var(--bg)', })), diff --git a/packages/client/src/pages/gallery/edit.vue b/packages/client/src/pages/gallery/edit.vue index e3fa1a0fcd..25ee513186 100644 --- a/packages/client/src/pages/gallery/edit.vue +++ b/packages/client/src/pages/gallery/edit.vue @@ -92,7 +92,7 @@ export default defineComponent({ methods: { selectFile(e) { - selectFiles(e.currentTarget || e.target, null).then(files => { + selectFiles(e.currentTarget ?? e.target, null).then(files => { this.files = this.files.concat(files); }); }, diff --git a/packages/client/src/pages/instance-info.vue b/packages/client/src/pages/instance-info.vue index fa36db0659..f19cb9d1a2 100644 --- a/packages/client/src/pages/instance-info.vue +++ b/packages/client/src/pages/instance-info.vue @@ -29,6 +29,7 @@ <template #label>Moderation</template> <FormSwitch v-model="suspended" class="_formBlock" @update:modelValue="toggleSuspend">{{ $ts.stopActivityDelivery }}</FormSwitch> <FormSwitch v-model="isBlocked" class="_formBlock" @update:modelValue="toggleBlock">{{ $ts.blockThisInstance }}</FormSwitch> + <MkButton @click="refreshMetadata">Refresh metadata</MkButton> </FormSection> <FormSection> @@ -111,6 +112,7 @@ import MkChart from '@/components/chart.vue'; import MkObjectView from '@/components/object-view.vue'; import FormLink from '@/components/form/link.vue'; import MkLink from '@/components/link.vue'; +import MkButton from '@/components/ui/button.vue'; import FormSection from '@/components/form/section.vue'; import MkKeyValue from '@/components/key-value.vue'; import MkSelect from '@/components/form/select.vue'; @@ -155,6 +157,15 @@ async function toggleSuspend(v) { }); } +function refreshMetadata() { + os.api('admin/federation/refresh-remote-instance-metadata', { + host: instance.host, + }); + os.alert({ + text: 'Refresh requested', + }); +} + fetch(); defineExpose({ diff --git a/packages/client/src/pages/mentions.vue b/packages/client/src/pages/mentions.vue index bda56fc729..9b57c956bf 100644 --- a/packages/client/src/pages/mentions.vue +++ b/packages/client/src/pages/mentions.vue @@ -16,7 +16,7 @@ const pagination = { defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale.mentions, + title: i18n.ts.mentions, icon: 'fas fa-at', bg: 'var(--bg)', }, diff --git a/packages/client/src/pages/messages.vue b/packages/client/src/pages/messages.vue index 8efdc55586..9c5fb9b341 100644 --- a/packages/client/src/pages/messages.vue +++ b/packages/client/src/pages/messages.vue @@ -12,14 +12,14 @@ import { i18n } from '@/i18n'; const pagination = { endpoint: 'notes/mentions' as const, limit: 10, - params: () => ({ + params: { visibility: 'specified' - }), + }, }; defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale.directNotes, + title: i18n.ts.directNotes, icon: 'fas fa-envelope', bg: 'var(--bg)', }, diff --git a/packages/client/src/pages/messaging/index.vue b/packages/client/src/pages/messaging/index.vue index 554ebc4b6b..88a1e07afc 100644 --- a/packages/client/src/pages/messaging/index.vue +++ b/packages/client/src/pages/messaging/index.vue @@ -128,7 +128,7 @@ export default defineComponent({ text: this.$ts.messagingWithGroup, icon: 'fas fa-users', action: () => { this.startGroup() } - }], ev.currentTarget || ev.target); + }], ev.currentTarget ?? ev.target); }, async startUser() { diff --git a/packages/client/src/pages/messaging/messaging-room.form.vue b/packages/client/src/pages/messaging/messaging-room.form.vue index 1b9421ca9a..3863c8f82b 100644 --- a/packages/client/src/pages/messaging/messaging-room.form.vue +++ b/packages/client/src/pages/messaging/messaging-room.form.vue @@ -154,7 +154,7 @@ export default defineComponent({ }, chooseFile(e) { - selectFile(e.currentTarget || e.target, this.$ts.selectFile).then(file => { + selectFile(e.currentTarget ?? e.target, this.$ts.selectFile).then(file => { this.file = file; }); }, @@ -214,7 +214,7 @@ export default defineComponent({ }, async insertEmoji(ev) { - os.openEmojiPicker(ev.currentTarget || ev.target, {}, this.$refs.text); + os.openEmojiPicker(ev.currentTarget ?? ev.target, {}, this.$refs.text); } } }); diff --git a/packages/client/src/pages/messaging/messaging-room.vue b/packages/client/src/pages/messaging/messaging-room.vue index 65c44ce113..2ecc68eb54 100644 --- a/packages/client/src/pages/messaging/messaging-room.vue +++ b/packages/client/src/pages/messaging/messaging-room.vue @@ -335,7 +335,7 @@ const Component = defineComponent({ popout(path); this.$router.back(); }, - }], ev.currentTarget || ev.target); + }], ev.currentTarget ?? ev.target); } } }); diff --git a/packages/client/src/pages/my-antennas/create.vue b/packages/client/src/pages/my-antennas/create.vue index 427c9935c3..a08bece731 100644 --- a/packages/client/src/pages/my-antennas/create.vue +++ b/packages/client/src/pages/my-antennas/create.vue @@ -31,7 +31,7 @@ function onAntennaCreated() { defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale.manageAntennas, + title: i18n.ts.manageAntennas, icon: 'fas fa-satellite', bg: 'var(--bg)', }, diff --git a/packages/client/src/pages/my-clips/index.vue b/packages/client/src/pages/my-clips/index.vue index 97b563f6f8..e287357a42 100644 --- a/packages/client/src/pages/my-clips/index.vue +++ b/packages/client/src/pages/my-clips/index.vue @@ -19,7 +19,7 @@ import MkPagination from '@/components/ui/pagination.vue'; import MkButton from '@/components/ui/button.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; -import i18n from '@/components/global/i18n'; +import { i18n } from '@/i18n'; const pagination = { endpoint: 'clips/list' as const, @@ -29,20 +29,20 @@ const pagination = { const pagingComponent = $ref<InstanceType<typeof MkPagination>>(); async function create() { - const { canceled, result } = await os.form(i18n.locale.createNewClip, { + const { canceled, result } = await os.form(i18n.ts.createNewClip, { name: { type: 'string', - label: i18n.locale.name, + label: i18n.ts.name, }, description: { type: 'string', required: false, multiline: true, - label: i18n.locale.description, + label: i18n.ts.description, }, isPublic: { type: 'boolean', - label: i18n.locale.public, + label: i18n.ts.public, default: false, }, }); @@ -63,7 +63,7 @@ function onClipDeleted() { defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale.clip, + title: i18n.ts.clip, icon: 'fas fa-paperclip', bg: 'var(--bg)', action: { diff --git a/packages/client/src/pages/my-lists/index.vue b/packages/client/src/pages/my-lists/index.vue index e6fcba1b34..9ed9e2960e 100644 --- a/packages/client/src/pages/my-lists/index.vue +++ b/packages/client/src/pages/my-lists/index.vue @@ -31,7 +31,7 @@ const pagination = { async function create() { const { canceled, result: name } = await os.inputText({ - title: i18n.locale.enterListName, + title: i18n.ts.enterListName, }); if (canceled) return; await os.apiWithDialog('users/lists/create', { name: name }); @@ -40,7 +40,7 @@ async function create() { defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale.manageLists, + title: i18n.ts.manageLists, icon: 'fas fa-list-ul', bg: 'var(--bg)', action: { diff --git a/packages/client/src/pages/not-found.vue b/packages/client/src/pages/not-found.vue index 914fdb9297..cdeb54b88b 100644 --- a/packages/client/src/pages/not-found.vue +++ b/packages/client/src/pages/not-found.vue @@ -13,7 +13,7 @@ import { i18n } from '@/i18n'; defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale.notFound, + title: i18n.ts.notFound, icon: 'fas fa-exclamation-triangle', bg: 'var(--bg)', }, diff --git a/packages/client/src/pages/notifications.vue b/packages/client/src/pages/notifications.vue index 090e80f99a..36e423e534 100644 --- a/packages/client/src/pages/notifications.vue +++ b/packages/client/src/pages/notifications.vue @@ -27,26 +27,26 @@ function setFilter(ev) { })); const items = includeTypes != null ? [{ icon: 'fas fa-times', - text: i18n.locale.clear, + text: i18n.ts.clear, action: () => { includeTypes = null; } }, null, ...typeItems] : typeItems; - os.popupMenu(items, ev.currentTarget || ev.target); + os.popupMenu(items, ev.currentTarget ?? ev.target); } defineExpose({ [symbols.PAGE_INFO]: computed(() => ({ - title: i18n.locale.notifications, + title: i18n.ts.notifications, icon: 'fas fa-bell', bg: 'var(--bg)', actions: [{ - text: i18n.locale.filter, + text: i18n.ts.filter, icon: 'fas fa-filter', highlighted: includeTypes != null, handler: setFilter, }, { - text: i18n.locale.markAllAsRead, + text: i18n.ts.markAllAsRead, icon: 'fas fa-check', handler: () => { os.apiWithDialog('notifications/mark-all-as-read'); @@ -54,11 +54,11 @@ defineExpose({ }], tabs: [{ active: tab === 'all', - title: i18n.locale.all, + title: i18n.ts.all, onClick: () => { tab = 'all'; }, }, { active: tab === 'unread', - title: i18n.locale.unread, + title: i18n.ts.unread, onClick: () => { tab = 'unread'; }, },] })), diff --git a/packages/client/src/pages/page-editor/page-editor.vue b/packages/client/src/pages/page-editor/page-editor.vue index fe207555f8..f302ac4f90 100644 --- a/packages/client/src/pages/page-editor/page-editor.vue +++ b/packages/client/src/pages/page-editor/page-editor.vue @@ -448,7 +448,7 @@ export default defineComponent({ }, setEyeCatchingImage(e) { - selectFile(e.currentTarget || e.target, null).then(file => { + selectFile(e.currentTarget ?? e.target, null).then(file => { this.eyeCatchingImageId = file.id; }); }, diff --git a/packages/client/src/pages/preview.vue b/packages/client/src/pages/preview.vue index 8eb4549516..4accac4192 100644 --- a/packages/client/src/pages/preview.vue +++ b/packages/client/src/pages/preview.vue @@ -12,7 +12,7 @@ import { i18n } from '@/i18n'; defineExpose({ [symbols.PAGE_INFO]: computed(() => ({ - title: i18n.locale.preview, + title: i18n.ts.preview, icon: 'fas fa-eye', bg: 'var(--bg)', })), diff --git a/packages/client/src/pages/reset-password.vue b/packages/client/src/pages/reset-password.vue index 8ef73858f6..7d008ae75c 100644 --- a/packages/client/src/pages/reset-password.vue +++ b/packages/client/src/pages/reset-password.vue @@ -3,10 +3,10 @@ <div class="_formRoot"> <FormInput v-model="password" type="password" class="_formBlock"> <template #prefix><i class="fas fa-lock"></i></template> - <template #label>{{ i18n.locale.newPassword }}</template> + <template #label>{{ i18n.ts.newPassword }}</template> </FormInput> - <FormButton primary class="_formBlock" @click="save">{{ i18n.locale.save }}</FormButton> + <FormButton primary class="_formBlock" @click="save">{{ i18n.ts.save }}</FormButton> </div> </MkSpacer> </template> @@ -43,7 +43,7 @@ onMounted(() => { defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale.resetPassword, + title: i18n.ts.resetPassword, icon: 'fas fa-lock', bg: 'var(--bg)', }, diff --git a/packages/client/src/pages/settings/accounts.vue b/packages/client/src/pages/settings/accounts.vue index c795ede8ac..a744a031d4 100644 --- a/packages/client/src/pages/settings/accounts.vue +++ b/packages/client/src/pages/settings/accounts.vue @@ -64,7 +64,7 @@ export default defineComponent({ icon: 'fas fa-trash-alt', danger: true, action: () => this.removeAccount(account), - }], ev.currentTarget || ev.target); + }], ev.currentTarget ?? ev.target); }, addAccount(ev) { @@ -74,7 +74,7 @@ export default defineComponent({ }, { text: this.$ts.createAccount, action: () => { this.createAccount(); }, - }], ev.currentTarget || ev.target); + }], ev.currentTarget ?? ev.target); }, addExistingAccount() { diff --git a/packages/client/src/pages/settings/drive.vue b/packages/client/src/pages/settings/drive.vue index f1016ebd84..134fa63308 100644 --- a/packages/client/src/pages/settings/drive.vue +++ b/packages/client/src/pages/settings/drive.vue @@ -28,6 +28,7 @@ <template #suffix>{{ uploadFolder ? uploadFolder.name : '-' }}</template> <template #suffixIcon><i class="fas fa-folder-open"></i></template> </FormLink> + <FormSwitch v-model="keepOriginalUploading" class="_formBlock">{{ $ts.keepOriginalUploading }}<template #caption>{{ $ts.keepOriginalUploadingDescription }}</template></FormSwitch> </FormSection> </div> </template> @@ -36,18 +37,21 @@ import { defineComponent } from 'vue'; import * as tinycolor from 'tinycolor2'; import FormLink from '@/components/form/link.vue'; +import FormSwitch from '@/components/form/switch.vue'; import FormSection from '@/components/form/section.vue'; import MkKeyValue from '@/components/key-value.vue'; import FormSplit from '@/components/form/split.vue'; import * as os from '@/os'; import bytes from '@/filters/bytes'; import * as symbols from '@/symbols'; +import { defaultStore } from '@/store'; // TODO: render chart export default defineComponent({ components: { FormLink, + FormSwitch, FormSection, MkKeyValue, FormSplit, @@ -79,7 +83,8 @@ export default defineComponent({ l: 0.5 }) }; - } + }, + keepOriginalUploading: defaultStore.makeGetterSetter('keepOriginalUploading'), }, async created() { diff --git a/packages/client/src/pages/settings/email.vue b/packages/client/src/pages/settings/email.vue index 54557f8773..4697fec9b7 100644 --- a/packages/client/src/pages/settings/email.vue +++ b/packages/client/src/pages/settings/email.vue @@ -62,7 +62,7 @@ export default defineComponent({ const emailAddress = ref($i.email); const INFO = { - title: i18n.locale.email, + title: i18n.ts.email, icon: 'fas fa-envelope', bg: 'var(--bg)', }; @@ -75,7 +75,7 @@ export default defineComponent({ const saveEmailAddress = () => { os.inputText({ - title: i18n.locale.password, + title: i18n.ts.password, type: 'password' }).then(({ canceled, result: password }) => { if (canceled) return; diff --git a/packages/client/src/pages/settings/import-export.vue b/packages/client/src/pages/settings/import-export.vue index 21031c559e..c153b4d28c 100644 --- a/packages/client/src/pages/settings/import-export.vue +++ b/packages/client/src/pages/settings/import-export.vue @@ -60,7 +60,7 @@ export default defineComponent({ setup(props, context) { const INFO = { - title: i18n.locale.importAndExport, + title: i18n.ts.importAndExport, icon: 'fas fa-boxes', bg: 'var(--bg)', }; @@ -71,14 +71,14 @@ export default defineComponent({ const onExportSuccess = () => { os.alert({ type: 'info', - text: i18n.locale.exportRequested, + text: i18n.ts.exportRequested, }); }; const onImportSuccess = () => { os.alert({ type: 'info', - text: i18n.locale.importRequested, + text: i18n.ts.importRequested, }); }; @@ -114,22 +114,22 @@ export default defineComponent({ }; const importFollowing = async (ev) => { - const file = await selectFile(ev.currentTarget || ev.target); + const file = await selectFile(ev.currentTarget ?? ev.target); os.api('i/import-following', { fileId: file.id }).then(onImportSuccess).catch(onError); }; const importUserLists = async (ev) => { - const file = await selectFile(ev.currentTarget || ev.target); + const file = await selectFile(ev.currentTarget ?? ev.target); os.api('i/import-user-lists', { fileId: file.id }).then(onImportSuccess).catch(onError); }; const importMuting = async (ev) => { - const file = await selectFile(ev.currentTarget || ev.target); + const file = await selectFile(ev.currentTarget ?? ev.target); os.api('i/import-muting', { fileId: file.id }).then(onImportSuccess).catch(onError); }; const importBlocking = async (ev) => { - const file = await selectFile(ev.currentTarget || ev.target); + const file = await selectFile(ev.currentTarget ?? ev.target); os.api('i/import-blocking', { fileId: file.id }).then(onImportSuccess).catch(onError); }; diff --git a/packages/client/src/pages/settings/index.vue b/packages/client/src/pages/settings/index.vue index 66c8b147bb..ac8414ddbc 100644 --- a/packages/client/src/pages/settings/index.vue +++ b/packages/client/src/pages/settings/index.vue @@ -49,7 +49,7 @@ export default defineComponent({ setup(props, context) { const indexInfo = { - title: i18n.locale.settings, + title: i18n.ts.settings, icon: 'fas fa-cog', bg: 'var(--bg)', hideHeader: true, @@ -61,96 +61,96 @@ export default defineComponent({ const el = ref(null); const childInfo = ref(null); const menuDef = computed(() => [{ - title: i18n.locale.basicSettings, + title: i18n.ts.basicSettings, items: [{ icon: 'fas fa-user', - text: i18n.locale.profile, + text: i18n.ts.profile, to: '/settings/profile', active: page.value === 'profile', }, { icon: 'fas fa-lock-open', - text: i18n.locale.privacy, + text: i18n.ts.privacy, to: '/settings/privacy', active: page.value === 'privacy', }, { icon: 'fas fa-laugh', - text: i18n.locale.reaction, + text: i18n.ts.reaction, to: '/settings/reaction', active: page.value === 'reaction', }, { icon: 'fas fa-cloud', - text: i18n.locale.drive, + text: i18n.ts.drive, to: '/settings/drive', active: page.value === 'drive', }, { icon: 'fas fa-bell', - text: i18n.locale.notifications, + text: i18n.ts.notifications, to: '/settings/notifications', active: page.value === 'notifications', }, { icon: 'fas fa-envelope', - text: i18n.locale.email, + text: i18n.ts.email, to: '/settings/email', active: page.value === 'email', }, { icon: 'fas fa-share-alt', - text: i18n.locale.integration, + text: i18n.ts.integration, to: '/settings/integration', active: page.value === 'integration', }, { icon: 'fas fa-lock', - text: i18n.locale.security, + text: i18n.ts.security, to: '/settings/security', active: page.value === 'security', }], }, { - title: i18n.locale.clientSettings, + title: i18n.ts.clientSettings, items: [{ icon: 'fas fa-cogs', - text: i18n.locale.general, + text: i18n.ts.general, to: '/settings/general', active: page.value === 'general', }, { icon: 'fas fa-palette', - text: i18n.locale.theme, + text: i18n.ts.theme, to: '/settings/theme', active: page.value === 'theme', }, { icon: 'fas fa-list-ul', - text: i18n.locale.menu, + text: i18n.ts.menu, to: '/settings/menu', active: page.value === 'menu', }, { icon: 'fas fa-music', - text: i18n.locale.sounds, + text: i18n.ts.sounds, to: '/settings/sounds', active: page.value === 'sounds', }, { icon: 'fas fa-plug', - text: i18n.locale.plugins, + text: i18n.ts.plugins, to: '/settings/plugin', active: page.value === 'plugin', }], }, { - title: i18n.locale.otherSettings, + title: i18n.ts.otherSettings, items: [{ icon: 'fas fa-boxes', - text: i18n.locale.importAndExport, + text: i18n.ts.importAndExport, to: '/settings/import-export', active: page.value === 'import-export', }, { icon: 'fas fa-volume-mute', - text: i18n.locale.instanceMute, + text: i18n.ts.instanceMute, to: '/settings/instance-mute', active: page.value === 'instance-mute', }, { icon: 'fas fa-ban', - text: i18n.locale.muteAndBlock, + text: i18n.ts.muteAndBlock, to: '/settings/mute-block', active: page.value === 'mute-block', }, { icon: 'fas fa-comment-slash', - text: i18n.locale.wordMute, + text: i18n.ts.wordMute, to: '/settings/word-mute', active: page.value === 'word-mute', }, { @@ -160,7 +160,7 @@ export default defineComponent({ active: page.value === 'api', }, { icon: 'fas fa-ellipsis-h', - text: i18n.locale.other, + text: i18n.ts.other, to: '/settings/other', active: page.value === 'other', }], @@ -168,7 +168,7 @@ export default defineComponent({ items: [{ type: 'button', icon: 'fas fa-trash', - text: i18n.locale.clearCache, + text: i18n.ts.clearCache, action: () => { localStorage.removeItem('locale'); localStorage.removeItem('theme'); @@ -177,7 +177,7 @@ export default defineComponent({ }, { type: 'button', icon: 'fas fa-sign-in-alt fa-flip-horizontal', - text: i18n.locale.logout, + text: i18n.ts.logout, action: () => { signout(); }, diff --git a/packages/client/src/pages/settings/mute-block.vue b/packages/client/src/pages/settings/mute-block.vue index f4f9ebf8dd..28d11809e3 100644 --- a/packages/client/src/pages/settings/mute-block.vue +++ b/packages/client/src/pages/settings/mute-block.vue @@ -52,7 +52,7 @@ const blockingPagination = { defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale.muteAndBlock, + title: i18n.ts.muteAndBlock, icon: 'fas fa-ban', bg: 'var(--bg)', }, diff --git a/packages/client/src/pages/settings/privacy.vue b/packages/client/src/pages/settings/privacy.vue index dd13ba4bd0..cfae7e9ca8 100644 --- a/packages/client/src/pages/settings/privacy.vue +++ b/packages/client/src/pages/settings/privacy.vue @@ -86,7 +86,7 @@ function save() { defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale.privacy, + title: i18n.ts.privacy, icon: 'fas fa-lock-open', bg: 'var(--bg)', }, diff --git a/packages/client/src/pages/settings/profile.vue b/packages/client/src/pages/settings/profile.vue index f875146a2c..66b654d87f 100644 --- a/packages/client/src/pages/settings/profile.vue +++ b/packages/client/src/pages/settings/profile.vue @@ -3,45 +3,45 @@ <div class="llvierxe" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }"> <div class="avatar _acrylic"> <MkAvatar class="avatar" :user="$i" :disable-link="true" @click="changeAvatar"/> - <MkButton primary class="avatarEdit" @click="changeAvatar">{{ i18n.locale._profile.changeAvatar }}</MkButton> + <MkButton primary class="avatarEdit" @click="changeAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton> </div> - <MkButton primary class="bannerEdit" @click="changeBanner">{{ i18n.locale._profile.changeBanner }}</MkButton> + <MkButton primary class="bannerEdit" @click="changeBanner">{{ i18n.ts._profile.changeBanner }}</MkButton> </div> <FormInput v-model="profile.name" :max="30" manual-save class="_formBlock"> - <template #label>{{ i18n.locale._profile.name }}</template> + <template #label>{{ i18n.ts._profile.name }}</template> </FormInput> <FormTextarea v-model="profile.description" :max="500" tall manual-save class="_formBlock"> - <template #label>{{ i18n.locale._profile.description }}</template> - <template #caption>{{ i18n.locale._profile.youCanIncludeHashtags }}</template> + <template #label>{{ i18n.ts._profile.description }}</template> + <template #caption>{{ i18n.ts._profile.youCanIncludeHashtags }}</template> </FormTextarea> <FormInput v-model="profile.location" manual-save class="_formBlock"> - <template #label>{{ i18n.locale.location }}</template> + <template #label>{{ i18n.ts.location }}</template> <template #prefix><i class="fas fa-map-marker-alt"></i></template> </FormInput> <FormInput v-model="profile.birthday" type="date" manual-save class="_formBlock"> - <template #label>{{ i18n.locale.birthday }}</template> + <template #label>{{ i18n.ts.birthday }}</template> <template #prefix><i class="fas fa-birthday-cake"></i></template> </FormInput> <FormSelect v-model="profile.lang" class="_formBlock"> - <template #label>{{ i18n.locale.language }}</template> + <template #label>{{ i18n.ts.language }}</template> <option v-for="x in langs" :key="x[0]" :value="x[0]">{{ x[1] }}</option> </FormSelect> <FormSlot> - <MkButton @click="editMetadata">{{ i18n.locale._profile.metadataEdit }}</MkButton> - <template #caption>{{ i18n.locale._profile.metadataDescription }}</template> + <MkButton @click="editMetadata">{{ i18n.ts._profile.metadataEdit }}</MkButton> + <template #caption>{{ i18n.ts._profile.metadataDescription }}</template> </FormSlot> - <FormSwitch v-model="profile.isCat" class="_formBlock">{{ i18n.locale.flagAsCat }}<template #caption>{{ i18n.locale.flagAsCatDescription }}</template></FormSwitch> + <FormSwitch v-model="profile.isCat" class="_formBlock">{{ i18n.ts.flagAsCat }}<template #caption>{{ i18n.ts.flagAsCatDescription }}</template></FormSwitch> - <FormSwitch v-model="profile.isBot" class="_formBlock">{{ i18n.locale.flagAsBot }}<template #caption>{{ i18n.locale.flagAsBotDescription }}</template></FormSwitch> + <FormSwitch v-model="profile.isBot" class="_formBlock">{{ i18n.ts.flagAsBot }}<template #caption>{{ i18n.ts.flagAsBotDescription }}</template></FormSwitch> - <FormSwitch v-model="profile.alwaysMarkNsfw" class="_formBlock">{{ i18n.locale.alwaysMarkSensitive }}</FormSwitch> + <FormSwitch v-model="profile.alwaysMarkNsfw" class="_formBlock">{{ i18n.ts.alwaysMarkSensitive }}</FormSwitch> </div> </template> @@ -102,7 +102,7 @@ function save() { } function changeAvatar(ev) { - selectFile(ev.currentTarget || ev.target, i18n.locale.avatar).then(async (file) => { + selectFile(ev.currentTarget ?? ev.target, i18n.ts.avatar).then(async (file) => { const i = await os.apiWithDialog('i/update', { avatarId: file.id, }); @@ -112,7 +112,7 @@ function changeAvatar(ev) { } function changeBanner(ev) { - selectFile(ev.currentTarget || ev.target, i18n.locale.banner).then(async (file) => { + selectFile(ev.currentTarget ?? ev.target, i18n.ts.banner).then(async (file) => { const i = await os.apiWithDialog('i/update', { bannerId: file.id, }); @@ -122,45 +122,45 @@ function changeBanner(ev) { } async function editMetadata() { - const { canceled, result } = await os.form(i18n.locale._profile.metadata, { + const { canceled, result } = await os.form(i18n.ts._profile.metadata, { fieldName0: { type: 'string', - label: i18n.locale._profile.metadataLabel + ' 1', + label: i18n.ts._profile.metadataLabel + ' 1', default: additionalFields.fieldName0, }, fieldValue0: { type: 'string', - label: i18n.locale._profile.metadataContent + ' 1', + label: i18n.ts._profile.metadataContent + ' 1', default: additionalFields.fieldValue0, }, fieldName1: { type: 'string', - label: i18n.locale._profile.metadataLabel + ' 2', + label: i18n.ts._profile.metadataLabel + ' 2', default: additionalFields.fieldName1, }, fieldValue1: { type: 'string', - label: i18n.locale._profile.metadataContent + ' 2', + label: i18n.ts._profile.metadataContent + ' 2', default: additionalFields.fieldValue1, }, fieldName2: { type: 'string', - label: i18n.locale._profile.metadataLabel + ' 3', + label: i18n.ts._profile.metadataLabel + ' 3', default: additionalFields.fieldName2, }, fieldValue2: { type: 'string', - label: i18n.locale._profile.metadataContent + ' 3', + label: i18n.ts._profile.metadataContent + ' 3', default: additionalFields.fieldValue2, }, fieldName3: { type: 'string', - label: i18n.locale._profile.metadataLabel + ' 4', + label: i18n.ts._profile.metadataLabel + ' 4', default: additionalFields.fieldName3, }, fieldValue3: { type: 'string', - label: i18n.locale._profile.metadataContent + ' 4', + label: i18n.ts._profile.metadataContent + ' 4', default: additionalFields.fieldValue3, }, }); @@ -196,7 +196,7 @@ async function editMetadata() { defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale.profile, + title: i18n.ts.profile, icon: 'fas fa-user', bg: 'var(--bg)', }, diff --git a/packages/client/src/pages/settings/reaction.vue b/packages/client/src/pages/settings/reaction.vue index e5b1189947..ae3e1a1187 100644 --- a/packages/client/src/pages/settings/reaction.vue +++ b/packages/client/src/pages/settings/reaction.vue @@ -44,8 +44,8 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { watch } from 'vue'; import XDraggable from 'vuedraggable'; import FormInput from '@/components/form/input.vue'; import FormRadios from '@/components/form/radios.vue'; @@ -56,91 +56,70 @@ import FormSwitch from '@/components/form/switch.vue'; import * as os from '@/os'; import { defaultStore } from '@/store'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - FormInput, - FormButton, - FromSlot, - FormRadios, - FormSection, - FormSwitch, - XDraggable, - }, +let reactions = $ref(JSON.parse(JSON.stringify(defaultStore.state.reactions))); - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.reaction, - icon: 'fas fa-laugh', - action: { - icon: 'fas fa-eye', - handler: this.preview - }, - bg: 'var(--bg)', - }, - reactions: JSON.parse(JSON.stringify(this.$store.state.reactions)), - } - }, +const reactionPickerWidth = $computed(defaultStore.makeGetterSetter('reactionPickerWidth')); +const reactionPickerHeight = $computed(defaultStore.makeGetterSetter('reactionPickerHeight')); +const reactionPickerUseDrawerForMobile = $computed(defaultStore.makeGetterSetter('reactionPickerUseDrawerForMobile')); - computed: { - reactionPickerWidth: defaultStore.makeGetterSetter('reactionPickerWidth'), - reactionPickerHeight: defaultStore.makeGetterSetter('reactionPickerHeight'), - reactionPickerUseDrawerForMobile: defaultStore.makeGetterSetter('reactionPickerUseDrawerForMobile'), - }, +function save() { + defaultStore.set('reactions', reactions); +} - watch: { - reactions: { - handler() { - this.save(); - }, - deep: true +function remove(reaction, ev: MouseEvent) { + os.popupMenu([{ + text: i18n.ts.remove, + action: () => { + reactions = reactions.filter(x => x !== reaction); } - }, + }], ev.currentTarget ?? ev.target); +} - methods: { - save() { - this.$store.set('reactions', this.reactions); - }, +function preview(ev: MouseEvent) { + os.popup(import('@/components/emoji-picker-dialog.vue'), { + asReactionPicker: true, + src: ev.currentTarget ?? ev.target, + }, {}, 'closed'); +} - remove(reaction, ev) { - os.popupMenu([{ - text: this.$ts.remove, - action: () => { - this.reactions = this.reactions.filter(x => x !== reaction) - } - }], ev.currentTarget || ev.target); - }, +async function setDefault() { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.ts.resetAreYouSure, + }); + if (canceled) return; - preview(ev) { - os.popup(import('@/components/emoji-picker-dialog.vue'), { - asReactionPicker: true, - src: ev.currentTarget || ev.target, - }, {}, 'closed'); - }, + reactions = JSON.parse(JSON.stringify(defaultStore.def.reactions.default)); +} - async setDefault() { - const { canceled } = await os.confirm({ - type: 'warning', - text: this.$ts.resetAreYouSure, - }); - if (canceled) return; +function chooseEmoji(ev: MouseEvent) { + os.pickEmoji(ev.currentTarget ?? ev.target, { + showPinned: false + }).then(emoji => { + if (!reactions.includes(emoji)) { + reactions.push(emoji); + } + }); +} - this.reactions = JSON.parse(JSON.stringify(this.$store.def.reactions.default)); - }, +watch($$(reactions), () => { + save(); +}, { + deep: true, +}); - chooseEmoji(ev) { - os.pickEmoji(ev.currentTarget || ev.target, { - showPinned: false - }).then(emoji => { - if (!this.reactions.includes(emoji)) { - this.reactions.push(emoji); - } - }); - } - } +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.reaction, + icon: 'fas fa-laugh', + action: { + icon: 'fas fa-eye', + handler: preview, + }, + bg: 'var(--bg)', + }, }); </script> diff --git a/packages/client/src/pages/settings/theme.install.vue b/packages/client/src/pages/settings/theme.install.vue index e2a3f042b9..2d3514342e 100644 --- a/packages/client/src/pages/settings/theme.install.vue +++ b/packages/client/src/pages/settings/theme.install.vue @@ -1,12 +1,12 @@ <template> <div class="_formRoot"> <FormTextarea v-model="installThemeCode" class="_formBlock"> - <template #label>{{ i18n.locale._theme.code }}</template> + <template #label>{{ i18n.ts._theme.code }}</template> </FormTextarea> <div class="_formBlock" style="display: flex; gap: var(--margin); flex-wrap: wrap;"> - <FormButton :disabled="installThemeCode == null" inline @click="() => preview(installThemeCode)"><i class="fas fa-eye"></i> {{ i18n.locale.preview }}</FormButton> - <FormButton :disabled="installThemeCode == null" primary inline @click="() => install(installThemeCode)"><i class="fas fa-check"></i> {{ i18n.locale.install }}</FormButton> + <FormButton :disabled="installThemeCode == null" inline @click="() => preview(installThemeCode)"><i class="fas fa-eye"></i> {{ i18n.ts.preview }}</FormButton> + <FormButton :disabled="installThemeCode == null" primary inline @click="() => install(installThemeCode)"><i class="fas fa-check"></i> {{ i18n.ts.install }}</FormButton> </div> </div> </template> @@ -32,21 +32,21 @@ function parseThemeCode(code: string) { } catch (e) { os.alert({ type: 'error', - text: i18n.locale._theme.invalid + text: i18n.ts._theme.invalid }); return false; } if (!validateTheme(theme)) { os.alert({ type: 'error', - text: i18n.locale._theme.invalid + text: i18n.ts._theme.invalid }); return false; } if (getThemes().some(t => t.id === theme.id)) { os.alert({ type: 'info', - text: i18n.locale._theme.alreadyInstalled + text: i18n.ts._theme.alreadyInstalled }); return false; } @@ -71,7 +71,7 @@ async function install(code: string): Promise<void> { defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale._theme.install, + title: i18n.ts._theme.install, icon: 'fas fa-download', bg: 'var(--bg)', }, diff --git a/packages/client/src/pages/settings/theme.vue b/packages/client/src/pages/settings/theme.vue index 658e36ec05..3e4ec1b2af 100644 --- a/packages/client/src/pages/settings/theme.vue +++ b/packages/client/src/pages/settings/theme.vue @@ -116,7 +116,7 @@ export default defineComponent({ setup(props, { emit }) { const INFO = { - title: i18n.locale.theme, + title: i18n.ts.theme, icon: 'fas fa-palette', bg: 'var(--bg)', }; @@ -184,7 +184,7 @@ export default defineComponent({ themesCount, wallpaper, setWallpaper(e) { - selectFile(e.currentTarget || e.target, null).then(file => { + selectFile(e.currentTarget ?? e.target, null).then(file => { wallpaper.value = file.url; }); }, diff --git a/packages/client/src/pages/signup-complete.vue b/packages/client/src/pages/signup-complete.vue index a10af1a4cc..344c9195f7 100644 --- a/packages/client/src/pages/signup-complete.vue +++ b/packages/client/src/pages/signup-complete.vue @@ -1,6 +1,6 @@ <template> <div> - {{ i18n.locale.processing }} + {{ i18n.ts.processing }} </div> </template> @@ -18,7 +18,7 @@ const props = defineProps<{ onMounted(async () => { await os.alert({ type: 'info', - text: i18n.t('clickToFinishEmailVerification', { ok: i18n.locale.gotIt }), + text: i18n.t('clickToFinishEmailVerification', { ok: i18n.ts.gotIt }), }); const res = await os.apiWithDialog('signup-pending', { code: props.code, @@ -28,7 +28,7 @@ onMounted(async () => { defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale.signup, + title: i18n.ts.signup, icon: 'fas fa-user', }, }); diff --git a/packages/client/src/pages/theme-editor.vue b/packages/client/src/pages/theme-editor.vue index 80b8c7806c..a53e23c1c5 100644 --- a/packages/client/src/pages/theme-editor.vue +++ b/packages/client/src/pages/theme-editor.vue @@ -2,7 +2,7 @@ <MkSpacer :content-max="800" :margin-min="16" :margin-max="32"> <div class="cwepdizn _formRoot"> <FormFolder :default-open="true" class="_formBlock"> - <template #label>{{ i18n.locale.backgroundColor }}</template> + <template #label>{{ i18n.ts.backgroundColor }}</template> <div class="cwepdizn-colors"> <div class="row"> <button v-for="color in bgColors.filter(x => x.kind === 'light')" :key="color.color" class="color _button" :class="{ active: theme.props.bg === color.color }" @click="setBgColor(color)"> @@ -18,7 +18,7 @@ </FormFolder> <FormFolder :default-open="true" class="_formBlock"> - <template #label>{{ i18n.locale.accentColor }}</template> + <template #label>{{ i18n.ts.accentColor }}</template> <div class="cwepdizn-colors"> <div class="row"> <button v-for="color in accentColors" :key="color" class="color rounded _button" :class="{ active: theme.props.accent === color }" @click="setAccentColor(color)"> @@ -29,7 +29,7 @@ </FormFolder> <FormFolder :default-open="true" class="_formBlock"> - <template #label>{{ i18n.locale.textColor }}</template> + <template #label>{{ i18n.ts.textColor }}</template> <div class="cwepdizn-colors"> <div class="row"> <button v-for="color in fgColors" :key="color" class="color char _button" :class="{ active: (theme.props.fg === color.forLight) || (theme.props.fg === color.forDark) }" @click="setFgColor(color)"> @@ -41,22 +41,22 @@ <FormFolder :default-open="false" class="_formBlock"> <template #icon><i class="fas fa-code"></i></template> - <template #label>{{ i18n.locale.editCode }}</template> + <template #label>{{ i18n.ts.editCode }}</template> <div class="_formRoot"> <FormTextarea v-model="themeCode" tall class="_formBlock"> - <template #label>{{ i18n.locale._theme.code }}</template> + <template #label>{{ i18n.ts._theme.code }}</template> </FormTextarea> - <FormButton primary class="_formBlock" @click="applyThemeCode">{{ i18n.locale.apply }}</FormButton> + <FormButton primary class="_formBlock" @click="applyThemeCode">{{ i18n.ts.apply }}</FormButton> </div> </FormFolder> <FormFolder :default-open="false" class="_formBlock"> - <template #label>{{ i18n.locale.addDescription }}</template> + <template #label>{{ i18n.ts.addDescription }}</template> <div class="_formRoot"> <FormTextarea v-model="description"> - <template #label>{{ i18n.locale._theme.description }}</template> + <template #label>{{ i18n.ts._theme.description }}</template> </FormTextarea> </div> </FormFolder> @@ -167,7 +167,7 @@ function applyThemeCode() { } catch (err) { os.alert({ type: 'error', - text: i18n.locale._theme.invalid, + text: i18n.ts._theme.invalid, }); return; } @@ -177,7 +177,7 @@ function applyThemeCode() { async function saveAs() { const { canceled, result: name } = await os.inputText({ - title: i18n.locale.name, + title: i18n.ts.name, allowEmpty: false, }); if (canceled) return; @@ -204,18 +204,18 @@ watch($$(theme), apply, { deep: true }); defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale.themeEditor, + title: i18n.ts.themeEditor, icon: 'fas fa-palette', bg: 'var(--bg)', actions: [{ asFullButton: true, icon: 'fas fa-eye', - text: i18n.locale.preview, + text: i18n.ts.preview, handler: showPreview, }, { asFullButton: true, icon: 'fas fa-check', - text: i18n.locale.saveAs, + text: i18n.ts.saveAs, handler: saveAs, }], }, diff --git a/packages/client/src/pages/timeline.vue b/packages/client/src/pages/timeline.vue index aabb953aec..b2266d22c3 100644 --- a/packages/client/src/pages/timeline.vue +++ b/packages/client/src/pages/timeline.vue @@ -64,7 +64,7 @@ async function chooseList(ev: MouseEvent): Promise<void> { text: list.name, to: `/timeline/list/${list.id}`, })); - os.popupMenu(items, ev.currentTarget || ev.target); + os.popupMenu(items, ev.currentTarget ?? ev.target); } async function chooseAntenna(ev: MouseEvent): Promise<void> { @@ -75,7 +75,7 @@ async function chooseAntenna(ev: MouseEvent): Promise<void> { indicate: antenna.hasUnreadNote, to: `/timeline/antenna/${antenna.id}`, })); - os.popupMenu(items, ev.currentTarget || ev.target); + os.popupMenu(items, ev.currentTarget ?? ev.target); } async function chooseChannel(ev: MouseEvent): Promise<void> { @@ -86,7 +86,7 @@ async function chooseChannel(ev: MouseEvent): Promise<void> { indicate: channel.hasUnreadNote, to: `/channels/${channel.id}`, })); - os.popupMenu(items, ev.currentTarget || ev.target); + os.popupMenu(items, ev.currentTarget ?? ev.target); } function saveSrc(): void { @@ -97,7 +97,7 @@ function saveSrc(): void { async function timetravel(): Promise<void> { const { canceled, result: date } = await os.inputDate({ - title: i18n.locale.date, + title: i18n.ts.date, }); if (canceled) return; @@ -110,47 +110,47 @@ function focus(): void { defineExpose({ [symbols.PAGE_INFO]: computed(() => ({ - title: i18n.locale.timeline, + title: i18n.ts.timeline, icon: src === 'local' ? 'fas fa-comments' : src === 'social' ? 'fas fa-share-alt' : src === 'global' ? 'fas fa-globe' : 'fas fa-home', bg: 'var(--bg)', actions: [{ icon: 'fas fa-list-ul', - text: i18n.locale.lists, + text: i18n.ts.lists, handler: chooseList, }, { icon: 'fas fa-satellite', - text: i18n.locale.antennas, + text: i18n.ts.antennas, handler: chooseAntenna, }, { icon: 'fas fa-satellite-dish', - text: i18n.locale.channel, + text: i18n.ts.channel, handler: chooseChannel, }, { icon: 'fas fa-calendar-alt', - text: i18n.locale.jumpToSpecifiedDate, + text: i18n.ts.jumpToSpecifiedDate, handler: timetravel, }], tabs: [{ active: src === 'home', - title: i18n.locale._timelines.home, + title: i18n.ts._timelines.home, icon: 'fas fa-home', iconOnly: true, onClick: () => { src = 'home'; saveSrc(); }, }, ...(isLocalTimelineAvailable ? [{ active: src === 'local', - title: i18n.locale._timelines.local, + title: i18n.ts._timelines.local, icon: 'fas fa-comments', iconOnly: true, onClick: () => { src = 'local'; saveSrc(); }, }, { active: src === 'social', - title: i18n.locale._timelines.social, + title: i18n.ts._timelines.social, icon: 'fas fa-share-alt', iconOnly: true, onClick: () => { src = 'social'; saveSrc(); }, }] : []), ...(isGlobalTimelineAvailable ? [{ active: src === 'global', - title: i18n.locale._timelines.global, + title: i18n.ts._timelines.global, icon: 'fas fa-globe', iconOnly: true, onClick: () => { src = 'global'; saveSrc(); }, diff --git a/packages/client/src/pages/user/index.vue b/packages/client/src/pages/user/index.vue index 599e24d81c..10a86243f9 100644 --- a/packages/client/src/pages/user/index.vue +++ b/packages/client/src/pages/user/index.vue @@ -264,7 +264,7 @@ export default defineComponent({ }, menu(ev) { - os.popupMenu(getUserMenu(this.user), ev.currentTarget || ev.target); + os.popupMenu(getUserMenu(this.user), ev.currentTarget ?? ev.target); }, parallaxLoop() { diff --git a/packages/client/src/pages/welcome.entrance.a.vue b/packages/client/src/pages/welcome.entrance.a.vue index efdc038b7e..47e1f12342 100644 --- a/packages/client/src/pages/welcome.entrance.a.vue +++ b/packages/client/src/pages/welcome.entrance.a.vue @@ -135,7 +135,7 @@ export default defineComponent({ action: () => { window.open(`https://misskey-hub.net/help.md`, '_blank'); } - }], ev.currentTarget || ev.target); + }], ev.currentTarget ?? ev.target); }, number diff --git a/packages/client/src/pages/welcome.entrance.b.vue b/packages/client/src/pages/welcome.entrance.b.vue index 93344dc9a8..053087fda0 100644 --- a/packages/client/src/pages/welcome.entrance.b.vue +++ b/packages/client/src/pages/welcome.entrance.b.vue @@ -119,7 +119,7 @@ export default defineComponent({ action: () => { window.open(`https://misskey-hub.net/help.md`, '_blank'); } - }], ev.currentTarget || ev.target); + }], ev.currentTarget ?? ev.target); }, number diff --git a/packages/client/src/pages/welcome.entrance.c.vue b/packages/client/src/pages/welcome.entrance.c.vue index 36b61647a6..6bf487e16e 100644 --- a/packages/client/src/pages/welcome.entrance.c.vue +++ b/packages/client/src/pages/welcome.entrance.c.vue @@ -139,7 +139,7 @@ export default defineComponent({ action: () => { window.open(`https://misskey-hub.net/help.md`, '_blank'); } - }], ev.currentTarget || ev.target); + }], ev.currentTarget ?? ev.target); }, number diff --git a/packages/client/src/scripts/get-note-menu.ts b/packages/client/src/scripts/get-note-menu.ts index 3634f39632..b19656d3cc 100644 --- a/packages/client/src/scripts/get-note-menu.ts +++ b/packages/client/src/scripts/get-note-menu.ts @@ -27,7 +27,7 @@ export function getNoteMenu(props: { function del(): void { os.confirm({ type: 'warning', - text: i18n.locale.noteDeleteConfirm, + text: i18n.ts.noteDeleteConfirm, }).then(({ canceled }) => { if (canceled) return; @@ -40,7 +40,7 @@ export function getNoteMenu(props: { function delEdit(): void { os.confirm({ type: 'warning', - text: i18n.locale.deleteAndEditConfirm, + text: i18n.ts.deleteAndEditConfirm, }).then(({ canceled }) => { if (canceled) return; @@ -87,7 +87,7 @@ export function getNoteMenu(props: { if (e.id === '72dab508-c64d-498f-8740-a8eec1ba385a') { os.alert({ type: 'error', - text: i18n.locale.pinLimitExceeded + text: i18n.ts.pinLimitExceeded }); } }); @@ -97,22 +97,22 @@ export function getNoteMenu(props: { const clips = await os.api('clips/list'); os.popupMenu([{ icon: 'fas fa-plus', - text: i18n.locale.createNew, + text: i18n.ts.createNew, action: async () => { - const { canceled, result } = await os.form(i18n.locale.createNewClip, { + const { canceled, result } = await os.form(i18n.ts.createNewClip, { name: { type: 'string', - label: i18n.locale.name + label: i18n.ts.name }, description: { type: 'string', required: false, multiline: true, - label: i18n.locale.description + label: i18n.ts.description }, isPublic: { type: 'boolean', - label: i18n.locale.public, + label: i18n.ts.public, default: false } }); @@ -133,7 +133,7 @@ export function getNoteMenu(props: { async function promote(): Promise<void> { const { canceled, result: days } = await os.inputNumber({ - title: i18n.locale.numberOfDays, + title: i18n.ts.numberOfDays, }); if (canceled) return; @@ -171,69 +171,69 @@ export function getNoteMenu(props: { menu = [{ icon: 'fas fa-copy', - text: i18n.locale.copyContent, + text: i18n.ts.copyContent, action: copyContent }, { icon: 'fas fa-link', - text: i18n.locale.copyLink, + text: i18n.ts.copyLink, action: copyLink }, (appearNote.url || appearNote.uri) ? { icon: 'fas fa-external-link-square-alt', - text: i18n.locale.showOnRemote, + text: i18n.ts.showOnRemote, action: () => { window.open(appearNote.url || appearNote.uri, '_blank'); } } : undefined, { icon: 'fas fa-share-alt', - text: i18n.locale.share, + text: i18n.ts.share, action: share }, instance.translatorAvailable ? { icon: 'fas fa-language', - text: i18n.locale.translate, + text: i18n.ts.translate, action: translate } : undefined, null, statePromise.then(state => state.isFavorited ? { icon: 'fas fa-star', - text: i18n.locale.unfavorite, + text: i18n.ts.unfavorite, action: () => toggleFavorite(false) } : { icon: 'fas fa-star', - text: i18n.locale.favorite, + text: i18n.ts.favorite, action: () => toggleFavorite(true) }), { icon: 'fas fa-paperclip', - text: i18n.locale.clip, + text: i18n.ts.clip, action: () => clip() }, (appearNote.userId != $i.id) ? statePromise.then(state => state.isWatching ? { icon: 'fas fa-eye-slash', - text: i18n.locale.unwatch, + text: i18n.ts.unwatch, action: () => toggleWatch(false) } : { icon: 'fas fa-eye', - text: i18n.locale.watch, + text: i18n.ts.watch, action: () => toggleWatch(true) }) : undefined, statePromise.then(state => state.isMutedThread ? { icon: 'fas fa-comment-slash', - text: i18n.locale.unmuteThread, + text: i18n.ts.unmuteThread, action: () => toggleThreadMute(false) } : { icon: 'fas fa-comment-slash', - text: i18n.locale.muteThread, + text: i18n.ts.muteThread, action: () => toggleThreadMute(true) }), appearNote.userId == $i.id ? ($i.pinnedNoteIds || []).includes(appearNote.id) ? { icon: 'fas fa-thumbtack', - text: i18n.locale.unpin, + text: i18n.ts.unpin, action: () => togglePin(false) } : { icon: 'fas fa-thumbtack', - text: i18n.locale.pin, + text: i18n.ts.pin, action: () => togglePin(true) } : undefined, /* @@ -241,7 +241,7 @@ export function getNoteMenu(props: { null, { icon: 'fas fa-bullhorn', - text: i18n.locale.promote, + text: i18n.ts.promote, action: promote }] : [] @@ -250,7 +250,7 @@ export function getNoteMenu(props: { null, { icon: 'fas fa-exclamation-circle', - text: i18n.locale.reportAbuse, + text: i18n.ts.reportAbuse, action: () => { const u = appearNote.url || appearNote.uri || `${url}/notes/${appearNote.id}`; os.popup(import('@/components/abuse-report-window.vue'), { @@ -265,12 +265,12 @@ export function getNoteMenu(props: { null, appearNote.userId == $i.id ? { icon: 'fas fa-edit', - text: i18n.locale.deleteAndEdit, + text: i18n.ts.deleteAndEdit, action: delEdit } : undefined, { icon: 'fas fa-trash-alt', - text: i18n.locale.delete, + text: i18n.ts.delete, danger: true, action: del }] @@ -280,15 +280,15 @@ export function getNoteMenu(props: { } else { menu = [{ icon: 'fas fa-copy', - text: i18n.locale.copyContent, + text: i18n.ts.copyContent, action: copyContent }, { icon: 'fas fa-link', - text: i18n.locale.copyLink, + text: i18n.ts.copyLink, action: copyLink }, (appearNote.url || appearNote.uri) ? { icon: 'fas fa-external-link-square-alt', - text: i18n.locale.showOnRemote, + text: i18n.ts.showOnRemote, action: () => { window.open(appearNote.url || appearNote.uri, '_blank'); } diff --git a/packages/client/src/scripts/get-note-summary.ts b/packages/client/src/scripts/get-note-summary.ts index bd394279cb..54b8d109d6 100644 --- a/packages/client/src/scripts/get-note-summary.ts +++ b/packages/client/src/scripts/get-note-summary.ts @@ -7,11 +7,11 @@ import { i18n } from '@/i18n'; */ export const getNoteSummary = (note: misskey.entities.Note): string => { if (note.deletedAt) { - return `(${i18n.locale.deletedNote})`; + return `(${i18n.ts.deletedNote})`; } if (note.isHidden) { - return `(${i18n.locale.invisibleNote})`; + return `(${i18n.ts.invisibleNote})`; } let summary = ''; @@ -30,7 +30,7 @@ export const getNoteSummary = (note: misskey.entities.Note): string => { // 投票が添付されているとき if (note.poll) { - summary += ` (${i18n.locale.poll})`; + summary += ` (${i18n.ts.poll})`; } // 返信のとき diff --git a/packages/client/src/scripts/get-user-menu.ts b/packages/client/src/scripts/get-user-menu.ts index 7b910a0083..6d1f25a942 100644 --- a/packages/client/src/scripts/get-user-menu.ts +++ b/packages/client/src/scripts/get-user-menu.ts @@ -11,12 +11,12 @@ export function getUserMenu(user) { const meId = $i ? $i.id : null; async function pushList() { - const t = i18n.locale.selectList; // なぜか後で参照すると null になるので最初にメモリに確保しておく + const t = i18n.ts.selectList; // なぜか後で参照すると null になるので最初にメモリに確保しておく const lists = await os.api('users/lists/list'); if (lists.length === 0) { os.alert({ type: 'error', - text: i18n.locale.youHaveNoLists + text: i18n.ts.youHaveNoLists }); return; } @@ -38,12 +38,12 @@ export function getUserMenu(user) { if (groups.length === 0) { os.alert({ type: 'error', - text: i18n.locale.youHaveNoGroups + text: i18n.ts.youHaveNoGroups }); return; } const { canceled, result: groupId } = await os.select({ - title: i18n.locale.group, + title: i18n.ts.group, items: groups.map(group => ({ value: group.id, text: group.name })) @@ -64,7 +64,7 @@ export function getUserMenu(user) { } async function toggleBlock() { - if (!await getConfirmed(user.isBlocking ? i18n.locale.unblockConfirm : i18n.locale.blockConfirm)) return; + if (!await getConfirmed(user.isBlocking ? i18n.ts.unblockConfirm : i18n.ts.blockConfirm)) return; os.apiWithDialog(user.isBlocking ? 'blocking/delete' : 'blocking/create', { userId: user.id @@ -119,70 +119,70 @@ export function getUserMenu(user) { let menu = [{ icon: 'fas fa-at', - text: i18n.locale.copyUsername, + text: i18n.ts.copyUsername, action: () => { copyToClipboard(`@${user.username}@${user.host || host}`); } }, { icon: 'fas fa-info-circle', - text: i18n.locale.info, + text: i18n.ts.info, action: () => { os.pageWindow(`/user-info/${user.id}`); } }, { icon: 'fas fa-envelope', - text: i18n.locale.sendMessage, + text: i18n.ts.sendMessage, action: () => { os.post({ specified: user }); } }, meId != user.id ? { type: 'link', icon: 'fas fa-comments', - text: i18n.locale.startMessaging, + text: i18n.ts.startMessaging, to: '/my/messaging/' + Acct.toString(user), } : undefined, null, { icon: 'fas fa-list-ul', - text: i18n.locale.addToList, + text: i18n.ts.addToList, action: pushList }, meId != user.id ? { icon: 'fas fa-users', - text: i18n.locale.inviteToGroup, + text: i18n.ts.inviteToGroup, action: inviteGroup } : undefined] as any; if ($i && meId != user.id) { menu = menu.concat([null, { icon: user.isMuted ? 'fas fa-eye' : 'fas fa-eye-slash', - text: user.isMuted ? i18n.locale.unmute : i18n.locale.mute, + text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute, action: toggleMute }, { icon: 'fas fa-ban', - text: user.isBlocking ? i18n.locale.unblock : i18n.locale.block, + text: user.isBlocking ? i18n.ts.unblock : i18n.ts.block, action: toggleBlock }]); if (user.isFollowed) { menu = menu.concat([{ icon: 'fas fa-unlink', - text: i18n.locale.breakFollow, + text: i18n.ts.breakFollow, action: invalidateFollow }]); } menu = menu.concat([null, { icon: 'fas fa-exclamation-circle', - text: i18n.locale.reportAbuse, + text: i18n.ts.reportAbuse, action: reportAbuse }]); if (iAmModerator) { menu = menu.concat([null, { icon: 'fas fa-microphone-slash', - text: user.isSilenced ? i18n.locale.unsilence : i18n.locale.silence, + text: user.isSilenced ? i18n.ts.unsilence : i18n.ts.silence, action: toggleSilence }, { icon: 'fas fa-snowflake', - text: user.isSuspended ? i18n.locale.unsuspend : i18n.locale.suspend, + text: user.isSuspended ? i18n.ts.unsuspend : i18n.ts.suspend, action: toggleSuspend }]); } @@ -191,7 +191,7 @@ export function getUserMenu(user) { if ($i && meId === user.id) { menu = menu.concat([null, { icon: 'fas fa-pencil-alt', - text: i18n.locale.editProfile, + text: i18n.ts.editProfile, action: () => { router.push('/settings/profile'); } diff --git a/packages/client/src/scripts/i18n.ts b/packages/client/src/scripts/i18n.ts index 4fa398763a..3fe88e5514 100644 --- a/packages/client/src/scripts/i18n.ts +++ b/packages/client/src/scripts/i18n.ts @@ -1,8 +1,8 @@ export class I18n<T extends Record<string, any>> { - public locale: T; + public ts: T; constructor(locale: T) { - this.locale = locale; + this.ts = locale; //#region BIND this.t = this.t.bind(this); @@ -11,9 +11,9 @@ export class I18n<T extends Record<string, any>> { // string にしているのは、ドット区切りでのパス指定を許可するため // なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも - public t(key: string, args?: Record<string, any>): string { + public t(key: string, args?: Record<string, string>): string { try { - let str = key.split('.').reduce((o, i) => o[i], this.locale) as string; + let str = key.split('.').reduce((o, i) => o[i], this.ts) as unknown as string; if (args) { for (const [k, v] of Object.entries(args)) { @@ -21,7 +21,7 @@ export class I18n<T extends Record<string, any>> { } } return str; - } catch (e) { + } catch (err) { console.warn(`missing localization '${key}'`); return key; } diff --git a/packages/client/src/scripts/lookup-user.ts b/packages/client/src/scripts/lookup-user.ts index 64874f86f6..8de5c84ce8 100644 --- a/packages/client/src/scripts/lookup-user.ts +++ b/packages/client/src/scripts/lookup-user.ts @@ -4,7 +4,7 @@ import * as os from '@/os'; export async function lookupUser() { const { canceled, result } = await os.inputText({ - title: i18n.locale.usernameOrUserId, + title: i18n.ts.usernameOrUserId, }); if (canceled) return; @@ -19,7 +19,7 @@ export async function lookupUser() { if (_notFound) { os.alert({ type: 'error', - text: i18n.locale.noSuchUser + text: i18n.ts.noSuchUser }); } else { _notFound = true; diff --git a/packages/client/src/scripts/please-login.ts b/packages/client/src/scripts/please-login.ts index fe3919e4c7..aeaafa124b 100644 --- a/packages/client/src/scripts/please-login.ts +++ b/packages/client/src/scripts/please-login.ts @@ -6,7 +6,7 @@ export function pleaseLogin() { if ($i) return; alert({ - title: i18n.locale.signinRequired, + title: i18n.ts.signinRequired, text: null }); diff --git a/packages/client/src/scripts/search.ts b/packages/client/src/scripts/search.ts index a070b1121c..0aedee9c98 100644 --- a/packages/client/src/scripts/search.ts +++ b/packages/client/src/scripts/search.ts @@ -4,7 +4,7 @@ import { router } from '@/router'; export async function search() { const { canceled, result: query } = await os.inputText({ - title: i18n.locale.search, + title: i18n.ts.search, }); if (canceled || query == null || query === '') return; @@ -46,7 +46,7 @@ export async function search() { uri: q }); - os.promiseDialog(promise, null, null, i18n.locale.fetchingAsApObject); + os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject); const res = await promise; diff --git a/packages/client/src/scripts/select-file.ts b/packages/client/src/scripts/select-file.ts index 6bb3f8bf8a..23df4edf54 100644 --- a/packages/client/src/scripts/select-file.ts +++ b/packages/client/src/scripts/select-file.ts @@ -1,3 +1,4 @@ +import { ref } from 'vue'; import * as os from '@/os'; import { stream } from '@/stream'; import { i18n } from '@/i18n'; @@ -6,12 +7,14 @@ import { DriveFile } from 'misskey-js/built/entities'; function select(src: any, label: string | null, multiple: boolean): Promise<DriveFile | DriveFile[]> { return new Promise((res, rej) => { + const keepOriginal = ref(defaultStore.state.keepOriginalUploading); + const chooseFileFromPc = () => { const input = document.createElement('input'); input.type = 'file'; input.multiple = multiple; input.onchange = () => { - const promises = Array.from(input.files).map(file => os.upload(file, defaultStore.state.uploadFolder)); + const promises = Array.from(input.files).map(file => os.upload(file, defaultStore.state.uploadFolder, undefined, keepOriginal.value)); Promise.all(promises).then(driveFiles => { res(multiple ? driveFiles : driveFiles[0]); @@ -41,9 +44,9 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Driv const chooseFileFromUrl = () => { os.inputText({ - title: i18n.locale.uploadFromUrl, + title: i18n.ts.uploadFromUrl, type: 'url', - placeholder: i18n.locale.uploadFromUrlDescription + placeholder: i18n.ts.uploadFromUrlDescription }).then(({ canceled, result: url }) => { if (canceled) return; @@ -64,8 +67,8 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Driv }); os.alert({ - title: i18n.locale.uploadFromUrlRequested, - text: i18n.locale.uploadFromUrlMayTakeTime + title: i18n.ts.uploadFromUrlRequested, + text: i18n.ts.uploadFromUrlMayTakeTime }); }); }; @@ -74,15 +77,19 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Driv text: label, type: 'label' } : undefined, { - text: i18n.locale.upload, + type: 'switch', + text: i18n.ts.keepOriginalUploading, + ref: keepOriginal + }, { + text: i18n.ts.upload, icon: 'fas fa-upload', action: chooseFileFromPc }, { - text: i18n.locale.fromDrive, + text: i18n.ts.fromDrive, icon: 'fas fa-cloud', action: chooseFileFromDrive }, { - text: i18n.locale.fromUrl, + text: i18n.ts.fromUrl, icon: 'fas fa-link', action: chooseFileFromUrl }], src); diff --git a/packages/client/src/scripts/show-suspended-dialog.ts b/packages/client/src/scripts/show-suspended-dialog.ts index dcbb66933c..acfbc60e92 100644 --- a/packages/client/src/scripts/show-suspended-dialog.ts +++ b/packages/client/src/scripts/show-suspended-dialog.ts @@ -4,7 +4,7 @@ import { i18n } from '@/i18n'; export function showSuspendedDialog() { return os.alert({ type: 'error', - title: i18n.locale.yourAccountSuspendedTitle, - text: i18n.locale.yourAccountSuspendedDescription + title: i18n.ts.yourAccountSuspendedTitle, + text: i18n.ts.yourAccountSuspendedDescription }); } diff --git a/packages/client/src/scripts/use-leave-guard.ts b/packages/client/src/scripts/use-leave-guard.ts index 3984256251..33eea6b522 100644 --- a/packages/client/src/scripts/use-leave-guard.ts +++ b/packages/client/src/scripts/use-leave-guard.ts @@ -12,7 +12,7 @@ export function useLeaveGuard(enabled: Ref<boolean>) { const { canceled } = await os.confirm({ type: 'warning', - text: i18n.locale.leaveConfirm, + text: i18n.ts.leaveConfirm, }); return canceled; @@ -23,7 +23,7 @@ export function useLeaveGuard(enabled: Ref<boolean>) { const { canceled } = await os.confirm({ type: 'warning', - text: i18n.locale.leaveConfirm, + text: i18n.ts.leaveConfirm, }); return !canceled; diff --git a/packages/client/src/scripts/use-note-capture.ts b/packages/client/src/scripts/use-note-capture.ts index bb00e464e3..b7cf99d5e1 100644 --- a/packages/client/src/scripts/use-note-capture.ts +++ b/packages/client/src/scripts/use-note-capture.ts @@ -19,51 +19,41 @@ export function useNoteCapture(props: { case 'reacted': { const reaction = body.reaction; - const updated = JSON.parse(JSON.stringify(appearNote.value)); - if (body.emoji) { const emojis = appearNote.value.emojis || []; if (!emojis.includes(body.emoji)) { - updated.emojis = [...emojis, body.emoji]; + appearNote.value.emojis = [...emojis, body.emoji]; } } // TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる const currentCount = (appearNote.value.reactions || {})[reaction] || 0; - updated.reactions[reaction] = currentCount + 1; + appearNote.value.reactions[reaction] = currentCount + 1; if ($i && (body.userId === $i.id)) { - updated.myReaction = reaction; + appearNote.value.myReaction = reaction; } - - appearNote.value = updated; break; } case 'unreacted': { const reaction = body.reaction; - const updated = JSON.parse(JSON.stringify(appearNote.value)); - // TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる const currentCount = (appearNote.value.reactions || {})[reaction] || 0; - updated.reactions[reaction] = Math.max(0, currentCount - 1); + appearNote.value.reactions[reaction] = Math.max(0, currentCount - 1); if ($i && (body.userId === $i.id)) { - updated.myReaction = null; + appearNote.value.myReaction = null; } - - appearNote.value = updated; break; } case 'pollVoted': { const choice = body.choice; - const updated = JSON.parse(JSON.stringify(appearNote.value)); - const choices = [...appearNote.value.poll.choices]; choices[choice] = { ...choices[choice], @@ -73,16 +63,12 @@ export function useNoteCapture(props: { } : {}) }; - updated.poll.choices = choices; - - appearNote.value = updated; + appearNote.value.poll.choices = choices; break; } case 'deleted': { - const updated = JSON.parse(JSON.stringify(appearNote.value)); - updated.value = true; - appearNote.value = updated; + appearNote.value.deletedAt = new Date(); break; } } diff --git a/packages/client/src/store.ts b/packages/client/src/store.ts index cd358d29d0..b80fc8bbe3 100644 --- a/packages/client/src/store.ts +++ b/packages/client/src/store.ts @@ -43,6 +43,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'account', default: 'yyyy-MM-dd HH-mm-ss [{{number}}]' }, + keepOriginalUploading: { + where: 'account', + default: false + }, memo: { where: 'account', default: null diff --git a/packages/client/src/types/menu.ts b/packages/client/src/types/menu.ts new file mode 100644 index 0000000000..ed67e6ab88 --- /dev/null +++ b/packages/client/src/types/menu.ts @@ -0,0 +1,20 @@ +import * as Misskey from 'misskey-js'; +import { Ref } from 'vue'; + +export type MenuAction = (ev: MouseEvent) => void; + +export type MenuDivider = null; +export type MenuNull = undefined; +export type MenuLabel = { type: 'label', text: string }; +export type MenuLink = { type: 'link', to: string, text: string, icon?: string, indicate?: boolean, avatar?: Misskey.entities.User }; +export type MenuA = { type: 'a', href: string, target?: string, download?: string, text: string, icon?: string, indicate?: boolean }; +export type MenuUser = { type: 'user', user: Misskey.entities.User, active?: boolean, indicate?: boolean, action: MenuAction }; +export type MenuSwitch = { type: 'switch', ref: Ref<boolean>, text: string, disabled?: boolean }; +export type MenuButton = { type?: 'button', text: string, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean, avatar?: Misskey.entities.User; action: MenuAction }; + +export type MenuPending = { type: 'pending' }; + +type OuterMenuItem = MenuDivider | MenuNull | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton; +type OuterPromiseMenuItem = Promise<MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton>; +export type MenuItem = OuterMenuItem | OuterPromiseMenuItem; +export type InnerMenuItem = MenuDivider | MenuPending | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton; diff --git a/packages/client/src/ui/classic.side.vue b/packages/client/src/ui/classic.side.vue index f816834141..6c2329194e 100644 --- a/packages/client/src/ui/classic.side.vue +++ b/packages/client/src/ui/classic.side.vue @@ -4,7 +4,7 @@ <header class="header" @contextmenu.prevent.stop="onContextmenu"> <button v-if="history.length > 0" class="_button" @click="back()"><i class="fas fa-chevron-left"></i></button> <button v-else class="_button" style="pointer-events: none;"><!-- マージンのバランスを取るためのダミー --></button> - <span class="title">{{ pageInfo.title }}</span> + <span class="title" v-text="pageInfo?.title" /> <button class="_button" @click="close()"><i class="fas fa-times"></i></button> </header> <MkHeader class="pageHeader" :info="pageInfo"/> @@ -13,99 +13,89 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { provide } from 'vue'; import * as os from '@/os'; import copyToClipboard from '@/scripts/copy-to-clipboard'; -import { resolve } from '@/router'; -import { url } from '@/config'; +import { resolve, router } from '@/router'; +import { url as root } from '@/config'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -export default defineComponent({ - provide() { - return { - navHook: (path) => { - this.navigate(path); - } - }; - }, +provide('navHook', navigate); - data() { - return { - path: null, - component: null, - props: {}, - pageInfo: null, - history: [], - }; - }, +let path: string | null = $ref(null); +let component: ReturnType<typeof resolve>['component'] | null = $ref(null); +let props: any | null = $ref(null); +let pageInfo: any | null = $ref(null); +let history: string[] = $ref([]); - computed: { - url(): string { - return url + this.path; - } - }, +let url = $computed(() => `${root}${path}`); - methods: { - changePage(page) { - if (page == null) return; - if (page[symbols.PAGE_INFO]) { - this.pageInfo = page[symbols.PAGE_INFO]; - } - }, +function changePage(page) { + if (page == null) return; + if (page[symbols.PAGE_INFO]) { + pageInfo = page[symbols.PAGE_INFO]; + } +} - navigate(path, record = true) { - if (record && this.path) this.history.push(this.path); - this.path = path; - const { component, props } = resolve(path); - this.component = component; - this.props = props; - }, +function navigate(_path: string, record = true) { + if (record && path) history.push($$(path).value); + path = _path; + const resolved = resolve(path); + component = resolved.component; + props = resolved.props; +} - back() { - this.navigate(this.history.pop(), false); - }, +function back() { + const prev = history.pop(); + if (prev) navigate(prev, false); +} - close() { - this.path = null; - this.component = null; - this.props = {}; - }, +function close() { + path = null; + component = null; + props = {}; +} - onContextmenu(ev: MouseEvent) { - os.contextMenu([{ - type: 'label', - text: this.path, - }, { - icon: 'fas fa-expand-alt', - text: this.$ts.showInPage, - action: () => { - this.$router.push(this.path); - this.close(); - } - }, { - icon: 'fas fa-window-maximize', - text: this.$ts.openInWindow, - action: () => { - os.pageWindow(this.path); - this.close(); - } - }, null, { - icon: 'fas fa-external-link-alt', - text: this.$ts.openInNewTab, - action: () => { - window.open(this.url, '_blank'); - this.close(); - } - }, { - icon: 'fas fa-link', - text: this.$ts.copyLink, - action: () => { - copyToClipboard(this.url); - } - }], ev); +function onContextmenu(ev: MouseEvent) { + os.contextMenu([{ + type: 'label', + text: path || '', + }, { + icon: 'fas fa-expand-alt', + text: i18n.ts.showInPage, + action: () => { + if (path) router.push(path); + close(); } - } + }, { + icon: 'fas fa-window-maximize', + text: i18n.ts.openInWindow, + action: () => { + if (path) os.pageWindow(path); + close(); + } + }, null, { + icon: 'fas fa-external-link-alt', + text: i18n.ts.openInNewTab, + action: () => { + window.open(url, '_blank'); + close(); + } + }, { + icon: 'fas fa-link', + text: i18n.ts.copyLink, + action: () => { + copyToClipboard(url); + } + }], ev); +} + +defineExpose({ + navigate, + back, + close, }); </script> diff --git a/packages/client/src/ui/deck.vue b/packages/client/src/ui/deck.vue index 51a4853e9d..9accc34a88 100644 --- a/packages/client/src/ui/deck.vue +++ b/packages/client/src/ui/deck.vue @@ -104,7 +104,7 @@ export default defineComponent({ ]; const { canceled, result: column } = await os.select({ - title: i18n.locale._deck.addColumn, + title: i18n.ts._deck.addColumn, items: columns.map(column => ({ value: column, text: i18n.t('_deck._columns.' + column) })) @@ -121,7 +121,7 @@ export default defineComponent({ const onContextmenu = (ev) => { os.contextMenu([{ - text: i18n.locale._deck.addColumn, + text: i18n.ts._deck.addColumn, icon: null, action: addColumn }], ev); diff --git a/packages/client/src/ui/deck/deck-store.ts b/packages/client/src/ui/deck/deck-store.ts index 6b6b02f3f9..66db5e83ed 100644 --- a/packages/client/src/ui/deck/deck-store.ts +++ b/packages/client/src/ui/deck/deck-store.ts @@ -77,12 +77,12 @@ export const loadDeck = async () => { deckStore.set('columns', [{ id: 'a', type: 'main', - name: i18n.locale._deck._columns.main, + name: i18n.ts._deck._columns.main, width: 350, }, { id: 'b', type: 'notifications', - name: i18n.locale._deck._columns.notifications, + name: i18n.ts._deck._columns.notifications, width: 330, }]); deckStore.set('layout', [['a'], ['b']]); diff --git a/packages/client/src/ui/universal.vue b/packages/client/src/ui/universal.vue index 16cc9a4f06..b0dfc5aadc 100644 --- a/packages/client/src/ui/universal.vue +++ b/packages/client/src/ui/universal.vue @@ -20,7 +20,7 @@ </main> </div> - <XSideView v-if="isDesktop" ref="side" class="side"/> + <XSideView v-if="isDesktop" ref="sideEl" class="side"/> <div v-if="isDesktop" ref="widgetsEl" class="widgets"> <XWidgets @mounted="attachSticky"/> @@ -31,9 +31,9 @@ <div v-if="isMobile" class="buttons"> <button class="button nav _button" @click="drawerMenuShowing = true"><i class="fas fa-bars"></i><span v-if="menuIndicated" class="indicator"><i class="fas fa-circle"></i></span></button> <button class="button home _button" @click="$route.name === 'index' ? top() : $router.push('/')"><i class="fas fa-home"></i></button> - <button class="button notifications _button" @click="$router.push('/my/notifications')"><i class="fas fa-bell"></i><span v-if="$i.hasUnreadNotification" class="indicator"><i class="fas fa-circle"></i></span></button> + <button class="button notifications _button" @click="$router.push('/my/notifications')"><i class="fas fa-bell"></i><span v-if="$i?.hasUnreadNotification" class="indicator"><i class="fas fa-circle"></i></span></button> <button class="button widget _button" @click="widgetsShowing = true"><i class="fas fa-layer-group"></i></button> - <button class="button post _button" @click="post()"><i class="fas fa-pencil-alt"></i></button> + <button class="button post _button" @click="os.post()"><i class="fas fa-pencil-alt"></i></button> </div> <transition :name="$store.state.animation ? 'menuDrawer-back' : ''"> @@ -64,155 +64,133 @@ </div> </template> -<script lang="ts"> -import { defineComponent, defineAsyncComponent, provide, onMounted, computed, ref, watch } from 'vue'; +<script lang="ts" setup> +import { defineAsyncComponent, provide, onMounted, computed, ref, watch } from 'vue'; import { instanceName } from '@/config'; import { StickySidebar } from '@/scripts/sticky-sidebar'; -import XSidebar from '@/ui/_common_/sidebar.vue'; import XDrawerMenu from '@/ui/_common_/sidebar-for-mobile.vue'; import XCommon from './_common_/common.vue'; import XSideView from './classic.side.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; import { defaultStore } from '@/store'; -import * as EventEmitter from 'eventemitter3'; import { menuDef } from '@/menu'; import { useRoute } from 'vue-router'; import { i18n } from '@/i18n'; +import { $i } from '@/account'; +const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue')); +const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/sidebar.vue')); const DESKTOP_THRESHOLD = 1100; const MOBILE_THRESHOLD = 500; -export default defineComponent({ - components: { - XCommon, - XSidebar, - XDrawerMenu, - XWidgets: defineAsyncComponent(() => import('./universal.widgets.vue')), - XSideView, // NOTE: dynamic importするとAsyncComponentWrapperが間に入るせいでref取得できなくて面倒になる - }, +const isDesktop = ref(window.innerWidth >= DESKTOP_THRESHOLD); +const isMobile = ref(window.innerWidth <= MOBILE_THRESHOLD); +window.addEventListener('resize', () => { + isMobile.value = window.innerWidth <= MOBILE_THRESHOLD; +}); - setup() { - const isDesktop = ref(window.innerWidth >= DESKTOP_THRESHOLD); - const isMobile = ref(window.innerWidth <= MOBILE_THRESHOLD); - window.addEventListener('resize', () => { - isMobile.value = window.innerWidth <= MOBILE_THRESHOLD; - }); +const pageInfo = ref(); +const widgetsEl = $ref<HTMLElement>(); +const widgetsShowing = ref(false); - const pageInfo = ref(); - const widgetsEl = ref<HTMLElement>(); - const widgetsShowing = ref(false); +let sideEl = $ref<InstanceType<typeof XSideView>>(); - const sideViewController = new EventEmitter(); +provide('sideViewHook', isDesktop.value ? (url) => { + sideEl.navigate(url); +} : null); - provide('sideViewHook', isDesktop.value ? (url) => { - sideViewController.emit('navigate', url); - } : null); +const menuIndicated = computed(() => { + for (const def in menuDef) { + if (def === 'notifications') continue; // 通知は下にボタンとして表示されてるから + if (menuDef[def].indicated) return true; + } + return false; +}); - const menuIndicated = computed(() => { - for (const def in menuDef) { - if (def === 'notifications') continue; // 通知は下にボタンとして表示されてるから - if (menuDef[def].indicated) return true; - } - return false; - }); +const drawerMenuShowing = ref(false); - const drawerMenuShowing = ref(false); +const route = useRoute(); +watch(route, () => { + drawerMenuShowing.value = false; +}); - const route = useRoute(); - watch(route, () => { - drawerMenuShowing.value = false; - }); +document.documentElement.style.overflowY = 'scroll'; - document.documentElement.style.overflowY = 'scroll'; +if (defaultStore.state.widgets.length === 0) { + defaultStore.set('widgets', [{ + name: 'calendar', + id: 'a', place: 'right', data: {} + }, { + name: 'notifications', + id: 'b', place: 'right', data: {} + }, { + name: 'trends', + id: 'c', place: 'right', data: {} + }]); +} - if (defaultStore.state.widgets.length === 0) { - defaultStore.set('widgets', [{ - name: 'calendar', - id: 'a', place: 'right', data: {} - }, { - name: 'notifications', - id: 'b', place: 'right', data: {} - }, { - name: 'trends', - id: 'c', place: 'right', data: {} - }]); - } +onMounted(() => { + if (!isDesktop.value) { + window.addEventListener('resize', () => { + if (window.innerWidth >= DESKTOP_THRESHOLD) isDesktop.value = true; + }, { passive: true }); + } +}); - onMounted(() => { - if (!isDesktop.value) { - window.addEventListener('resize', () => { - if (window.innerWidth >= DESKTOP_THRESHOLD) isDesktop.value = true; - }, { passive: true }); - } - }); +const changePage = (page) => { + if (page == null) return; + if (page[symbols.PAGE_INFO]) { + pageInfo.value = page[symbols.PAGE_INFO]; + document.title = `${pageInfo.value.title} | ${instanceName}`; + } +}; - const changePage = (page) => { - if (page == null) return; - if (page[symbols.PAGE_INFO]) { - pageInfo.value = page[symbols.PAGE_INFO]; - document.title = `${pageInfo.value.title} | ${instanceName}`; - } - }; +const onContextmenu = (ev) => { + const isLink = (el: HTMLElement) => { + if (el.tagName === 'A') return true; + if (el.parentElement) { + return isLink(el.parentElement); + } + }; + if (isLink(ev.target)) return; + if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return; + if (window.getSelection()?.toString() !== '') return; + const path = route.path; + os.contextMenu([{ + type: 'label', + text: path, + }, { + icon: 'fas fa-columns', + text: i18n.ts.openInSideView, + action: () => { + sideEl.navigate(path); + } + }, { + icon: 'fas fa-window-maximize', + text: i18n.ts.openInWindow, + action: () => { + os.pageWindow(path); + } + }], ev); +}; - const onContextmenu = (ev) => { - const isLink = (el: HTMLElement) => { - if (el.tagName === 'A') return true; - if (el.parentElement) { - return isLink(el.parentElement); - } - }; - if (isLink(ev.target)) return; - if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return; - if (window.getSelection().toString() !== '') return; - const path = route.path; - os.contextMenu([{ - type: 'label', - text: path, - }, { - icon: 'fas fa-columns', - text: i18n.locale.openInSideView, - action: () => { - this.$refs.side.navigate(path); - } - }, { - icon: 'fas fa-window-maximize', - text: i18n.locale.openInWindow, - action: () => { - os.pageWindow(path); - } - }], ev); - }; +const attachSticky = (el) => { + const sticky = new StickySidebar(widgetsEl); + window.addEventListener('scroll', () => { + sticky.calc(window.scrollY); + }, { passive: true }); +}; + +function top() { + window.scroll({ top: 0, behavior: 'smooth' }); +} - const attachSticky = (el) => { - const sticky = new StickySidebar(widgetsEl.value); - window.addEventListener('scroll', () => { - sticky.calc(window.scrollY); - }, { passive: true }); - }; +function onTransition() { + if (window._scroll) window._scroll(); +} - return { - pageInfo, - isDesktop, - isMobile, - widgetsEl, - widgetsShowing, - drawerMenuShowing, - menuIndicated, - wallpaper: localStorage.getItem('wallpaper') != null, - changePage, - top: () => { - window.scroll({ top: 0, behavior: 'smooth' }); - }, - onTransition: () => { - if (window._scroll) window._scroll(); - }, - post: os.post, - onContextmenu, - attachSticky, - }; - }, -}); +const wallpaper = localStorage.getItem('wallpaper') != null; </script> <style lang="scss" scoped> diff --git a/packages/client/src/widgets/calendar.vue b/packages/client/src/widgets/calendar.vue index b0e3edcb12..c6a69b3fb8 100644 --- a/packages/client/src/widgets/calendar.vue +++ b/packages/client/src/widgets/calendar.vue @@ -79,13 +79,13 @@ const tick = () => { month.value = nm + 1; day.value = nd; weekDay.value = [ - i18n.locale._weekday.sunday, - i18n.locale._weekday.monday, - i18n.locale._weekday.tuesday, - i18n.locale._weekday.wednesday, - i18n.locale._weekday.thursday, - i18n.locale._weekday.friday, - i18n.locale._weekday.saturday + i18n.ts._weekday.sunday, + i18n.ts._weekday.monday, + i18n.ts._weekday.tuesday, + i18n.ts._weekday.wednesday, + i18n.ts._weekday.thursday, + i18n.ts._weekday.friday, + i18n.ts._weekday.saturday ][now.getDay()]; const dayNumer = now.getTime() - new Date(ny, nm, nd).getTime(); diff --git a/packages/client/src/widgets/federation.vue b/packages/client/src/widgets/federation.vue index 4c43117e48..5f1131dce1 100644 --- a/packages/client/src/widgets/federation.vue +++ b/packages/client/src/widgets/federation.vue @@ -54,13 +54,13 @@ const charts = ref([]); const fetching = ref(true); const fetch = async () => { - const instances = await os.api('federation/instances', { + const fetchedInstances = await os.api('federation/instances', { sort: '+lastCommunicatedAt', limit: 5 }); - const charts = await Promise.all(instances.map(i => os.api('charts/instance', { host: i.host, limit: 16, span: 'hour' }))); - instances.value = instances; - charts.value = charts; + const fetchedCharts = await Promise.all(fetchedInstances.map(i => os.api('charts/instance', { host: i.host, limit: 16, span: 'hour' }))); + instances.value = fetchedInstances; + charts.value = fetchedCharts; fetching.value = false; }; diff --git a/packages/client/src/widgets/timeline.vue b/packages/client/src/widgets/timeline.vue index fa700cc8ee..34e3b20e36 100644 --- a/packages/client/src/widgets/timeline.vue +++ b/packages/client/src/widgets/timeline.vue @@ -101,22 +101,22 @@ const choose = async (ev) => { } })); os.popupMenu([{ - text: i18n.locale._timelines.home, + text: i18n.ts._timelines.home, icon: 'fas fa-home', action: () => { setSrc('home') } }, { - text: i18n.locale._timelines.local, + text: i18n.ts._timelines.local, icon: 'fas fa-comments', action: () => { setSrc('local') } }, { - text: i18n.locale._timelines.social, + text: i18n.ts._timelines.social, icon: 'fas fa-share-alt', action: () => { setSrc('social') } }, { - text: i18n.locale._timelines.global, + text: i18n.ts._timelines.global, icon: 'fas fa-globe', action: () => { setSrc('global') } - }, antennaItems.length > 0 ? null : undefined, ...antennaItems, listItems.length > 0 ? null : undefined, ...listItems], ev.currentTarget || ev.target).then(() => { + }, antennaItems.length > 0 ? null : undefined, ...antennaItems, listItems.length > 0 ? null : undefined, ...listItems], ev.currentTarget ?? ev.target).then(() => { menuOpened.value = false; }); }; diff --git a/packages/client/src/widgets/trends.vue b/packages/client/src/widgets/trends.vue index eb5eb4049f..a34710eae7 100644 --- a/packages/client/src/widgets/trends.vue +++ b/packages/client/src/widgets/trends.vue @@ -52,8 +52,8 @@ const stats = ref([]); const fetching = ref(true); const fetch = () => { - os.api('hashtags/trend').then(stats => { - stats.value = stats; + os.api('hashtags/trend').then(res => { + stats.value = res; fetching.value = false; }); }; |