fix: random visual bugs + enhancements

This commit is contained in:
diced
2025-07-02 20:41:37 -07:00
parent 6a76c5243f
commit b566d13c8d
9 changed files with 166 additions and 92 deletions

View File

@@ -2,11 +2,11 @@
## Supported Versions ## Supported Versions
| Version | Supported | | Version | Supported |
| ------- | ------------------------------------- | | ------- | ------------------ |
| 4.x.x | :white_check_mark: | | 4.2.x | :white_check_mark: |
| < 3 | :white_check_mark: (EOL at June 2025) | | < 3 | :x: |
| < 2 | :x: | | < 2 | :x: |
## Reporting a Vulnerability ## Reporting a Vulnerability

View File

@@ -18,7 +18,6 @@ import {
Modal, Modal,
Pill, Pill,
PillsInput, PillsInput,
ScrollArea,
SimpleGrid, SimpleGrid,
Text, Text,
Title, Title,
@@ -61,8 +60,8 @@ import {
removeFromFolder, removeFromFolder,
viewFile, viewFile,
} from '../actions'; } from '../actions';
import FileStat from './FileStat';
import EditFileDetailsModal from './EditFileDetailsModal'; import EditFileDetailsModal from './EditFileDetailsModal';
import FileStat from './FileStat';
function ActionButton({ function ActionButton({
Icon, Icon,
@@ -189,9 +188,9 @@ export default function FileModal({
</Text> </Text>
} }
size='auto' size='auto'
maw='90vw'
centered centered
zIndex={200} zIndex={200}
scrollAreaComponent={ScrollArea.Autosize}
> >
{file ? ( {file ? (
<> <>

View File

@@ -11,7 +11,7 @@ import {
Text, Text,
} from '@mantine/core'; } from '@mantine/core';
import { Icon, IconFileUnknown, IconPlayerPlay, IconShieldLockFilled } from '@tabler/icons-react'; import { Icon, IconFileUnknown, IconPlayerPlay, IconShieldLockFilled } from '@tabler/icons-react';
import { useEffect, useState } from 'react'; import { useEffect, useState, useCallback, useMemo } from 'react';
import { renderMode } from '../pages/upload/renderMode'; import { renderMode } from '../pages/upload/renderMode';
import Render from '../render/Render'; import Render from '../render/Render';
import fileIcon from './fileIcon'; import fileIcon from './fileIcon';
@@ -30,7 +30,7 @@ function PlaceholderContent({ text, Icon }: { text: string; Icon: Icon }) {
function Placeholder({ text, Icon, ...props }: { text: string; Icon: Icon; onClick?: () => void }) { function Placeholder({ text, Icon, ...props }: { text: string; Icon: Icon; onClick?: () => void }) {
return ( return (
<Center py='xs' style={{ height: '100%', width: '100%', cursor: 'pointed' }} {...props}> <Center py='xs' style={{ height: '100%', width: '100%', cursor: 'pointer' }} {...props}>
<PlaceholderContent text={text} Icon={Icon} /> <PlaceholderContent text={text} Icon={Icon} />
</Center> </Center>
); );
@@ -83,57 +83,60 @@ export default function DashboardFileType({
const disableMediaPreview = useSettingsStore((state) => state.settings.disableMediaPreview); const disableMediaPreview = useSettingsStore((state) => state.settings.disableMediaPreview);
const dbFile = 'id' in file; const dbFile = 'id' in file;
const renderIn = renderMode(file.name.split('.').pop() || ''); const renderIn = useMemo(() => renderMode(file.name.split('.').pop() || ''), [file.name]);
const [fileContent, setFileContent] = useState(''); const [fileContent, setFileContent] = useState('');
const [type, setType] = useState<string>(file.type.split('/')[0]); const [type, setType] = useState<string>(file.type.split('/')[0]);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const gettext = async () => { const getText = useCallback(async () => {
if (!dbFile) { try {
const reader = new FileReader(); if (!dbFile) {
reader.onload = () => { const reader = new FileReader();
if ((reader.result! as string).length > 1 * 1024 * 1024) { reader.onload = () => {
setFileContent( if ((reader.result! as string).length > 1 * 1024 * 1024) {
reader.result!.slice(0, 1 * 1024 * 1024) + setFileContent(
'\n...\nThe file is too big to display click the download icon to view/download it.', reader.result!.slice(0, 1 * 1024 * 1024) +
); '\n...\nThe file is too big to display click the download icon to view/download it.',
} else { );
setFileContent(reader.result as string); } else {
} setFileContent(reader.result as string);
}; }
reader.readAsText(file); };
reader.readAsText(file);
return;
}
return; if (file.size > 1 * 1024 * 1024) {
} const res = await fetch(`/raw/${file.name}${password ? `?pw=${password}` : ''}`, {
headers: {
if (file.size > 1 * 1024 * 1024) { Range: 'bytes=0-' + 1 * 1024 * 1024, // 0 mb to 1 mb
const res = await fetch(`/raw/${file.name}${password ? `?pw=${password}` : ''}`, { },
headers: { });
Range: 'bytes=0-' + 1 * 1024 * 1024, // 0 mb to 1 mb if (!res.ok) throw new Error('Failed to fetch file');
}, const text = await res.text();
}); setFileContent(
text + '\n...\nThe file is too big to display click the download icon to view/download it.',
);
return;
}
const res = await fetch(`/raw/${file.name}${password ? `?pw=${password}` : ''}`);
if (!res.ok) throw new Error('Failed to fetch file');
const text = await res.text(); const text = await res.text();
setFileContent( setFileContent(text);
text + '\n...\nThe file is too big to display click the download icon to view/download it.', } catch {
); setFileContent('Error loading file.');
return;
} }
}, [dbFile, file, password]);
const res = await fetch(`/raw/${file.name}${password ? `?pw=${password}` : ''}`);
const text = await res.text();
setFileContent(text);
};
useEffect(() => { useEffect(() => {
if (code) { if (code) {
setType('text'); setType('text');
gettext(); getText();
} else if (overrideType === 'text' || type === 'text') { } else if (overrideType === 'text' || type === 'text') {
gettext(); getText();
} else { } else {
return; return;
} }
@@ -177,7 +180,10 @@ export default function DashboardFileType({
/> />
) : (file as DbFile).thumbnail && dbFile ? ( ) : (file as DbFile).thumbnail && dbFile ? (
<Box pos='relative'> <Box pos='relative'>
<MantineImage src={`/raw/${(file as DbFile).thumbnail!.path}`} alt={file.name} /> <MantineImage
src={`/raw/${(file as DbFile).thumbnail!.path}`}
alt={file.name || 'Video thumbnail'}
/>
<Center <Center
pos='absolute' pos='absolute'
@@ -203,7 +209,7 @@ export default function DashboardFileType({
<Center> <Center>
<MantineImage <MantineImage
src={dbFile ? `/raw/${file.name}${password ? `?pw=${password}` : ''}` : URL.createObjectURL(file)} src={dbFile ? `/raw/${file.name}${password ? `?pw=${password}` : ''}` : URL.createObjectURL(file)}
alt={file.name} alt={file.name || 'Image'}
style={{ style={{
cursor: allowZoom ? 'zoom-in' : 'default', cursor: allowZoom ? 'zoom-in' : 'default',
maxWidth: '70vw', maxWidth: '70vw',
@@ -217,7 +223,7 @@ export default function DashboardFileType({
src={ src={
dbFile ? `/raw/${file.name}${password ? `?pw=${password}` : ''}` : URL.createObjectURL(file) dbFile ? `/raw/${file.name}${password ? `?pw=${password}` : ''}` : URL.createObjectURL(file)
} }
alt={file.name} alt={file.name || 'Image'}
style={{ style={{
maxWidth: '95vw', maxWidth: '95vw',
maxHeight: '95vh', maxHeight: '95vh',
@@ -234,7 +240,7 @@ export default function DashboardFileType({
fit='contain' fit='contain'
mah={400} mah={400}
src={dbFile ? `/raw/${file.name}${password ? `?pw=${password}` : ''}` : URL.createObjectURL(file)} src={dbFile ? `/raw/${file.name}${password ? `?pw=${password}` : ''}` : URL.createObjectURL(file)}
alt={file.name} alt={file.name || 'Image'}
/> />
); );
case 'audio': case 'audio':

View File

@@ -7,6 +7,7 @@ import FilesUrlsCountGraph from './parts/FilesUrlsCountGraph';
import { useApiStats } from './useStats'; import { useApiStats } from './useStats';
import { StatsCardsSkeleton } from './parts/StatsCards'; import { StatsCardsSkeleton } from './parts/StatsCards';
import { StatsTablesSkeleton } from './parts/StatsTables'; import { StatsTablesSkeleton } from './parts/StatsTables';
import dayjs from 'dayjs';
const StatsCards = dynamic(() => import('./parts/StatsCards')); const StatsCards = dynamic(() => import('./parts/StatsCards'));
const StatsTables = dynamic(() => import('./parts/StatsTables')); const StatsTables = dynamic(() => import('./parts/StatsTables'));
@@ -14,9 +15,11 @@ const StorageGraph = dynamic(() => import('./parts/StorageGraph'));
const ViewsGraph = dynamic(() => import('./parts/ViewsGraph')); const ViewsGraph = dynamic(() => import('./parts/ViewsGraph'));
export default function DashboardMetrics() { export default function DashboardMetrics() {
const today = dayjs();
const [dateRange, setDateRange] = useState<[string | null, string | null]>([ const [dateRange, setDateRange] = useState<[string | null, string | null]>([
new Date(Date.now() - 86400000 * 7).toISOString(), today.subtract(7, 'day').toISOString(),
new Date().toISOString(), today.toISOString(),
]); ]);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@@ -40,17 +43,49 @@ export default function DashboardMetrics() {
return ( return (
<> <>
<Modal title='Change range' opened={open} onClose={() => setOpen(false)} size='auto'> <Modal title='Change range' opened={open} onClose={() => setOpen(false)} size='auto'>
<Paper withBorder> <Paper withBorder style={{ minHeight: 300 }}>
<DatePicker <DatePicker
type='range' type='range'
value={dateRange} value={dateRange}
onChange={handleDateChange} onChange={handleDateChange}
allowSingleDateInRange={false} allowSingleDateInRange={false}
maxDate={new Date()} maxDate={new Date()}
presets={[
{
value: [today.subtract(2, 'day').format('YYYY-MM-DD'), today.format('YYYY-MM-DD')],
label: 'Last two days',
},
{
value: [today.subtract(7, 'day').format('YYYY-MM-DD'), today.format('YYYY-MM-DD')],
label: 'Last 7 days',
},
{
value: [today.startOf('month').format('YYYY-MM-DD'), today.format('YYYY-MM-DD')],
label: 'This month',
},
{
value: [
today.subtract(1, 'month').startOf('month').format('YYYY-MM-DD'),
today.subtract(1, 'month').endOf('month').format('YYYY-MM-DD'),
],
label: 'Last month',
},
{
value: [today.startOf('year').format('YYYY-MM-DD'), today.format('YYYY-MM-DD')],
label: 'This year',
},
{
value: [
today.subtract(1, 'year').startOf('year').format('YYYY-MM-DD'),
today.subtract(1, 'year').endOf('year').format('YYYY-MM-DD'),
],
label: 'Last year',
},
]}
/> />
</Paper> </Paper>
<Group mt='md'> <Group mt='lg'>
<Button fullWidth onClick={() => setOpen(false)}> <Button fullWidth onClick={() => setOpen(false)}>
Close Close
</Button> </Button>

View File

@@ -63,12 +63,35 @@ export default function Domains({
</Button> </Button>
</Group> </Group>
<SimpleGrid mt='md' cols={{ base: 1, sm: 2, md: 3 }} spacing='xs'> <SimpleGrid mt='md' cols={{ base: 1, sm: 2, md: 3 }} spacing='md' verticalSpacing='md'>
{domains.map((domain, index) => ( {domains.map((domain, index) => (
<Paper key={index} withBorder p='xs'> <Paper
<Group justify='space-between'> key={index}
<div> withBorder
<strong>{domain}</strong> p='md'
radius='md'
shadow='xs'
style={{
background: 'rgba(0,0,0,0.03)',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
minHeight: 64,
}}
>
<Group justify='space-between' align='center' wrap='nowrap'>
<div
style={{
minWidth: 0,
flex: 1,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
fontWeight: 500,
fontSize: 16,
}}
>
{domain}
</div> </div>
<Button <Button
variant='subtle' variant='subtle'

View File

@@ -20,7 +20,7 @@ import { useClipboard, useColorScheme } from '@mantine/hooks';
import { notifications, showNotification } from '@mantine/notifications'; import { notifications, showNotification } from '@mantine/notifications';
import { IconDeviceSdCard, IconFiles, IconUpload, IconX } from '@tabler/icons-react'; import { IconDeviceSdCard, IconFiles, IconUpload, IconX } from '@tabler/icons-react';
import Link from 'next/link'; import Link from 'next/link';
import { useEffect, useState } from 'react'; import { useEffect, useState, useCallback } from 'react';
import UploadOptionsButton from '../UploadOptionsButton'; import UploadOptionsButton from '../UploadOptionsButton';
import { uploadFiles } from '../uploadFiles'; import { uploadFiles } from '../uploadFiles';
import ToUploadFile from './ToUploadFile'; import ToUploadFile from './ToUploadFile';
@@ -49,34 +49,23 @@ export default function UploadFile({ title, folder }: { title?: string; folder?:
}); });
const [dropLoading, setLoading] = useState(false); const [dropLoading, setLoading] = useState(false);
const handlePaste = (e: ClipboardEvent) => { const aggSize = useCallback(() => files.reduce((acc, file) => acc + file.size, 0), [files]);
if (!e.clipboardData) return;
const handlePaste = useCallback((e: ClipboardEvent) => {
if (!e.clipboardData) return;
for (let i = 0; i !== e.clipboardData.items.length; ++i) { for (let i = 0; i !== e.clipboardData.items.length; ++i) {
if (!e.clipboardData.items[i].type.startsWith('image')) return; if (!e.clipboardData.items[i].type.startsWith('image')) return;
const blob = e.clipboardData.items[i].getAsFile(); const blob = e.clipboardData.items[i].getAsFile();
if (!blob) return; if (!blob) return;
setFiles((prev) => [...prev, blob]);
setFiles([...files, blob]); showNotification({ message: `Image ${blob.name} pasted from clipboard`, color: 'blue' });
showNotification({
message: `Image ${blob.name} pasted from clipboard`,
color: 'blue',
});
} }
}; }, []);
const aggSize = () => files.reduce((acc, file) => acc + file.size, 0);
const upload = () => { const upload = () => {
const toPartialFiles: File[] = []; const toPartialFiles: File[] = files.filter(
for (let i = 0; i !== files.length; ++i) { (file) => config.chunks.enabled && file.size >= bytes(config.chunks.max),
const file = files[i]; );
if (config.chunks.enabled && file.size >= bytes(config.chunks.max)) {
toPartialFiles.push(file);
}
}
if (toPartialFiles.length > 0) { if (toPartialFiles.length > 0) {
uploadPartialFiles(toPartialFiles, { uploadPartialFiles(toPartialFiles, {
setFiles, setFiles,
@@ -91,7 +80,7 @@ export default function UploadFile({ title, folder }: { title?: string; folder?:
}); });
} else { } else {
const size = aggSize(); const size = aggSize();
if (size > bytes(config.files.maxFileSize) && !toPartialFiles.length) { if (size > bytes(config.files.maxFileSize)) {
notifications.show({ notifications.show({
title: 'Upload may fail', title: 'Upload may fail',
color: 'yellow', color: 'yellow',
@@ -105,7 +94,6 @@ export default function UploadFile({ title, folder }: { title?: string; folder?:
), ),
}); });
} }
uploadFiles(files, { uploadFiles(files, {
setFiles, setFiles,
setLoading, setLoading,
@@ -121,11 +109,22 @@ export default function UploadFile({ title, folder }: { title?: string; folder?:
useEffect(() => { useEffect(() => {
document.addEventListener('paste', handlePaste); document.addEventListener('paste', handlePaste);
return () => { return () => {
document.removeEventListener('paste', handlePaste); document.removeEventListener('paste', handlePaste);
}; };
}, []); }, [handlePaste]);
useEffect(() => {
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
if (files.length > 0) {
e.preventDefault();
}
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => {
window.removeEventListener('beforeunload', handleBeforeUnload);
};
}, [files.length]);
return ( return (
<> <>
@@ -142,7 +141,7 @@ export default function UploadFile({ title, folder }: { title?: string; folder?:
</Group> </Group>
<Dropzone <Dropzone
onDrop={(f) => setFiles([...f, ...files])} onDrop={(f) => setFiles((prev) => [...f, ...prev])}
my='sm' my='sm'
loading={dropLoading} loading={dropLoading}
disabled={dropLoading} disabled={dropLoading}
@@ -220,7 +219,6 @@ export default function UploadFile({ title, folder }: { title?: string; folder?:
<Group justify='right' gap='sm' my='md'> <Group justify='right' gap='sm' my='md'>
<UploadOptionsButton folder={folder} numFiles={files.length} /> <UploadOptionsButton folder={folder} numFiles={files.length} />
<Button <Button
variant='outline' variant='outline'
leftSection={<IconUpload size={18} />} leftSection={<IconUpload size={18} />}

View File

@@ -16,7 +16,7 @@ import {
import { useClipboard } from '@mantine/hooks'; import { useClipboard } from '@mantine/hooks';
import { IconCursorText, IconEyeFilled, IconFiles, IconUpload } from '@tabler/icons-react'; import { IconCursorText, IconEyeFilled, IconFiles, IconUpload } from '@tabler/icons-react';
import Link from 'next/link'; import Link from 'next/link';
import { useState } from 'react'; import { useEffect, useState } from 'react';
import UploadOptionsButton from '../UploadOptionsButton'; import UploadOptionsButton from '../UploadOptionsButton';
import { renderMode } from '../renderMode'; import { renderMode } from '../renderMode';
import { uploadFiles } from '../uploadFiles'; import { uploadFiles } from '../uploadFiles';
@@ -30,15 +30,26 @@ export default function UploadText({
codeMeta: Parameters<typeof DashboardUploadText>[0]['codeMeta']; codeMeta: Parameters<typeof DashboardUploadText>[0]['codeMeta'];
}) { }) {
const clipboard = useClipboard(); const clipboard = useClipboard();
const [options, ephemeral, clearEphemeral] = useUploadOptionsStore( const [options, ephemeral, clearEphemeral] = useUploadOptionsStore(
useShallow((state) => [state.options, state.ephemeral, state.clearEphemeral]), useShallow((state) => [state.options, state.ephemeral, state.clearEphemeral]),
); );
const [selectedLanguage, setSelectedLanguage] = useState('txt'); const [selectedLanguage, setSelectedLanguage] = useState('txt');
const [text, setText] = useState(''); const [text, setText] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
useEffect(() => {
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
if (text.length > 0) {
e.preventDefault();
}
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => {
window.removeEventListener('beforeunload', handleBeforeUnload);
};
}, [text]);
const renderIn = renderMode(selectedLanguage); const renderIn = renderMode(selectedLanguage);
const handleTab = (e: React.KeyboardEvent<HTMLTextAreaElement>) => { const handleTab = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
@@ -52,12 +63,10 @@ export default function UploadText({
const upload = () => { const upload = () => {
const blob = new Blob([text]); const blob = new Blob([text]);
const file = new File([blob], `text.${selectedLanguage}`, { const file = new File([blob], `text.${selectedLanguage}`, {
type: codeMeta.find((meta) => meta.ext === selectedLanguage)?.mime, type: codeMeta.find((meta) => meta.ext === selectedLanguage)?.mime,
lastModified: Date.now(), lastModified: Date.now(),
}); });
uploadFiles([file], { uploadFiles([file], {
clipboard, clipboard,
setFiles: () => {}, setFiles: () => {},

View File

@@ -37,7 +37,7 @@ export default function HighlightCode({ language, code }: { language: string; co
</CopyButton> </CopyButton>
<ScrollArea type='auto' dir='ltr' offsetScrollbars={false}> <ScrollArea type='auto' dir='ltr' offsetScrollbars={false}>
<pre style={{ margin: 0 }} className='theme'> <pre style={{ margin: 0, whiteSpace: 'pre', overflowX: 'auto' }} className='theme'>
<code className='theme'> <code className='theme'>
{displayLines.map((line, i) => ( {displayLines.map((line, i) => (
<div key={i}> <div key={i}>

View File

@@ -143,6 +143,10 @@ export async function read() {
const database = (await readDatabaseSettings()) as Record<string, any>; const database = (await readDatabaseSettings()) as Record<string, any>;
const { dbEnv, env } = readEnv(); const { dbEnv, env } = readEnv();
if (global.__tamperedConfig__) {
global.__tamperedConfig__ = [];
}
// this overwrites database settings with provided env vars if they exist // this overwrites database settings with provided env vars if they exist
for (const [propPath, val] of Object.entries(dbEnv)) { for (const [propPath, val] of Object.entries(dbEnv)) {
const col = Object.entries(DATABASE_TO_PROP).find(([_colName, path]) => path === propPath)?.[0]; const col = Object.entries(DATABASE_TO_PROP).find(([_colName, path]) => path === propPath)?.[0];