pull/134/head
Alex Kern 1 year ago
parent 450841484a
commit 4583eaec1d
No known key found for this signature in database
GPG Key ID: EF051FACCACBEE25

@ -20,6 +20,7 @@
"url": "https://github.com/kern/filepizza/issues"
},
"dependencies": {
"@tanstack/react-query": "^5.55.2",
"autoprefixer": "^10.4.20",
"debug": "^4.3.6",
"express": "^4.19.2",

@ -5,6 +5,9 @@ settings:
excludeLinksFromLockfile: false
dependencies:
'@tanstack/react-query':
specifier: ^5.55.2
version: 5.55.2(react@18.3.1)
autoprefixer:
specifier: ^10.4.20
version: 10.4.20(postcss@8.4.44)
@ -2314,6 +2317,19 @@ packages:
tslib: 2.7.0
dev: false
/@tanstack/query-core@5.55.2:
resolution: {integrity: sha512-WfuUx06sw/V3R2tRRQf4KZj0g4Q5EHrTdwJmCUovMFuHWVzDvLd5U+B7d5Pk8ni5w5SGpUuzGuQv3nN8xo1QbQ==}
dev: false
/@tanstack/react-query@5.55.2(react@18.3.1):
resolution: {integrity: sha512-PkbEmO64nphWPhufkE9aJ+iEScg8tNNCykYlH7vDwb2R6G8uJC+HTJgXc2n8cRaJe6ETVIeQLFFESVd+2N/tww==}
peerDependencies:
react: ^18 || ^19
dependencies:
'@tanstack/query-core': 5.55.2
react: 18.3.1
dev: false
/@types/body-parser@1.19.5:
resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==}
dependencies:

