Big redesign. Use bulma
3
Makefile
|
@ -30,7 +30,9 @@ clean:
|
||||||
rm -rf lufi.db files/
|
rm -rf lufi.db files/
|
||||||
|
|
||||||
dev: clean
|
dev: clean
|
||||||
|
deno task watch&
|
||||||
$(CARTON) morbo $(LUFI) --listen http://$(MORBO_HOST):$(MORBO_PORT) --watch lib/ --watch script/ --watch themes/ --watch lufi.conf
|
$(CARTON) morbo $(LUFI) --listen http://$(MORBO_HOST):$(MORBO_PORT) --watch lib/ --watch script/ --watch themes/ --watch lufi.conf
|
||||||
|
|
||||||
|
|
||||||
ldap:
|
ldap:
|
||||||
podman run -d -p $(LOCAL_LDAP_PORT):10389 $(LDAP_CONTAINER_IMAGE); exit 0
|
podman run -d -p $(LOCAL_LDAP_PORT):10389 $(LDAP_CONTAINER_IMAGE); exit 0
|
||||||
|
@ -49,4 +51,5 @@ devlog:
|
||||||
multitail log/development.log
|
multitail log/development.log
|
||||||
|
|
||||||
prod:
|
prod:
|
||||||
|
deno task build
|
||||||
$(CARTON) hypnotoad -f $(LUFI)
|
$(CARTON) hypnotoad -f $(LUFI)
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
import {buildCSS, buildJS} from "./bundler.ts"
|
||||||
|
|
||||||
|
console.debug("Building css...")
|
||||||
|
await buildCSS.rebuild();
|
||||||
|
buildCSS.dispose();
|
||||||
|
|
||||||
|
console.debug("Building javascript...")
|
||||||
|
await buildJS.rebuild();
|
||||||
|
buildJS.dispose();
|
|
@ -0,0 +1,66 @@
|
||||||
|
// deno-lint-ignore-file
|
||||||
|
import * as esbuild from "npm:esbuild";
|
||||||
|
import { fontawesomeSubset } from "fontawesome-subset";
|
||||||
|
import { denoPlugins } from "jsr:@luca/esbuild-deno-loader";
|
||||||
|
|
||||||
|
const themeDir = "./themes/default";
|
||||||
|
|
||||||
|
fontawesomeSubset(
|
||||||
|
[
|
||||||
|
"circle-plus",
|
||||||
|
"close",
|
||||||
|
"copy",
|
||||||
|
"download",
|
||||||
|
"envelope",
|
||||||
|
"eraser",
|
||||||
|
"eye",
|
||||||
|
"eye-slash",
|
||||||
|
"file",
|
||||||
|
"file-zipper",
|
||||||
|
"globe",
|
||||||
|
"lock",
|
||||||
|
"recycle",
|
||||||
|
"trash",
|
||||||
|
"upload",
|
||||||
|
"user",
|
||||||
|
],
|
||||||
|
`${themeDir}/public/webfonts`,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
const ignoreFontsPlugin: esbuild.Plugin = {
|
||||||
|
name: "file",
|
||||||
|
setup(build) {
|
||||||
|
build.onResolve({ filter: /\.woff2|ttf$/ }, (args) => ({
|
||||||
|
external: true,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildCSS = await esbuild.context({
|
||||||
|
plugins: [ignoreFontsPlugin],
|
||||||
|
entryPoints: [`${themeDir}/public/css/main.css`],
|
||||||
|
outfile: `${themeDir}/public/css/main.min.css`,
|
||||||
|
loader: {
|
||||||
|
".ttf": "file",
|
||||||
|
".woff2": "file",
|
||||||
|
},
|
||||||
|
bundle: true,
|
||||||
|
minify: true,
|
||||||
|
sourcemap: false,
|
||||||
|
allowOverwrite: true,
|
||||||
|
target: ["deno2", "chrome67", "firefox68"],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const buildJS = await esbuild.context({
|
||||||
|
plugins: [...denoPlugins()],
|
||||||
|
entryPoints: [`${themeDir}/public/js/*.js`],
|
||||||
|
outdir: `${themeDir}/public/js/minified`,
|
||||||
|
entryNames: "[dir]/[name].min",
|
||||||
|
bundle: true,
|
||||||
|
minify: false,
|
||||||
|
sourcemap: false,
|
||||||
|
allowOverwrite: true,
|
||||||
|
format: "esm",
|
||||||
|
target: ["deno2", "chrome67", "firefox68"],
|
||||||
|
});
|
|
@ -0,0 +1,5 @@
|
||||||
|
import {buildCSS, buildJS} from "./bundler.ts"
|
||||||
|
|
||||||
|
await buildCSS.watch();
|
||||||
|
await buildJS.watch();
|
||||||
|
console.debug("esbuild is watching...")
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"tasks": {
|
||||||
|
"build": "deno run --allow-all bundler.build.ts",
|
||||||
|
"watch": "deno run --allow-all bundler.watch.ts"
|
||||||
|
},
|
||||||
|
"imports": {
|
||||||
|
"@fortawesome/fontawesome-free": "npm:@fortawesome/fontawesome-free@^6.7.1",
|
||||||
|
"esbuild": "npm:esbuild@^0.24.0",
|
||||||
|
"esbuild-plugin-ignore": "npm:esbuild-plugin-ignore@^1.1.1",
|
||||||
|
"@luca/esbuild-deno-loader": "jsr:@luca/esbuild-deno-loader@^0.11.0",
|
||||||
|
"fontawesome-subset": "npm:fontawesome-subset@^4.4.0",
|
||||||
|
"~/": "./themes/default/public/js/"
|
||||||
|
}
|
||||||
|
}
|
|
@ -306,7 +306,7 @@ sub download {
|
||||||
# Do we need a password?
|
# Do we need a password?
|
||||||
my $valid = 1;
|
my $valid = 1;
|
||||||
if ($c->config('allow_pwd_on_files') && defined($f->{passwd})) {
|
if ($c->config('allow_pwd_on_files') && defined($f->{passwd})) {
|
||||||
if ($f->{passwd} == $json->{file_pwd}) {
|
if ($json->{file_pwd} eq $f->{passwd}) {
|
||||||
$valid = 1;
|
$valid = 1;
|
||||||
} else {
|
} else {
|
||||||
$valid = 0;
|
$valid = 0;
|
||||||
|
|
|
@ -0,0 +1,199 @@
|
||||||
|
/**
|
||||||
|
* Custom CSS Rules
|
||||||
|
* Some are inspired by Signal design
|
||||||
|
*/
|
||||||
|
:root {
|
||||||
|
--bulma-primary-h: 182deg;
|
||||||
|
--bulma-link-h: 182deg;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
--bulma-link-l: 19%;
|
||||||
|
--bulma-link-on-scheme-l: 19%;
|
||||||
|
--bulma-danger-l: 35%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.has-text-primary {
|
||||||
|
--bulma-primary-l: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.is-danger {
|
||||||
|
--bulma-button-background-l-delta: 35%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--bulma-danger-l: 75%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
main {
|
||||||
|
/* Equivalent to hero is-fullheight-with-navbar value */
|
||||||
|
height: calc(100vh - var(--bulma-navbar-height));
|
||||||
|
max-height: calc(100vh - var(--bulma-navbar-height));
|
||||||
|
}
|
||||||
|
|
||||||
|
#section-main .left-zone,
|
||||||
|
#section-main .right-zone,
|
||||||
|
#section-main .left-zone .fixed-grid,
|
||||||
|
#section-main .left-zone .fixed-grid .grid,
|
||||||
|
#section-main .left-zone .file-cta.full-version {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#upload-box {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
--bulma-input-placeholder-color: hsla(var(--bulma-text-h), var(--bulma-text-s), var(--bulma-text-strong-l), .6) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-label .label.disabled {
|
||||||
|
color: var(--bulma-text-weak);
|
||||||
|
}
|
||||||
|
|
||||||
|
a:not(#main-menu a) a:hover {
|
||||||
|
color: hsl(var(--bulma-link-h), var(--bulma-link-s), calc(var(--bulma-link-on-scheme-l) - 15%))
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress::-webkit-progress-value {
|
||||||
|
transition: width 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#modal-language-selector .modal-card-foot {
|
||||||
|
background: var(--bulma-modal-card-body-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#modal-language-selector a.navbar-item:hover {
|
||||||
|
color: var(--bulma-link-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
#main-menu div.navbar-item:hover:has(form) {
|
||||||
|
--bulma-navbar-item-background-l-delta: var(--bulma-navbar-item-hover-background-l-delta);
|
||||||
|
--bulma-navbar-item-background-a: 1;
|
||||||
|
|
||||||
|
background-color: hsla(var(--bulma-navbar-h),
|
||||||
|
var(--bulma-navbar-s),
|
||||||
|
calc(var(--bulma-navbar-item-background-l) + var(--bulma-navbar-item-background-l-delta)),
|
||||||
|
var(--bulma-navbar-item-background-a));
|
||||||
|
}
|
||||||
|
|
||||||
|
#main-menu div.navbar-item form {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
align-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#upload-box .file-cta {
|
||||||
|
border-style: dashed;
|
||||||
|
border-width: 2px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#upload-box #password-control .icon.is-right {
|
||||||
|
pointer-events: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wb-all {
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wb-word {
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
#provided-files {
|
||||||
|
height: 30vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
border-radius: var(--bulma-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
#provided-files .icon-text {
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center
|
||||||
|
}
|
||||||
|
|
||||||
|
#uploaded-files {
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: hsl(var(--bulma-scheme-h), var(--bulma-scheme-s), calc(var(--bulma-scheme-main-ter-l)));
|
||||||
|
border-radius: var(--bulma-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
#file-js-upload {
|
||||||
|
background: hsl(var(--bulma-scheme-h), var(--bulma-scheme-s), calc(var(--bulma-scheme-main-ter-l)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#file-js-upload:has(.full-version:not(.is-hidden)) .file-cta {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%
|
||||||
|
}
|
||||||
|
|
||||||
|
#file-js-upload:has(.small-version:not(.is-hidden)) .grid {
|
||||||
|
align-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input:focus~.file-cta {
|
||||||
|
border-color: hsl(var(--bulma-primary-h), var(--bulma-primary-s), var(--bulma-primary-l));
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
#file-js-upload {
|
||||||
|
background: hsl(var(--bulma-scheme-h), var(--bulma-scheme-s), calc(var(--bulma-scheme-main-ter-l)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifications-container {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification {
|
||||||
|
display: flex;
|
||||||
|
box-shadow: 0 0.1rem 0.3rem 0 rgba(0, 0, 0, 0.16), 0 0.1rem 0.7rem 0 rgba(0, 0, 0, 0.12);
|
||||||
|
width: 30rem;
|
||||||
|
min-height: 3rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: left;
|
||||||
|
position: relative;
|
||||||
|
margin: auto;
|
||||||
|
bottom: 2rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
text-wrap: pretty;
|
||||||
|
animation: fadein 0.5s forwards, fadeout 0.5s 3.5s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadein {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
bottom: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeout {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.deleted {
|
||||||
|
background: repeating-linear-gradient(-45deg, transparent 0 2rem, var(--bulma-danger-95) 2rem 4rem);
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
@import "./fontawesome.min.css";
|
||||||
|
@import "./solid.min.css";
|
||||||
|
@import "./bulma.min.css";
|
||||||
|
@import "./custom.css";
|
|
@ -0,0 +1,6 @@
|
||||||
|
/*!
|
||||||
|
* Font Awesome Free 6.7.1 by @fontawesome - https://fontawesome.com
|
||||||
|
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||||
|
* Copyright 2024 Fonticons, Inc.
|
||||||
|
*/
|
||||||
|
:host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-solid:normal 900 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}.fa-solid,.fas{font-weight:900}
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="m480-240 160-160-56-56-64 64v-168h-80v168l-64-64-56 56 160 160ZM200-120q-33 0-56.5-23.5T120-200v-499q0-14 4.5-27t13.5-24l50-61q11-14 27.5-21.5T250-840h460q18 0 34.5 7.5T772-811l50 61q9 11 13.5 24t4.5 27v499q0 33-23.5 56.5T760-120H200Zm16-600h528l-34-40H250l-34 40Z"/></svg>
|
|
Before Width: | Height: | Size: 347 B |
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M382-240 154-468l57-57 171 171 367-367 57 57-424 424Z"/></svg>
|
|
Before Width: | Height: | Size: 136 B |
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"/></svg>
|
|
Before Width: | Height: | Size: 180 B |
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M360-240q-33 0-56.5-23.5T280-320v-480q0-33 23.5-56.5T360-880h360q33 0 56.5 23.5T800-800v480q0 33-23.5 56.5T720-240H360ZM200-80q-33 0-56.5-23.5T120-160v-560h80v560h440v80H200Z"/></svg>
|
|
Before Width: | Height: | Size: 257 B |
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="m376-300 104-104 104 104 56-56-104-104 104-104-56-56-104 104-104-104-56 56 104 104-104 104 56 56Zm-96 180q-33 0-56.5-23.5T200-200v-520h-40v-80h200v-40h240v40h200v80h-40v520q0 33-23.5 56.5T680-120H280Z"/></svg>
|
|
Before Width: | Height: | Size: 283 B |
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M160-80v-80h640v80H160Zm320-160L200-600h160v-280h240v280h160L480-240Z"/></svg>
|
|
Before Width: | Height: | Size: 152 B |
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="m791-55-91-91q-49 32-104.5 49T480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-60 17-115.5T146-700l-91-91 57-57 736 736-57 57Zm23-205L260-814q49-32 104.5-49T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 60-17 115.5T814-260Z"/></svg>
|
|
Before Width: | Height: | Size: 321 B |
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M440-280h80v-240h-80v240Zm40-320q17 0 28.5-11.5T520-640q0-17-11.5-28.5T480-680q-17 0-28.5 11.5T440-640q0 17 11.5 28.5T480-600Zm0 520q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Z"/></svg>
|
|
Before Width: | Height: | Size: 397 B |
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160H160Zm320-280 320-200v-80L480-520 160-720v80l320 200Z"/></svg>
|
|
Before Width: | Height: | Size: 249 B |
|
@ -0,0 +1,178 @@
|
||||||
|
import { lufi } from "~/lib/lufi.js";
|
||||||
|
import { filesize } from "~/lib/filesize.esm.min.js";
|
||||||
|
import { notify, escapeHtml } from "~/lib/utils.js";
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const passwordFormDOM = document.getElementById("password-form");
|
||||||
|
/**
|
||||||
|
* Update the DOM depending on the situation
|
||||||
|
*
|
||||||
|
* @param {string} type Can be aborted, success or ongoing
|
||||||
|
* @param {Node} existingCard Existing card to be replaced with the new one
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const updateDOM = (type) => {
|
||||||
|
const blockDOM = document
|
||||||
|
.querySelector(`template#block-download-${type}`)
|
||||||
|
.content.cloneNode(true).children[0];
|
||||||
|
|
||||||
|
blockDOM.querySelector(".size").innerText = filesize(
|
||||||
|
blockDOM.querySelector(".size").dataset.filesize
|
||||||
|
);
|
||||||
|
|
||||||
|
if (blockDOM.querySelector(".description").dataset.isZipped === "true") {
|
||||||
|
blockDOM
|
||||||
|
.querySelector(".icon")
|
||||||
|
.classList.replace("fa-file", "fa-file-zipper");
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("download-container").replaceChildren(blockDOM);
|
||||||
|
|
||||||
|
if (type === "success") {
|
||||||
|
document.getElementById("download-button").focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
return blockDOM;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPasswordNeeded = () => passwordFormDOM !== null;
|
||||||
|
|
||||||
|
const onPasswordEvents = () => {
|
||||||
|
passwordFormDOM.onsubmit = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
passwordFormDOM.classList.add("is-hidden");
|
||||||
|
|
||||||
|
startDownload();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const showZipContent = (zipFile) => {
|
||||||
|
const zipContainerDOM = document.getElementById("zip-container");
|
||||||
|
const showZipButtonDOM = document.getElementById("show-zip-button");
|
||||||
|
|
||||||
|
zipContainerDOM.classList.remove("is-hidden");
|
||||||
|
showZipButtonDOM.href = window.location; // Avoid removing the client key from URL when clicking
|
||||||
|
|
||||||
|
showZipButtonDOM.onclick = () => {
|
||||||
|
const zipContentDOM = zipContainerDOM.querySelector(".content");
|
||||||
|
|
||||||
|
document.body.style.cursor = "wait";
|
||||||
|
showZipButtonDOM.style.cursor = "wait";
|
||||||
|
|
||||||
|
lufi
|
||||||
|
.decompress(zipFile)
|
||||||
|
.andThen((job) => job.waitForCompletion())
|
||||||
|
.map((job) => {
|
||||||
|
zipContentDOM.replaceChildren();
|
||||||
|
|
||||||
|
zipContentDOM.classList.remove("has-text-centered");
|
||||||
|
|
||||||
|
job.archiveFiles.forEach((file) => {
|
||||||
|
const itemDOM = document
|
||||||
|
.querySelector("template#card-zipped-item")
|
||||||
|
.content.cloneNode(true).children[0];
|
||||||
|
|
||||||
|
itemDOM.querySelector(".name").innerText = escapeHtml(file.name);
|
||||||
|
itemDOM.querySelector(".size").innerText = filesize(file.size);
|
||||||
|
|
||||||
|
const downloadItemDOM = itemDOM.querySelector(".action-download");
|
||||||
|
|
||||||
|
downloadItemDOM.download = escapeHtml(file.name);
|
||||||
|
downloadItemDOM.href = URL.createObjectURL(file);
|
||||||
|
|
||||||
|
zipContentDOM.append(itemDOM);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.style.cursor = "auto";
|
||||||
|
})
|
||||||
|
.mapErr((error) => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const startDownload = () => {
|
||||||
|
lufi
|
||||||
|
.download(window.location, document.getElementById("password")?.value)
|
||||||
|
.andThen((job) => {
|
||||||
|
const blockDOM = updateDOM("ongoing");
|
||||||
|
|
||||||
|
warnOnReload();
|
||||||
|
job.onProgress(() => {
|
||||||
|
const percent = Math.round(
|
||||||
|
(job.lufiFile.chunksReady * 100) / job.lufiFile.totalChunks
|
||||||
|
);
|
||||||
|
|
||||||
|
blockDOM.querySelector(".progress").value = percent;
|
||||||
|
blockDOM.querySelector(".progress").innerText = percent;
|
||||||
|
blockDOM.querySelector(".progress-text").innerText = percent + "%";
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("abort-button").onclick = () => {
|
||||||
|
job.terminate();
|
||||||
|
|
||||||
|
warnOnReload(false);
|
||||||
|
|
||||||
|
updateDOM("aborted");
|
||||||
|
|
||||||
|
document.getElementById("reload-button").onclick = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
window.location.reload();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return job.waitForCompletion();
|
||||||
|
})
|
||||||
|
.mapErr((error) => {
|
||||||
|
updateDOM("error").querySelector(".message-body").innerText =
|
||||||
|
error.message;
|
||||||
|
|
||||||
|
warnOnReload(false);
|
||||||
|
})
|
||||||
|
.andThen((job) => {
|
||||||
|
notify(i18n.fileDownloaded, job.lufiFile.name);
|
||||||
|
|
||||||
|
const downloadDOM = updateDOM("success");
|
||||||
|
const downloadButtonDOM = document.getElementById("download-button");
|
||||||
|
|
||||||
|
const blobURL = URL.createObjectURL(job.downloadedFile);
|
||||||
|
|
||||||
|
downloadButtonDOM.href = blobURL;
|
||||||
|
downloadButtonDOM.download = escapeHtml(job.lufiFile.name);
|
||||||
|
|
||||||
|
const isZipped =
|
||||||
|
downloadDOM.querySelector(".description").dataset.isZipped === "true";
|
||||||
|
|
||||||
|
let content;
|
||||||
|
|
||||||
|
if (job.lufiFile.type.match(/^image\//) !== null) {
|
||||||
|
content = `<img alt="${escapeHtml(
|
||||||
|
job.lufiFile.name
|
||||||
|
)}" src="${blobURL}">`;
|
||||||
|
} else if (job.lufiFile.type.match(/^video\//) !== null) {
|
||||||
|
content = `<video controls><source src="${blobURL}" type="${job.lufiFile.type}"></video>`;
|
||||||
|
} else if (job.lufiFile.type.match(/^audio\//) !== null) {
|
||||||
|
content = `<audio controls><source src="${blobURL}" type="${job.lufiFile.type}"></audio>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content) {
|
||||||
|
downloadDOM.querySelector(".content").innerHTML = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isZipped) {
|
||||||
|
showZipContent(job.downloadedFile);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const warnOnReload = (toWarn = true) => {
|
||||||
|
window.onbeforeunload = toWarn ? i18n.confirmExit : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isPasswordNeeded()) {
|
||||||
|
onPasswordEvents();
|
||||||
|
} else {
|
||||||
|
startDownload();
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { formatDate } from "./lib/utils.js";
|
||||||
|
|
||||||
// Add item to localStorage
|
// Add item to localStorage
|
||||||
const addItem = (item) => {
|
const addItem = (item) => {
|
||||||
const files = JSON.parse(localStorage.getItem(`${prefix}files`)) || [];
|
const files = JSON.parse(localStorage.getItem(`${prefix}files`)) || [];
|
||||||
|
@ -20,10 +22,11 @@ const itemExists = (serverKey) => {
|
||||||
return files.some((file) => file.short === serverKey);
|
return files.some((file) => file.short === serverKey);
|
||||||
};
|
};
|
||||||
|
|
||||||
const invertSelection = () => {
|
const updateSelection = (event) => {
|
||||||
document.querySelectorAll(".item .column.selection input").forEach((node) => {
|
document.querySelectorAll(".item .checkbox input").forEach((node) => {
|
||||||
node.click();
|
node.checked = event.target.checked;
|
||||||
});
|
});
|
||||||
|
|
||||||
checkItemSelection();
|
checkItemSelection();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -33,7 +36,7 @@ const purgeExpired = () => {
|
||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
const fileDOM = document.querySelector(`.item-${file.short}`);
|
const fileDOM = document.querySelector(`.item-${file.short}`);
|
||||||
|
|
||||||
if (fileDOM.classList.contains("deleted")) {
|
if (fileDOM?.classList.contains("deleted")) {
|
||||||
deleteFromStorage(file.short);
|
deleteFromStorage(file.short);
|
||||||
fileDOM.remove();
|
fileDOM.remove();
|
||||||
}
|
}
|
||||||
|
@ -41,7 +44,7 @@ const purgeExpired = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const exportStorage = () => {
|
const exportStorage = () => {
|
||||||
const exportStorageDOM = document.querySelector(".action-export-storage");
|
const exportStorageDOM = document.getElementById("action-export-storage");
|
||||||
|
|
||||||
const storageData = [localStorage.getItem(`${prefix}files`)];
|
const storageData = [localStorage.getItem(`${prefix}files`)];
|
||||||
const exportFile = new Blob(storageData, { type: "application/json" });
|
const exportFile = new Blob(storageData, { type: "application/json" });
|
||||||
|
@ -69,8 +72,6 @@ const importStorage = (event) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
populateFilesTable();
|
populateFilesTable();
|
||||||
|
|
||||||
addToast(i18n.importProcessed, "success");
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err);
|
alert(err);
|
||||||
}
|
}
|
||||||
|
@ -87,10 +88,11 @@ const validURL = (str) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteFile = (node) => {
|
const deleteFile = (node) => {
|
||||||
const serverKey = node.getAttribute("data-serverKey");
|
const serverKey = node.dataset.serverKey;
|
||||||
const deleteUrl = new URL(
|
const deleteUrl = new URL(
|
||||||
`${actionURL}d/${serverKey}/${node.getAttribute("data-actionKey")}`
|
`${actionURL}d/${serverKey}/${node.dataset.actionKey}`
|
||||||
);
|
);
|
||||||
|
|
||||||
deleteUrl.searchParams.append("_format", "json");
|
deleteUrl.searchParams.append("_format", "json");
|
||||||
|
|
||||||
fetch(deleteUrl, {
|
fetch(deleteUrl, {
|
||||||
|
@ -115,12 +117,9 @@ const deleteFile = (node) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkItemSelection = () => {
|
const checkItemSelection = () => {
|
||||||
const deleteSelectionDOM = document.querySelector(".action-delete-selection");
|
const deleteSelectionDOM = document.getElementById("action-delete-selection");
|
||||||
|
|
||||||
if (
|
if (document.querySelectorAll(".item .checkbox input:checked").length > 0) {
|
||||||
document.querySelectorAll(".column.selection .checkbox input:checked")
|
|
||||||
.length > 0
|
|
||||||
) {
|
|
||||||
deleteSelectionDOM.disabled = false;
|
deleteSelectionDOM.disabled = false;
|
||||||
} else {
|
} else {
|
||||||
deleteSelectionDOM.disabled = true;
|
deleteSelectionDOM.disabled = true;
|
||||||
|
@ -129,13 +128,13 @@ const checkItemSelection = () => {
|
||||||
|
|
||||||
const deleteSelection = () => {
|
const deleteSelection = () => {
|
||||||
document
|
document
|
||||||
.querySelectorAll(".item:has(.column.selection .checkbox input:checked)")
|
.querySelectorAll(".item:has(.checkbox input:checked)")
|
||||||
.forEach((node) => deleteFile(node));
|
.forEach((node) => deleteFile(node));
|
||||||
};
|
};
|
||||||
|
|
||||||
const populateFilesTable = () => {
|
const populateFilesTable = () => {
|
||||||
const filesItemsDOM = document.querySelector(".files-items");
|
const itemsTableDOM = document.getElementById("items-table");
|
||||||
filesItemsDOM.querySelectorAll("tr").forEach((node) => node.remove());
|
itemsTableDOM.replaceChildren();
|
||||||
|
|
||||||
let files = localStorage.getItem(`${prefix}files`);
|
let files = localStorage.getItem(`${prefix}files`);
|
||||||
|
|
||||||
|
@ -143,7 +142,7 @@ const populateFilesTable = () => {
|
||||||
const filesWithoutPrefix = localStorage.getItem("files");
|
const filesWithoutPrefix = localStorage.getItem("files");
|
||||||
|
|
||||||
if (filesWithoutPrefix !== null) {
|
if (filesWithoutPrefix !== null) {
|
||||||
if (window.confirm(i18n.importFilesWithoutPrefix)) {
|
if (confirm(i18n.importFilesWithoutPrefix)) {
|
||||||
localStorage.setItem(`${prefix}files`, filesWithoutPrefix);
|
localStorage.setItem(`${prefix}files`, filesWithoutPrefix);
|
||||||
|
|
||||||
files = JSON.parse(filesWithoutPrefix);
|
files = JSON.parse(filesWithoutPrefix);
|
||||||
|
@ -158,7 +157,7 @@ const populateFilesTable = () => {
|
||||||
files = JSON.parse(files);
|
files = JSON.parse(files);
|
||||||
}
|
}
|
||||||
|
|
||||||
files.sort((a, b) => a.created_at - b.created_at);
|
files.sort((a, b) => b.created_at - a.created_at);
|
||||||
|
|
||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
const itemDOM = document
|
const itemDOM = document
|
||||||
|
@ -167,34 +166,36 @@ const populateFilesTable = () => {
|
||||||
|
|
||||||
itemDOM.classList.add(`item-${file.short}`);
|
itemDOM.classList.add(`item-${file.short}`);
|
||||||
|
|
||||||
itemDOM.setAttribute("data-serverKey", file.short);
|
itemDOM.dataset.serverKey = file.short;
|
||||||
itemDOM.setAttribute("data-actionKey", file.token);
|
itemDOM.dataset.actionKey = file.token;
|
||||||
|
|
||||||
itemDOM.querySelector(".column.name").innerText = file.name;
|
itemDOM.querySelector(".name").innerText = file.name;
|
||||||
itemDOM.querySelector(".column.download a").href = file.url;
|
itemDOM.querySelector(".download a").href = file.url;
|
||||||
itemDOM
|
itemDOM
|
||||||
.querySelector(".column.delete-at-first-view .icon")
|
.querySelector(".delete-at-first-view .icon")
|
||||||
.classList.add(file.del_at_first_view ? "check" : "close");
|
.classList.add(file.del_at_first_view ? "fa-eraser" : "fa-close");
|
||||||
itemDOM.querySelector(".column.created-at").innerText = formatDate(
|
itemDOM.querySelector(".created-at").innerText = formatDate(
|
||||||
file.created_at
|
file.created_at
|
||||||
);
|
);
|
||||||
itemDOM.querySelector(".column.expires-at").innerText =
|
itemDOM.querySelector(".expires-at").innerText =
|
||||||
file.delay == 0
|
file.delay == 0
|
||||||
? i18n.noExpiration
|
? i18n.noExpiration
|
||||||
: formatDate(file.delay * 86400 + file.created_at);
|
: formatDate(file.delay * 86400 + file.created_at);
|
||||||
|
|
||||||
itemDOM.querySelector(
|
itemDOM.querySelector(
|
||||||
".column.mail a"
|
".mail a"
|
||||||
).href = `${actionURL}m?links=["${file.short}"]`;
|
).href = `${actionURL}m?links=["${file.short}"]`;
|
||||||
|
|
||||||
itemDOM.querySelector(".column.deletion button").onclick = () =>
|
itemDOM.querySelector(".action-delete-item").onclick = () =>
|
||||||
deleteFile(itemDOM);
|
deleteFile(itemDOM);
|
||||||
|
|
||||||
itemDOM.querySelector(".column.selection .checkbox input").onclick = () => {
|
itemDOM.querySelector(".checkbox input").onclick = () => {
|
||||||
checkItemSelection();
|
checkItemSelection();
|
||||||
};
|
};
|
||||||
|
|
||||||
filesItemsDOM.append(itemDOM);
|
itemsTableDOM.append(itemDOM);
|
||||||
|
|
||||||
|
console.debug(file.short, file.token);
|
||||||
|
|
||||||
fetch(counterURL, {
|
fetch(counterURL, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
@ -214,22 +215,20 @@ const populateFilesTable = () => {
|
||||||
return response.json();
|
return response.json();
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data.success) {
|
const countDOM = itemDOM.querySelector(".counter");
|
||||||
const countDOM = itemDOM.querySelector(".column.counter");
|
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
countDOM.innerText = data.counter;
|
countDOM.innerText = data.counter;
|
||||||
|
|
||||||
if (data.deleted) {
|
if (data.deleted) {
|
||||||
if (data.deleted) {
|
countDOM.parentElement.classList.add("deleted");
|
||||||
countDOM.parentElement.classList.add("deleted");
|
}
|
||||||
} else {
|
} else {
|
||||||
alert(data.msg);
|
alert(data.msg);
|
||||||
countDOM.parentElement.remove();
|
countDOM.parentElement.remove();
|
||||||
|
|
||||||
if (data.missing) {
|
if (data.missing) {
|
||||||
deleteFromStorage(data.short);
|
deleteFromStorage(data.short);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -239,9 +238,9 @@ const populateFilesTable = () => {
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
populateFilesTable();
|
populateFilesTable();
|
||||||
document.querySelector(".action-invert-selection").onclick = invertSelection;
|
document.getElementById("action-select-all").onclick = updateSelection;
|
||||||
document.querySelector(".action-export-storage").onclick = exportStorage;
|
document.getElementById("action-export-storage").onclick = exportStorage;
|
||||||
document.querySelector(".action-purge-expired").onclick = purgeExpired;
|
document.getElementById("action-purge-expired").onclick = purgeExpired;
|
||||||
document.querySelector(".action-import-storage").onchange = importStorage;
|
document.getElementById("action-import-storage").onchange = importStorage;
|
||||||
document.querySelector(".action-delete-selection").onclick = deleteSelection;
|
document.getElementById("action-delete-selection").onclick = deleteSelection;
|
||||||
});
|
});
|
|
@ -0,0 +1,95 @@
|
||||||
|
const entityMap = {
|
||||||
|
"&": "&",
|
||||||
|
"<": "<",
|
||||||
|
">": ">",
|
||||||
|
'"': """,
|
||||||
|
"'": "'",
|
||||||
|
"/": "/",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const copyToClipboard = async (text) => {
|
||||||
|
try {
|
||||||
|
if (navigator.clipboard) {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
} else {
|
||||||
|
// Unsecure and deprecated method. It's a fallback for non-secure contexts
|
||||||
|
const textArea = document.createElement("textarea");
|
||||||
|
textArea.value = text;
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.focus();
|
||||||
|
textArea.select();
|
||||||
|
document.execCommand("copy");
|
||||||
|
|
||||||
|
document.body.removeChild(textArea);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ensureNode = (selector, callback) => {
|
||||||
|
const node = document.querySelector(selector);
|
||||||
|
|
||||||
|
if (node) {
|
||||||
|
callback(node);
|
||||||
|
} else {
|
||||||
|
console.warning(`The node with ${selector} does not exist.`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const escapeHtml = (string) =>
|
||||||
|
String(string).replace(/[&<>"'\/]/g, (s) => entityMap[s]);
|
||||||
|
|
||||||
|
export const formatDate = (unixTimestamp) =>
|
||||||
|
new Date(unixTimestamp * 1000).toLocaleString(window.navigator.language, {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
weekday: "long",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
|
||||||
|
// export const notify = (message, type) => {
|
||||||
|
// const notification = document
|
||||||
|
// .querySelector("template#notification")
|
||||||
|
// .content.cloneNode(true).children[0];
|
||||||
|
|
||||||
|
// notification.classList.add(`is-${type}`);
|
||||||
|
|
||||||
|
// notification.querySelector(".message").innerText = message;
|
||||||
|
|
||||||
|
// document.getElementById("notifications-container").append(notification);
|
||||||
|
|
||||||
|
// setTimeout(() => {
|
||||||
|
// notification.remove();
|
||||||
|
// }, 3500);
|
||||||
|
// };
|
||||||
|
|
||||||
|
export const notify = (title, body) => {
|
||||||
|
if (isSecureContext) {
|
||||||
|
if (!("Notification" in window) || typeof Notification === "undefined") {
|
||||||
|
console.log(
|
||||||
|
`This browser does not support desktop notification, cannot send following message: ${title} ${body}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Notification.permission !== "granted") {
|
||||||
|
Notification.requestPermission();
|
||||||
|
} else {
|
||||||
|
new Notification(title, {
|
||||||
|
body,
|
||||||
|
icon: "/img/lufi.svg",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const uuidv4 = () =>
|
||||||
|
"10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c) =>
|
||||||
|
(
|
||||||
|
+c ^
|
||||||
|
(crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (+c / 4)))
|
||||||
|
).toString(16)
|
||||||
|
);
|
|
@ -1,4 +1,4 @@
|
||||||
import { filesize } from "./filesize.esm.min.js";
|
import { filesize } from "~/lib/filesize.esm.min.js";
|
||||||
|
|
||||||
const updateButtonsStatus = () => {
|
const updateButtonsStatus = () => {
|
||||||
const targetSelectionDOM = document.querySelectorAll(".target-selection");
|
const targetSelectionDOM = document.querySelectorAll(".target-selection");
|
|
@ -1,183 +0,0 @@
|
||||||
import { lufi } from "./lufi.js";
|
|
||||||
import { filesize } from "./filesize.esm.min.js";
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
const reloadButtonDOM = document.querySelector(".action-reload");
|
|
||||||
const passwordFormDOM = document.querySelector(".password-form");
|
|
||||||
const downloadContainerDOM = document.querySelector(".download-container");
|
|
||||||
const passwordInputDOM = document.querySelector(".input-password");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the DOM depending on the situation
|
|
||||||
*
|
|
||||||
* @param {string} type Can be aborted, success or ongoing
|
|
||||||
* @param {Node} existingCard Existing card to be replaced with the new one
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
const updateDOM = (type) => {
|
|
||||||
const cardDOM = document
|
|
||||||
.querySelector(`template#download-card-${type}`)
|
|
||||||
.content.cloneNode(true).children[0];
|
|
||||||
|
|
||||||
cardDOM.querySelector(".file-size").innerText = filesize(
|
|
||||||
cardDOM.querySelector(".file-size").getAttribute("data-filesize")
|
|
||||||
);
|
|
||||||
|
|
||||||
downloadContainerDOM.replaceChildren(cardDOM);
|
|
||||||
|
|
||||||
if (type === "success") {
|
|
||||||
cardDOM.querySelector(".action-download").focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
return cardDOM;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isPasswordNeeded = () => passwordInputDOM !== null;
|
|
||||||
|
|
||||||
const onPasswordEvents = () => {
|
|
||||||
passwordFormDOM.onsubmit = (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
hideNode(passwordFormDOM);
|
|
||||||
|
|
||||||
startDownload();
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const showZipContent = (zipFile, cardDOM) => {
|
|
||||||
const zipContainerDOM = cardDOM.querySelector(".content .zip-container");
|
|
||||||
|
|
||||||
showNode(zipContainerDOM);
|
|
||||||
|
|
||||||
zipContainerDOM.querySelector(".action-show-zip").onclick = (event) => {
|
|
||||||
hideNode(event.target);
|
|
||||||
|
|
||||||
const zipContentDOM = zipContainerDOM.querySelector(".zip-content");
|
|
||||||
|
|
||||||
showNode(zipContentDOM);
|
|
||||||
|
|
||||||
document.body.style.cursor = "wait";
|
|
||||||
|
|
||||||
lufi
|
|
||||||
.decompress(zipFile)
|
|
||||||
.andThen((job) => job.waitForCompletion())
|
|
||||||
.map((job) => {
|
|
||||||
job.archiveFiles.forEach((file) => {
|
|
||||||
const itemDOM = document
|
|
||||||
.querySelector("template#zip-item")
|
|
||||||
.content.cloneNode(true).children[0];
|
|
||||||
|
|
||||||
itemDOM.querySelector(".file-name").innerText = escapeHtml(
|
|
||||||
file.name
|
|
||||||
);
|
|
||||||
itemDOM.querySelector(".file-size").innerText = filesize(file.size);
|
|
||||||
|
|
||||||
const downloadItemDOM = itemDOM.querySelector(
|
|
||||||
".action-download-item"
|
|
||||||
);
|
|
||||||
|
|
||||||
downloadItemDOM.download = escapeHtml(file.name);
|
|
||||||
downloadItemDOM.href = URL.createObjectURL(file);
|
|
||||||
|
|
||||||
zipContentDOM.querySelector("ul").append(itemDOM);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.body.style.cursor = "auto";
|
|
||||||
})
|
|
||||||
.mapErr((error) => {
|
|
||||||
console.error(error);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const startDownload = () => {
|
|
||||||
lufi
|
|
||||||
.download(
|
|
||||||
window.location,
|
|
||||||
passwordInputDOM?.querySelector("#file-password").value
|
|
||||||
)
|
|
||||||
.andThen((job) => {
|
|
||||||
const cardDOM = updateDOM("ongoing");
|
|
||||||
|
|
||||||
warnOnReload();
|
|
||||||
job.onProgress(() => {
|
|
||||||
const percent =
|
|
||||||
Math.round(
|
|
||||||
(job.lufiFile.chunksReady * 100) / job.lufiFile.totalChunks
|
|
||||||
) + "%";
|
|
||||||
|
|
||||||
cardDOM.querySelector(".progress-bar").style.width = percent;
|
|
||||||
cardDOM.querySelector(".loading-message").innerText =
|
|
||||||
i18n.loading.replace("XX1", job.lufiFile.chunksReady);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelector(".action-abort").onclick = () => {
|
|
||||||
job.terminate();
|
|
||||||
|
|
||||||
warnOnReload(false);
|
|
||||||
|
|
||||||
updateDOM("aborted");
|
|
||||||
|
|
||||||
reloadButtonDOM.onclick = (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
window.location.reload();
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return job.waitForCompletion();
|
|
||||||
})
|
|
||||||
.mapErr((error) => {
|
|
||||||
updateDOM("error").querySelector(".message-card").innerText =
|
|
||||||
error.message;
|
|
||||||
|
|
||||||
warnOnReload(false);
|
|
||||||
})
|
|
||||||
.andThen((job) => {
|
|
||||||
notify(i18n.fileDownloaded, job.lufiFile.name);
|
|
||||||
|
|
||||||
const downloadDOM = updateDOM("success");
|
|
||||||
|
|
||||||
const blobURL = URL.createObjectURL(job.downloadedFile);
|
|
||||||
|
|
||||||
downloadDOM.querySelector(".action-download").href = blobURL;
|
|
||||||
downloadDOM.querySelector(".action-download").download = escapeHtml(
|
|
||||||
job.lufiFile.name
|
|
||||||
);
|
|
||||||
|
|
||||||
const isZipped =
|
|
||||||
downloadDOM
|
|
||||||
.querySelector(".file-description")
|
|
||||||
.getAttribute("data-isZipped") === "true";
|
|
||||||
|
|
||||||
let content;
|
|
||||||
|
|
||||||
if (job.lufiFile.type.match(/^image\//) !== null) {
|
|
||||||
content = `<img alt="${escapeHtml(
|
|
||||||
job.lufiFile.name
|
|
||||||
)}" src="${blobURL}">`;
|
|
||||||
} else if (job.lufiFile.type.match(/^video\//) !== null) {
|
|
||||||
content = `<video controls><source src="${blobURL}" type="${job.lufiFile.type}"></video>`;
|
|
||||||
} else if (job.lufiFile.type.match(/^audio\//) !== null) {
|
|
||||||
content = `<audio controls><source src="${blobURL}" type="${job.lufiFile.type}"></audio>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content) {
|
|
||||||
downloadDOM.querySelector(".content").innerHTML = content;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isZipped) {
|
|
||||||
showZipContent(job.downloadedFile, downloadDOM);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const warnOnReload = (toWarn = true) => {
|
|
||||||
window.onbeforeunload = toWarn ? i18n.confirmExit : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isPasswordNeeded()) {
|
|
||||||
onPasswordEvents();
|
|
||||||
} else {
|
|
||||||
startDownload();
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,566 +0,0 @@
|
||||||
import {
|
|
||||||
lufi,
|
|
||||||
errAsync,
|
|
||||||
okAsync,
|
|
||||||
ResultAsync,
|
|
||||||
isSecureContext,
|
|
||||||
CryptoAlgorithm,
|
|
||||||
} from "./lufi.js";
|
|
||||||
|
|
||||||
import { filesize } from "./filesize.esm.min.js";
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
const deleteAfterDaysDOM = document.getElementById("delete-after-days");
|
|
||||||
const deleteOnFirstViewDOM = document.getElementById("delete-on-first-view");
|
|
||||||
const dropZoneDOM = document.querySelector(".drop-zone");
|
|
||||||
const fileCardsDOM = document.querySelector(".file-cards");
|
|
||||||
const maxFileSizeDOM = document.querySelector(".max-file-size");
|
|
||||||
const messagesZoneDOM = document.querySelector(".messages-zone");
|
|
||||||
const mustZipDOM = document.getElementById("must-zip");
|
|
||||||
const passwordDOM = document.getElementById("password");
|
|
||||||
const uploadButtonDOM = document.querySelector("#upload-button");
|
|
||||||
const uploadedZoneDOM = document.querySelector(".uploaded-zone");
|
|
||||||
const uploadZipDOM = document.querySelector(".action-upload-zip");
|
|
||||||
const zipNameDOM = document.getElementById("zip-name");
|
|
||||||
const zipZoneDOM = document.querySelector(".zip-zone");
|
|
||||||
const inputZipNameDOM = document.querySelector(".input-zip-name");
|
|
||||||
|
|
||||||
// Global zip objects for currently created zip file
|
|
||||||
let zipSize = 0;
|
|
||||||
// Init the list of files (used by LDAP invitation feature)
|
|
||||||
let filesURLs = [];
|
|
||||||
|
|
||||||
let archiveEntries;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add item to localStorage
|
|
||||||
*
|
|
||||||
* @param {string} name
|
|
||||||
* @param {string} url
|
|
||||||
* @param {number} size
|
|
||||||
* @param {boolean} del_at_first_view
|
|
||||||
* @param {number} created_at
|
|
||||||
* @param {number} delay
|
|
||||||
* @param {string} serverKey
|
|
||||||
* @param {string} actionToken
|
|
||||||
*/
|
|
||||||
const addItem = (
|
|
||||||
name,
|
|
||||||
url,
|
|
||||||
size,
|
|
||||||
del_at_first_view,
|
|
||||||
created_at,
|
|
||||||
delay,
|
|
||||||
serverKey,
|
|
||||||
actionToken
|
|
||||||
) => {
|
|
||||||
let files = JSON.parse(localStorage.getItem(`${prefix}files`)) || [];
|
|
||||||
|
|
||||||
files.push({
|
|
||||||
name,
|
|
||||||
short: serverKey,
|
|
||||||
url,
|
|
||||||
size,
|
|
||||||
del_at_first_view,
|
|
||||||
created_at,
|
|
||||||
delay,
|
|
||||||
token: actionToken,
|
|
||||||
});
|
|
||||||
localStorage.setItem(`${prefix}files`, JSON.stringify(files));
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearZip = () => {
|
|
||||||
hideNode(zipZoneDOM);
|
|
||||||
showNode(zipZoneDOM.querySelector(".action-upload-zip"));
|
|
||||||
hideNode(zipZoneDOM.querySelector(".zip-compressing"));
|
|
||||||
hideNode(inputZipNameDOM);
|
|
||||||
|
|
||||||
zipZoneDOM.querySelector(".files-list").replaceChildren();
|
|
||||||
|
|
||||||
archiveEntries = undefined;
|
|
||||||
zipSize = 0;
|
|
||||||
mustZipDOM.checked = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const copyLinksToClipboard = async () => {
|
|
||||||
const inputs = document.querySelectorAll(".download-input");
|
|
||||||
const textArray = [];
|
|
||||||
|
|
||||||
inputs.forEach((node) => textArray.push(node.value));
|
|
||||||
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(textArray.join("\n")).then(() => {
|
|
||||||
addToast(i18n.copySuccess, "success");
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
alert(i18n.hits);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new File Card element
|
|
||||||
*
|
|
||||||
* @param {LufiJob} job
|
|
||||||
* @param {string} type Can be error, success or ongoing
|
|
||||||
* @param {Node} existingCard Existing card to be replaced with the new one
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
const createFileCard = (job, type, existingCard = undefined) => {
|
|
||||||
const { lufiFile } = job;
|
|
||||||
const cardDOM = document
|
|
||||||
.querySelector(`template#file-card-${type}`)
|
|
||||||
.content.cloneNode(true).children[0];
|
|
||||||
|
|
||||||
cardDOM.id = `file-card-${lufiFile.keys.client}`;
|
|
||||||
|
|
||||||
cardDOM.querySelector(".file-name").innerText = escapeHtml(lufiFile.name);
|
|
||||||
cardDOM.querySelector(".file-size").innerText = filesize(lufiFile.size);
|
|
||||||
|
|
||||||
cardDOM.querySelector(".action-close").onclick = () => {
|
|
||||||
lufi
|
|
||||||
.cancel(job)
|
|
||||||
.map(() => {
|
|
||||||
removeFileCard(job.lufiFile.keys.server, cardDOM);
|
|
||||||
})
|
|
||||||
.mapErr((error) => {
|
|
||||||
showErrorOnCard(job, ongoingFileCardDOM, error.msg);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case "success":
|
|
||||||
cardDOM.querySelector(
|
|
||||||
".action-mail"
|
|
||||||
).href = `${actionURL}m?links=${encodeURIComponent(
|
|
||||||
`["${lufiFile.keys.server}"]`
|
|
||||||
)}`;
|
|
||||||
|
|
||||||
const expirationDate =
|
|
||||||
lufiFile.delay === 0
|
|
||||||
? i18n.noLimit
|
|
||||||
: `${i18n.expiration} ${formatDate(
|
|
||||||
lufiFile.delay * 86400 + lufiFile.createdAt
|
|
||||||
)}`;
|
|
||||||
|
|
||||||
cardDOM.querySelector(".file-expiration").innerText = expirationDate;
|
|
||||||
|
|
||||||
cardDOM.querySelector(".download-button").href = lufiFile.downloadUrl();
|
|
||||||
cardDOM.querySelector(".delete-button").href = lufiFile.removeUrl();
|
|
||||||
cardDOM.querySelector(".copy-button").onclick = async (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await navigator.clipboard
|
|
||||||
.writeText(lufiFile.downloadUrl())
|
|
||||||
.then(() => {
|
|
||||||
addToast(i18n.copySuccess, "success");
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
showMessage(error.message, "error");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
cardDOM.querySelector(".download-input").value = lufiFile.downloadUrl();
|
|
||||||
cardDOM.querySelector(".delete-input").value = lufiFile.removeUrl();
|
|
||||||
|
|
||||||
updateMailLinksButton(lufiFile.keys.server);
|
|
||||||
break;
|
|
||||||
case "ongoing":
|
|
||||||
cardDOM.querySelector(".progress-bar").max = lufiFile.totalChunks;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingCard) {
|
|
||||||
existingCard.replaceWith(cardDOM);
|
|
||||||
} else {
|
|
||||||
fileCardsDOM.prepend(cardDOM);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fileCardsDOM.querySelectorAll(".file-card.success").length > 0) {
|
|
||||||
showNode(uploadedZoneDOM.querySelector(".buttons"));
|
|
||||||
}
|
|
||||||
|
|
||||||
return cardDOM;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleZipEvents = () => {
|
|
||||||
zipZoneDOM.querySelector(".action-close").onclick = () => {
|
|
||||||
clearZip();
|
|
||||||
};
|
|
||||||
|
|
||||||
mustZipDOM.onchange = () => {
|
|
||||||
if (!mustZipDOM.checked) {
|
|
||||||
clearZip();
|
|
||||||
} else {
|
|
||||||
showNode(inputZipNameDOM);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
zipNameDOM.oninput = () => {
|
|
||||||
let name = zipNameDOM.value || "documents.zip";
|
|
||||||
|
|
||||||
if (!name.endsWith(".zip")) {
|
|
||||||
name += name.endsWith(".") ? "zip" : ".zip";
|
|
||||||
}
|
|
||||||
|
|
||||||
name = escapeHtml(name);
|
|
||||||
|
|
||||||
zipZoneDOM.querySelector(".file-name").innerText = name;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decide what to do with files passed to upload zone
|
|
||||||
*
|
|
||||||
* @param {FileList} files
|
|
||||||
*/
|
|
||||||
const handleFiles = (files) => {
|
|
||||||
files = Array.from(files) || [];
|
|
||||||
|
|
||||||
document.body.style.cursor = "wait";
|
|
||||||
|
|
||||||
const { deleteDays, shouldDeleteOnFirstView, password, mustZip } =
|
|
||||||
retrieveUploadParams();
|
|
||||||
|
|
||||||
if (!mustZip) {
|
|
||||||
files.forEach((file) => {
|
|
||||||
addToast(i18n.enqueued.replace("XXX", file.name), "success");
|
|
||||||
});
|
|
||||||
|
|
||||||
document.body.style.cursor = "auto";
|
|
||||||
|
|
||||||
startUpload(
|
|
||||||
files,
|
|
||||||
deleteDays,
|
|
||||||
shouldDeleteOnFirstView,
|
|
||||||
mustZip,
|
|
||||||
undefined,
|
|
||||||
password
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
showNode(zipZoneDOM);
|
|
||||||
|
|
||||||
lufi
|
|
||||||
.addFilesToArchive(files, archiveEntries)
|
|
||||||
.andThen((entries) => {
|
|
||||||
archiveEntries = entries;
|
|
||||||
|
|
||||||
const listDOM = zipZoneDOM.querySelector(".files-list");
|
|
||||||
|
|
||||||
listDOM.replaceChildren();
|
|
||||||
|
|
||||||
for (const [name, file] of Object.entries(archiveEntries)) {
|
|
||||||
zipSize += file.length;
|
|
||||||
|
|
||||||
const listItemDOM = document.createElement("li");
|
|
||||||
listItemDOM.innerText = `${escapeHtml(name)} (${filesize(
|
|
||||||
file.length
|
|
||||||
)})`;
|
|
||||||
|
|
||||||
listDOM.appendChild(listItemDOM);
|
|
||||||
}
|
|
||||||
|
|
||||||
zipZoneDOM.querySelector(".file-size").textContent =
|
|
||||||
filesize(zipSize);
|
|
||||||
|
|
||||||
document.body.style.cursor = "auto";
|
|
||||||
|
|
||||||
return okAsync(undefined);
|
|
||||||
})
|
|
||||||
.orElse((error) => console.error(error.message));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUploadEvents = () => {
|
|
||||||
dropZoneDOM.addEventListener("drop", (event) => {
|
|
||||||
handleFiles(event.dataTransfer.files);
|
|
||||||
});
|
|
||||||
dropZoneDOM.addEventListener("dragover", (event) => {
|
|
||||||
event.dataTransfer.dropEffect = "copy";
|
|
||||||
});
|
|
||||||
|
|
||||||
uploadButtonDOM.addEventListener("change", (event) => {
|
|
||||||
handleFiles(event.target.files);
|
|
||||||
});
|
|
||||||
|
|
||||||
uploadZipDOM.onclick = uploadZip;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a File Card element and hide Uploaded Zone if there is no File Card left
|
|
||||||
*
|
|
||||||
* @param {Node} card
|
|
||||||
*/
|
|
||||||
const removeFileCard = (serverKey, card) => {
|
|
||||||
card.remove();
|
|
||||||
|
|
||||||
updateMailLinksButton(serverKey, true);
|
|
||||||
|
|
||||||
if (fileCardsDOM.children.length === 0) {
|
|
||||||
hideNode(uploadedZoneDOM);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fileCardsDOM.querySelectorAll(".file-card.success").length === 0) {
|
|
||||||
hideNode(uploadedZoneDOM);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const retrieveUploadParams = () => {
|
|
||||||
const mustZip = document.querySelector("#must-zip").checked;
|
|
||||||
const deleteDays = deleteAfterDaysDOM.value;
|
|
||||||
const shouldDeleteOnFirstView = deleteOnFirstViewDOM.checked;
|
|
||||||
const password = passwordDOM?.value || "";
|
|
||||||
const zipName = zipNameDOM.value;
|
|
||||||
|
|
||||||
return { deleteDays, shouldDeleteOnFirstView, password, mustZip, zipName };
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show a notification at the top of the page
|
|
||||||
*
|
|
||||||
* @param {string} text
|
|
||||||
* @param {string} type Can be error or success
|
|
||||||
*/
|
|
||||||
const showMessage = (text, type) => {
|
|
||||||
const messageDOM = document.createElement("div");
|
|
||||||
|
|
||||||
messageDOM.innerText = text;
|
|
||||||
messageDOM.classList.add("message-card", type);
|
|
||||||
|
|
||||||
messagesZoneDOM.appendChild(messageDOM);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show an error message inside the File Card.
|
|
||||||
*
|
|
||||||
* @param {LufiJob} job
|
|
||||||
* @param {Node} cardDOM
|
|
||||||
* @param {string} text
|
|
||||||
*/
|
|
||||||
const showErrorOnCard = (job, cardDOM, text) => {
|
|
||||||
const errorCardDOM = createFileCard(job, "error", cardDOM);
|
|
||||||
|
|
||||||
errorCardDOM.querySelector(".upload-error").innerText = text;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [Invitation feature] Send URLs of files to server
|
|
||||||
*/
|
|
||||||
const sendFilesURLs = () => {
|
|
||||||
if (filesURLs.length > 0) {
|
|
||||||
fetch(sendFilesURLsURL, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
urls: filesURLs,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.then((response) => response.json())
|
|
||||||
.then((data) => {
|
|
||||||
if (data.success) {
|
|
||||||
addToast(data.msg, "success");
|
|
||||||
} else {
|
|
||||||
addToast(data.msg, "error");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Error:", error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start the upload of provided files
|
|
||||||
*
|
|
||||||
* @param {File[]} files
|
|
||||||
* @param {number} delay
|
|
||||||
* @param {boolean} delAtFirstView
|
|
||||||
* @param {boolean} isZipped
|
|
||||||
* @param {string} zipName
|
|
||||||
* @param {string} password
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
const startUpload = (
|
|
||||||
files,
|
|
||||||
delay,
|
|
||||||
delAtFirstView,
|
|
||||||
isZipped,
|
|
||||||
zipName,
|
|
||||||
password
|
|
||||||
) => {
|
|
||||||
showNode(uploadedZoneDOM);
|
|
||||||
|
|
||||||
const serverUrl = new URL(ws_url.replace("/upload", ""));
|
|
||||||
serverUrl.protocol = serverUrl.protocol === "ws:" ? "http:" : "https:";
|
|
||||||
|
|
||||||
return lufi
|
|
||||||
.upload(
|
|
||||||
serverUrl.href,
|
|
||||||
files,
|
|
||||||
delay,
|
|
||||||
delAtFirstView,
|
|
||||||
isZipped,
|
|
||||||
zipName,
|
|
||||||
password,
|
|
||||||
isSecureContext() ? CryptoAlgorithm.WebCrypto : CryptoAlgorithm.Sjcl
|
|
||||||
)
|
|
||||||
.andThen((jobs) =>
|
|
||||||
ResultAsync.combine(
|
|
||||||
jobs.map((job) => {
|
|
||||||
const ongoingFileCardDOM = createFileCard(job, "ongoing");
|
|
||||||
|
|
||||||
job.onProgress(() => {
|
|
||||||
updateProgressBar(job.lufiFile, ongoingFileCardDOM);
|
|
||||||
});
|
|
||||||
|
|
||||||
return job
|
|
||||||
.waitForCompletion()
|
|
||||||
.andThen((job) => {
|
|
||||||
notify(i18n.fileUploaded, job.lufiFile.name);
|
|
||||||
|
|
||||||
createFileCard(
|
|
||||||
job,
|
|
||||||
isGuest ? "guest" : "success",
|
|
||||||
ongoingFileCardDOM
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add the file to localStorage
|
|
||||||
if (!isGuest) {
|
|
||||||
addItem(
|
|
||||||
job.lufiFile.name,
|
|
||||||
job.lufiFile.downloadUrl(),
|
|
||||||
job.lufiFile.size,
|
|
||||||
delAtFirstView,
|
|
||||||
job.lufiFile.createdAt,
|
|
||||||
delay,
|
|
||||||
job.lufiFile.keys.server,
|
|
||||||
job.lufiFile.actionToken
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isGuest && job.lufiFile.keys.server !== null) {
|
|
||||||
filesURLs.push(
|
|
||||||
JSON.stringify({
|
|
||||||
name: job.lufiFile.name,
|
|
||||||
short: job.lufiFile.keys.server,
|
|
||||||
url: job.lufiFile.downloadUrl(),
|
|
||||||
size: job.lufiFile.size,
|
|
||||||
created_at: job.lufiFile.createdAt,
|
|
||||||
delay,
|
|
||||||
token: job.lufiFile.actionToken,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
sendFilesURLs();
|
|
||||||
}
|
|
||||||
|
|
||||||
return okAsync(job);
|
|
||||||
})
|
|
||||||
.orElse((error) => {
|
|
||||||
showErrorOnCard(job, ongoingFileCardDOM, error.message);
|
|
||||||
|
|
||||||
if (isGuest) {
|
|
||||||
sendFilesURLs();
|
|
||||||
}
|
|
||||||
|
|
||||||
return errAsync(error);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.orElse((error) => console.error(error));
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the "send all links by mail" button.
|
|
||||||
*
|
|
||||||
* @param {string} serverKey
|
|
||||||
* @param {boolean} remove If we should remove the serverKey from the links
|
|
||||||
*/
|
|
||||||
const updateMailLinksButton = (serverKey, remove = false) => {
|
|
||||||
const buttonDOM = document.querySelector(".action-mail-links");
|
|
||||||
const url = new URL(buttonDOM.href);
|
|
||||||
let links = JSON.parse(url.searchParams.get("links") || "[]");
|
|
||||||
|
|
||||||
if (remove) {
|
|
||||||
links = links.filter((item) => item !== serverKey);
|
|
||||||
} else {
|
|
||||||
links.push(serverKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
url.searchParams.set("links", JSON.stringify(links));
|
|
||||||
|
|
||||||
buttonDOM.href = url;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the progress bar of the File Card
|
|
||||||
*
|
|
||||||
* @param {LufiFile} lufiFile
|
|
||||||
* @param {Node} cardDOM
|
|
||||||
*/
|
|
||||||
const updateProgressBar = (lufiFile, cardDOM) => {
|
|
||||||
const percent = Math.round(
|
|
||||||
(lufiFile.chunksReady * 100) / lufiFile.totalChunks
|
|
||||||
);
|
|
||||||
|
|
||||||
cardDOM.querySelector(".progress-bar").style.width = `${percent}%`;
|
|
||||||
|
|
||||||
cardDOM.querySelector(".progress-percent").innerText = percent;
|
|
||||||
};
|
|
||||||
|
|
||||||
const uploadZip = () => {
|
|
||||||
document.body.style.cursor = "wait";
|
|
||||||
hideNode(zipZoneDOM.querySelector(".action-upload-zip"));
|
|
||||||
showNode(zipZoneDOM.querySelector(".zip-compressing"));
|
|
||||||
|
|
||||||
const { zipName, deleteDays, shouldDeleteOnFirstView, password } =
|
|
||||||
retrieveUploadParams();
|
|
||||||
|
|
||||||
lufi
|
|
||||||
.compress(archiveEntries, zipName)
|
|
||||||
.andThen((job) => {
|
|
||||||
document.body.style.cursor = "auto";
|
|
||||||
|
|
||||||
zipZoneDOM.querySelector(".action-close").onclick = () => {
|
|
||||||
job.terminate();
|
|
||||||
|
|
||||||
clearZip();
|
|
||||||
};
|
|
||||||
|
|
||||||
return job.waitForCompletion();
|
|
||||||
})
|
|
||||||
.map((job) => {
|
|
||||||
// if '.zip-zone' is hidden, the zipping has been aborted
|
|
||||||
if (!zipZoneDOM.classList.contains("hidden")) {
|
|
||||||
addToast(i18n.enqueued.replace("XXX", zipName), "success");
|
|
||||||
|
|
||||||
clearZip();
|
|
||||||
|
|
||||||
return startUpload(
|
|
||||||
[job.archiveFile],
|
|
||||||
deleteDays,
|
|
||||||
shouldDeleteOnFirstView,
|
|
||||||
true,
|
|
||||||
zipName,
|
|
||||||
password
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.mapErr((error) => showMessage(error.message));
|
|
||||||
};
|
|
||||||
|
|
||||||
if (maxSize) {
|
|
||||||
maxFileSizeDOM.style.display = "block";
|
|
||||||
maxFileSizeDOM.textContent = i18n.maxSize.replace("XXX", filesize(maxSize));
|
|
||||||
}
|
|
||||||
|
|
||||||
handleUploadEvents();
|
|
||||||
handleZipEvents();
|
|
||||||
|
|
||||||
document.querySelector(".action-copy-links").onclick = () => {
|
|
||||||
copyLinksToClipboard();
|
|
||||||
};
|
|
||||||
});
|
|
|
@ -0,0 +1,191 @@
|
||||||
|
// themes/default/public/js/lib/utils.js
|
||||||
|
var formatDate = (unixTimestamp) => new Date(unixTimestamp * 1e3).toLocaleString(window.navigator.language, {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
weekday: "long",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit"
|
||||||
|
});
|
||||||
|
|
||||||
|
// themes/default/public/js/files.js
|
||||||
|
var addItem = (item) => {
|
||||||
|
const files = JSON.parse(localStorage.getItem(`${prefix}files`)) || [];
|
||||||
|
files.push(item);
|
||||||
|
localStorage.setItem(`${prefix}files`, JSON.stringify(files));
|
||||||
|
};
|
||||||
|
var deleteFromStorage = (serverKey) => {
|
||||||
|
let files = JSON.parse(localStorage.getItem(`${prefix}files`)) || [];
|
||||||
|
files = files.filter((file) => file.short !== serverKey);
|
||||||
|
localStorage.setItem(`${prefix}files`, JSON.stringify(files));
|
||||||
|
};
|
||||||
|
var itemExists = (serverKey) => {
|
||||||
|
const files = JSON.parse(localStorage.getItem(`${prefix}files`)) || [];
|
||||||
|
return files.some((file) => file.short === serverKey);
|
||||||
|
};
|
||||||
|
var updateSelection = (event) => {
|
||||||
|
document.querySelectorAll(".item .checkbox input").forEach((node) => {
|
||||||
|
node.checked = event.target.checked;
|
||||||
|
});
|
||||||
|
checkItemSelection();
|
||||||
|
};
|
||||||
|
var purgeExpired = () => {
|
||||||
|
const files = JSON.parse(localStorage.getItem(`${prefix}files`));
|
||||||
|
files.forEach((file) => {
|
||||||
|
const fileDOM = document.querySelector(`.item-${file.short}`);
|
||||||
|
if (fileDOM == null ? void 0 : fileDOM.classList.contains("deleted")) {
|
||||||
|
deleteFromStorage(file.short);
|
||||||
|
fileDOM.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
var exportStorage = () => {
|
||||||
|
const exportStorageDOM = document.getElementById("action-export-storage");
|
||||||
|
const storageData = [localStorage.getItem(`${prefix}files`)];
|
||||||
|
const exportFile = new Blob(storageData, { type: "application/json" });
|
||||||
|
exportStorageDOM.href = window.URL.createObjectURL(exportFile);
|
||||||
|
exportStorageDOM.download = "data.json";
|
||||||
|
};
|
||||||
|
var importStorage = (event) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.addEventListener("loadend", () => {
|
||||||
|
try {
|
||||||
|
const newFiles = JSON.parse(
|
||||||
|
String.fromCharCode.apply(null, new Uint8Array(reader.result))
|
||||||
|
);
|
||||||
|
let importedCounter = 0;
|
||||||
|
newFiles.forEach((file) => {
|
||||||
|
if (validURL(file.url) && !itemExists(file.short)) {
|
||||||
|
addItem(file);
|
||||||
|
importedCounter++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
populateFilesTable();
|
||||||
|
} catch (err) {
|
||||||
|
alert(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
reader.readAsArrayBuffer(event.target.files[0]);
|
||||||
|
};
|
||||||
|
var validURL = (str) => {
|
||||||
|
try {
|
||||||
|
return new URL(str).host ? true : false;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var deleteFile = (node) => {
|
||||||
|
const serverKey = node.dataset.serverKey;
|
||||||
|
const deleteUrl = new URL(
|
||||||
|
`${actionURL}d/${serverKey}/${node.dataset.actionKey}`
|
||||||
|
);
|
||||||
|
deleteUrl.searchParams.append("_format", "json");
|
||||||
|
fetch(deleteUrl, {
|
||||||
|
method: "GET"
|
||||||
|
}).then((response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Network error while deleting file");
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
}).then((data) => {
|
||||||
|
if (data.success) {
|
||||||
|
node.remove();
|
||||||
|
deleteFromStorage(serverKey);
|
||||||
|
} else {
|
||||||
|
alert(data.msg);
|
||||||
|
}
|
||||||
|
checkItemSelection();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
var checkItemSelection = () => {
|
||||||
|
const deleteSelectionDOM = document.getElementById("action-delete-selection");
|
||||||
|
if (document.querySelectorAll(".item .checkbox input:checked").length > 0) {
|
||||||
|
deleteSelectionDOM.disabled = false;
|
||||||
|
} else {
|
||||||
|
deleteSelectionDOM.disabled = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var deleteSelection = () => {
|
||||||
|
document.querySelectorAll(".item:has(.checkbox input:checked)").forEach((node) => deleteFile(node));
|
||||||
|
};
|
||||||
|
var populateFilesTable = () => {
|
||||||
|
const itemsTableDOM = document.getElementById("items-table");
|
||||||
|
itemsTableDOM.replaceChildren();
|
||||||
|
let files = localStorage.getItem(`${prefix}files`);
|
||||||
|
if (files === null) {
|
||||||
|
const filesWithoutPrefix = localStorage.getItem("files");
|
||||||
|
if (filesWithoutPrefix !== null) {
|
||||||
|
if (confirm(i18n.importFilesWithoutPrefix)) {
|
||||||
|
localStorage.setItem(`${prefix}files`, filesWithoutPrefix);
|
||||||
|
files = JSON.parse(filesWithoutPrefix);
|
||||||
|
} else {
|
||||||
|
localStorage.setItem(`${prefix}files`, JSON.stringify([]));
|
||||||
|
files = [];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
files = [];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
files = JSON.parse(files);
|
||||||
|
}
|
||||||
|
files.sort((a, b) => b.created_at - a.created_at);
|
||||||
|
files.forEach((file) => {
|
||||||
|
const itemDOM = document.querySelector("template#item").content.cloneNode(true).children[0];
|
||||||
|
itemDOM.classList.add(`item-${file.short}`);
|
||||||
|
itemDOM.dataset.serverKey = file.short;
|
||||||
|
itemDOM.dataset.actionKey = file.token;
|
||||||
|
itemDOM.querySelector(".name").innerText = file.name;
|
||||||
|
itemDOM.querySelector(".download a").href = file.url;
|
||||||
|
itemDOM.querySelector(".delete-at-first-view .icon").classList.add(file.del_at_first_view ? "fa-eraser" : "fa-close");
|
||||||
|
itemDOM.querySelector(".created-at").innerText = formatDate(
|
||||||
|
file.created_at
|
||||||
|
);
|
||||||
|
itemDOM.querySelector(".expires-at").innerText = file.delay == 0 ? i18n.noExpiration : formatDate(file.delay * 86400 + file.created_at);
|
||||||
|
itemDOM.querySelector(
|
||||||
|
".mail a"
|
||||||
|
).href = `${actionURL}m?links=["${file.short}"]`;
|
||||||
|
itemDOM.querySelector(".action-delete-item").onclick = () => deleteFile(itemDOM);
|
||||||
|
itemDOM.querySelector(".checkbox input").onclick = () => {
|
||||||
|
checkItemSelection();
|
||||||
|
};
|
||||||
|
itemsTableDOM.append(itemDOM);
|
||||||
|
console.debug(file.short, file.token);
|
||||||
|
fetch(counterURL, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
short: file.short,
|
||||||
|
token: file.token
|
||||||
|
})
|
||||||
|
}).then((response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Request error: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
}).then((data) => {
|
||||||
|
const countDOM = itemDOM.querySelector(".counter");
|
||||||
|
if (data.success) {
|
||||||
|
countDOM.innerText = data.counter;
|
||||||
|
if (data.deleted) {
|
||||||
|
countDOM.parentElement.classList.add("deleted");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert(data.msg);
|
||||||
|
countDOM.parentElement.remove();
|
||||||
|
if (data.missing) {
|
||||||
|
deleteFromStorage(data.short);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).catch((error) => console.error(error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
populateFilesTable();
|
||||||
|
document.getElementById("action-select-all").onclick = updateSelection;
|
||||||
|
document.getElementById("action-export-storage").onclick = exportStorage;
|
||||||
|
document.getElementById("action-purge-expired").onclick = purgeExpired;
|
||||||
|
document.getElementById("action-import-storage").onchange = importStorage;
|
||||||
|
document.getElementById("action-delete-selection").onclick = deleteSelection;
|
||||||
|
});
|
|
@ -0,0 +1,235 @@
|
||||||
|
// themes/default/public/js/lib/filesize.esm.min.js
|
||||||
|
var t = "array";
|
||||||
|
var i = "bit";
|
||||||
|
var o = "bits";
|
||||||
|
var e = "byte";
|
||||||
|
var n = "bytes";
|
||||||
|
var r = "";
|
||||||
|
var a = "exponent";
|
||||||
|
var b = "function";
|
||||||
|
var s = "iec";
|
||||||
|
var l = "Invalid number";
|
||||||
|
var p = "Invalid rounding method";
|
||||||
|
var u = "jedec";
|
||||||
|
var c = "object";
|
||||||
|
var d = ".";
|
||||||
|
var f = "round";
|
||||||
|
var g = "s";
|
||||||
|
var m = "si";
|
||||||
|
var B = "kbit";
|
||||||
|
var y = "kB";
|
||||||
|
var h = " ";
|
||||||
|
var M = "string";
|
||||||
|
var x = "0";
|
||||||
|
var w = { symbol: { iec: { bits: ["bit", "Kibit", "Mibit", "Gibit", "Tibit", "Pibit", "Eibit", "Zibit", "Yibit"], bytes: ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"] }, jedec: { bits: ["bit", "Kbit", "Mbit", "Gbit", "Tbit", "Pbit", "Ebit", "Zbit", "Ybit"], bytes: ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] } }, fullform: { iec: ["", "kibi", "mebi", "gibi", "tebi", "pebi", "exbi", "zebi", "yobi"], jedec: ["", "kilo", "mega", "giga", "tera", "peta", "exa", "zetta", "yotta"] } };
|
||||||
|
function E(E2, { bits: T = false, pad: j = false, base: N = -1, round: P = 2, locale: S = r, localeOptions: k = {}, separator: G = r, spacer: K = h, symbols: Y = {}, standard: Z = r, output: v = M, fullform: O = false, fullforms: $ = [], exponent: z = -1, roundingMethod: I = f, precision: L = 0 } = {}) {
|
||||||
|
let D = z, q = Number(E2), A = [], C = 0, F = r;
|
||||||
|
Z === m ? (N = 10, Z = u) : Z === s || Z === u ? N = 2 : 2 === N ? Z = s : (N = 10, Z = u);
|
||||||
|
const H = 10 === N ? 1e3 : 1024, J = true === O, Q = q < 0, R = Math[I];
|
||||||
|
if ("bigint" != typeof E2 && isNaN(E2)) throw new TypeError(l);
|
||||||
|
if (typeof R !== b) throw new TypeError(p);
|
||||||
|
if (Q && (q = -q), (-1 === D || isNaN(D)) && (D = Math.floor(Math.log(q) / Math.log(H)), D < 0 && (D = 0)), D > 8 && (L > 0 && (L += 8 - D), D = 8), v === a) return D;
|
||||||
|
if (0 === q) A[0] = 0, F = A[1] = w.symbol[Z][T ? o : n][D];
|
||||||
|
else {
|
||||||
|
C = q / (2 === N ? Math.pow(2, 10 * D) : Math.pow(1e3, D)), T && (C *= 8, C >= H && D < 8 && (C /= H, D++));
|
||||||
|
const t2 = Math.pow(10, D > 0 ? P : 0);
|
||||||
|
A[0] = R(C * t2) / t2, A[0] === H && D < 8 && -1 === z && (A[0] = 1, D++), F = A[1] = 10 === N && 1 === D ? T ? B : y : w.symbol[Z][T ? o : n][D];
|
||||||
|
}
|
||||||
|
if (Q && (A[0] = -A[0]), L > 0 && (A[0] = A[0].toPrecision(L)), A[1] = Y[A[1]] || A[1], true === S ? A[0] = A[0].toLocaleString() : S.length > 0 ? A[0] = A[0].toLocaleString(S, k) : G.length > 0 && (A[0] = A[0].toString().replace(d, G)), j && P > 0) {
|
||||||
|
const t2 = A[0].toString(), i2 = G || (t2.match(/(\D)/g) || []).pop() || d, o2 = t2.toString().split(i2), e2 = o2[1] || r, n2 = e2.length, a2 = P - n2;
|
||||||
|
A[0] = `${o2[0]}${i2}${e2.padEnd(n2 + a2, x)}`;
|
||||||
|
}
|
||||||
|
return J && (A[1] = $[D] ? $[D] : w.fullform[Z][D] + (T ? i : e) + (1 === A[0] ? r : g)), v === t ? A : v === c ? { value: A[0], symbol: A[1], exponent: D, unit: F } : A.join(K);
|
||||||
|
}
|
||||||
|
|
||||||
|
// themes/default/public/js/list-invitations.js
|
||||||
|
var updateButtonsStatus = () => {
|
||||||
|
const targetSelectionDOM = document.querySelectorAll(".target-selection");
|
||||||
|
if (document.querySelectorAll(".column.selection .checkbox input:checked").length > 0) {
|
||||||
|
targetSelectionDOM.forEach((node) => node.disabled = false);
|
||||||
|
} else {
|
||||||
|
targetSelectionDOM.forEach((node) => node.disabled = true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var invertSelection = () => {
|
||||||
|
document.querySelectorAll(".item .column.selection input").forEach((node) => {
|
||||||
|
node.click();
|
||||||
|
});
|
||||||
|
updateButtonsStatus();
|
||||||
|
};
|
||||||
|
var toggleHidden = () => {
|
||||||
|
const invitationsListDOM = document.querySelector(".invitations-list");
|
||||||
|
const toggleButtonDOM = document.querySelector(".action-toggle-hidden");
|
||||||
|
const itemsHiddenDOM = invitationsListDOM.querySelectorAll(
|
||||||
|
".item[data-visibility='0']"
|
||||||
|
);
|
||||||
|
if (invitationsListDOM.getAttribute("data-visibility") === "hidden") {
|
||||||
|
toggleButtonDOM.innerText = i18n.hideText;
|
||||||
|
itemsHiddenDOM.forEach((item) => showNode(item));
|
||||||
|
invitationsListDOM.setAttribute("data-visibility", "shown");
|
||||||
|
} else {
|
||||||
|
toggleButtonDOM.innerText = i18n.showText;
|
||||||
|
itemsHiddenDOM.forEach((item) => {
|
||||||
|
hideNode(item);
|
||||||
|
const checkbox = item.querySelector("input");
|
||||||
|
if (checkbox.checked) {
|
||||||
|
checkbox.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
invitationsListDOM.setAttribute("data-visibility", "hidden");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var deleteInvitation = () => {
|
||||||
|
if (confirm(i18n.confirmDeleteInvit)) {
|
||||||
|
try {
|
||||||
|
fetch(deleteURL, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
|
||||||
|
},
|
||||||
|
body: getTokensBody()
|
||||||
|
}).then((response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Request error: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
}).then((data) => {
|
||||||
|
if (data.success) {
|
||||||
|
data.tokens.forEach((t2) => {
|
||||||
|
addToast(t2.msg, "success");
|
||||||
|
document.getElementById(`row-${t2.token}`).remove();
|
||||||
|
});
|
||||||
|
data.failures.forEach((msg) => {
|
||||||
|
addToast(msg, "error");
|
||||||
|
});
|
||||||
|
updateButtonsStatus();
|
||||||
|
} else {
|
||||||
|
data.failures.forEach((msg) => {
|
||||||
|
addToast(msg, "error");
|
||||||
|
});
|
||||||
|
if (data.msg) {
|
||||||
|
addToast(data.msg, "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var resendInvitation = () => {
|
||||||
|
if (confirm(i18n.confirmResendMail)) {
|
||||||
|
fetch(resendURL, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
|
||||||
|
},
|
||||||
|
body: getTokensBody()
|
||||||
|
}).then((response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Request error: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
}).then((data) => {
|
||||||
|
if (data.success) {
|
||||||
|
data.tokens.forEach((t2) => {
|
||||||
|
const itemDOM = document.getElementById(`row-${t2.token}`);
|
||||||
|
itemDOM.querySelector(".column.expiration-date").innerText = t2.expires;
|
||||||
|
itemDOM.querySelector(".column.selection input").click();
|
||||||
|
addToast(t2.msg, "success");
|
||||||
|
});
|
||||||
|
data.failures.forEach((msg) => {
|
||||||
|
addToast(msg, "error");
|
||||||
|
});
|
||||||
|
updateButtonsStatus();
|
||||||
|
}
|
||||||
|
}).catch((error) => console.error(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var toggleVisibility = () => {
|
||||||
|
fetch(toggleURL, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
|
||||||
|
},
|
||||||
|
body: getTokensBody()
|
||||||
|
}).then((response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Request error: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
}).then((data) => {
|
||||||
|
if (data.success) {
|
||||||
|
data.tokens.forEach((t2) => {
|
||||||
|
const itemDOM = document.getElementById(`row-${t2.token}`);
|
||||||
|
if (t2.show) {
|
||||||
|
itemDOM.setAttribute("data-visibility", 1);
|
||||||
|
showNode(itemDOM);
|
||||||
|
itemDOM.querySelector(".column.selection .icon.hide-source").remove();
|
||||||
|
} else {
|
||||||
|
itemDOM.setAttribute("data-visibility", 0);
|
||||||
|
if (document.querySelector(".invitations-list").getAttribute("data-visibility") === "hidden") {
|
||||||
|
hideNode(itemDOM);
|
||||||
|
}
|
||||||
|
itemDOM.querySelector(".column.selection").appendChild(
|
||||||
|
document.querySelector("template#icon-hide-source").content.cloneNode(true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
itemDOM.querySelector(".column.selection input").click();
|
||||||
|
});
|
||||||
|
updateButtonsStatus();
|
||||||
|
} else {
|
||||||
|
addToast(data.msg, "error");
|
||||||
|
}
|
||||||
|
}).catch((error) => console.error(error));
|
||||||
|
};
|
||||||
|
var getTokensBody = () => {
|
||||||
|
const tokens = new URLSearchParams();
|
||||||
|
document.querySelectorAll(".column.selection input:checked").forEach(
|
||||||
|
(item) => tokens.append("tokens[]", item.getAttribute("data-token"))
|
||||||
|
);
|
||||||
|
return tokens;
|
||||||
|
};
|
||||||
|
var fillModal = (event) => {
|
||||||
|
const buttonDOM = event.target;
|
||||||
|
const modalDOM = document.querySelector(".modal.files-info");
|
||||||
|
modalDOM.querySelector(".files-list").replaceChildren();
|
||||||
|
modalDOM.querySelector("h1").innerText = i18n.listFiles.replace("XX1", buttonDOM.getAttribute("data-token")).replace("XX2", buttonDOM.getAttribute("data-guest"));
|
||||||
|
const files = JSON.parse(buttonDOM.getAttribute("data-files")) || [];
|
||||||
|
const itemList = new DocumentFragment();
|
||||||
|
files.forEach((file) => {
|
||||||
|
const expires = i18n.expiration.replace(
|
||||||
|
"XXX",
|
||||||
|
formatDate(file.delay * 86400 + file.created_at)
|
||||||
|
);
|
||||||
|
const item = modalDOM.querySelector("template#item").content.cloneNode(true);
|
||||||
|
item.querySelector(".file-link").href = file.url;
|
||||||
|
item.querySelector(".file-link").value = file.name;
|
||||||
|
item.querySelector(".file-size").innerText = `${E(
|
||||||
|
file.size
|
||||||
|
)}, ${expires}`;
|
||||||
|
itemList.appendChild(item);
|
||||||
|
});
|
||||||
|
modalDOM.querySelector(".files-list").appendChild(itemList);
|
||||||
|
};
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
document.querySelectorAll(".modal-button.action-files-info").forEach(
|
||||||
|
(button) => button.onclick = (event) => {
|
||||||
|
fillModal(event);
|
||||||
|
document.querySelector(".modal.files-info").showModal();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
document.querySelector(".close-modal").onclick = () => {
|
||||||
|
document.querySelector(".modal").close();
|
||||||
|
};
|
||||||
|
document.querySelectorAll(".column.selection input").forEach((node) => node.onclick = updateButtonsStatus);
|
||||||
|
document.querySelector(".action-invert-selection").onclick = invertSelection;
|
||||||
|
document.querySelector(".action-toggle-hidden").onclick = toggleHidden;
|
||||||
|
document.querySelector(".action-delete-invitation").onclick = deleteInvitation;
|
||||||
|
document.querySelector(".action-resend-invitation").onclick = resendInvitation;
|
||||||
|
document.querySelector(".action-toggle-visibility").onclick = toggleVisibility;
|
||||||
|
});
|
||||||
|
/*!
|
||||||
|
2024 Jason Mulligan <jason.mulligan@avoidwork.com>
|
||||||
|
@version 10.1.6
|
||||||
|
*/
|
|
@ -0,0 +1,491 @@
|
||||||
|
import {
|
||||||
|
lufi,
|
||||||
|
errAsync,
|
||||||
|
okAsync,
|
||||||
|
ResultAsync,
|
||||||
|
CryptoAlgorithm,
|
||||||
|
JobStatus,
|
||||||
|
} from "~/lib/lufi.js";
|
||||||
|
import { copyToClipboard, formatDate, notify, uuidv4 } from "~/lib/utils.js";
|
||||||
|
|
||||||
|
import { filesize } from "~/lib/filesize.esm.min.js";
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const initCard = (cardType, cardId = null) => {
|
||||||
|
const card = document
|
||||||
|
.querySelector(`template#card-file-${cardType}`)
|
||||||
|
.content.cloneNode(true).children[0];
|
||||||
|
|
||||||
|
if (cardId) {
|
||||||
|
card.classList.add(`card-${encodeURIComponent(cardId)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
card
|
||||||
|
.querySelector("button .delete")
|
||||||
|
.parentNode.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === " " || event.key === "enter") {
|
||||||
|
event.target.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
card
|
||||||
|
.querySelector("button .delete")
|
||||||
|
.parentNode.addEventListener("click", () => {
|
||||||
|
card.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
return card;
|
||||||
|
};
|
||||||
|
|
||||||
|
const showErrorCard = (error, cardId, lufiFile = null) => {
|
||||||
|
const errorCard = initCard("error", cardId);
|
||||||
|
|
||||||
|
if (lufiFile) {
|
||||||
|
errorCard.querySelector(".name").innerText = lufiFile.name;
|
||||||
|
errorCard.querySelector(".size").innerText = filesize(lufiFile.size);
|
||||||
|
}
|
||||||
|
|
||||||
|
const actualCard = document.querySelector(`.card-${cardId}`);
|
||||||
|
|
||||||
|
if (actualCard.classList.contains("card-file-error")) {
|
||||||
|
errorCard.querySelector(
|
||||||
|
".message-body"
|
||||||
|
).innerText += `\n${error.message}`;
|
||||||
|
} else {
|
||||||
|
errorCard.querySelector(".message-body").innerText = error.message;
|
||||||
|
|
||||||
|
document
|
||||||
|
.getElementById("uploaded-files")
|
||||||
|
.replaceChild(errorCard, document.querySelector(`.card-${cardId}`));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add item to localStorage
|
||||||
|
*
|
||||||
|
* @param {string} name
|
||||||
|
* @param {string} url
|
||||||
|
* @param {number} size
|
||||||
|
* @param {boolean} del_at_first_view
|
||||||
|
* @param {number} created_at
|
||||||
|
* @param {number} delay
|
||||||
|
* @param {string} serverKey
|
||||||
|
* @param {string} actionToken
|
||||||
|
*/
|
||||||
|
const addItem = (
|
||||||
|
name,
|
||||||
|
url,
|
||||||
|
size,
|
||||||
|
del_at_first_view,
|
||||||
|
created_at,
|
||||||
|
delay,
|
||||||
|
serverKey,
|
||||||
|
actionToken
|
||||||
|
) => {
|
||||||
|
const files = JSON.parse(localStorage.getItem(`${prefix}files`)) || [];
|
||||||
|
|
||||||
|
console.debug(serverKey);
|
||||||
|
|
||||||
|
files.push({
|
||||||
|
name,
|
||||||
|
short: serverKey,
|
||||||
|
url,
|
||||||
|
size,
|
||||||
|
del_at_first_view,
|
||||||
|
created_at,
|
||||||
|
delay,
|
||||||
|
token: actionToken,
|
||||||
|
});
|
||||||
|
localStorage.setItem(`${prefix}files`, JSON.stringify(files));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [Invitation feature] Send URLs of files to server
|
||||||
|
*/
|
||||||
|
const sendFilesURLs = () => {
|
||||||
|
if (filesURLs.length > 0) {
|
||||||
|
fetch(sendFilesURLsURL, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
urls: filesURLs,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (data.success) {
|
||||||
|
notify(data.msg, "success");
|
||||||
|
} else {
|
||||||
|
notify(data.msg, "danger");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error:", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the upload of provided files
|
||||||
|
*
|
||||||
|
* @param {File[]} files
|
||||||
|
* @param {number} delay
|
||||||
|
* @param {boolean} delAtFirstView
|
||||||
|
* @param {boolean} isZipped
|
||||||
|
* @param {string} zipName
|
||||||
|
* @param {string} password
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const startUpload = (
|
||||||
|
files,
|
||||||
|
delay,
|
||||||
|
delAtFirstView,
|
||||||
|
isZipped,
|
||||||
|
zipName,
|
||||||
|
password
|
||||||
|
) => {
|
||||||
|
const uploadFilesDOM = document.getElementById("uploaded-files");
|
||||||
|
|
||||||
|
const serverUrl = new URL(ws_url.replace("/upload", ""));
|
||||||
|
serverUrl.protocol = serverUrl.protocol === "ws:" ? "http:" : "https:";
|
||||||
|
|
||||||
|
const cardId = isSecureContext ? crypto.randomUUID() : uuidv4();
|
||||||
|
|
||||||
|
const uploadingFileCard = initCard("uploading", cardId);
|
||||||
|
|
||||||
|
uploadingFileCard.querySelector(".name").innerText = zipName;
|
||||||
|
uploadingFileCard.querySelector(".size").innerText = i18n.unknownYet;
|
||||||
|
uploadingFileCard.querySelector(".info").innerText = i18n.compressing;
|
||||||
|
|
||||||
|
uploadFilesDOM.prepend(uploadingFileCard);
|
||||||
|
|
||||||
|
const runUpload = (job = null) => {
|
||||||
|
if (!job || job.status === JobStatus.COMPLETE) {
|
||||||
|
return lufi
|
||||||
|
.upload(
|
||||||
|
serverUrl.href,
|
||||||
|
files,
|
||||||
|
delay,
|
||||||
|
delAtFirstView,
|
||||||
|
isZipped,
|
||||||
|
zipName,
|
||||||
|
password,
|
||||||
|
isSecureContext ? CryptoAlgorithm.WebCrypto : CryptoAlgorithm.Sjcl
|
||||||
|
)
|
||||||
|
.andThen((jobs) =>
|
||||||
|
ResultAsync.combine(
|
||||||
|
jobs.map((job) => {
|
||||||
|
uploadingFileCard.querySelector(".name").innerText =
|
||||||
|
job.lufiFile.name;
|
||||||
|
uploadingFileCard.querySelector(".size").innerText = filesize(
|
||||||
|
job.lufiFile.size
|
||||||
|
);
|
||||||
|
|
||||||
|
uploadingFileCard.querySelector(".info").innerText =
|
||||||
|
i18n.uploading;
|
||||||
|
|
||||||
|
uploadingFileCard
|
||||||
|
.querySelector("button .delete")
|
||||||
|
.parentNode.addEventListener("click", () => {
|
||||||
|
lufi.cancel(job);
|
||||||
|
});
|
||||||
|
|
||||||
|
job.onProgress(() => {
|
||||||
|
updateProgressBar(job.lufiFile, uploadingFileCard);
|
||||||
|
});
|
||||||
|
|
||||||
|
return job
|
||||||
|
.waitForCompletion()
|
||||||
|
.andThen((job) => {
|
||||||
|
notify(i18n.fileUploaded, job.lufiFile.name);
|
||||||
|
const uploadedFileCard = initCard("uploaded", cardId);
|
||||||
|
|
||||||
|
const expirationDate =
|
||||||
|
job.lufiFile.delay === 0
|
||||||
|
? i18n.noLimit
|
||||||
|
: `${i18n.expiration} ${formatDate(
|
||||||
|
job.lufiFile.delay * 86400 + job.lufiFile.createdAt
|
||||||
|
)}`;
|
||||||
|
|
||||||
|
if (job.lufiFile.type === "application/zip") {
|
||||||
|
uploadedFileCard
|
||||||
|
.querySelector(".content .icon")
|
||||||
|
.classList.replace("fa-file", "fa-file-zipper");
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadedFileCard.querySelector(".name").innerText =
|
||||||
|
job.lufiFile.name;
|
||||||
|
uploadedFileCard.querySelector(".size").innerText =
|
||||||
|
filesize(job.lufiFile.size);
|
||||||
|
uploadedFileCard.querySelector(".expiration").innerText =
|
||||||
|
expirationDate;
|
||||||
|
uploadedFileCard.querySelector(".action-download").href =
|
||||||
|
job.lufiFile.downloadUrl();
|
||||||
|
uploadedFileCard.querySelector(".action-delete").href =
|
||||||
|
job.lufiFile.removeUrl();
|
||||||
|
uploadedFileCard.querySelector(".action-share").href = `${
|
||||||
|
job.lufiFile.serverUrl
|
||||||
|
}m?links=${encodeURIComponent(
|
||||||
|
'["' + job.lufiFile.keys.server + '"]'
|
||||||
|
)}`;
|
||||||
|
|
||||||
|
uploadedFileCard
|
||||||
|
.querySelector(".action-copy")
|
||||||
|
.addEventListener("click", async () => {
|
||||||
|
await copyToClipboard(job.lufiFile.downloadUrl());
|
||||||
|
|
||||||
|
uploadedFileCard.querySelector(
|
||||||
|
".action-copy .text"
|
||||||
|
).innerText = i18n.copied;
|
||||||
|
setTimeout(() => {
|
||||||
|
uploadedFileCard.querySelector(
|
||||||
|
".action-copy .text"
|
||||||
|
).innerText = i18n.copyLink;
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
uploadFilesDOM.replaceChild(
|
||||||
|
uploadedFileCard,
|
||||||
|
uploadingFileCard
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add the file to localStorage
|
||||||
|
if (!isGuest) {
|
||||||
|
addItem(
|
||||||
|
job.lufiFile.name,
|
||||||
|
job.lufiFile.downloadUrl(),
|
||||||
|
job.lufiFile.size,
|
||||||
|
delAtFirstView,
|
||||||
|
job.lufiFile.createdAt,
|
||||||
|
delay,
|
||||||
|
job.lufiFile.keys.server,
|
||||||
|
job.lufiFile.actionToken
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isGuest && job.lufiFile.keys.server !== null) {
|
||||||
|
filesURLs.push(
|
||||||
|
JSON.stringify({
|
||||||
|
name: job.lufiFile.name,
|
||||||
|
short: job.lufiFile.keys.server,
|
||||||
|
url: job.lufiFile.downloadUrl(),
|
||||||
|
size: job.lufiFile.size,
|
||||||
|
created_at: job.lufiFile.createdAt,
|
||||||
|
delay,
|
||||||
|
token: job.lufiFile.actionToken,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
sendFilesURLs();
|
||||||
|
}
|
||||||
|
|
||||||
|
return okAsync(job);
|
||||||
|
})
|
||||||
|
.orElse((error) => {
|
||||||
|
showErrorCard(error, cardId, job.lufiFile);
|
||||||
|
return errAsync(error);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.orElse((error) => {
|
||||||
|
showErrorCard(error, cardId);
|
||||||
|
|
||||||
|
return errAsync(error);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return okAsync(undefined);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isZipped) {
|
||||||
|
return lufi
|
||||||
|
.addFilesToArchive(files)
|
||||||
|
.andThen((archiveEntries) => lufi.compress(archiveEntries, zipName))
|
||||||
|
.andThen((job) => {
|
||||||
|
if (uploadingFileCard.querySelector(".name")) {
|
||||||
|
uploadingFileCard
|
||||||
|
.querySelector("button .delete")
|
||||||
|
.parentNode.addEventListener("click", () => {
|
||||||
|
job.terminate();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// If card has already been deleted,
|
||||||
|
job.terminate();
|
||||||
|
}
|
||||||
|
|
||||||
|
return job.waitForCompletion();
|
||||||
|
})
|
||||||
|
.andThen(runUpload)
|
||||||
|
.mapErr((error) => {
|
||||||
|
showErrorCard(error, cardId);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return runUpload();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the progress bar of the File Card
|
||||||
|
*
|
||||||
|
* @param {LufiFile} lufiFile
|
||||||
|
* @param {Node} cardDOM
|
||||||
|
*/
|
||||||
|
const updateProgressBar = (lufiFile, cardDOM) => {
|
||||||
|
const percent = Math.round(
|
||||||
|
(lufiFile.chunksReady * 100) / lufiFile.totalChunks
|
||||||
|
);
|
||||||
|
|
||||||
|
cardDOM.querySelector(".progress").value = percent;
|
||||||
|
cardDOM.querySelector(".progress").innerText = percent;
|
||||||
|
cardDOM.querySelector(".progress-text").innerText = percent + "%";
|
||||||
|
};
|
||||||
|
|
||||||
|
const fileInput = document.querySelector("#file-js-upload input[type=file]");
|
||||||
|
let providedFiles = [];
|
||||||
|
let totalSize = 0;
|
||||||
|
|
||||||
|
fileInput.onchange = () => {
|
||||||
|
const providedFilesDOM = document.getElementById("provided-files");
|
||||||
|
|
||||||
|
Array.from(fileInput.files).forEach((file) => {
|
||||||
|
if (!providedFiles.find((f) => file.name === f.name)) {
|
||||||
|
providedFiles.push(file);
|
||||||
|
|
||||||
|
const fileCard = initCard("to-upload");
|
||||||
|
|
||||||
|
fileCard.querySelector(".name").innerText = file.name;
|
||||||
|
fileCard.querySelector(".size").innerText = filesize(file.size);
|
||||||
|
|
||||||
|
fileCard
|
||||||
|
.querySelector("button .delete")
|
||||||
|
.parentNode.addEventListener("click", () => {
|
||||||
|
providedFiles = providedFiles.filter((f) => file.name !== f.name);
|
||||||
|
|
||||||
|
totalSize -= file.size;
|
||||||
|
document.querySelector(".total-size .size").innerText =
|
||||||
|
filesize(totalSize);
|
||||||
|
|
||||||
|
if (providedFiles.length === 0) {
|
||||||
|
document
|
||||||
|
.getElementById("upload-controls")
|
||||||
|
.classList.add("is-hidden");
|
||||||
|
providedFilesDOM.classList.add("is-hidden");
|
||||||
|
document
|
||||||
|
.querySelector("#file-js-upload .small-version")
|
||||||
|
.classList.add("is-hidden");
|
||||||
|
document
|
||||||
|
.querySelector("#file-js-upload .full-version")
|
||||||
|
.classList.remove("is-hidden");
|
||||||
|
|
||||||
|
showFullUploadZone();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
providedFilesDOM.append(fileCard);
|
||||||
|
totalSize += file.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector(".total-size .size").innerText =
|
||||||
|
filesize(totalSize);
|
||||||
|
|
||||||
|
showSmallUploadZone();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById("zip-multiple").onchange = () => {
|
||||||
|
document.getElementById("zip-name").classList.toggle("is-invisible");
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById("use-password").onchange = () => {
|
||||||
|
document
|
||||||
|
.getElementById("password-control")
|
||||||
|
.classList.toggle("is-invisible");
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById("password-preview-button").onclick = (event) => {
|
||||||
|
if (event.target.classList.contains("fa-eye")) {
|
||||||
|
event.target.classList.replace("fa-eye", "fa-eye-slash");
|
||||||
|
document.querySelector("#password-control input").type = "text";
|
||||||
|
} else {
|
||||||
|
event.target.classList.replace("fa-eye-slash", "fa-eye");
|
||||||
|
document.querySelector("#password-control input").type = "password";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById("upload-zone-button").onclick = () =>
|
||||||
|
fileInput.click();
|
||||||
|
|
||||||
|
document.getElementById("submit-button").onclick = () => {
|
||||||
|
const delay = document.getElementById("expiration-delay").value;
|
||||||
|
const deleteAfterDownload = document.getElementById(
|
||||||
|
"delete-at-first-download"
|
||||||
|
).checked;
|
||||||
|
const password = document.getElementById("use-password").checked
|
||||||
|
? document.getElementById("password-input").value
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const zipMultiple = document.getElementById("zip-multiple").checked;
|
||||||
|
const zipName = document.getElementById("zip-name").value;
|
||||||
|
|
||||||
|
const mustZip = providedFiles.length > 1 ? zipMultiple : false;
|
||||||
|
|
||||||
|
startUpload(
|
||||||
|
providedFiles,
|
||||||
|
delay,
|
||||||
|
deleteAfterDownload,
|
||||||
|
mustZip,
|
||||||
|
zipName,
|
||||||
|
password
|
||||||
|
);
|
||||||
|
|
||||||
|
fileInput.value = null;
|
||||||
|
providedFiles = [];
|
||||||
|
document.getElementById("provided-files").replaceChildren();
|
||||||
|
|
||||||
|
showFullUploadZone();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (maxSize) {
|
||||||
|
const maxSizeDOM = document.createElement("span");
|
||||||
|
maxSizeDOM.innerText = i18n.maxSize.replace("XXX", filesize(maxSize));
|
||||||
|
|
||||||
|
maxSizeDOM.classList.add("is-size-5");
|
||||||
|
|
||||||
|
document
|
||||||
|
.querySelector("#upload-box .file-cta .file-label")
|
||||||
|
.append(maxSizeDOM);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const showSmallUploadZone = () => {
|
||||||
|
document.getElementById("provided-files").classList.remove("is-hidden");
|
||||||
|
document.getElementById("upload-controls").classList.remove("is-hidden");
|
||||||
|
document
|
||||||
|
.querySelector("#file-js-upload .total-size")
|
||||||
|
.classList.remove("is-hidden");
|
||||||
|
|
||||||
|
document
|
||||||
|
.querySelector("#file-js-upload .small-version")
|
||||||
|
.classList.remove("is-hidden");
|
||||||
|
document
|
||||||
|
.querySelector("#file-js-upload .full-version")
|
||||||
|
.classList.add("is-hidden");
|
||||||
|
};
|
||||||
|
|
||||||
|
const showFullUploadZone = () => {
|
||||||
|
document.querySelector("#provided-files").classList.add("is-hidden");
|
||||||
|
document.getElementById("upload-controls").classList.add("is-hidden");
|
||||||
|
document
|
||||||
|
.querySelector("#file-js-upload .total-size")
|
||||||
|
.classList.add("is-hidden");
|
||||||
|
|
||||||
|
document
|
||||||
|
.querySelector("#file-js-upload .small-version")
|
||||||
|
.classList.add("is-hidden");
|
||||||
|
document
|
||||||
|
.querySelector("#file-js-upload .full-version")
|
||||||
|
.classList.remove("is-hidden");
|
||||||
|
};
|
|
@ -1,5 +1,5 @@
|
||||||
<section class="about-section">
|
<section class="section container">
|
||||||
<h1><%= l('About') %></h1>
|
<h2><%= l('About') %></h2>
|
||||||
<h3><%= l('What is Lufi?') %></h3>
|
<h3><%= l('What is Lufi?') %></h3>
|
||||||
<p><%= l('Lufi is a free (as in free speech) file hosting software.') %></p>
|
<p><%= l('Lufi is a free (as in free speech) file hosting software.') %></p>
|
||||||
<h3><%= l('Privacy') %></h3>
|
<h3><%= l('Privacy') %></h3>
|
||||||
|
|
|
@ -1,75 +1,114 @@
|
||||||
<section class="my-files-section">
|
|
||||||
<h1><%= l('My files') %></h1>
|
|
||||||
|
|
||||||
<p class="intro">
|
|
||||||
|
<div class="box">
|
||||||
|
<h1 class="title is-1 has-text-centered mb-6"><%= l('My files') %></h1>
|
||||||
|
|
||||||
|
<div class="intro">
|
||||||
<%= l('Only the files sent with this browser will be listed here. This list is stored in localStorage: if you delete your localStorage data, you\'ll lose this list.') %><br>
|
<%= l('Only the files sent with this browser will be listed here. This list is stored in localStorage: if you delete your localStorage data, you\'ll lose this list.') %><br>
|
||||||
<%= l('Rows in red mean that the files have expired and are no longer available.') %>
|
<%= l('Rows in red mean that the files have expired and are no longer available.') %>
|
||||||
</p>
|
|
||||||
<div class="buttons actions-buttons">
|
|
||||||
<a href="#" class="button action-export-storage">
|
|
||||||
<%= l('Export localStorage data') %>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<button type="button" class="button action-purge-expired">
|
|
||||||
<%= l('Purge expired files from localStorage') %>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<label class="button file-button">
|
|
||||||
<%= l('Import localStorage data') %>
|
|
||||||
<input type="file" class="input-file action-import-storage" >
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<button type="button" class="button action-invert-selection">
|
|
||||||
<%= l('Invert selection') %>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button type="button" class="button action-delete-selection" disabled="disabled">
|
|
||||||
<%= l('Delete selected files') %>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class="files-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th><%= l('Action') %></th>
|
|
||||||
<th><%= l('File name') %></th>
|
|
||||||
<th><%= l('Download link') %></th>
|
|
||||||
<th><%= l('Counter') %></th>
|
|
||||||
<th><%= l('Delete at first download?') %></th>
|
|
||||||
<th><%= l('Uploaded at') %></th>
|
|
||||||
<th><%= l('Expires at') %></th>
|
|
||||||
<th><%= l('Deletion link') %></th>
|
|
||||||
<th><%= l('Mail') %></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="files-items">
|
|
||||||
|
|
||||||
</tbody>
|
<div class="buttons">
|
||||||
</table>
|
<button id="action-delete-selection" type="button" class="button is-danger" disabled="disabled">
|
||||||
</section>
|
<span class="icon-text">
|
||||||
|
<span class="icon fas fa-trash"></span>
|
||||||
|
<span><%= l('Delete selected files') %></span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button id="action-purge-expired" type="button" class="button">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon fas fa-recycle"></span>
|
||||||
|
<span><%= l('Purge expired files') %></span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<a id="action-export-storage" href="#" class="button">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon fas fa-download"></span>
|
||||||
|
<span><%= l('Export local data') %></span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="file">
|
||||||
|
<label class="file-label">
|
||||||
|
<input id="action-import-storage" type="file" class="file-input" name="import-local-data">
|
||||||
|
<span class="file-cta">
|
||||||
|
<span class="file-icon">
|
||||||
|
<span class="fas fa-upload"></span>
|
||||||
|
</span>
|
||||||
|
<span class="file-label">
|
||||||
|
<%= l('Import local data') %>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-container">
|
||||||
|
<table class="table is-stripped is-hoverable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="has-text-centered">
|
||||||
|
<span><%= l('Selection') %></span>
|
||||||
|
<div class="checkbox">
|
||||||
|
<input type="checkbox" id="action-select-all">
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th class="has-text-centered"><%= l('File name') %></th>
|
||||||
|
<th class="has-text-centered"><%= l('Counter') %></th>
|
||||||
|
<th class="has-text-centered"><%= l('Delete at first download?') %></th>
|
||||||
|
<th class="has-text-centered"><%= l('Uploaded at') %></th>
|
||||||
|
<th class="has-text-centered"><%= l('Expires at') %></th>
|
||||||
|
<th class="has-text-centered"><%= l('Download link') %></th>
|
||||||
|
<th class="has-text-centered"><%= l('Share link') %></th>
|
||||||
|
<th class="has-text-centered"><%= l('Deletion link') %></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody id="items-table"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<template id="item">
|
<template id="item">
|
||||||
<tr class="item">
|
<tr class="item">
|
||||||
<td class="column selection">
|
<td class="selection is-vcentered has-text-centered">
|
||||||
<div class="checkbox input-delete-on-first-view">
|
<div class="checkbox input-delete-on-first-view">
|
||||||
<input type="checkbox" autocomplete="off" aria-label="Select">
|
<input type="checkbox" autocomplete="off" aria-label="Select">
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="column name"></td>
|
<td class="name is-vcentered"></td>
|
||||||
<td class="column download">
|
<td class="counter is-vcentered"></td>
|
||||||
<a class="icon-button icon download" title="<%= l('Download') %>" href="#"></a>
|
<td class="delete-at-first-view is-vcentered has-text-centered">
|
||||||
|
<span class="icon fas"></span>
|
||||||
</td>
|
</td>
|
||||||
<td class="column counter"></td>
|
<td class="created-at is-vcentered"></td>
|
||||||
<td class="column delete-at-first-view">
|
<td class="expires-at is-vcentered"></td>
|
||||||
<span class="icon"></span>
|
<td class="download is-vcentered">
|
||||||
|
<a title="<%= l('Download') %>" href="#">
|
||||||
|
<span class="icon-text is-justify-content-center">
|
||||||
|
<span class="icon fas fa-download" aria-hidden="true"></span>
|
||||||
|
<span><%= l('Download') %></span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="column created-at"></td>
|
<td class="mail is-vcentered">
|
||||||
<td class="column expires-at"></td>
|
<a title="<%= l('Mail') %>" href="#">
|
||||||
<td class="column deletion">
|
<span class="icon-text is-justify-content-center">
|
||||||
<button type="button" class="icon icon-button delete" title="<%= l('Delete') %>"></button>
|
<span class="icon fas fa-envelope" aria-hidden="true"></span>
|
||||||
|
<span><%= l('Share') %></span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="column mail">
|
<td class="deletion is-vcentered">
|
||||||
<a class="icon icon-button mail" title="<%= l('Mail') %>" href="#"></a>
|
<a class="action-delete-item" title="<%= l('Delete') %>" href="#">
|
||||||
|
<span class="icon-text is-justify-content-center">
|
||||||
|
<span class="icon fas fa-trash" aria-hidden="true"></span>
|
||||||
|
<span><%= l('Delete') %></span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
|
@ -90,4 +129,4 @@ const i18n = {
|
||||||
importFilesWithoutPrefix: "<%= l('Lufi recently changed its way to store files information.\n\nNo files have been found in the new localStorage location but we found files in the old one.\nDo you want to import those informations?\n\nPlease note that this is the only time that we will ask you this.') %>",
|
importFilesWithoutPrefix: "<%= l('Lufi recently changed its way to store files information.\n\nNo files have been found in the new localStorage location but we found files in the old one.\nDo you want to import those informations?\n\nPlease note that this is the only time that we will ask you this.') %>",
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
%= javascript '/js/lufi-files.js'
|
%= javascript '/js/minified/files.min.js', type => "module", defer => "true"
|
||||||
|
|
|
@ -4,250 +4,311 @@
|
||||||
% delay_365 => l('1 year')
|
% delay_365 => l('1 year')
|
||||||
% );
|
% );
|
||||||
|
|
||||||
<section class="messages-zone">
|
|
||||||
<noscript class="message-card error">
|
|
||||||
<%= l('Javascript is disabled. You won\'t be able to use Lufi.') %>
|
|
||||||
</noscript>
|
|
||||||
% if (defined(config('broadcast_message'))) {
|
|
||||||
<div class="message-card error">
|
|
||||||
<%= config('broadcast_message') %>
|
|
||||||
</div>
|
|
||||||
% }
|
|
||||||
% if (stash('invitation')) {
|
|
||||||
<div class="message-card success">
|
|
||||||
<%= l('The link(s) of your file(s) will automatically be sent by mail to %1 (%2)', stash('invitation')->ldap_user, stash('invitation')->ldap_user_mail) %>
|
|
||||||
</div>
|
|
||||||
% }
|
|
||||||
% if (stop_upload) {
|
|
||||||
<div class="message-card error">
|
|
||||||
<%= l('Sorry, the uploading is currently disabled. Please try again later.') %>
|
|
||||||
</div>
|
|
||||||
% }
|
|
||||||
</section>
|
|
||||||
|
|
||||||
% if (!stop_upload) {
|
% if (!stop_upload) {
|
||||||
<section class="upload-zone">
|
<div id="upload-box" class="box columns is-desktop">
|
||||||
<form class="upload-form">
|
<div class="left-zone column">
|
||||||
<div class="input-delete-after-days">
|
<div class="fixed-grid has-1-cols">
|
||||||
<button type="button" class="icon-button info-delete-after-days modal-button" title="<%= l('Important: more information on delays') %>"></button>
|
<div class="grid">
|
||||||
|
<div id="file-js-upload" class="cell fixed-grid has-1-cols">
|
||||||
<select class="select-delete-after-days" id="delete-after-days" aria-label="Delay">
|
<div class="grid file is-boxed is-fullwidth">
|
||||||
% for my $delay (qw/0 1 7 30 365/) {
|
<div id="provided-files" class="cell is-fullwidth is-rounded p-0 is-hidden"></div>
|
||||||
% my $text = ($delay == 7 || $delay == 30) ? l('%1 days', $delay) : $d{'delay_'.$delay};
|
|
||||||
% if (max_delay) {
|
<div class="cell total-size p-2 is-hidden">
|
||||||
% if ($delay) {
|
<span><%= l('Total size:')%></span>
|
||||||
% if ($delay < max_delay) {
|
<span class="size"></span>
|
||||||
<option value="<%= $delay %>" <%== is_selected($delay) %>><%= $text %></option>
|
</div>
|
||||||
% } elsif ($delay == max_delay) {
|
|
||||||
<option value="<%= $delay %>" <%== is_selected($delay) %>><%= $text %></option>
|
<label class="file-label is-clickable">
|
||||||
|
<input class="file-input" type="file" name="upload" multiple>
|
||||||
|
|
||||||
% last;
|
<div class="file-cta px-0 is-size-1 full-version">
|
||||||
% } else {
|
<span class="file-icon mr-0">
|
||||||
% my $text = ($delay == 1) ? l('24 hours') : l('%1 days', $delay);
|
<span class="icon fas fa-circle-plus"></span>
|
||||||
<option value="<%= max_delay %>" <%== is_selected(max_delay) %>><%= l('%1 days', max_delay) %></option>
|
</span>
|
||||||
% last;
|
<div class="file-label has-text-centered is-gap-3">
|
||||||
% }
|
<span class="is-size-3"><%= l('Drop files here') %></span>
|
||||||
% }
|
<span class="is-size-5"><%= l('or') %></span>
|
||||||
% } else {
|
<button type="button" id="upload-zone-button" class="button m-auto is-primary" tabindex=-1><%= l('Click to open the file browser') %></button>
|
||||||
<option value="<%= $delay %>" <%== is_selected($delay) %>><%= $text %></option>
|
</div>
|
||||||
% }
|
</div>
|
||||||
% }
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="checkbox input-delete-on-first-view">
|
<div class="file-cta px-0 is-small small-version has-text-primary is-hidden">
|
||||||
<input type="checkbox" id="delete-on-first-view" name="delete-on-first-view" autocomplete="off" <%= 'checked="true"' if config('force_burn_after_reading') %> <%= 'disabled="disabled"' if config('force_burn_after_reading') %>>
|
<span class="file-icon mr-0">
|
||||||
<label for="delete-on-first-view"><%= l('Delete at first download?') %></label>
|
<span class="icon fas fa-circle-plus"></span>
|
||||||
</div>
|
</span>
|
||||||
|
<div class="file-label has-text-centered">
|
||||||
% if (config('allow_pwd_on_files') && (!stash('invitation'))) {
|
<span><%= l('Add files') %></span>
|
||||||
<div class="input-text input-password">
|
</div>
|
||||||
<label for="password"><%= l('Add a password to file(s)') %></label>
|
</div>
|
||||||
<input type="password" id="password" placeholder="<%= l('Password') %>" autocomplete="off">
|
</label>
|
||||||
</div>
|
</div>
|
||||||
% }
|
|
||||||
|
|
||||||
<div class="checkbox input-zip-files">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="must-zip"
|
|
||||||
>
|
|
||||||
<label for="must-zip"><%= l('Create a zip archive with the files before uploading?') %></label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hidden input-text input-zip-name">
|
|
||||||
<label for="zip-name"><%= l('Name of the zip file') %></label>
|
|
||||||
<input type="text" id="zip-name" name="zip-name" placeholder="documents.zip" value="documents.zip" autocomplete="off">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="drop-zone">
|
|
||||||
<h1><%= l('Drop files here') %></h1>
|
|
||||||
<p class="max-file-size"></p>
|
|
||||||
<p><%= l('or') %></p>
|
|
||||||
<label class="button" for="upload-button">
|
|
||||||
<%= l('Click to open the file browser') %>
|
|
||||||
</label>
|
|
||||||
<input type="file" id="upload-button" multiple autofocus>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="hidden zip-zone card" aria-hidden="true">
|
|
||||||
<button type="button" class="icon-button icon close action-close" title="<%= l('Close') %>"></button>
|
|
||||||
|
|
||||||
<div class="file-description">
|
|
||||||
<p class="file-name">documents.zip</p>
|
|
||||||
<p class="file-size">0</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul class="files-list"></ul>
|
|
||||||
|
|
||||||
<button type="button" class="button action-upload-zip"><%= l("Upload generated zip file") %></button>
|
|
||||||
<div class="zip-compressing hidden" aria-hidden="true">
|
|
||||||
<div class="icon-container pulse">
|
|
||||||
<span class="icon archive"></span>
|
|
||||||
</div>
|
</div>
|
||||||
<p><%= l('Compressing zip file…') %></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="hidden uploaded-zone" aria-hidden="true">
|
<div id="upload-controls" class="cell is-hidden">
|
||||||
<h1><%= l('Uploaded files') %></h1>
|
<div class="field">
|
||||||
<header class="hidden buttons" aria-hidden="true">
|
<div class="field is-horizontal is-align-items-center">
|
||||||
<div class="actions-buttons upload-success">
|
<div class="field-label">
|
||||||
<button type="button" class="button action-copy-links"><%= l('Copy all links to clipboard') %></button>
|
<label class="label<%= ' disabled' if(max_delay) %>" for="expiration-delay">
|
||||||
<a class="button action-mail-links" href="<%= url_for('/')->to_abs() %>m"><%= l('Send all links by email') %></a>
|
<%= l('Expires after') %>
|
||||||
</div>
|
</label>
|
||||||
</header>
|
</div>
|
||||||
|
|
||||||
<div class="file-cards"></div>
|
<div class="field-body is-align-items-center">
|
||||||
</section>
|
<div class="field">
|
||||||
|
<div class="control">
|
||||||
<template id="file-card-error">
|
<div class="select">
|
||||||
<article class="card file-card error">
|
<select id="expiration-delay" <%= 'disabled' if(max_delay) %>>
|
||||||
<button type="button" class="icon-button icon close action-close"></button>
|
% for my $delay (qw/0 1 7 30 365/) {
|
||||||
<div class="file-description">
|
% my $text = ($delay == 7 || $delay == 30) ? l('%1 days', $delay) : $d{'delay_'.$delay};
|
||||||
<p class="file-name"></p>
|
% if (max_delay) {
|
||||||
<p class="file-size"></p>
|
% if ($delay) {
|
||||||
</div>
|
% if ($delay < max_delay) {
|
||||||
|
<option value="<%= $delay %>" <%= is_selected($delay) %>><%= $text %></option>
|
||||||
|
% } elsif ($delay == max_delay) {
|
||||||
|
<option value="<%= $delay %>" <%= is_selected($delay) %>><%= $text %></option>
|
||||||
|
|
||||||
|
% last;
|
||||||
|
% } else {
|
||||||
|
% my $text = ($delay == 1) ? l('24 hours') : l('%1 days', $delay);
|
||||||
|
<option value="<%= max_delay %>" <%= is_selected(max_delay) %>><%= l('%1 days', max_delay) %></option>
|
||||||
|
% last;
|
||||||
|
% }
|
||||||
|
% }
|
||||||
|
% } else {
|
||||||
|
<option value="<%= $delay %>" <%= is_selected($delay) %>><%= $text %></option>
|
||||||
|
% }
|
||||||
|
% }
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="message-card error upload-error"></div>
|
<div class="field is-narrow">or</div>
|
||||||
</article>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template id="file-card-guest">
|
|
||||||
<article class="card file-card guest">
|
|
||||||
<button type="button" class="icon-button icon close action-close"></button>
|
|
||||||
<div class="file-description">
|
|
||||||
<p class="file-name"></p>
|
|
||||||
<p class="file-size"></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="file-expiration"></p>
|
|
||||||
</article>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template id="file-card-ongoing">
|
<fieldset <%= 'disabled' if config('force_burn_after_reading') %>>
|
||||||
<article class="card file-card ongoing">
|
<div class="field">
|
||||||
<button type="button" class="icon-button icon close action-close"></button>
|
<div class="control">
|
||||||
<div class="file-description">
|
<label for="delete-at-first-download" class="label is-flex is-gap-1 checkbox">
|
||||||
<p class="file-name"></p>
|
<%= l('After first download?') %>
|
||||||
<p class="file-size"></p>
|
<input
|
||||||
</div>
|
type="checkbox"
|
||||||
|
id="delete-at-first-download"
|
||||||
|
<%= 'checked disabled' if config('force_burn_after_reading') %>
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p class="progress-percent">0</p>
|
<div class="field is-horizontal is-align-items-center<%= ' is-hidden' if (!config('allow_pwd_on_files') || stash('invitation')) %>">
|
||||||
|
<div class="field-label">
|
||||||
<div class="progress-container">
|
<label for="use-password" class="label">
|
||||||
<div class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
|
<%= l('Protect with password') %>
|
||||||
</div>
|
</label>
|
||||||
</article>
|
</div>
|
||||||
</template>
|
<div class="field-body">
|
||||||
|
<div class="field is-grouped is-align-items-center">
|
||||||
|
<div class="control">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="use-password"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label id="password-control" class="label is-invisible">
|
||||||
|
Password
|
||||||
|
<div class="control has-icons-left has-icons-right is-expanded">
|
||||||
|
<input class="input" id="password-input" type="password" placeholder="<%= l('Password') %>" autocomplete="off" value="">
|
||||||
|
|
||||||
|
<span class="icon is-small is-left fas fa-lock" aria-hidden="true"></span>
|
||||||
|
<span id="password-preview-button" class="icon is-small is-right fas fa-eye is-clickable has-text-primary" aria-hidden="true"></span>
|
||||||
|
</div>
|
||||||
|
<label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<template id="file-card-success">
|
<div class="field is-horizontal is-align-items-center">
|
||||||
<article class="card file-card success">
|
<div class="field-label">
|
||||||
<button type="button" class="icon-button icon close action-close"></button>
|
<label for="zip-multiple" class="label">
|
||||||
<div class="file-description">
|
<%= l('Compress multiple files') %>
|
||||||
<p class="file-name"></p>
|
</label>
|
||||||
<p class="file-size"></p>
|
</div>
|
||||||
<a class="icon-button icon mail action-mail" href="#"></a>
|
<div class="field-body">
|
||||||
</div>
|
<div class="field is-grouped is-align-items-center">
|
||||||
|
<div class="control">
|
||||||
<p class="file-expiration"></p>
|
<input
|
||||||
|
type="checkbox"
|
||||||
<div class="links-zone">
|
id="zip-multiple"
|
||||||
<div class="download-link">
|
checked
|
||||||
<a class="icon-button icon download download-button" title="<%= l('Download link') %>" href="#"></a>
|
>
|
||||||
<button type="button" title="<%= l('Copy to clipboard') %>" class="icon-button icon copy copy-button"></button>
|
</div>
|
||||||
<div class="input-text">
|
|
||||||
<label>
|
<div class="control is-expanded">
|
||||||
<%= l('Download link') %>
|
<label class="label">
|
||||||
<input class="download-input" type="text">
|
<%= l('Name of the zip file') %>
|
||||||
</label>
|
<input id="zip-name" type="text" class="input" value="documents.zip" placeholder="documents.zip">
|
||||||
</div>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="delete-link">
|
</div>
|
||||||
<a class="icon-button icon delete delete-button" title="<%= l('Deletion link') %>" href="#"></a>
|
</div>
|
||||||
<div class="input-text">
|
</div>
|
||||||
<label>
|
|
||||||
<%= l('Deletion link') %>
|
<div class="field">
|
||||||
<input class="delete-input" type="text">
|
<div class="control has-text-centered">
|
||||||
</label>
|
<button id="submit-button" type="button" class="button is-primary is-medium is-fullwidth"><%= l('Send') %></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</div>
|
||||||
</template>
|
|
||||||
|
|
||||||
<dialog class="modal modal-delete-after-days">
|
<div class="right-zone column">
|
||||||
% use Number::Bytes::Human qw(format_bytes);
|
<div id="uploaded-files" class="block">
|
||||||
<section>
|
|
||||||
<header>
|
|
||||||
<h1><%= l('Information about delays') %></h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="content">
|
|
||||||
<p>
|
|
||||||
<%= l('If you choose a delay, the file will be deleted after that delay.') %><br>
|
|
||||||
<%= l('Don\'t worry: if a user begins to download the file before the expiration and the download ends after the expiration, he will be able to get the file.') %>
|
|
||||||
</p>
|
|
||||||
% if (defined(config('delay_for_size'))) {
|
|
||||||
<p>
|
|
||||||
<%= l('This server sets limitations according to the file size. The expiration delay of your file will be the minimum between what you choose and the following limitations:') %>
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
% my $delays = config('delay_for_size');
|
|
||||||
% $delays->{0} = max_delay;
|
|
||||||
% my @keys = sort {$a <=> $b} keys %{$delays};
|
|
||||||
% my $i = 0;
|
|
||||||
% for my $key (@keys) {
|
|
||||||
% my $delay = $delays->{$keys[$i]};
|
|
||||||
% if ($i + 1 < scalar(@keys)) {
|
|
||||||
% if ($delay) {
|
|
||||||
<li><%= l('between %1 and %2, the file will be kept %3 day(s).', format_bytes($keys[$i]), format_bytes($keys[$i + 1]), $delay) %></li>
|
|
||||||
% } else {
|
|
||||||
<li><%= l('between %1 and %2, the file will be kept forever.', format_bytes($keys[$i]), format_bytes($keys[$i + 1]), $delay) %></li>
|
|
||||||
% }
|
|
||||||
% } else {
|
|
||||||
% if ($delay) {
|
|
||||||
<li><%= l('for %1 and more, the file will be kept %2 day(s)', format_bytes($keys[$i]), $delay) %></li>
|
|
||||||
% } else {
|
|
||||||
<li><%= l('for %1 and more, the file will be kept forever.', format_bytes($keys[$i]), $delay) %></li>
|
|
||||||
% }
|
|
||||||
% }
|
|
||||||
% $i++;
|
|
||||||
% }
|
|
||||||
</ul>
|
|
||||||
% }
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<footer class="actions">
|
</div>
|
||||||
<button autofocus class="button close-modal"><%= l('Close') %></button>
|
|
||||||
</footer>
|
|
||||||
</section>
|
|
||||||
</dialog>
|
|
||||||
% }
|
% }
|
||||||
|
|
||||||
|
<template id="card-file-error">
|
||||||
|
<article class="card-file-error card m-2 is-size-6">
|
||||||
|
<div class="card-content p-2">
|
||||||
|
<div class="content fixed-grid has-12-cols">
|
||||||
|
<div class="grid is-vcentered">
|
||||||
|
<div class="cell is-col-1">
|
||||||
|
<span class="icon fas fa-file fa-2x is-large has-text-primary"></span>
|
||||||
|
</div>
|
||||||
|
<div class="cell is-col-span-10">
|
||||||
|
<strong class="is-block wb-all name is-overflow-x"></strong>
|
||||||
|
<small class="is-block size mt-1"></small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cell is-col-12 ">
|
||||||
|
<button class="is-narrow card-header-icon" aria-label="<%= l('Close') %>">
|
||||||
|
<span class="delete"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="message is-danger has-text-danger">
|
||||||
|
<div class="message-body"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="card-file-to-upload">
|
||||||
|
<article class="card-file-to-upload card m-2 is-size-6">
|
||||||
|
<div class="card-content p-2">
|
||||||
|
<div class="content fixed-grid has-12-cols">
|
||||||
|
<div class="grid is-vcentered">
|
||||||
|
<div class="cell is-col-1">
|
||||||
|
<span class="icon fas fa-file fa-2x is-large has-text-primary" aria-hidden="true"></span>
|
||||||
|
</div>
|
||||||
|
<div class="cell is-col-span-10">
|
||||||
|
<strong class="is-block wb-all name is-overflow-x"></strong>
|
||||||
|
<small class="is-block size mt-1"></small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cell is-col-12 ">
|
||||||
|
<button class="is-narrow card-header-icon" aria-label="<%= l('Close') %>">
|
||||||
|
<span class="delete"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="card-file-uploading">
|
||||||
|
<article class="card-file-uploading card m-2 is-size-6">
|
||||||
|
<div class="card-content p-2">
|
||||||
|
<div class="content fixed-grid has-12-cols">
|
||||||
|
<div class="grid is-vcentered">
|
||||||
|
<div class="cell is-col-1">
|
||||||
|
<span class="icon fas fa-file fa-2x is-large has-text-primary" aria-hidden="true"></span>
|
||||||
|
</div>
|
||||||
|
<div class="cell is-col-span-10">
|
||||||
|
<strong class="is-block wb-all name is-overflow-x"></strong>
|
||||||
|
<small class="is-block size mt-1"></small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="cell is-col-12 is-narrow has-text-right card-header-icon" aria-label="<%= l('Close') %>">
|
||||||
|
<span class="delete"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="block info"><%= l('Compression in progress') %></div>
|
||||||
|
|
||||||
|
<div class="progress-text has-text-centered">0%</div>
|
||||||
|
<progress class="progress is-primary is-small" max="100">0%</progress>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="card-file-uploaded">
|
||||||
|
<article class="card-file-uploaded card m-2 is-size-6">
|
||||||
|
<div class="card-content p-2">
|
||||||
|
<div class="content fixed-grid has-12-cols">
|
||||||
|
<div class="grid is-vcentered">
|
||||||
|
<div class="cell is-col-1">
|
||||||
|
<span class="icon fas fa-file fa-2x is-large has-text-primary" aria-hidden="true"></span>
|
||||||
|
</div>
|
||||||
|
<div class="cell is-col-span-10">
|
||||||
|
<strong class="is-block wb-all name is-overflow-x name"></strong>
|
||||||
|
<small class="is-block size mt-1 size"></small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="cell is-col-12 is-narrow has-text-right card-header-icon" aria-label="<%= l('Close') %>">
|
||||||
|
<span class="delete"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="expiration"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
% if (!defined stash('invitation')) {
|
||||||
|
<footer class="card-footer is-align-items-center">
|
||||||
|
<a href="#" class="card-footer-item action-copy">
|
||||||
|
<span class="icon-text is-justify-content-center">
|
||||||
|
<span class="icon fas fa-copy" aria-hidden="true"></span>
|
||||||
|
<span class="text"><%= l('Copy link') %></span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="card-footer-item action-download">
|
||||||
|
<span class="icon-text is-justify-content-center">
|
||||||
|
<span class="icon fas fa-download" aria-hidden="true"></span>
|
||||||
|
<span><%= l('Download') %></span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="card-footer-item action-share">
|
||||||
|
<span class="icon-text is-justify-content-center">
|
||||||
|
<span class="icon fas fa-envelope" aria-hidden="true"></span>
|
||||||
|
<span><%= l('Share') %></span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="card-footer-item has-text-danger action-delete">
|
||||||
|
<span class="icon-text is-justify-content-center">
|
||||||
|
<span class="icon fas fa-trash" aria-hidden="true"></span>
|
||||||
|
<span><%= l('Delete') %></span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</footer>
|
||||||
|
% }
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
const ws_url = '<%= url_for('upload')->to_abs() %>';
|
const ws_url = '<%= url_for('upload')->to_abs() %>';
|
||||||
% if (defined($self->config('fixed_domain')) && $self->config('fixed_domain')) {
|
% if (defined($self->config('fixed_domain')) && $self->config('fixed_domain')) {
|
||||||
|
@ -258,7 +319,10 @@ const baseURL = '<%= url_for('/')->to_abs() %>';
|
||||||
const actionURL = '<%= url_for('/')->to_abs() %>';
|
const actionURL = '<%= url_for('/')->to_abs() %>';
|
||||||
const i18n = {
|
const i18n = {
|
||||||
enqueued: '<%= l('XXX file has been added to upload queue.') %>',
|
enqueued: '<%= l('XXX file has been added to upload queue.') %>',
|
||||||
|
compressing: '<%= l('Compression in progress...') %>',
|
||||||
confirmExit: '<%= l('You have attempted to leave this page. The upload will be canceled. Are you sure?') %>',
|
confirmExit: '<%= l('You have attempted to leave this page. The upload will be canceled. Are you sure?') %>',
|
||||||
|
copied: '<%= l('Copied') %>',
|
||||||
|
copyLink: '<%= l('Copy link') %>',
|
||||||
copyAll: '<%= l('Copy all links to clipboard') %>',
|
copyAll: '<%= l('Copy all links to clipboard') %>',
|
||||||
copySuccess: '<%= l('The link(s) has been copied to your clipboard') %>',
|
copySuccess: '<%= l('The link(s) has been copied to your clipboard') %>',
|
||||||
copyFail: '<%= l('Unable to copy the link(s) to your clipboard') %>',
|
copyFail: '<%= l('Unable to copy the link(s) to your clipboard') %>',
|
||||||
|
@ -275,6 +339,8 @@ const i18n = {
|
||||||
maxSize: '<%= l('(max size: XXX)') %>',
|
maxSize: '<%= l('(max size: XXX)') %>',
|
||||||
noLimit: '<%= l('No expiration delay') %>',
|
noLimit: '<%= l('No expiration delay') %>',
|
||||||
sending: '<%= l('Sending part XX1 of XX2. Please, be patient, the progress bar can take a while to move.') %>',
|
sending: '<%= l('Sending part XX1 of XX2. Please, be patient, the progress bar can take a while to move.') %>',
|
||||||
|
unknownYet: '<%= l('Unknown yet') %>',
|
||||||
|
uploading: '<%= l('Upload in progress...') %>',
|
||||||
wsProblem: '<%= l('Websocket communication error') %>',
|
wsProblem: '<%= l('Websocket communication error') %>',
|
||||||
};
|
};
|
||||||
const maxSize = <%= config('max_file_size') || 0 %>;
|
const maxSize = <%= config('max_file_size') || 0 %>;
|
||||||
|
@ -286,20 +352,6 @@ const sendFilesURLsURL = '<%= url_for('guest_send_mail', token => stash('invitat
|
||||||
const isGuest = false;
|
const isGuest = false;
|
||||||
% }
|
% }
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
document.querySelector(".drop-zone").onkeydown = (event) => {
|
|
||||||
if(event.key === " ") {
|
|
||||||
document.getElementById("file-browser").click();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.querySelector(".info-delete-after-days").onclick = () => {
|
|
||||||
document.querySelector(".modal-delete-after-days").showModal();
|
|
||||||
}
|
|
||||||
|
|
||||||
document.querySelector(".close-modal").onclick = () => {
|
|
||||||
document.querySelector(".modal").close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
%= javascript '/js/lufi-upload.js', type => 'module', defer => "true"
|
|
||||||
|
%= javascript '/js/minified/upload.min.js', type => 'module', defer => "true"
|
|
@ -109,4 +109,4 @@ const deleteURL = '<%= url_for('invite_list_delete') %>';
|
||||||
const resendURL = '<%= url_for('invite_list_resend') %>';
|
const resendURL = '<%= url_for('invite_list_resend') %>';
|
||||||
const toggleURL = '<%= url_for('invite_list_visibility') %>';
|
const toggleURL = '<%= url_for('invite_list_visibility') %>';
|
||||||
</script>
|
</script>
|
||||||
%= javascript '/js/lufi-list-invitations.js', type => 'module'
|
%= javascript '/js/min.lufi-list-invitations.js', type => 'module', defer => "true"
|
||||||
|
|
|
@ -17,74 +17,137 @@
|
||||||
<link rel="apple-touch-icon" sizes="152x152" href="<%= url_for('/img/lufi152.png') %>">
|
<link rel="apple-touch-icon" sizes="152x152" href="<%= url_for('/img/lufi152.png') %>">
|
||||||
<link rel="apple-touch-icon-precomposed" sizes="128x128" href="<%= url_for('/img/lufi128.png') %>">
|
<link rel="apple-touch-icon-precomposed" sizes="128x128" href="<%= url_for('/img/lufi128.png') %>">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link rel="preload" href="/css/lufi.css" as="style">
|
<link rel="stylesheet" href="/css/main.min.css">
|
||||||
<link rel="stylesheet" href="/css/lufi.css">
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header role="banner">
|
<header class="header" role="banner">
|
||||||
<nav id="main-menu" role="navigation">
|
<nav class="navbar is-primary" role="navigation" aria-label="main navigation">
|
||||||
<a href="<%= url_for('/') %>" class="instance-name"><img class="logo" src="<%= url_for('/img/lufi.svg') %>" alt="Lufi logo"><%= config('instance_name') %></a>
|
<div class="navbar-brand">
|
||||||
<ul>
|
<a class="navbar-item" href="<%= url_for('/') %>">
|
||||||
<li><a href="<%= $self->config('report') %>" class="menu-item"><%= l('Report file') %></a></li>
|
<img src="<%= url_for('/img/lufi.svg') %>" alt="Lufi logo">
|
||||||
|
<%= config('instance_name') %>
|
||||||
|
</a>
|
||||||
% if ((!defined(config('ldap')) && !defined(config('htpasswd')) && !defined(config('auth_headers'))) || is_user_authenticated()) {
|
% if ((!defined(config('ldap')) && !defined(config('htpasswd')) && !defined(config('auth_headers'))) || is_user_authenticated()) {
|
||||||
<li<%== ' class="active"' if (current_route eq 'index') %>><a href="<%= url_for('/') %>" class="menu-item"><%= l('Upload files') %></a></li>
|
<a href="<%= url_for('/') %>" class="navbar-item<%== ' has-background-primary-35' if (current_route eq 'index') %>"><%= l('Upload files') %></a>
|
||||||
<li<%== ' class="active"' if (current_route eq 'files') %>><a href="<%= url_for('/files') %>" class="menu-item"><%= l('My files') %></a></li>
|
<a href="<%= url_for('/files') %>" class="navbar-item<%== ' has-background-primary-35' if (current_route eq 'files') %>"><%= l('My files') %></a>
|
||||||
% if ((defined config('ldap') || defined config('auth_headers')) && defined config('invitations')) {
|
|
||||||
<li<%== ' class="active"' if (current_route eq 'invite') %>><a href="<%= url_for('/invite') %>" class="menu-item"><%= l('Invite a guest') %></a></li>
|
|
||||||
<li<%== ' class="active"' if (current_route eq 'invite/list') %>><a href="<%= url_for('/invite/list') %>" class="menu-item"><%= l('My invitations') %></a></li>
|
|
||||||
% }
|
|
||||||
% } else {
|
|
||||||
<li><a href="<%= url_for('/login') %>" class="menu-item"><%= l('Signin') %></a></li>
|
|
||||||
% }
|
% }
|
||||||
|
|
||||||
<li>
|
|
||||||
<label for="menu-language" class="hidden"><%= l('Language') %></label>
|
|
||||||
|
|
||||||
<select class="menu-item" id="menu-language" aria-label="<%= l('Language') %>">
|
|
||||||
<optgroup label="<%= l('Language') %>">
|
|
||||||
% for my $i (@{$self->available_langs}) {
|
|
||||||
<option value="<%= $i %>" <%= 'selected="true"' if ($i eq $lang) %>><%= iso639_native_name($i) %></option>
|
|
||||||
% }
|
|
||||||
</optgroup>
|
|
||||||
</select>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li<%== ' class="active"' if (current_route eq 'about') %>><a href="<%= url_for('/about') %>" class="menu-item"><%= l('About') %></a></li>
|
|
||||||
|
|
||||||
% if ((defined(config('ldap')) || defined(config('htpasswd')) || defined(config('auth_headers'))) && is_user_authenticated()) {
|
<a href="<%= $self->config('report') %>" class="navbar-item"><%= l('Report file') %></a>
|
||||||
<li>
|
|
||||||
<form action="<%= url_for('/logout') %>" method="POST">
|
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="main-menu">
|
||||||
%= csrf_field
|
<span aria-hidden="true"></span>
|
||||||
<button class="menu-item reset" type="submit"><%= l('Logout') %></button>
|
<span aria-hidden="true"></span>
|
||||||
</form>
|
<span aria-hidden="true"></span>
|
||||||
</li>
|
<span aria-hidden="true"></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="navbar-menu" id="main-menu">
|
||||||
|
<div class="navbar-start">
|
||||||
|
% if ((!defined(config('ldap')) && !defined(config('htpasswd')) && !defined(config('auth_headers'))) || is_user_authenticated()) {
|
||||||
|
% if ((defined config('ldap') || defined config('auth_headers')) && defined config('invitations')) {
|
||||||
|
<div class="navbar-item has-dropdown is-hoverable" role="navigation" tabindex=0>
|
||||||
|
<a class="navbar-link"><%= l('My invitations') %></a>
|
||||||
|
|
||||||
|
<div class="navbar-dropdown" role="button" aria-expanded="false" aria-label="<%= l('My invitations') %>">
|
||||||
|
<a href="<%= url_for('/invite') %>" class="navbar-item<%= ' has-background-primary-35' if (current_route eq 'invite') %>"><%= l('Invite a guest') %></a>
|
||||||
|
<a href="<%= url_for('/invite/list') %>" class="navbar-item<%= ' has-background-primary-35' if (current_route eq 'invite/list') %>"><%= l('My invitations') %></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% }
|
||||||
|
% }
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="navbar-end">
|
||||||
|
<a href="<%= url_for('/about') %>" class="navbar-item<%= ' has-background-primary-35' if (current_route eq 'about') %>"><%= l('About') %></a>
|
||||||
|
|
||||||
|
% if ((defined(config('ldap')) || defined(config('htpasswd')) || defined(config('auth_headers')))) {
|
||||||
|
% if (is_user_authenticated()) {
|
||||||
|
<div class="navbar-item">
|
||||||
|
<form action="<%= url_for('/logout') %>" method="POST">
|
||||||
|
%= csrf_field
|
||||||
|
<button type="submit"><%= l('Logout') %></button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
% } else {
|
||||||
|
<a href="<%= url_for('/login') %>" class="navbar-item <%= ' has-background-primary-35' if (current_route eq 'login') %>">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon fa-solid fa-user" aria-hidden="true"></span>
|
||||||
|
<span><%= l('Signin') %></span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
% }
|
||||||
% }
|
% }
|
||||||
</ul>
|
|
||||||
|
<a href="#" id="language-selector" class="js-modal-trigger navbar-item" data-target="modal-language-selector" role="button" aria-haspopup="true" aria-expanded="false" aria-label="<%= l('Languages') %>">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon">
|
||||||
|
<span class="fa-solid fa-globe" aria-hidden="true"></span>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<%= iso639_native_name($lang) %>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main role="main">
|
<main role="main">
|
||||||
<%= content %>
|
<div id="notifications-container" class="is-overlay"></div>
|
||||||
|
|
||||||
|
<noscript>
|
||||||
|
<div class="message is-danger">
|
||||||
|
<div class="message-body">
|
||||||
|
<%= l('Javascript is disabled. You won\'t be able to use Lufi.') %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
|
|
||||||
|
<section id="section-main" class="hero is-fullheight-with-navbar">
|
||||||
|
<div class="hero-body is-fullwidth">
|
||||||
|
<div class="container section-container">
|
||||||
|
<%= content %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
% if (defined(config('piwik_img'))) {
|
% if (defined(config('piwik_img'))) {
|
||||||
<img src="<%== config('piwik_img') %>" class="no_border" alt="" />
|
<img src="<%== config('piwik_img') %>" class="no_border" alt="" />
|
||||||
% }
|
% }
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<div class="toast-container"></div>
|
<div id="modal-language-selector" class="modal">
|
||||||
|
<div class="modal-background"></div>
|
||||||
|
<div class="modal-card">
|
||||||
|
<header class="modal-card-head is-shadowless">
|
||||||
|
<p class="modal-card-title">
|
||||||
|
<%= l('Languages') %>
|
||||||
|
<button class="delete is-pulled-right" aria-label="close"></button>
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
<template id="toast">
|
<section class="modal-card-body">
|
||||||
<div class="toast">
|
<div class="fixed-grid is-fullwidth">
|
||||||
|
<div class="grid has-4-cols" aria-labelledby="language-selector">
|
||||||
|
% for my $i (@{$self->available_langs}) {
|
||||||
|
<a class="navbar-item cell" href="<%= url_for('lang') . $i %>"><%= iso639_native_name($i) %></a>
|
||||||
|
% }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<footer class="modal-card-foot"></footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template id="notification">
|
||||||
|
<div class="notification">
|
||||||
<div class="message"></div>
|
<div class="message"></div>
|
||||||
<div class="progress-container">
|
|
||||||
<div class="progress-bar"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
const langUrl = '<%= url_for('lang') %>';
|
|
||||||
const prefix = '<%= substr(config('prefix'), 1) %>';
|
const prefix = '<%= substr(config('prefix'), 1) %>';
|
||||||
|
|
||||||
const entityMap = {
|
const entityMap = {
|
||||||
|
@ -98,75 +161,91 @@
|
||||||
|
|
||||||
const escapeHtml = (string) => String(string).replace(/[&<>"'\/]/g, (s) => entityMap[s]);
|
const escapeHtml = (string) => String(string).replace(/[&<>"'\/]/g, (s) => entityMap[s]);
|
||||||
|
|
||||||
const formatDate = (unixTimestamp) => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
return new Date(unixTimestamp * 1000).toLocaleString(
|
if (isSecureContext && Notification.permission !== "granted") {
|
||||||
window.navigator.language,
|
Notification.requestPermission();
|
||||||
{
|
}
|
||||||
year: "numeric",
|
|
||||||
month: "long",
|
// Get all "navbar-burger" elements
|
||||||
day: "numeric",
|
const $navbarBurgers = Array.prototype.slice.call(
|
||||||
weekday: "long",
|
document.querySelectorAll(".navbar-burger"),
|
||||||
hour: "2-digit",
|
0
|
||||||
minute: "2-digit",
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
const addToast = (text, type) => {
|
// Add a click event on each of them
|
||||||
const toastDOM = document.querySelector("template#toast").content.cloneNode(true).children[0];
|
$navbarBurgers.forEach((el) => {
|
||||||
toastDOM.classList.add(type)
|
el.addEventListener("click", () => {
|
||||||
|
// Get the target from the "data-target" attribute
|
||||||
|
const target = el.dataset.target;
|
||||||
|
const $target = document.getElementById(target);
|
||||||
|
|
||||||
toastDOM.querySelector(".message").innerText = escapeHtml(text);
|
// Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu"
|
||||||
|
el.classList.toggle("is-active");
|
||||||
document.querySelector(".toast-container").appendChild(toastDOM);
|
$target.classList.toggle("is-active");
|
||||||
setTimeout(() => {
|
|
||||||
toastDOM.remove();
|
|
||||||
}, 3500)
|
|
||||||
|
|
||||||
toastDOM.onclick = () => {
|
|
||||||
toastDOM.remove();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const notify = (title, body) => {
|
|
||||||
if (!"Notification" in window || typeof Notification === "undefined") {
|
|
||||||
console.log(
|
|
||||||
`This browser does not support desktop notification, cannot send following message: ${title} ${body}`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Notification.permission !== "granted") {
|
|
||||||
Notification.requestPermission();
|
|
||||||
} else {
|
|
||||||
new Notification(title, {
|
|
||||||
body,
|
|
||||||
icon: "/img/lufi196.png",
|
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
};
|
|
||||||
|
|
||||||
const hideNode = (node) => {
|
// Functions to open and close a modal
|
||||||
node.classList.add("hidden");
|
const openModal = ($el) => {
|
||||||
node.setAttribute("aria-hidden", true);
|
$el.classList.add("is-active");
|
||||||
}
|
};
|
||||||
|
|
||||||
const showNode = (node) => {
|
const closeModal = ($el) => {
|
||||||
node.classList.remove("hidden");
|
$el.classList.remove("is-active");
|
||||||
node.setAttribute("aria-hidden", false);
|
};
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
if (!"Notification" in window || typeof Notification === "undefined") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Notification.permission !== "granted") {
|
const closeAllModals = () => {
|
||||||
Notification.requestPermission();
|
(document.querySelectorAll(".modal") || []).forEach(($modal) => {
|
||||||
}
|
closeModal($modal);
|
||||||
|
});
|
||||||
document.getElementById("menu-language").onchange = (event) => window.location = langUrl + event.target.value;
|
};
|
||||||
})
|
|
||||||
|
// Add a click event on buttons to open a specific modal
|
||||||
|
(document.querySelectorAll(".js-modal-trigger") || []).forEach(($trigger) => {
|
||||||
|
const modal = $trigger.dataset.target;
|
||||||
|
const $target = document.getElementById(modal);
|
||||||
|
|
||||||
|
$trigger.addEventListener("click", () => {
|
||||||
|
openModal($target);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add a click event on various child elements to close the parent modal
|
||||||
|
(
|
||||||
|
document.querySelectorAll(
|
||||||
|
".modal-background, .modal-close, .modal-card-head .delete, .modal-card-foot .button"
|
||||||
|
) || []
|
||||||
|
).forEach(($close) => {
|
||||||
|
const $target = $close.closest(".modal");
|
||||||
|
|
||||||
|
$close.addEventListener("click", () => {
|
||||||
|
closeModal($target);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add a keyboard event to close all modals
|
||||||
|
document.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
closeAllModals();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
(document.querySelectorAll('.notification .delete') || []).forEach(($delete) => {
|
||||||
|
const $notification = $delete.parentNode;
|
||||||
|
|
||||||
|
$delete.addEventListener('click', () => {
|
||||||
|
$notification.parentNode.removeChild($notification);
|
||||||
|
});
|
||||||
|
|
||||||
|
$delete.addEventListener('keydown', (event) => {
|
||||||
|
if (event.key === " " || event.key === "enter") {
|
||||||
|
event.target.click()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<section class="login-section">
|
<section class="section login-section">
|
||||||
% if (defined stash('msg')) {
|
% if (defined stash('msg')) {
|
||||||
<section class="messages-zone">
|
<section class="messages-zone">
|
||||||
<div class="message-card error">
|
<div class="message-card error">
|
||||||
|
|
|
@ -1,43 +1,51 @@
|
||||||
<section class="mail-section">
|
<div class="box">
|
||||||
<section class="messages-zone">
|
|
||||||
% if (defined(stash('msg'))) {
|
% if (defined(stash('msg'))) {
|
||||||
<article class="message-card error">
|
<div class="message">
|
||||||
<%== stash('msg')%>
|
<div class="message-body is-danger">
|
||||||
</article>
|
<%= stash('msg')%>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
% }
|
% }
|
||||||
|
|
||||||
<article class="message-card info">
|
<div class="message">
|
||||||
<p><%= l('If you send the mail from this server, the links will be sent to the server, which may lower your privacy protection.') %></p>
|
<div class="message-body is-info">
|
||||||
<p><%= l('Adding URLs not related to this Lufi instance to the mail body or subject is prohibited.') %></p>
|
<p><%= l('If you send the mail from this server, the links will be sent to the server, which may lower your privacy protection.') %></p>
|
||||||
</article>
|
<p><%= l('Adding URLs not related to this Lufi instance to the mail body or subject is prohibited.') %></p>
|
||||||
</section>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form action="<%= url_for('m') %>" method="post" class="mail-form">
|
<form class="field" action="<%= url_for('m') %>" method="post" class="mail-form">
|
||||||
%= csrf_field
|
%= csrf_field
|
||||||
<div class="input-text emails">
|
<div class="field emails">
|
||||||
<label for="emails"><%= l('Comma-separated email addresses') %></label>
|
<label class="label" for="emails"><%= l('Comma-separated email addresses') %></label>
|
||||||
<input type="text" name="emails" placeholder="<%= l('Emails') %>" value="<%= defined(stash('values')) ? stash('values')->{emails} : '' %>" required>
|
<div class="control">
|
||||||
|
<input class="input" type="text" name="emails" placeholder="<%= l('Emails') %>" value="<%= defined(stash('values')) ? stash('values')->{emails} : '' %>" required>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-text subject">
|
<div class="field subject">
|
||||||
<label for="subject"><%= l('Email subject') %></label>
|
<label class="label" for="subject"><%= l('Email subject') %></label>
|
||||||
<input type="text" name="subject" placeholder="<%= l('Email subject') %>" value="<%= defined(stash('values')) ? stash('values')->{subject} : l('Here\'s some files') %>" required>
|
<div class="control">
|
||||||
|
<input class="input" type="text" name="subject" placeholder="<%= l('Email subject') %>" value="<%= defined(stash('values')) ? stash('values')->{subject} : l('Here\'s some files') %>" required>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-text body">
|
<div class="field body">
|
||||||
<label for="body"><%= l('Email body') %></label>
|
<label class="label" for="body"><%= l('Email body') %></label>
|
||||||
<textarea id="body" name="body" placeholder="<%= l('Email body') %>" required><%= defined(stash('values')) ? stash('values')->{body} : "" %></textarea>
|
<div class="control">
|
||||||
|
<textarea class="textarea" id="body" name="body" placeholder="<%= l('Email body') %>" required><%= defined(stash('values')) ? stash('values')->{body} : "" %></textarea>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions-buttons">
|
<div class="actions-buttons has-text-centered">
|
||||||
% if (!$self->config('disable_mail_sending')) {
|
% if (!$self->config('disable_mail_sending')) {
|
||||||
<button type="submit" class="button action-submit"><%= l('Send with this server') %></button>
|
<button type="submit" class="button action-submit is-primary"><%= l('Send with this server') %></button>
|
||||||
% }
|
% }
|
||||||
|
|
||||||
<a href="#" class="button action-own-software"><%= l('Send with your own mail software') %></a>
|
<a href="#" class="button action-own-software is-primary"><%= l('Send with your own mail software') %></a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<section>
|
</div>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
const retrieveItemFromStorage = (serverKey) =>
|
const retrieveItemFromStorage = (serverKey) =>
|
||||||
|
@ -46,9 +54,9 @@
|
||||||
|
|
||||||
const updateMailtoLink = () => {
|
const updateMailtoLink = () => {
|
||||||
const ownSoftwareButtonDOM = document.querySelector('.action-own-software');
|
const ownSoftwareButtonDOM = document.querySelector('.action-own-software');
|
||||||
const emails = document.querySelector('.input-text.emails input').value;
|
const emails = document.querySelector('.emails input').value;
|
||||||
const subject = document.querySelector('.input-text.subject input').value;
|
const subject = document.querySelector('.subject input').value;
|
||||||
const body = document.querySelector('.input-text.body textarea').value;
|
const body = document.querySelector('.body textarea').value;
|
||||||
|
|
||||||
ownSoftwareButtonDOM.href = `mailto:${encodeURIComponent(emails)}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
|
ownSoftwareButtonDOM.href = `mailto:${encodeURIComponent(emails)}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
|
||||||
}
|
}
|
||||||
|
@ -77,14 +85,14 @@
|
||||||
text += "\n-- \n<%= l('Share your files in total privacy on %1', url_for('/')->to_abs) %>";
|
text += "\n-- \n<%= l('Share your files in total privacy on %1', url_for('/')->to_abs) %>";
|
||||||
% }
|
% }
|
||||||
|
|
||||||
document.querySelector('.input-text.body textarea').value = text;
|
document.querySelector('.body textarea').value = text;
|
||||||
updateMailtoLink();
|
updateMailtoLink();
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
populateBody();
|
populateBody();
|
||||||
|
|
||||||
document.querySelectorAll(".input-text > *:not(label)")
|
document.querySelectorAll(".control > *")
|
||||||
.forEach(node => node.addEventListener('change', updateMailtoLink));
|
.forEach(node => node.addEventListener('change', updateMailtoLink));
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
|
@ -1,18 +1,22 @@
|
||||||
<section class="messages-zone">
|
<section class="box">
|
||||||
% if (!defined(stash('f')) && defined(stash('msg'))) {
|
% if (!defined(stash('f')) && defined(stash('msg'))) {
|
||||||
<article class="message-card error">
|
<div class="message is-danger">
|
||||||
<p><%= stash('msg') %></p>
|
<div class="message-body">
|
||||||
</article>
|
<%= stash('msg')%>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
% } elsif (defined(stash('msg_success'))) {
|
% } elsif (defined(stash('msg_success'))) {
|
||||||
<article class="message-card success">
|
<div class="message is-success">
|
||||||
<p><%= stash('msg_success') %></p>
|
<div class="message-body">
|
||||||
</article>
|
<%= stash('msg_success')%>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
% } else {
|
% } else {
|
||||||
<article class="message-card success">
|
<h1 class="title is-1"><%= stash('f')->filename %></h1>
|
||||||
<header>
|
<div class="message is-success">
|
||||||
<h1><%= stash('f')->filename %></h1>
|
<div class="message-body">
|
||||||
</header>
|
<%= stash('msg')%>
|
||||||
<p><%= stash('msg') %></p>
|
</div>
|
||||||
</article>
|
</div>
|
||||||
% }
|
% }
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -1,117 +1,186 @@
|
||||||
<section class="download-section">
|
% if (!defined(stash('f')) && defined(stash('msg'))) {
|
||||||
% if (!defined(stash('f')) && defined(stash('msg'))) {
|
<div class="message">
|
||||||
<article class="message-card error">
|
<div class="message-body is-danger">
|
||||||
<%= stash('msg')%>
|
<%= stash('msg')%>
|
||||||
</article>
|
</div>
|
||||||
% } else {
|
</div>
|
||||||
<h1 class="title-filename"><%= stash('f')->filename %></h1>
|
% } else {
|
||||||
|
<div class="box">
|
||||||
|
<h1 class="title is-1 has-text-centered mb-6"><%= l('Download file') %></h1>
|
||||||
% if (defined(stash('msg'))) {
|
% if (defined(stash('msg'))) {
|
||||||
<article class="message-card error">
|
<div class="message">
|
||||||
<%= stash('msg')%>
|
<div class="message-body is-danger">
|
||||||
</article>
|
<%= stash('msg')%>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
% } else {
|
% } else {
|
||||||
% if (stash('file_pwd')) {
|
% if (stash('file_pwd')) {
|
||||||
<form class="password-form">
|
<form id="password-form" class="field has-addons is-align-items-center is-justify-content-center">
|
||||||
<div class="input-text input-password">
|
<label class="label mb-0 mr-3" for="password"><%= l('Password') %></label>
|
||||||
<label for="file-password"><%= l('Add a password to file(s)') %></label>
|
<div class="control">
|
||||||
<input type="password" id="file-password" autocomplete="off" autofocus placeholder="<%= l('Password') %>">
|
<input class="input" type="password" id="password" autocomplete="off" autofocus placeholder="<%= l('Password') %>">
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="button action-download" href="#"><%= l('Download') %></button>
|
|
||||||
</form>
|
<div class="control">
|
||||||
|
<button type="submit" class="button is-primary action-download" href="#"><%= l('Download') %></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
% }
|
% }
|
||||||
|
|
||||||
<div class="download-container"></div>
|
<div id="download-container" class="is-size-5">
|
||||||
|
</div>
|
||||||
|
|
||||||
<template id="download-card-ongoing">
|
<template id="block-download-ongoing">
|
||||||
<article class="download-card ongoing">
|
<article class="block block-download-ongoing">
|
||||||
<div class="file-description">
|
<div class="columns is-justify-content-center">
|
||||||
<p class="file-size" data-filesize="<%= stash('f')->filesize %>"></p>
|
<div class="box column is-half">
|
||||||
</div>
|
<div class="content fixed-grid has-12-cols">
|
||||||
|
<div class="grid is-vcentered">
|
||||||
|
<div class="cell is-col-1">
|
||||||
|
<span class="icon fas fa-file fa-2x is-large has-text-primary" aria-hidden="true"></span>
|
||||||
|
</div>
|
||||||
|
<div class="cell is-col-span-11 description" data-is-zipped="<%= (stash('f')->zipped) ? 'true' : 'false' %>">
|
||||||
|
<strong class="is-block wb-all name is-overflow-x"><%= stash('f')->filename %></strong>
|
||||||
|
<small class="is-block size mt-1" data-filesize="<%= stash('f')->filesize %>"></small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p class="wait-message"><%= l('Please wait while we are getting your file. We first need to download and decrypt all parts before you can get it.') %></p>
|
<div class="progress-text has-text-centered has-text-weight-bold">0%</div>
|
||||||
<p class="loading-message"></p>
|
<progress class="progress is-primary is-small" max="100">0%</progress>
|
||||||
|
</div>
|
||||||
<div class="progress-container">
|
</div>
|
||||||
<div class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="button" class="button action-abort"><%= l('Abort') %></a>
|
<div class="columns is-justify-content-center">
|
||||||
|
<div class="column has-text-centered is-half px-0">
|
||||||
|
<button id="abort-button" type="button" class="button is-danger is-fullwidth is-size-5"><%= l('Abort') %></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template id="block-download-aborted"><article class="block download-aborted">
|
||||||
|
<article class="block block-download-aborted">
|
||||||
|
<div class="columns is-justify-content-center">
|
||||||
|
<div class="box column is-half">
|
||||||
|
<div class="content fixed-grid has-12-cols">
|
||||||
|
<div class="grid is-vcentered">
|
||||||
|
<div class="cell is-col-1">
|
||||||
|
<span class="icon fas fa-file fa-2x is-large has-text-primary" aria-hidden="true"></span>
|
||||||
|
</div>
|
||||||
|
<div class="cell is-col-span-11 description" data-iszipped="<%= (stash('f')->zipped) ? 'true' : 'false' %>">
|
||||||
|
<strong class="is-block wb-all name is-overflow-x"><%= stash('f')->filename %></strong>
|
||||||
|
<small class="is-block size mt-1" data-filesize="<%= stash('f')->filesize %>"></small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<template id="download-card-aborted">
|
|
||||||
<article class="download-card aborted">
|
<div class="message is-info">
|
||||||
<div class="file-description">
|
<div class="message-body">
|
||||||
<p class="file-size" data-filesize="<%= stash('f')->filesize %>"></p>
|
<%= l('Download aborted.') %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="columns is-justify-content-center">
|
||||||
|
<div class="column has-text-centered is-half px-0">
|
||||||
|
<button id="reload-button" type="button" class="button is-primary is-fullwidth is-size-5"><%= l('Click here to refresh the page and restart the download.') %></button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="abort-message"></p>
|
|
||||||
<button type="button" class="button action-reload"><%= l('Click here to refresh the page and restart the download.') %></a>
|
|
||||||
</article>
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<template id="download-card-success">
|
<template id="block-download-success">
|
||||||
<article class="download-card success">
|
<article class="block block-success">
|
||||||
<div class="file-description" data-isZipped="<%= (stash('f')->zipped) ? 'true' : 'false' %>">
|
<div class="columns is-justify-content-center">
|
||||||
<p class="file-size" data-filesize="<%= stash('f')->filesize %>"></p>
|
<div class="box column is-half">
|
||||||
|
<div class="content fixed-grid has-12-cols">
|
||||||
|
<div class="grid is-vcentered">
|
||||||
|
<div class="cell is-col-1">
|
||||||
|
<span class="icon fas fa-file fa-2x is-large has-text-primary" aria-hidden="true"></span>
|
||||||
|
</div>
|
||||||
|
<div class="cell is-col-span-11 description" data-iszipped="<%= (stash('f')->zipped) ? 'true' : 'false' %>">
|
||||||
|
<strong class="is-block wb-all name is-overflow-x"><%= stash('f')->filename %></strong>
|
||||||
|
<small class="is-block size mt-1" data-filesize="<%= stash('f')->filesize %>"></small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a class="button action-download" autofocus href="#"><%= l('Get the file')%></a>
|
|
||||||
|
|
||||||
<div class="content">
|
<div class="columns is-justify-content-center">
|
||||||
<div class="hidden zip-container" aria-hidden="true">
|
<div class="column has-text-centered is-half px-0">
|
||||||
<button type="button" class="button action-show-zip"><%= l('Show zip content') %></button>
|
<a href="#" id="download-button" class="button is-primary is-fullwidth is-size-5"><%= l('Get the file') %></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="zip-container" class="columns is-justify-content-center is-hidden mt-6">
|
||||||
|
<div class="column is-half has-text-centered is-size-6 p-0 content">
|
||||||
|
<a id="show-zip-button" href="" class="button"><%= l('Show zip content') %></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div class="hidden zip-content" aria-hidden="true">
|
<template id="block-download-error">
|
||||||
<h2><%= l('Zip content:') %></h2>
|
<article class="block block-download-error">
|
||||||
<ul>
|
<div class="columns is-justify-content-center">
|
||||||
</ul>
|
<div class="box column is-half">
|
||||||
|
<div class="content fixed-grid has-12-cols">
|
||||||
|
<div class="grid is-vcentered">
|
||||||
|
<div class="cell is-col-1">
|
||||||
|
<span class="icon fas fa-file fa-2x is-large has-text-primary" aria-hidden="true"></span>
|
||||||
|
</div>
|
||||||
|
<div class="cell is-col-span-11 description">
|
||||||
|
<strong class="is-block wb-all name is-overflow-x"><%= stash('f')->filename %></strong>
|
||||||
|
<small class="is-block size mt-1" data-filesize="<%= stash('f')->filesize %>"></small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="message is-danger">
|
||||||
|
<div class="message-body"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template id="download-card-error">
|
<template id="card-zipped-item">
|
||||||
<article class="download-card error">
|
<article class="card card-zipped-item">
|
||||||
<div class="file-description">
|
<div class="card-content fixed-grid has-12-cols p-1 mb-0">
|
||||||
<p class="file-name"></p>
|
<div class="grid is-vcentered is-align-items-center p-0">
|
||||||
<p class="file-size" data-filesize="<%= stash('f')->filesize %>"></p>
|
<div class="cell is-col-1">
|
||||||
|
<span class="icon fas fa-file has-text-primary" aria-hidden="true"></span>
|
||||||
|
</div>
|
||||||
|
<div class="cell is-col-span-11">
|
||||||
|
<strong class="is-block wb-all name is-overflow-x">test</strong>
|
||||||
|
<small class="is-block size mt-1">a</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="message-card error"></div>
|
<footer class="card-footer">
|
||||||
|
<a href="#" class="card-footer-item py-1 action-download">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon fas fa-download" aria-hidden="true"></span>
|
||||||
|
<span><%= l('Download') %></span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</footer>
|
||||||
</article>
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template id="zip-item">
|
|
||||||
<li class="zip-item">
|
|
||||||
<span class="file-name"></span>
|
|
||||||
<span class="file-size"></span>
|
|
||||||
<a class="icon-button icon download action-download-item" title="<%= l('Get the file') %>" href="#"></a>
|
|
||||||
</li>
|
|
||||||
</template>
|
|
||||||
% }
|
% }
|
||||||
% }
|
</div>
|
||||||
</section>
|
% }
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
const ws_url = '<%= url_for('download')->to_abs().stash('f')->short %>';
|
const ws_url = '<%= url_for('download')->to_abs().stash('f')->short %>';
|
||||||
const i18n = {
|
const i18n = {
|
||||||
aborted1: '<%= l('Download aborted.') %>',
|
|
||||||
aborted2: '<%= l('Click here to refresh the page and restart the download.') %>',
|
|
||||||
badkey: '<%= l('It seems that the key in your URL is incorrect. Please, verify your URL.') %>',
|
|
||||||
confirmExit: '<%= l('You have attempted to leave this page. The download will be canceled. Are you sure?') %>',
|
confirmExit: '<%= l('You have attempted to leave this page. The download will be canceled. Are you sure?') %>',
|
||||||
download: '<%= l('Get the file') %>',
|
|
||||||
fileDownloaded: '<%= l('File downloaded') %>',
|
fileDownloaded: '<%= l('File downloaded') %>',
|
||||||
loading: '<%= l('Asking for file part XX1 of %1', stash('f')->nbslices) %>',
|
|
||||||
nokey: '<%= l('You don\'t seem to have a key in your URL. You won\'t be able to decrypt the file. Download canceled.') %>',
|
|
||||||
showZipContent: '<%= l('Show zip content') %>',
|
|
||||||
tooMuchAttempts: '<%= l('Unable to download the file: too much unsuccessful attempts to open a websocket. Please, contact the administrator.') %>',
|
|
||||||
zipContent: '<%= l('Zip content:') %>'
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
%= javascript '/js/lufi-download.js', type => 'module'
|
%= javascript '/js/minified/download.min.js', type => 'module', defer => "true"
|