This commit is contained in:
Klemek
2025-03-19 10:55:58 +01:00
parent d9f73759eb
commit 55573328bc
6 changed files with 157 additions and 276 deletions
+1 -6
View File
@@ -3,11 +3,6 @@
### [Tool link](https://klemek.github.io/file-whizz/)
## Tips
* [Material design colors](https://materialui.co/colors/) are available, you can use `class="red-500"` on your HTML
* [Lucide icons](https://lucide.dev/icons) are available, you can use `<i icon=house></i>` on your HTM
## Roadmap
* [x] Clean project
@@ -18,6 +13,6 @@
* [x] See download speed
* [ ] Fix turn server settings to work on all servers
* [ ] Fix edge cases
* [ ] DaisyUI redesign
* [x] DaisyUI redesign
* [ ] QRCode
* [ ] Multiple files
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 798 KiB

+88 -48
View File
@@ -8,6 +8,9 @@
<link rel="stylesheet" href="material-colors.css" />
<script src="https://cdn.jsdelivr.net/npm/peerjs@1.5.4/dist/peerjs.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lucide@0/dist/umd/lucide.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" />
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<link href="https://cdn.jsdelivr.net/npm/daisyui@5/themes.css" rel="stylesheet" type="text/css" />
<script type="importmap">
{
"imports": {
@@ -19,30 +22,93 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta property="og:title" content="File Whizz">
<meta property="og:description" content="No need for cloud when you have wings">
<meta property="og:url" content="https://file.klemek.fr">
<meta property="og:image" content="https://file.klemek.fr/preview.jpg">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
</head>
<body>
<main id="app" style="display: none">
<h1>
<i icon="file-volume-2"></i>
File Whizz
</h1>
<br />
<div>
<label>Status</label><br>
<input :value="statusText" disabled />
<br>
<br>
<template v-if="serverIsReady">
<input disabled :value="server.url" />
<input type="submit" @click.prevent="onShare" :value="shareText" />
<br>
<br>
</template>
<body data-theme="night" class="min-h-screen flex flex-col">
<div class="hero flex-grow bg-base-200">
<main id="app" class="hero-content text-center" style="display: none">
<div class="card w-96 bg-base-100/95 card-xl shadow-sm">
<div class="card-body">
<h2 class="card-title text-4xl"><i icon="file-volume-2"></i> File Whizz</h2>
<div v-if="isServer" class="mt-2 w-full italic">
Send files easily through <a class="underline" target="_blank" href="https://webrtc.org/">WebRTC</a>
directly to another device, without an intermediary server.
</div>
<div v-else class="mt-2 w-full">
Receive files easily through <a class="underline" target="_blank" href="https://webrtc.org/">WebRTC</a>
directly from another device, without an intermediary server.
</div>
<div class="mt-2 w-full">
<div class="inline-grid *:[grid-area:1/1] align-middle mr-1">
<div :class="`status status-${statusColor(status)} status-lg animate-ping`"></div>
<div :class="`status status-${statusColor(status)} status-lg`"></div>
</div> {{ status }}
</div>
<div v-if="error" class="mt-2 text-error">
{{ error }}
</div>
<div v-if="fileName" class="mt-2 w-full">
<code>{{ prettyFileSize }} - {{ fileName }}</code>
</div>
<template v-if="isServer">
<div v-if="!server.data" class="my-t w-full">
<label for="file-input" class="btn btn-primary w-full"><i icon="file-up"></i> Select file</label>
<input @change="onFileChange" id="file-input" type="file" class="hidden" />
</div>
<div v-if="serverIsReady" @click="onShare" class="mt-2 w-full btn btn-primary">
<i icon="share-2"></i> {{ shareText }}
</div>
<ul v-if="serverIsReady && server.clients.length"
class="mt-2 list bg-base-100 rounded-box shadow-md text-left">
<li class="p-4 pb-2 text-xs opacity-60 tracking-wide">Connected peers</li>
<li class="list-row" v-for="client in server.clients" v-bind:key="client.id">
<div class="list-col-grow">
<div>{{ client.userAgent ?? client.id }}</div>
<div>
<div :class="`status status-${statusColor(client.status)}`"></div> {{ client.status }}
</div>
<progress :value="client.sent" :max="downloadTotal" aria-busy="!client.connected" class="progress"
:class="client.sent >=downloadTotal ? 'progress-success' : 'progress-primary'"></progress>
</div>
</li>
</ul>
</template>
<template v-else>
<div @click="onDownload" :class="readyToDownload ? 'btn-primary' : 'btn-disabled'" class="mt-2 w-full btn">
<i icon="file-down"></i> Download
</div>
<div v-if="downloading || client.downloadEnd" class="w-full">
<progress :class="client.downloadEnd ? 'progress-success' : 'progress-primary'" :value="downloadProgress"
:max="downloadTotal" class="progress w-full"></progress>
<label>{{ prettyDownloadSpeed }}<span v-if="!client.downloadEnd"> - {{ prettyRemainingTime
}}</span></label>
</div>
</template>
</div>
</div>
</main>
</div>
<footer class="footer sm:footer-horizontal footer-center bg-base-300 text-base-content p-4">
<aside>
<p>
2025 - Made by
<i icon="at-sign"></i><a href="https://github.com/klemek" target="_blank" class="underline">klemek</a>
(<i icon="github"></i> <a href="https://github.com/klemek/file-whizz" target="_blank"
class="underline">Repository</a>) -
Photo by
<a href="https://unsplash.com/photos/low-angle-photography-of-highrise-building-p-rN-n6Miag"
class="underline">JOHN TOWNER</a>
on <a href="https://unsplash.com/" target="_blank" class="underline">Unsplash</a>
</p>
</aside>
</footer>
<!--
<div>
<template v-if="isServer">
<input type="file" @change="onFileChange" :disabled="server.data" />
<br>
<br>
<template v-for="client in server.clients">
<label>{{ client.userAgent ?? client.id }} - {{ client.status }}</label><br>
<progress :value="client.sent" :max="downloadTotal" aria-busy="!client.connected"></progress>
@@ -50,34 +116,8 @@
<br>
</template>
</template>
<template v-else>
<label v-if="fileName">{{ prettyFileSize }} - "{{ fileName }}"</label><br>
<template v-if="readyToDownload">
<input type="submit" @click.prevent="onDownload" value="Download" />
<br>
<br>
</template>
<template v-if="downloading">
<progress :value="downloadProgress" :max="downloadTotal"></progress><br>
<label>{{ prettyDownloadSpeed }}</label><label v-if="!client.downloadEnd"> - {{ prettyRemainingTime }}</label>
<br>
<br>
</template>
</template>
<template v-if="error">
<span class="red">{{error}}</span>
<br>
<br>
</template>
</div>
<br>
<small class="footer">
<i icon="at-sign"></i>&nbsp;<a href="https://github.com/klemek" target="_blank">klemek</a>
-
<i icon="github"></i>&nbsp;<a href="https://github.com/klemek/file-whizz" target="_blank">Repository</a>
- 2025
</small>
</main>
-->
</body>
</html>
+64 -21
View File
@@ -77,6 +77,32 @@ const MESSAGE_TYPE = {
ClientDone: "client-done",
};
const STATUS = {
Error: "Error",
Connecting: "Acquiring ID...",
ServerNoFile: "Online",
ServerReady: "Ready to send file",
ClientConnecting: "Connecting to peer...",
ClientWaiting: "Waiting for file info...",
ClientReady: "Ready to download",
ClientDownloading: "Downloading file...",
ClientDownloaded: "File downloaded",
ClientDisconnected: "Disconnected",
};
const STATUS_COLOR = {
[STATUS.Error]: "error",
[STATUS.Connecting]: "neutral",
[STATUS.ServerNoFile]: "primary",
[STATUS.ServerReady]: "success",
[STATUS.ClientConnecting]: "primary",
[STATUS.ClientWaiting]: "primary",
[STATUS.ClientReady]: "success",
[STATUS.ClientDownloading]: "primary",
[STATUS.ClientDownloaded]: "success",
[STATUS.ClientDisconnected]: "neutral",
};
const MAX_CHUNK_SIZE = 12 * 1024; // 12 KB
const app = createApp({
@@ -86,6 +112,7 @@ const app = createApp({
fileName: null,
fileSize: null,
localId: null,
error: null,
isServer: true,
server: {
@@ -103,7 +130,6 @@ const app = createApp({
received: [],
buffer: null, // TODO multiple files
},
error: null,
};
},
computed: {
@@ -111,13 +137,19 @@ const app = createApp({
return this.peer !== null && this.localId !== null;
},
readyToDownload() {
return this.client.buffer !== null && !this.client.downloadStart;
return (
this.client.connection !== null &&
this.client.buffer !== null &&
this.client.downloadStart === null
);
},
serverIsReady() {
return this.canConnect && this.server.data !== null;
},
downloading() {
return this.client.downloadStart !== null;
return (
this.client.downloadStart !== null && this.client.downloadEnd === null
);
},
downloadProgress() {
return this.client.received.length * MAX_CHUNK_SIZE;
@@ -134,32 +166,35 @@ const app = createApp({
}
return "Copy link";
},
statusText() {
status() {
if (this.error) {
return this.error;
return STATUS.Error;
}
if (!this.canConnect) {
return "Acquiring ID...";
return STATUS.Connecting;
}
if (this.isServer) {
if (!this.server.data) {
return "Waiting for file upload";
return STATUS.ServerNoFile;
}
return "Ready to send file";
return STATUS.ServerReady;
}
if (!this.client.connected) {
return "Connecting to peer...";
return STATUS.ClientConnecting;
}
if (this.readyToDownload) {
return "Ready to download";
}
if (!this.downloading) {
return "Waiting for file info...";
if (!this.client.connection) {
return STATUS.ClientDisconnected;
}
if (this.client.downloadEnd) {
return "File downloaded";
return STATUS.ClientDownloaded;
}
return "Downloading...";
if (this.readyToDownload) {
return STATUS.ClientReady;
}
if (!this.downloading) {
return STATUS.ClientWaiting;
}
return STATUS.ClientDownloading;
},
prettyFileSize() {
return utils.prettyBytes(this.fileSize);
@@ -254,7 +289,7 @@ const app = createApp({
done: false,
sent: 0,
connected: false,
status: "Connecting...",
status: STATUS.ClientConnecting,
userAgent: null,
};
if (index === -1) {
@@ -296,6 +331,9 @@ const app = createApp({
this.peer.connect(this.client.remoteId, { reliable: false })
);
},
statusColor(status) {
return STATUS_COLOR[status];
},
// PEER EVENTS
onPeerOpen(id) {
console.log("onPeerOpen", id);
@@ -331,7 +369,7 @@ const app = createApp({
onServerConnectionOpen(index) {
console.log("onServerConnectionOpen", index);
this.server.clients[index].connected = true;
this.server.clients[index].status = "Connected";
this.server.clients[index].status = STATUS.ClientReady;
this.sendServerInfo(index);
},
onServerConnectionData(index, data) {
@@ -353,11 +391,11 @@ const app = createApp({
},
onServerConnectionClose(index) {
console.log("onServerConnectionClose", index);
this.server.clients[index].status = "Disconnected";
this.server.clients[index].status = STATUS.ClientDisconnected;
},
onServerConnectionError(index, err) {
console.log("onServerConnectionError", index, err);
this.server.clients[index].status = "Error";
this.server.clients[index].status = STATUS.Error;
},
// CLIENT CONNECTION EVENTS
onClientConnectionOpen() {
@@ -455,6 +493,7 @@ const app = createApp({
});
},
handleClientSeek(index, data) {
this.server.clients[index].status = STATUS.ClientDownloading;
if (data.indexes) {
data.indexes.forEach((chunkIndex) => {
setTimeout(() => this.sendServerChunk(index, chunkIndex));
@@ -478,7 +517,7 @@ const app = createApp({
handleClientDone(index) {
this.server.clients[index].connection.close();
this.server.clients[index].done = true;
this.server.clients[index].status = "Done";
this.server.clients[index].status = STATUS.ClientDisconnected;
},
// UI EVENTS
onFileChange(event) {
@@ -499,7 +538,11 @@ const app = createApp({
reader.readAsArrayBuffer(file);
},
onDownload() {
if (!this.readyToDownload) {
return;
}
this.client.downloadStart = new Date();
this.client.downloadEnd = null;
this.sendClientSeek();
},
onShare() {
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

+5 -202
View File
@@ -1,199 +1,3 @@
/*
=================================================
https://www.joshwcomeau.com/css/custom-css-reset/
=================================================
*/
/*
1. Use a more-intuitive box-sizing model.
*/
*,
*::before,
*::after {
box-sizing: border-box;
}
/*
2. Remove default margin
*/
* {
margin: 0;
}
/*
3. Allow percentage-based heights in the application
*/
html,
body {
height: 100%;
}
/*
Typographic tweaks!
4. Add accessible line-height
5. Improve text rendering
*/
body {
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
/*
6. Improve media defaults
*/
img,
picture,
video,
canvas,
svg {
display: block;
max-width: 100%;
}
/*
7. Remove built-in form typography styles
*/
input,
button,
textarea,
select {
font: inherit;
}
/*
8. Avoid text overflows
*/
p,
h1,
h2,
h3,
h4,
h5,
h6 {
overflow-wrap: break-word;
}
/*
9. Create a root stacking context
*/
#root,
#__next {
isolation: isolate;
}
/*
=================================================
CUSTOM STYLE
=================================================
*/
@import url("https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap");
:root {
/* https://materialui.co/colors/ */
--hue-primary: 65.52;
--sat-primary: 20%;
--background: hsl(var(--hue-primary), var(--sat-primary), 96.08%);
--background-primary: hsl(var(--hue-primary), var(--sat-primary), 93.33%);
--background-secondary: hsl(var(--hue-primary), var(--sat-primary), 90%);
--color-primary: hsl(var(--hue-primary), var(--sat-primary), 50%);
--text-primary: hsl(var(--hue-primary), var(--sat-primary), 25%);
--text-secondary: hsl(var(--hue-primary), var(--sat-primary), 30%);
}
/*
=================================================
https://blog.koley.in/2019/339-bytes-of-responsive-css
https://www.swyx.io/css-100-bytes
https://gist.github.com/JoeyBurzynski/617fb6201335779f8424ad9528b72c41
=================================================
*/
html,
body {
padding: 0;
max-width: 100%;
color: var(--text-primary);
font-family: "Roboto", Verdana, serif;
}
body {
background-color: var(--background);
}
main {
padding: 1.5rem;
margin: auto;
background-color: var(--background-primary);
min-height: 100%;
}
table {
border-collapse: collapse;
width: 100%;
font-size: 0.9em;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 1em 0 0.5em;
}
p,
ul,
ol {
margin-bottom: 2em;
color: var(--text-secondary);
}
textarea,
input,
select,
.mono {
font-family: "Roboto Mono", monospace;
}
textarea {
width: 100%;
min-width: 100%;
max-width: 100%;
}
a {
color: inherit;
}
@media only screen and (min-width: 768px) {
main {
max-width: 42rem;
}
table {
font-size: inherit;
}
}
/*
=================================================
APP STYLE
=================================================
*/
.button {
display: block;
width: 100%;
text-decoration: none;
padding: 1em;
margin-bottom: 0.75em;
border: 1px solid var(--color-primary);
border-radius: 0.5em;
background-color: var(--background);
cursor: pointer;
font-size: 1.333em;
}
.button:hover {
background-color: var(--background-secondary);
}
svg.lucide {
display: inline-block;
vertical-align: text-top;
@@ -206,14 +10,13 @@ h3 .lucide,
h4 .lucide,
h5 .lucide,
h6 .lucide {
stroke-width: 3;
stroke-width: 2;
}
.footer {
opacity: 50%;
.bg-base-100\/95 {
background-color: color-mix(in oklab, var(--color-base-100) 95%, #0000);
}
input,
progress {
width: 100%;
.hero {
background-image: url(./background.jpg);
}