mirror of
https://github.com/diced/zipline.git
synced 2025-05-11 10:26:05 +02:00
feat: pwa
This commit is contained in:
parent
fbb1f54b27
commit
a62e03b439
13 changed files with 204 additions and 3 deletions
|
@ -116,6 +116,13 @@ model Zipline {
|
||||||
discordOnShortenAvatarUrl String?
|
discordOnShortenAvatarUrl String?
|
||||||
discordOnShortenContent String?
|
discordOnShortenContent String?
|
||||||
discordOnShortenEmbed Json?
|
discordOnShortenEmbed Json?
|
||||||
|
|
||||||
|
pwaEnabled Boolean @default(false)
|
||||||
|
pwaTitle String @default("Zipline")
|
||||||
|
pwaShortName String @default("Zipline")
|
||||||
|
pwaDescription String @default("Zipline")
|
||||||
|
pwaThemeColor String @default("#000000")
|
||||||
|
pwaBackgroundColor String @default("#000000")
|
||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
|
|
BIN
public/favicon-128x128.png
Normal file
BIN
public/favicon-128x128.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5 KiB |
BIN
public/favicon-16x16.png
Normal file
BIN
public/favicon-16x16.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 582 B |
BIN
public/favicon-32x32.png
Normal file
BIN
public/favicon-32x32.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
BIN
public/favicon-512x512.png
Normal file
BIN
public/favicon-512x512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
BIN
public/favicon-64x64.png
Normal file
BIN
public/favicon-64x64.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
|
@ -14,6 +14,7 @@ import ServerSettingsRatelimit from './parts/ServerSettingsRatelimit';
|
||||||
import ServerSettingsTasks from './parts/ServerSettingsTasks';
|
import ServerSettingsTasks from './parts/ServerSettingsTasks';
|
||||||
import ServerSettingsUrls from './parts/ServerSettingsUrls';
|
import ServerSettingsUrls from './parts/ServerSettingsUrls';
|
||||||
import ServerSettingsWebsite from './parts/ServerSettingsWebsite';
|
import ServerSettingsWebsite from './parts/ServerSettingsWebsite';
|
||||||
|
import ServerSettingsPWA from './parts/ServerSettingsPWA';
|
||||||
|
|
||||||
export default function DashboardSettings() {
|
export default function DashboardSettings() {
|
||||||
const { data, isLoading, error } = useSWR<Response['/api/server/settings']>('/api/server/settings');
|
const { data, isLoading, error } = useSWR<Response['/api/server/settings']>('/api/server/settings');
|
||||||
|
@ -44,6 +45,10 @@ export default function DashboardSettings() {
|
||||||
<ServerSettingsRatelimit swr={{ data, isLoading }} />
|
<ServerSettingsRatelimit swr={{ data, isLoading }} />
|
||||||
<ServerSettingsWebsite swr={{ data, isLoading }} />
|
<ServerSettingsWebsite swr={{ data, isLoading }} />
|
||||||
<ServerSettingsOauth swr={{ data, isLoading }} />
|
<ServerSettingsOauth swr={{ data, isLoading }} />
|
||||||
|
|
||||||
|
<ServerSettingsPWA swr={{ data, isLoading }} />
|
||||||
|
|
||||||
|
<ServerSettingsHttpWebhook swr={{ data, isLoading }} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
@ -53,8 +58,6 @@ export default function DashboardSettings() {
|
||||||
<div>Error loading server settings</div>
|
<div>Error loading server settings</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<ServerSettingsHttpWebhook swr={{ data, isLoading }} />
|
|
||||||
|
|
||||||
<ServerSettingsDiscord swr={{ data, isLoading }} />
|
<ServerSettingsDiscord swr={{ data, isLoading }} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
126
src/components/pages/serverSettings/parts/ServerSettingsPWA.tsx
Normal file
126
src/components/pages/serverSettings/parts/ServerSettingsPWA.tsx
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
import { Response } from '@/lib/api/response';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
ColorInput,
|
||||||
|
LoadingOverlay,
|
||||||
|
Paper,
|
||||||
|
SimpleGrid,
|
||||||
|
Switch,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { useForm } from '@mantine/form';
|
||||||
|
import { IconDeviceFloppy } from '@tabler/icons-react';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { settingsOnSubmit } from '../settingsOnSubmit';
|
||||||
|
|
||||||
|
export default function ServerSettingsPWA({
|
||||||
|
swr: { data, isLoading },
|
||||||
|
}: {
|
||||||
|
swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean };
|
||||||
|
}) {
|
||||||
|
const router = useRouter();
|
||||||
|
const form = useForm({
|
||||||
|
initialValues: {
|
||||||
|
pwaEnabled: false,
|
||||||
|
pwaTitle: '',
|
||||||
|
pwaShortName: '',
|
||||||
|
pwaDescription: '',
|
||||||
|
pwaThemeColor: '',
|
||||||
|
pwaBackgroundColor: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (values: typeof form.values) => {
|
||||||
|
const sendValues: Record<string, any> = {};
|
||||||
|
|
||||||
|
sendValues.pwaTitle = values.pwaTitle.trim() === '' ? null : values.pwaTitle.trim();
|
||||||
|
sendValues.pwaShortName = values.pwaShortName.trim() === '' ? null : values.pwaShortName.trim();
|
||||||
|
sendValues.pwaDescription = values.pwaDescription.trim() === '' ? null : values.pwaDescription.trim();
|
||||||
|
|
||||||
|
return settingsOnSubmit(
|
||||||
|
router,
|
||||||
|
form,
|
||||||
|
)({
|
||||||
|
...sendValues,
|
||||||
|
pwaEnabled: values.pwaEnabled,
|
||||||
|
pwaThemeColor: values.pwaThemeColor,
|
||||||
|
pwaBackgroundColor: values.pwaBackgroundColor,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.setValues({
|
||||||
|
pwaEnabled: data?.pwaEnabled ?? false,
|
||||||
|
pwaTitle: data?.pwaTitle ?? '',
|
||||||
|
pwaShortName: data?.pwaShortName ?? '',
|
||||||
|
pwaDescription: data?.pwaDescription ?? '',
|
||||||
|
pwaThemeColor: data?.pwaThemeColor ?? '',
|
||||||
|
pwaBackgroundColor: data?.pwaBackgroundColor ?? '',
|
||||||
|
});
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper withBorder p='sm' pos='relative'>
|
||||||
|
<LoadingOverlay visible={isLoading} />
|
||||||
|
|
||||||
|
<Title order={2}>PWA</Title>
|
||||||
|
|
||||||
|
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||||
|
<Switch
|
||||||
|
mt='md'
|
||||||
|
label='PWA Enabled'
|
||||||
|
description='Allow users to install the Zipline PWA on their devices. After enabling, refresh the page to see the download button.'
|
||||||
|
{...form.getInputProps('pwaEnabled', { type: 'checkbox' })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SimpleGrid mt='md' cols={{ base: 1, md: 2 }} spacing='lg'>
|
||||||
|
<TextInput
|
||||||
|
label='Title'
|
||||||
|
description='The title for the PWA'
|
||||||
|
placeholder='Zipline'
|
||||||
|
disabled={!form.values.pwaEnabled}
|
||||||
|
{...form.getInputProps('pwaTitle')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
label='Short Name'
|
||||||
|
description='The short name for the PWA'
|
||||||
|
placeholder='Zipline'
|
||||||
|
disabled={!form.values.pwaEnabled}
|
||||||
|
{...form.getInputProps('pwaShortName')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
label='Description'
|
||||||
|
description='The description for the PWA'
|
||||||
|
placeholder='Zipline'
|
||||||
|
disabled={!form.values.pwaEnabled}
|
||||||
|
{...form.getInputProps('pwaDescription')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ColorInput
|
||||||
|
label='Theme Color'
|
||||||
|
description='The theme color for the PWA'
|
||||||
|
placeholder='#000000'
|
||||||
|
disabled={!form.values.pwaEnabled}
|
||||||
|
{...form.getInputProps('pwaThemeColor')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ColorInput
|
||||||
|
label='Background Color'
|
||||||
|
description='The background color for the PWA'
|
||||||
|
placeholder='#ffffff'
|
||||||
|
disabled={!form.values.pwaEnabled}
|
||||||
|
{...form.getInputProps('pwaBackgroundColor')}
|
||||||
|
/>
|
||||||
|
</SimpleGrid>
|
||||||
|
|
||||||
|
<Button type='submit' mt='md' loading={isLoading} leftSection={<IconDeviceFloppy size='1rem' />}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
|
@ -129,6 +129,14 @@ export const rawConfig: any = {
|
||||||
key: undefined,
|
key: undefined,
|
||||||
cert: undefined,
|
cert: undefined,
|
||||||
},
|
},
|
||||||
|
pwa: {
|
||||||
|
enabled: undefined,
|
||||||
|
title: undefined,
|
||||||
|
shortName: undefined,
|
||||||
|
description: undefined,
|
||||||
|
backgroundColor: undefined,
|
||||||
|
themeColor: undefined,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PROP_TO_ENV = {
|
export const PROP_TO_ENV = {
|
||||||
|
@ -256,6 +264,13 @@ export const DATABASE_TO_PROP = {
|
||||||
discordOnShortenAvatarUrl: 'discord.onShorten.avatarUrl',
|
discordOnShortenAvatarUrl: 'discord.onShorten.avatarUrl',
|
||||||
discordOnShortenContent: 'discord.onShorten.content',
|
discordOnShortenContent: 'discord.onShorten.content',
|
||||||
discordOnShortenEmbed: 'discord.onShorten.embed',
|
discordOnShortenEmbed: 'discord.onShorten.embed',
|
||||||
|
|
||||||
|
pwaEnabled: 'pwa.enabled',
|
||||||
|
pwaTitle: 'pwa.title',
|
||||||
|
pwaShortName: 'pwa.shortName',
|
||||||
|
pwaDescription: 'pwa.description',
|
||||||
|
pwaThemeColor: 'pwa.themeColor',
|
||||||
|
pwaBackgroundColor: 'pwa.backgroundColor',
|
||||||
};
|
};
|
||||||
|
|
||||||
const logger = log('config').c('read');
|
const logger = log('config').c('read');
|
||||||
|
|
|
@ -297,6 +297,14 @@ export const schema = z.object({
|
||||||
.nullable()
|
.nullable()
|
||||||
.default(null),
|
.default(null),
|
||||||
}),
|
}),
|
||||||
|
pwa: z.object({
|
||||||
|
enabled: z.boolean().default(true),
|
||||||
|
title: z.string().default('Zipline'),
|
||||||
|
shortName: z.string().default('Zipline'),
|
||||||
|
description: z.string().default('Zipline'),
|
||||||
|
themeColor: z.string().default('#000000'),
|
||||||
|
backgroundColor: z.string().default('#000000'),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Config = z.infer<typeof schema>;
|
export type Config = z.infer<typeof schema>;
|
||||||
|
|
|
@ -12,7 +12,6 @@ import '@mantine/dropzone/styles.css';
|
||||||
import '@mantine/notifications/styles.css';
|
import '@mantine/notifications/styles.css';
|
||||||
import 'mantine-datatable/styles.css';
|
import 'mantine-datatable/styles.css';
|
||||||
|
|
||||||
import '@/lib/theme/override.css';
|
|
||||||
import '@/components/render/code/HighlightCode.theme.css';
|
import '@/components/render/code/HighlightCode.theme.css';
|
||||||
|
|
||||||
const fetcher = async (url: RequestInfo | URL) => {
|
const fetcher = async (url: RequestInfo | URL) => {
|
||||||
|
@ -35,6 +34,7 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||||
<Head>
|
<Head>
|
||||||
<title>Zipline</title>
|
<title>Zipline</title>
|
||||||
<meta name='viewport' content='minimum-scale=1, initial-scale=1, width=device-width' />
|
<meta name='viewport' content='minimum-scale=1, initial-scale=1, width=device-width' />
|
||||||
|
<link rel='manifest' href='/manifest.json' />
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<SWRConfig
|
<SWRConfig
|
||||||
|
|
|
@ -220,6 +220,13 @@ export default fastifyPlugin(
|
||||||
discordOnShortenAvatarUrl: z.string().url().nullable(),
|
discordOnShortenAvatarUrl: z.string().url().nullable(),
|
||||||
discordOnShortenContent: z.string().nullable(),
|
discordOnShortenContent: z.string().nullable(),
|
||||||
discordOnShortenEmbed: discordEmbed,
|
discordOnShortenEmbed: discordEmbed,
|
||||||
|
|
||||||
|
pwaEnabled: z.boolean(),
|
||||||
|
pwaTitle: z.string(),
|
||||||
|
pwaShortName: z.string(),
|
||||||
|
pwaDescription: z.string(),
|
||||||
|
pwaThemeColor: z.string().regex(/^#?([a-f0-9]{6}|[a-f0-9]{3})$/),
|
||||||
|
pwaBackgroundColor: z.string().regex(/^#?([a-f0-9]{6}|[a-f0-9]{3})/),
|
||||||
})
|
})
|
||||||
.partial()
|
.partial()
|
||||||
.refine(
|
.refine(
|
||||||
|
|
35
src/server/routes/manifest.ts
Normal file
35
src/server/routes/manifest.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import { config } from '@/lib/config';
|
||||||
|
import fastifyPlugin from 'fastify-plugin';
|
||||||
|
import type { MetadataRoute } from 'next';
|
||||||
|
|
||||||
|
function generateIcons(sizes: number[]): MetadataRoute.Manifest['icons'] {
|
||||||
|
return sizes.map((size) => ({
|
||||||
|
src: `/favicon-${size}x${size}.png`,
|
||||||
|
sizes: `${size}x${size}`,
|
||||||
|
type: 'image/png',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PATH = '/manifest.json';
|
||||||
|
export default fastifyPlugin(
|
||||||
|
(server, _, done) => {
|
||||||
|
server.get(PATH, async (_, res) => {
|
||||||
|
if (!config.pwa.enabled) return res.notFound();
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: config.pwa.title,
|
||||||
|
short_name: config.pwa.shortName,
|
||||||
|
description: config.pwa.description,
|
||||||
|
theme_color: config.pwa.themeColor,
|
||||||
|
background_color: config.pwa.backgroundColor,
|
||||||
|
|
||||||
|
start_url: '/',
|
||||||
|
display: 'standalone',
|
||||||
|
icons: generateIcons([16, 32, 64, 128, 512]),
|
||||||
|
} satisfies MetadataRoute.Manifest;
|
||||||
|
});
|
||||||
|
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
{ name: PATH },
|
||||||
|
);
|
Loading…
Add table
Add a link
Reference in a new issue