From 539356023d4b964f41a73cc7b98f201d0ad3e433 Mon Sep 17 00:00:00 2001 From: tamaina Date: Sat, 26 Jul 2025 01:43:19 +0900 Subject: [PATCH 01/10] :v: --- .../frontend/src/components/MkPostForm.vue | 8 +- .../src/components/MkUploaderDialog.vue | 4 +- .../frontend/src/composables/use-uploader.ts | 148 +++++++++--------- packages/frontend/src/pages/share.vue | 59 ++++++- packages/frontend/src/types/post-form.ts | 1 + packages/sw/src/sw.ts | 30 +++- 6 files changed, 165 insertions(+), 85 deletions(-) diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 174a73e0fd..9e4cc54dce 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -146,7 +146,7 @@ import { getPluginHandlers } from '@/plugin.js'; import { DI } from '@/di.js'; import { globalEvents } from '@/events.js'; import { checkDragDataType, getDragData } from '@/drag-and-drop.js'; -import { useUploader } from '@/composables/use-uploader.js'; +import { FileUploader } from '@/composables/use-uploader.js'; const $i = ensureSignin(); @@ -214,7 +214,7 @@ const targetChannel = shallowRef(props.channel); const serverDraftId = ref(null); const postFormActions = getPluginHandlers('post_form_action'); -const uploader = useUploader({ +const uploader = new FileUploader({ multiple: true, }); @@ -223,6 +223,10 @@ uploader.events.on('itemUploaded', ctx => { uploader.removeItem(ctx.item); }); +if (props.initialLocalFiles) { + uploader.addFiles(props.initialLocalFiles); +} + const draftKey = computed((): string => { let key = targetChannel.value ? `channel:${targetChannel.value.id}` : ''; diff --git a/packages/frontend/src/components/MkUploaderDialog.vue b/packages/frontend/src/components/MkUploaderDialog.vue index ce098d71e4..372a61b881 100644 --- a/packages/frontend/src/components/MkUploaderDialog.vue +++ b/packages/frontend/src/components/MkUploaderDialog.vue @@ -58,7 +58,7 @@ import { i18n } from '@/i18n.js'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; import { ensureSignin } from '@/i.js'; -import { useUploader } from '@/composables/use-uploader.js'; +import { FileUploader } from '@/composables/use-uploader.js'; import MkUploaderItems from '@/components/MkUploaderItems.vue'; const $i = ensureSignin(); @@ -80,7 +80,7 @@ const emit = defineEmits<{ const dialog = useTemplateRef('dialog'); -const uploader = useUploader({ +const uploader = new FileUploader({ multiple: props.multiple, folderId: props.folderId, features: props.features, diff --git a/packages/frontend/src/composables/use-uploader.ts b/packages/frontend/src/composables/use-uploader.ts index 826d8c5203..63b6a2f437 100644 --- a/packages/frontend/src/composables/use-uploader.ts +++ b/packages/frontend/src/composables/use-uploader.ts @@ -99,31 +99,43 @@ function getCompressionSettings(level: 0 | 1 | 2 | 3) { } } -export function useUploader(options: { - folderId?: string | null; - multiple?: boolean; - features?: UploaderFeatures; -} = {}) { - const $i = ensureSignin(); - - const events = new EventEmitter<{ +export class FileUploader { + private $i: Misskey.entities.MeDetailed; + public events = new EventEmitter<{ 'itemUploaded': (ctx: { item: UploaderItem; }) => void; }>(); - - const uploaderFeatures = computed>(() => { + private uploaderFeatures = computed>(() => { return { - imageEditing: options.features?.imageEditing ?? true, - watermark: options.features?.watermark ?? true, + imageEditing: this.options.features?.imageEditing ?? true, + watermark: this.options.features?.watermark ?? true, }; }); + public items = ref([]); + public uploading = computed(() => this.items.value.some(item => item.uploading)); + public readyForUpload = computed(() => this.items.value.length > 0 && this.items.value.some(item => item.uploaded == null) && !this.items.value.some(item => item.uploading || item.preprocessing)); + public allItemsUploaded = computed(() => this.items.value.every(item => item.uploaded != null)); - const items = ref([]); + constructor( + public options: { + folderId?: string | null; + multiple?: boolean; + features?: UploaderFeatures; + } = {} + ) { + this.$i = ensureSignin(); - function initializeFile(file: File) { + onUnmounted(() => { + for (const item of this.items.value) { + if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail); + } + }); + } + + private initializeFile(file: File) { const id = genId(); const filename = file.name ?? 'untitled'; const extension = filename.split('.').length > 1 ? '.' + filename.split('.').pop() : ''; - items.value.push({ + this.items.value.push({ id, name: prefer.s.keepOriginalFilename ? filename : id + extension, progress: null, @@ -134,27 +146,27 @@ export function useUploader(options: { uploaded: null, uploadFailed: false, compressionLevel: prefer.s.defaultImageCompressionLevel, - watermarkPresetId: uploaderFeatures.value.watermark && $i.policies.watermarkAvailable ? prefer.s.defaultWatermarkPresetId : null, + watermarkPresetId: this.uploaderFeatures.value.watermark && this.$i.policies.watermarkAvailable ? prefer.s.defaultWatermarkPresetId : null, file: markRaw(file), }); - const reactiveItem = items.value.at(-1)!; - preprocess(reactiveItem).then(() => { - triggerRef(items); + const reactiveItem = this.items.value.at(-1)!; + this.preprocess(reactiveItem).then(() => { + triggerRef(this.items); }); } - function addFiles(newFiles: File[]) { + public addFiles(newFiles: File[]) { for (const file of newFiles) { - initializeFile(file); + this.initializeFile(file); } } - function removeItem(item: UploaderItem) { + public removeItem(item: UploaderItem) { if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail); - items.value.splice(items.value.indexOf(item), 1); + this.items.value.splice(this.items.value.indexOf(item), 1); } - function getMenu(item: UploaderItem): MenuItem[] { + public getMenu(item: UploaderItem): MenuItem[] { const menu: MenuItem[] = []; if ( @@ -206,7 +218,7 @@ export function useUploader(options: { } if ( - uploaderFeatures.value.imageEditing && + this.uploaderFeatures.value.imageEditing && IMAGE_EDITING_SUPPORTED_TYPES.includes(item.file.type) && !item.preprocessing && !item.uploading && @@ -222,14 +234,14 @@ export function useUploader(options: { action: async () => { const cropped = await os.cropImageFile(item.file, { aspectRatio: null }); if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail); - items.value.splice(items.value.indexOf(item), 1, { + this.items.value.splice(this.items.value.indexOf(item), 1, { ...item, file: markRaw(cropped), thumbnail: window.URL.createObjectURL(cropped), }); - const reactiveItem = items.value.find(x => x.id === item.id)!; - preprocess(reactiveItem).then(() => { - triggerRef(items); + const reactiveItem = this.items.value.find(x => x.id === item.id)!; + this.preprocess(reactiveItem).then(() => { + triggerRef(this.items); }); }, }, /*{ @@ -247,14 +259,14 @@ export function useUploader(options: { }, { ok: (file) => { if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail); - items.value.splice(items.value.indexOf(item), 1, { + this.items.value.splice(this.items.value.indexOf(item), 1, { ...item, file: markRaw(file), thumbnail: window.URL.createObjectURL(file), }); - const reactiveItem = items.value.find(x => x.id === item.id)!; - preprocess(reactiveItem).then(() => { - triggerRef(items); + const reactiveItem = this.items.value.find(x => x.id === item.id)!; + this.preprocess(reactiveItem).then(() => { + triggerRef(this.items); }); }, closed: () => dispose(), @@ -265,19 +277,19 @@ export function useUploader(options: { } if ( - uploaderFeatures.value.watermark && - $i.policies.watermarkAvailable && + this.uploaderFeatures.value.watermark && + this.$i.policies.watermarkAvailable && WATERMARK_SUPPORTED_TYPES.includes(item.file.type) && !item.preprocessing && !item.uploading && !item.uploaded ) { - function changeWatermarkPreset(presetId: string | null) { + const changeWatermarkPreset = (presetId: string | null) => { item.watermarkPresetId = presetId; - preprocess(item).then(() => { - triggerRef(items); + this.preprocess(item).then(() => { + triggerRef(this.items); }); - } + }; menu.push({ icon: 'ti ti-copyright', @@ -323,12 +335,12 @@ export function useUploader(options: { !item.uploading && !item.uploaded ) { - function changeCompressionLevel(level: 0 | 1 | 2 | 3) { + const changeCompressionLevel = (level: 0 | 1 | 2 | 3) => { item.compressionLevel = level; - preprocess(item).then(() => { - triggerRef(items); + this.preprocess(item).then(() => { + triggerRef(this.items); }); - } + }; menu.push({ icon: 'ti ti-leaf', @@ -381,14 +393,14 @@ export function useUploader(options: { icon: 'ti ti-upload', text: i18n.ts.upload, action: () => { - uploadOne(item); + this.uploadOne(item); }, }, { icon: 'ti ti-x', text: i18n.ts.remove, danger: true, action: () => { - removeItem(item); + this.removeItem(item); }, }); } else if (item.uploading) { @@ -409,13 +421,13 @@ export function useUploader(options: { return menu; } - async function uploadOne(item: UploaderItem): Promise { + private async uploadOne(item: UploaderItem): Promise { item.uploadFailed = false; item.uploading = true; const { filePromise, abort } = uploadFile(item.preprocessedFile ?? item.file, { name: item.uploadName ?? item.name, - folderId: options.folderId === undefined ? prefer.s.uploadFolder : options.folderId, + folderId: this.options.folderId === undefined ? prefer.s.uploadFolder : this.options.folderId, isSensitive: item.isSensitive ?? false, caption: item.caption ?? null, onProgress: (progress) => { @@ -438,7 +450,7 @@ export function useUploader(options: { await filePromise.then((file) => { item.uploaded = file; item.abort = null; - events.emit('itemUploaded', { item }); + this.events.emit('itemUploaded', { item }); }).catch(err => { item.uploadFailed = true; item.progress = null; @@ -450,26 +462,26 @@ export function useUploader(options: { }); } - async function upload() { // エラーハンドリングなどを考慮してシーケンシャルにやる - items.value = items.value.map(item => ({ + public async upload() { // エラーハンドリングなどを考慮してシーケンシャルにやる + this.items.value = this.items.value.map(item => ({ ...item, aborted: false, uploadFailed: false, uploading: false, })); - for (const item of items.value.filter(item => item.uploaded == null)) { + for (const item of this.items.value.filter(item => item.uploaded == null)) { // アップロード処理途中で値が変わる場合(途中で全キャンセルされたりなど)もあるので、Array filterではなくここでチェック if (item.aborted) { continue; } - await uploadOne(item); + await this.uploadOne(item); } } - function abortAll() { - for (const item of items.value) { + public abortAll() { + for (const item of this.items.value) { if (item.uploaded != null) { continue; } @@ -482,12 +494,12 @@ export function useUploader(options: { } } - async function preprocess(item: UploaderItem): Promise { + private async preprocess(item: UploaderItem): Promise { item.preprocessing = true; try { if (IMAGE_PREPROCESS_NEEDED_TYPES.includes(item.file.type)) { - await preprocessForImage(item); + await this.preprocessForImage(item); } } catch (err) { console.error('Failed to preprocess image', err); @@ -498,12 +510,12 @@ export function useUploader(options: { item.preprocessing = false; } - async function preprocessForImage(item: UploaderItem): Promise { + private async preprocessForImage(item: UploaderItem): Promise { const imageBitmap = await window.createImageBitmap(item.file); let preprocessedFile: Blob | File = item.file; - const needsWatermark = item.watermarkPresetId != null && WATERMARK_SUPPORTED_TYPES.includes(preprocessedFile.type) && $i.policies.watermarkAvailable; + const needsWatermark = item.watermarkPresetId != null && WATERMARK_SUPPORTED_TYPES.includes(preprocessedFile.type) && this.$i.policies.watermarkAvailable; const preset = prefer.s.watermarkPresets.find(p => p.id === item.watermarkPresetId); if (needsWatermark && preset != null) { const canvas = window.document.createElement('canvas'); @@ -563,24 +575,4 @@ export function useUploader(options: { item.thumbnail = THUMBNAIL_SUPPORTED_TYPES.includes(preprocessedFile.type) ? window.URL.createObjectURL(preprocessedFile) : null; item.preprocessedFile = markRaw(preprocessedFile); } - - onUnmounted(() => { - for (const item of items.value) { - if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail); - } - }); - - return { - items, - addFiles, - removeItem, - abortAll, - upload, - getMenu, - uploading: computed(() => items.value.some(item => item.uploading)), - readyForUpload: computed(() => items.value.length > 0 && items.value.some(item => item.uploaded == null) && !items.value.some(item => item.uploading || item.preprocessing)), - allItemsUploaded: computed(() => items.value.every(item => item.uploaded != null)), - events, - }; } - diff --git a/packages/frontend/src/pages/share.vue b/packages/frontend/src/pages/share.vue index 51ac9d66f0..fe23112c43 100644 --- a/packages/frontend/src/pages/share.vue +++ b/packages/frontend/src/pages/share.vue @@ -13,6 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only :initialText="initialText" :initialVisibility="visibility" :initialFiles="files" + :initialLocalFiles="tempFiles" :initialLocalOnly="localOnly" :reply="reply" :renote="renote" @@ -33,6 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref, computed } from 'vue'; import * as Misskey from 'misskey-js'; +import { get } from 'idb-keyval'; import MkButton from '@/components/MkButton.vue'; import MkPostForm from '@/components/MkPostForm.vue'; import * as os from '@/os.js'; @@ -41,7 +43,19 @@ import { definePage } from '@/page.js'; import { postMessageToParentWindow } from '@/utility/post-message.js'; import { i18n } from '@/i18n.js'; +//#region parameters const urlParams = new URLSearchParams(window.location.search); +// merge hash parameters +try { + const hashParams = new URLSearchParams(window.location.hash.slice(1)); + for (const [key, value] of hashParams.entries()) { + urlParams.set(key, value); + } +} catch (e) { + console.error('Failed to parse hash parameters:', e); +} +//#endregion + const localOnlyQuery = urlParams.get('localOnly'); const visibilityQuery = urlParams.get('visibility') as typeof Misskey.noteVisibilities[number]; @@ -56,6 +70,7 @@ const visibility = ref(Misskey.noteVisibilities.includes(visibilityQuery) ? visi const localOnly = ref(localOnlyQuery === '0' ? false : localOnlyQuery === '1' ? true : undefined); const files = ref([] as Misskey.entities.DriveFile[]); const visibleUsers = ref([] as Misskey.entities.UserDetailed[]); +const tempFiles = ref([] as File[]); async function init() { let noteText = ''; @@ -182,6 +197,29 @@ async function init() { }); } + //#region Local files + // If the browser supports IndexedDB, try to get the temporary files from temp. + if (typeof window !== 'undefined' ? !!(window.indexedDB && typeof window.indexedDB.open === 'function') : true) { + const filesFromIdb = await get('share-files-temp'); + if (Array.isArray(filesFromIdb) && filesFromIdb.length > 0 && filesFromIdb.every(file => file instanceof Blob)) { + tempFiles.value = filesFromIdb; + } + } + + if (urlParams.has('file')) { + try { + const file = await window.fetch(urlParams.get('file')).then(res => res.blob()); + if (file instanceof Blob) { + tempFiles.value.push(file as File); + } else { + console.error('Fetched file is not a Blob:', file); + } + } catch (e) { + console.error('Failed to fetch file:', e); + } + } + //#endregion + state.value = 'writing'; } @@ -205,7 +243,26 @@ function onPosted(): void { postMessageToParentWindow('misskey:shareForm:shareCompleted'); } -const headerActions = computed(() => []); +const headerActions = computed(() => [ + { + icon: 'ti ti-dots', + text: i18n.ts.menu, + handler: (ev: MouseEvent) => { + os.popupMenu([ + { + icon: 'ti ti-home', + text: i18n.ts.goToMisskey, + action: () => goToMisskey(), + }, + { + icon: 'ti ti-x', + text: i18n.ts.close, + action: () => close(), + }, + ], ev.currentTarget ?? ev.target); + }, + }, +]); const headerTabs = computed(() => []); diff --git a/packages/frontend/src/types/post-form.ts b/packages/frontend/src/types/post-form.ts index 10e68d2d4a..9cd2804fbc 100644 --- a/packages/frontend/src/types/post-form.ts +++ b/packages/frontend/src/types/post-form.ts @@ -15,6 +15,7 @@ export interface PostFormProps { initialCw?: string; initialVisibility?: (typeof Misskey.noteVisibilities)[number]; initialFiles?: Misskey.entities.DriveFile[]; + initialLocalFiles?: File[]; initialLocalOnly?: boolean; initialVisibleUsers?: Misskey.entities.UserDetailed[]; initialNote?: Misskey.entities.Note; diff --git a/packages/sw/src/sw.ts b/packages/sw/src/sw.ts index 298af4b4b6..15bdc80389 100644 --- a/packages/sw/src/sw.ts +++ b/packages/sw/src/sw.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { get } from 'idb-keyval'; +import { get, set } from 'idb-keyval'; import * as Misskey from 'misskey-js'; import type { PushNotificationDataMap } from '@/types.js'; import type { I18n } from '@@/js/i18n.js'; @@ -39,7 +39,32 @@ async function offlineContentHTML() { return `${messages.title}
${messages.header}
v${_VERSION_}
`; } -globalThis.addEventListener('fetch', ev => { +globalThis.addEventListener('fetch', async ev => { + //#region /sw/share + const url = new URL(ev.request.url); + if (url.pathname === '/sw/share') { + ev.respondWith((async () => { + const responseUrl = new URL(ev.request.url); + responseUrl.pathname = '/share'; + const formData = await ev.request.formData(); + + if (formData.has('files')) { + const files = formData.getAll('files'); + if (files.length > 0 && files.every(file => file instanceof Blob)) { + set('share-files-temp', files); + } + } + + formData.delete('files'); + for (const [key, value] of formData.entries()) { + responseUrl.searchParams.set(key, value.toString()); + } + + return Response.redirect(responseUrl, 303); + })()); + } + + //#region others let isHTMLRequest = false; if (ev.request.headers.get('sec-fetch-dest') === 'document') { isHTMLRequest = true; @@ -62,6 +87,7 @@ globalThis.addEventListener('fetch', ev => { }); }), ); + //#endregion }); globalThis.addEventListener('push', ev => { From 2315c09134c51acf34cae942e9f7e2a5047ffa8b Mon Sep 17 00:00:00 2001 From: tamaina Date: Sat, 26 Jul 2025 01:48:46 +0900 Subject: [PATCH 02/10] modify manifest --- packages/backend/src/server/web/ClientServerService.ts | 6 ++++++ packages/backend/src/server/web/manifest.json | 8 +++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index 8ca61a497d..af02363ab9 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -186,6 +186,12 @@ export class ClientServerService { 'title': 'title', 'text': 'text', 'url': 'url', + 'files': [ + { + 'name': 'file', + 'accept': '*/*' + }, + ], }, }, }; diff --git a/packages/backend/src/server/web/manifest.json b/packages/backend/src/server/web/manifest.json index 41171d62a1..437d349ac5 100644 --- a/packages/backend/src/server/web/manifest.json +++ b/packages/backend/src/server/web/manifest.json @@ -32,7 +32,13 @@ "params": { "title": "title", "text": "text", - "url": "url" + "url": "url", + "files": [ + { + "name": "file", + "accept": "*/*" + } + ] } } } From a672a757a28232fec897b262c1cf31a02133839b Mon Sep 17 00:00:00 2001 From: tamaina Date: Sat, 26 Jul 2025 01:55:32 +0900 Subject: [PATCH 03/10] modify manifest --- packages/backend/src/server/web/ClientServerService.ts | 8 ++++---- packages/backend/src/server/web/manifest.json | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index af02363ab9..49ee7dd46d 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -179,9 +179,9 @@ export class ClientServerService { 'purpose': 'any', }], 'share_target': { - 'action': '/share/', - 'method': 'GET', - 'enctype': 'application/x-www-form-urlencoded', + 'action': '/sw/share', + 'method': 'POST', + 'enctype': 'multipart/form-data', 'params': { 'title': 'title', 'text': 'text', @@ -189,7 +189,7 @@ export class ClientServerService { 'files': [ { 'name': 'file', - 'accept': '*/*' + 'accept': '*/*', }, ], }, diff --git a/packages/backend/src/server/web/manifest.json b/packages/backend/src/server/web/manifest.json index 437d349ac5..4d05440bc9 100644 --- a/packages/backend/src/server/web/manifest.json +++ b/packages/backend/src/server/web/manifest.json @@ -26,9 +26,9 @@ } ], "share_target": { - "action": "/share/", - "method": "GET", - "enctype": "application/x-www-form-urlencoded", + "action": "/sw/share", + "method": "POST", + "enctype": "multipart/form-data", "params": { "title": "title", "text": "text", From 135c8ee6a0595d159a24c82ffad8796c6009241b Mon Sep 17 00:00:00 2001 From: tamaina Date: Sat, 26 Jul 2025 02:07:59 +0900 Subject: [PATCH 04/10] fix sw.ts --- packages/sw/src/sw.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/sw/src/sw.ts b/packages/sw/src/sw.ts index 15bdc80389..551a86b652 100644 --- a/packages/sw/src/sw.ts +++ b/packages/sw/src/sw.ts @@ -62,6 +62,7 @@ globalThis.addEventListener('fetch', async ev => { return Response.redirect(responseUrl, 303); })()); + return; } //#region others From f968a9a19614e06a5cf61b92156b0f50b8db9654 Mon Sep 17 00:00:00 2001 From: tamaina Date: Sat, 26 Jul 2025 02:14:57 +0900 Subject: [PATCH 05/10] =?UTF-8?q?fix=20file=20=E2=86=92=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/server/web/ClientServerService.ts | 2 +- packages/backend/src/server/web/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index 49ee7dd46d..7a5e5de7a4 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -188,7 +188,7 @@ export class ClientServerService { 'url': 'url', 'files': [ { - 'name': 'file', + 'name': 'files', 'accept': '*/*', }, ], diff --git a/packages/backend/src/server/web/manifest.json b/packages/backend/src/server/web/manifest.json index 4d05440bc9..fd104dd84e 100644 --- a/packages/backend/src/server/web/manifest.json +++ b/packages/backend/src/server/web/manifest.json @@ -35,7 +35,7 @@ "url": "url", "files": [ { - "name": "file", + "name": "files", "accept": "*/*" } ] From c4137efec14b695d539fba25014f1bb66eea0359 Mon Sep 17 00:00:00 2001 From: tamaina Date: Sat, 26 Jul 2025 02:22:30 +0900 Subject: [PATCH 06/10] update share.vue --- packages/frontend/src/pages/share.vue | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/frontend/src/pages/share.vue b/packages/frontend/src/pages/share.vue index fe23112c43..a98b565341 100644 --- a/packages/frontend/src/pages/share.vue +++ b/packages/frontend/src/pages/share.vue @@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref, computed } from 'vue'; import * as Misskey from 'misskey-js'; -import { get } from 'idb-keyval'; +import { get, del } from 'idb-keyval'; import MkButton from '@/components/MkButton.vue'; import MkPostForm from '@/components/MkPostForm.vue'; import * as os from '@/os.js'; @@ -204,9 +204,10 @@ async function init() { if (Array.isArray(filesFromIdb) && filesFromIdb.length > 0 && filesFromIdb.every(file => file instanceof Blob)) { tempFiles.value = filesFromIdb; } + del('share-files-temp'); // Clear the temporary files from IndexedDB } - if (urlParams.has('file')) { + if (urlParams.has('file') && urlParams.get('file').startsWith('data:')) { try { const file = await window.fetch(urlParams.get('file')).then(res => res.blob()); if (file instanceof Blob) { From 02d6b258c4ec263a0487162af0eb6f3ab87dcc9d Mon Sep 17 00:00:00 2001 From: tamaina Date: Sat, 26 Jul 2025 02:29:59 +0900 Subject: [PATCH 07/10] =?UTF-8?q?SW=E3=81=8C=E4=BF=9D=E5=AD=98=E3=81=97?= =?UTF-8?q?=E3=81=9F=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=81=AF=E6=8A=95?= =?UTF-8?q?=E7=A8=BF=E3=81=8C=E5=AE=8C=E4=BA=86=E3=81=99=E3=82=8B=E3=81=BE?= =?UTF-8?q?=E3=81=A7IndexedDB=E3=81=AB=E4=BF=9D=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/frontend/src/pages/share.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/frontend/src/pages/share.vue b/packages/frontend/src/pages/share.vue index a98b565341..888dae99ba 100644 --- a/packages/frontend/src/pages/share.vue +++ b/packages/frontend/src/pages/share.vue @@ -204,7 +204,6 @@ async function init() { if (Array.isArray(filesFromIdb) && filesFromIdb.length > 0 && filesFromIdb.every(file => file instanceof Blob)) { tempFiles.value = filesFromIdb; } - del('share-files-temp'); // Clear the temporary files from IndexedDB } if (urlParams.has('file') && urlParams.get('file').startsWith('data:')) { @@ -241,6 +240,8 @@ function goToMisskey(): void { function onPosted(): void { state.value = 'posted'; + // SWが保存したファイルは投稿が完了するまでIndexedDBに保持 + del('share-files-temp'); postMessageToParentWindow('misskey:shareForm:shareCompleted'); } From 6c61ef8861d01434b28de2d57f90ff006211237a Mon Sep 17 00:00:00 2001 From: tamaina Date: Sat, 26 Jul 2025 02:32:07 +0900 Subject: [PATCH 08/10] remove async --- packages/sw/src/sw.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sw/src/sw.ts b/packages/sw/src/sw.ts index 551a86b652..91efdd0139 100644 --- a/packages/sw/src/sw.ts +++ b/packages/sw/src/sw.ts @@ -39,7 +39,7 @@ async function offlineContentHTML() { return `${messages.title}
${messages.header}
v${_VERSION_}
`; } -globalThis.addEventListener('fetch', async ev => { +globalThis.addEventListener('fetch', ev => { //#region /sw/share const url = new URL(ev.request.url); if (url.pathname === '/sw/share') { From 8b36fde37e7d0be32401a59fb4bdceff7cd454cf Mon Sep 17 00:00:00 2001 From: tamaina Date: Sat, 26 Jul 2025 02:46:06 +0900 Subject: [PATCH 09/10] =?UTF-8?q?sw.ts=E3=81=A7/sw/share=E3=81=8C=E5=8F=A9?= =?UTF-8?q?=E3=81=8B=E3=82=8C=E3=81=9F=E3=82=89=E5=BF=85=E3=81=9Ashare-url?= =?UTF-8?q?-temp=E3=82=92=E5=88=9D=E6=9C=9F=E5=8C=96=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/sw/src/sw.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/sw/src/sw.ts b/packages/sw/src/sw.ts index 91efdd0139..933437f8cf 100644 --- a/packages/sw/src/sw.ts +++ b/packages/sw/src/sw.ts @@ -43,11 +43,13 @@ globalThis.addEventListener('fetch', ev => { //#region /sw/share const url = new URL(ev.request.url); if (url.pathname === '/sw/share') { - ev.respondWith((async () => { + ev.respondWith((async () => { const responseUrl = new URL(ev.request.url); responseUrl.pathname = '/share'; const formData = await ev.request.formData(); + // とりあえず初期化 (IndexedDBの削除は時間がかかる可能性があるため空の配列をセット) + await set('share-url-temp', []); if (formData.has('files')) { const files = formData.getAll('files'); if (files.length > 0 && files.every(file => file instanceof Blob)) { @@ -60,8 +62,8 @@ globalThis.addEventListener('fetch', ev => { responseUrl.searchParams.set(key, value.toString()); } - return Response.redirect(responseUrl, 303); - })()); + return Response.redirect(responseUrl, 303); + })()); return; } From 76de4ebea982ac1f2d4f62121c7d523e14c3a345 Mon Sep 17 00:00:00 2001 From: tamaina Date: Fri, 1 Aug 2025 21:22:07 +0900 Subject: [PATCH 10/10] =?UTF-8?q?revert=20useUploader=E2=86=92FileUploader?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../frontend/src/components/MkPostForm.vue | 4 +- .../src/components/MkUploaderDialog.vue | 4 +- .../frontend/src/composables/use-uploader.ts | 148 +++++++++--------- 3 files changed, 82 insertions(+), 74 deletions(-) diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 9e4cc54dce..d76e3791e4 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -146,7 +146,7 @@ import { getPluginHandlers } from '@/plugin.js'; import { DI } from '@/di.js'; import { globalEvents } from '@/events.js'; import { checkDragDataType, getDragData } from '@/drag-and-drop.js'; -import { FileUploader } from '@/composables/use-uploader.js'; +import { useUploader } from '@/composables/use-uploader.js'; const $i = ensureSignin(); @@ -214,7 +214,7 @@ const targetChannel = shallowRef(props.channel); const serverDraftId = ref(null); const postFormActions = getPluginHandlers('post_form_action'); -const uploader = new FileUploader({ +const uploader = useUploader({ multiple: true, }); diff --git a/packages/frontend/src/components/MkUploaderDialog.vue b/packages/frontend/src/components/MkUploaderDialog.vue index 372a61b881..ce098d71e4 100644 --- a/packages/frontend/src/components/MkUploaderDialog.vue +++ b/packages/frontend/src/components/MkUploaderDialog.vue @@ -58,7 +58,7 @@ import { i18n } from '@/i18n.js'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; import { ensureSignin } from '@/i.js'; -import { FileUploader } from '@/composables/use-uploader.js'; +import { useUploader } from '@/composables/use-uploader.js'; import MkUploaderItems from '@/components/MkUploaderItems.vue'; const $i = ensureSignin(); @@ -80,7 +80,7 @@ const emit = defineEmits<{ const dialog = useTemplateRef('dialog'); -const uploader = new FileUploader({ +const uploader = useUploader({ multiple: props.multiple, folderId: props.folderId, features: props.features, diff --git a/packages/frontend/src/composables/use-uploader.ts b/packages/frontend/src/composables/use-uploader.ts index 63b6a2f437..826d8c5203 100644 --- a/packages/frontend/src/composables/use-uploader.ts +++ b/packages/frontend/src/composables/use-uploader.ts @@ -99,43 +99,31 @@ function getCompressionSettings(level: 0 | 1 | 2 | 3) { } } -export class FileUploader { - private $i: Misskey.entities.MeDetailed; - public events = new EventEmitter<{ +export function useUploader(options: { + folderId?: string | null; + multiple?: boolean; + features?: UploaderFeatures; +} = {}) { + const $i = ensureSignin(); + + const events = new EventEmitter<{ 'itemUploaded': (ctx: { item: UploaderItem; }) => void; }>(); - private uploaderFeatures = computed>(() => { + + const uploaderFeatures = computed>(() => { return { - imageEditing: this.options.features?.imageEditing ?? true, - watermark: this.options.features?.watermark ?? true, + imageEditing: options.features?.imageEditing ?? true, + watermark: options.features?.watermark ?? true, }; }); - public items = ref([]); - public uploading = computed(() => this.items.value.some(item => item.uploading)); - public readyForUpload = computed(() => this.items.value.length > 0 && this.items.value.some(item => item.uploaded == null) && !this.items.value.some(item => item.uploading || item.preprocessing)); - public allItemsUploaded = computed(() => this.items.value.every(item => item.uploaded != null)); - constructor( - public options: { - folderId?: string | null; - multiple?: boolean; - features?: UploaderFeatures; - } = {} - ) { - this.$i = ensureSignin(); + const items = ref([]); - onUnmounted(() => { - for (const item of this.items.value) { - if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail); - } - }); - } - - private initializeFile(file: File) { + function initializeFile(file: File) { const id = genId(); const filename = file.name ?? 'untitled'; const extension = filename.split('.').length > 1 ? '.' + filename.split('.').pop() : ''; - this.items.value.push({ + items.value.push({ id, name: prefer.s.keepOriginalFilename ? filename : id + extension, progress: null, @@ -146,27 +134,27 @@ export class FileUploader { uploaded: null, uploadFailed: false, compressionLevel: prefer.s.defaultImageCompressionLevel, - watermarkPresetId: this.uploaderFeatures.value.watermark && this.$i.policies.watermarkAvailable ? prefer.s.defaultWatermarkPresetId : null, + watermarkPresetId: uploaderFeatures.value.watermark && $i.policies.watermarkAvailable ? prefer.s.defaultWatermarkPresetId : null, file: markRaw(file), }); - const reactiveItem = this.items.value.at(-1)!; - this.preprocess(reactiveItem).then(() => { - triggerRef(this.items); + const reactiveItem = items.value.at(-1)!; + preprocess(reactiveItem).then(() => { + triggerRef(items); }); } - public addFiles(newFiles: File[]) { + function addFiles(newFiles: File[]) { for (const file of newFiles) { - this.initializeFile(file); + initializeFile(file); } } - public removeItem(item: UploaderItem) { + function removeItem(item: UploaderItem) { if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail); - this.items.value.splice(this.items.value.indexOf(item), 1); + items.value.splice(items.value.indexOf(item), 1); } - public getMenu(item: UploaderItem): MenuItem[] { + function getMenu(item: UploaderItem): MenuItem[] { const menu: MenuItem[] = []; if ( @@ -218,7 +206,7 @@ export class FileUploader { } if ( - this.uploaderFeatures.value.imageEditing && + uploaderFeatures.value.imageEditing && IMAGE_EDITING_SUPPORTED_TYPES.includes(item.file.type) && !item.preprocessing && !item.uploading && @@ -234,14 +222,14 @@ export class FileUploader { action: async () => { const cropped = await os.cropImageFile(item.file, { aspectRatio: null }); if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail); - this.items.value.splice(this.items.value.indexOf(item), 1, { + items.value.splice(items.value.indexOf(item), 1, { ...item, file: markRaw(cropped), thumbnail: window.URL.createObjectURL(cropped), }); - const reactiveItem = this.items.value.find(x => x.id === item.id)!; - this.preprocess(reactiveItem).then(() => { - triggerRef(this.items); + const reactiveItem = items.value.find(x => x.id === item.id)!; + preprocess(reactiveItem).then(() => { + triggerRef(items); }); }, }, /*{ @@ -259,14 +247,14 @@ export class FileUploader { }, { ok: (file) => { if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail); - this.items.value.splice(this.items.value.indexOf(item), 1, { + items.value.splice(items.value.indexOf(item), 1, { ...item, file: markRaw(file), thumbnail: window.URL.createObjectURL(file), }); - const reactiveItem = this.items.value.find(x => x.id === item.id)!; - this.preprocess(reactiveItem).then(() => { - triggerRef(this.items); + const reactiveItem = items.value.find(x => x.id === item.id)!; + preprocess(reactiveItem).then(() => { + triggerRef(items); }); }, closed: () => dispose(), @@ -277,19 +265,19 @@ export class FileUploader { } if ( - this.uploaderFeatures.value.watermark && - this.$i.policies.watermarkAvailable && + uploaderFeatures.value.watermark && + $i.policies.watermarkAvailable && WATERMARK_SUPPORTED_TYPES.includes(item.file.type) && !item.preprocessing && !item.uploading && !item.uploaded ) { - const changeWatermarkPreset = (presetId: string | null) => { + function changeWatermarkPreset(presetId: string | null) { item.watermarkPresetId = presetId; - this.preprocess(item).then(() => { - triggerRef(this.items); + preprocess(item).then(() => { + triggerRef(items); }); - }; + } menu.push({ icon: 'ti ti-copyright', @@ -335,12 +323,12 @@ export class FileUploader { !item.uploading && !item.uploaded ) { - const changeCompressionLevel = (level: 0 | 1 | 2 | 3) => { + function changeCompressionLevel(level: 0 | 1 | 2 | 3) { item.compressionLevel = level; - this.preprocess(item).then(() => { - triggerRef(this.items); + preprocess(item).then(() => { + triggerRef(items); }); - }; + } menu.push({ icon: 'ti ti-leaf', @@ -393,14 +381,14 @@ export class FileUploader { icon: 'ti ti-upload', text: i18n.ts.upload, action: () => { - this.uploadOne(item); + uploadOne(item); }, }, { icon: 'ti ti-x', text: i18n.ts.remove, danger: true, action: () => { - this.removeItem(item); + removeItem(item); }, }); } else if (item.uploading) { @@ -421,13 +409,13 @@ export class FileUploader { return menu; } - private async uploadOne(item: UploaderItem): Promise { + async function uploadOne(item: UploaderItem): Promise { item.uploadFailed = false; item.uploading = true; const { filePromise, abort } = uploadFile(item.preprocessedFile ?? item.file, { name: item.uploadName ?? item.name, - folderId: this.options.folderId === undefined ? prefer.s.uploadFolder : this.options.folderId, + folderId: options.folderId === undefined ? prefer.s.uploadFolder : options.folderId, isSensitive: item.isSensitive ?? false, caption: item.caption ?? null, onProgress: (progress) => { @@ -450,7 +438,7 @@ export class FileUploader { await filePromise.then((file) => { item.uploaded = file; item.abort = null; - this.events.emit('itemUploaded', { item }); + events.emit('itemUploaded', { item }); }).catch(err => { item.uploadFailed = true; item.progress = null; @@ -462,26 +450,26 @@ export class FileUploader { }); } - public async upload() { // エラーハンドリングなどを考慮してシーケンシャルにやる - this.items.value = this.items.value.map(item => ({ + async function upload() { // エラーハンドリングなどを考慮してシーケンシャルにやる + items.value = items.value.map(item => ({ ...item, aborted: false, uploadFailed: false, uploading: false, })); - for (const item of this.items.value.filter(item => item.uploaded == null)) { + for (const item of items.value.filter(item => item.uploaded == null)) { // アップロード処理途中で値が変わる場合(途中で全キャンセルされたりなど)もあるので、Array filterではなくここでチェック if (item.aborted) { continue; } - await this.uploadOne(item); + await uploadOne(item); } } - public abortAll() { - for (const item of this.items.value) { + function abortAll() { + for (const item of items.value) { if (item.uploaded != null) { continue; } @@ -494,12 +482,12 @@ export class FileUploader { } } - private async preprocess(item: UploaderItem): Promise { + async function preprocess(item: UploaderItem): Promise { item.preprocessing = true; try { if (IMAGE_PREPROCESS_NEEDED_TYPES.includes(item.file.type)) { - await this.preprocessForImage(item); + await preprocessForImage(item); } } catch (err) { console.error('Failed to preprocess image', err); @@ -510,12 +498,12 @@ export class FileUploader { item.preprocessing = false; } - private async preprocessForImage(item: UploaderItem): Promise { + async function preprocessForImage(item: UploaderItem): Promise { const imageBitmap = await window.createImageBitmap(item.file); let preprocessedFile: Blob | File = item.file; - const needsWatermark = item.watermarkPresetId != null && WATERMARK_SUPPORTED_TYPES.includes(preprocessedFile.type) && this.$i.policies.watermarkAvailable; + const needsWatermark = item.watermarkPresetId != null && WATERMARK_SUPPORTED_TYPES.includes(preprocessedFile.type) && $i.policies.watermarkAvailable; const preset = prefer.s.watermarkPresets.find(p => p.id === item.watermarkPresetId); if (needsWatermark && preset != null) { const canvas = window.document.createElement('canvas'); @@ -575,4 +563,24 @@ export class FileUploader { item.thumbnail = THUMBNAIL_SUPPORTED_TYPES.includes(preprocessedFile.type) ? window.URL.createObjectURL(preprocessedFile) : null; item.preprocessedFile = markRaw(preprocessedFile); } + + onUnmounted(() => { + for (const item of items.value) { + if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail); + } + }); + + return { + items, + addFiles, + removeItem, + abortAll, + upload, + getMenu, + uploading: computed(() => items.value.some(item => item.uploading)), + readyForUpload: computed(() => items.value.length > 0 && items.value.some(item => item.uploaded == null) && !items.value.some(item => item.uploading || item.preprocessing)), + allItemsUploaded: computed(() => items.value.every(item => item.uploaded != null)), + events, + }; } +