@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react'
import { UploadedFile } from '../types'
import { useWebRTC } from './WebRTCProvider'
import useFetch from 'use-http'
import { useQuery } from '@tanstack/react-query'
import Peer, { DataConnection } from 'peerjs'
import { decodeMessage, Message, MessageType } from '../messages'
import QRCode from 'react-qr-code'
@ -11,6 +11,7 @@ import Loading from './Loading'
import ProgressBar from './ProgressBar'
import useClipboard from '../hooks/useClipboard'
import InputLabel from './InputLabel'
import { useUploaderChannelRenewal } from '../hooks/useUploaderChannelRenewal'
enum UploaderConnectionStatus {
Pending = 'PENDING',
@ -32,68 +33,62 @@ type UploaderConnection = {
mobileModel?: string
uploadingFullPath?: string
uploadingOffset?: number
completedFiles: number
totalFiles: number
currentFileProgress: number
}
// TODO(@kern): Use better values
const RENEW_INTERVAL = 5000 // 20 minutes
const MAX_CHUNK_SIZE = 10 * 1024 * 1024 // 10 Mi
const QR_CODE_SIZE = 128
function generateURL(slug: string): string {
const hostPrefix =
window.location.protocol +
'//' +
window.location.hostname +
(['80', '443'].includes(window.location.port)
? ''
: ':' + window.location.port)
return `${hostPrefix}/download/${slug}`
}
function useUploaderChannel(uploaderPeerID: string): {
loading: boolean
error: Error | null
longSlug: string
shortSlug: string
longSlug: string | undefined
shortSlug: string | undefined
longURL: string | undefined
shortURL: string | undefined
} {
const { loading, error, data } = useFetch(
'/api/create',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ uploaderPeerID }),
const { isLoading, error, data } = useQuery({
queryKey: ['uploaderChannel', uploaderPeerID],
queryFn: async () => {
const response = await fetch('/api/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ uploaderPeerID }),
})
if (!response.ok) {
throw new Error('Network response was not ok')
}
return response.json()
},
[uploaderPeerID],
)
})
if (!data) {
return { loading, error, longSlug: null, shortSlug: null }
}
const longURL = data?.longSlug ? generateURL(data.longSlug) : undefined
const shortURL = data?.shortSlug ? generateURL(data.shortSlug) : undefined
return {
loading: false,
error: null,
longSlug: data.longSlug,
shortSlug: data.shortSlug,
loading: isLoading,
error: error as Error | null,
longSlug: data?.longSlug,
shortSlug: data?.shortSlug,
longURL,
shortURL,
}
}
function useUploaderChannelRenewal(shortSlug: string): void {
const { post } = useFetch('/api/renew')
useEffect(() => {
let timeout = null
const run = (): void => {
timeout = setTimeout(() => {
post({ slug: shortSlug })
.then(() => {
run()
})
.catch((err) => {
console.error(err)
run()
})
}, RENEW_INTERVAL)
}
run()
return () => {
clearTimeout(timeout)
}
}, [shortSlug])
}
function validateOffset(
files: UploadedFile[],
fullPath: string,
@ -117,10 +112,13 @@ function useUploaderConnections(
useEffect(() => {
peer.on('connection', (conn: DataConnection) => {
let sendChunkTimeout: number | null = null
let sendChunkTimeout: NodeJS.Timeout | null = null
const newConn = {
status: UploaderConnectionStatus.Pending,
dataConnection: conn,
completedFiles: 0,
totalFiles: files.length,
currentFileProgress: 0,
}
setConnections((conns) => [...conns, newConn])
@ -211,6 +209,7 @@ function useUploaderConnections(
draft.status = UploaderConnectionStatus.Uploading
draft.uploadingFullPath = fullPath
draft.uploadingOffset = offset
draft.currentFileProgress = offset / file.size
})
const sendNextChunk = () => {
@ -229,11 +228,14 @@ function useUploaderConnections(
updateConnection((draft) => {
offset = end
draft.uploadingOffset = end
draft.currentFileProgress = end / file.size
if (final) {
draft.status = UploaderConnectionStatus.Paused
draft.completedFiles += 1
draft.currentFileProgress = 0
} else {
sendChunkTimeout = window.setTimeout(() => {
sendChunkTimeout = setTimeout(() => {
sendNextChunk()
}, 0)
}
@ -300,32 +302,43 @@ function useUploaderConnections(
return connections
}
function CopyableInput({ label, value }: { label: string; value: string }) {
const { hasCopied, onCopy } = useClipboard(value)
return (
<div className="flex flex-col w-full">
<InputLabel>{label}</InputLabel>
<div className="flex w-full">
<input
className="flex-grow px-3 py-2 text-xs border border-r-0 rounded-l"
value={value}
readOnly
/>
<button
className="px-4 py-2 text-sm text-stone-700 bg-stone-100 hover:bg-stone-200 rounded-r border-t border-r border-b"
onClick={onCopy}
>
{hasCopied ? 'Copied' : 'Copy'}
</button>
</div>
</div>
)
}
export default function Uploader({
files,
password,
renewInterval = 5000,
}: {
files: UploadedFile[]
password: string
renewInterval?: number
}): JSX.Element {
const peer = useWebRTC()
const { longSlug, shortSlug } = useUploaderChannel(peer.id)
useUploaderChannelRenewal(shortSlug)
const { longSlug, shortSlug, longURL, shortURL } = useUploaderChannel(peer.id)
useUploaderChannelRenewal(shortSlug, renewInterval)
const connections = useUploaderConnections(peer, files, password)
const hostPrefix =
window.location.protocol +
'//' +
window.location.hostname +
(['80', '443'].includes(window.location.port)
? ''
: ':' + window.location.port)
const longURL = `${hostPrefix}/download/${longSlug}`
const shortURL = `${hostPrefix}/download/${shortSlug}`
const { hasCopied: hasCopiedLongURL, onCopy: onCopyLongURL } =
useClipboard(longURL)
const { hasCopied: hasCopiedShortURL, onCopy: onCopyShortURL } =
useClipboard(shortURL)
if (!longSlug || !shortSlug) {
return <Loading text="Creating channel" />
}
@ -334,41 +347,11 @@ export default function Uploader({
<>
<div className="flex w-full items-center">
<div className="flex-none mr-4">
<QRCode value={shortURL} size={QR_CODE_SIZE} />
<QRCode value={shortURL ?? ''} size={QR_CODE_SIZE} />
</div>
<div className="flex-auto flex flex-col justify-center space-y-2">
<div className="flex flex-col w-full">
<InputLabel>Long URL</InputLabel>
<div className="flex w-full">
<input
className="flex-grow px-3 py-2 text-xs border border-r-0 rounded-l"
value={longURL}
readOnly
/>
<button
className="px-4 py-2 text-sm text-stone-700 bg-stone-100 hover:bg-stone-200 rounded-r border-t border-r border-b"
onClick={onCopyLongURL}
>
{hasCopiedLongURL ? 'Copied' : 'Copy'}
</button>
</div>
</div>
<div className="flex flex-col w-full mt-2">
<InputLabel>Short URL</InputLabel>
<div className="flex w-full">
<input
className="flex-grow px-3 py-2 text-xs border border-r-0 rounded-l"
value={shortURL}
readOnly
/>
<button
className="px-4 py-2 text-sm text-stone-700 bg-stone-100 hover:bg-stone-200 rounded-r border-t border-r border-b"
onClick={onCopyShortURL}
>
{hasCopiedShortURL ? 'Copied' : 'Copy'}
</button>
</div>
</div>
<CopyableInput label="Long URL" value={longURL ?? ''} />
<CopyableInput label="Short URL" value={shortURL ?? ''} />
</div>
</div>
{connections.map((conn, i) => (
@ -377,7 +360,12 @@ export default function Uploader({
<div className="text-sm">
{conn.status} {conn.browserName} {conn.browserVersion}
</div>
<ProgressBar value={50} max={100} />
<ProgressBar
value={
(conn.completedFiles + conn.currentFileProgress) / conn.totalFiles
}
max={1}
/>
</div>
))}
</>

@ -7,7 +7,7 @@ import Loading from './Loading'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Peer = typeof window !== 'undefined' ? require('peerjs').default : null
export type WebRTCValue = PeerType | null
export type WebRTCValue = PeerType
const ICE_SERVERS: RTCConfiguration = {
iceServers: [

Loading…
Cancel
Save