mirror of
https://github.com/pyrohost/pyrodactyl.git
synced 2026-04-06 04:01:58 +02:00
@@ -1,8 +1,6 @@
|
||||
// FIXME: replace with radix tooltip
|
||||
// import Tooltip from '@/components/elements/tooltip/Tooltip';
|
||||
// FIXME: add icons back
|
||||
import { useSignal } from '@preact/signals-react';
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { Xmark } from '@gravity-ui/icons';
|
||||
import * as Tooltip from '@radix-ui/react-tooltip';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
|
||||
import ActionButton from '@/components/elements/ActionButton';
|
||||
import Code from '@/components/elements/Code';
|
||||
@@ -12,27 +10,39 @@ import asDialog from '@/hoc/asDialog';
|
||||
|
||||
import { ServerContext } from '@/state/server';
|
||||
|
||||
const svgProps = {
|
||||
cx: 16,
|
||||
cy: 16,
|
||||
r: 14,
|
||||
strokeWidth: 3,
|
||||
fill: 'none',
|
||||
stroke: 'currentColor',
|
||||
};
|
||||
// TODO: Make it more pretty
|
||||
const CircleProgress = ({ progress, className }: { progress: number; className?: string }) => {
|
||||
const radius = 12;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
const offset = circumference - (progress / 100) * circumference;
|
||||
|
||||
const Spinner = ({ progress, className }: { progress: number; className?: string }) => (
|
||||
<svg viewBox={'0 0 32 32'} className={className}>
|
||||
<circle {...svgProps} className={'opacity-25'} />
|
||||
<circle
|
||||
{...svgProps}
|
||||
stroke={'white'}
|
||||
strokeDasharray={28 * Math.PI}
|
||||
className={'rotate-[-90deg] origin-[50%_50%] transition-[stroke-dashoffset] duration-300'}
|
||||
style={{ strokeDashoffset: ((100 - progress) / 100) * 28 * Math.PI }}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
return (
|
||||
<svg className={className} viewBox='0 0 32 32'>
|
||||
<circle
|
||||
stroke='currentColor'
|
||||
strokeWidth='4'
|
||||
fill='transparent'
|
||||
r={radius}
|
||||
cx='16'
|
||||
cy='16'
|
||||
className='opacity-25'
|
||||
/>
|
||||
<circle
|
||||
className='transition-all duration-300'
|
||||
stroke='currentColor'
|
||||
strokeWidth='4'
|
||||
strokeLinecap='round'
|
||||
fill='transparent'
|
||||
r={radius}
|
||||
cx='16'
|
||||
cy='16'
|
||||
strokeDasharray={circumference}
|
||||
strokeDashoffset={offset}
|
||||
transform='rotate(-90 16 16)'
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const FileUploadList = () => {
|
||||
const { close } = useContext(DialogWrapperContext);
|
||||
@@ -43,34 +53,62 @@ const FileUploadList = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={'space-y-2 mt-6'}>
|
||||
{uploads.map(([name, file]) => (
|
||||
<div key={name} className={'flex items-center space-x-3 bg-zinc-700 p-3 rounded-sm'}>
|
||||
{/* <Tooltip content={`${Math.floor((file.loaded / file.total) * 100)}%`} placement={'left'}> */}
|
||||
<div className={'shrink-0'}>
|
||||
<Spinner progress={(file.loaded / file.total) * 100} className={'w-6 h-6'} />
|
||||
<Tooltip.Provider>
|
||||
<div className={'space-y-2 mt-6'}>
|
||||
{uploads.map(([name, file]) => (
|
||||
<div key={name} className={'flex items-center space-x-3 bg-zinc-700 p-3 rounded-sm'}>
|
||||
<Tooltip.Root delayDuration={200}>
|
||||
<Tooltip.Trigger asChild>
|
||||
<div className={'shrink-0'}>
|
||||
<CircleProgress progress={(file.loaded / file.total) * 100} className={'w-6 h-6'} />
|
||||
</div>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content
|
||||
side='left'
|
||||
className='px-2 py-1 text-sm bg-gray-800 text-gray-100 rounded shadow-lg z-9999'
|
||||
sideOffset={5}
|
||||
>
|
||||
{`${Math.floor((file.loaded / file.total) * 100)}%`}
|
||||
<Tooltip.Arrow className='fill-gray-800' />
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
<Code className={'flex-1 truncate'}>{name}</Code>
|
||||
<Tooltip.Root delayDuration={200}>
|
||||
<Tooltip.Trigger asChild>
|
||||
<ActionButton
|
||||
variant='secondary'
|
||||
size='sm'
|
||||
onClick={cancelFileUpload.bind(this, name)}
|
||||
className='hover:!text-red-400'
|
||||
>
|
||||
<Xmark />
|
||||
</ActionButton>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content
|
||||
side='right'
|
||||
className='px-2 py-1 text-sm bg-gray-800 text-red-400 rounded shadow-lg z-9999'
|
||||
sideOffset={5}
|
||||
>
|
||||
Cancel
|
||||
<Tooltip.Arrow className='fill-gray-800' />
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
{/* </Tooltip> */}
|
||||
<Code className={'flex-1 truncate'}>{name}</Code>
|
||||
<ActionButton
|
||||
variant='secondary'
|
||||
size='sm'
|
||||
onClick={cancelFileUpload.bind(this, name)}
|
||||
className='hover:!text-red-400'
|
||||
>
|
||||
Cancel
|
||||
))}
|
||||
<Dialog.Footer>
|
||||
<ActionButton variant='danger' onClick={() => clearFileUploads()}>
|
||||
Cancel Uploads
|
||||
</ActionButton>
|
||||
</div>
|
||||
))}
|
||||
<Dialog.Footer>
|
||||
<ActionButton variant='danger' onClick={() => clearFileUploads()}>
|
||||
Cancel Uploads
|
||||
</ActionButton>
|
||||
<ActionButton variant='secondary' onClick={close}>
|
||||
Close
|
||||
</ActionButton>
|
||||
</Dialog.Footer>
|
||||
</div>
|
||||
<ActionButton variant='secondary' onClick={close}>
|
||||
Close
|
||||
</ActionButton>
|
||||
</Dialog.Footer>
|
||||
</div>
|
||||
</Tooltip.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -80,50 +118,65 @@ const FileUploadListDialog = asDialog({
|
||||
})(FileUploadList);
|
||||
|
||||
const FileManagerStatus = () => {
|
||||
const open = useSignal(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const count = ServerContext.useStoreState((state) => Object.keys(state.files.uploads).length);
|
||||
|
||||
useEffect(() => {
|
||||
if (count === 0) {
|
||||
open.value = false;
|
||||
setOpen(false);
|
||||
}
|
||||
}, [count]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{count > 0 && (
|
||||
// <Tooltip content={`${count} files are uploading, click to view`}>
|
||||
<ActionButton
|
||||
variant='secondary'
|
||||
size='sm'
|
||||
className='w-10 h-10 p-0'
|
||||
onClick={() => (open.value = true)}
|
||||
>
|
||||
<svg
|
||||
className='animate-spin h-5 w-5 text-white'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
fill='none'
|
||||
viewBox='0 0 24 24'
|
||||
>
|
||||
<circle
|
||||
className='opacity-25'
|
||||
cx='12'
|
||||
cy='12'
|
||||
r='10'
|
||||
stroke='currentColor'
|
||||
strokeWidth='4'
|
||||
></circle>
|
||||
<path
|
||||
className='opacity-75'
|
||||
fill='currentColor'
|
||||
d='M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z'
|
||||
></path>
|
||||
</svg>
|
||||
</ActionButton>
|
||||
// </Tooltip>
|
||||
)}
|
||||
<FileUploadListDialog open={open.value} onClose={() => (open.value = false)} />
|
||||
<Tooltip.Provider>
|
||||
{count > 0 && (
|
||||
<Tooltip.Root delayDuration={200}>
|
||||
<Tooltip.Trigger asChild>
|
||||
<ActionButton
|
||||
variant='secondary'
|
||||
size='sm'
|
||||
className='w-10 h-10 p-0'
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
className='animate-spin h-5 w-5 text-white'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
fill='none'
|
||||
viewBox='0 0 24 24'
|
||||
>
|
||||
<circle
|
||||
className='opacity-25'
|
||||
cx='12'
|
||||
cy='12'
|
||||
r='10'
|
||||
stroke='currentColor'
|
||||
strokeWidth='4'
|
||||
></circle>
|
||||
<path
|
||||
className='opacity-75'
|
||||
fill='currentColor'
|
||||
d='M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z'
|
||||
></path>
|
||||
</svg>
|
||||
</ActionButton>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content
|
||||
side='top'
|
||||
className='px-2 py-1 text-sm bg-gray-800 text-gray-100 rounded shadow-lg'
|
||||
sideOffset={5}
|
||||
>
|
||||
{`${count} files are uploading, click to view`}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
)}
|
||||
<FileUploadListDialog open={open} onClose={() => setOpen(false)} />
|
||||
</Tooltip.Provider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import { Progress } from '@radix-ui/react-progress';
|
||||
import { X } from 'lucide-react';
|
||||
|
||||
import Code from '@/components/elements/Code';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import { useStoreActions } from '@/state/hooks';
|
||||
import { ServerContext } from '@/state/server';
|
||||
|
||||
// Assuming you use a utility like this for conditional classnames
|
||||
|
||||
interface FileUploadRowProps {
|
||||
name: string;
|
||||
loaded: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export default function FileUploadRow({ name, loaded, total }: FileUploadRowProps) {
|
||||
const cancel = ServerContext.useStoreActions((actions) => actions.files.cancelFileUpload);
|
||||
|
||||
const percent = Math.floor((loaded / total) * 100);
|
||||
|
||||
return (
|
||||
<div className='flex items-center px-4 py-3 bg-zinc-800 border-b border-zinc-700 rounded-md space-x-4'>
|
||||
<div className='flex-1 truncate'>
|
||||
<Code>{name}</Code>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col w-1/3'>
|
||||
<Progress value={percent} className='h-2 rounded bg-zinc-700 overflow-hidden'>
|
||||
<div className='h-full bg-blue-500 transition-all' style={{ width: `${percent}%` }} />
|
||||
</Progress>
|
||||
<div className='text-xs text-zinc-400 mt-1'>{percent}%</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => cancelFileUpload(name)}
|
||||
className={cn('text-red-400 hover:text-red-200 transition-colors', 'p-1')}
|
||||
title='Cancel upload'
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
0
storage/app/private/.gitignore
vendored
Normal file → Executable file
0
storage/app/private/.gitignore
vendored
Normal file → Executable file
0
storage/app/public/.gitignore
vendored
Normal file → Executable file
0
storage/app/public/.gitignore
vendored
Normal file → Executable file
Reference in New Issue
Block a user