From dfb5a665f0291038178a55fe549cad80a34d1b0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20S=C3=A1nchez?= Date: Sun, 21 May 2023 18:28:28 +0200 Subject: [PATCH] feat(pwa): add screenshots and orientation to webmanifest (#2109) --- composables/masto/publish.ts | 7 +- locales/en.json | 4 + locales/es.json | 4 + modules/pwa/i18n.ts | 290 ++++++++++++++--------- package.json | 2 +- pages/compose.vue | 5 + pnpm-lock.yaml | 22 +- public/screenshots/dark-1.webp | Bin 0 -> 163748 bytes public/screenshots/light-1.webp | Bin 0 -> 165476 bytes public/shortcuts/compose-96x96.png | Bin 0 -> 1782 bytes public/shortcuts/compose.png | Bin 0 -> 2973 bytes public/shortcuts/home-96x96.png | Bin 0 -> 781 bytes public/shortcuts/home.png | Bin 0 -> 1246 bytes public/shortcuts/local-96x96.png | Bin 0 -> 2428 bytes public/shortcuts/local.png | Bin 0 -> 4100 bytes public/shortcuts/notifications-96x96.png | Bin 0 -> 1179 bytes public/shortcuts/notifications.png | Bin 0 -> 2182 bytes public/shortcuts/settings-96x96.png | Bin 0 -> 2229 bytes public/shortcuts/settings.png | Bin 0 -> 3868 bytes service-worker/sw.ts | 23 +- 20 files changed, 230 insertions(+), 127 deletions(-) create mode 100644 public/screenshots/dark-1.webp create mode 100644 public/screenshots/light-1.webp create mode 100644 public/shortcuts/compose-96x96.png create mode 100644 public/shortcuts/compose.png create mode 100644 public/shortcuts/home-96x96.png create mode 100644 public/shortcuts/home.png create mode 100644 public/shortcuts/local-96x96.png create mode 100644 public/shortcuts/local.png create mode 100644 public/shortcuts/notifications-96x96.png create mode 100644 public/shortcuts/notifications.png create mode 100644 public/shortcuts/settings-96x96.png create mode 100644 public/shortcuts/settings.png diff --git a/composables/masto/publish.ts b/composables/masto/publish.ts index 312ed2a2..7e7cd47a 100644 --- a/composables/masto/publish.ts +++ b/composables/masto/publish.ts @@ -136,9 +136,10 @@ export function useUploadMediaAttachment(draftRef: Ref) { let failedAttachments = $ref([]) const dropZoneRef = ref() - const maxPixels - = currentInstance.value!.configuration?.mediaAttachments?.imageMatrixLimit - ?? 4096 ** 2 + const maxPixels = $computed(() => { + return currentInstance.value?.configuration?.mediaAttachments?.imageMatrixLimit + ?? 4096 ** 2 + }) const loadImage = (inputFile: Blob) => new Promise((resolve, reject) => { const url = URL.createObjectURL(inputFile) diff --git a/locales/en.json b/locales/en.json index ea3af38e..9a41aa7f 100644 --- a/locales/en.json +++ b/locales/en.json @@ -323,6 +323,10 @@ "dismiss": "Dismiss", "install": "Install", "install_title": "Install Elk", + "screenshots": { + "dark": "Screenshot of Elk running in dark mode", + "light": "Screenshot of Elk running in light mode" + }, "title": "New Elk update available!", "update": "Update", "update_available_short": "Update Elk", diff --git a/locales/es.json b/locales/es.json index d9fd08c4..f42fa5ed 100644 --- a/locales/es.json +++ b/locales/es.json @@ -311,6 +311,10 @@ "dismiss": "Descartar", "install": "Instalar", "install_title": "Instalar Elk", + "screenshots": { + "dark": "Captura de pantalla de Elk ejecutándose en modo oscuro", + "light": "Captura de pantalla de Elk ejecutándose en modo claro" + }, "title": "Nueva versión de Elk disponible", "update": "Actualizar", "update_available_short": "Actualiza Elk", diff --git a/modules/pwa/i18n.ts b/modules/pwa/i18n.ts index c65daf9d..e52bdc8b 100644 --- a/modules/pwa/i18n.ts +++ b/modules/pwa/i18n.ts @@ -6,48 +6,124 @@ import { getEnv } from '../../config/env' import { i18n } from '../../config/i18n' import type { LocaleObject } from '#i18n' -// We have to extend the ManifestOptions interface from 'vite-plugin-pwa' -// as that interface doesn't define the share_target field of Web App Manifests. -interface ExtendedManifestOptions extends ManifestOptions { - share_target: { - action: string - method: string - enctype: string - params: { - title: string - text: string - url: string - files: [{ - name: string - accept: string[] - }] - } - } -} - -export type LocalizedWebManifest = Record> +export type LocalizedWebManifest = Record> export const pwaLocales = i18n.locales as LocaleObject[] -type WebManifestEntry = Pick -type RequiredWebManifestEntry = Required> +type WebManifestEntry = Pick +type RequiredWebManifestEntry = Required> export async function createI18n(): Promise { const { env } = await getEnv() const envName = `${env === 'release' ? '' : `(${env})`}` - const { pwa } = await readI18nFile('en.json') + const { action, nav, pwa } = await readI18nFile('en.json') const defaultManifest: Required = pwa.webmanifest[env] + const defaultShortcuts: ManifestOptions['shortcuts'] = [{ + name: nav.home, + url: '/home', + icons: [ + { src: 'shortcuts/home-96x96.png', sizes: '96x96', type: 'image/png' }, + { src: 'shortcuts/home.png', sizes: '192x192', type: 'image/png' }, + ], + }, { + name: nav.local, + url: '/', + icons: [ + { src: 'shortcuts/local-96x96.png', sizes: '96x96', type: 'image/png' }, + { src: 'shortcuts/local.png', sizes: '192x192', type: 'image/png' }, + ], + }, { + name: nav.notifications, + url: '/notifications', + icons: [ + { src: 'shortcuts/notifications-96x96.png', sizes: '96x96', type: 'image/png' }, + { src: 'shortcuts/notifications.png', sizes: '192x192', type: 'image/png' }, + ], + }, { + name: action.compose, + url: '/compose', + icons: [ + { src: 'shortcuts/compose-96x96.png', sizes: '96x96', type: 'image/png' }, + { src: 'shortcuts/compose.png', sizes: '192x192', type: 'image/png' }, + ], + }, { + name: nav.settings, + url: '/settings', + icons: [ + { src: 'shortcuts/settings-96x96.png', sizes: '96x96', type: 'image/png' }, + { src: 'shortcuts/settings.png', sizes: '192x192', type: 'image/png' }, + ], + }] + + const defaultScreenshots: ManifestOptions['screenshots'] = [{ + src: 'screenshots/dark-1.webp', + sizes: '3840x2400', + type: 'image/webp', + label: pwa.screenshots.dark, + }, { + src: 'screenshots/light-1.webp', + sizes: '3840x2400', + type: 'image/webp', + label: pwa.screenshots.light, + }] + + const manifestEntries: Partial = { + scope: '/', + id: '/', + start_url: '/', + orientation: 'natural', + display: 'standalone', + display_override: ['window-controls-overlay'], + categories: ['social', 'social networking'], + icons: [ + { + src: 'pwa-192x192.png', + sizes: '192x192', + type: 'image/png', + }, + { + src: 'pwa-512x512.png', + sizes: '512x512', + type: 'image/png', + purpose: 'any', + }, + { + src: 'maskable-icon.png', + sizes: '512x512', + type: 'image/png', + purpose: 'maskable', + }, + ], + share_target: { + action: '/web-share-target', + method: 'POST', + enctype: 'multipart/form-data', + params: { + title: 'title', + text: 'text', + url: 'url', + files: [ + { + name: 'files', + accept: ['image/*', 'video/*'], + }, + ], + }, + }, + } + const locales: RequiredWebManifestEntry[] = await Promise.all( pwaLocales .filter(l => l.code !== 'en-US') .map(async ({ code, dir = 'ltr', file, files }) => { // read locale file or files - const { pwa, app_name, app_desc_short } = file + const { action, app_desc_short, app_name, nav, pwa } = file ? await readI18nFile(file) : await findBestWebManifestData(files, env) - const entry: WebManifestEntry = pwa?.webmanifest?.[env] ?? {} + const entry = pwa?.webmanifest?.[env] ?? {} + if (!entry.name && app_name) entry.name = dir === 'rtl' ? `${envName} ${app_name}` : `${app_name} ${envName}` @@ -57,11 +133,45 @@ export async function createI18n(): Promise { if (!entry.description && app_desc_short) entry.description = app_desc_short + // clone default screenshots and shortcuts + const useScreenshots = [...defaultScreenshots.map(screenshot => ({ ...screenshot }))] + const useShortcuts = [...defaultShortcuts.map(shortcut => ({ ...shortcut }))] + + const pwaScreenshots = pwa?.screenshots + if (pwaScreenshots) { + useScreenshots.forEach((screenshot, idx) => { + if (idx === 0 && pwaScreenshots?.dark) + screenshot.label = pwaScreenshots.dark + + if (idx === 1 && pwaScreenshots?.light) + screenshot.label = pwaScreenshots.light + }) + } + + useShortcuts.forEach((shortcut, idx) => { + if (idx === 0 && nav?.home) + shortcut.name = nav.home + + if (idx === 1 && nav?.local) + shortcut.name = nav.local + + if (idx === 2 && nav?.notifications) + shortcut.name = nav.notifications + + if (idx === 3 && action?.compose) + shortcut.name = action?.compose + + if (idx === 4 && nav?.settings) + shortcut.name = nav.settings + }) + return { ...defaultManifest, ...entry, lang: code, dir, + screenshots: useScreenshots, + shortcuts: useShortcuts, } }), ) @@ -69,13 +179,19 @@ export async function createI18n(): Promise { ...defaultManifest, lang: 'en-US', dir: 'ltr', + screenshots: defaultScreenshots, + shortcuts: defaultShortcuts, }) - return locales.reduce((acc, { lang, dir, name, short_name, description }) => { + return locales.reduce((acc, { + lang, + dir, + name, + short_name, + description, + shortcuts, + screenshots, + }) => { acc[lang] = { - scope: '/', - id: '/', - start_url: '/', - display: 'standalone', lang, name, short_name, @@ -83,47 +199,11 @@ export async function createI18n(): Promise { dir, background_color: '#ffffff', theme_color: '#ffffff', - icons: [ - { - src: 'pwa-192x192.png', - sizes: '192x192', - type: 'image/png', - }, - { - src: 'pwa-512x512.png', - sizes: '512x512', - type: 'image/png', - purpose: 'any', - }, - { - src: 'maskable-icon.png', - sizes: '512x512', - type: 'image/png', - purpose: 'maskable', - }, - ], - share_target: { - action: '/web-share-target', - method: 'POST', - enctype: 'multipart/form-data', - params: { - title: 'title', - text: 'text', - url: 'url', - files: [ - { - name: 'files', - accept: ['image/*', 'video/*'], - }, - ], - }, - }, + ...manifestEntries, + shortcuts, + screenshots, } acc[`${lang}-dark`] = { - scope: '/', - id: '/', - start_url: '/', - display: 'standalone', lang, name, short_name, @@ -131,41 +211,9 @@ export async function createI18n(): Promise { dir, background_color: '#111111', theme_color: '#111111', - icons: [ - { - src: 'pwa-192x192.png', - sizes: '192x192', - type: 'image/png', - }, - { - src: 'pwa-512x512.png', - sizes: '512x512', - type: 'image/png', - purpose: 'any', - }, - { - src: 'maskable-icon.png', - sizes: '512x512', - type: 'image/png', - purpose: 'maskable', - }, - ], - share_target: { - action: '/web-share-target', - method: 'POST', - enctype: 'multipart/form-data', - params: { - title: 'title', - text: 'text', - url: 'url', - files: [ - { - name: 'files', - accept: ['image/*', 'video/*'], - }, - ], - }, - }, + ...manifestEntries, + shortcuts, + screenshots, } return acc @@ -185,23 +233,30 @@ interface PWAEntry { short_name?: string description?: string }> + screenshots?: Record + shortcuts?: Record } interface JsonEntry { pwa?: PWAEntry app_name?: string app_desc_short?: string + action?: Record + nav?: Record + screenshots?: Record } async function findBestWebManifestData(files: string[], env: string) { const entries: JsonEntry[] = await Promise.all(files.map(async (file) => { - const { pwa, app_name, app_desc_short } = await readI18nFile(file) - return { pwa, app_name, app_desc_short } + const { action, app_name, app_desc_short, nav, pwa } = await readI18nFile(file) + return { action, app_name, app_desc_short, nav, pwa } })) let pwa: PWAEntry | undefined let app_name: string | undefined let app_desc_short: string | undefined + const action: Record = {} + const nav: Record = {} for (const entry of entries) { const webmanifest = entry?.pwa?.webmanifest?.[env] @@ -226,7 +281,28 @@ async function findBestWebManifestData(files: string[], env: string) { if (entry.app_desc_short) app_desc_short = entry.app_desc_short + + if (entry.nav) { + ['home', 'local', 'notifications', 'settings'].forEach((key) => { + const value = entry.nav![key] + if (value) + nav[key] = value + }) + } + + if (entry.action?.compose) + action.compose = entry.action.compose + + if (entry.pwa?.screenshots) { + if (!pwa) + pwa = {} + + pwa.screenshots = pwa.screenshots ?? {} + Object + .entries(entry.pwa.screenshots) + .forEach(([key, value]) => pwa!.screenshots![key] = value) + } } - return { pwa, app_name, app_desc_short } + return { action, app_desc_short, app_name, nav, pwa } } diff --git a/package.json b/package.json index 3e6fb109..bfd0617e 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "ufo": "^1.1.1", "ultrahtml": "^1.2.0", "unimport": "^3.0.6", - "vite-plugin-pwa": "^0.14.7", + "vite-plugin-pwa": "^0.15.0", "vue-advanced-cropper": "^2.8.8", "vue-virtual-scroller": "2.0.0-beta.8", "workbox-build": "^6.5.4", diff --git a/pages/compose.vue b/pages/compose.vue index a21ad452..4b2e59ef 100644 --- a/pages/compose.vue +++ b/pages/compose.vue @@ -1,5 +1,10 @@