From 8ec973c3ff0c8e4badd86735d9b7c5494ab0de55 Mon Sep 17 00:00:00 2001 From: Vitor Oliveira Date: Sat, 25 Apr 2026 14:19:49 -0300 Subject: [PATCH] feat: add Web Share Target support --- app/pages/index.vue | 26 +++++++++++++++++++++++-- nuxt.config.ts | 19 +++++++++++++++++- public/sw-share-target.js | 41 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 public/sw-share-target.js diff --git a/app/pages/index.vue b/app/pages/index.vue index 42a04c6..d187a53 100644 --- a/app/pages/index.vue +++ b/app/pages/index.vue @@ -106,6 +106,8 @@ const runtimeConfig = useRuntimeConfig(); const { t } = useI18n(); +const sharedFiles = ref([]); + const { open: openFileDialog, onChange } = useFileDialog(); onChange(async (files) => { @@ -131,7 +133,20 @@ const targetId = ref(""); const selectPeer = (id: string) => { targetId.value = id; - openFileDialog(); + + if (sharedFiles.value.length > 0) { + // Came from share target — use already received files + const files = sharedFiles.value; + sharedFiles.value = []; + startSendSession({ + files: files as unknown as FileList, + targetId: id, + onPin: async () => prompt(t("index.enterPin")), + }); + } else { + // Normal flow — open file dialog + openFileDialog(); + } }; const updateAlias = async () => { @@ -202,6 +217,13 @@ onMounted(async () => { return prompt(t("index.enterPin")); }, }); + + // Listen for shared files from the Web Share Target SW handler + navigator.serviceWorker.addEventListener("message", (event) => { + if (event.data?.type === "share-target") { + sharedFiles.value = event.data.files ?? []; + } + }); }); @@ -214,4 +236,4 @@ onMounted(async () => { transform: rotate(360deg); } } - + \ No newline at end of file diff --git a/nuxt.config.ts b/nuxt.config.ts index 9496687..ac4b776 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -146,8 +146,25 @@ export default defineNuxtConfig({ purpose: "any maskable", }, ], + share_target: { + action: "/", + method: "POST", + enctype: "multipart/form-data", + params: { + title: "title", + text: "text", + url: "url", + files: [ + { + name: "files", + accept: ["*/*"], + }, + ], + }, + }, }, workbox: { + importScripts: ["sw-share-target.js"], globPatterns: ["/", "**/*.{js,css,html,png,svg,ico}"], navigateFallback: "/", runtimeCaching: [ @@ -174,4 +191,4 @@ export default defineNuxtConfig({ type: "module", }, }, -}); +}); \ No newline at end of file diff --git a/public/sw-share-target.js b/public/sw-share-target.js new file mode 100644 index 0000000..5b3ee56 --- /dev/null +++ b/public/sw-share-target.js @@ -0,0 +1,41 @@ +self.addEventListener('fetch', (event) => { + const url = new URL(event.request.url) + if (url.pathname !== '/' || event.request.method !== 'POST') return + + event.respondWith( + (async () => { + const formData = await event.request.formData() + const files = formData.getAll('files') + const title = formData.get('title') ?? '' + const text = formData.get('text') ?? '' + const sharedUrl = formData.get('url') ?? '' + + const payload = { + type: 'share-target', + files, + title, + text, + url: sharedUrl, + } + + // Redirect first, then postMessage after client loads + const response = Response.redirect(self.location.origin + '/', 303) + + event.waitUntil( + (async () => { + // Wait for the client to be ready after redirect + await new Promise(resolve => setTimeout(resolve, 1000)) + const allClients = await self.clients.matchAll({ + type: 'window', + includeUncontrolled: true, + }) + for (const client of allClients) { + client.postMessage(payload) + } + })() + ) + + return response + })() + ) +}) \ No newline at end of file