feat: themes

This commit is contained in:
diced 2023-08-08 00:29:45 -07:00
parent 453a66b1b4
commit 7093c5f11e
No known key found for this signature in database
GPG key ID: 370BD1BA142842D1
40 changed files with 497 additions and 129 deletions

View file

@ -286,7 +286,6 @@ export default function Layout({ children, config }: { children: React.ReactNode
<Menu.Target>
<Button
variant='subtle'
color='gray'
leftIcon={
avatar ? (
<Avatar src={avatar} radius='sm' size='sm' alt={user?.username ?? 'User avatar'} />
@ -340,7 +339,11 @@ export default function Layout({ children, config }: { children: React.ReactNode
}
>
<ConfigProvider config={config}>
<Paper m={2} withBorder p={'xs'}>
<Paper
m={2}
withBorder
p='xs'
>
{children}
</Paper>
</ConfigProvider>

View file

@ -0,0 +1,54 @@
import { useSettingsStore } from '@/lib/store/settings';
import { ZiplineTheme, themeComponents } from '@/lib/theme';
import { MantineProvider } from '@mantine/core';
import { useColorScheme } from '@mantine/hooks';
import { createContext, useContext, useEffect } from 'react';
const ThemeContext = createContext<{
themes: ZiplineTheme[];
}>({
themes: [],
});
export function useThemes() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error('useThemes must be used within a ThemeProvider');
return ctx.themes;
}
export default function Theming({ themes, children }: { themes: ZiplineTheme[]; children: React.ReactNode }) {
const [currentTheme, preferredDark, preferredLight] = useSettingsStore((state) => [
state.settings.theme,
state.settings.themeDark,
state.settings.themeLight,
]);
const systemTheme = useColorScheme();
let theme = themes.find((theme) => theme.id === currentTheme);
if (currentTheme === 'system') {
theme =
systemTheme === 'dark'
? themes.find((theme) => theme.id === preferredDark)
: themes.find((theme) => theme.id === preferredLight);
}
if (!theme) {
theme =
themes.find((theme) => theme.id === 'builtin:dark_gray') ??
({
id: 'builtin:dark_gray',
name: 'Dark Gray',
colorScheme: 'dark',
primaryColor: 'gray',
} as ZiplineTheme); // back up theme if all else fails lol
}
return (
<ThemeContext.Provider value={{ themes }}>
<MantineProvider withGlobalStyles withNormalizeCSS withCSSVariables theme={themeComponents(theme)}>
{children}
</MantineProvider>
</ThemeContext.Provider>
);
}

View file

@ -61,7 +61,7 @@ export default function DashboardHome() {
<Stat Icon={IconDeviceSdCard} title='Storage used' value={bytes(stats!.storageUsed)} />
<Stat Icon={IconDeviceSdCard} title='Average storage used' value={bytes(stats!.avgStorageUsed)} />
<Stat Icon={IconEyeFilled} title='File views' value={stats!.views} />
<Stat Icon={IconEyeFilled} title='File average views' value={stats!.avgViews} />
<Stat Icon={IconEyeFilled} title='File average views' value={Math.round(stats!.avgViews)} />
<Stat Icon={IconLink} title='Links created' value={stats!.urlsCreated} />
</SimpleGrid>

View file

@ -78,7 +78,6 @@ export default function FavoriteFiles() {
</Group>
<Button
variant='outline'
color='gray'
compact
leftIcon={<IconFileUpload size='1rem' />}
component={Link}

View file

@ -211,7 +211,6 @@ export default function FileTable({ id }: { id?: string }) {
<Button
variant='outline'
color='gray'
onClick={() => {
setSelectedFiles([]);
}}
@ -295,7 +294,7 @@ export default function FileTable({ id }: { id?: string }) {
render: (file) => (
<Group spacing='sm'>
<Tooltip label='More details'>
<ActionIcon variant='outline' color='gray'>
<ActionIcon variant='outline'>
<IconFile size='1rem' />
</ActionIcon>
</Tooltip>
@ -303,7 +302,6 @@ export default function FileTable({ id }: { id?: string }) {
<Tooltip label='View file in new tab'>
<ActionIcon
variant='outline'
color='gray'
onClick={(e) => {
e.stopPropagation();
viewFile(file);
@ -316,7 +314,6 @@ export default function FileTable({ id }: { id?: string }) {
<Tooltip label='Copy file link to clipboard'>
<ActionIcon
variant='outline'
color='gray'
onClick={(e) => {
e.stopPropagation();
copyFile(file, clipboard);

View file

@ -68,7 +68,6 @@ export default function Files({ id }: { id?: string }) {
</Group>
<Button
variant='outline'
color='gray'
compact
leftIcon={<IconFileUpload size='1rem' />}
component={Link}

View file

@ -78,7 +78,6 @@ export default function FavoriteFiles() {
</Group>
<Button
variant='outline'
color='gray'
compact
leftIcon={<IconFileUpload size='1rem' />}
component={Link}

View file

@ -74,13 +74,7 @@ export default function DashboardFolders() {
{...form.getInputProps('isPublic', { type: 'checkbox' })}
/>
<Button
type='submit'
variant='outline'
color='gray'
radius='sm'
leftIcon={<IconFolderPlus size='1rem' />}
>
<Button type='submit' variant='outline' radius='sm' leftIcon={<IconFolderPlus size='1rem' />}>
Create
</Button>
</Stack>
@ -91,7 +85,7 @@ export default function DashboardFolders() {
<Title>Folders</Title>
<Tooltip label='Create a new folder'>
<ActionIcon variant='outline' color='gray' onClick={() => setOpen(true)}>
<ActionIcon variant='outline' onClick={() => setOpen(true)}>
<IconPlus size='1rem' />
</ActionIcon>
</Tooltip>

View file

@ -86,7 +86,6 @@ export default function FolderTableView() {
<Tooltip label='View files'>
<ActionIcon
variant='outline'
color='gray'
onClick={(e) => {
e.stopPropagation();
setSelectedFolder(folder);
@ -98,7 +97,6 @@ export default function FolderTableView() {
<Tooltip label='Copy folder link'>
<ActionIcon
variant='outline'
color='gray'
onClick={(e) => {
e.stopPropagation();
copyFolderUrl(folder, clipboard);

View file

@ -5,7 +5,7 @@ import { readToDataURL } from '@/lib/base64';
import { useUserStore } from '@/lib/store/user';
import { Avatar, Button, Card, FileInput, Group, Paper, Stack, Text, Title } from '@mantine/core';
import { notifications } from '@mantine/notifications';
import { IconChevronDown, IconPhoto, IconPhotoCancel, IconSettingsFilled } from '@tabler/icons-react';
import { IconChevronDown, IconPhoto, IconPhotoCancel, IconPhotoUp, IconSettingsFilled } from '@tabler/icons-react';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import useSWR from 'swr';
@ -94,6 +94,7 @@ export default function SettingsAvatar() {
placeholder='Upload new avatar...'
value={avatar}
onChange={(file) => setAvatar(file)}
icon={<IconPhotoUp size='1rem' />}
/>
<Card withBorder shadow='sm'>
@ -103,7 +104,6 @@ export default function SettingsAvatar() {
<Button
variant='subtle'
color='gray'
leftIcon={
avatarSrc ? (
<Avatar src={avatarSrc} radius='sm' size='sm' alt={user?.username ?? 'Proposed avatar'} />
@ -138,7 +138,7 @@ export default function SettingsAvatar() {
Remove Avatar
</Button>
)}
<Button variant='outline' color='gray' disabled={!avatar} onClick={saveAvatar}>
<Button variant='outline' disabled={!avatar} onClick={saveAvatar}>
Save
</Button>
</Group>

View file

@ -1,8 +1,52 @@
import { useThemes } from '@/components/ThemeProvider';
import { mergeTheme } from '@/lib/mergeTheme';
import { useSettingsStore } from '@/lib/store/settings';
import { NumberInput, Paper, Stack, Switch, Text, Title } from '@mantine/core';
import { ZiplineTheme } from '@/lib/theme';
import {
ColorSwatch,
DEFAULT_THEME,
Group,
MantineProvider,
MantineThemeOverride,
NumberInput,
Paper,
Select,
Stack,
Switch,
Text,
Title,
} from '@mantine/core';
import {
IconFile,
IconMoonFilled,
IconPaintFilled,
IconPercentage,
IconSunFilled,
} from '@tabler/icons-react';
function ThemeSelectItem({ value, label, ...others }: { value: string; label: string }) {
const themes = useThemes();
const theme: ZiplineTheme | undefined = themes.find((theme) => theme.id === value);
const mergedTheme = mergeTheme(DEFAULT_THEME, theme as MantineThemeOverride);
return (
<Group {...others}>
<div>{label}</div>
{value !== 'system' && (
<div style={{ display: 'flex', alignItems: 'center' }}>
{mergedTheme.colors[mergedTheme?.primaryColor!]?.map((color) => (
<ColorSwatch key={color} color={color} size={18} style={{ marginRight: '0.5rem' }} />
))}
</div>
)}
</Group>
);
}
export default function SettingsDashboard() {
const [settings, update] = useSettingsStore((state) => [state.settings, state.update]);
const themes = useThemes();
return (
<Paper withBorder p='sm'>
@ -12,18 +56,20 @@ export default function SettingsDashboard() {
</Text>
<Stack spacing='sm' my='xs'>
<Switch
label='Disable Media Preview'
description='Disable previews of files in the dashboard. This is useful to save data as Zipline, by default, will load previews of files.'
checked={settings.disableMediaPreview}
onChange={(event) => update('disableMediaPreview', event.currentTarget.checked)}
/>
<Switch
label='Warn on deletion'
description='Show a warning when deleting files. This is useful to prevent accidental deletion of files.'
checked={settings.warnDeletion}
onChange={(event) => update('warnDeletion', event.currentTarget.checked)}
/>
<Group grow>
<Switch
label='Disable Media Preview'
description='Disable previews of files in the dashboard. This is useful to save data as Zipline, by default, will load previews of files.'
checked={settings.disableMediaPreview}
onChange={(event) => update('disableMediaPreview', event.currentTarget.checked)}
/>
<Switch
label='Warn on deletion'
description='Show a warning when deleting files. This is useful to prevent accidental deletion of files.'
checked={settings.warnDeletion}
onChange={(event) => update('warnDeletion', event.currentTarget.checked)}
/>
</Group>
<NumberInput
label='Search Treshold'
@ -34,7 +80,51 @@ export default function SettingsDashboard() {
onChange={(value) => update('searchTreshold', value === '' ? 0 : value)}
step={0.01}
precision={2}
icon={<IconPercentage size='1rem' />}
/>
<Select
label='Theme'
description='The theme to use for the dashboard. This is only a visual change on your browser and does not change the theme for other users.'
data={[
{ value: 'system', label: 'System' },
...themes.map((theme) => ({ value: theme.id, label: theme.name })),
]}
value={settings.theme}
onChange={(value) => update('theme', value ?? 'builtin:dark_gray')}
itemComponent={ThemeSelectItem}
icon={<IconPaintFilled size='1rem' />}
/>
{settings.theme === 'system' && (
<Group grow>
<Select
label='Dark Theme'
description='The theme to use for the dashboard when your system is in dark mode.'
data={themes
.filter((theme) => theme.colorScheme === 'dark')
.map((theme) => ({ value: theme.id, label: theme.name }))}
value={settings.themeDark}
onChange={(value) => update('themeDark', value ?? 'builtin:dark_gray')}
disabled={settings.theme !== 'system'}
itemComponent={ThemeSelectItem}
icon={<IconMoonFilled size='1rem' />}
/>
<Select
label='Light Theme'
description='The theme to use for the dashboard when your system is in light mode.'
data={themes
.filter((theme) => theme.colorScheme === 'light')
.map((theme) => ({ value: theme.id, label: theme.name }))}
value={settings.themeLight}
onChange={(value) => update('themeLight', value ?? 'builtin:light_gray')}
disabled={settings.theme !== 'system'}
itemComponent={ThemeSelectItem}
icon={<IconSunFilled size='1rem' />}
/>
</Group>
)}
</Stack>
</Paper>
);

View file

@ -162,7 +162,7 @@ export default function SettingsFileView() {
</SimpleGrid>
<Group position='left' mt='sm'>
<Button variant='outline' color='gray' type='submit'>
<Button variant='outline' type='submit'>
Save
</Button>
</Group>

View file

@ -16,7 +16,7 @@ import {
} from '@mantine/core';
import { hasLength, useForm } from '@mantine/form';
import { notifications } from '@mantine/notifications';
import { IconCheck, IconCopy, IconUserCancel } from '@tabler/icons-react';
import { IconAsteriskSimple, IconCheck, IconCopy, IconUser, IconUserCancel } from '@tabler/icons-react';
import { useEffect, useState } from 'react';
import { mutate } from 'swr';
@ -103,19 +103,21 @@ export default function SettingsUser() {
component='span'
label='Token'
onClick={() => setTokenShown(true)}
icon={<IconAsteriskSimple size='1rem' />}
>
<ScrollArea scrollbarSize={5}>{tokenShown ? token : '[click to reveal]'}</ScrollArea>
</TextInput>
<TextInput label='Username' {...form.getInputProps('username')} />
<TextInput label='Username' {...form.getInputProps('username')} icon={<IconUser size='1rem' />} />
<PasswordInput
label='Password'
description='Leave blank to keep the same password'
{...form.getInputProps('password')}
icon={<IconAsteriskSimple size='1rem' />}
/>
<Group position='left' mt='sm'>
<Button variant='outline' color='gray' type='submit'>
<Button variant='outline' type='submit'>
Save
</Button>
</Group>

View file

@ -51,8 +51,7 @@ export default function UploadFile() {
message: (
<>
The upload may fail because the total size of the files you are trying to upload is{' '}
<b>{bytes(size)}</b>, which is larger than the limit of{' '}
<b>{bytes(config.files.maxFileSize)}</b>
<b>{bytes(size)}</b>, which is larger than the limit of <b>{bytes(config.files.maxFileSize)}</b>
</>
),
});
@ -75,7 +74,7 @@ export default function UploadFile() {
<Title order={1}>Upload files</Title>
<Tooltip label='View your files'>
<ActionIcon component={Link} href='/dashboard/files' variant='outline' color='gray' radius='sm'>
<ActionIcon component={Link} href='/dashboard/files' variant='outline' radius='sm'>
<IconFiles size={18} />
</ActionIcon>
</Tooltip>
@ -132,7 +131,6 @@ export default function UploadFile() {
<Button
variant='outline'
color='gray'
leftIcon={<IconUpload size={18} />}
disabled={files.length === 0 || dropLoading}
onClick={upload}

View file

@ -74,7 +74,7 @@ export default function UploadText({
<Title order={1}>Upload text</Title>
<Tooltip label='View your files'>
<ActionIcon component={Link} href='/dashboard/files' variant='outline' color='gray' radius='sm'>
<ActionIcon component={Link} href='/dashboard/files' variant='outline' radius='sm'>
<IconFiles size={18} />
</ActionIcon>
</Tooltip>
@ -129,7 +129,6 @@ export default function UploadText({
<UploadOptionsButton numFiles={1} />
<Button
variant='outline'
color='gray'
leftIcon={<IconUpload size='1rem' />}
disabled={text.length === 0 || loading}
onClick={upload}

View file

@ -265,7 +265,6 @@ export default function UploadOptionsButton({ numFiles }: { numFiles: number })
<Button
variant='outline'
color='gray'
leftIcon={<IconArrowsMinimize size='1rem' />}
onClick={() => setOpen(false)}
>
@ -276,15 +275,8 @@ export default function UploadOptionsButton({ numFiles }: { numFiles: number })
<Button
variant={changes() !== 0 ? 'filled' : 'outline'}
rightIcon={
changes() !== 0 ? (
<Badge variant='outline' color='gray'>
{changes()}
</Badge>
) : null
}
rightIcon={changes() !== 0 ? <Badge variant='outline'>{changes()}</Badge> : null}
onClick={() => setOpen(true)}
color='gray'
>
Options
</Button>

View file

@ -139,13 +139,7 @@ export default function DashboardURLs() {
{...form.getInputProps('maxViews')}
/>
<Button
type='submit'
variant='outline'
color='gray'
radius='sm'
leftIcon={<IconLink size='1rem' />}
>
<Button type='submit' variant='outline' radius='sm' leftIcon={<IconLink size='1rem' />}>
Create
</Button>
</Stack>
@ -156,7 +150,7 @@ export default function DashboardURLs() {
<Title>URLs</Title>
<Tooltip label='Shorten a URL'>
<ActionIcon variant='outline' color='gray' onClick={() => setOpen(true)}>
<ActionIcon variant='outline' onClick={() => setOpen(true)}>
<IconLink size='1rem' />
</ActionIcon>
</Tooltip>

View file

@ -90,7 +90,6 @@ export default function UrlTableView() {
<Tooltip label='Copy URL'>
<ActionIcon
variant='outline'
color='gray'
onClick={(e) => {
e.stopPropagation();
copyUrl(url, config, clipboard);

View file

@ -118,7 +118,6 @@ export default function EditUserModal({
<Tooltip label='Clear avatar'>
<ActionIcon
variant='transparent'
color='gray'
disabled={!form.values.avatar}
onClick={() => form.setFieldValue('avatar', null)}
>

View file

@ -17,7 +17,7 @@ export default function ViewFiles({ user }: { user: User }) {
<Group>
<Title>{user.username}&apos;s files</Title>
<Tooltip label='Back to users'>
<ActionIcon variant='outline' color='gray' component={Link} href='/dashboard/admin/users'>
<ActionIcon variant='outline' component={Link} href='/dashboard/admin/users'>
<IconArrowBackUp size='1rem' />
</ActionIcon>
</Tooltip>

View file

@ -116,7 +116,6 @@ export default function DashboardUsers() {
<Tooltip label='Clear avatar'>
<ActionIcon
variant='transparent'
color='gray'
disabled={!form.values.avatar}
onClick={() => form.setFieldValue('avatar', null)}
>
@ -141,13 +140,7 @@ export default function DashboardUsers() {
{...form.getInputProps('role')}
/>
<Button
type='submit'
variant='outline'
color='gray'
radius='sm'
leftIcon={<IconUserPlus size='1rem' />}
>
<Button type='submit' variant='outline' radius='sm' leftIcon={<IconUserPlus size='1rem' />}>
Create
</Button>
</Stack>
@ -158,7 +151,7 @@ export default function DashboardUsers() {
<Title>Users</Title>
<Tooltip label='Create a new user'>
<ActionIcon variant='outline' color='gray' onClick={() => setOpen(true)}>
<ActionIcon variant='outline' onClick={() => setOpen(true)}>
<IconUserPlus size='1rem' />
</ActionIcon>
</Tooltip>

View file

@ -44,11 +44,7 @@ export default function UserTableView() {
return (
<>
<EditUserModal
opened={!!selectedUser}
onClose={() => setSelectedUser(null)}
user={selectedUser}
/>
<EditUserModal opened={!!selectedUser} onClose={() => setSelectedUser(null)} user={selectedUser} />
<Box my='sm'>
<DataTable
@ -87,7 +83,6 @@ export default function UserTableView() {
<Tooltip label="View user's files">
<ActionIcon
variant='outline'
color='gray'
component={Link}
href={`/dashboard/admin/users/${user.id}/files`}
disabled={!canInteract(currentUser?.role, user?.role)}
@ -99,7 +94,6 @@ export default function UserTableView() {
<Tooltip label='Edit user'>
<ActionIcon
variant='outline'
color='gray'
onClick={(e) => {
e.stopPropagation();
setSelectedUser(user);

View file

@ -16,17 +16,9 @@ export function RenderAlert({
change: (s: boolean) => void;
}) {
return (
<Alert color='gray' icon={<IconEyeFilled size='1rem' />} variant='outline' mb='sm'>
<Alert icon={<IconEyeFilled size='1rem' />} variant='outline' mb='sm'>
{!state ? `This file is rendered through ${renderer}` : `This file can be rendered through ${renderer}`}
<Button
mx='sm'
variant='outline'
compact
onClick={() => change(!state)}
color='gray'
pos='absolute'
right={0}
>
<Button mx='sm' variant='outline' compact onClick={() => change(!state)} pos='absolute' right={0}>
{state ? 'Show' : 'Hide'} rendered version
</Button>
</Alert>

View file

@ -7,7 +7,7 @@ export type SafeConfig = Omit<Config, 'oauth' | 'datasource' | 'core'> & {
oauth: {
bypassLocalLogin: boolean;
loginOnly: boolean;
}
};
};
export function safeConfig(): SafeConfig {

11
src/lib/fs.ts Normal file
View file

@ -0,0 +1,11 @@
import { PathLike } from 'fs';
import { access } from 'fs/promises';
export async function exists(path: PathLike): Promise<boolean> {
try {
await access(path);
return true;
} catch {
return false;
}
}

72
src/lib/mergeTheme.ts Normal file
View file

@ -0,0 +1,72 @@
// @ts-nocheck
// from mantine https://github.com/mantinedev/mantine/blob/master/src/mantine-styles/src/theme/utils/merge-theme/merge-theme.ts
// was not included in the dist so we are using it here instead
import { MantineThemeBase, MantineThemeOverride, getBreakpointValue } from '@mantine/core';
export function mergeTheme(
currentTheme: MantineThemeBase,
themeOverride?: MantineThemeOverride
): MantineThemeBase {
if (!themeOverride) {
return currentTheme;
}
const result: MantineThemeBase = Object.keys(currentTheme).reduce((acc, key) => {
if (key === 'headings' && themeOverride.headings) {
const sizes = themeOverride.headings.sizes
? Object.keys(currentTheme.headings.sizes).reduce((headingsAcc, h) => {
// eslint-disable-next-line no-param-reassign
headingsAcc[h] = {
...currentTheme.headings.sizes[h],
...themeOverride.headings.sizes[h],
};
return headingsAcc;
}, {} as MantineThemeBase['headings']['sizes'])
: currentTheme.headings.sizes;
return {
...acc,
headings: {
...currentTheme.headings,
...themeOverride.headings,
sizes,
},
};
}
if (key === 'breakpoints' && themeOverride.breakpoints) {
const mergedBreakpoints = { ...currentTheme.breakpoints, ...themeOverride.breakpoints };
return {
...acc,
breakpoints: Object.fromEntries(
Object.entries(mergedBreakpoints).sort(
(a, b) => getBreakpointValue(a[1]) - getBreakpointValue(b[1])
)
),
};
}
acc[key] =
typeof themeOverride[key] === 'object'
? { ...currentTheme[key], ...themeOverride[key] }
: typeof themeOverride[key] === 'number' ||
typeof themeOverride[key] === 'boolean' ||
typeof themeOverride[key] === 'function'
? themeOverride[key]
: themeOverride[key] || currentTheme[key];
return acc;
}, {} as MantineThemeBase);
if (themeOverride?.fontFamily && !themeOverride?.headings?.fontFamily) {
result.headings.fontFamily = themeOverride.fontFamily as string;
}
if (!(result.primaryColor in result.colors)) {
throw new Error(
'MantineProvider: Invalid theme.primaryColor, it accepts only key of theme.colors, learn more https://mantine.dev/theming/colors/#primary-color'
);
}
return result;
}

View file

@ -1,4 +1,6 @@
import { SafeConfig, safeConfig } from '@/lib/config/safe';
import { ZiplineTheme } from '@/lib/theme';
import { readThemes } from '@/lib/theme/file';
import { GetServerSideProps, GetServerSidePropsContext } from 'next';
export function withSafeConfig<T = {}>(
@ -6,11 +8,13 @@ export function withSafeConfig<T = {}>(
): GetServerSideProps<
T & {
config: SafeConfig;
themes: ZiplineTheme[];
notFound?: boolean;
}
> {
return async (ctx) => {
const config = safeConfig();
const data = await fn(ctx);
if ((data as any) && (data as any).notFound)
@ -26,6 +30,7 @@ export function withSafeConfig<T = {}>(
return {
props: {
config,
themes: await readThemes(),
...data,
},
};

View file

@ -6,6 +6,9 @@ export type SettingsStore = {
disableMediaPreview: boolean;
warnDeletion: boolean;
searchTreshold: number;
theme: string;
themeDark: string;
themeLight: string;
};
update: <K extends keyof SettingsStore['settings']>(key: K, value: SettingsStore['settings'][K]) => void;
@ -18,6 +21,9 @@ export const useSettingsStore = create<SettingsStore>()(
disableMediaPreview: false,
warnDeletion: true,
searchTreshold: 0.1,
theme: 'builtin:dark_gray',
themeDark: 'builtin:dark_gray',
themeLight: 'builtin:light_gray',
},
update: (key, value) =>

View file

@ -0,0 +1,6 @@
{
"name": "Dark Gray",
"id": "builtin:dark_gray",
"colorScheme": "dark",
"primaryColor": "gray"
}

View file

@ -0,0 +1,6 @@
{
"name": "Light Gray",
"id": "builtin:light_gray",
"colorScheme": "light",
"primaryColor": "gray"
}

64
src/lib/theme/file.ts Normal file
View file

@ -0,0 +1,64 @@
import { readFile, readdir } from 'fs/promises';
import { basename, join } from 'path';
import { ZiplineTheme } from '.';
import { exists } from '../fs';
import dark_gray from './builtins/dark_gray.theme.json';
import light_gray from './builtins/light_gray.theme.json';
const THEMES_DIR = './themes';
export async function readThemes(includeBuiltins: boolean = true): Promise<ZiplineTheme[]> {
const themes = await readThemesDir();
const parsedThemes = await parseThemes(themes);
for (let i = 0; i !== parsedThemes.length; ++i) {
parsedThemes[i] = await handleOverrideColors(parsedThemes[i]);
}
if (includeBuiltins) {
parsedThemes.push(
await handleOverrideColors(dark_gray as ZiplineTheme),
await handleOverrideColors(light_gray as ZiplineTheme)
);
}
return parsedThemes;
}
export async function readThemesDir(): Promise<string[]> {
const absDir = join(process.cwd(), THEMES_DIR);
if (!(await exists(absDir))) return [];
const files = await readdir(absDir);
const themes = files.filter((file) => file.endsWith('.theme.json')).map((file) => join(absDir, file));
return themes;
}
export async function parseThemes(themes: string[]): Promise<ZiplineTheme[]> {
const parsedThemes = [];
for (const theme of themes) {
const themeData: any = await readFile(theme, 'utf-8');
const themeS = JSON.parse(themeData);
themeS.id = basename(theme, '.theme.json');
parsedThemes.push(themeS);
}
return parsedThemes;
}
export async function handleOverrideColors(theme: ZiplineTheme) {
return {
...theme,
colors: {
...theme.colors,
google: theme.colors?.google || ['#4285F4'],
github: theme.colors?.github || ['#24292E'],
authentik: theme.colors?.authentik || ['#FD4B2D'],
discord: theme.colors?.discord || ['#5865F2'],
},
} as ZiplineTheme;
}

46
src/lib/theme/index.ts Normal file
View file

@ -0,0 +1,46 @@
import { MantineTheme, MantineThemeOverride, rem } from '@mantine/core';
export type ZiplineTheme = MantineTheme & { id: string; name: string };
export function themeComponents(theme: ZiplineTheme): MantineThemeOverride {
return {
...theme,
components: {
Paper: {
styles: (theme) => ({
root: {
'&[data-with-border]': {
border: `${rem(1)} solid ${
theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[3]
}`,
},
},
}),
},
AppShell: {
styles: (theme) => ({
main: {
background: `${
theme.colorScheme === 'dark' ? theme.colors.dark[7] : theme.colors.gray[0]
} !important`,
},
}),
},
LoadingOverlay: {
defaultProps: {
// overlayColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : 'white',
overlayOpacity: 0.3,
},
},
Modal: {
defaultProps: {
closeButtonProps: { size: 'lg' },
centered: true,
overlayProps: {
blur: 6,
},
},
},
},
};
}

View file

@ -5,6 +5,8 @@ import { SWRConfig } from 'swr';
import { ModalsProvider } from '@mantine/modals';
import { Notifications } from '@mantine/notifications';
import '@/components/render/code/HighlightCode.css';
import { ZiplineTheme, themeComponents } from '@/lib/theme';
import Theming from '@/components/ThemeProvider';
const fetcher = async (url: RequestInfo | URL) => {
const res = await fetch(url);
@ -18,8 +20,8 @@ const fetcher = async (url: RequestInfo | URL) => {
return res.json();
};
export default function App(props: AppProps) {
const { Component, pageProps } = props;
export default function App({ Component, pageProps }: AppProps) {
const themes: ZiplineTheme[] = pageProps.themes;
return (
<>
@ -33,20 +35,7 @@ export default function App(props: AppProps) {
fetcher,
}}
>
<MantineProvider
withGlobalStyles
withNormalizeCSS
withCSSVariables
theme={{
colorScheme: 'dark',
colors: {
discord: ['#5865F2'],
google: ['#4285F4'],
authentik: ['#FD4B2D'],
github: ['#24292E'],
},
}}
>
<Theming themes={themes}>
<ModalsProvider
modalProps={{
overlayProps: {
@ -59,7 +48,7 @@ export default function App(props: AppProps) {
<Notifications />
<Component {...pageProps} />
</ModalsProvider>
</MantineProvider>
</Theming>
</SWRConfig>
</>
);

View file

@ -150,7 +150,6 @@ export default function Login({ config }: InferGetServerSidePropsType<typeof get
type='submit'
loading={isLoading}
variant={config.website.loginBackground ? 'outline' : 'filled'}
color='gray'
>
Login
</Button>
@ -167,7 +166,7 @@ export default function Login({ config }: InferGetServerSidePropsType<typeof get
)}
{config.features.userRegistration && (
<Button size='lg' fullWidth variant='outline' color='gray'>
<Button size='lg' fullWidth variant='outline'>
Sign up
</Button>
)}

View file

@ -1,21 +1,9 @@
import { Response } from '@/lib/api/response';
import { fetchApi } from '@/lib/fetchApi';
import { useUserStore } from '@/lib/store/user';
import { Group, LoadingOverlay, Text } from '@mantine/core';
import { Icon as TIcon } from '@tabler/icons-react';
import { LoadingOverlay } from '@mantine/core';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import { mutate } from 'swr';
function IconText({ Icon, text }: { Icon: TIcon; text: string }) {
return (
<Group spacing='xs' align='center'>
<Icon />
<Text>{text}</Text>
</Group>
);
}
export default function Login() {
const router = useRouter();
const [setUser, setToken] = useUserStore((state) => [state.setUser, state.setToken]);

View file

@ -8,6 +8,8 @@ import { fileSelect, type File } from '@/lib/db/models/file';
import { User, userSelect } from '@/lib/db/models/user';
import { fetchApi } from '@/lib/fetchApi';
import { parseString } from '@/lib/parser';
import { ZiplineTheme } from '@/lib/theme';
import { readThemes } from '@/lib/theme/file';
import { formatRootUrl } from '@/lib/url';
import {
Button,
@ -174,7 +176,6 @@ export default function ViewFile({
<Button
fullWidth
variant='outline'
color='gray'
my='sm'
onClick={() => verifyPassword()}
disabled={passwordValue.trim().length === 0}
@ -189,7 +190,7 @@ export default function ViewFile({
<Group position='apart' py={5} px='xs'>
<Text color='dimmed'>{file.name}</Text>
<Button compact size='sm' variant='outline' color='gray' onClick={() => setDetailsOpen((o) => !o)}>
<Button compact size='sm' variant='outline' onClick={() => setDetailsOpen((o) => !o)}>
Toggle Details
</Button>
</Group>
@ -247,7 +248,6 @@ export default function ViewFile({
href={`/raw/${file.name}?download=true${pw ? `&pw=${pw}` : ''}`}
target='_blank'
compact
color='gray'
leftIcon={<IconFileDownload size='1rem' />}
>
Download
@ -290,6 +290,7 @@ export const getServerSideProps: GetServerSideProps<{
user?: Omit<User, 'oauthProviders'>;
config?: SafeConfig;
host: string;
themes: ZiplineTheme[];
}> = async (context) => {
const { id, pw } = context.query;
if (!id) return { notFound: true };
@ -343,11 +344,13 @@ export const getServerSideProps: GetServerSideProps<{
const code = await isCode(file.name);
const themes = await readThemes();
if (pw) {
const verified = await verifyPassword(pw as string, file.password!);
delete (file as any).password;
if (verified) return { props: { file, pw: pw as string, code, host } };
if (verified) return { props: { file, pw: pw as string, code, host, themes } };
}
const password = !!file.password;
@ -374,6 +377,7 @@ export const getServerSideProps: GetServerSideProps<{
user,
config,
host,
themes,
},
};
};

View file

@ -16,6 +16,7 @@ import { urlsRoute } from './routes/urls';
import { Scheduler } from '@/lib/scheduler';
import deleteJob from '@/lib/scheduler/jobs/delete';
import maxViewsJob from '@/lib/scheduler/jobs/maxViews';
import { handleOverrideColors, parseThemes, readThemesDir } from '@/lib/theme/file';
const MODE = process.env.NODE_ENV || 'production';
@ -27,6 +28,13 @@ async function main() {
const server = express();
const themes = await readThemesDir();
console.log('themes', themes)
const parsedThemes = await parseThemes(themes);
console.log('parsedThemes', parsedThemes)
const overriden = await handleOverrideColors(parsedThemes[0]);
console.log('overriden', overriden)
logger.info('reading environment for configuration');
const config = validateEnv(readEnv());

View file

@ -0,0 +1,19 @@
{
"name": "Black Dark",
"colorScheme": "dark",
"colors": {
"dark": [
"#ffffff",
"#A7A9AD",
"#7B7E84",
"#61646A",
"#54575D",
"#303236",
"#282a2e",
"#060606",
"#141517",
"#000000"
]
},
"primaryColor": "gray"
}

View file

@ -0,0 +1,31 @@
{
"name": "Dark Blue",
"colorScheme": "dark",
"colors": {
"blue": [
"#FFFFFF",
"#7C7DC2",
"#7778C0",
"#6C6FBC",
"#575DB5",
"#4D54B2",
"#424BAE",
"#3742AA",
"#323EA8",
"#2C39A6"
],
"dark": [
"#FFFFFF",
"#293747",
"#6C7A8D",
"#232F41",
"#41566e",
"#171F35",
"#181c28",
"#0c101c",
"#060824",
"#00001E"
]
},
"primaryColor": "blue"
}

View file

@ -0,0 +1,19 @@
{
"name": "Light Blue",
"colorScheme": "light",
"colors": {
"blue": [
"#FFFFFF",
"#7C7DC2",
"#7778C0",
"#6C6FBC",
"#575DB5",
"#4D54B2",
"#424BAE",
"#3742AA",
"#323EA8",
"#2C39A6"
]
},
"primaryColor": "blue"
}