Merge pull request #34 from mollersuite/notifs

This commit is contained in:
Jack W 2023-10-14 16:42:25 -04:00 committed by GitHub
commit cbb9152529
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 296 additions and 147 deletions

4
.vscode/tasks.json vendored
View file

@ -3,7 +3,7 @@
"tasks": [
{
"type": "shell",
"command":"tsc\nsass src/style:out/style\nrollup -c",
"command":"npm run build",
"group": {
"kind": "build",
"isDefault": true
@ -12,7 +12,7 @@
},
{
"type": "shell",
"command":"tsc\nsass src/style:out/style\nrollup -c\nnode ./out/server/index.js\ndel ./out/* -Recurse",
"command":"npm run build\nnode ./out/server/index.js\ndel ./out/* -Recurse",
"group": {
"kind": "build",
"isDefault": true

39
package-lock.json generated
View file

@ -1,17 +1,14 @@
{
"name": "monofile",
"version": "1.3.0-beta",
"version": "1.4.0-dev",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "monofile",
"version": "1.3.0-beta",
"version": "1.4.0-dev",
"license": "Unlicense",
"dependencies": {
"@fontsource/fira-code": "^5.0.8",
"@fontsource/inconsolata": "^5.0.8",
"@fontsource/source-sans-pro": "^5.0.8",
"@types/body-parser": "^1.19.2",
"@types/express": "^4.17.14",
"@types/multer": "^1.4.7",
@ -37,7 +34,7 @@
"svelte": "^3.55.1"
},
"engines": {
"node": ">=v18"
"node": ">=v16.11"
}
},
"node_modules/@discordjs/builders": {
@ -90,21 +87,6 @@
"node": ">=16.9.0"
}
},
"node_modules/@fontsource/fira-code": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/@fontsource/fira-code/-/fira-code-5.0.8.tgz",
"integrity": "sha512-kp/tJUVnjaZeLHENMBFTTSgP2B7+/rIboeofuMfoGB40s2U0DKXNqQcOqIF5PtDhJ5QTG1LcviYXMnc1yG6oYQ=="
},
"node_modules/@fontsource/inconsolata": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/@fontsource/inconsolata/-/inconsolata-5.0.8.tgz",
"integrity": "sha512-KpBU6q1yCovfycaFprVEauh8U5RsWty3konFfUukyRRxZBK4Sf73XmGQc8iJ4CPrOP4dplGfdX2kjbRgdymajA=="
},
"node_modules/@fontsource/source-sans-pro": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/@fontsource/source-sans-pro/-/source-sans-pro-5.0.8.tgz",
"integrity": "sha512-5U2UvIYRkCMozZ388gCE73PEpa2MFgN/0t9O4a1FF7bGT/MIneQWSL1XpWZ8iMVYdh6ntxRf3iFA6slCIuFgkg=="
},
"node_modules/@rollup/plugin-node-resolve": {
"version": "15.0.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.0.1.tgz",
@ -1750,21 +1732,6 @@
"resolved": "https://registry.npmjs.org/@discordjs/util/-/util-0.1.0.tgz",
"integrity": "sha512-e7d+PaTLVQav6rOc2tojh2y6FE8S7REkqLldq1XF4soCx74XB/DIjbVbVLtBemf0nLW77ntz0v+o5DytKwFNLQ=="
},
"@fontsource/fira-code": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/@fontsource/fira-code/-/fira-code-5.0.8.tgz",
"integrity": "sha512-kp/tJUVnjaZeLHENMBFTTSgP2B7+/rIboeofuMfoGB40s2U0DKXNqQcOqIF5PtDhJ5QTG1LcviYXMnc1yG6oYQ=="
},
"@fontsource/inconsolata": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/@fontsource/inconsolata/-/inconsolata-5.0.8.tgz",
"integrity": "sha512-KpBU6q1yCovfycaFprVEauh8U5RsWty3konFfUukyRRxZBK4Sf73XmGQc8iJ4CPrOP4dplGfdX2kjbRgdymajA=="
},
"@fontsource/source-sans-pro": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/@fontsource/source-sans-pro/-/source-sans-pro-5.0.8.tgz",
"integrity": "sha512-5U2UvIYRkCMozZ388gCE73PEpa2MFgN/0t9O4a1FF7bGT/MIneQWSL1XpWZ8iMVYdh6ntxRf3iFA6slCIuFgkg=="
},
"@rollup/plugin-node-resolve": {
"version": "15.0.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.0.1.tgz",

View file

@ -5,10 +5,11 @@
"main": "index.js",
"scripts": {
"start": "node ./out/server/index.js",
"build": "tsc\nsass src/style:out/style\nrollup -c",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "Etcetera (https://cetera.uk)",
"author": "Etcetera (https://cetera.uk)",
"license": "Unlicense",
"engines": {
"node": ">=v16.11"

View file

@ -23,6 +23,20 @@
font-size: 14px;
}
h1 > button {
background-color: #0000 !important;
padding: 0;
margin-top: 5px;
img {
color: #666666;
}
&:hover {
img { color: #DDD; }
}
}
a {
color:#999;
}

View file

@ -1,22 +1,22 @@
<script>
import { _void } from "./transition/_void.js";
import { _void } from "./transition/_void.js"
import { padding_scaleY } from "./transition/padding_scaleY.js"
import { fade } from "svelte/transition";
import { circIn, circOut } from "svelte/easing";
import { serverStats, refresh_stats, account } from "./stores.mjs";
import { fade } from "svelte/transition"
import { circIn, circOut } from "svelte/easing"
import { serverStats, refresh_stats, account } from "./stores.mjs"
import AttachmentZone from "./uploader/AttachmentZone.svelte";
import AttachmentZone from "./uploader/AttachmentZone.svelte"
// stats
refresh_stats()
// uploads
let attachmentZone;
let uploads = {};
let uploadInProgress = false;
let attachmentZone
let uploads = {}
let uploadInProgress = false
let notificationPermission = Notification.permission
let handle_file_upload = (ev) => {
if (ev.detail.type == "clone") {
uploads[Math.random().toString().slice(2)] = {
@ -25,31 +25,31 @@
url: ev.detail.url,
params: {
uploadId: ""
uploadId: "",
},
uploadStatus:{
uploadStatus: {
fileId: null,
error: null,
}
},
}
uploads = uploads
} else if (ev.detail.type == "upload") {
ev.detail.files.forEach((v,x) => {
ev.detail.files.forEach((v, x) => {
uploads[Math.random().toString().slice(2)] = {
type: "upload",
name: v.name,
file: v,
params: {
uploadId: ""
uploadId: "",
},
uploadStatus:{
uploadStatus: {
fileId: null,
error: null,
}
},
}
})
@ -57,18 +57,48 @@
}
}
let handle_fetch_promise = (x,prom) => {
return prom.then(async (res) => {
let txt = await res.text()
if (txt.startsWith("[err]")) uploads[x].uploadStatus.error = txt;
else {
uploads[x].uploadStatus.fileId = txt;
refresh_stats();
}
}).catch((err) => {
uploads[x].uploadStatus.error = err.toString();
})
let handle_fetch_promise = (x, prom) => {
return prom
.then(async (res) => {
let txt = await res.text()
if (txt.startsWith("[err]")) uploads[x].uploadStatus.error = txt
else {
uploads[x].uploadStatus.fileId = txt
try {
new Notification("Upload complete", {
body: `View at ${location.origin}/${uploads[x].uploadStatus.fileId}`,
actions: [
{
action: "open",
title: "Open",
},
{
action: "copy",
title: "Copy",
},
],
}).addEventListener(
"notificationclick",
({ action }) => {
if (action === "open") {
open(
"/download/" +
uploads[x].uploadStatus.fileId
)
} else {
navigator.clipboard.writeText(
`${location.origin}/${uploads[x].uploadStatus.fileId}`
)
}
}
)
} catch (_) {}
refresh_stats()
}
})
.catch((err) => {
uploads[x].uploadStatus.error = err.toString()
})
}
let upload_files = async () => {
@ -77,112 +107,224 @@
let sequential = localStorage.getItem("sequentialMode") == "true"
// go through all files
for (let [x,v] of Object.entries(uploads)) {
for (let [x, v] of Object.entries(uploads)) {
// quick patch-in to allow for a switch to have everything upload sequentially
// switch will have a proper menu option later, for now i'm lazy so it's just gonna be a Secret
let hdl = () => {
switch(v.type) {
switch (v.type) {
case "upload":
let fd = new FormData()
fd.append("file",v.file)
fd.append("file", v.file)
return handle_fetch_promise(x,fetch("/upload",{
headers: {
"monofile-params": JSON.stringify(v.params)
},
method: "POST",
body: fd
}))
break
case "clone":
return handle_fetch_promise(x,fetch("/clone",{
method: "POST",
body: JSON.stringify({
url: v.url,
...v.params
return handle_fetch_promise(
x,
fetch("/upload", {
headers: {
"monofile-params": JSON.stringify(v.params),
},
method: "POST",
body: fd,
})
}))
break
)
break
case "clone":
return handle_fetch_promise(
x,
fetch("/clone", {
method: "POST",
body: JSON.stringify({
url: v.url,
...v.params,
}),
})
)
break
}
}
if (sequential) await hdl();
else hdl();
if (sequential) await hdl()
else hdl()
}
}
// animation
function fileTransition(node) {
return {
duration: 300,
css: t => {
css: (t) => {
let eased = circOut(t)
return `
height: ${eased*(node.offsetHeight-22)}px;
padding: ${eased*10}px 10px;
height: ${eased * (node.offsetHeight - 22)}px;
padding: ${eased * 10}px 10px;
`
}
},
}
}
</script>
<div id="uploadWindow">
<h1>monofile</h1>
<h1>
monofile
{#if notificationPermission === "default"}
<button
on:click={() => {
Notification.requestPermission().then(
(permission) => (notificationPermission = permission)
)
}}
style="float:right"
title="Notify me when the upload finishes"
>
<svg
width="20"
height="20"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-label="Notify me when the upload finishes"
><path
d="M9.042 19.003h5.916a3 3 0 0 1-5.916 0Zm2.958-17a7.5 7.5 0 0 1 7.5 7.5v4l1.418 3.16A.95.95 0 0 1 20.052 18h-16.1a.95.95 0 0 1-.867-1.338l1.415-3.16V9.49l.005-.25A7.5 7.5 0 0 1 12 2.004Z"
fill="currentColor"
/></svg
>
</button>
{/if}
</h1>
<p style:color="#999999">
<span class="number">{$serverStats.version ? `v${$serverStats.version}` : "•••"}</span>&nbsp;&nbsp;&nbsp;&nbsp;Discord based file sharing
<span class="number"
>{$serverStats.version ? `v${$serverStats.version}` : "•••"}</span
>&nbsp;&nbsp;&nbsp;&nbsp;Discord based file sharing
</p>
<div style:min-height="10px" />
<!-- consider splitting the file thing into a separate element maybe -->
<div class="uploadContainer">
{#each Object.entries(uploads) as upload (upload[0])}
<!-- container to allow for animate directive -->
<div>
<div class="file" transition:fileTransition style:border={upload[1].uploadStatus.error ? "1px solid #BB7070" : ""}>
<h2>{upload[1].name} <span style:color="#999999" style:font-weight="400">{upload[1].type}{@html upload[1].type == "upload" ? `&nbsp;(${Math.round(upload[1].file.size/1048576)}MiB)` : ""}</span></h2>
<div
class="file"
transition:fileTransition
style:border={upload[1].uploadStatus.error
? "1px solid #BB7070"
: ""}
>
<h2>
{upload[1].name}
<span style:color="#999999" style:font-weight="400"
>{upload[1].type}{@html upload[1].type == "upload"
? `&nbsp;(${Math.round(
upload[1].file.size / 1048576
)}MiB)`
: ""}</span
>
</h2>
{#if upload[1].maximized && !uploadInProgress}
<div transition:padding_scaleY|local>
<div style:height="10px" />
<input placeholder="custom id" type="text" bind:value={ uploads[upload[0]].params.uploadId }>
<input
placeholder="custom id"
type="text"
bind:value={uploads[upload[0]].params.uploadId}
/>
<div style:height="10px" />
<div class="buttonContainer">
<button on:click={() => {delete uploads[upload[0]];uploads=uploads;}}>
<button
on:click={() => {
delete uploads[upload[0]]
uploads = uploads
}}
>
delete
</button>
<button on:click={() => uploads[upload[0]].maximized = false}>
<button
on:click={() =>
(uploads[upload[0]].maximized = false)}
>
minimize
</button>
</div>
</div>
{:else if !uploadInProgress}
<button on:click={() => uploads[upload[0]].maximized = true} class="hitbox"></button>
<button
on:click={() =>
(uploads[upload[0]].maximized = true)}
class="hitbox"
/>
{:else}
<div transition:padding_scaleY|local class="uploadingContainer">
<div
transition:padding_scaleY|local
class="uploadingContainer"
>
{#if !upload[1].uploadStatus.fileId}
<p in:fade={{duration:300, delay:400, easingFunc:circOut}} out:padding_scaleY={{easingFunc:circIn,op:true}}>{upload[1].uploadStatus.error ?? "Uploading..."}</p>
<p
in:fade={{
duration: 300,
delay: 400,
easingFunc: circOut,
}}
out:padding_scaleY={{
easingFunc: circIn,
op: true,
}}
>
{upload[1].uploadStatus.error ??
"Uploading..."}
</p>
{/if}
{#if upload[1].uploadStatus.fileId}
<div style:height="10px" transition:padding_scaleY />
<div
style:height="10px"
transition:padding_scaleY
/>
{#if !upload[1].viewingUrl}
<div class="buttonContainer" out:_void in:_void={{easingFunc:circOut}}>
<button on:click={() => uploads[upload[0]].viewingUrl = true}>
<div
class="buttonContainer"
out:_void
in:_void={{ easingFunc: circOut }}
>
<button
on:click={() =>
(uploads[
upload[0]
].viewingUrl = true)}
>
view url
</button>
<button on:click={() => navigator.clipboard.writeText(`https://${window.location.host}/download/${upload[1].uploadStatus.fileId}`)}>
<button
on:click={() =>
navigator.clipboard.writeText(
`https://${window.location.host}/download/${upload[1].uploadStatus.fileId}`
)}
>
copy url
</button>
</div>
{:else}
<div class="buttonContainer" out:_void in:_void={{easingFunc:circOut}}>
<input type="text" readonly value={`https://${window.location.host}/download/${upload[1].uploadStatus.fileId}`} style:flex-basis="80%">
<button on:click={() => uploads[upload[0]].viewingUrl = false} style:flex-basis="20%">
<div
class="buttonContainer"
out:_void
in:_void={{ easingFunc: circOut }}
>
<input
type="text"
readonly
value={`https://${window.location.host}/download/${upload[1].uploadStatus.fileId}`}
style:flex-basis="80%"
/>
<button
on:click={() =>
(uploads[
upload[0]
].viewingUrl = false)}
style:flex-basis="20%"
>
ok
</button>
</div>
@ -195,48 +337,73 @@
</div>
{/each}
</div>
{#if uploadInProgress == false}
<!-- if required for upload, check if logged in -->
{#if ($serverStats.accounts||{}).requiredForUpload ? !!$account.username : true}
<AttachmentZone bind:this={attachmentZone} on:addFiles={handle_file_upload}/>
<div style:min-height="10px" transition:_void={{rTarg:"height",prop:"min-height"}} />
{#if ($serverStats.accounts || {}).requiredForUpload ? !!$account.username : true}
<AttachmentZone
bind:this={attachmentZone}
on:addFiles={handle_file_upload}
/>
<div
style:min-height="10px"
transition:_void={{ rTarg: "height", prop: "min-height" }}
/>
{#if Object.keys(uploads).length > 0}
<button in:padding_scaleY={{easingFunc:circOut}} out:_void on:click={upload_files}>upload</button>
<div transition:_void={{rTarg:"height",prop:"min-height"}} style:min-height="10px" />
<button
in:padding_scaleY={{ easingFunc: circOut }}
out:_void
on:click={upload_files}>upload</button
>
<div
transition:_void={{ rTarg: "height", prop: "min-height" }}
style:min-height="10px"
/>
{/if}
{:else}
<p transition:_void style:color="#999999" style:text-align="center">Please log in to upload files.</p>
<div transition:_void={{rTarg:"height",prop:"min-height"}} style:min-height="10px" />
<p transition:_void style:color="#999999" style:text-align="center">
Please log in to upload files.
</p>
<div
transition:_void={{ rTarg: "height", prop: "min-height" }}
style:min-height="10px"
/>
{/if}
{/if}
<p style:color="#999999" style:text-align="center">
Hosting <span class="number" style:font-weight="600">{$serverStats.files || "•••"}</span> files
Maximum filesize is <span class="number" style:font-weight="600">{(($serverStats.maxDiscordFileSize || 0)*($serverStats.maxDiscordFiles || 0))/1048576 || "•••"}MiB</span>
Hosting <span class="number" style:font-weight="600"
>{$serverStats.files || "•••"}</span
>
files — Maximum filesize is
<span class="number" style:font-weight="600"
>{(($serverStats.maxDiscordFileSize || 0) *
($serverStats.maxDiscordFiles || 0)) /
1048576 || "•••"}MiB</span
>
<br />
</p>
<p style:color="#999999" style:text-align="center" style:font-size="12px">
Made with {Math.floor(Math.random()*10)==0 ? "🐟" : "❤"} by <a href="https://cetera.uk" style:font-size="12px"><svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 173.8 275.72"
height="16"
style="vertical-align:middle"
fill="currentColor">
<circle cx="37.13" cy="26.43" r="21.6" />
<circle cx="34.62" cy="117.87" r="34.62" class="middle" />
<circle cx="119.78" cy="130.68" r="21.6" class="middle" />
<circle cx="127.16" cy="46.64" r="46.65" />
<circle cx="102.68" cy="219.58" r="56.14" class="bottom" />
</svg> Etcetera</a><a href="https://github.com/mollersuite/monofile" style:font-size="12px">source</a>
Made with {Math.floor(Math.random() * 10) == 0 ? "🐟" : "❤"} by
<a href="https://cetera.uk" style:font-size="12px"
><svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 173.8 275.72"
height="16"
style="vertical-align:middle"
fill="currentColor"
>
<circle cx="37.13" cy="26.43" r="21.6" />
<circle cx="34.62" cy="117.87" r="34.62" class="middle" />
<circle cx="119.78" cy="130.68" r="21.6" class="middle" />
<circle cx="127.16" cy="46.64" r="46.65" />
<circle cx="102.68" cy="219.58" r="56.14" class="bottom" />
</svg> Etcetera</a
>
<a href="https://github.com/mollersuite/monofile" style:font-size="12px"
>source</a
>
</p>
<div style:height="10px" />
</div>
</div>