-
Notifications
You must be signed in to change notification settings - Fork 62
feat: upload queue #203
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: upload queue #203
Changes from all commits
9adbb38
de817f0
ac85ca0
6b375be
6a579c9
bead7ea
e2762f1
9177a73
bda62f8
a7f04ef
dcc50f4
4b07aba
5f58ace
5ad3bbb
c0a4fc1
2e29747
d89a982
39fcf41
f539d8f
0b26ee5
f8c7e20
d4ce6f8
7a047bf
d8cb419
a174f2e
86687d5
b7ce2cf
2ef62f3
314ae63
b750140
c4e42f8
a66fa17
0c3fcf4
5ee607a
c45fefd
765426e
4bd9300
43d7e8c
696a1fb
26ea286
c88ecc1
6aa3de9
edd2e89
2d6d416
cd40799
9740d82
44de4e8
a03a3c4
590b5c0
89f4ff4
a6c59c8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| import { Component, createMemo } from 'solid-js' | ||
|
|
||
| import { COMMA_CONNECT_PRIORITY } from '~/api/athena' | ||
| import Icon from '~/components/material/Icon' | ||
| import LinearProgress from '~/components/material/LinearProgress' | ||
| import { UploadItem } from '~/types' | ||
|
|
||
| const QueueItem: Component<{ item: UploadItem }> = (props) => { | ||
| const progress = createMemo(() => { | ||
| if (props.item.status === 'waiting_for_network') return 'Waiting for network' | ||
| if (props.item.status === 'queued') return 'Queued' | ||
| if (props.item.progress === 100) return 'Finishing' | ||
| return `${props.item.progress}%` | ||
| }) | ||
|
|
||
| const progressColor = createMemo(() => { | ||
| switch (props.item.status) { | ||
| case 'completed': | ||
| return 'tertiary' | ||
| case 'queued': | ||
| case 'waiting_for_network': | ||
| return 'secondary' | ||
| default: | ||
| return 'primary' | ||
| } | ||
| }) | ||
|
|
||
| const icon = createMemo(() => (props.item.priority === COMMA_CONNECT_PRIORITY ? 'face' : 'local_fire_department')) | ||
|
|
||
| return ( | ||
| <div class="flex flex-col"> | ||
| <div class="flex items-center justify-between flex-wrap mb-1 gap-x-4 min-w-0"> | ||
| <div class="flex items-center min-w-0 flex-1"> | ||
| <Icon class="text-on-surface-variant flex-shrink-0 mr-2" name={icon()} /> | ||
| <div class="flex min-w-0 gap-1"> | ||
| <span class="text-body-sm font-mono truncate text-on-surface"> | ||
| {[props.item.route, props.item.segment, props.item.filename].join(' ')} | ||
| </span> | ||
| </div> | ||
| </div> | ||
| <div class="flex items-center gap-2 flex-shrink-0 justify-end"> | ||
| <span class="text-body-sm font-mono whitespace-nowrap">{progress()}</span> | ||
| </div> | ||
| </div> | ||
| <div class="h-1.5 w-full overflow-hidden rounded-full bg-surface-container-highest"> | ||
| <LinearProgress progress={props.item.progress / 100} color={progressColor()} /> | ||
| </div> | ||
| </div> | ||
| ) | ||
| } | ||
|
|
||
| export default QueueItem | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| import { Component, Suspense } from 'solid-js' | ||
| import { For, Match, Switch } from 'solid-js' | ||
| import { Transition, TransitionGroup } from 'solid-transition-group' | ||
|
|
||
| import Icon from '~/components/material/Icon' | ||
| import { UploadItem } from '~/types' | ||
|
|
||
| import QueueItem from './QueueItem' | ||
| import clsx from 'clsx' | ||
|
|
||
| const animations = (slide: boolean) => { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Animations look like something which could just be added in a separate PR later, where there is time to test them properly |
||
| return { | ||
| enterActiveClass: 'transition-all duration-300 ease-in-out', | ||
| exitActiveClass: 'transition-all duration-300 ease-in-out', | ||
| enterClass: clsx('opacity-0', slide && 'transform translate-x-4'), | ||
| enterToClass: clsx('opacity-100', slide && 'transform translate-x-0'), | ||
| exitClass: clsx('opacity-100', slide && 'transform translate-x-0'), | ||
| exitToClass: clsx('opacity-0', slide && 'transform -translate-x-4'), | ||
| moveClass: clsx(slide && 'transition-transform duration-300'), | ||
| } | ||
| } | ||
|
|
||
| const QueueItemTable: Component<{ items?: () => UploadItem[] | undefined; error?: () => string | undefined; offline?: boolean }> = ( | ||
| props, | ||
| ) => { | ||
| return ( | ||
| <div class="relative h-[calc(4*3rem)] sm:h-[calc(6*3rem)]"> | ||
| <Transition appear {...animations(false)}> | ||
| <Suspense | ||
| fallback={ | ||
| <div class="flex justify-center items-center h-full animate-spin absolute inset-0"> | ||
| <Icon name="progress_activity" /> | ||
| </div> | ||
| } | ||
| > | ||
| <Switch> | ||
| <Match when={props.error?.() !== undefined && props.items?.()?.length === 0}> | ||
| <div class="flex items-center justify-center h-full gap-2 text-on-surface-variant absolute inset-0"> | ||
| <Icon name="signal_disconnected" /> | ||
| <span>{props.error?.()}</span> | ||
| </div> | ||
| </Match> | ||
| <Match when={props.items?.()?.length === 0}> | ||
| <div class="flex items-center justify-center h-full gap-2 text-on-surface-variant absolute inset-0"> | ||
| <Icon name="cloud_done" /> | ||
| <span>No files in queue</span> | ||
| </div> | ||
| </Match> | ||
| <Match when={true}> | ||
| <div class="absolute inset-0 overflow-y-auto hide-scrollbar"> | ||
| <TransitionGroup name="list" {...animations(true)}> | ||
| <For each={props.items?.()}> | ||
| {(item) => ( | ||
| <div class="bg-surface-container-lowest rounded-md pb-1 sm:pb-2"> | ||
| <QueueItem item={item} /> | ||
| </div> | ||
| )} | ||
| </For> | ||
| </TransitionGroup> | ||
| </div> | ||
| </Match> | ||
| </Switch> | ||
| </Suspense> | ||
| </Transition> | ||
| </div> | ||
| ) | ||
| } | ||
|
|
||
| export default QueueItemTable | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| import clsx from 'clsx' | ||
| import { createResource, Suspense } from 'solid-js' | ||
| import type { Component } from 'solid-js' | ||
|
|
||
| import IconButton from '~/components/material/IconButton' | ||
| import { useUploadQueue } from '~/hooks/use-upload-queue' | ||
|
|
||
| import QueueItemTable from './QueueItemTable' | ||
| import StatisticBar from '../StatisticBar' | ||
|
|
||
| const UploadQueue: Component<{ dongleId: string }> = (props) => { | ||
| const [queue] = createResource(() => props.dongleId, useUploadQueue) | ||
| const items = () => queue()?.items() | ||
| return ( | ||
| <div class="flex flex-col border-2 border-t-0 border-surface-container-high bg-surface-container-lowest"> | ||
| <div class="flex"> | ||
| <div class="flex-auto p-4"> | ||
| <StatisticBar | ||
| statistics={[ | ||
| { | ||
| label: 'Uploading', | ||
| value: () => | ||
| queue() | ||
| ?.items() | ||
| .filter((i) => i.status === 'uploading').length, | ||
| }, | ||
| { | ||
| label: 'Waiting', | ||
| value: () => | ||
| queue() | ||
| ?.items() | ||
| .filter((i) => i.status === 'queued').length, | ||
| }, | ||
| { label: 'Total', value: () => queue()?.items().length }, | ||
|
Comment on lines
+20
to
+34
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this formatting can be more compact but Biome was doing this. |
||
| ]} | ||
| /> | ||
| </div> | ||
| <div class="flex p-4"> | ||
| <Suspense fallback={<IconButton name="delete" />}> | ||
| <IconButton | ||
| class={clsx(queue()?.clearingQueue() && 'animate-spin')} | ||
| name={queue()?.clearingQueue() ? 'progress_activity' : queue()?.clearQueueError() ? 'error' : 'delete'} | ||
| onClick={() => void queue()?.clearQueue()} | ||
| disabled={queue()?.clearingQueue()} | ||
| /> | ||
| </Suspense> | ||
| </div> | ||
| </div> | ||
| <div class="rounded-md border-2 border-surface-container-high mx-4 mb-4 p-4"> | ||
| <QueueItemTable items={items} error={queue()?.error} offline={queue()?.offline()} /> | ||
| </div> | ||
| </div> | ||
| ) | ||
| } | ||
|
|
||
| export default UploadQueue | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would just put all of the components in the same file if they aren't going to be used elsewhere in the app