feat(pwa): add screenshots and orientation to webmanifest (#2109)
This commit is contained in:
parent
22556984fa
commit
dfb5a665f0
20 changed files with 230 additions and 127 deletions
|
@ -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<string, Partial<ExtendedManifestOptions>>
|
||||
export type LocalizedWebManifest = Record<string, Partial<ManifestOptions>>
|
||||
|
||||
export const pwaLocales = i18n.locales as LocaleObject[]
|
||||
|
||||
type WebManifestEntry = Pick<ExtendedManifestOptions, 'name' | 'short_name' | 'description'>
|
||||
type RequiredWebManifestEntry = Required<WebManifestEntry & Pick<ExtendedManifestOptions, 'dir' | 'lang'>>
|
||||
type WebManifestEntry = Pick<ManifestOptions, 'name' | 'short_name' | 'description' | 'screenshots' | 'shortcuts'>
|
||||
type RequiredWebManifestEntry = Required<WebManifestEntry & Pick<ManifestOptions, 'dir' | 'lang' | 'screenshots' | 'shortcuts'>>
|
||||
|
||||
export async function createI18n(): Promise<LocalizedWebManifest> {
|
||||
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<WebManifestEntry> = 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<ManifestOptions> = {
|
||||
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<LocalizedWebManifest> {
|
|||
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 <RequiredWebManifestEntry>{
|
||||
...defaultManifest,
|
||||
...entry,
|
||||
lang: code,
|
||||
dir,
|
||||
screenshots: useScreenshots,
|
||||
shortcuts: useShortcuts,
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
@ -69,13 +179,19 @@ export async function createI18n(): Promise<LocalizedWebManifest> {
|
|||
...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<LocalizedWebManifest> {
|
|||
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<LocalizedWebManifest> {
|
|||
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<string, string>
|
||||
shortcuts?: Record<string, string>
|
||||
}
|
||||
|
||||
interface JsonEntry {
|
||||
pwa?: PWAEntry
|
||||
app_name?: string
|
||||
app_desc_short?: string
|
||||
action?: Record<string, any>
|
||||
nav?: Record<string, any>
|
||||
screenshots?: Record<string, string>
|
||||
}
|
||||
|
||||
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<string, any> = {}
|
||||
const nav: Record<string, any> = {}
|
||||
|
||||
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 }
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue