diff options
| author | tamaina <tamaina@hotmail.co.jp> | 2022-01-18 23:06:16 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-01-18 23:06:16 +0900 |
| commit | 7be09a4af9f57981298cb20c32a69755241dc227 (patch) | |
| tree | 57f4dda7fc5f9b0e3033a73f296b4e70640ea12d | |
| parent | refactor: APIエンドポイントファイルの定義を良い感じにす... (diff) | |
| download | misskey-7be09a4af9f57981298cb20c32a69755241dc227.tar.gz misskey-7be09a4af9f57981298cb20c32a69755241dc227.tar.bz2 misskey-7be09a4af9f57981298cb20c32a69755241dc227.zip | |
refactor: Composition APIへ移行 (#8138)
* components/drive-file-thumbnail.vue
* components/drive-select-dialog.vue
* components/drive-window.vue
* wip
* wip drive.file.vue, drive.vue
* fix prop
* wip(
* components/drive.folder.vue
* maybe ok
* :v:
* fix variable
* FIX FOLDER VARIABLE
* components/emoji-picker-dialog.vue
* Hate `$emit`
* hate global property
* components/emoji-picker-window.vue
* components/emoji-picker.section.vue
* fix
* fixx
* wip components/emoji-picker.vue
* fix
* defineExpose
* ユニコード絵文字の型をもっといい感じに
* components/featured-photos.vue
* components/follow-button.vue
* forgot-password.vue
* forgot-password.vue
* :art:
* fix
17 files changed, 1436 insertions, 1591 deletions
diff --git a/packages/client/src/components/dialog.vue b/packages/client/src/components/dialog.vue index 9cd5234684..b6b649cde9 100644 --- a/packages/client/src/components/dialog.vue +++ b/packages/client/src/components/dialog.vue @@ -1,5 +1,5 @@ <template> -<MkModal ref="modal" :prefer-type="'dialog'" :z-priority="'high'" @click="done(true)" @closed="$emit('closed')"> +<MkModal ref="modal" :prefer-type="'dialog'" :z-priority="'high'" @click="done(true)" @closed="emit('closed')"> <div class="mk-dialog"> <div v-if="icon" class="icon"> <i :class="icon"></i> @@ -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) ? $ts.ok : $ts.gotIt }}</MkButton> - <MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ $ts.cancel }}</MkButton> + <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> </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> @@ -44,6 +44,7 @@ import MkModal from '@/components/ui/modal.vue'; import MkButton from '@/components/ui/button.vue'; import MkInput from '@/components/form/input.vue'; import MkSelect from '@/components/form/select.vue'; +import { i18n } from '@/i18n'; type Input = { type: HTMLInputElement['type']; diff --git a/packages/client/src/components/drive-file-thumbnail.vue b/packages/client/src/components/drive-file-thumbnail.vue index e94b6b8bcb..81b80e7e8e 100644 --- a/packages/client/src/components/drive-file-thumbnail.vue +++ b/packages/client/src/components/drive-file-thumbnail.vue @@ -14,71 +14,42 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { computed } from 'vue'; +import * as Misskey from 'misskey-js'; import ImgWithBlurhash from '@/components/img-with-blurhash.vue'; -import { ColdDeviceStorage } from '@/store'; -export default defineComponent({ - components: { - ImgWithBlurhash - }, - props: { - file: { - type: Object, - required: true - }, - fit: { - type: String, - required: false, - default: 'cover' - }, - }, - data() { - return { - isContextmenuShowing: false, - isDragging: false, +const props = defineProps<{ + file: Misskey.entities.DriveFile; + fit: string; +}>(); - }; - }, - computed: { - is(): 'image' | 'video' | 'midi' | 'audio' | 'csv' | 'pdf' | 'textfile' | 'archive' | 'unknown' { - if (this.file.type.startsWith('image/')) return 'image'; - if (this.file.type.startsWith('video/')) return 'video'; - if (this.file.type === 'audio/midi') return 'midi'; - if (this.file.type.startsWith('audio/')) return 'audio'; - if (this.file.type.endsWith('/csv')) return 'csv'; - if (this.file.type.endsWith('/pdf')) return 'pdf'; - if (this.file.type.startsWith('text/')) return 'textfile'; - if ([ - "application/zip", - "application/x-cpio", - "application/x-bzip", - "application/x-bzip2", - "application/java-archive", - "application/x-rar-compressed", - "application/x-tar", - "application/gzip", - "application/x-7z-compressed" - ].some(e => e === this.file.type)) return 'archive'; - return 'unknown'; - }, - isThumbnailAvailable(): boolean { - return this.file.thumbnailUrl - ? (this.is === 'image' || this.is === 'video') - : false; - }, - }, - mounted() { - const audioTag = this.$refs.volumectrl as HTMLAudioElement; - if (audioTag) audioTag.volume = ColdDeviceStorage.get('mediaVolume'); - }, - methods: { - volumechange() { - const audioTag = this.$refs.volumectrl as HTMLAudioElement; - ColdDeviceStorage.set('mediaVolume', audioTag.volume); - } - } +const is = computed(() => { + if (props.file.type.startsWith('image/')) return 'image'; + if (props.file.type.startsWith('video/')) return 'video'; + if (props.file.type === 'audio/midi') return 'midi'; + if (props.file.type.startsWith('audio/')) return 'audio'; + if (props.file.type.endsWith('/csv')) return 'csv'; + if (props.file.type.endsWith('/pdf')) return 'pdf'; + if (props.file.type.startsWith('text/')) return 'textfile'; + if ([ + "application/zip", + "application/x-cpio", + "application/x-bzip", + "application/x-bzip2", + "application/java-archive", + "application/x-rar-compressed", + "application/x-tar", + "application/gzip", + "application/x-7z-compressed" + ].some(e => e === props.file.type)) return 'archive'; + return 'unknown'; +}); + +const isThumbnailAvailable = computed(() => { + return props.file.thumbnailUrl + ? (is.value === 'image' as const || is.value === 'video') + : false; }); </script> diff --git a/packages/client/src/components/drive-select-dialog.vue b/packages/client/src/components/drive-select-dialog.vue index 75537dfe3e..6d84511277 100644 --- a/packages/client/src/components/drive-select-dialog.vue +++ b/packages/client/src/components/drive-select-dialog.vue @@ -7,64 +7,51 @@ @click="cancel()" @close="cancel()" @ok="ok()" - @closed="$emit('closed')" + @closed="emit('closed')" > <template #header> - {{ multiple ? ((type === 'file') ? $ts.selectFiles : $ts.selectFolders) : ((type === 'file') ? $ts.selectFile : $ts.selectFolder) }} + {{ multiple ? ((type === 'file') ? i18n.locale.selectFiles : i18n.locale.selectFolders) : ((type === 'file') ? i18n.locale.selectFile : i18n.locale.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()"/> </XModalWindow> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { ref } from 'vue'; +import * as Misskey from 'misskey-js'; import XDrive from './drive.vue'; import XModalWindow from '@/components/ui/modal-window.vue'; import number from '@/filters/number'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - XDrive, - XModalWindow, - }, - - props: { - type: { - type: String, - required: false, - default: 'file' - }, - multiple: { - type: Boolean, - default: false - } - }, +withDefaults(defineProps<{ + type?: 'file' | 'folder'; + multiple: boolean; +}>(), { + type: 'file', +}); - emits: ['done', 'closed'], +const emit = defineEmits<{ + (e: 'done', r?: Misskey.entities.DriveFile[]): void; + (e: 'closed'): void; +}>(); - data() { - return { - selected: [] - }; - }, +const dialog = ref<InstanceType<typeof XModalWindow>>(); - methods: { - ok() { - this.$emit('done', this.selected); - this.$refs.dialog.close(); - }, +const selected = ref<Misskey.entities.DriveFile[]>([]); - cancel() { - this.$emit('done'); - this.$refs.dialog.close(); - }, +function ok() { + emit('done', selected.value); + dialog.value?.close(); +} - onChangeSelection(xs) { - this.selected = xs; - }, +function cancel() { + emit('done'); + dialog.value?.close(); +} - number - } -}); +function onChangeSelection(files: Misskey.entities.DriveFile[]) { + selected.value = files; +} </script> diff --git a/packages/client/src/components/drive-window.vue b/packages/client/src/components/drive-window.vue index 43f07ebe76..8b60bf7794 100644 --- a/packages/client/src/components/drive-window.vue +++ b/packages/client/src/components/drive-window.vue @@ -3,42 +3,27 @@ :initial-width="800" :initial-height="500" :can-resize="true" - @closed="$emit('closed')" + @closed="emit('closed')" > <template #header> - {{ $ts.drive }} + {{ i18n.locale.drive }} </template> <XDrive :initial-folder="initialFolder"/> </XWindow> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; +import * as Misskey from 'misskey-js'; import XDrive from './drive.vue'; import XWindow from '@/components/ui/window.vue'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - XDrive, - XWindow, - }, +defineProps<{ + initialFolder?: Misskey.entities.DriveFolder; +}>(); - props: { - initialFolder: { - type: Object, - required: false - }, - }, - - emits: ['closed'], - - data() { - return { - }; - }, - - methods: { - - } -}); +const emit = defineEmits<{ + (e: 'closed'): void; +}>(); </script> diff --git a/packages/client/src/components/drive.file.vue b/packages/client/src/components/drive.file.vue index 511647229e..fd6a813838 100644 --- a/packages/client/src/components/drive.file.vue +++ b/packages/client/src/components/drive.file.vue @@ -8,17 +8,17 @@ @dragstart="onDragstart" @dragend="onDragend" > - <div v-if="$i.avatarId == file.id" class="label"> + <div v-if="$i?.avatarId == file.id" class="label"> <img src="/client-assets/label.svg"/> - <p>{{ $ts.avatar }}</p> + <p>{{ i18n.locale.avatar }}</p> </div> - <div v-if="$i.bannerId == file.id" class="label"> + <div v-if="$i?.bannerId == file.id" class="label"> <img src="/client-assets/label.svg"/> - <p>{{ $ts.banner }}</p> + <p>{{ i18n.locale.banner }}</p> </div> <div v-if="file.isSensitive" class="label red"> <img src="/client-assets/label-red.svg"/> - <p>{{ $ts.nsfw }}</p> + <p>{{ i18n.locale.nsfw }}</p> </div> <MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/> @@ -30,179 +30,155 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { computed, ref } from 'vue'; +import * as Misskey from 'misskey-js'; import copyToClipboard from '@/scripts/copy-to-clipboard'; import MkDriveFileThumbnail from './drive-file-thumbnail.vue'; import bytes from '@/filters/bytes'; import * as os from '@/os'; +import { i18n } from '@/i18n'; +import { $i } from '@/account'; -export default defineComponent({ - components: { - MkDriveFileThumbnail - }, +const props = withDefaults(defineProps<{ + file: Misskey.entities.DriveFile; + isSelected?: boolean; + selectMode?: boolean; +}>(), { + isSelected: false, + selectMode: false, +}); - props: { - file: { - type: Object, - required: true, - }, - isSelected: { - type: Boolean, - required: false, - default: false, - }, - selectMode: { - type: Boolean, - required: false, - default: false, - } - }, +const emit = defineEmits<{ + (e: 'chosen', r: Misskey.entities.DriveFile): void; + (e: 'dragstart'): void; + (e: 'dragend'): void; +}>(); - emits: ['chosen'], +const isDragging = ref(false); - data() { - return { - isDragging: false - }; - }, +const title = computed(() => `${props.file.name}\n${props.file.type} ${bytes(props.file.size)}`); - computed: { - // TODO: parentへの参照を無くす - browser(): any { - return this.$parent; - }, - title(): string { - return `${this.file.name}\n${this.file.type} ${bytes(this.file.size)}`; - } - }, - - methods: { - getMenu() { - return [{ - text: this.$ts.rename, - icon: 'fas fa-i-cursor', - action: this.rename - }, { - text: this.file.isSensitive ? this.$ts.unmarkAsSensitive : this.$ts.markAsSensitive, - icon: this.file.isSensitive ? 'fas fa-eye' : 'fas fa-eye-slash', - action: this.toggleSensitive - }, { - text: this.$ts.describeFile, - icon: 'fas fa-i-cursor', - action: this.describe - }, null, { - text: this.$ts.copyUrl, - icon: 'fas fa-link', - action: this.copyUrl - }, { - type: 'a', - href: this.file.url, - target: '_blank', - text: this.$ts.download, - icon: 'fas fa-download', - download: this.file.name - }, null, { - text: this.$ts.delete, - icon: 'fas fa-trash-alt', - danger: true, - action: this.deleteFile - }]; - }, +function getMenu() { + return [{ + text: i18n.locale.rename, + icon: 'fas fa-i-cursor', + action: rename + }, { + text: props.file.isSensitive ? i18n.locale.unmarkAsSensitive : i18n.locale.markAsSensitive, + icon: props.file.isSensitive ? 'fas fa-eye' : 'fas fa-eye-slash', + action: toggleSensitive + }, { + text: i18n.locale.describeFile, + icon: 'fas fa-i-cursor', + action: describe + }, null, { + text: i18n.locale.copyUrl, + icon: 'fas fa-link', + action: copyUrl + }, { + type: 'a', + href: props.file.url, + target: '_blank', + text: i18n.locale.download, + icon: 'fas fa-download', + download: props.file.name + }, null, { + text: i18n.locale.delete, + icon: 'fas fa-trash-alt', + danger: true, + action: deleteFile + }]; +} - onClick(ev) { - if (this.selectMode) { - this.$emit('chosen', this.file); - } else { - os.popupMenu(this.getMenu(), ev.currentTarget || ev.target); - } - }, +function onClick(ev: MouseEvent) { + if (props.selectMode) { + emit('chosen', props.file); + } else { + os.popupMenu(getMenu(), (ev.currentTarget || ev.target || undefined) as HTMLElement | undefined); + } +} - onContextmenu(e) { - os.contextMenu(this.getMenu(), e); - }, +function onContextmenu(e: MouseEvent) { + os.contextMenu(getMenu(), e); +} - onDragstart(e) { - e.dataTransfer.effectAllowed = 'move'; - e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FILE_, JSON.stringify(this.file)); - this.isDragging = true; +function onDragstart(e: DragEvent) { + if (e.dataTransfer) { + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FILE_, JSON.stringify(props.file)); + } + isDragging.value = true; - // 親ブラウザに対して、ドラッグが開始されたフラグを立てる - // (=あなたの子供が、ドラッグを開始しましたよ) - this.browser.isDragSource = true; - }, + emit('dragstart'); +} - onDragend(e) { - this.isDragging = false; - this.browser.isDragSource = false; - }, +function onDragend() { + isDragging.value = false; + emit('dragend'); +} - rename() { - os.inputText({ - title: this.$ts.renameFile, - placeholder: this.$ts.inputNewFileName, - default: this.file.name, - allowEmpty: false - }).then(({ canceled, result: name }) => { - if (canceled) return; - os.api('drive/files/update', { - fileId: this.file.id, - name: name - }); - }); - }, +function rename() { + os.inputText({ + title: i18n.locale.renameFile, + placeholder: i18n.locale.inputNewFileName, + default: props.file.name, + }).then(({ canceled, result: name }) => { + if (canceled) return; + os.api('drive/files/update', { + fileId: props.file.id, + name: name + }); + }); +} - describe() { - os.popup(import('@/components/media-caption.vue'), { - title: this.$ts.describeFile, - input: { - placeholder: this.$ts.inputNewDescription, - default: this.file.comment !== null ? this.file.comment : '', - }, - image: this.file - }, { - done: result => { - if (!result || result.canceled) return; - let comment = result.result; - os.api('drive/files/update', { - fileId: this.file.id, - comment: comment.length == 0 ? null : comment - }); - } - }, 'closed'); +function describe() { + os.popup(import('@/components/media-caption.vue'), { + title: i18n.locale.describeFile, + input: { + placeholder: i18n.locale.inputNewDescription, + default: props.file.comment !== null ? props.file.comment : '', }, - - toggleSensitive() { + image: props.file + }, { + done: result => { + if (!result || result.canceled) return; + let comment = result.result; os.api('drive/files/update', { - fileId: this.file.id, - isSensitive: !this.file.isSensitive + fileId: props.file.id, + comment: comment.length == 0 ? null : comment }); - }, - - copyUrl() { - copyToClipboard(this.file.url); - os.success(); - }, - - addApp() { - alert('not implemented yet'); - }, + } + }, 'closed'); +} - async deleteFile() { - const { canceled } = await os.confirm({ - type: 'warning', - text: this.$t('driveFileDeleteConfirm', { name: this.file.name }), - }); - if (canceled) return; +function toggleSensitive() { + os.api('drive/files/update', { + fileId: props.file.id, + isSensitive: !props.file.isSensitive + }); +} - os.api('drive/files/delete', { - fileId: this.file.id - }); - }, +function copyUrl() { + copyToClipboard(props.file.url); + os.success(); +} +/* +function addApp() { + alert('not implemented yet'); +} +*/ +async function deleteFile() { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.t('driveFileDeleteConfirm', { name: props.file.name }), + }); - bytes - } -}); + if (canceled) return; + os.api('drive/files/delete', { + fileId: props.file.id + }); +} </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/drive.folder.vue b/packages/client/src/components/drive.folder.vue index aaba736cf8..8e8d9f3bb3 100644 --- a/packages/client/src/components/drive.folder.vue +++ b/packages/client/src/components/drive.folder.vue @@ -19,243 +19,233 @@ <template v-if="!hover"><i class="fas fa-folder fa-fw"></i></template> {{ folder.name }} </p> - <p v-if="$store.state.uploadFolder == folder.id" class="upload"> - {{ $ts.uploadFolder }} + <p v-if="defaultStore.state.uploadFolder == folder.id" class="upload"> + {{ i18n.locale.uploadFolder }} </p> <button v-if="selectMode" class="checkbox _button" :class="{ checked: isSelected }" @click.prevent.stop="checkboxClicked"></button> </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { computed, ref } from 'vue'; +import * as Misskey from 'misskey-js'; import * as os from '@/os'; +import { i18n } from '@/i18n'; +import { defaultStore } from '@/store'; -export default defineComponent({ - props: { - folder: { - type: Object, - required: true, - }, - isSelected: { - type: Boolean, - required: false, - default: false, - }, - selectMode: { - type: Boolean, - required: false, - default: false, - } - }, +const props = withDefaults(defineProps<{ + folder: Misskey.entities.DriveFolder; + isSelected?: boolean; + selectMode?: boolean; +}>(), { + isSelected: false, + selectMode: false, +}); - emits: ['chosen'], +const emit = defineEmits<{ + (e: 'chosen', v: Misskey.entities.DriveFolder): void; + (e: 'move', v: Misskey.entities.DriveFolder): void; + (e: 'upload', file: File, folder: Misskey.entities.DriveFolder); + (e: 'removeFile', v: Misskey.entities.DriveFile['id']): void; + (e: 'removeFolder', v: Misskey.entities.DriveFolder['id']): void; + (e: 'dragstart'): void; + (e: 'dragend'): void; +}>(); - data() { - return { - hover: false, - draghover: false, - isDragging: false, - }; - }, +const hover = ref(false); +const draghover = ref(false); +const isDragging = ref(false); - computed: { - browser(): any { - return this.$parent; - }, - title(): string { - return this.folder.name; - } - }, +const title = computed(() => props.folder.name); - methods: { - checkboxClicked(e) { - this.$emit('chosen', this.folder); - }, +function checkboxClicked(e) { + emit('chosen', props.folder); +} - onClick() { - this.browser.move(this.folder); - }, +function onClick() { + emit('move', props.folder); +} - onMouseover() { - this.hover = true; - }, +function onMouseover() { + hover.value = true; +} - onMouseout() { - this.hover = false - }, +function onMouseout() { + hover.value = false +} - onDragover(e) { - // 自分自身がドラッグされている場合 - if (this.isDragging) { - // 自分自身にはドロップさせない - e.dataTransfer.dropEffect = 'none'; - return; - } +function onDragover(e: DragEvent) { + if (!e.dataTransfer) return; - const isFile = e.dataTransfer.items[0].kind == 'file'; - const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_; - const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_; + // 自分自身がドラッグされている場合 + if (isDragging.value) { + // 自分自身にはドロップさせない + e.dataTransfer.dropEffect = 'none'; + return; + } - if (isFile || isDriveFile || isDriveFolder) { - e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; - } else { - e.dataTransfer.dropEffect = 'none'; - } - }, + const isFile = e.dataTransfer.items[0].kind == 'file'; + const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_; + const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_; - onDragenter() { - if (!this.isDragging) this.draghover = true; - }, + if (isFile || isDriveFile || isDriveFolder) { + e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; + } else { + e.dataTransfer.dropEffect = 'none'; + } +} - onDragleave() { - this.draghover = false; - }, +function onDragenter() { + if (!isDragging.value) draghover.value = true; +} - onDrop(e) { - this.draghover = false; +function onDragleave() { + draghover.value = false; +} - // ファイルだったら - if (e.dataTransfer.files.length > 0) { - for (const file of Array.from(e.dataTransfer.files)) { - this.browser.upload(file, this.folder); - } - return; - } +function onDrop(e: DragEvent) { + draghover.value = false; - //#region ドライブのファイル - const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); - if (driveFile != null && driveFile != '') { - const file = JSON.parse(driveFile); - this.browser.removeFile(file.id); - os.api('drive/files/update', { - fileId: file.id, - folderId: this.folder.id - }); - } - //#endregion + if (!e.dataTransfer) return; + + // ファイルだったら + if (e.dataTransfer.files.length > 0) { + for (const file of Array.from(e.dataTransfer.files)) { + emit('upload', file, props.folder); + } + return; + } - //#region ドライブのフォルダ - const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_); - if (driveFolder != null && driveFolder != '') { - const folder = JSON.parse(driveFolder); + //#region ドライブのファイル + const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); + if (driveFile != null && driveFile != '') { + const file = JSON.parse(driveFile); + emit('removeFile', file.id); + os.api('drive/files/update', { + fileId: file.id, + folderId: props.folder.id + }); + } + //#endregion - // 移動先が自分自身ならreject - if (folder.id == this.folder.id) return; + //#region ドライブのフォルダ + const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_); + if (driveFolder != null && driveFolder != '') { + const folder = JSON.parse(driveFolder); - this.browser.removeFolder(folder.id); - os.api('drive/folders/update', { - folderId: folder.id, - parentId: this.folder.id - }).then(() => { - // noop - }).catch(err => { - switch (err) { - case 'detected-circular-definition': - os.alert({ - title: this.$ts.unableToProcess, - text: this.$ts.circularReferenceFolder - }); - break; - default: - os.alert({ - type: 'error', - text: this.$ts.somethingHappened - }); - } - }); + // 移動先が自分自身ならreject + if (folder.id == props.folder.id) return; + + emit('removeFolder', folder.id); + os.api('drive/folders/update', { + folderId: folder.id, + parentId: props.folder.id + }).then(() => { + // noop + }).catch(err => { + switch (err) { + case 'detected-circular-definition': + os.alert({ + title: i18n.locale.unableToProcess, + text: i18n.locale.circularReferenceFolder + }); + break; + default: + os.alert({ + type: 'error', + text: i18n.locale.somethingHappened + }); } - //#endregion - }, + }); + } + //#endregion +} - onDragstart(e) { - e.dataTransfer.effectAllowed = 'move'; - e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FOLDER_, JSON.stringify(this.folder)); - this.isDragging = true; +function onDragstart(e: DragEvent) { + if (!e.dataTransfer) return; - // 親ブラウザに対して、ドラッグが開始されたフラグを立てる - // (=あなたの子供が、ドラッグを開始しましたよ) - this.browser.isDragSource = true; - }, + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FOLDER_, JSON.stringify(props.folder)); + isDragging.value = true; - onDragend() { - this.isDragging = false; - this.browser.isDragSource = false; - }, + // 親ブラウザに対して、ドラッグが開始されたフラグを立てる + // (=あなたの子供が、ドラッグを開始しましたよ) + emit('dragstart'); +} - go() { - this.browser.move(this.folder.id); - }, +function onDragend() { + isDragging.value = false; + emit('dragend'); +} - newWindow() { - this.browser.newWindow(this.folder); - }, +function go() { + emit('move', props.folder.id); +} - rename() { - os.inputText({ - title: this.$ts.renameFolder, - placeholder: this.$ts.inputNewFolderName, - default: this.folder.name - }).then(({ canceled, result: name }) => { - if (canceled) return; - os.api('drive/folders/update', { - folderId: this.folder.id, - name: name - }); - }); - }, +function rename() { + os.inputText({ + title: i18n.locale.renameFolder, + placeholder: i18n.locale.inputNewFolderName, + default: props.folder.name + }).then(({ canceled, result: name }) => { + if (canceled) return; + os.api('drive/folders/update', { + folderId: props.folder.id, + name: name + }); + }); +} - deleteFolder() { - os.api('drive/folders/delete', { - folderId: this.folder.id - }).then(() => { - if (this.$store.state.uploadFolder === this.folder.id) { - this.$store.set('uploadFolder', null); - } - }).catch(err => { - switch(err.id) { - case 'b0fc8a17-963c-405d-bfbc-859a487295e1': - os.alert({ - type: 'error', - title: this.$ts.unableToDelete, - text: this.$ts.hasChildFilesOrFolders - }); - break; - default: - os.alert({ - type: 'error', - text: this.$ts.unableToDelete - }); - } - }); - }, +function deleteFolder() { + os.api('drive/folders/delete', { + folderId: props.folder.id + }).then(() => { + if (defaultStore.state.uploadFolder === props.folder.id) { + defaultStore.set('uploadFolder', null); + } + }).catch(err => { + switch(err.id) { + case 'b0fc8a17-963c-405d-bfbc-859a487295e1': + os.alert({ + type: 'error', + title: i18n.locale.unableToDelete, + text: i18n.locale.hasChildFilesOrFolders + }); + break; + default: + os.alert({ + type: 'error', + text: i18n.locale.unableToDelete + }); + } + }); +} - setAsUploadFolder() { - this.$store.set('uploadFolder', this.folder.id); - }, +function setAsUploadFolder() { + defaultStore.set('uploadFolder', props.folder.id); +} - onContextmenu(e) { - os.contextMenu([{ - text: this.$ts.openInWindow, - icon: 'fas fa-window-restore', - action: () => { - os.popup(import('./drive-window.vue'), { - initialFolder: this.folder - }, { - }, 'closed'); - } - }, null, { - text: this.$ts.rename, - icon: 'fas fa-i-cursor', - action: this.rename - }, null, { - text: this.$ts.delete, - icon: 'fas fa-trash-alt', - danger: true, - action: this.deleteFolder - }], e); - }, - } -}); +function onContextmenu(e) { + os.contextMenu([{ + text: i18n.locale.openInWindow, + icon: 'fas fa-window-restore', + action: () => { + os.popup(import('./drive-window.vue'), { + initialFolder: props.folder + }, { + }, 'closed'); + } + }, null, { + text: i18n.locale.rename, + icon: 'fas fa-i-cursor', + action: rename, + }, null, { + text: i18n.locale.delete, + icon: 'fas fa-trash-alt', + danger: true, + action: deleteFolder, + }], e); +} </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/drive.nav-folder.vue b/packages/client/src/components/drive.nav-folder.vue index 4f0e6ce0e9..7c35c5d3da 100644 --- a/packages/client/src/components/drive.nav-folder.vue +++ b/packages/client/src/components/drive.nav-folder.vue @@ -8,114 +8,111 @@ @drop.stop="onDrop" > <i v-if="folder == null" class="fas fa-cloud"></i> - <span>{{ folder == null ? $ts.drive : folder.name }}</span> + <span>{{ folder == null ? i18n.locale.drive : folder.name }}</span> </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { ref } from 'vue'; +import * as Misskey from 'misskey-js'; import * as os from '@/os'; +import { i18n } from '@/i18n'; -export default defineComponent({ - props: { - folder: { - type: Object, - required: false, - } - }, +const props = defineProps<{ + folder?: Misskey.entities.DriveFolder; + parentFolder: Misskey.entities.DriveFolder | null; +}>(); - data() { - return { - hover: false, - draghover: false, - }; - }, +const emit = defineEmits<{ + (e: 'move', v?: Misskey.entities.DriveFolder): void; + (e: 'upload', file: File, folder?: Misskey.entities.DriveFolder | null): void; + (e: 'removeFile', v: Misskey.entities.DriveFile['id']): void; + (e: 'removeFolder', v: Misskey.entities.DriveFolder['id']): void; +}>(); - computed: { - browser(): any { - return this.$parent; - } - }, +const hover = ref(false); +const draghover = ref(false); - methods: { - onClick() { - this.browser.move(this.folder); - }, +function onClick() { + emit('move', props.folder); +} - onMouseover() { - this.hover = true; - }, +function onMouseover() { + hover.value = true; +} - onMouseout() { - this.hover = false; - }, +function onMouseout() { + hover.value = false; +} - onDragover(e) { - // このフォルダがルートかつカレントディレクトリならドロップ禁止 - if (this.folder == null && this.browser.folder == null) { - e.dataTransfer.dropEffect = 'none'; - } +function onDragover(e: DragEvent) { + if (!e.dataTransfer) return; - const isFile = e.dataTransfer.items[0].kind == 'file'; - const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_; - const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_; + // このフォルダがルートかつカレントディレクトリならドロップ禁止 + if (props.folder == null && props.parentFolder == null) { + e.dataTransfer.dropEffect = 'none'; + } - if (isFile || isDriveFile || isDriveFolder) { - e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; - } else { - e.dataTransfer.dropEffect = 'none'; - } + const isFile = e.dataTransfer.items[0].kind == 'file'; + const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_; + const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_; - return false; - }, + if (isFile || isDriveFile || isDriveFolder) { + e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; + } else { + e.dataTransfer.dropEffect = 'none'; + } - onDragenter() { - if (this.folder || this.browser.folder) this.draghover = true; - }, + return false; +} - onDragleave() { - if (this.folder || this.browser.folder) this.draghover = false; - }, +function onDragenter() { + if (props.folder || props.parentFolder) draghover.value = true; +} - onDrop(e) { - this.draghover = false; +function onDragleave() { + if (props.folder || props.parentFolder) draghover.value = false; +} - // ファイルだったら - if (e.dataTransfer.files.length > 0) { - for (const file of Array.from(e.dataTransfer.files)) { - this.browser.upload(file, this.folder); - } - return; - } +function onDrop(e: DragEvent) { + draghover.value = false; - //#region ドライブのファイル - const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); - if (driveFile != null && driveFile != '') { - const file = JSON.parse(driveFile); - this.browser.removeFile(file.id); - os.api('drive/files/update', { - fileId: file.id, - folderId: this.folder ? this.folder.id : null - }); - } - //#endregion + if (!e.dataTransfer) return; - //#region ドライブのフォルダ - const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_); - if (driveFolder != null && driveFolder != '') { - const folder = JSON.parse(driveFolder); - // 移動先が自分自身ならreject - if (this.folder && folder.id == this.folder.id) return; - this.browser.removeFolder(folder.id); - os.api('drive/folders/update', { - folderId: folder.id, - parentId: this.folder ? this.folder.id : null - }); - } - //#endregion + // ファイルだったら + if (e.dataTransfer.files.length > 0) { + for (const file of Array.from(e.dataTransfer.files)) { + emit('upload', file, props.folder); } + return; + } + + //#region ドライブのファイル + const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); + if (driveFile != null && driveFile != '') { + const file = JSON.parse(driveFile); + emit('removeFile', file.id); + os.api('drive/files/update', { + fileId: file.id, + folderId: props.folder ? props.folder.id : null + }); } -}); + //#endregion + + //#region ドライブのフォルダ + const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_); + if (driveFolder != null && driveFolder != '') { + const folder = JSON.parse(driveFolder); + // 移動先が自分自身ならreject + if (props.folder && folder.id == props.folder.id) return; + emit('removeFolder', folder.id); + os.api('drive/folders/update', { + folderId: folder.id, + parentId: props.folder ? props.folder.id : null + }); + } + //#endregion +} </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/drive.vue b/packages/client/src/components/drive.vue index f8d3d810b7..e27b0a5fbb 100644 --- a/packages/client/src/components/drive.vue +++ b/packages/client/src/components/drive.vue @@ -2,10 +2,24 @@ <div class="yfudmmck"> <nav> <div class="path" @contextmenu.prevent.stop="() => {}"> - <XNavFolder :class="{ current: folder == null }"/> + <XNavFolder + :class="{ current: folder == null }" + :parent-folder="folder" + @move="move" + @upload="upload" + @removeFile="removeFile" + @removeFolder="removeFolder" + /> <template v-for="f in hierarchyFolders"> <span class="separator"><i class="fas fa-angle-right"></i></span> - <XNavFolder :folder="f"/> + <XNavFolder + :folder="f" + :parent-folder="folder" + @move="move" + @upload="upload" + @removeFile="removeFile" + @removeFolder="removeFolder" + /> </template> <span v-if="folder != null" class="separator"><i class="fas fa-angle-right"></i></span> <span v-if="folder != null" class="folder current">{{ folder.name }}</span> @@ -22,617 +36,601 @@ > <div ref="contents" class="contents"> <div v-show="folders.length > 0" ref="foldersContainer" class="folders"> - <XFolder v-for="(f, i) in folders" :key="f.id" v-anim="i" class="folder" :folder="f" :select-mode="select === 'folder'" :is-selected="selectedFolders.some(x => x.id === f.id)" @chosen="chooseFolder"/> + <XFolder + v-for="(f, i) in folders" + :key="f.id" + v-anim="i" + class="folder" + :folder="f" + :select-mode="select === 'folder'" + :is-selected="selectedFolders.some(x => x.id === f.id)" + @chosen="chooseFolder" + @move="move" + @upload="upload" + @removeFile="removeFile" + @removeFolder="removeFolder" + @dragstart="isDragSource = true" + @dragend="isDragSource = false" + /> <!-- 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">{{ $ts.loadMore }}</MkButton> + <MkButton v-if="moreFolders" ref="moreFolders">{{ i18n.locale.loadMore }}</MkButton> </div> <div v-show="files.length > 0" ref="filesContainer" class="files"> - <XFile v-for="(file, i) in files" :key="file.id" v-anim="i" class="file" :file="file" :select-mode="select === 'file'" :is-selected="selectedFiles.some(x => x.id === file.id)" @chosen="chooseFile"/> + <XFile + v-for="(file, i) in files" + :key="file.id" + v-anim="i" + class="file" + :file="file" + :select-mode="select === 'file'" + :is-selected="selectedFiles.some(x => x.id === file.id)" + @chosen="chooseFile" + @dragstart="isDragSource = true" + @dragend="isDragSource = false" + /> <!-- 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">{{ $ts.loadMore }}</MkButton> + <MkButton v-show="moreFiles" ref="loadMoreFiles" @click="fetchMoreFiles">{{ i18n.locale.loadMore }}</MkButton> </div> <div v-if="files.length == 0 && folders.length == 0 && !fetching" class="empty"> - <p v-if="draghover">{{ $t('empty-draghover') }}</p> - <p v-if="!draghover && folder == null"><strong>{{ $ts.emptyDrive }}</strong><br/>{{ $t('empty-drive-description') }}</p> - <p v-if="!draghover && folder != null">{{ $ts.emptyFolder }}</p> + <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> </div> </div> <MkLoading v-if="fetching"/> </div> <div v-if="draghover" class="dropzone"></div> - <input ref="fileInput" type="file" accept="*/*" multiple="multiple" tabindex="-1" @change="onChangeFileInput"/> + <input ref="fileInput" type="file" accept="*/*" multiple tabindex="-1" @change="onChangeFileInput"/> </div> </template> -<script lang="ts"> -import { defineComponent, markRaw } from 'vue'; +<script lang="ts" setup> +import { markRaw, nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue'; +import * as Misskey from 'misskey-js'; import XNavFolder from './drive.nav-folder.vue'; import XFolder from './drive.folder.vue'; import XFile from './drive.file.vue'; import MkButton from './ui/button.vue'; import * as os from '@/os'; import { stream } from '@/stream'; +import { defaultStore } from '@/store'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - XNavFolder, - XFolder, - XFile, - MkButton, - }, - - props: { - initialFolder: { - type: Object, - required: false - }, - type: { - type: String, - required: false, - default: undefined - }, - multiple: { - type: Boolean, - required: false, - default: false - }, - select: { - type: String, - required: false, - default: null - } - }, - - emits: ['selected', 'change-selection', 'move-root', 'cd', 'open-folder'], - - data() { - return { - /** - * 現在の階層(フォルダ) - * * null でルートを表す - */ - folder: null, +const props = withDefaults(defineProps<{ + initialFolder?: Misskey.entities.DriveFolder; + type?: string; + multiple?: boolean; + select?: 'file' | 'folder' | null; +}>(), { + multiple: false, + select: null, +}); - files: [], - folders: [], - moreFiles: false, - moreFolders: false, - hierarchyFolders: [], - selectedFiles: [], - selectedFolders: [], - uploadings: os.uploads, - connection: null, +const emit = defineEmits<{ + (e: 'selected', v: Misskey.entities.DriveFile | Misskey.entities.DriveFolder): void; + (e: 'change-selection', v: Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]): void; + (e: 'move-root'): void; + (e: 'cd', v: Misskey.entities.DriveFolder | null): void; + (e: 'open-folder', v: Misskey.entities.DriveFolder): void; +}>(); - /** - * ドロップされようとしているか - */ - draghover: false, +const loadMoreFiles = ref<InstanceType<typeof MkButton>>(); +const fileInput = ref<HTMLInputElement>(); - /** - * 自信の所有するアイテムがドラッグをスタートさせたか - * (自分自身の階層にドロップできないようにするためのフラグ) - */ - isDragSource: false, +const folder = ref<Misskey.entities.DriveFolder | null>(null); +const files = ref<Misskey.entities.DriveFile[]>([]); +const folders = ref<Misskey.entities.DriveFolder[]>([]); +const moreFiles = ref(false); +const moreFolders = ref(false); +const hierarchyFolders = ref<Misskey.entities.DriveFolder[]>([]); +const selectedFiles = ref<Misskey.entities.DriveFile[]>([]); +const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]); +const uploadings = os.uploads; +const connection = stream.useChannel('drive'); - fetching: true, +// ドロップされようとしているか +const draghover = ref(false); - ilFilesObserver: new IntersectionObserver( - (entries) => entries.some((entry) => entry.isIntersecting) - && !this.fetching && this.moreFiles && - this.fetchMoreFiles() - ), - moreFilesElement: null as Element, +// 自身の所有するアイテムがドラッグをスタートさせたか +// (自分自身の階層にドロップできないようにするためのフラグ) +const isDragSource = ref(false); - }; - }, +const fetching = ref(true); - watch: { - folder() { - this.$emit('cd', this.folder); - } - }, +const ilFilesObserver = new IntersectionObserver( + (entries) => entries.some((entry) => entry.isIntersecting) && !fetching.value && moreFiles.value && fetchMoreFiles() +) - mounted() { - if (this.$store.state.enableInfiniteScroll && this.$refs.loadMoreFiles) { - this.$nextTick(() => { - this.ilFilesObserver.observe((this.$refs.loadMoreFiles as Vue).$el) - }); - } +watch(folder, () => emit('cd', folder.value)); - this.connection = markRaw(stream.useChannel('drive')); - - this.connection.on('fileCreated', this.onStreamDriveFileCreated); - this.connection.on('fileUpdated', this.onStreamDriveFileUpdated); - this.connection.on('fileDeleted', this.onStreamDriveFileDeleted); - this.connection.on('folderCreated', this.onStreamDriveFolderCreated); - this.connection.on('folderUpdated', this.onStreamDriveFolderUpdated); - this.connection.on('folderDeleted', this.onStreamDriveFolderDeleted); +function onStreamDriveFileCreated(file: Misskey.entities.DriveFile) { + addFile(file, true); +} - if (this.initialFolder) { - this.move(this.initialFolder); - } else { - this.fetch(); - } - }, +function onStreamDriveFileUpdated(file: Misskey.entities.DriveFile) { + const current = folder.value ? folder.value.id : null; + if (current != file.folderId) { + removeFile(file); + } else { + addFile(file, true); + } +} - activated() { - if (this.$store.state.enableInfiniteScroll) { - this.$nextTick(() => { - this.ilFilesObserver.observe((this.$refs.loadMoreFiles as Vue).$el) - }); - } - }, +function onStreamDriveFileDeleted(fileId: string) { + removeFile(fileId); +} - beforeUnmount() { - this.connection.dispose(); - this.ilFilesObserver.disconnect(); - }, +function onStreamDriveFolderCreated(createdFolder: Misskey.entities.DriveFolder) { + addFolder(createdFolder, true); +} - methods: { - onStreamDriveFileCreated(file) { - this.addFile(file, true); - }, +function onStreamDriveFolderUpdated(updatedFolder: Misskey.entities.DriveFolder) { + const current = folder.value ? folder.value.id : null; + if (current != updatedFolder.parentId) { + removeFolder(updatedFolder); + } else { + addFolder(updatedFolder, true); + } +} - onStreamDriveFileUpdated(file) { - const current = this.folder ? this.folder.id : null; - if (current != file.folderId) { - this.removeFile(file); - } else { - this.addFile(file, true); - } - }, +function onStreamDriveFolderDeleted(folderId: string) { + removeFolder(folderId); +} - onStreamDriveFileDeleted(fileId) { - this.removeFile(fileId); - }, +function onDragover(e: DragEvent): any { + if (!e.dataTransfer) return; - onStreamDriveFolderCreated(folder) { - this.addFolder(folder, true); - }, + // ドラッグ元が自分自身の所有するアイテムだったら + if (isDragSource.value) { + // 自分自身にはドロップさせない + e.dataTransfer.dropEffect = 'none'; + return; + } - onStreamDriveFolderUpdated(folder) { - const current = this.folder ? this.folder.id : null; - if (current != folder.parentId) { - this.removeFolder(folder); - } else { - this.addFolder(folder, true); - } - }, + const isFile = e.dataTransfer.items[0].kind == 'file'; + const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_; + const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_; + if (isFile || isDriveFile || isDriveFolder) { + e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; + } else { + e.dataTransfer.dropEffect = 'none'; + } - onStreamDriveFolderDeleted(folderId) { - this.removeFolder(folderId); - }, + return false; +} - onDragover(e): any { - // ドラッグ元が自分自身の所有するアイテムだったら - if (this.isDragSource) { - // 自分自身にはドロップさせない - e.dataTransfer.dropEffect = 'none'; - return; - } +function onDragenter() { + if (!isDragSource.value) draghover.value = true; +} - const isFile = e.dataTransfer.items[0].kind == 'file'; - const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_; - const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_; +function onDragleave() { + draghover.value = false; +} - if (isFile || isDriveFile || isDriveFolder) { - e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; - } else { - e.dataTransfer.dropEffect = 'none'; - } +function onDrop(e: DragEvent): any { + draghover.value = false; - return false; - }, + if (!e.dataTransfer) return; - onDragenter(e) { - if (!this.isDragSource) this.draghover = true; - }, + // ドロップされてきたものがファイルだったら + if (e.dataTransfer.files.length > 0) { + for (const file of Array.from(e.dataTransfer.files)) { + upload(file, folder.value); + } + return; + } - onDragleave(e) { - this.draghover = false; - }, + //#region ドライブのファイル + const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); + if (driveFile != null && driveFile != '') { + const file = JSON.parse(driveFile); + if (files.value.some(f => f.id == file.id)) return; + removeFile(file.id); + os.api('drive/files/update', { + fileId: file.id, + folderId: folder.value ? folder.value.id : null + }); + } + //#endregion - onDrop(e): any { - this.draghover = false; + //#region ドライブのフォルダ + const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_); + if (driveFolder != null && driveFolder != '') { + const droppedFolder = JSON.parse(driveFolder); - // ドロップされてきたものがファイルだったら - if (e.dataTransfer.files.length > 0) { - for (const file of Array.from(e.dataTransfer.files)) { - this.upload(file, this.folder); - } - return; + // 移動先が自分自身ならreject + if (folder.value && droppedFolder.id == folder.value.id) return false; + if (folders.value.some(f => f.id == droppedFolder.id)) return false; + removeFolder(droppedFolder.id); + os.api('drive/folders/update', { + folderId: droppedFolder.id, + parentId: folder.value ? folder.value.id : null + }).then(() => { + // noop + }).catch(err => { + switch (err) { + case 'detected-circular-definition': + os.alert({ + title: i18n.locale.unableToProcess, + text: i18n.locale.circularReferenceFolder + }); + break; + default: + os.alert({ + type: 'error', + text: i18n.locale.somethingHappened + }); } + }); + } + //#endregion +} - //#region ドライブのファイル - const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); - if (driveFile != null && driveFile != '') { - const file = JSON.parse(driveFile); - if (this.files.some(f => f.id == file.id)) return; - this.removeFile(file.id); - os.api('drive/files/update', { - fileId: file.id, - folderId: this.folder ? this.folder.id : null - }); - } - //#endregion +function selectLocalFile() { + fileInput.value?.click(); +} - //#region ドライブのフォルダ - const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_); - if (driveFolder != null && driveFolder != '') { - const folder = JSON.parse(driveFolder); +function urlUpload() { + os.inputText({ + title: i18n.locale.uploadFromUrl, + type: 'url', + placeholder: i18n.locale.uploadFromUrlDescription + }).then(({ canceled, result: url }) => { + if (canceled || !url) return; + os.api('drive/files/upload-from-url', { + url: url, + folderId: folder.value ? folder.value.id : undefined + }); - // 移動先が自分自身ならreject - if (this.folder && folder.id == this.folder.id) return false; - if (this.folders.some(f => f.id == folder.id)) return false; - this.removeFolder(folder.id); - os.api('drive/folders/update', { - folderId: folder.id, - parentId: this.folder ? this.folder.id : null - }).then(() => { - // noop - }).catch(err => { - switch (err) { - case 'detected-circular-definition': - os.alert({ - title: this.$ts.unableToProcess, - text: this.$ts.circularReferenceFolder - }); - break; - default: - os.alert({ - type: 'error', - text: this.$ts.somethingHappened - }); - } - }); - } - //#endregion - }, + os.alert({ + title: i18n.locale.uploadFromUrlRequested, + text: i18n.locale.uploadFromUrlMayTakeTime + }); + }); +} - selectLocalFile() { - (this.$refs.fileInput as any).click(); - }, +function createFolder() { + os.inputText({ + title: i18n.locale.createFolder, + placeholder: i18n.locale.folderName + }).then(({ canceled, result: name }) => { + if (canceled) return; + os.api('drive/folders/create', { + name: name, + parentId: folder.value ? folder.value.id : undefined + }).then(createdFolder => { + addFolder(createdFolder, true); + }); + }); +} - urlUpload() { - os.inputText({ - title: this.$ts.uploadFromUrl, - type: 'url', - placeholder: this.$ts.uploadFromUrlDescription - }).then(({ canceled, result: url }) => { - if (canceled) return; - os.api('drive/files/upload-from-url', { - url: url, - folderId: this.folder ? this.folder.id : undefined - }); +function renameFolder(folderToRename: Misskey.entities.DriveFolder) { + os.inputText({ + title: i18n.locale.renameFolder, + placeholder: i18n.locale.inputNewFolderName, + default: folderToRename.name + }).then(({ canceled, result: name }) => { + if (canceled) return; + os.api('drive/folders/update', { + folderId: folderToRename.id, + name: name + }).then(updatedFolder => { + // FIXME: 画面を更新するために自分自身に移動 + move(updatedFolder); + }); + }); +} +function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) { + os.api('drive/folders/delete', { + folderId: folderToDelete.id + }).then(() => { + // 削除時に親フォルダに移動 + move(folderToDelete.parentId); + }).catch(err => { + switch(err.id) { + case 'b0fc8a17-963c-405d-bfbc-859a487295e1': os.alert({ - title: this.$ts.uploadFromUrlRequested, - text: this.$ts.uploadFromUrlMayTakeTime + type: 'error', + title: i18n.locale.unableToDelete, + text: i18n.locale.hasChildFilesOrFolders }); - }); - }, - - createFolder() { - os.inputText({ - title: this.$ts.createFolder, - placeholder: this.$ts.folderName - }).then(({ canceled, result: name }) => { - if (canceled) return; - os.api('drive/folders/create', { - name: name, - parentId: this.folder ? this.folder.id : undefined - }).then(folder => { - this.addFolder(folder, true); + break; + default: + os.alert({ + type: 'error', + text: i18n.locale.unableToDelete }); - }); - }, + } + }); +} - renameFolder(folder) { - os.inputText({ - title: this.$ts.renameFolder, - placeholder: this.$ts.inputNewFolderName, - default: folder.name - }).then(({ canceled, result: name }) => { - if (canceled) return; - os.api('drive/folders/update', { - folderId: folder.id, - name: name - }).then(folder => { - // FIXME: 画面を更新するために自分自身に移動 - this.move(folder); - }); - }); - }, +function onChangeFileInput() { + if (!fileInput.value?.files) return; + for (const file of Array.from(fileInput.value.files)) { + upload(file, folder.value); + } +} - deleteFolder(folder) { - os.api('drive/folders/delete', { - folderId: folder.id - }).then(() => { - // 削除時に親フォルダに移動 - this.move(folder.parentId); - }).catch(err => { - switch(err.id) { - case 'b0fc8a17-963c-405d-bfbc-859a487295e1': - os.alert({ - type: 'error', - title: this.$ts.unableToDelete, - text: this.$ts.hasChildFilesOrFolders - }); - break; - default: - os.alert({ - type: 'error', - text: this.$ts.unableToDelete - }); - } - }); - }, +function upload(file: File, folderToUpload?: Misskey.entities.DriveFolder | null) { + os.upload(file, (folderToUpload && typeof folderToUpload == 'object') ? folderToUpload.id : null).then(res => { + addFile(res, true); + }); +} - onChangeFileInput() { - for (const file of Array.from((this.$refs.fileInput as any).files)) { - this.upload(file, this.folder); - } - }, +function chooseFile(file: Misskey.entities.DriveFile) { + const isAlreadySelected = selectedFiles.value.some(f => f.id == file.id); + if (props.multiple) { + if (isAlreadySelected) { + selectedFiles.value = selectedFiles.value.filter(f => f.id != file.id); + } else { + selectedFiles.value.push(file); + } + emit('change-selection', selectedFiles.value); + } else { + if (isAlreadySelected) { + emit('selected', file); + } else { + selectedFiles.value = [file]; + emit('change-selection', [file]); + } + } +} - upload(file, folder) { - if (folder && typeof folder == 'object') folder = folder.id; - os.upload(file, folder).then(res => { - this.addFile(res, true); - }); - }, +function chooseFolder(folderToChoose: Misskey.entities.DriveFolder) { + const isAlreadySelected = selectedFolders.value.some(f => f.id == folderToChoose.id); + if (props.multiple) { + if (isAlreadySelected) { + selectedFolders.value = selectedFolders.value.filter(f => f.id != folderToChoose.id); + } else { + selectedFolders.value.push(folderToChoose); + } + emit('change-selection', selectedFolders.value); + } else { + if (isAlreadySelected) { + emit('selected', folderToChoose); + } else { + selectedFolders.value = [folderToChoose]; + emit('change-selection', [folderToChoose]); + } + } +} - chooseFile(file) { - const isAlreadySelected = this.selectedFiles.some(f => f.id == file.id); - if (this.multiple) { - if (isAlreadySelected) { - this.selectedFiles = this.selectedFiles.filter(f => f.id != file.id); - } else { - this.selectedFiles.push(file); - } - this.$emit('change-selection', this.selectedFiles); - } else { - if (isAlreadySelected) { - this.$emit('selected', file); - } else { - this.selectedFiles = [file]; - this.$emit('change-selection', [file]); - } - } - }, +function move(target?: Misskey.entities.DriveFolder) { + if (!target) { + goRoot(); + return; + } else if (typeof target == 'object') { + target = target.id; + } - chooseFolder(folder) { - const isAlreadySelected = this.selectedFolders.some(f => f.id == folder.id); - if (this.multiple) { - if (isAlreadySelected) { - this.selectedFolders = this.selectedFolders.filter(f => f.id != folder.id); - } else { - this.selectedFolders.push(folder); - } - this.$emit('change-selection', this.selectedFolders); - } else { - if (isAlreadySelected) { - this.$emit('selected', folder); - } else { - this.selectedFolders = [folder]; - this.$emit('change-selection', [folder]); - } - } - }, + fetching.value = true; - move(target) { - if (target == null) { - this.goRoot(); - return; - } else if (typeof target == 'object') { - target = target.id; - } + os.api('drive/folders/show', { + folderId: target + }).then(folderToMove => { + folder.value = folderToMove; + hierarchyFolders.value = []; - this.fetching = true; + const dive = folderToDive => { + hierarchyFolders.value.unshift(folderToDive); + if (folderToDive.parent) dive(folderToDive.parent); + }; + + if (folderToMove.parent) dive(folderToMove.parent); - os.api('drive/folders/show', { - folderId: target - }).then(folder => { - this.folder = folder; - this.hierarchyFolders = []; + emit('open-folder', folderToMove); + fetch(); + }); +} - const dive = folder => { - this.hierarchyFolders.unshift(folder); - if (folder.parent) dive(folder.parent); - }; +function addFolder(folderToAdd: Misskey.entities.DriveFolder, unshift = false) { + const current = folder.value ? folder.value.id : null; + if (current != folderToAdd.parentId) return; - if (folder.parent) dive(folder.parent); + if (folders.value.some(f => f.id == folderToAdd.id)) { + const exist = folders.value.map(f => f.id).indexOf(folderToAdd.id); + folders.value[exist] = folderToAdd; + return; + } - this.$emit('open-folder', folder); - this.fetch(); - }); - }, + if (unshift) { + folders.value.unshift(folderToAdd); + } else { + folders.value.push(folderToAdd); + } +} - addFolder(folder, unshift = false) { - const current = this.folder ? this.folder.id : null; - if (current != folder.parentId) return; +function addFile(fileToAdd: Misskey.entities.DriveFile, unshift = false) { + const current = folder.value ? folder.value.id : null; + if (current != fileToAdd.folderId) return; - if (this.folders.some(f => f.id == folder.id)) { - const exist = this.folders.map(f => f.id).indexOf(folder.id); - this.folders[exist] = folder; - return; - } + if (files.value.some(f => f.id == fileToAdd.id)) { + const exist = files.value.map(f => f.id).indexOf(fileToAdd.id); + files.value[exist] = fileToAdd; + return; + } - if (unshift) { - this.folders.unshift(folder); - } else { - this.folders.push(folder); - } - }, + if (unshift) { + files.value.unshift(fileToAdd); + } else { + files.value.push(fileToAdd); + } +} - addFile(file, unshift = false) { - const current = this.folder ? this.folder.id : null; - if (current != file.folderId) return; +function removeFolder(folderToRemove: Misskey.entities.DriveFolder | string) { + const folderIdToRemove = typeof folderToRemove === 'object' ? folderToRemove.id : folderToRemove; + folders.value = folders.value.filter(f => f.id != folderIdToRemove); +} - if (this.files.some(f => f.id == file.id)) { - const exist = this.files.map(f => f.id).indexOf(file.id); - this.files[exist] = file; - return; - } +function removeFile(file: Misskey.entities.DriveFile | string) { + const fileId = typeof file === 'object' ? file.id : file; + files.value = files.value.filter(f => f.id != fileId); +} - if (unshift) { - this.files.unshift(file); - } else { - this.files.push(file); - } - }, +function appendFile(file: Misskey.entities.DriveFile) { + addFile(file); +} - removeFolder(folder) { - if (typeof folder == 'object') folder = folder.id; - this.folders = this.folders.filter(f => f.id != folder); - }, +function appendFolder(folderToAppend: Misskey.entities.DriveFolder) { + addFolder(folderToAppend); +} +/* +function prependFile(file: Misskey.entities.DriveFile) { + addFile(file, true); +} - removeFile(file) { - if (typeof file == 'object') file = file.id; - this.files = this.files.filter(f => f.id != file); - }, +function prependFolder(folderToPrepend: Misskey.entities.DriveFolder) { + addFolder(folderToPrepend, true); +} +*/ +function goRoot() { + // 既にrootにいるなら何もしない + if (folder.value == null) return; - appendFile(file) { - this.addFile(file); - }, + folder.value = null; + hierarchyFolders.value = []; + emit('move-root'); + fetch(); +} - appendFolder(folder) { - this.addFolder(folder); - }, +async function fetch() { + folders.value = []; + files.value = []; + moreFolders.value = false; + moreFiles.value = false; + fetching.value = true; - prependFile(file) { - this.addFile(file, true); - }, + const foldersMax = 30; + const filesMax = 30; - prependFolder(folder) { - this.addFolder(folder, true); - }, + const foldersPromise = os.api('drive/folders', { + folderId: folder.value ? folder.value.id : null, + limit: foldersMax + 1 + }).then(fetchedFolders => { + if (fetchedFolders.length == foldersMax + 1) { + moreFolders.value = true; + fetchedFolders.pop(); + } + return fetchedFolders; + }); - goRoot() { - // 既にrootにいるなら何もしない - if (this.folder == null) return; + const filesPromise = os.api('drive/files', { + folderId: folder.value ? folder.value.id : null, + type: props.type, + limit: filesMax + 1 + }).then(fetchedFiles => { + if (fetchedFiles.length == filesMax + 1) { + moreFiles.value = true; + fetchedFiles.pop(); + } + return fetchedFiles; + }); - this.folder = null; - this.hierarchyFolders = []; - this.$emit('move-root'); - this.fetch(); - }, + const [fetchedFolders, fetchedFiles] = await Promise.all([foldersPromise, filesPromise]); - fetch() { - this.folders = []; - this.files = []; - this.moreFolders = false; - this.moreFiles = false; - this.fetching = true; + for (const x of fetchedFolders) appendFolder(x); + for (const x of fetchedFiles) appendFile(x); - let fetchedFolders = null; - let fetchedFiles = null; + fetching.value = false; +} - const foldersMax = 30; - const filesMax = 30; +function fetchMoreFiles() { + fetching.value = true; - // フォルダ一覧取得 - os.api('drive/folders', { - folderId: this.folder ? this.folder.id : null, - limit: foldersMax + 1 - }).then(folders => { - if (folders.length == foldersMax + 1) { - this.moreFolders = true; - folders.pop(); - } - fetchedFolders = folders; - complete(); - }); + const max = 30; - // ファイル一覧取得 - os.api('drive/files', { - folderId: this.folder ? this.folder.id : null, - type: this.type, - limit: filesMax + 1 - }).then(files => { - if (files.length == filesMax + 1) { - this.moreFiles = true; - files.pop(); - } - fetchedFiles = files; - complete(); - }); + // ファイル一覧取得 + os.api('drive/files', { + folderId: folder.value ? folder.value.id : null, + type: props.type, + untilId: files.value[files.value.length - 1].id, + limit: max + 1 + }).then(files => { + if (files.length == max + 1) { + moreFiles.value = true; + files.pop(); + } else { + moreFiles.value = false; + } + for (const x of files) appendFile(x); + fetching.value = false; + }); +} - let flag = false; - const complete = () => { - if (flag) { - for (const x of fetchedFolders) this.appendFolder(x); - for (const x of fetchedFiles) this.appendFile(x); - this.fetching = false; - } else { - flag = true; - } - }; - }, +function getMenu() { + return [{ + text: i18n.locale.addFile, + type: 'label' + }, { + text: i18n.locale.upload, + icon: 'fas fa-upload', + action: () => { selectLocalFile(); } + }, { + text: i18n.locale.fromUrl, + icon: 'fas fa-link', + action: () => { urlUpload(); } + }, null, { + text: folder.value ? folder.value.name : i18n.locale.drive, + type: 'label' + }, folder.value ? { + text: i18n.locale.renameFolder, + icon: 'fas fa-i-cursor', + action: () => { renameFolder(folder.value); } + } : undefined, folder.value ? { + text: i18n.locale.deleteFolder, + icon: 'fas fa-trash-alt', + action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); } + } : undefined, { + text: i18n.locale.createFolder, + icon: 'fas fa-folder-plus', + action: () => { createFolder(); } + }]; +} - fetchMoreFiles() { - this.fetching = true; +function showMenu(ev: MouseEvent) { + os.popupMenu(getMenu(), (ev.currentTarget || ev.target || undefined) as HTMLElement | undefined); +} - const max = 30; +function onContextmenu(ev: MouseEvent) { + os.contextMenu(getMenu(), ev); +} - // ファイル一覧取得 - os.api('drive/files', { - folderId: this.folder ? this.folder.id : null, - type: this.type, - untilId: this.files[this.files.length - 1].id, - limit: max + 1 - }).then(files => { - if (files.length == max + 1) { - this.moreFiles = true; - files.pop(); - } else { - this.moreFiles = false; - } - for (const x of files) this.appendFile(x); - this.fetching = false; - }); - }, +onMounted(() => { + if (defaultStore.state.enableInfiniteScroll && loadMoreFiles.value) { + nextTick(() => { + ilFilesObserver.observe(loadMoreFiles.value?.$el) + }); + } - getMenu() { - return [{ - text: this.$ts.addFile, - type: 'label' - }, { - text: this.$ts.upload, - icon: 'fas fa-upload', - action: () => { this.selectLocalFile(); } - }, { - text: this.$ts.fromUrl, - icon: 'fas fa-link', - action: () => { this.urlUpload(); } - }, null, { - text: this.folder ? this.folder.name : this.$ts.drive, - type: 'label' - }, this.folder ? { - text: this.$ts.renameFolder, - icon: 'fas fa-i-cursor', - action: () => { this.renameFolder(this.folder); } - } : undefined, this.folder ? { - text: this.$ts.deleteFolder, - icon: 'fas fa-trash-alt', - action: () => { this.deleteFolder(this.folder); } - } : undefined, { - text: this.$ts.createFolder, - icon: 'fas fa-folder-plus', - action: () => { this.createFolder(); } - }]; - }, + connection.on('fileCreated', onStreamDriveFileCreated); + connection.on('fileUpdated', onStreamDriveFileUpdated); + connection.on('fileDeleted', onStreamDriveFileDeleted); + connection.on('folderCreated', onStreamDriveFolderCreated); + connection.on('folderUpdated', onStreamDriveFolderUpdated); + connection.on('folderDeleted', onStreamDriveFolderDeleted); - showMenu(ev) { - os.popupMenu(this.getMenu(), ev.currentTarget || ev.target); - }, + if (props.initialFolder) { + move(props.initialFolder); + } else { + fetch(); + } +}); - onContextmenu(ev) { - os.contextMenu(this.getMenu(), ev); - }, +onActivated(() => { + if (defaultStore.state.enableInfiniteScroll) { + nextTick(() => { + ilFilesObserver.observe(loadMoreFiles.value?.$el) + }); } }); + +onBeforeUnmount(() => { + connection.dispose(); + ilFilesObserver.disconnect(); +}); </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/emoji-picker-dialog.vue b/packages/client/src/components/emoji-picker-dialog.vue index 51c634dd8e..f06a24636c 100644 --- a/packages/client/src/components/emoji-picker-dialog.vue +++ b/packages/client/src/components/emoji-picker-dialog.vue @@ -1,58 +1,65 @@ <template> -<MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'middle'" :prefer-type="asReactionPicker && $store.state.reactionPickerUseDrawerForMobile === false ? 'popup' : 'auto'" :transparent-bg="true" :manual-showing="manualShowing" :src="src" @click="$refs.modal.close()" @opening="opening" @close="$emit('close')" @closed="$emit('closed')"> - <MkEmojiPicker ref="picker" class="ryghynhb _popup _shadow" :class="{ drawer: type === 'drawer' }" :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" :as-drawer="type === 'drawer'" :max-height="maxHeight" @chosen="chosen"/> +<MkModal + ref="modal" + v-slot="{ type, maxHeight }" + :z-priority="'middle'" + :prefer-type="asReactionPicker && defaultStore.state.reactionPickerUseDrawerForMobile === false ? 'popup' : 'auto'" + :transparent-bg="true" + :manual-showing="manualShowing" + :src="src" + @click="modal?.close()" + @opening="opening" + @close="emit('close')" + @closed="emit('closed')" +> + <MkEmojiPicker + ref="picker" + class="ryghynhb _popup _shadow" + :class="{ drawer: type === 'drawer' }" + :show-pinned="showPinned" + :as-reaction-picker="asReactionPicker" + :as-drawer="type === 'drawer'" + :max-height="maxHeight" + @chosen="chosen" + /> </MkModal> </template> -<script lang="ts"> -import { defineComponent, markRaw } from 'vue'; +<script lang="ts" setup> +import { ref } from 'vue'; import MkModal from '@/components/ui/modal.vue'; import MkEmojiPicker from '@/components/emoji-picker.vue'; +import { defaultStore } from '@/store'; -export default defineComponent({ - components: { - MkModal, - MkEmojiPicker, - }, - - props: { - manualShowing: { - type: Boolean, - required: false, - default: null, - }, - src: { - required: false - }, - showPinned: { - required: false, - default: true - }, - asReactionPicker: { - required: false - }, - }, - - emits: ['done', 'close', 'closed'], +withDefaults(defineProps<{ + manualShowing?: boolean; + src?: HTMLElement; + showPinned?: boolean; + asReactionPicker?: boolean; +}>(), { + manualShowing: false, + showPinned: true, + asReactionPicker: false, +}); - data() { - return { +const emit = defineEmits<{ + (e: 'done', v: any): void; + (e: 'close'): void; + (e: 'closed'): void; +}>(); - }; - }, +const modal = ref<InstanceType<typeof MkModal>>(); +const picker = ref<InstanceType<typeof MkEmojiPicker>>(); - methods: { - chosen(emoji: any) { - this.$emit('done', emoji); - this.$refs.modal.close(); - }, +function chosen(emoji: any) { + emit('done', emoji); + modal.value?.close(); +} - opening() { - this.$refs.picker.reset(); - this.$refs.picker.focus(); - } - } -}); +function opening() { + picker.value?.reset(); + picker.value?.focus(); +} </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/emoji-picker-window.vue b/packages/client/src/components/emoji-picker-window.vue index 0ffa0c1187..4d27fb48ba 100644 --- a/packages/client/src/components/emoji-picker-window.vue +++ b/packages/client/src/components/emoji-picker-window.vue @@ -5,50 +5,33 @@ :can-resize="false" :mini="true" :front="true" - @closed="$emit('closed')" + @closed="emit('closed')" > <MkEmojiPicker :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" @chosen="chosen"/> </MkWindow> </template> -<script lang="ts"> -import { defineComponent, markRaw } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import MkWindow from '@/components/ui/window.vue'; import MkEmojiPicker from '@/components/emoji-picker.vue'; -export default defineComponent({ - components: { - MkWindow, - MkEmojiPicker, - }, - - props: { - src: { - required: false - }, - showPinned: { - required: false, - default: true - }, - asReactionPicker: { - required: false - }, - }, - - emits: ['chosen', 'closed'], - - data() { - return { +withDefaults(defineProps<{ + src?: HTMLElement; + showPinned?: boolean; + asReactionPicker?: boolean; +}>(), { + showPinned: true, +}); - }; - }, +const emit = defineEmits<{ + (e: 'chosen', v: any): void; + (e: 'closed'): void; +}>(); - methods: { - chosen(emoji: any) { - this.$emit('chosen', emoji); - }, - } -}); +function chosen(emoji: any) { + emit('chosen', emoji); +} </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/emoji-picker.section.vue b/packages/client/src/components/emoji-picker.section.vue index 08c4f6813d..1026e894d1 100644 --- a/packages/client/src/components/emoji-picker.section.vue +++ b/packages/client/src/components/emoji-picker.section.vue @@ -7,7 +7,7 @@ <button v-for="emoji in emojis" :key="emoji" class="_button" - @click="chosen(emoji, $event)" + @click="emit('chosen', emoji, $event)" > <MkEmoji :emoji="emoji" :normal="true"/> </button> @@ -15,35 +15,19 @@ </section> </template> -<script lang="ts"> -import { defineComponent, markRaw } from 'vue'; -import { getStaticImageUrl } from '@/scripts/get-static-image-url'; +<script lang="ts" setup> +import { ref } from 'vue'; -export default defineComponent({ - props: { - emojis: { - required: true, - }, - initialShown: { - required: false - } - }, +const props = defineProps<{ + emojis: string[]; + initialShown?: boolean; +}>(); - emits: ['chosen'], +const emit = defineEmits<{ + (e: 'chosen', v: string, ev: MouseEvent): void; +}>(); - data() { - return { - getStaticImageUrl, - shown: this.initialShown, - }; - }, - - methods: { - chosen(emoji: any, ev) { - this.$parent.chosen(emoji, ev); - }, - } -}); +const shown = ref(!!props.initialShown); </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/emoji-picker.vue b/packages/client/src/components/emoji-picker.vue index a8eed1ca21..96670fa58c 100644 --- a/packages/client/src/components/emoji-picker.vue +++ b/packages/client/src/components/emoji-picker.vue @@ -1,18 +1,18 @@ <template> -<div class="omfetrab" :class="['w' + width, 'h' + height, { big, asDrawer }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : null }"> - <input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="$ts.search" @paste.stop="paste" @keyup.enter="done()"> +<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()"> <div ref="emojis" class="emojis"> <section class="result"> <div v-if="searchResultCustom.length > 0"> <button v-for="emoji in searchResultCustom" - :key="emoji" + :key="emoji.id" class="_button" :title="emoji.name" tabindex="0" @click="chosen(emoji, $event)" > - <MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/> - <img v-else :src="$store.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/> + <!--<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>--> + <img :src="disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/> </button> </div> <div v-if="searchResultUnicode.length > 0"> @@ -43,9 +43,9 @@ </section> <section> - <header class="_acrylic"><i class="far fa-clock fa-fw"></i> {{ $ts.recentUsed }}</header> + <header class="_acrylic"><i class="far fa-clock fa-fw"></i> {{ i18n.locale.recentUsed }}</header> <div> - <button v-for="emoji in $store.state.recentlyUsedEmojis" + <button v-for="emoji in recentlyUsedEmojis" :key="emoji" class="_button" @click="chosen(emoji, $event)" @@ -56,12 +56,12 @@ </section> </div> <div> - <header class="_acrylic">{{ $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 + ':')">{{ category || $ts.other }}</XSection> + <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> </div> <div> - <header class="_acrylic">{{ $ts.emoji }}</header> - <XSection v-for="category in categories" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)">{{ category }}</XSection> + <header class="_acrylic">{{ i18n.locale.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> <div class="tabs"> @@ -73,277 +73,272 @@ </div> </template> -<script lang="ts"> -import { defineComponent, markRaw } from 'vue'; -import { emojilist } from '@/scripts/emojilist'; +<script lang="ts" setup> +import { ref, computed, watch, onMounted } from 'vue'; +import * as Misskey from 'misskey-js'; +import { emojilist, UnicodeEmojiDef, unicodeEmojiCategories as categories } from '@/scripts/emojilist'; import { getStaticImageUrl } from '@/scripts/get-static-image-url'; import Ripple from '@/components/ripple.vue'; import * as os from '@/os'; import { isTouchUsing } from '@/scripts/touch'; import { isMobile } from '@/scripts/is-mobile'; -import { emojiCategories } from '@/instance'; +import { emojiCategories, instance } from '@/instance'; import XSection from './emoji-picker.section.vue'; +import { i18n } from '@/i18n'; +import { defaultStore } from '@/store'; -export default defineComponent({ - components: { - XSection - }, - - props: { - showPinned: { - required: false, - default: true, - }, - asReactionPicker: { - required: false, - }, - maxHeight: { - type: Number, - required: false, - }, - asDrawer: { - type: Boolean, - required: false - }, - }, +const props = withDefaults(defineProps<{ + showPinned?: boolean; + asReactionPicker?: boolean; + maxHeight?: number; + asDrawer?: boolean; +}>(), { + showPinned: true, +}); - emits: ['chosen'], +const emit = defineEmits<{ + (e: 'chosen', v: string): void; +}>(); - data() { - return { - emojilist: markRaw(emojilist), - getStaticImageUrl, - pinned: this.$store.reactiveState.reactions, - width: this.asReactionPicker ? this.$store.state.reactionPickerWidth : 3, - height: this.asReactionPicker ? this.$store.state.reactionPickerHeight : 2, - big: this.asReactionPicker ? isTouchUsing : false, - customEmojiCategories: emojiCategories, - customEmojis: this.$instance.emojis, - q: null, - searchResultCustom: [], - searchResultUnicode: [], - tab: 'index', - categories: ['face', 'people', 'animals_and_nature', 'food_and_drink', 'activity', 'travel_and_places', 'objects', 'symbols', 'flags'], - }; - }, +const search = ref<HTMLInputElement>(); +const emojis = ref<HTMLDivElement>(); - watch: { - q() { - this.$refs.emojis.scrollTop = 0; +const { + reactions: pinned, + reactionPickerWidth, + reactionPickerHeight, + disableShowingAnimatedImages, + recentlyUsedEmojis, +} = defaultStore.reactiveState; - if (this.q == null || this.q === '') { - this.searchResultCustom = []; - this.searchResultUnicode = []; - return; - } +const width = computed(() => props.asReactionPicker ? reactionPickerWidth.value : 3); +const height = computed(() => props.asReactionPicker ? reactionPickerHeight.value : 2); +const big = props.asReactionPicker ? isTouchUsing : false; +const customEmojiCategories = emojiCategories; +const customEmojis = instance.emojis; +const q = ref<string | null>(null); +const searchResultCustom = ref<Misskey.entities.CustomEmoji[]>([]); +const searchResultUnicode = ref<UnicodeEmojiDef[]>([]); +const tab = ref<'index' | 'custom' | 'unicode' | 'tags'>('index'); - const q = this.q.replace(/:/g, ''); +watch(q, () => { + if (emojis.value) emojis.value.scrollTop = 0; - const searchCustom = () => { - const max = 8; - const emojis = this.customEmojis; - const matches = new Set(); + if (q.value == null || q.value === '') { + searchResultCustom.value = []; + searchResultUnicode.value = []; + return; + } - const exactMatch = emojis.find(e => e.name === q); - if (exactMatch) matches.add(exactMatch); + const newQ = q.value.replace(/:/g, ''); - if (q.includes(' ')) { // AND検索 - const keywords = q.split(' '); + const searchCustom = () => { + const max = 8; + const emojis = customEmojis; + const matches = new Set<Misskey.entities.CustomEmoji>(); - // 名前にキーワードが含まれている - for (const emoji of emojis) { - if (keywords.every(keyword => emoji.name.includes(keyword))) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - if (matches.size >= max) return matches; + const exactMatch = emojis.find(e => e.name === newQ); + if (exactMatch) matches.add(exactMatch); - // 名前またはエイリアスにキーワードが含まれている - for (const emoji of emojis) { - if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.aliases.some(alias => alias.includes(keyword)))) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - } else { - for (const emoji of emojis) { - if (emoji.name.startsWith(q)) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - if (matches.size >= max) return matches; + if (newQ.includes(' ')) { // AND検索 + const keywords = newQ.split(' '); - for (const emoji of emojis) { - if (emoji.aliases.some(alias => alias.startsWith(q))) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - if (matches.size >= max) return matches; + // 名前にキーワードが含まれている + for (const emoji of emojis) { + if (keywords.every(keyword => emoji.name.includes(keyword))) { + matches.add(emoji); + if (matches.size >= max) break; + } + } + if (matches.size >= max) return matches; - for (const emoji of emojis) { - if (emoji.name.includes(q)) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - if (matches.size >= max) return matches; + // 名前またはエイリアスにキーワードが含まれている + for (const emoji of emojis) { + if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.aliases.some(alias => alias.includes(keyword)))) { + matches.add(emoji); + if (matches.size >= max) break; + } + } + } else { + for (const emoji of emojis) { + if (emoji.name.startsWith(newQ)) { + matches.add(emoji); + if (matches.size >= max) break; + } + } + if (matches.size >= max) return matches; - for (const emoji of emojis) { - if (emoji.aliases.some(alias => alias.includes(q))) { - matches.add(emoji); - if (matches.size >= max) break; - } - } + for (const emoji of emojis) { + if (emoji.aliases.some(alias => alias.startsWith(newQ))) { + matches.add(emoji); + if (matches.size >= max) break; } + } + if (matches.size >= max) return matches; - return matches; - }; + for (const emoji of emojis) { + if (emoji.name.includes(newQ)) { + matches.add(emoji); + if (matches.size >= max) break; + } + } + if (matches.size >= max) return matches; - const searchUnicode = () => { - const max = 8; - const emojis = this.emojilist; - const matches = new Set(); + for (const emoji of emojis) { + if (emoji.aliases.some(alias => alias.includes(newQ))) { + matches.add(emoji); + if (matches.size >= max) break; + } + } + } - const exactMatch = emojis.find(e => e.name === q); - if (exactMatch) matches.add(exactMatch); + return matches; + }; - if (q.includes(' ')) { // AND検索 - const keywords = q.split(' '); + const searchUnicode = () => { + const max = 8; + const emojis = emojilist; + const matches = new Set<UnicodeEmojiDef>(); - // 名前にキーワードが含まれている - for (const emoji of emojis) { - if (keywords.every(keyword => emoji.name.includes(keyword))) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - if (matches.size >= max) return matches; + const exactMatch = emojis.find(e => e.name === newQ); + if (exactMatch) matches.add(exactMatch); - // 名前またはエイリアスにキーワードが含まれている - for (const emoji of emojis) { - if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.keywords.some(alias => alias.includes(keyword)))) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - } else { - for (const emoji of emojis) { - if (emoji.name.startsWith(q)) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - if (matches.size >= max) return matches; + if (newQ.includes(' ')) { // AND検索 + const keywords = newQ.split(' '); - for (const emoji of emojis) { - if (emoji.keywords.some(keyword => keyword.startsWith(q))) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - if (matches.size >= max) return matches; + // 名前にキーワードが含まれている + for (const emoji of emojis) { + if (keywords.every(keyword => emoji.name.includes(keyword))) { + matches.add(emoji); + if (matches.size >= max) break; + } + } + if (matches.size >= max) return matches; - for (const emoji of emojis) { - if (emoji.name.includes(q)) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - if (matches.size >= max) return matches; + // 名前またはエイリアスにキーワードが含まれている + for (const emoji of emojis) { + if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.keywords.some(alias => alias.includes(keyword)))) { + matches.add(emoji); + if (matches.size >= max) break; + } + } + } else { + for (const emoji of emojis) { + if (emoji.name.startsWith(newQ)) { + matches.add(emoji); + if (matches.size >= max) break; + } + } + if (matches.size >= max) return matches; - for (const emoji of emojis) { - if (emoji.keywords.some(keyword => keyword.includes(q))) { - matches.add(emoji); - if (matches.size >= max) break; - } - } + for (const emoji of emojis) { + if (emoji.keywords.some(keyword => keyword.startsWith(newQ))) { + matches.add(emoji); + if (matches.size >= max) break; } + } + if (matches.size >= max) return matches; - return matches; - }; + for (const emoji of emojis) { + if (emoji.name.includes(newQ)) { + matches.add(emoji); + if (matches.size >= max) break; + } + } + if (matches.size >= max) return matches; - this.searchResultCustom = Array.from(searchCustom()); - this.searchResultUnicode = Array.from(searchUnicode()); + for (const emoji of emojis) { + if (emoji.keywords.some(keyword => keyword.includes(newQ))) { + matches.add(emoji); + if (matches.size >= max) break; + } + } } - }, - mounted() { - this.focus(); - }, + return matches; + }; - methods: { - focus() { - if (!isMobile && !isTouchUsing) { - this.$refs.search.focus({ - preventScroll: true - }); - } - }, + searchResultCustom.value = Array.from(searchCustom()); + searchResultUnicode.value = Array.from(searchUnicode()); +}); - reset() { - this.$refs.emojis.scrollTop = 0; - this.q = ''; - }, +function focus() { + if (!isMobile && !isTouchUsing) { + search.value?.focus({ + preventScroll: true + }); + } +} - getKey(emoji: any) { - return typeof emoji === 'string' ? emoji : (emoji.char || `:${emoji.name}:`); - }, +function reset() { + if (emojis.value) emojis.value.scrollTop = 0; + q.value = ''; +} - chosen(emoji: any, ev) { - if (ev) { - const el = ev.currentTarget || ev.target; - const rect = el.getBoundingClientRect(); - const x = rect.left + (el.offsetWidth / 2); - const y = rect.top + (el.offsetHeight / 2); - os.popup(Ripple, { x, y }, {}, 'end'); - } +function getKey(emoji: string | Misskey.entities.CustomEmoji | UnicodeEmojiDef): string { + return typeof emoji === 'string' ? emoji : (emoji.char || `:${emoji.name}:`); +} - const key = this.getKey(emoji); - this.$emit('chosen', key); +function chosen(emoji: any, ev?: MouseEvent) { + const el = ev && (ev.currentTarget || ev.target) as HTMLElement | null | undefined; + if (el) { + const rect = el.getBoundingClientRect(); + const x = rect.left + (el.offsetWidth / 2); + const y = rect.top + (el.offsetHeight / 2); + os.popup(Ripple, { x, y }, {}, 'end'); + } - // 最近使った絵文字更新 - if (!this.pinned.includes(key)) { - let recents = this.$store.state.recentlyUsedEmojis; - recents = recents.filter((e: any) => e !== key); - recents.unshift(key); - this.$store.set('recentlyUsedEmojis', recents.splice(0, 32)); - } - }, + const key = getKey(emoji); + emit('chosen', key); - paste(event) { - const paste = (event.clipboardData || window.clipboardData).getData('text'); - if (this.done(paste)) { - event.preventDefault(); - } - }, + // 最近使った絵文字更新 + if (!pinned.value.includes(key)) { + let recents = defaultStore.state.recentlyUsedEmojis; + recents = recents.filter((e: any) => e !== key); + recents.unshift(key); + defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32)); + } +} - done(query) { - if (query == null) query = this.q; - if (query == null) return; - const q = query.replace(/:/g, ''); - const exactMatchCustom = this.customEmojis.find(e => e.name === q); - if (exactMatchCustom) { - this.chosen(exactMatchCustom); - return true; - } - const exactMatchUnicode = this.emojilist.find(e => e.char === q || e.name === q); - if (exactMatchUnicode) { - this.chosen(exactMatchUnicode); - return true; - } - if (this.searchResultCustom.length > 0) { - this.chosen(this.searchResultCustom[0]); - return true; - } - if (this.searchResultUnicode.length > 0) { - this.chosen(this.searchResultUnicode[0]); - return true; - } - }, +function paste(event: ClipboardEvent) { + const paste = (event.clipboardData || window.clipboardData).getData('text'); + if (done(paste)) { + event.preventDefault(); } +} + +function done(query?: any): boolean | void { + if (query == null) query = q.value; + if (query == null || typeof query !== 'string') return; + + const q2 = query.replace(/:/g, ''); + const exactMatchCustom = customEmojis.find(e => e.name === q2); + if (exactMatchCustom) { + chosen(exactMatchCustom); + return true; + } + const exactMatchUnicode = emojilist.find(e => e.char === q2 || e.name === q2); + if (exactMatchUnicode) { + chosen(exactMatchUnicode); + return true; + } + if (searchResultCustom.value.length > 0) { + chosen(searchResultCustom.value[0]); + return true; + } + if (searchResultUnicode.value.length > 0) { + chosen(searchResultUnicode.value[0]); + return true; + } +} + +onMounted(() => { + focus(); +}); + +defineExpose({ + focus, + reset, }); </script> diff --git a/packages/client/src/components/featured-photos.vue b/packages/client/src/components/featured-photos.vue index af5892c98e..e58b5d2849 100644 --- a/packages/client/src/components/featured-photos.vue +++ b/packages/client/src/components/featured-photos.vue @@ -2,25 +2,15 @@ <div v-if="meta" class="xfbouadm" :style="{ backgroundImage: `url(${ meta.backgroundImageUrl })` }"></div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { ref } from 'vue'; +import * as Misskey from 'misskey-js'; import * as os from '@/os'; -export default defineComponent({ - components: { - }, +const meta = ref<Misskey.entities.DetailedInstanceMetadata>(); - data() { - return { - meta: null, - }; - }, - - created() { - os.api('meta', { detail: true }).then(meta => { - this.meta = meta; - }); - }, +os.api('meta', { detail: true }).then(gotMeta => { + meta.value = gotMeta; }); </script> diff --git a/packages/client/src/components/follow-button.vue b/packages/client/src/components/follow-button.vue index b16b22f26f..345edb6441 100644 --- a/packages/client/src/components/follow-button.vue +++ b/packages/client/src/components/follow-button.vue @@ -6,129 +6,110 @@ > <template v-if="!wait"> <template v-if="hasPendingFollowRequestFromYou && user.isLocked"> - <span v-if="full">{{ $ts.followRequestPending }}</span><i class="fas fa-hourglass-half"></i> + <span v-if="full">{{ i18n.locale.followRequestPending }}</span><i class="fas fa-hourglass-half"></i> </template> <template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked"> <!-- つまりリモートフォローの場合。 --> - <span v-if="full">{{ $ts.processing }}</span><i class="fas fa-spinner fa-pulse"></i> + <span v-if="full">{{ i18n.locale.processing }}</span><i class="fas fa-spinner fa-pulse"></i> </template> <template v-else-if="isFollowing"> - <span v-if="full">{{ $ts.unfollow }}</span><i class="fas fa-minus"></i> + <span v-if="full">{{ i18n.locale.unfollow }}</span><i class="fas fa-minus"></i> </template> <template v-else-if="!isFollowing && user.isLocked"> - <span v-if="full">{{ $ts.followRequest }}</span><i class="fas fa-plus"></i> + <span v-if="full">{{ i18n.locale.followRequest }}</span><i class="fas fa-plus"></i> </template> <template v-else-if="!isFollowing && !user.isLocked"> - <span v-if="full">{{ $ts.follow }}</span><i class="fas fa-plus"></i> + <span v-if="full">{{ i18n.locale.follow }}</span><i class="fas fa-plus"></i> </template> </template> <template v-else> - <span v-if="full">{{ $ts.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i> + <span v-if="full">{{ i18n.locale.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i> </template> </button> </template> -<script lang="ts"> -import { defineComponent, markRaw } from 'vue'; +<script lang="ts" setup> +import { onBeforeUnmount, onMounted, ref } from 'vue'; +import * as Misskey from 'misskey-js'; import * as os from '@/os'; import { stream } from '@/stream'; +import { i18n } from '@/i18n'; -export default defineComponent({ - props: { - user: { - type: Object, - required: true - }, - full: { - type: Boolean, - required: false, - default: false, - }, - large: { - type: Boolean, - required: false, - default: false, - }, - }, - - data() { - return { - isFollowing: this.user.isFollowing, - hasPendingFollowRequestFromYou: this.user.hasPendingFollowRequestFromYou, - wait: false, - connection: null, - }; - }, - - created() { - // 渡されたユーザー情報が不完全な場合 - if (this.user.isFollowing == null) { - os.api('users/show', { - userId: this.user.id - }).then(u => { - this.isFollowing = u.isFollowing; - this.hasPendingFollowRequestFromYou = u.hasPendingFollowRequestFromYou; - }); - } - }, - - mounted() { - this.connection = markRaw(stream.useChannel('main')); +const props = withDefaults(defineProps<{ + user: Misskey.entities.UserDetailed, + full?: boolean, + large?: boolean, +}>(), { + full: false, + large: false, +}); - this.connection.on('follow', this.onFollowChange); - this.connection.on('unfollow', this.onFollowChange); - }, +const isFollowing = ref(props.user.isFollowing); +const hasPendingFollowRequestFromYou = ref(props.user.hasPendingFollowRequestFromYou); +const wait = ref(false); +const connection = stream.useChannel('main'); - beforeUnmount() { - this.connection.dispose(); - }, +if (props.user.isFollowing == null) { + os.api('users/show', { + userId: props.user.id + }).then(u => { + isFollowing.value = u.isFollowing; + hasPendingFollowRequestFromYou.value = u.hasPendingFollowRequestFromYou; + }); +} - methods: { - onFollowChange(user) { - if (user.id == this.user.id) { - this.isFollowing = user.isFollowing; - this.hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou; - } - }, +function onFollowChange(user: Misskey.entities.UserDetailed) { + if (user.id == props.user.id) { + isFollowing.value = user.isFollowing; + hasPendingFollowRequestFromYou.value = user.hasPendingFollowRequestFromYou; + } +} - async onClick() { - this.wait = true; +async function onClick() { + wait.value = true; - try { - if (this.isFollowing) { - const { canceled } = await os.confirm({ - type: 'warning', - text: this.$t('unfollowConfirm', { name: this.user.name || this.user.username }), - }); + try { + if (isFollowing.value) { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.t('unfollowConfirm', { name: props.user.name || props.user.username }), + }); - if (canceled) return; + if (canceled) return; - await os.api('following/delete', { - userId: this.user.id - }); - } else { - if (this.hasPendingFollowRequestFromYou) { - await os.api('following/requests/cancel', { - userId: this.user.id - }); - } else if (this.user.isLocked) { - await os.api('following/create', { - userId: this.user.id - }); - this.hasPendingFollowRequestFromYou = true; - } else { - await os.api('following/create', { - userId: this.user.id - }); - this.hasPendingFollowRequestFromYou = true; - } - } - } catch (e) { - console.error(e); - } finally { - this.wait = false; + await os.api('following/delete', { + userId: props.user.id + }); + } else { + if (hasPendingFollowRequestFromYou.value) { + await os.api('following/requests/cancel', { + userId: props.user.id + }); + } else if (props.user.isLocked) { + await os.api('following/create', { + userId: props.user.id + }); + hasPendingFollowRequestFromYou.value = true; + } else { + await os.api('following/create', { + userId: props.user.id + }); + hasPendingFollowRequestFromYou.value = true; } } + } catch (e) { + console.error(e); + } finally { + wait.value = false; } +} + +onMounted(() => { + connection.on('follow', onFollowChange); + connection.on('unfollow', onFollowChange); +}); + +onBeforeUnmount(() => { + connection.dispose(); }); </script> diff --git a/packages/client/src/components/forgot-password.vue b/packages/client/src/components/forgot-password.vue index b03a6133b4..c74e1ac75e 100644 --- a/packages/client/src/components/forgot-password.vue +++ b/packages/client/src/components/forgot-password.vue @@ -2,72 +2,64 @@ <XModalWindow ref="dialog" :width="370" :height="400" - @close="$refs.dialog.close()" - @closed="$emit('closed')" + @close="dialog.close()" + @closed="emit('closed')" > - <template #header>{{ $ts.forgotPassword }}</template> + <template #header>{{ i18n.locale.forgotPassword }}</template> - <form v-if="$instance.enableEmail" class="bafeceda" @submit.prevent="onSubmit"> + <form v-if="instance.enableEmail" class="bafeceda" @submit.prevent="onSubmit"> <div class="main _formRoot"> <MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required> - <template #label>{{ $ts.username }}</template> + <template #label>{{ i18n.locale.username }}</template> <template #prefix>@</template> </MkInput> <MkInput v-model="email" class="_formBlock" type="email" spellcheck="false" required> - <template #label>{{ $ts.emailAddress }}</template> - <template #caption>{{ $ts._forgotPassword.enterEmail }}</template> + <template #label>{{ i18n.locale.emailAddress }}</template> + <template #caption>{{ i18n.locale._forgotPassword.enterEmail }}</template> </MkInput> - <MkButton class="_formBlock" type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ $ts.send }}</MkButton> + <MkButton class="_formBlock" type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ i18n.locale.send }}</MkButton> </div> <div class="sub"> - <MkA to="/about" class="_link">{{ $ts._forgotPassword.ifNoEmail }}</MkA> + <MkA to="/about" class="_link">{{ i18n.locale._forgotPassword.ifNoEmail }}</MkA> </div> </form> - <div v-else> - {{ $ts._forgotPassword.contactAdmin }} + <div v-else class="bafecedb"> + {{ i18n.locale._forgotPassword.contactAdmin }} </div> </XModalWindow> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import XModalWindow from '@/components/ui/modal-window.vue'; import MkButton from '@/components/ui/button.vue'; import MkInput from '@/components/form/input.vue'; import * as os from '@/os'; +import { instance } from '@/instance'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - XModalWindow, - MkButton, - MkInput, - }, +const emit = defineEmits<{ + (e: 'done'): void; + (e: 'closed'): void; +}>(); - emits: ['done', 'closed'], +let dialog: InstanceType<typeof XModalWindow> = $ref(); - data() { - return { - username: '', - email: '', - processing: false, - }; - }, +let username = $ref(''); +let email = $ref(''); +let processing = $ref(false); - methods: { - async onSubmit() { - this.processing = true; - await os.apiWithDialog('request-reset-password', { - username: this.username, - email: this.email, - }); - - this.$emit('done'); - this.$refs.dialog.close(); - } - } -}); +async function onSubmit() { + processing = true; + await os.apiWithDialog('request-reset-password', { + username, + email, + }); + emit('done'); + dialog.close(); +} </script> <style lang="scss" scoped> @@ -81,4 +73,8 @@ export default defineComponent({ padding: 24px; } } + +.bafecedb { + padding: 24px; +} </style> diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts index dd7fdea4bd..378523e1bc 100644 --- a/packages/client/src/os.ts +++ b/packages/client/src/os.ts @@ -541,7 +541,7 @@ export const uploads = ref<{ img: string; }[]>([]); -export function upload(file: File, folder?: any, name?: string) { +export function upload(file: File, folder?: any, name?: string): Promise<Misskey.entities.DriveFile> { if (folder && typeof folder == 'object') folder = folder.id; return new Promise((resolve, reject) => { diff --git a/packages/client/src/scripts/emojilist.ts b/packages/client/src/scripts/emojilist.ts index de7591f5a0..bd8689e4f8 100644 --- a/packages/client/src/scripts/emojilist.ts +++ b/packages/client/src/scripts/emojilist.ts @@ -1,7 +1,11 @@ -// initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb -export const emojilist = require('../emojilist.json') as { +export const unicodeEmojiCategories = ['face', 'people', 'animals_and_nature', 'food_and_drink', 'activity', 'travel_and_places', 'objects', 'symbols', 'flags'] as const; + +export type UnicodeEmojiDef = { name: string; keywords: string[]; char: string; - category: 'people' | 'animals_and_nature' | 'food_and_drink' | 'activity' | 'travel_and_places' | 'objects' | 'symbols' | 'flags'; -}[]; + category: typeof unicodeEmojiCategories[number]; +} + +// initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb +export const emojilist = require('../emojilist.json') as UnicodeEmojiDef[]; |