more progress

pull/134/head
Alex Kern 1 year ago
parent bebf6c319a
commit 2959e72bb4
No known key found for this signature in database
GPG Key ID: EF051FACCACBEE25

@ -8,7 +8,7 @@ export default function DownloadButton({
return ( return (
<button <button
onClick={onClick} onClick={onClick}
className="px-4 py-2 bg-green-500 text-white font-semibold rounded-md hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-opacity-50" className="px-4 py-2 bg-gradient-to-b from-green-500 to-green-600 text-white rounded-md hover:from-green-500 hover:to-green-700 transition-all duration-200 border border-green-600 shadow-sm hover:shadow-md text-shadow"
> >
Download Download
</button> </button>

@ -11,6 +11,7 @@ import StopButton from './StopButton'
import ProgressBar from './ProgressBar' import ProgressBar from './ProgressBar'
import TitleText from './TitleText' import TitleText from './TitleText'
import ReturnHome from './ReturnHome' import ReturnHome from './ReturnHome'
import { pluralize } from '../utils/pluralize'
interface FileInfo { interface FileInfo {
fileName: string fileName: string
@ -29,7 +30,9 @@ export function DownloadComplete({
}): JSX.Element { }): JSX.Element {
return ( return (
<> <>
<TitleText>You downloaded {filesInfo.length} files.</TitleText> <TitleText>
You downloaded {pluralize(filesInfo.length, 'file', 'files')}.
</TitleText>
<div className="flex flex-col space-y-5 w-full"> <div className="flex flex-col space-y-5 w-full">
<UploadFileList files={filesInfo} /> <UploadFileList files={filesInfo} />
<div className="w-full"> <div className="w-full">
@ -55,15 +58,17 @@ export function DownloadInProgress({
return ( return (
<> <>
<TitleText> <TitleText>
You are about to start downloading {filesInfo.length} files. You are downloading {pluralize(filesInfo.length, 'file', 'files')}.
</TitleText> </TitleText>
<div className="flex flex-col space-y-5 w-full"> <div className="flex flex-col space-y-5 w-full">
<UploadFileList files={filesInfo} /> <UploadFileList files={filesInfo} />
<div className="w-full"> <div className="w-full">
<ProgressBar value={bytesDownloaded} max={totalSize} /> <ProgressBar value={bytesDownloaded} max={totalSize} />
</div> </div>
<div className="flex justify-center w-full">
<StopButton onClick={onStop} isDownloading /> <StopButton onClick={onStop} isDownloading />
</div> </div>
</div>
</> </>
) )
} }
@ -78,7 +83,8 @@ export function ReadyToDownload({
return ( return (
<> <>
<TitleText> <TitleText>
You are about to start downloading {filesInfo.length} files. You are about to start downloading{' '}
{pluralize(filesInfo.length, 'file', 'files')}.
</TitleText> </TitleText>
<div className="flex flex-col space-y-5 w-full"> <div className="flex flex-col space-y-5 w-full">
<UploadFileList files={filesInfo} /> <UploadFileList files={filesInfo} />
@ -106,9 +112,7 @@ export function PasswordEntry({
return ( return (
<> <>
<TitleText> <TitleText>This download requires a password.</TitleText>
{errorMessage || 'This download requires a password.'}
</TitleText>
<div className="flex flex-col space-y-5 w-full"> <div className="flex flex-col space-y-5 w-full">
<form <form
action="#" action="#"
@ -127,6 +131,14 @@ export function PasswordEntry({
</div> </div>
</form> </form>
</div> </div>
{errorMessage && (
<div
className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative"
role="alert"
>
<span className="block sm:inline">{errorMessage}</span>
</div>
)}
</> </>
) )
} }

@ -2,11 +2,17 @@ import React from 'react'
export default function InputLabel({ export default function InputLabel({
children, children,
hasError = false,
}: { }: {
children: React.ReactNode children: React.ReactNode
hasError?: boolean
}): JSX.Element { }): JSX.Element {
return ( return (
<label className="text-[10px] text-stone-400 mb-0.5 font-bold"> <label
className={`text-[10px] mb-0.5 font-bold ${
hasError ? 'text-red-500' : 'text-stone-400'
}`}
>
{children} {children}
</label> </label>
) )

@ -1,7 +1,12 @@
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import InputLabel from './InputLabel' import InputLabel from './InputLabel'
export default function PasswordField(props: { export default function PasswordField({
value,
onChange,
isRequired = false,
isInvalid = false,
}: {
value: string value: string
onChange: (v: string) => void onChange: (v: string) => void
isRequired?: boolean isRequired?: boolean
@ -9,24 +14,24 @@ export default function PasswordField(props: {
}): JSX.Element { }): JSX.Element {
const handleChange = useCallback( const handleChange = useCallback(
function (e: React.ChangeEvent<HTMLInputElement>): void { function (e: React.ChangeEvent<HTMLInputElement>): void {
props.onChange(e.target.value) onChange(e.target.value)
}, },
[props.onChange], [onChange],
) )
return ( return (
<div className="flex flex-col w-full"> <div className="flex flex-col w-full">
<InputLabel> <InputLabel hasError={isInvalid}>
{props.isRequired ? 'Password' : 'Password (optional)'} {isRequired ? 'Password' : 'Password (optional)'}
</InputLabel> </InputLabel>
<input <input
autoFocus autoFocus
type="password" type="password"
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${ className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
props.isInvalid ? 'border-red-500' : 'border-stone-300' isInvalid ? 'border-red-500' : 'border-stone-300'
}`} }`}
placeholder="Enter a secret password for this FilePizza..." placeholder="Enter a secret password for this FilePizza..."
value={props.value} value={value}
onChange={handleChange} onChange={handleChange}
/> />
</div> </div>

@ -8,7 +8,7 @@ export default function StartButton({
return ( return (
<button <button
onClick={onClick} onClick={onClick}
className="px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600 transition-colors duration-200" className="px-4 py-2 bg-gradient-to-b from-green-500 to-green-600 text-white rounded-md hover:from-green-500 hover:to-green-700 transition-all duration-200 border border-green-600 shadow-sm hover:shadow-md text-shadow"
> >
Start Start
</button> </button>

@ -4,11 +4,11 @@ export default function UnlockButton({
onClick, onClick,
}: { }: {
onClick?: React.MouseEventHandler<HTMLButtonElement> onClick?: React.MouseEventHandler<HTMLButtonElement>
}): React.ReactElement { }): JSX.Element {
return ( return (
<button <button
onClick={onClick} onClick={onClick}
className="px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600 transition-colors duration-200" className="px-4 py-2 bg-gradient-to-b from-green-500 to-green-600 text-white rounded-md hover:from-green-500 hover:to-green-700 transition-all duration-200 border border-green-600 shadow-sm hover:shadow-md text-shadow"
> >
Unlock Unlock
</button> </button>

@ -37,7 +37,6 @@ export function useDownloader(uploaderPeerID: string): {
bytesDownloaded: number bytesDownloaded: number
} { } {
const peer = useWebRTC() const peer = useWebRTC()
const [password, setPassword] = useState('')
const [dataConnection, setDataConnection] = useState<DataConnection | null>( const [dataConnection, setDataConnection] = useState<DataConnection | null>(
null, null,
) )
@ -50,11 +49,11 @@ export function useDownloader(uploaderPeerID: string): {
((message: z.infer<typeof ChunkMessage>) => void) | null ((message: z.infer<typeof ChunkMessage>) => void) | null
>(null) >(null)
const [isConnected, setIsConnected] = useState(false) const [isConnected, setIsConnected] = useState(false)
const [isDownloading, setDownloading] = useState(false) const [isPasswordRequired, setIsPasswordRequired] = useState(false)
const [isDownloading, setIsDownloading] = useState(false)
const [isDone, setDone] = useState(false) const [isDone, setDone] = useState(false)
const [bytesDownloaded, setBytesDownloaded] = useState(0) const [bytesDownloaded, setBytesDownloaded] = useState(0)
const [errorMessage, setErrorMessage] = useState<string | null>(null) const [errorMessage, setErrorMessage] = useState<string | null>(null)
const [isPasswordRequired, setIsPasswordRequired] = useState(false)
useEffect(() => { useEffect(() => {
const conn = peer.connect(uploaderPeerID, { reliable: true }) const conn = peer.connect(uploaderPeerID, { reliable: true })
@ -79,6 +78,7 @@ export function useDownloader(uploaderPeerID: string): {
switch (message.type) { switch (message.type) {
case MessageType.PasswordRequired: case MessageType.PasswordRequired:
setIsPasswordRequired(true) setIsPasswordRequired(true)
if (message.errorMessage) setErrorMessage(message.errorMessage)
break break
case MessageType.Info: case MessageType.Info:
setFilesInfo(message.files) setFilesInfo(message.files)
@ -100,7 +100,7 @@ export function useDownloader(uploaderPeerID: string): {
const handleClose = () => { const handleClose = () => {
setDataConnection(null) setDataConnection(null)
setIsConnected(false) setIsConnected(false)
setDownloading(false) setIsDownloading(false)
} }
const handleError = (err: Error) => { const handleError = (err: Error) => {
@ -124,7 +124,7 @@ export function useDownloader(uploaderPeerID: string): {
conn.off('close', handleClose) conn.off('close', handleClose)
peer.off('error', handleError) peer.off('error', handleError)
} }
}, [peer, password, uploaderPeerID]) }, [peer, uploaderPeerID])
const submitPassword = useCallback( const submitPassword = useCallback(
(pass: string) => { (pass: string) => {
@ -134,12 +134,12 @@ export function useDownloader(uploaderPeerID: string): {
password: pass, password: pass,
} as z.infer<typeof Message>) } as z.infer<typeof Message>)
}, },
[dataConnection, password], [dataConnection],
) )
const startDownload = useCallback(() => { const startDownload = useCallback(() => {
if (!filesInfo || !dataConnection) return if (!filesInfo || !dataConnection) return
setDownloading(true) setIsDownloading(true)
const fileStreamByPath: Record< const fileStreamByPath: Record<
string, string,
@ -213,8 +213,23 @@ export function useDownloader(uploaderPeerID: string): {
}, [dataConnection, filesInfo]) }, [dataConnection, filesInfo])
const stopDownload = useCallback(() => { const stopDownload = useCallback(() => {
// TODO(@kern): Implement me // TODO(@kern): Continue here with stop / pause logic
}, []) if (dataConnection) {
dataConnection.send({ type: MessageType.Pause } as z.infer<
typeof Message
>)
dataConnection.close()
}
setIsDownloading(false)
setDone(false)
setBytesDownloaded(0)
setErrorMessage(null)
// fileStreams.forEach((stream) => stream.cancel())
// fileStreams.length = 0
// Object.values(fileStreamByPath).forEach((stream) => stream.cancel())
// Object.keys(fileStreamByPath).forEach((key) => delete fileStreamByPath[key])
// }, [dataConnection, fileStreams, fileStreamByPath])
}, [dataConnection])
return { return {
filesInfo, filesInfo,

@ -57,13 +57,19 @@ export function useUploaderConnections(
const message = decodeMessage(data) const message = decodeMessage(data)
switch (message.type) { switch (message.type) {
case MessageType.RequestInfo: { case MessageType.RequestInfo: {
const newConnectionState = {
browserName: message.browserName,
browserVersion: message.browserVersion,
osName: message.osName,
osVersion: message.osVersion,
mobileVendor: message.mobileVendor,
mobileModel: message.mobileModel,
}
if (password) { if (password) {
// TODO(@kern): Check password
const request: Message = { const request: Message = {
type: MessageType.Error, type: MessageType.PasswordRequired,
error: 'Invalid password',
} }
conn.send(request) conn.send(request)
updateConnection((draft) => { updateConnection((draft) => {
@ -73,13 +79,8 @@ export function useUploaderConnections(
return { return {
...draft, ...draft,
status: UploaderConnectionStatus.InvalidPassword, ...newConnectionState,
browserName: message.browserName, status: UploaderConnectionStatus.Authenticating,
browserVersion: message.browserVersion,
osName: message.osName,
osVersion: message.osVersion,
mobileVendor: message.mobileVendor,
mobileModel: message.mobileModel,
} }
}) })
@ -93,13 +94,8 @@ export function useUploaderConnections(
return { return {
...draft, ...draft,
...newConnectionState,
status: UploaderConnectionStatus.Paused, status: UploaderConnectionStatus.Paused,
browserName: message.browserName,
browserVersion: message.browserVersion,
osName: message.osName,
osVersion: message.osVersion,
mobileVendor: message.mobileVendor,
mobileModel: message.mobileModel,
} }
}) })
@ -120,6 +116,57 @@ export function useUploaderConnections(
break break
} }
case MessageType.UsePassword: {
const { password: submittedPassword } = message
if (submittedPassword === password) {
updateConnection((draft) => {
if (
draft.status !== UploaderConnectionStatus.Authenticating
) {
return draft
}
return {
...draft,
status: UploaderConnectionStatus.Paused,
}
})
const fileInfo = files.map((f) => ({
fileName: getFileName(f),
size: f.size,
type: f.type,
}))
const request: Message = {
type: MessageType.Info,
files: fileInfo,
}
conn.send(request)
} else {
updateConnection((draft) => {
if (
draft.status !== UploaderConnectionStatus.Authenticating
) {
return draft
}
return {
...draft,
status: UploaderConnectionStatus.InvalidPassword,
}
})
const request: Message = {
type: MessageType.PasswordRequired,
errorMessage: 'Invalid password',
}
conn.send(request)
}
break
}
case MessageType.Start: { case MessageType.Start: {
const fileName = message.fileName const fileName = message.fileName
let offset = message.offset let offset = message.offset

@ -57,6 +57,7 @@ export const ErrorMessage = z.object({
export const PasswordRequiredMessage = z.object({ export const PasswordRequiredMessage = z.object({
type: z.literal(MessageType.PasswordRequired), type: z.literal(MessageType.PasswordRequired),
errorMessage: z.string().optional(),
}) })
export const UsePasswordMessage = z.object({ export const UsePasswordMessage = z.object({

@ -7,6 +7,7 @@ export enum UploaderConnectionStatus {
Paused = 'PAUSED', Paused = 'PAUSED',
Uploading = 'UPLOADING', Uploading = 'UPLOADING',
Done = 'DONE', Done = 'DONE',
Authenticating = 'AUTHENTICATING',
InvalidPassword = 'INVALID_PASSWORD', InvalidPassword = 'INVALID_PASSWORD',
Closed = 'CLOSED', Closed = 'CLOSED',
} }

@ -0,0 +1,7 @@
export function pluralize(
count: number,
singular: string,
plural: string,
): string {
return `${count} ${count === 1 ? singular : plural}`
}
Loading…
Cancel
Save