redesign
This commit is contained in:
@@ -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
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 798 KiB |
+87
-47
@@ -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 />
|
||||
<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>
|
||||
<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>
|
||||
<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> <a href="https://github.com/klemek" target="_blank">klemek</a>
|
||||
-
|
||||
<i icon="github"></i> <a href="https://github.com/klemek/file-whizz" target="_blank">Repository</a>
|
||||
- 2025
|
||||
</small>
|
||||
</main>
|
||||
-->
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -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
Binary file not shown.
|
After Width: | Height: | Size: 144 KiB |
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user