diff options
| author | syuilo <4439005+syuilo@users.noreply.github.com> | 2025-05-21 07:31:24 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-05-21 07:31:24 +0900 |
| commit | 9480120eba1db238072b0bdfc9e6d01b2494cb3b (patch) | |
| tree | 3a4d7963e7dd9e540713d6f2b26fc98e2c586223 /packages/frontend/src/utility/drive.ts | |
| parent | enhance(frontend): URLプレビューをユーザーサイドで無効化で... (diff) | |
| download | misskey-9480120eba1db238072b0bdfc9e6d01b2494cb3b.tar.gz misskey-9480120eba1db238072b0bdfc9e6d01b2494cb3b.tar.bz2 misskey-9480120eba1db238072b0bdfc9e6d01b2494cb3b.zip | |
Feat: ドライブ周りのUIの強化 (#16011)
* wip
* wip
* Update MkDrive.vue
* wip
* Update MkDrive.vue
* Update MkDrive.vue
* wip
* Update MkDrive.vue
* Update MkDrive.vue
* wip
* Update MkDrive.vue
* wip
* wip
* wip
* wip
* Update MkDrive.vue
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* feat(frontend): upload dialog (#16032)
* wip
* wip
* Update MkUploadDialog.vue
* wip
* wip
* wip
* wip
* wip
* Update MkUploadDialog.vue
* wip
* wip
* Update MkDrive.vue
* wip
* wip
* Update MkPostForm.vue
* wip
* Update room.form.vue
* Update os.ts
* wiop
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* Update select-file.ts
* wip
* wip
* Update MkDrive.vue
* Update drag-and-drop.ts
* wip
* wip
* wop
* wip
* wip
* Update MkDrive.vue
* Update CHANGELOG.md
* wipo
* Update MkDrive.folder.vue
* wip
* Update MkUploaderDialog.vue
* wip
* wip
* Update MkUploaderDialog.vue
* wip
* Update MkDrive.vue
* Update MkDrive.vue
* wip
* wip
Diffstat (limited to 'packages/frontend/src/utility/drive.ts')
| -rw-r--r-- | packages/frontend/src/utility/drive.ts | 246 |
1 files changed, 246 insertions, 0 deletions
diff --git a/packages/frontend/src/utility/drive.ts b/packages/frontend/src/utility/drive.ts new file mode 100644 index 0000000000..e29b010c81 --- /dev/null +++ b/packages/frontend/src/utility/drive.ts @@ -0,0 +1,246 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { defineAsyncComponent } from 'vue'; +import * as Misskey from 'misskey-js'; +import { apiUrl } from '@@/js/config.js'; +import * as os from '@/os.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; +import { useStream } from '@/stream.js'; +import { i18n } from '@/i18n.js'; +import { prefer } from '@/preferences.js'; +import { $i } from '@/i.js'; +import { instance } from '@/instance.js'; +import { globalEvents } from '@/events.js'; +import { getProxiedImageUrl } from '@/utility/media-proxy.js'; + +export function uploadFile(file: File | Blob, options: { + name?: string; + folderId?: string | null; + onProgress?: (ctx: { total: number; loaded: number; }) => void; +} = {}): Promise<Misskey.entities.DriveFile> { + return new Promise((resolve, reject) => { + if ($i == null) return reject(); + + if ((file.size > instance.maxFileSize) || (file.size > ($i.policies.maxFileSizeMb * 1024 * 1024))) { + os.alert({ + type: 'error', + title: i18n.ts.failedToUpload, + text: i18n.ts.cannotUploadBecauseExceedsFileSizeLimit, + }); + return reject(); + } + + const xhr = new XMLHttpRequest(); + xhr.open('POST', apiUrl + '/drive/files/create', true); + xhr.onload = ((ev: ProgressEvent<XMLHttpRequest>) => { + if (xhr.status !== 200 || ev.target == null || ev.target.response == null) { + if (xhr.status === 413) { + os.alert({ + type: 'error', + title: i18n.ts.failedToUpload, + text: i18n.ts.cannotUploadBecauseExceedsFileSizeLimit, + }); + } else if (ev.target?.response) { + const res = JSON.parse(ev.target.response); + if (res.error?.id === 'bec5bd69-fba3-43c9-b4fb-2894b66ad5d2') { + os.alert({ + type: 'error', + title: i18n.ts.failedToUpload, + text: i18n.ts.cannotUploadBecauseInappropriate, + }); + } else if (res.error?.id === 'd08dbc37-a6a9-463a-8c47-96c32ab5f064') { + os.alert({ + type: 'error', + title: i18n.ts.failedToUpload, + text: i18n.ts.cannotUploadBecauseNoFreeSpace, + }); + } else { + os.alert({ + type: 'error', + title: i18n.ts.failedToUpload, + text: `${res.error?.message}\n${res.error?.code}\n${res.error?.id}`, + }); + } + } else { + os.alert({ + type: 'error', + title: 'Failed to upload', + text: `${JSON.stringify(ev.target?.response)}, ${JSON.stringify(xhr.response)}`, + }); + } + + reject(); + return; + } + + const driveFile = JSON.parse(ev.target.response); + globalEvents.emit('driveFileCreated', driveFile); + resolve(driveFile); + }) as (ev: ProgressEvent<EventTarget>) => any; + + if (options.onProgress) { + xhr.upload.onprogress = ev => { + if (ev.lengthComputable) { + options.onProgress({ + total: ev.total, + loaded: ev.loaded, + }); + } + }; + } + + const formData = new FormData(); + formData.append('i', $i.token); + formData.append('force', 'true'); + formData.append('file', file); + formData.append('name', options.name ?? file.name ?? 'untitled'); + if (options.folderId) formData.append('folderId', options.folderId); + + xhr.send(formData); + }); +} + +export function chooseFileFromPcAndUpload( + options: { + multiple?: boolean; + folderId?: string | null; + } = {}, +): Promise<Misskey.entities.DriveFile[]> { + return new Promise((res, rej) => { + os.chooseFileFromPc({ multiple: options.multiple }).then(files => { + if (files.length === 0) return; + os.launchUploader(files, { + folderId: options.folderId, + }).then(driveFiles => { + res(driveFiles); + }); + }); + }); +} + +export function chooseDriveFile(options: { + multiple?: boolean; +} = {}): Promise<Misskey.entities.DriveFile[]> { + return new Promise(resolve => { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkDriveFileSelectDialog.vue')), { + multiple: options.multiple, + }, { + done: files => { + if (files) { + resolve(files); + } + }, + closed: () => dispose(), + }); + }); +} + +export function chooseFileFromUrl(): Promise<Misskey.entities.DriveFile> { + return new Promise((res, rej) => { + os.inputText({ + title: i18n.ts.uploadFromUrl, + type: 'url', + placeholder: i18n.ts.uploadFromUrlDescription, + }).then(({ canceled, result: url }) => { + if (canceled) return; + + const marker = Math.random().toString(); // TODO: UUIDとか使う + + // TODO: no websocketモード対応 + const connection = useStream().useChannel('main'); + connection.on('urlUploadFinished', urlResponse => { + if (urlResponse.marker === marker) { + res(urlResponse.file); + connection.dispose(); + } + }); + + misskeyApi('drive/files/upload-from-url', { + url: url, + folderId: prefer.s.uploadFolder, + marker, + }); + + os.alert({ + title: i18n.ts.uploadFromUrlRequested, + text: i18n.ts.uploadFromUrlMayTakeTime, + }); + }); + }); +} + +function select(src: HTMLElement | EventTarget | null, label: string | null, multiple: boolean): Promise<Misskey.entities.DriveFile[]> { + return new Promise((res, rej) => { + os.popupMenu([label ? { + text: label, + type: 'label', + } : undefined, { + text: i18n.ts.upload, + icon: 'ti ti-upload', + action: () => chooseFileFromPcAndUpload({ multiple }).then(files => res(files)), + }, { + text: i18n.ts.fromDrive, + icon: 'ti ti-cloud', + action: () => chooseDriveFile({ multiple }).then(files => res(files)), + }, { + text: i18n.ts.fromUrl, + icon: 'ti ti-link', + action: () => chooseFileFromUrl().then(file => res([file])), + }], src); + }); +} + +export function selectFile(src: HTMLElement | EventTarget | null, label: string | null = null): Promise<Misskey.entities.DriveFile> { + return select(src, label, false).then(files => files[0]); +} + +export function selectFiles(src: HTMLElement | EventTarget | null, label: string | null = null): Promise<Misskey.entities.DriveFile[]> { + return select(src, label, true); +} + +export async function createCroppedImageDriveFileFromImageDriveFile(imageDriveFile: Misskey.entities.DriveFile, options: { + aspectRatio: number | null; +}): Promise<Misskey.entities.DriveFile> { + return new Promise(resolve => { + const imgUrl = getProxiedImageUrl(imageDriveFile.url, undefined, true); + const image = new Image(); + image.src = imgUrl; + image.onload = () => { + const canvas = window.document.createElement('canvas'); + const ctx = canvas.getContext('2d')!; + canvas.width = image.width; + canvas.height = image.height; + ctx.drawImage(image, 0, 0); + canvas.toBlob(blob => { + os.cropImageFile(blob, { + aspectRatio: options.aspectRatio, + }).then(croppedImageFile => { + uploadFile(croppedImageFile, { + name: imageDriveFile.name, + folderId: imageDriveFile.folderId, + }).then(driveFile => { + resolve(driveFile); + }); + }); + }); + }; + }); +} + +export async function selectDriveFolder(initialFolder: Misskey.entities.DriveFolder['id'] | null): Promise<Misskey.entities.DriveFolder[]> { + return new Promise(resolve => { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkDriveFolderSelectDialog.vue')), { + initialFolder, + }, { + done: folders => { + if (folders) { + resolve(folders); + } + }, + closed: () => dispose(), + }); + }); +} |