summaryrefslogtreecommitdiff
path: root/packages/frontend/src/utility/drive.ts
diff options
context:
space:
mode:
authorsyuilo <4439005+syuilo@users.noreply.github.com>2025-05-21 07:31:24 +0900
committerGitHub <noreply@github.com>2025-05-21 07:31:24 +0900
commit9480120eba1db238072b0bdfc9e6d01b2494cb3b (patch)
tree3a4d7963e7dd9e540713d6f2b26fc98e2c586223 /packages/frontend/src/utility/drive.ts
parentenhance(frontend): URLプレビューをユーザーサイドで無効化で... (diff)
downloadmisskey-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.ts246
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(),
+ });
+ });
+}