diff options
Diffstat (limited to 'packages/client/src/components')
43 files changed, 556 insertions, 631 deletions
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 770804cf44..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> @@ -38,7 +38,7 @@ const props = defineProps<{ }>(); const emit = defineEmits<{ - (e: 'update:modelValue', v: string | null): void; + (ev: 'update:modelValue', v: string | null): void; }>(); const available = ref(false); 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.vue b/packages/client/src/components/chart.vue index 1959271f5d..d17c0c9f3e 100644 --- a/packages/client/src/components/chart.vue +++ b/packages/client/src/components/chart.vue @@ -143,6 +143,7 @@ export default defineComponent({ } 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'); @@ -255,6 +256,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(); + } + } + }] }); }; 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/suspense.vue b/packages/client/src/components/form/suspense.vue index 4d5debe604..2ad55dacae 100644 --- a/packages/client/src/components/form/suspense.vue +++ b/packages/client/src/components/form/suspense.vue @@ -1,5 +1,5 @@ <template> -<transition name="fade" mode="out-in"> +<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in"> <div v-if="pending"> <MkLoading/> </div> diff --git a/packages/client/src/components/global/a.vue b/packages/client/src/components/global/a.vue index cf7385ca22..b1b6a0cdaf 100644 --- a/packages/client/src/components/global/a.vue +++ b/packages/client/src/components/global/a.vue @@ -43,31 +43,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 2970d06c97..532627edbd 100644 --- a/packages/client/src/components/media-list.vue +++ b/packages/client/src/components/media-list.vue @@ -3,7 +3,7 @@ <XBanner v-for="media in mediaList.filter(media => !previewable(media))" :key="media.id" :media="media"/> <div v-if="mediaList.filter(media => previewable(media)).length > 0" class="gird-container"> <div ref="gallery" :data-count="mediaList.filter(media => previewable(media)).length"> - <template v-for="media in mediaList"> + <template v-for="media in mediaList.filter(media => previewable(media))"> <XVideo v-if="media.type.startsWith('video')" :key="media.id" :video="media"/> <XImage v-else-if="media.type.startsWith('image')" :key="media.id" class="image" :data-id="media.id" :image="media" :raw="raw"/> </template> @@ -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'; @@ -22,93 +22,83 @@ import XBanner from './media-banner.vue'; import XImage from './media-image.vue'; import XVideo from './media-video.vue'; 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 => media.type.startsWith('image')).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 => { - return file.type.startsWith('video') || file.type.startsWith('image'); - }; + 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-toast.vue b/packages/client/src/components/notification-toast.vue index fbd8467a6e..b2ab1029ad 100644 --- a/packages/client/src/components/notification-toast.vue +++ b/packages/client/src/components/notification-toast.vue @@ -1,6 +1,6 @@ <template> <div class="mk-notification-toast" :style="{ zIndex }"> - <transition name="notification-toast" appear @after-leave="$emit('closed')"> + <transition :name="$store.state.animation ? 'notification-toast' : ''" appear @after-leave="$emit('closed')"> <XNotification v-if="showing" :notification="notification" class="notification _acrylic"/> </transition> </div> 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/poll-editor.vue b/packages/client/src/components/poll-editor.vue index fad0cf1593..6f3f23a2d3 100644 --- a/packages/client/src/components/poll-editor.vue +++ b/packages/client/src/components/poll-editor.vue @@ -3,7 +3,7 @@ <p v-if="choices.length < 2" class="caution"> <i class="fas fa-exclamation-triangle"></i>{{ $ts._poll.noOnlyOneChoice }} </p> - <ul ref="choices"> + <ul> <li v-for="(choice, i) in choices" :key="i"> <MkInput class="input" :model-value="choice" :placeholder="$t('_poll.choiceN', { n: i + 1 })" @update:modelValue="onInput(i, $event)"> </MkInput> @@ -14,8 +14,8 @@ </ul> <MkButton v-if="choices.length < 10" class="add" @click="add">{{ $ts.add }}</MkButton> <MkButton v-else class="add" disabled>{{ $ts._poll.noMore }}</MkButton> + <MkSwitch v-model="multiple">{{ $ts._poll.canMultipleVote }}</MkSwitch> <section> - <MkSwitch v-model="multiple">{{ $ts._poll.canMultipleVote }}</MkSwitch> <div> <MkSelect v-model="expiration"> <template #label>{{ $ts._poll.expiration }}</template> @@ -31,7 +31,7 @@ <template #label>{{ $ts._poll.deadlineTime }}</template> </MkInput> </section> - <section v-if="expiration === 'after'"> + <section v-else-if="expiration === 'after'"> <MkInput v-model="after" type="number" class="input"> <template #label>{{ $ts._poll.duration }}</template> </MkInput> @@ -47,8 +47,8 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { ref, watch } from 'vue'; import { addTime } from '@/scripts/time'; import { formatDateTimeString } from '@/scripts/format-time-string'; import MkInput from './form/input.vue'; @@ -56,131 +56,91 @@ import MkSelect from './form/select.vue'; import MkSwitch from './form/switch.vue'; import MkButton from './ui/button.vue'; -export default defineComponent({ - components: { - MkInput, - MkSelect, - MkSwitch, - MkButton, - }, +const props = defineProps<{ + modelValue: { + expiresAt: string; + expiredAfter: number; + choices: string[]; + multiple: boolean; + }; +}>(); +const emit = defineEmits<{ + (ev: 'update:modelValue', v: { + expiresAt: string; + expiredAfter: number; + choices: string[]; + multiple: boolean; + }): void; +}>(); - props: { - poll: { - type: Object, - required: true - } - }, - - emits: ['updated'], - - data() { - return { - choices: this.poll.choices, - multiple: this.poll.multiple, - expiration: 'infinite', - atDate: formatDateTimeString(addTime(new Date(), 1, 'day'), 'yyyy-MM-dd'), - atTime: '00:00', - after: 0, - unit: 'second', - }; - }, +const choices = ref(props.modelValue.choices); +const multiple = ref(props.modelValue.multiple); +const expiration = ref('infinite'); +const atDate = ref(formatDateTimeString(addTime(new Date(), 1, 'day'), 'yyyy-MM-dd')); +const atTime = ref('00:00'); +const after = ref(0); +const unit = ref('second'); - watch: { - choices: { - handler() { - this.$emit('updated', this.get()); - }, - deep: true - }, - multiple: { - handler() { - this.$emit('updated', this.get()); - }, - }, - expiration: { - handler() { - this.$emit('updated', this.get()); - }, - }, - atDate: { - handler() { - this.$emit('updated', this.get()); - }, - }, - after: { - handler() { - this.$emit('updated', this.get()); - }, - }, - unit: { - handler() { - this.$emit('updated', this.get()); - }, - }, - }, +if (props.modelValue.expiresAt) { + expiration.value = 'at'; + atDate.value = atTime.value = props.modelValue.expiresAt; +} else if (typeof props.modelValue.expiredAfter === 'number') { + expiration.value = 'after'; + after.value = props.modelValue.expiredAfter / 1000; +} else { + expiration.value = 'infinite'; +} - created() { - const poll = this.poll; - if (poll.expiresAt) { - this.expiration = 'at'; - this.atDate = this.atTime = poll.expiresAt; - } else if (typeof poll.expiredAfter === 'number') { - this.expiration = 'after'; - this.after = poll.expiredAfter / 1000; - } else { - this.expiration = 'infinite'; - } - }, +function onInput(i, value) { + choices.value[i] = value; +} - methods: { - onInput(i, e) { - this.choices[i] = e; - }, +function add() { + choices.value.push(''); + // TODO + // nextTick(() => { + // (this.$refs.choices as any).childNodes[this.choices.length - 1].childNodes[0].focus(); + // }); +} - add() { - this.choices.push(''); - this.$nextTick(() => { - // TODO - //(this.$refs.choices as any).childNodes[this.choices.length - 1].childNodes[0].focus(); - }); - }, +function remove(i) { + choices.value = choices.value.filter((_, _i) => _i != i); +} - remove(i) { - this.choices = this.choices.filter((_, _i) => _i != i); - }, +function get() { + const calcAt = () => { + return new Date(`${atDate.value} ${atTime.value}`).getTime(); + }; - get() { - const at = () => { - return new Date(`${this.atDate} ${this.atTime}`).getTime(); - }; + const calcAfter = () => { + let base = parseInt(after.value); + switch (unit.value) { + case 'day': base *= 24; + case 'hour': base *= 60; + case 'minute': base *= 60; + case 'second': return base *= 1000; + default: return null; + } + }; - const after = () => { - let base = parseInt(this.after); - switch (this.unit) { - case 'day': base *= 24; - case 'hour': base *= 60; - case 'minute': base *= 60; - case 'second': return base *= 1000; - default: return null; - } - }; + return { + choices: choices.value, + multiple: multiple.value, + ...( + expiration.value === 'at' ? { expiresAt: calcAt() } : + expiration.value === 'after' ? { expiredAfter: calcAfter() } : {} + ) + }; +} - return { - choices: this.choices, - multiple: this.multiple, - ...( - this.expiration === 'at' ? { expiresAt: at() } : - this.expiration === 'after' ? { expiredAfter: after() } : {} - ) - }; - }, - } +watch([choices, multiple, expiration, atDate, atTime, after, unit], () => emit('update:modelValue', get()), { + deep: true, }); </script> <style lang="scss" scoped> .zmdxowus { - padding: 8px; + padding: 8px 16px; > .caution { margin: 0 0 8px 0; @@ -216,7 +176,7 @@ export default defineComponent({ } > .add { - margin: 8px 0 0 0; + margin: 8px 0; z-index: 1; } @@ -225,21 +185,27 @@ export default defineComponent({ > div { margin: 0 8px; + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 12px; &:last-child { flex: 1 0 auto; + > div { + flex-grow: 1; + } + > section { - align-items: center; + // MAGIC: Prevent div above from growing unless wrapped to its own line + flex-grow: 9999; + align-items: end; display: flex; - margin: -32px 0 0; - - > &:first-child { - margin-right: 16px; - } + gap: 4px; > .input { - flex: 1 0 auto; + flex: 1 1 auto; } } } 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 0dcec26932..8c5027f8e7 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" :poll="poll" @destroyed="poll = null" @updated="onPollUpdate"/> + <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"/> @@ -102,7 +102,7 @@ const props = withDefaults(defineProps<{ initialLocalOnly?: boolean; initialVisibleUsers?: misskey.entities.User[]; initialNote?: misskey.entities.Note; - share?: boolean; + instant?: boolean; fixed?: boolean; autofocus?: boolean; }>(), { @@ -111,9 +111,9 @@ const props = withDefaults(defineProps<{ }); const emit = defineEmits<{ - (e: 'posted'): void; - (e: 'cancel'): void; - (e: 'esc'): void; + (ev: 'posted'): void; + (ev: 'cancel'): void; + (ev: 'esc'): void; }>(); const textareaEl = $ref<HTMLTextAreaElement | null>(null); @@ -127,8 +127,8 @@ let files = $ref(props.initialFiles ?? []); let poll = $ref<{ choices: string[]; multiple: boolean; - expiresAt: string; - expiredAfter: string; + expiresAt: string | null; + expiredAfter: string | null; } | null>(null); let useCw = $ref(false); let showPreview = $ref(false); @@ -165,19 +165,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 +185,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 => { @@ -342,7 +342,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); } @@ -371,11 +371,6 @@ function upload(file: File, name?: string) { }); } -function onPollUpdate(poll) { - poll = poll; - saveDraft(); -} - function setVisibility() { if (props.channel) { // TODO: information dialog @@ -452,7 +447,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); @@ -597,7 +592,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) { @@ -610,7 +605,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); @@ -646,7 +641,7 @@ onMounted(() => { nextTick(() => { // 書きかけの投稿を復元 - if (!props.share && !props.mention && !props.specified) { + if (!props.instant && !props.mention && !props.specified) { const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[draftKey]; if (draft) { text = draft.data.text; 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/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/toast.vue b/packages/client/src/components/toast.vue index 031aa45633..c114379716 100644 --- a/packages/client/src/components/toast.vue +++ b/packages/client/src/components/toast.vue @@ -1,6 +1,6 @@ <template> <div class="mk-toast"> - <transition name="toast" appear @after-leave="emit('closed')"> + <transition :name="$store.state.animation ? 'toast' : ''" appear @after-leave="emit('closed')"> <div v-if="showing" class="body _acrylic" :style="{ zIndex }"> <div class="message"> {{ message }} diff --git a/packages/client/src/components/ui/container.vue b/packages/client/src/components/ui/container.vue index fcd9f32290..7c595d8116 100644 --- a/packages/client/src/components/ui/container.vue +++ b/packages/client/src/components/ui/container.vue @@ -10,7 +10,7 @@ </button> </div> </header> - <transition name="container-toggle" + <transition :name="$store.state.animation ? 'container-toggle' : ''" @enter="enter" @after-enter="afterEnter" @leave="leave" diff --git a/packages/client/src/components/ui/folder.vue b/packages/client/src/components/ui/folder.vue index 9795b1d81a..fe1602b2bb 100644 --- a/packages/client/src/components/ui/folder.vue +++ b/packages/client/src/components/ui/folder.vue @@ -8,7 +8,7 @@ <template v-else><i class="fas fa-angle-down"></i></template> </button> </header> - <transition name="folder-toggle" + <transition :name="$store.state.animation ? 'folder-toggle' : ''" @enter="enter" @after-enter="afterEnter" @leave="leave" 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/pagination.vue b/packages/client/src/components/ui/pagination.vue index 9c18fc5ce5..13f3215671 100644 --- a/packages/client/src/components/ui/pagination.vue +++ b/packages/client/src/components/ui/pagination.vue @@ -1,5 +1,5 @@ <template> -<transition name="fade" mode="out-in"> +<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in"> <MkLoading v-if="fetching"/> <MkError v-else-if="error" @retry="init()"/> diff --git a/packages/client/src/components/ui/tooltip.vue b/packages/client/src/components/ui/tooltip.vue index 2e48ab623e..394b068352 100644 --- a/packages/client/src/components/ui/tooltip.vue +++ b/packages/client/src/components/ui/tooltip.vue @@ -1,5 +1,5 @@ <template> -<transition name="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> diff --git a/packages/client/src/components/url-preview-popup.vue b/packages/client/src/components/url-preview-popup.vue index c345bafcf9..5f3717ab91 100644 --- a/packages/client/src/components/url-preview-popup.vue +++ b/packages/client/src/components/url-preview-popup.vue @@ -1,6 +1,6 @@ <template> <div class="fgmtyycl" :style="{ zIndex, top: top + 'px', left: left + 'px' }"> - <transition name="zoom" @after-leave="$emit('closed')"> + <transition :name="$store.state.animation ? 'zoom' : ''" @after-leave="$emit('closed')"> <MkUrlPreview v-if="showing" class="_popup _shadow" :url="url"/> </transition> </div> diff --git a/packages/client/src/components/url-preview.vue b/packages/client/src/components/url-preview.vue index bf3b358797..6c57957617 100644 --- a/packages/client/src/components/url-preview.vue +++ b/packages/client/src/components/url-preview.vue @@ -7,7 +7,7 @@ <iframe ref="tweet" scrolling="no" frameborder="no" :style="{ position: 'relative', width: '100%', height: `${tweetHeight}px` }" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&hideCard=false&hideThread=false&lang=en&theme=${$store.state.darkMode ? 'dark' : 'light'}&id=${tweetId}`"></iframe> </div> <div v-else v-size="{ max: [400, 350] }" class="mk-url-preview"> - <transition name="zoom" mode="out-in"> + <transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in"> <component :is="self ? 'MkA' : 'a'" v-if="!fetching" :class="{ compact }" :[attr]="self ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url"> <div v-if="thumbnail" class="thumbnail" :style="`background-image: url('${thumbnail}')`"> <button v-if="!playerEnabled && player.url" class="_button" :title="$ts.enablePlayer" @click.prevent="playerEnabled = true"><i class="fas fa-play-circle"></i></button> 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/components/user-preview.vue b/packages/client/src/components/user-preview.vue index f85a32fbe7..51c5330564 100644 --- a/packages/client/src/components/user-preview.vue +++ b/packages/client/src/components/user-preview.vue @@ -1,5 +1,5 @@ <template> -<transition name="popup" appear @after-leave="$emit('closed')"> +<transition :name="$store.state.animation ? 'popup' : ''" appear @after-leave="$emit('closed')"> <div v-if="showing" class="fxxzrfni _popup _shadow" :style="{ zIndex, top: top + 'px', left: left + 'px' }" @mouseover="() => { $emit('mouseover'); }" @mouseleave="() => { $emit('mouseleave'); }"> <div v-if="fetched" class="info"> <div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''"></div> |