checkpoint

pull/134/head
Alex Kern 1 year ago
parent 4cb02da415
commit 3c17d2604f
No known key found for this signature in database
GPG Key ID: EF051FACCACBEE25

@ -12,6 +12,8 @@ import { UploadedFile } from '../types'
import Spinner from '../components/Spinner' import Spinner from '../components/Spinner'
import Wordmark from '../components/Wordmark' import Wordmark from '../components/Wordmark'
import CancelButton from '../components/CancelButton' import CancelButton from '../components/CancelButton'
import { useMemo } from 'react'
import { getFileName } from '../fs'
const queryClient = new QueryClient() const queryClient = new QueryClient()
@ -53,6 +55,15 @@ function InitialState({
) )
} }
function useUploaderFileListData(uploadedFiles: UploadedFile[]) {
return useMemo(() => {
return uploadedFiles.map((item) => ({
fileName: getFileName(item),
type: item.type,
}))
}, [uploadedFiles])
}
function ConfirmUploadState({ function ConfirmUploadState({
uploadedFiles, uploadedFiles,
password, password,
@ -68,13 +79,14 @@ function ConfirmUploadState({
onStart: () => void onStart: () => void
onFileListChange: (updatedFiles: UploadedFile[]) => void onFileListChange: (updatedFiles: UploadedFile[]) => void
}): JSX.Element { }): JSX.Element {
const fileListData = useUploaderFileListData(uploadedFiles)
return ( return (
<PageWrapper> <PageWrapper>
<p className="text-lg text-center text-stone-800 max-w-md"> <p className="text-lg text-center text-stone-800 max-w-md">
You are about to start uploading {uploadedFiles.length}{' '} You are about to start uploading {uploadedFiles.length}{' '}
{uploadedFiles.length === 1 ? 'file' : 'files'}. {uploadedFiles.length === 1 ? 'file' : 'files'}.
</p> </p>
<UploadFileList files={uploadedFiles} onChange={onFileListChange} /> <UploadFileList files={fileListData} onChange={onFileListChange} />
<PasswordField value={password} onChange={onChangePassword} /> <PasswordField value={password} onChange={onChangePassword} />
<div className="flex space-x-4"> <div className="flex space-x-4">
<CancelButton onClick={onCancel} /> <CancelButton onClick={onCancel} />
@ -93,13 +105,14 @@ function UploadingState({
password: string password: string
onStop: () => void onStop: () => void
}): JSX.Element { }): JSX.Element {
const fileListData = useUploaderFileListData(uploadedFiles)
return ( return (
<PageWrapper isRotating={true}> <PageWrapper isRotating={true}>
<p className="text-lg text-center text-stone-800 max-w-md"> <p className="text-lg text-center text-stone-800 max-w-md">
You are uploading {uploadedFiles.length}{' '} You are uploading {uploadedFiles.length}{' '}
{uploadedFiles.length === 1 ? 'file' : 'files'}. {uploadedFiles.length === 1 ? 'file' : 'files'}.
</p> </p>
<UploadFileList files={uploadedFiles} /> <UploadFileList files={fileListData} />
<WebRTCProvider> <WebRTCProvider>
<Uploader files={uploadedFiles} password={password} onStop={onStop} /> <Uploader files={uploadedFiles} password={password} onStop={onStop} />
</WebRTCProvider> </WebRTCProvider>

@ -10,11 +10,11 @@ export function ConnectionListItem({
const getStatusColor = (status: UploaderConnectionStatus) => { const getStatusColor = (status: UploaderConnectionStatus) => {
switch (status) { switch (status) {
case UploaderConnectionStatus.Uploading: case UploaderConnectionStatus.Uploading:
return 'bg-green-500' return 'bg-blue-500'
case UploaderConnectionStatus.Paused: case UploaderConnectionStatus.Paused:
return 'bg-yellow-500' return 'bg-yellow-500'
case UploaderConnectionStatus.Done: case UploaderConnectionStatus.Done:
return 'bg-blue-500' return 'bg-green-500'
case UploaderConnectionStatus.Closed: case UploaderConnectionStatus.Closed:
return 'bg-red-500' return 'bg-red-500'
default: default:

@ -105,7 +105,7 @@ export default function Downloader({
null, null,
) )
const [filesInfo, setFilesInfo] = useState<Array<{ const [filesInfo, setFilesInfo] = useState<Array<{
fullPath: string fileName: string
size: number size: number
type: string type: string
}> | null>(null) }> | null>(null)
@ -234,7 +234,7 @@ export default function Downloader({
close = () => ctrl.close() close = () => ctrl.close()
}, },
}) })
fileStreamByPath[info.fullPath] = { fileStreamByPath[info.fileName] = {
stream, stream,
enqueue, enqueue,
close, close,
@ -250,7 +250,7 @@ export default function Downloader({
const request: t.TypeOf<typeof Message> = { const request: t.TypeOf<typeof Message> = {
type: MessageType.Start, type: MessageType.Start,
fullPath: filesInfo[nextFileIndex].fullPath, fileName: filesInfo[nextFileIndex].fileName,
offset: 0, offset: 0,
} }
dataConnection.send(request) dataConnection.send(request)
@ -258,9 +258,9 @@ export default function Downloader({
} }
const processChunkFunc = (message: t.TypeOf<typeof ChunkMessage>): void => { const processChunkFunc = (message: t.TypeOf<typeof ChunkMessage>): void => {
const fileStream = fileStreamByPath[message.fullPath] const fileStream = fileStreamByPath[message.fileName]
if (!fileStream) { if (!fileStream) {
console.error('no stream found for ' + message.fullPath) console.error('no stream found for ' + message.fileName)
return return
} }
@ -275,7 +275,7 @@ export default function Downloader({
processChunk.current = processChunkFunc processChunk.current = processChunkFunc
const downloads = filesInfo.map((info, i) => ({ const downloads = filesInfo.map((info, i) => ({
name: info.fullPath.replace(/^\//, ''), name: info.fileName.replace(/^\//, ''),
size: info.size, size: info.size,
stream: () => fileStreams[i], stream: () => fileStreams[i],
})) }))

@ -11,13 +11,19 @@ export default function ProgressBar({
const isComplete = value === max const isComplete = value === max
return ( return (
<div className="w-full h-12 bg-stone-200 rounded-md overflow-hidden"> <div className="w-full h-12 bg-stone-200 rounded-md overflow-hidden relative">
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-black font-bold">{Math.round(percentage)}%</span>
</div>
<div <div
className={`h-full ${ className={`h-full ${
isComplete ? 'bg-green-500' : 'bg-blue-500' isComplete ? 'bg-green-500' : 'bg-blue-500'
} transition-all duration-300 ease-in-out`} } transition-all duration-300 ease-in-out`}
style={{ width: `${percentage}%` }} style={{ width: `${percentage}%` }}
/> />
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-white font-bold">{Math.round(percentage)}%</span>
</div>
</div> </div>
) )
} }

@ -2,18 +2,10 @@ import React from 'react'
import TypeBadge from './TypeBadge' import TypeBadge from './TypeBadge'
type UploadedFileLike = { type UploadedFileLike = {
fullPath?: string fileName?: string
name?: string
type: string type: string
} }
function getFileName(file: UploadedFileLike): string {
if (file.fullPath) {
return file.fullPath.slice(1)
}
return file.name || 'Unknown'
}
export default function UploadFileList({ export default function UploadFileList({
files, files,
onChange, onChange,
@ -30,11 +22,11 @@ export default function UploadFileList({
const items = files.map((f: UploadedFileLike, i: number) => ( const items = files.map((f: UploadedFileLike, i: number) => (
<div <div
key={getFileName(f)} key={f.fileName}
className="w-full border border-stone-300 rounded-md mb-2 group" className="w-full border border-stone-300 rounded-md mb-2 group"
> >
<div className="flex justify-between items-center py-2 px-2.5"> <div className="flex justify-between items-center py-2 px-2.5">
<p className="truncate text-sm font-medium">{getFileName(f)}</p> <p className="truncate text-sm font-medium">{f.fileName}</p>
<div className="flex items-center"> <div className="flex items-center">
<TypeBadge type={f.type} /> <TypeBadge type={f.type} />
{onChange && ( {onChange && (

@ -1,5 +1,7 @@
'use client'
import React from 'react' import React from 'react'
import { UploadedFile } from '../types' import { UploadedFile, UploaderConnectionStatus } from '../types'
import { useWebRTC } from './WebRTCProvider' import { useWebRTC } from './WebRTCProvider'
import QRCode from 'react-qr-code' import QRCode from 'react-qr-code'
import Loading from './Loading' import Loading from './Loading'
@ -32,6 +34,10 @@ export default function Uploader({
return <Loading text="Creating channel" /> return <Loading text="Creating channel" />
} }
const activeDownloaders = connections.filter(
(conn) => conn.status === UploaderConnectionStatus.Uploading,
).length
return ( return (
<> <>
<div className="flex w-full items-center"> <div className="flex w-full items-center">
@ -46,8 +52,7 @@ export default function Uploader({
<div className="mt-6 pt-4 border-t border-gray-200 w-full"> <div className="mt-6 pt-4 border-t border-gray-200 w-full">
<div className="flex justify-between items-center mb-2"> <div className="flex justify-between items-center mb-2">
<h2 className="text-lg font-semibold text-stone-400"> <h2 className="text-lg font-semibold text-stone-400">
{connections.length}{' '} {activeDownloaders} Downloading, {connections.length} Total
{connections.length === 1 ? 'Downloader' : 'Downloaders'}
</h2> </h2>
<StopButton onClick={onStop} /> <StopButton onClick={onStop} />
</div> </div>

@ -3,7 +3,7 @@ import { UploadedFile } from './types'
const getAsFile = (entry: any): Promise<File> => const getAsFile = (entry: any): Promise<File> =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
entry.file((file: UploadedFile) => { entry.file((file: UploadedFile) => {
file.fullPath = entry.fullPath file.entryFullPath = entry.fullPath
resolve(file) resolve(file)
}, reject) }, reject)
}) })
@ -77,3 +77,7 @@ export const formatSize = (bytes: number): string => {
const i = Math.floor(Math.log(bytes) / Math.log(k)) const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${(bytes / Math.pow(k, i)).toPrecision(3)} ${sizes[i]}` return `${(bytes / Math.pow(k, i)).toPrecision(3)} ${sizes[i]}`
} }
export const getFileName = (file: UploadedFile): string => {
return file.name ?? file.entryFullPath ?? ''
}

@ -7,19 +7,18 @@ import {
} from '../types' } from '../types'
import { decodeMessage, Message, MessageType } from '../messages' import { decodeMessage, Message, MessageType } from '../messages'
import * as t from 'io-ts' import * as t from 'io-ts'
import { getFileName } from '../fs'
// TODO(@kern): Test for better values // TODO(@kern): Test for better values
const MAX_CHUNK_SIZE = 10 * 1024 * 1024 // 10 Mi const MAX_CHUNK_SIZE = 10 * 1024 * 1024 // 10 Mi
function validateOffset( function validateOffset(
files: UploadedFile[], files: UploadedFile[],
fullPath: string, fileName: string,
offset: number, offset: number,
): UploadedFile { ): UploadedFile {
const validFile = files.find( const validFile = files.find(
(file) => (file) => getFileName(file) === fileName && offset <= file.size,
(file.fullPath === fullPath || file.name === fullPath) &&
offset <= file.size,
) )
if (!validFile) { if (!validFile) {
throw new Error('invalid file offset') throw new Error('invalid file offset')
@ -106,7 +105,7 @@ export function useUploaderConnections(
const fileInfo = files.map((f) => { const fileInfo = files.map((f) => {
return { return {
fullPath: f.fullPath ?? f.name ?? '', fileName: f.fileName ?? f.name ?? '',
size: f.size, size: f.size,
type: f.type, type: f.type,
} }
@ -122,9 +121,9 @@ export function useUploaderConnections(
} }
case MessageType.Start: { case MessageType.Start: {
const fullPath = message.fullPath const fileName = message.fileName
let offset = message.offset let offset = message.offset
const file = validateOffset(files, fullPath, offset) const file = validateOffset(files, fileName, offset)
updateConnection((draft) => { updateConnection((draft) => {
if (draft.status !== UploaderConnectionStatus.Paused) { if (draft.status !== UploaderConnectionStatus.Paused) {
return draft return draft
@ -133,7 +132,7 @@ export function useUploaderConnections(
return { return {
...draft, ...draft,
status: UploaderConnectionStatus.Uploading, status: UploaderConnectionStatus.Uploading,
uploadingFullPath: fullPath, uploadingFileName: fileName,
uploadingOffset: offset, uploadingOffset: offset,
currentFileProgress: offset / file.size, currentFileProgress: offset / file.size,
} }
@ -145,7 +144,7 @@ export function useUploaderConnections(
const final = chunkSize < MAX_CHUNK_SIZE const final = chunkSize < MAX_CHUNK_SIZE
const request: t.TypeOf<typeof Message> = { const request: t.TypeOf<typeof Message> = {
type: MessageType.Chunk, type: MessageType.Chunk,
fullPath, fileName,
offset, offset,
bytes: file.slice(offset, end), bytes: file.slice(offset, end),
final, final,

@ -27,7 +27,7 @@ export const InfoMessage = t.type({
type: t.literal(MessageType.Info), type: t.literal(MessageType.Info),
files: t.array( files: t.array(
t.type({ t.type({
fullPath: t.string, fileName: t.string,
size: t.number, size: t.number,
type: t.string, type: t.string,
}), }),
@ -36,13 +36,13 @@ export const InfoMessage = t.type({
export const StartMessage = t.type({ export const StartMessage = t.type({
type: t.literal(MessageType.Start), type: t.literal(MessageType.Start),
fullPath: t.string, fileName: t.string,
offset: t.number, offset: t.number,
}) })
export const ChunkMessage = t.type({ export const ChunkMessage = t.type({
type: t.literal(MessageType.Chunk), type: t.literal(MessageType.Chunk),
fullPath: t.string, fileName: t.string,
offset: t.number, offset: t.number,
bytes: t.unknown, bytes: t.unknown,
final: t.boolean, final: t.boolean,

@ -1,6 +1,6 @@
import type { DataConnection } from 'peerjs' import type { DataConnection } from 'peerjs'
export type UploadedFile = File & { fullPath?: string; name?: string } export type UploadedFile = File & { entryFullPath?: string }
export enum UploaderConnectionStatus { export enum UploaderConnectionStatus {
Pending = 'PENDING', Pending = 'PENDING',
@ -20,7 +20,7 @@ export type UploaderConnection = {
osVersion?: string osVersion?: string
mobileVendor?: string mobileVendor?: string
mobileModel?: string mobileModel?: string
uploadingFullPath?: string uploadingFileName?: string
uploadingOffset?: number uploadingOffset?: number
completedFiles: number completedFiles: number
totalFiles: number totalFiles: number

Loading…
Cancel
Save