Convert multiple pages into SPA

This commit is contained in:
2025-10-09 15:48:23 +02:00
parent c616b2da7f
commit 06d04a1abc
110 changed files with 1245 additions and 1319 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

BIN
res/static/img/carina.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

BIN
res/static/img/catspaw.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

BIN
res/static/img/fnx_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 777 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -58,12 +58,6 @@ a>svg {
box-sizing: border-box; box-sizing: border-box;
} }
html,
body {
/* This makes sure that no scrollbar shows up when the menu is open on small screens*/
overflow-x: hidden;
}
body { body {
margin: 0; margin: 0;
font-family: system-ui, sans-serif; font-family: system-ui, sans-serif;
@@ -76,12 +70,6 @@ body {
transition: opacity 0.2s; transition: opacity 0.2s;
} }
.checkers {
background-image: var(--background_pattern);
background-color: var(--background_pattern_color);
background-repeat: repeat;
}
header, header,
footer { footer {
text-align: center; text-align: center;
@@ -90,24 +78,16 @@ footer {
} }
footer { footer {
background-image: url("/res/img/nebula.webp"); background-color: var(--shaded_background);
background-color: var(--background_color); backdrop-filter: blur(4px);
background-blend-mode: luminosity; border-top: 1px solid var(--separator);
box-shadow: inset 0 0 10px -4px var(--shadow_color);
border-radius: 8px;
margin: 16px;
} }
footer>.footer_content { footer>.footer_content {
background: var(--body_background);
color: var(--body_text_color);
display: inline-block; display: inline-block;
width: 1000px; width: 100%;
max-width: 100%;
padding-top: 10px; padding-top: 10px;
padding-bottom: 10px; padding-bottom: 10px;
border-radius: 8px;
margin: 120px 0 60px 0;
} }
header>h1 { header>h1 {
@@ -173,52 +153,17 @@ pre>code {
transition: left 0.5s; transition: left 0.5s;
} }
.page_body {
position: relative;
right: 0;
height: auto;
left: 0;
margin-left: 300px;
min-width: 300px;
display: block;
text-align: center;
/* Center the header and body */
overflow-y: auto;
overflow-x: hidden;
transition: margin 0.5s;
}
.page_content { .page_content {
background: var(--body_background); background: var(--shaded_background);
border-radius: 6px; backdrop-filter: blur(4px);
overflow: hidden; text-align: center;
} }
.page_content, @media (max-width: 1000px) {
.page_margins,
footer {
margin-right: 20px;
margin-left: 20px;
}
@media (max-width: 1100px) {
.page_navigation { .page_navigation {
left: -300px; left: -300px;
} }
.page_body {
margin-left: 0;
margin-right: 0;
width: 100%;
}
.page_content,
.page_margins,
footer {
margin-left: 0;
margin-right: 0;
}
header>h1 { header>h1 {
/* We want the header text to appear below the menu button, so the top /* We want the header text to appear below the menu button, so the top
margin needs to be fairly large when the screen is small */ margin needs to be fairly large when the screen is small */
@@ -537,6 +482,12 @@ input[type="color"] {
line-height: 1.3em; line-height: 1.3em;
} }
.button.flat {
background: none;
color: var(--body_text_color);
box-shadow: none;
}
button:hover, button:hover,
.button:hover, .button:hover,
input[type="submit"]:hover, input[type="submit"]:hover,

View File

@@ -1,34 +0,0 @@
{{define "admin"}}
<!DOCTYPE html>
<html lang="en">
{{if and .Authenticated .User.IsAdmin}}
<head>
{{template "meta_tags" "Administrator panel"}}
<script>
window.api_endpoint = '{{.APIEndpoint}}';
window.server_hostname = "{{.Hostname}}";
</script>
<script defer src='/res/svelte/admin_panel.js?v{{cacheID}}'></script>
</head>
<body>
{{template "menu" .}}
<div id="page_body" class="page_body"></div>
</body>
{{else}}
<head>
{{template "meta_tags" "Administrator panel"}}
</head>
<body>
{{template "page_top" .}}
<header>
<h1>Admin Panel</h1>
</header>
<div id="page_content" class="page_content">
;-)
</div>
{{template "page_bottom" .}}
</body>
{{end}}
</html>
{{end}}

View File

@@ -1,152 +0,0 @@
{{define "appearance"}}<!DOCTYPE html>
<html lang="en">
<head>
{{template "meta_tags" "Appearance settings"}}
<link id="stylesheet_theme_2" rel="stylesheet" type="text/css" href="/theme.css"/>
</head>
<body>
{{template "page_top" .}}
<header>
<h1>Change website appearance</h1>
</header>
<div id="page_content" class="page_content">
<section>
<p>
You can change how pixeldrain looks! Your theme choice will
be saved in a cookie.
</p>
<h2>Theme</h2>
<input type="radio" id="style_nord" name="style"><label for="style_nord">Nord</label>
(Inspired by <a href="https://www.nordtheme.com/" target="_blank">Nord</a>)
<br/>
Dynamic theme, changes based on operating system settings. Here you can choose a specific variant:
<br/>
<input type="radio" id="style_nord_dark" name="style"><label for="style_nord_dark">Nord dark</label>
<br/>
<input type="radio" id="style_nord_light" name="style"><label for="style_nord_light">Nord light</label>
<br/>
<br/>
<input type="radio" id="style_solarized" name="style"><label for="style_solarized">Solarized</label>
(Inspired by <a href="https://ethanschoonover.com/solarized/" target="_blank">Solarized</a>)
<br/>
Dynamic theme, changes based on operating system settings. Here you can choose a specific variant:
<br/>
<input type="radio" id="style_solarized_dark" name="style"><label for="style_solarized_dark">Solarized dark</label>
<br/>
<input type="radio" id="style_solarized_light" name="style"><label for="style_solarized_light">Solarized light</label>
<br/>
<!-- <br/> -->
<!-- <input type="radio" id="style_adwaita" name="style"><label for="style_adwaita">Adwaita</label><br/> -->
<br/>
<input type="radio" id="style_purple_drain" name="style"><label for="style_purple_drain">Purple drain</label>
<br/>
Classic 2022 style, with purple gradients
<br/>
<br/>
<input type="radio" id="style_classic" name="style"><label for="style_classic">Pixeldrain classic (gray)</label>
<br/>
Classic pre-2020 pixeldrain style, dark gray
<br/>
<br/>
Other (experimental) themes
<br/>
<input type="radio" id="style_maroon" name="style"><label for="style_maroon">Maroon Style</label>
<br/>
<input type="radio" id="style_hacker" name="style"><label for="style_hacker">Hacker Style</label>
<br/>
<input type="radio" id="style_canta" name="style"><label for="style_canta">Canta Style</label>
(Inspired by <a href="https://github.com/vinceliuice/Canta-theme" target="_blank">Canta GTK</a>)
<br/>
<input type="radio" id="style_skeuos" name="style"><label for="style_skeuos">Skeuos Style</label>
(Inspired by <a href="https://www.gnome-look.org/p/1441725/" target="_blank">Skeuos GTK</a>)
<br/>
<input type="radio" id="style_sweet" name="style"><label for="style_sweet">Sweet</label>
<br/>
<br/>
<input type="radio" id="style_adwaita" name="style"><label for="style_adwaita">Adwaita (dynamic)</label>
<br/>
<input type="radio" id="style_adwaita_dark" name="style"><label for="style_adwaita_dark">Adwaita dark</label>
<br/>
<input type="radio" id="style_adwaita_light" name="style"><label for="style_adwaita_light">Adwaita light</label>
<br/><br/>
<input type="radio" id="style_pixeldrain98" name="style"><label for="style_pixeldrain98">Pixeldrain 98</label>
<h2>Hue</h2>
<p>
Many themes support custom hues. The hue does not change the
contrast of the theme, only the color itself.
</p>
<input type="radio" id="hue_default" name="hue"><label for="hue_default">Default</label><br/>
<input type="radio" id="hue_354" name="hue"><label for="hue_354">Red</label><br/>
<input type="radio" id="hue_14" name="hue"><label for="hue_14">Orange</label><br/>
<input type="radio" id="hue_40" name="hue"><label for="hue_40">Yellow</label><br/>
<input type="radio" id="hue_92" name="hue"><label for="hue_92">Green</label><br/>
<input type="radio" id="hue_311" name="hue"><label for="hue_311">Purple</label><br/>
</section>
</div>
<script>
function get_cookie(cname) {
let name = cname + "=";
let decodedCookie = decodeURIComponent(document.cookie);
let ca = decodedCookie.split(';');
for(let i = 0; i <ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
let style = get_cookie("style")
let hue = get_cookie("hue")
// Style selector
document.getElementsByName("style").forEach(function(elem) {
elem.addEventListener("change", e => {
style = e.target.id.substring(6)
var date = new Date();
date.setTime(date.getTime() + (10 * 365 * 24 * 60 * 60 * 1000));
document.cookie = "style="+style+"; expires=" + date.toUTCString() + "; path=/"
reload_sheet()
})
});
document.getElementsByName("hue").forEach(function(elem) {
elem.addEventListener("change", e => {
hue = e.target.id.substring(4)
if (hue === "default") {
hue = -1
document.cookie = "hue=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/"
}
var date = new Date();
date.setTime(date.getTime() + (10 * 365 * 24 * 60 * 60 * 1000));
document.cookie = "hue="+hue+"; expires=" + date.toUTCString() + "; path=/"
reload_sheet()
})
});
function reload_sheet() {
let stylesheet1 = document.getElementById("stylesheet_theme")
let stylesheet2 = document.getElementById("stylesheet_theme_2")
// First load the sheet in the secondary tag, wait for it to load,
// and replace the original sheet when it has finished loading
stylesheet2.href= "/theme.css?style="+style+"&hue="+hue
stylesheet2.onload = e => {
stylesheet1.href= "/theme.css?style="+style+"&hue="+hue
}
}
</script>
{{template "page_bottom" .}}
</body>
</html>
{{end}}

View File

@@ -1,19 +0,0 @@
{{define "home"}}
<!DOCTYPE html>
<html lang="en">
<head>
{{template "meta_tags" "Cloud storage and data transfer services"}}
<script>
window.api_endpoint = '{{.APIEndpoint}}';
window.user = {{.User}};
window.server_hostname = "{{.Hostname}}";
</script>
<script defer src='/res/svelte/home_page.js?v{{cacheID}}'></script>
</head>
<body>
{{template "menu" .}}
<div id="page_body" class="page_body"></div>
</body>
</html>
{{end}}

View File

@@ -1,19 +0,0 @@
{{define "login"}}<!DOCTYPE html>
<html lang="en">
<head>
{{template "meta_tags" "Login" }}
<script>
window.api_endpoint = '{{.APIEndpoint}}';
window.user = {{.User}};
window.server_hostname = "{{.Hostname}}";
</script>
<script defer src='/res/svelte/login.js?v{{cacheID}}'></script>
</head>
<body>
{{template "menu" .}}
<div id="page_body" class="page_body"></div>
</body>
</html>
{{end}}

View File

@@ -1,19 +0,0 @@
{{define "speedtest"}}
<!DOCTYPE html>
<html lang="en">
<head>
{{template "meta_tags" "Speedtest"}}
<script>
window.api_endpoint = '{{.APIEndpoint}}';
window.user = {{.User}};
window.server_hostname = "{{.Hostname}}";
</script>
<script defer src='/res/svelte/speedtest.js?v{{cacheID}}'></script>
</head>
<body>
{{template "menu" .}}
<div id="page_body" class="page_body"></div>
</body>
</html>
{{end}}

View File

@@ -1,19 +0,0 @@
{{define "user_home"}}<!DOCTYPE html>
<html lang="en">
<head>
{{template "meta_tags" .User.Username }}
<script>
window.api_endpoint = '{{.APIEndpoint}}';
window.user = {{.User}};
window.server_hostname = "{{.Hostname}}";
</script>
<script defer src='/res/svelte/user_home.js?v{{cacheID}}'></script>
</head>
<body>
{{template "menu" .}}
<div id="page_body" class="page_body"></div>
</body>
</html>
{{end}}

View File

@@ -1,4 +1,4 @@
{{define "filesystem"}} {{define "wrap"}}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
@@ -23,12 +23,12 @@
{{ template "opengraph" .OGData }} {{ template "opengraph" .OGData }}
<script> <script>
window.initial_node = {{.Other}};
window.user = {{.User}}; window.user = {{.User}};
window.api_endpoint = '{{.APIEndpoint}}'; window.api_endpoint = '{{.APIEndpoint}}';
window.server_hostname = "{{.Hostname}}";
</script> </script>
<script defer src='/res/svelte/filesystem.js?v{{cacheID}}'></script> <script defer src='/res/svelte/wrap.js?v{{cacheID}}'></script>
</head> </head>
<body></body> <body></body>
</html> </html>

211
svelte/package-lock.json generated
View File

@@ -11,7 +11,6 @@
"behave-js": "^1.5.0", "behave-js": "^1.5.0",
"chart.js": "^4.4.6", "chart.js": "^4.4.6",
"country-data-list": "^1.4.0", "country-data-list": "^1.4.0",
"pdfjs-dist": "^5.4.149",
"pure-color": "^1.3.0", "pure-color": "^1.3.0",
"rollup-plugin-includepaths": "^0.2.4", "rollup-plugin-includepaths": "^0.2.4",
"svelte-preprocess": "^6.0.3", "svelte-preprocess": "^6.0.3",
@@ -76,6 +75,7 @@
"integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.2.0", "@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.26.0", "@babel/code-frame": "^7.26.0",
@@ -1652,191 +1652,6 @@
"integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==", "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@napi-rs/canvas": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.80.tgz",
"integrity": "sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww==",
"license": "MIT",
"optional": true,
"workspaces": [
"e2e/*"
],
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
"@napi-rs/canvas-android-arm64": "0.1.80",
"@napi-rs/canvas-darwin-arm64": "0.1.80",
"@napi-rs/canvas-darwin-x64": "0.1.80",
"@napi-rs/canvas-linux-arm-gnueabihf": "0.1.80",
"@napi-rs/canvas-linux-arm64-gnu": "0.1.80",
"@napi-rs/canvas-linux-arm64-musl": "0.1.80",
"@napi-rs/canvas-linux-riscv64-gnu": "0.1.80",
"@napi-rs/canvas-linux-x64-gnu": "0.1.80",
"@napi-rs/canvas-linux-x64-musl": "0.1.80",
"@napi-rs/canvas-win32-x64-msvc": "0.1.80"
}
},
"node_modules/@napi-rs/canvas-android-arm64": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.80.tgz",
"integrity": "sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-darwin-arm64": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.80.tgz",
"integrity": "sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-darwin-x64": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.80.tgz",
"integrity": "sha512-FqqSU7qFce0Cp3pwnTjVkKjjOtxMqRe6lmINxpIZYaZNnVI0H5FtsaraZJ36SiTHNjZlUB69/HhxNDT1Aaa9vA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.80.tgz",
"integrity": "sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-arm64-gnu": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.80.tgz",
"integrity": "sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-arm64-musl": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.80.tgz",
"integrity": "sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.80.tgz",
"integrity": "sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-x64-gnu": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.80.tgz",
"integrity": "sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-x64-musl": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.80.tgz",
"integrity": "sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-win32-x64-msvc": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.80.tgz",
"integrity": "sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@pkgjs/parseargs": { "node_modules/@pkgjs/parseargs": {
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -2465,6 +2280,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"caniuse-lite": "^1.0.30001669", "caniuse-lite": "^1.0.30001669",
"electron-to-chromium": "^1.5.41", "electron-to-chromium": "^1.5.41",
@@ -2486,9 +2302,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001712", "version": "1.0.30001749",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001712.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001749.tgz",
"integrity": "sha512-MBqPpGYYdQ7/hfKiet9SCI+nmN5/hp4ZzveOJubl5DTAMa5oggjAuoi0Z4onBpKPFI2ePGnQuQIzF3VxDjDJig==", "integrity": "sha512-0rw2fJOmLfnzCRbkm8EyHL8SvI2Apu5UbnQuTsJ0ClgrH8hcwFooJ1s5R0EP8o8aVrFu8++ae29Kt9/gZAZp/Q==",
"devOptional": true, "devOptional": true,
"funding": [ "funding": [
{ {
@@ -3166,18 +2982,6 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/pdfjs-dist": {
"version": "5.4.149",
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.149.tgz",
"integrity": "sha512-Xe8/1FMJEQPUVSti25AlDpwpUm2QAVmNOpFP0SIahaPIOKBKICaefbzogLdwey3XGGoaP4Lb9wqiw2e9Jqp0LA==",
"license": "Apache-2.0",
"engines": {
"node": ">=20.16.0 || >=22.3.0"
},
"optionalDependencies": {
"@napi-rs/canvas": "^0.1.77"
}
},
"node_modules/periscopic": { "node_modules/periscopic": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz",
@@ -3371,6 +3175,7 @@
"integrity": "sha512-vGorVWIsWfX3xbcyAS+I047kFKapHYivmkaT63Smj77XwvLSJos6M1xGqZnBPFQFBRZDOcG1QnYEIxAvTr/HjA==", "integrity": "sha512-vGorVWIsWfX3xbcyAS+I047kFKapHYivmkaT63Smj77XwvLSJos6M1xGqZnBPFQFBRZDOcG1QnYEIxAvTr/HjA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/estree": "1.0.6" "@types/estree": "1.0.6"
}, },
@@ -3703,6 +3508,7 @@
"resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.19.tgz", "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.19.tgz",
"integrity": "sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==", "integrity": "sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.2.1", "@ampproject/remapping": "^2.2.1",
"@jridgewell/sourcemap-codec": "^1.4.15", "@jridgewell/sourcemap-codec": "^1.4.15",
@@ -3832,7 +3638,8 @@
"version": "2.8.1", "version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD" "license": "0BSD",
"peer": true
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.6.3", "version": "5.6.3",

View File

@@ -11,12 +11,7 @@ const production = !process.env.ROLLUP_WATCH;
const builddir = "../res/static/svelte" const builddir = "../res/static/svelte"
export default [ export default [
"filesystem", "wrap",
"user_home",
"admin_panel",
"home_page",
"speedtest",
"login",
].map((name, index) => ({ ].map((name, index) => ({
input: `src/${name}.js`, input: `src/${name}.js`,
output: { output: {

View File

@@ -1,8 +0,0 @@
import App from './admin_panel/Router.svelte';
const app = new App({
target: document.getElementById("page_body"),
props: {}
});
export default app;

View File

@@ -1,7 +1,7 @@
<script> <script>
import { onMount } from "svelte"; import { onMount } from "svelte";
import LoadingIndicator from "util/LoadingIndicator.svelte";
import AbuseReport from "./AbuseReport.svelte"; import AbuseReport from "./AbuseReport.svelte";
import { loading_finish, loading_start } from "lib/Loading";
let loading = true let loading = true
let reports = [] let reports = []
@@ -12,7 +12,7 @@ let endPicker
let tab = "pending" let tab = "pending"
const get_reports = async () => { const get_reports = async () => {
loading = true; loading_start()
// Remove refresh timeout if there is one // Remove refresh timeout if there is one
clearTimeout(refresh_timeout) clearTimeout(refresh_timeout)
@@ -70,7 +70,7 @@ const get_reports = async () => {
} catch (err) { } catch (err) {
alert(err); alert(err);
} finally { } finally {
loading = false; loading_finish()
} }
}; };
@@ -156,8 +156,6 @@ onMount(() => {
}); });
</script> </script>
<LoadingIndicator loading={loading}/>
<section> <section>
<div class="toolbar" style="text-align: left;"> <div class="toolbar" style="text-align: left;">
<div>Reports: {reports.length}</div> <div>Reports: {reports.length}</div>

View File

@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import { onMount, tick } from "svelte"; import { onMount, tick } from "svelte";
import LoadingIndicator from "util/LoadingIndicator.svelte";
import EmailReportersTable from "./EmailReportersTable.svelte"; import EmailReportersTable from "./EmailReportersTable.svelte";
import { get_endpoint } from "lib/PixeldrainAPI"; import { get_endpoint } from "lib/PixeldrainAPI";
import { loading_finish, loading_start } from "lib/Loading";
type Reporter = { type Reporter = {
from_address: string, from_address: string,
@@ -17,7 +17,6 @@ type Reporter = {
last_message_html: string, last_message_html: string,
} }
let loading = true
let reporters: Reporter[] = [] let reporters: Reporter[] = []
$: reporters_pending = reporters.reduce((acc, val) => { $: reporters_pending = reporters.reduce((acc, val) => {
if (val.status === "pending") { if (val.status === "pending") {
@@ -39,7 +38,7 @@ $: reporters_rejected = reporters.reduce((acc, val) => {
}, []) }, [])
const get_reporters = async () => { const get_reporters = async () => {
loading = true; loading_start()
try { try {
const resp = await fetch(get_endpoint()+"/admin/abuse_reporter"); const resp = await fetch(get_endpoint()+"/admin/abuse_reporter");
if(resp.status >= 400) { if(resp.status >= 400) {
@@ -49,7 +48,7 @@ const get_reporters = async () => {
} catch (err) { } catch (err) {
alert(err); alert(err);
} finally { } finally {
loading = false; loading_finish()
} }
}; };
@@ -137,8 +136,6 @@ const delete_reporter = async (reporter: Reporter) => {
onMount(get_reporters); onMount(get_reporters);
</script> </script>
<LoadingIndicator loading={loading}/>
<section> <section>
<div class="toolbar" style="text-align: left;"> <div class="toolbar" style="text-align: left;">
<div class="toolbar_spacer"></div> <div class="toolbar_spacer"></div>

View File

@@ -276,24 +276,12 @@ onDestroy(() => {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr>
<td>File statistics (per file)</td>
<td>{status.stats_watcher_threads}</td>
<td>{status.stats_watcher_listeners}</td>
<td>{(status.stats_watcher_listeners / status.stats_watcher_threads).toPrecision(3)}</td>
</tr>
<tr> <tr>
<td>Filesystem statistics (per file)</td> <td>Filesystem statistics (per file)</td>
<td>{status.filesystem_watcher_threads}</td> <td>{status.filesystem_watcher_threads}</td>
<td>{status.filesystem_watcher_listeners}</td> <td>{status.filesystem_watcher_listeners}</td>
<td>{(status.filesystem_watcher_listeners / status.filesystem_watcher_threads).toPrecision(3)}</td> <td>{(status.filesystem_watcher_listeners / status.filesystem_watcher_threads).toPrecision(3)}</td>
</tr> </tr>
<tr>
<td>Rate limits (per IP)</td>
<td>{status.rate_limit_watcher_threads}</td>
<td>{status.rate_limit_watcher_listeners}</td>
<td>{(status.rate_limit_watcher_listeners / status.rate_limit_watcher_threads).toPrecision(3)}</td>
</tr>
<tr> <tr>
<td>Downloads (per IP)</td> <td>Downloads (per IP)</td>
<td>{status.download_clients}</td> <td>{status.download_clients}</td>

View File

@@ -2,7 +2,7 @@
import { onMount } from "svelte"; import { onMount } from "svelte";
import { formatDate } from "util/Formatting"; import { formatDate } from "util/Formatting";
import Expandable from "util/Expandable.svelte"; import Expandable from "util/Expandable.svelte";
import LoadingIndicator from "util/LoadingIndicator.svelte"; import { loading_finish } from "lib/Loading";
const abuse_types = [ const abuse_types = [
"copyright", "copyright",
@@ -15,7 +15,6 @@ const abuse_types = [
"revenge_porn", "revenge_porn",
] ]
let loading = true
let rows = [] let rows = []
let total_offences = 0 let total_offences = 0
@@ -25,7 +24,7 @@ let new_ban_address
let new_ban_reason = abuse_types[0] let new_ban_reason = abuse_types[0]
const get_bans = async () => { const get_bans = async () => {
loading = true; loading_start()
try { try {
const resp = await fetch(window.api_endpoint+"/admin/ip_ban"); const resp = await fetch(window.api_endpoint+"/admin/ip_ban");
if(resp.status >= 400) { if(resp.status >= 400) {
@@ -39,7 +38,7 @@ const get_bans = async () => {
} catch (err) { } catch (err) {
alert(err); alert(err);
} finally { } finally {
loading = false; loading_finish();
} }
}; };
@@ -95,8 +94,6 @@ const delete_ban = async (addr) => {
onMount(get_bans); onMount(get_bans);
</script> </script>
<LoadingIndicator loading={loading}/>
<section> <section>
<div class="toolbar"> <div class="toolbar">
<div class="toolbar_label"> <div class="toolbar_label">

View File

@@ -1,13 +1,12 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
import { formatDate } from "util/Formatting"; import { formatDate } from "util/Formatting";
import LoadingIndicator from "util/LoadingIndicator.svelte";
import Expandable from "util/Expandable.svelte"; import Expandable from "util/Expandable.svelte";
import SortableTable, { FieldType } from "layout/SortableTable.svelte"; import SortableTable, { FieldType } from "layout/SortableTable.svelte";
import { country_name, get_admin_invoices, type Invoice } from "lib/AdminAPI"; import { country_name, get_admin_invoices, type Invoice } from "lib/AdminAPI";
import PayPalVat from "./PayPalVAT.svelte"; import PayPalVat from "./PayPalVAT.svelte";
import { loading_finish, loading_start } from "lib/Loading";
let loading = true
let invoices: Invoice[] = [] let invoices: Invoice[] = []
let year = 0 let year = 0
@@ -62,7 +61,7 @@ const obj_to_list_eu = (obj: {[id: string]: Total}) => {
} }
const get_invoices = async () => { const get_invoices = async () => {
loading = true; loading_start()
try { try {
const resp = await get_admin_invoices(year, month) const resp = await get_admin_invoices(year, month)
@@ -92,7 +91,7 @@ const get_invoices = async () => {
} catch (err) { } catch (err) {
alert(err); alert(err);
} finally { } finally {
loading = false; loading_finish()
} }
}; };
@@ -158,8 +157,6 @@ let records_hidden = 0
let invoices_filtered: Invoice[] = [] let invoices_filtered: Invoice[] = []
</script> </script>
<LoadingIndicator loading={loading}/>
<section> <section>
<h3>{year + "-" + ("00"+(month)).slice(-2)}</h3> <h3>{year + "-" + ("00"+(month)).slice(-2)}</h3>
<div class="toolbar"> <div class="toolbar">

View File

@@ -2,11 +2,10 @@
import { onMount } from "svelte"; import { onMount } from "svelte";
import { formatDate } from "util/Formatting"; import { formatDate } from "util/Formatting";
import { mollie_proxy_call } from "./MollieAPI"; import { mollie_proxy_call } from "./MollieAPI";
import LoadingIndicator from "util/LoadingIndicator.svelte";
import Euro from "util/Euro.svelte"; import Euro from "util/Euro.svelte";
import { loading_start } from "lib/Loading";
export let settlement = {} export let settlement = {}
let loading = true
let payments = [] let payments = []
let per_country = {} let per_country = {}
@@ -49,7 +48,7 @@ const load_all_payments = async (settlement_id) => {
} }
const get_payments = async () => { const get_payments = async () => {
loading = true; loading_start()
try { try {
payments = await load_all_payments(settlement.id) payments = await load_all_payments(settlement.id)
@@ -84,15 +83,13 @@ const get_payments = async () => {
} catch (err) { } catch (err) {
alert(err); alert(err);
} finally { } finally {
loading = false; loading_finish()
} }
}; };
onMount(get_payments); onMount(get_payments);
</script> </script>
<LoadingIndicator loading={loading}/>
<h3>Accounting information</h3> <h3>Accounting information</h3>
{#if per_country.NL} {#if per_country.NL}

View File

@@ -2,17 +2,16 @@
import { onMount } from "svelte"; import { onMount } from "svelte";
import { formatDate } from "util/Formatting"; import { formatDate } from "util/Formatting";
import Expandable from "util/Expandable.svelte"; import Expandable from "util/Expandable.svelte";
import LoadingIndicator from "util/LoadingIndicator.svelte";
import Euro from "util/Euro.svelte"; import Euro from "util/Euro.svelte";
import MollieSettlement from "./MollieSettlement.svelte"; import MollieSettlement from "./MollieSettlement.svelte";
import { mollie_proxy_call } from "./MollieAPI"; import { mollie_proxy_call } from "./MollieAPI";
import { loading_finish, loading_start } from "lib/Loading";
let loading = true
let response = {} let response = {}
let settlements = [] let settlements = []
const get_settlements = async () => { const get_settlements = async () => {
loading = true; loading_start()
try { try {
const req = await mollie_proxy_call("settlements?limit=250"); const req = await mollie_proxy_call("settlements?limit=250");
if(req.status >= 400) { if(req.status >= 400) {
@@ -23,15 +22,13 @@ const get_settlements = async () => {
} catch (err) { } catch (err) {
alert(err); alert(err);
} finally { } finally {
loading = false; loading_finish()
} }
}; };
onMount(get_settlements); onMount(get_settlements);
</script> </script>
<LoadingIndicator loading={loading}/>
<section> <section>
{#each settlements as row (row.id)} {#each settlements as row (row.id)}
<Expandable click_expand> <Expandable click_expand>

View File

@@ -2,10 +2,9 @@
import { onMount } from "svelte"; import { onMount } from "svelte";
import { formatDate } from "util/Formatting"; import { formatDate } from "util/Formatting";
import Expandable from "util/Expandable.svelte"; import Expandable from "util/Expandable.svelte";
import LoadingIndicator from "util/LoadingIndicator.svelte";
import Euro from "util/Euro.svelte"; import Euro from "util/Euro.svelte";
import { loading_finish, loading_start } from "lib/Loading";
let loading = true
let response = {} let response = {}
let payments = [] let payments = []
@@ -40,8 +39,8 @@ const get_payments = async () => {
fee: 0, fee: 0,
} }
payments = [] payments = []
loading = true;
loading_start()
try { try {
await get_page("https://api.mollie.com/v2/payments?limit=250") await get_page("https://api.mollie.com/v2/payments?limit=250")
@@ -76,7 +75,7 @@ const get_payments = async () => {
} catch (err) { } catch (err) {
alert(err); alert(err);
} finally { } finally {
loading = false; loading_finish()
} }
} }
@@ -118,8 +117,6 @@ onMount(() => {
}); });
</script> </script>
<LoadingIndicator loading={loading}/>
<section> <section>
<div class="toolbar" style="text-align: left;"> <div class="toolbar" style="text-align: left;">
<div>Payments: {payments.length}</div> <div>Payments: {payments.length}</div>

View File

@@ -2,19 +2,18 @@
import { onMount } from "svelte"; import { onMount } from "svelte";
import { formatDate } from "util/Formatting"; import { formatDate } from "util/Formatting";
import Expandable from "util/Expandable.svelte"; import Expandable from "util/Expandable.svelte";
import LoadingIndicator from "util/LoadingIndicator.svelte";
import Button from "layout/Button.svelte" import Button from "layout/Button.svelte"
import UserFiles from "./UserFiles.svelte"; import UserFiles from "./UserFiles.svelte";
import BanDetails from "./BanDetails.svelte"; import BanDetails from "./BanDetails.svelte";
import UserLists from "./UserLists.svelte"; import UserLists from "./UserLists.svelte";
import { loading_finish, loading_start } from "lib/Loading";
let loading = true
let rows = [] let rows = []
let total_offences = 0 let total_offences = 0
let expanded = false let expanded = false
const get_bans = async () => { const get_bans = async () => {
loading = true; loading_start()
try { try {
const resp = await fetch(window.api_endpoint+"/admin/user_ban"); const resp = await fetch(window.api_endpoint+"/admin/user_ban");
if(resp.status >= 400) { if(resp.status >= 400) {
@@ -28,7 +27,7 @@ const get_bans = async () => {
} catch (err) { } catch (err) {
alert(err); alert(err);
} finally { } finally {
loading = false; loading_finish()
} }
}; };
@@ -105,8 +104,6 @@ const block_all_files = async (row, reason) => {
onMount(get_bans); onMount(get_bans);
</script> </script>
<LoadingIndicator loading={loading}/>
<section> <section>
<div class="toolbar"> <div class="toolbar">
<div class="toolbar_label"> <div class="toolbar_label">

View File

@@ -1,16 +1,16 @@
<script> <script>
import { onMount } from "svelte"; import { onMount } from "svelte";
import LoadingIndicator from "util/LoadingIndicator.svelte";
import { formatDataVolume, formatDate } from "util/Formatting"; import { formatDataVolume, formatDate } from "util/Formatting";
import SortButton from "layout/SortButton.svelte"; import SortButton from "layout/SortButton.svelte";
import { loading_finish, loading_start } from "lib/Loading";
export let user_id = "" export let user_id = ""
let files = [] let files = []
let loading = true
onMount(() => reload()) onMount(() => reload())
export const reload = async () => { export const reload = async () => {
loading_start()
try { try {
const req = await fetch( const req = await fetch(
window.api_endpoint+"/user/files", window.api_endpoint+"/user/files",
@@ -30,7 +30,7 @@ export const reload = async () => {
} catch (err) { } catch (err) {
alert(err); alert(err);
} finally { } finally {
loading = false; loading_finish()
} }
} }
@@ -67,8 +67,6 @@ const sort = (field) => {
} }
</script> </script>
<LoadingIndicator loading={loading}/>
<div class="table_scroll"> <div class="table_scroll">
<table> <table>
<thead> <thead>

View File

@@ -1,15 +1,15 @@
<script> <script>
import { loading_finish, loading_start } from "lib/Loading";
import { onMount } from "svelte"; import { onMount } from "svelte";
import LoadingIndicator from "util/LoadingIndicator.svelte";
import { formatDate } from "util/Formatting"; import { formatDate } from "util/Formatting";
export let user_id = "" export let user_id = ""
let lists = [] let lists = []
let loading = true
onMount(() => reload()) onMount(() => reload())
export const reload = async () => { export const reload = async () => {
loading_start()
try { try {
const req = await fetch( const req = await fetch(
window.api_endpoint+"/user/lists", window.api_endpoint+"/user/lists",
@@ -28,13 +28,11 @@ export const reload = async () => {
} catch (err) { } catch (err) {
alert(err); alert(err);
} finally { } finally {
loading = false; loading_finish()
} }
} }
</script> </script>
<LoadingIndicator loading={loading}/>
<div class="table_scroll"> <div class="table_scroll">
<table> <table>
<thead> <thead>

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { fs_encode_path, node_is_shared } from "./FilesystemAPI"; import { fs_encode_path, node_is_shared } from "lib/FilesystemAPI";
import type { FSNavigator } from "./FSNavigator"; import type { FSNavigator } from "./FSNavigator";
export let nav: FSNavigator export let nav: FSNavigator
@@ -9,8 +9,7 @@ export let nav: FSNavigator
{#each $nav.path as node, i (node.path)} {#each $nav.path as node, i (node.path)}
<a <a
href={"/d"+fs_encode_path(node.path)} href={"/d"+fs_encode_path(node.path)}
class="breadcrumb button" class="breadcrumb button flat"
class:button_highlight={$nav.base_index === i}
on:click|preventDefault={() => {nav.navigate(node.path, true)}} on:click|preventDefault={() => {nav.navigate(node.path, true)}}
> >
{#if node.abuse_type !== undefined} {#if node.abuse_type !== undefined}
@@ -22,19 +21,24 @@ export let nav: FSNavigator
{node.name} {node.name}
</div> </div>
</a> </a>
{#if $nav.base_index !== i}
<i class="icon">chevron_right</i>
{/if}
{/each} {/each}
</div> </div>
<style> <style>
.breadcrumbs { .breadcrumbs {
flex-grow: 1; flex: 0 0 auto;
flex-shrink: 1;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: left;
flex-wrap: wrap; flex-wrap: wrap;
flex-direction: row; flex-direction: row;
overflow: hidden; overflow: hidden;
background: var(--shaded_background);
backdrop-filter: blur(4px);
border-bottom: 1px solid var(--separator);
} }
.breadcrumb { .breadcrumb {
min-width: 1em; min-width: 1em;
@@ -42,6 +46,8 @@ export let nav: FSNavigator
word-break: break-all; word-break: break-all;
display: inline-flex; display: inline-flex;
flex-direction: row; flex-direction: row;
background-color: unset;
box-shadow: none;
} }
.node_name { .node_name {
max-width: 20vw; max-width: 20vw;

View File

@@ -2,7 +2,7 @@
import Chart from "util/Chart.svelte"; import Chart from "util/Chart.svelte";
import { formatDataVolume, formatDate, formatThousands } from "util/Formatting"; import { formatDataVolume, formatDate, formatThousands } from "util/Formatting";
import Modal from "util/Modal.svelte"; import Modal from "util/Modal.svelte";
import { fs_path_url, fs_share_hotlink_url, fs_share_url, fs_timeseries, type FSNode } from "./FilesystemAPI"; import { fs_path_url, fs_share_hotlink_url, fs_share_url, fs_timeseries, type FSNode } from "lib/FilesystemAPI";
import { color_by_name } from "util/Util.svelte"; import { color_by_name } from "util/Util.svelte";
import { tick } from "svelte"; import { tick } from "svelte";
import CopyButton from "layout/CopyButton.svelte"; import CopyButton from "layout/CopyButton.svelte";

View File

@@ -1,6 +1,6 @@
import { fs_get_node, fs_encode_path, fs_split_path } from "./FilesystemAPI"; import { loading_finish, loading_start } from "lib/Loading";
import type { FSNode, FSPath, FSPermissions, FSContext } from "./FilesystemAPI"; import { fs_get_node, fs_encode_path, fs_split_path } from "../lib/FilesystemAPI";
import type { Writable } from "svelte/store" import type { FSNode, FSPath, FSPermissions, FSContext } from "../lib/FilesystemAPI";
export class FSNavigator { export class FSNavigator {
// Parts of the raw API response // Parts of the raw API response
@@ -22,27 +22,16 @@ export class FSNavigator {
constructor(history_enabled = true) { constructor(history_enabled = true) {
this.history_enabled = history_enabled this.history_enabled = history_enabled
}
// If history logging is enabled we capture the popstate event, which // The popstate event can be used to listen for navigation events. Register
// fires when the user uses the back and forward buttons in the browser. // this event listener on the <svelte:window> in the parent element. When
// Instead of reloading the page we use the navigator to navigate to the // the user presses the back or forward buttons in the browser we'll catch
// new page // the event and navigate to the proper directory
if (history_enabled) { popstate = (e: PopStateEvent) => {
window.addEventListener("popstate", () => {
// Get the part of the URL after the fs root and navigate to it // Get the part of the URL after the fs root and navigate to it
const path = document.location.pathname.replace("/d/", "") const path = window.location.pathname.replace(/^\/d/, "")
this.navigate(decodeURIComponent(path), false) this.navigate(decodeURI(path), false)
})
}
}
// If you set the loading property to a boolean writable store the navigator
// will use it to publish its loading states
loading: Writable<boolean> | null = null
set_loading = (b: boolean) => {
if (this.loading !== null) {
this.loading.set(b)
}
} }
// The FSNavigator acts as a svelte store. This allows for DOM reactivity. // The FSNavigator acts as a svelte store. This allows for DOM reactivity.
@@ -72,7 +61,7 @@ export class FSNavigator {
console.debug("Navigating to path", path, push_history) console.debug("Navigating to path", path, push_history)
try { try {
this.set_loading(true) loading_start()
const resp = await fs_get_node(path) const resp = await fs_get_node(path)
this.open_node(resp, push_history) this.open_node(resp, push_history)
} catch (err: any) { } catch (err: any) {
@@ -89,7 +78,7 @@ export class FSNavigator {
alert("Error: " + err) alert("Error: " + err)
} }
} finally { } finally {
this.set_loading(false) loading_finish()
} }
} }
@@ -108,7 +97,7 @@ export class FSNavigator {
// we still replace the URL with replaceState. This way the user is not // we still replace the URL with replaceState. This way the user is not
// greeted to a 404 page when refreshing after renaming a file // greeted to a 404 page when refreshing after renaming a file
if (this.history_enabled) { if (this.history_enabled) {
window.document.title = node.path[node.base_index].name + " ~ pixeldrain" window.document.title = node.path[node.base_index].name + " / FNX"
const url = "/d" + fs_encode_path(node.path[node.base_index].path) + window.location.hash const url = "/d" + fs_encode_path(node.path[node.base_index].path) + window.location.hash
if (push_history) { if (push_history) {
window.history.pushState({}, window.document.title, url) window.history.pushState({}, window.document.title, url)
@@ -189,14 +178,14 @@ export class FSNavigator {
let siblings: Array<FSNode> let siblings: Array<FSNode>
try { try {
this.set_loading(true) loading_start()
siblings = await this.get_siblings() siblings = await this.get_siblings()
} catch (err) { } catch (err) {
console.error(err) console.error(err)
alert(err) alert(err)
return return
} finally { } finally {
this.set_loading(false) loading_finish()
} }
let next_sibling: FSNode | null = null let next_sibling: FSNode | null = null

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
import { formatDataVolume, formatThousands } from "util/Formatting" import { formatDataVolume, formatThousands } from "util/Formatting"
import { fs_path_url } from "./FilesystemAPI"; import { fs_path_url } from "lib/FilesystemAPI";
import type { FSNavigator } from "./FSNavigator"; import type { FSNavigator } from "./FSNavigator";
export let nav: FSNavigator export let nav: FSNavigator
@@ -104,7 +104,7 @@ const close_socket = () => {
</div> </div>
<div class="group"> <div class="group">
<div class="label">Transfer used</div> <div class="label">Egress</div>
<div class="stat"> <div class="stat">
{loading ? "Loading..." : formatDataVolume(transfer_used, 3)} {loading ? "Loading..." : formatDataVolume(transfer_used, 3)}
</div> </div>
@@ -140,18 +140,11 @@ const close_socket = () => {
text-align: center; text-align: center;
} }
.label { .label {
padding-left: 0.5em; text-align: center;
text-align: left;
font-size: 0.8em; font-size: 0.8em;
line-height: 1em; line-height: 1em;
} }
.stat { .stat {
line-height: 1.2em; line-height: 1.2em;
} }
@media (max-width: 1000px) {
.label {
text-align: center;
padding-left: 0;
}
}
</style> </style>

View File

@@ -1,20 +1,17 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
import LoadingIndicator from "util/LoadingIndicator.svelte";
import EditWindow from "./edit_window/EditWindow.svelte"; import EditWindow from "./edit_window/EditWindow.svelte";
import Toolbar from "./Toolbar.svelte"; import Toolbar from "./Toolbar.svelte";
import Breadcrumbs from "./Breadcrumbs.svelte"; import Breadcrumbs from "./Breadcrumbs.svelte";
import DetailsWindow from "./DetailsWindow.svelte"; import DetailsWindow from "./DetailsWindow.svelte";
import FilePreview from "./viewers/FilePreview.svelte"; import FilePreview from "./viewers/FilePreview.svelte";
import FSUploadWidget from "./upload_widget/FSUploadWidget.svelte"; import FSUploadWidget from "./upload_widget/FSUploadWidget.svelte";
import { fs_download, type FSPath } from "./FilesystemAPI"; import { fs_download, type FSPath } from "lib/FilesystemAPI";
import Menu from "./Menu.svelte";
import { FSNavigator } from "./FSNavigator" import { FSNavigator } from "./FSNavigator"
import { writable } from "svelte/store";
import { css_from_path } from "filesystem/edit_window/Branding"; import { css_from_path } from "filesystem/edit_window/Branding";
import AffiliatePrompt from "user_home/AffiliatePrompt.svelte"; import AffiliatePrompt from "user_home/AffiliatePrompt.svelte";
import { current_page_store } from "wrap/RouterStore";
let file_viewer: HTMLDivElement
let file_preview: FilePreview let file_preview: FilePreview
let toolbar: Toolbar let toolbar: Toolbar
let upload_widget: FSUploadWidget let upload_widget: FSUploadWidget
@@ -22,25 +19,37 @@ let details_visible = false
let edit_window: EditWindow let edit_window: EditWindow
let edit_visible = false let edit_visible = false
const loading = writable(true)
const nav = new FSNavigator(true) const nav = new FSNavigator(true)
onMount(() => { onMount(() => {
nav.loading = loading if ((window as any).intial_node !== undefined) {
console.debug("Loading initial node")
nav.open_node((window as any).initial_node as FSPath, false) nav.open_node((window as any).initial_node as FSPath, false)
} else {
console.debug("No initial node, fetching path", window.location.pathname)
nav.navigate(decodeURI(window.location.pathname).replace(/^\/d/, ""), false)
}
const page_sub = current_page_store.subscribe(() => {
console.debug("Caught page transition to", window.location.pathname)
nav.navigate(decodeURI(window.location.pathname).replace(/^\/d/, ""), false)
})
// Subscribe to navigation updates. This function returns a deconstructor // Subscribe to navigation updates. This function returns a deconstructor
// which we can conveniently return from our mount function as well // which we can conveniently return from our mount function as well
return nav.subscribe(nav => { const nav_sub = nav.subscribe(nav => {
if (!nav.initialized) { if (!nav.initialized) {
return return
} }
// Custom CSS rules for the whole viewer // Custom CSS rules for the whole viewer
document.documentElement.style = css_from_path(nav.path) document.documentElement.style = css_from_path(nav.path)
loading.set(false)
}) })
return () => {
page_sub()
nav_sub()
document.documentElement.style = ""
}
}) })
const keydown = (e: KeyboardEvent) => { const keydown = (e: KeyboardEvent) => {
@@ -127,23 +136,8 @@ const keydown = (e: KeyboardEvent) => {
<svelte:window on:keydown={keydown} /> <svelte:window on:keydown={keydown} />
<div bind:this={file_viewer} class="file_viewer"> <div class="filesystem">
<div class="headerbar">
<Menu/>
<Breadcrumbs nav={nav}/> <Breadcrumbs nav={nav}/>
</div>
<div class="viewer_area">
<Toolbar
bind:this={toolbar}
nav={nav}
file_viewer={file_viewer}
file_preview={file_preview}
bind:details_visible={details_visible}
edit_window={edit_window}
bind:edit_visible={edit_visible}
on:download={() => fs_download(nav.base)}
/>
<div class="file_preview"> <div class="file_preview">
<FilePreview <FilePreview
@@ -156,6 +150,15 @@ const keydown = (e: KeyboardEvent) => {
on:details={() => details_visible = !details_visible} on:details={() => details_visible = !details_visible}
/> />
</div> </div>
<Toolbar
bind:this={toolbar}
nav={nav}
bind:details_visible={details_visible}
edit_window={edit_window}
bind:edit_visible={edit_visible}
on:download={() => fs_download(nav.base)}
/>
</div> </div>
<DetailsWindow nav={nav} bind:visible={details_visible} /> <DetailsWindow nav={nav} bind:visible={details_visible} />
@@ -168,9 +171,6 @@ const keydown = (e: KeyboardEvent) => {
<AffiliatePrompt/> <AffiliatePrompt/>
<LoadingIndicator loading={$loading}/>
</div>
<style> <style>
:global(*) { :global(*) {
transition: background-color 0.2s, transition: background-color 0.2s,
@@ -183,56 +183,15 @@ const keydown = (e: KeyboardEvent) => {
} }
/* Viewer container */ /* Viewer container */
.file_viewer { .filesystem {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; height: 100vh;
width: 100%;
/* Force some variable usage that is normally out of scope */
color: var(--body_text_color);
background-image: var(--background_image, var(--background_pattern));
background-color: var(--background_pattern_color);
background-size: var(--background_image_size, initial);
background-position: var(--background_image_position, initial);
background-repeat: var(--background_image_repeat, repeat);
}
/* Headerbar (row 1) */
.headerbar {
flex: 0 0 0;
display: flex;
flex-direction: row;
text-align: left;
box-shadow: none;
background-color: var(--shaded_background);
backdrop-filter: blur(4px);
}
/* File preview area (row 2) */
.viewer_area {
flex: 1 1 0;
display: flex;
flex-direction: row;
overflow: hidden;
}
/* This max-width needs to be synced with the .toolbar max-width in
Toolbar.svelte and the .label max-width in FileStats.svelte */
@media (max-width: 1000px) {
.viewer_area {
flex-direction: column-reverse;
}
} }
.file_preview { .file_preview {
flex: 1 1 0; flex: 1 1 auto;
overflow: auto; overflow: auto;
border: 1px solid var(--separator);
} }
</style> </style>

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { FSNavigator } from "./FSNavigator"; import type { FSNavigator } from "./FSNavigator";
import { fs_node_icon, fs_share_hotlink_url, fs_share_url, fs_update, node_is_shared, type FSNode, type FSPermissions } from "./FilesystemAPI"; import { fs_node_icon, fs_share_hotlink_url, fs_share_url, fs_update, node_is_shared, type FSNode, type FSPermissions } from "lib/FilesystemAPI";
import { copy_text } from "util/Util.svelte"; import { copy_text } from "util/Util.svelte";
import CopyButton from "layout/CopyButton.svelte"; import CopyButton from "layout/CopyButton.svelte";
import Dialog from "layout/Dialog.svelte"; import Dialog from "layout/Dialog.svelte";

View File

@@ -4,8 +4,7 @@ import { copy_text } from "util/Util.svelte";
import FileStats from "./FileStats.svelte"; import FileStats from "./FileStats.svelte";
import type { FSNavigator } from "./FSNavigator"; import type { FSNavigator } from "./FSNavigator";
import EditWindow from "./edit_window/EditWindow.svelte"; import EditWindow from "./edit_window/EditWindow.svelte";
import FilePreview from "./viewers/FilePreview.svelte"; import { fs_share_url } from "lib/FilesystemAPI";
import { fs_share_url } from "./FilesystemAPI";
import ShareDialog from "./ShareDialog.svelte"; import ShareDialog from "./ShareDialog.svelte";
let dispatch = createEventDispatcher() let dispatch = createEventDispatcher()
@@ -14,8 +13,6 @@ export let nav: FSNavigator
export let details_visible = false export let details_visible = false
export let edit_window: EditWindow export let edit_window: EditWindow
export let edit_visible = false export let edit_visible = false
export let file_viewer: HTMLDivElement
export let file_preview: FilePreview
let share_dialog: ShareDialog let share_dialog: ShareDialog
$: share_url = fs_share_url($nav.path) $: share_url = fs_share_url($nav.path)
@@ -30,46 +27,11 @@ export const copy_link = () => {
link_copied = true link_copied = true
setTimeout(() => {link_copied = false}, 60000) setTimeout(() => {link_copied = false}, 60000)
} }
let fullscreen = false
export const toggle_fullscreen = () => {
if (document.fullscreenElement !== null) {
try {
document.exitFullscreen()
} catch (err) {
console.debug("Failed to exit fullscreen", err)
}
fullscreen = false
} else {
if (!file_preview.toggle_fullscreen()) {
file_viewer.requestFullscreen()
}
fullscreen = true
}
}
let expanded = true
let expand = (e: Event) => {
e.preventDefault()
e.stopPropagation()
expanded = !expanded
}
</script> </script>
<div class="toolbar" class:expanded> <div class="toolbar">
<div class="stats_container" on:click={expand} on:keypress={expand} role="button" tabindex="0">
<button class="button_expand hidden_vertical" on:click={expand}>
{#if expanded}
<i class="icon">expand_more</i>
{:else}
<i class="icon">expand_less</i>
{/if}
</button>
<FileStats nav={nav}/>
</div>
<div class="separator"></div>
<div class="grid"> <div class="grid">
<FileStats nav={nav}/>
<div class="button_row"> <div class="button_row">
<button on:click={() => {nav.open_sibling(-1)}}> <button on:click={() => {nav.open_sibling(-1)}}>
@@ -83,8 +45,6 @@ let expand = (e: Event) => {
</button> </button>
</div> </div>
<div class="separator hidden_horizontal"></div>
<button on:click={() => dispatch("download")}> <button on:click={() => dispatch("download")}>
<i class="icon">save</i> <i class="icon">save</i>
<span>Download</span> <span>Download</span>
@@ -105,21 +65,6 @@ let expand = (e: Event) => {
</button> </button>
{/if} {/if}
<button
class="toolbar_button"
on:click={toggle_fullscreen}
class:button_highlight={fullscreen}
title="Open page in full screen mode">
{#if fullscreen}
<i class="icon">fullscreen_exit</i>
{:else}
<i class="icon">fullscreen</i>
{/if}
<span>Fullscreen</span>
</button>
<div class="separator hidden_horizontal"></div>
<button on:click={() => details_visible = !details_visible} class:button_highlight={details_visible}> <button on:click={() => details_visible = !details_visible} class:button_highlight={details_visible}>
<i class="icon">help</i> <i class="icon">help</i>
<span>Deta<u>i</u>ls</span> <span>Deta<u>i</u>ls</span>
@@ -140,22 +85,17 @@ let expand = (e: Event) => {
.toolbar { .toolbar {
flex: 0 0 auto; flex: 0 0 auto;
overflow-x: hidden; overflow-x: hidden;
overflow-y: scroll; overflow-y: hidden;
transition: max-height 0.3s; transition: max-height 0.3s;
background-color: var(--shaded_background); border-top: 1px solid var(--separator);
background: var(--shaded_background);
backdrop-filter: blur(4px); backdrop-filter: blur(4px);
} }
.grid { .grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(7.5em, 1fr)); grid-template-columns: repeat(auto-fit, minmax(7.5em, 1fr));
} }
.separator {
height: 1px;
margin: 2px 0;
width: 100%;
background-color: var(--separator);
}
.button_row { .button_row {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@@ -164,46 +104,4 @@ let expand = (e: Event) => {
flex: 1 1 auto; flex: 1 1 auto;
justify-content: center; justify-content: center;
} }
.stats_container {
display: flex;
flex-direction: column;
}
.button_expand {
line-height: 1em;
}
.hidden_vertical {
display: none;
}
.hidden_horizontal {
display: block;
}
/* This max-width needs to be synced with the .viewer_area max-width in
Toolbar.svelte and the .label max-width in FileStats.svelte */
@media (max-width: 1000px) {
.toolbar {
overflow-y: hidden;
max-height: 2.1em;
}
.toolbar.expanded {
overflow-y: scroll;
max-height: 25vh;
}
.stats_container {
flex-direction: row;
}
.separator {
margin: 0;
}
.hidden_vertical {
display: block;
}
.hidden_horizontal {
display: none;
}
}
</style> </style>

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import Button from "layout/Button.svelte"; import Button from "layout/Button.svelte";
import type { FSPermissions, NodeOptions } from "filesystem/FilesystemAPI"; import type { FSPermissions, NodeOptions } from "lib/FilesystemAPI";
import PermissionButton from "./PermissionButton.svelte"; import PermissionButton from "./PermissionButton.svelte";
export let options: NodeOptions export let options: NodeOptions

View File

@@ -2,10 +2,38 @@ import parse from "pure-color/parse";
import rgb2hsl from "pure-color/convert/rgb2hsl"; import rgb2hsl from "pure-color/convert/rgb2hsl";
import hsl2rgb from "pure-color/convert/hsl2rgb"; import hsl2rgb from "pure-color/convert/hsl2rgb";
import rgb2hex from "pure-color/convert/rgb2hex"; import rgb2hex from "pure-color/convert/rgb2hex";
import type { FSNode, FSNodeProperties } from "lib/FilesystemAPI";
type Style = {
input_background: string,
input_hover_background: string,
input_text: string,
highlight_color: string,
highlight_background: string,
highlight_text_color: string,
link_color: string,
danger_color: string,
danger_text_color: string,
background_color: string,
background: string,
background_text_color: string,
background_pattern_color: string,
body_color: string,
body_background: string,
body_text_color: string,
shaded_background: string,
separator: string,
shadow_color: string,
card_color: string,
background_image: string,
background_image_size: string,
background_image_position: string,
background_image_repeat: string,
}
// Generate a branding style from a file's properties map // Generate a branding style from a file's properties map
export const branding_from_path = path => { export const branding_from_path = (path: Array<FSNode>) => {
let style = {} let style = <Style>{}
for (let node of path) { for (let node of path) {
add_styles(style, node.properties) add_styles(style, node.properties)
} }
@@ -15,17 +43,17 @@ export const branding_from_path = path => {
// The last style which was generated is cached, when we don't have a complete // The last style which was generated is cached, when we don't have a complete
// path to generate the style with we will use the cached style as a basis // path to generate the style with we will use the cached style as a basis
let last_generated_style = {} let last_generated_style = <Style>{}
export const branding_from_node = node => { export const branding_from_node = (node: FSNode) => {
add_styles(last_generated_style, node.properties) add_styles(last_generated_style, node.properties)
return gen_css(last_generated_style) return gen_css(last_generated_style)
} }
export const css_from_path = path => { export const css_from_path = (path: Array<FSNode>) => {
return gen_css(branding_from_path(path)) return gen_css(branding_from_path(path))
} }
const gen_css = style => { const gen_css = (style: Style) => {
return Object.entries(style).map(([key, value]) => `--${key}:${value}`).join(';'); return Object.entries(style).map(([key, value]) => `--${key}:${value}`).join(';');
} }
@@ -33,7 +61,7 @@ const gen_css = style => {
// existing style which is passed as the first argument. When navigating to a // existing style which is passed as the first argument. When navigating to a
// path this function is executed on every member of the path so all the styles // path this function is executed on every member of the path so all the styles
// get combined // get combined
const add_styles = (style, properties) => { const add_styles = (style: Style, properties: FSNodeProperties) => {
if (!properties || !properties.branding_enabled || properties.branding_enabled !== "true") { if (!properties || !properties.branding_enabled || properties.branding_enabled !== "true") {
return return
} }
@@ -83,7 +111,7 @@ const add_styles = (style, properties) => {
} }
} }
const add_contrast = (color, amt) => { const add_contrast = (color: string, amt: number) => {
let hsl = rgb2hsl(parse(color)) // Convert hex to hsl let hsl = rgb2hsl(parse(color)) // Convert hex to hsl
// If the lightness is less than 40 it is considered a dark colour. This // If the lightness is less than 40 it is considered a dark colour. This
// threshold is 40 instead of 50 because overall dark text is more legible // threshold is 40 instead of 50 because overall dark text is more legible
@@ -96,20 +124,20 @@ const add_contrast = (color, amt) => {
} }
// Darken and desaturate. Only used for shadows // Darken and desaturate. Only used for shadows
const darken = (color, percent) => { const darken = (color: string, percent: number) => {
let hsl = rgb2hsl(parse(color)) // Convert hex to hsl let hsl = rgb2hsl(parse(color)) // Convert hex to hsl
hsl[1] = hsl[1] * percent hsl[1] = hsl[1] * percent
hsl[2] = hsl[2] * percent hsl[2] = hsl[2] * percent
return rgb2hex(hsl2rgb(hsl)) // Convert back to hex return rgb2hex(hsl2rgb(hsl)) // Convert back to hex
} }
const set_alpha = (color, amt) => { const set_alpha = (color: string, amt: number) => {
let rgb = parse(color) let rgb = parse(color)
rgb.push(amt) rgb.push(amt)
return "rgba(" + rgb.join(", ") + ")" return "rgba(" + rgb.join(", ") + ")"
} }
const generate_link_color = (link_color, body_color) => { const generate_link_color = (link_color: string, body_color: string) => {
let link = rgb2hsl(parse(link_color)) let link = rgb2hsl(parse(link_color))
let body = rgb2hsl(parse(body_color)) let body = rgb2hsl(parse(body_color))

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import ThemePresets from "./ThemePresets.svelte"; import ThemePresets from "./ThemePresets.svelte";
import { fs_update, fs_node_type, type FSNode, type NodeOptions, node_is_shared, type FSPermissions } from "filesystem/FilesystemAPI"; import { fs_update, fs_node_type, type FSNode, type NodeOptions, node_is_shared, type FSPermissions } from "lib/FilesystemAPI";
import CustomBanner from "filesystem/viewers/CustomBanner.svelte"; import CustomBanner from "filesystem/viewers/CustomBanner.svelte";
import HelpButton from "layout/HelpButton.svelte"; import HelpButton from "layout/HelpButton.svelte";
import FilePicker from "filesystem/filemanager/FilePicker.svelte"; import FilePicker from "filesystem/filemanager/FilePicker.svelte";

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { fs_rename, fs_update, type FSNode, type NodeOptions } from "filesystem/FilesystemAPI"; import { fs_rename, fs_update, type FSNode, type NodeOptions } from "lib/FilesystemAPI";
import Modal from "util/Modal.svelte"; import Modal from "util/Modal.svelte";
import BrandingOptions from "./BrandingOptions.svelte"; import BrandingOptions from "./BrandingOptions.svelte";
import { branding_from_node } from "./Branding"; import { branding_from_node } from "./Branding";
@@ -39,18 +39,9 @@ export const edit = (f: FSNode, oae = false, open_tab = "") => {
} }
options.custom_domain_name = file.custom_domain_name options.custom_domain_name = file.custom_domain_name
options.shared = !(file.id === undefined || file.id === "")
if (options.shared) {
if (file.link_permissions === undefined) {
// Default to read-only for public links
file.link_permissions = { owner: false, read: true, write: false, delete: false}
} else {
options.link_permissions = file.link_permissions options.link_permissions = file.link_permissions
}
options.user_permissions = file.user_permissions options.user_permissions = file.user_permissions
options.password_permissions = file.password_permissions options.password_permissions = file.password_permissions
}
branding_enabled = options.branding_enabled === "true" branding_enabled = options.branding_enabled === "true"
if (branding_enabled) { if (branding_enabled) {
@@ -122,7 +113,7 @@ const save = async (keep_editing = false) => {
<i class="icon">share</i> <i class="icon">share</i>
Sharing Sharing
</button> </button>
{#if options.shared && $nav.permissions.owner} {#if $nav.permissions.owner}
<button class:button_highlight={tab === "access"} on:click={() => tab = "access"}> <button class:button_highlight={tab === "access"} on:click={() => tab = "access"}>
<i class="icon">key</i> <i class="icon">key</i>
Access control Access control

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import Button from "layout/Button.svelte"; import Button from "layout/Button.svelte";
import { fs_delete_all, type FSNode } from "filesystem/FilesystemAPI"; import { fs_delete_all, type FSNode } from "lib/FilesystemAPI";
import PathLink from "filesystem/util/PathLink.svelte"; import PathLink from "filesystem/util/PathLink.svelte";
import type { FSNavigator } from "filesystem/FSNavigator"; import type { FSNavigator } from "filesystem/FSNavigator";

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import ToggleButton from "layout/ToggleButton.svelte"; import ToggleButton from "layout/ToggleButton.svelte";
import type { FSPermissions } from "filesystem/FilesystemAPI"; import type { FSPermissions } from "lib/FilesystemAPI";
export let permissions = <FSPermissions>{} export let permissions = <FSPermissions>{}
</script> </script>

View File

@@ -1,11 +1,10 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from "svelte";
import { domain_url } from "util/Util.svelte"; import { domain_url } from "util/Util.svelte";
import CopyButton from "layout/CopyButton.svelte"; import CopyButton from "layout/CopyButton.svelte";
import { formatDate } from "util/Formatting"; import { formatDate } from "util/Formatting";
import type { FSNode, NodeOptions } from "filesystem/FilesystemAPI"; import { node_is_shared, type FSNode, type NodeOptions } from "lib/FilesystemAPI";
import AccessControl from "./AccessControl.svelte";
let dispatch = createEventDispatcher()
export let file: FSNode = {} as FSNode export let file: FSNode = {} as FSNode
export let options: NodeOptions export let options: NodeOptions
@@ -14,8 +13,8 @@ let preview_area: HTMLDivElement
$: share_link = window.location.protocol+"//"+window.location.host+"/d/"+file.id $: share_link = window.location.protocol+"//"+window.location.host+"/d/"+file.id
$: embed_iframe(file, options) $: embed_iframe(file, options)
let embed_iframe = (file: FSNode, options: NodeOptions) => { const embed_iframe = (file: FSNode, options: NodeOptions) => {
if (!options.shared) { if (!node_is_shared(file)) {
example = false example = false
embed_html = "File is not shared, can't generate embed code" embed_html = "File is not shared, can't generate embed code"
return return
@@ -31,7 +30,7 @@ let embed_iframe = (file: FSNode, options: NodeOptions) => {
let example = false let example = false
const toggle_example = () => { const toggle_example = () => {
if (options.shared) { if (node_is_shared(file)) {
example = !example example = !example
if (example) { if (example) {
preview_area.innerHTML = embed_html preview_area.innerHTML = embed_html
@@ -41,15 +40,6 @@ const toggle_example = () => {
} }
} }
const update_shared = () => {
// If sharing is enabled we automatically save the file so the user can copy
// the sharing link. But if the user disables sharing we don't automatically
// save so that the user can't accidentally discard a sharing link that's in
// use
if (options.shared && !file.id) {
dispatch("save")
}
}
</script> </script>
<fieldset> <fieldset>
@@ -64,34 +54,14 @@ const update_shared = () => {
</div> </div>
{/if} {/if}
<div>
<input
form="edit_form"
bind:checked={options.shared}
on:change={update_shared}
id="shared"
type="checkbox"
class="form_input"
/>
<label for="shared">Share this file or directory</label>
</div>
<div class="link_grid"> <div class="link_grid">
{#if options.shared} <a href={share_link}>{share_link}</a>
<span>Public link: <a href={share_link}>{share_link}</a></span>
<CopyButton text={share_link}>Copy</CopyButton> <CopyButton text={share_link}>Copy</CopyButton>
{/if}
</div> </div>
<p>
When a file or directory is shared it can be accessed through a
unique link. You can get the URL with the 'Copy link' button on
the toolbar, or share the link with the 'Share' button. If you
share a directory all the files within the directory are also
accessible from the link.
</p>
</fieldset> </fieldset>
<AccessControl options={options}/>
<fieldset> <fieldset>
<legend>Embedding</legend> <legend>Embedding</legend>
<p> <p>
@@ -108,7 +78,7 @@ const update_shared = () => {
<textarea bind:value={embed_html} style="width: 100%; height: 4em;"></textarea> <textarea bind:value={embed_html} style="width: 100%; height: 4em;"></textarea>
<br/> <br/>
<CopyButton text={embed_html}>Copy HTML</CopyButton> <CopyButton text={embed_html}>Copy HTML</CopyButton>
<button on:click={toggle_example} class:button_highlight={example} disabled={!options.shared}> <button on:click={toggle_example} class:button_highlight={example} disabled={!node_is_shared(file)}>
<i class="icon">visibility</i> Show example <i class="icon">visibility</i> Show example
</button> </button>
</div> </div>

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import type { FSNodeProperties } from "filesystem/FilesystemAPI"; import type { FSNodeProperties } from "lib/FilesystemAPI";
export let properties: FSNodeProperties = {} as FSNodeProperties export let properties: FSNodeProperties = {} as FSNodeProperties

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import { fs_encode_path, fs_node_icon, node_is_shared } from "filesystem/FilesystemAPI" import { fs_encode_path, fs_node_icon, node_is_shared } from "lib/FilesystemAPI"
import type { FSNavigator } from "filesystem/FSNavigator"; import type { FSNavigator } from "filesystem/FSNavigator";
import { FileAction } from "./FileManagerLib"; import { FileAction } from "./FileManagerLib";
@@ -36,21 +36,14 @@ export let hide_edit = false
</a> </a>
{/if} {/if}
{#if $nav.permissions.write && !hide_edit} {#if !hide_edit}
<button <button
class="action_button flat" class="action_button flat"
on:click={e => dispatch("file", {index: index, action: FileAction.Edit, original: e})} on:click={e => dispatch("file", {index: index, action: FileAction.Menu, original: e})}
> >
<i class="icon">edit</i> <i class="icon">menu</i>
</button> </button>
{/if} {/if}
<button
class="action_button flat"
on:click={e => dispatch("file", {index: index, action: FileAction.Download, original: e})}
>
<i class="icon">save</i>
</button>
</a> </a>
{/each} {/each}
</div> </div>
@@ -71,9 +64,9 @@ export let hide_edit = false
color: var(--body_text-color); color: var(--body_text-color);
padding: 2px; padding: 2px;
align-items: center; align-items: center;
background: var(--input_background); background: var(--shaded_background);
backdrop-filter: blur(4px);
border-radius: 4px; border-radius: 4px;
box-shadow: 1px 1px 8px 0px var(--shadow_color);
gap: 6px; gap: 6px;
} }
.node:hover:not(.node_selected) { .node:hover:not(.node_selected) {
@@ -92,7 +85,6 @@ export let hide_edit = false
height: 2em; height: 2em;
width: 2em; width: 2em;
vertical-align: middle; vertical-align: middle;
border-radius: 4px;
} }
.node_name { .node_name {
flex: 1 1 content; flex: 1 1 content;

View File

@@ -1,8 +1,9 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
import { fs_mkdir } from "filesystem/FilesystemAPI"; import { fs_mkdir } from "lib/FilesystemAPI";
import Button from "layout/Button.svelte"; import Button from "layout/Button.svelte";
import type { FSNavigator } from "filesystem/FSNavigator"; import type { FSNavigator } from "filesystem/FSNavigator";
import { loading_finish, loading_start } from "lib/Loading";
export let nav: FSNavigator export let nav: FSNavigator
@@ -14,7 +15,7 @@ let create_dir = async () => {
form.append("type", "dir") form.append("type", "dir")
try { try {
nav.set_loading(true) loading_start()
await fs_mkdir(nav.base.path+"/"+new_dir_name) await fs_mkdir(nav.base.path+"/"+new_dir_name)
new_dir_name = "" // Clear input field new_dir_name = "" // Clear input field
error_msg = "" // Clear error msg error_msg = "" // Clear error msg
@@ -26,7 +27,7 @@ let create_dir = async () => {
error_msg = "Server returned an error: code: '"+err.value+"' message: "+err.message error_msg = "Server returned an error: code: '"+err.value+"' message: "+err.message
} }
} finally { } finally {
nav.set_loading(false) loading_finish()
} }
} }

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { fs_delete_all, fs_download, fs_rename, type FSNode } from "filesystem/FilesystemAPI" import { fs_delete_all, fs_download, fs_rename, type FSNode } from "lib/FilesystemAPI"
import { onMount } from "svelte" import { onMount } from "svelte"
import CreateDirectory from "./CreateDirectory.svelte" import CreateDirectory from "./CreateDirectory.svelte"
import ListView from "./ListView.svelte" import ListView from "./ListView.svelte"
@@ -13,6 +13,7 @@ import type { FSNavigator } from "filesystem/FSNavigator";
import FsUploadWidget from "filesystem/upload_widget/FSUploadWidget.svelte"; import FsUploadWidget from "filesystem/upload_widget/FSUploadWidget.svelte";
import EditWindow from "filesystem/edit_window/EditWindow.svelte"; import EditWindow from "filesystem/edit_window/EditWindow.svelte";
import { FileAction, type FileEvent } from "./FileManagerLib"; import { FileAction, type FileEvent } from "./FileManagerLib";
import FileMenu from "./FileMenu.svelte";
export let nav: FSNavigator export let nav: FSNavigator
export let upload_widget: FsUploadWidget export let upload_widget: FsUploadWidget
@@ -23,6 +24,7 @@ let uploader: FsUploadWidget
let mode = "viewing" let mode = "viewing"
let creating_dir = false let creating_dir = false
let show_hidden = false let show_hidden = false
let file_menu: FileMenu
export const upload = (files: File[]) => { export const upload = (files: File[]) => {
return uploader.upload(files) return uploader.upload(files)
@@ -84,6 +86,11 @@ const file_event = (e: CustomEvent<FileEvent>) => {
e.detail.original.stopPropagation() e.detail.original.stopPropagation()
fs_download(nav.children[index]) fs_download(nav.children[index])
break break
case FileAction.Menu:
e.detail.original.preventDefault()
e.detail.original.stopPropagation()
file_menu.open(nav.children[index], e.detail.original.target)
break
} }
} }
@@ -410,10 +417,10 @@ onMount(() => {
{/if} {/if}
</div> </div>
<FileMenu bind:this={file_menu} bind:nav bind:edit_window />
<style> <style>
.container { .container {
height: 100%;
width: 100%;
padding: 0; padding: 0;
overflow: auto; overflow: auto;
display: block; display: block;

View File

@@ -3,4 +3,4 @@ export type FileEvent = {
action: FileAction, action: FileAction,
original: MouseEvent, original: MouseEvent,
} }
export enum FileAction { Click, Context, Edit, Share, Branding, Select, Download } export enum FileAction { Click, Context, Edit, Share, Branding, Select, Download, Menu }

View File

@@ -0,0 +1,64 @@
<script lang="ts">
import EditWindow from "filesystem/edit_window/EditWindow.svelte";
import type { FSNavigator } from "filesystem/FSNavigator";
import Button from "layout/Button.svelte";
import Dialog from "layout/Dialog.svelte";
import { bookmark_add, bookmark_del, bookmarks_store } from "lib/Bookmarks";
import { fs_download, type FSNode } from "lib/FilesystemAPI";
import { tick } from "svelte";
export let nav: FSNavigator
export let edit_window: EditWindow
let dialog: Dialog
let node: FSNode
let is_bookmark: boolean = false
export const open = async (n: FSNode, target: EventTarget) => {
node = n
is_bookmark = false
for (const bm of $bookmarks_store) {
console.log(bm)
if (bm.id === n.id) {
is_bookmark = true
break
}
}
// Wait for the view to update, so the dialog gets the proper measurements
await tick()
dialog.open((target as Element).closest("button").getBoundingClientRect())
}
const bookmark = () => {
bookmark_add({
id: node.id,
icon: "folder_shared",
label: node.name,
})
}
</script>
<Dialog bind:this={dialog}>
<div class="menu">
<Button click={() => {dialog.close(); fs_download(node)}} icon="save" label="Download"/>
{#if is_bookmark}
<Button click={() => {dialog.close(); bookmark_del(node.id)}} icon="bookmark_remove" label="Remove bookmark"/>
{:else}
<Button click={() => {dialog.close(); bookmark()}} icon="bookmark_add" label="Add bookmark"/>
{/if}
{#if $nav.permissions.write}
<Button click={() => {dialog.close(); edit_window.edit(node, false, "file")}} icon="edit" label="Edit"/>
<Button click={() => {dialog.close(); edit_window.edit(node, false, "share")}} icon="share" label="Share"/>
<Button click={() => {dialog.close(); edit_window.edit(node, false, "branding")}} icon="palette" label="Branding"/>
{/if}
</div>
</Dialog>
<style>
.menu {
display: flex;
flex-direction: column;
max-width: 20em;
}
</style>

View File

@@ -4,17 +4,15 @@ import ListView from "./ListView.svelte"
import GalleryView from "./GalleryView.svelte" import GalleryView from "./GalleryView.svelte"
import CompactView from "./CompactView.svelte" import CompactView from "./CompactView.svelte"
import Modal from "util/Modal.svelte"; import Modal from "util/Modal.svelte";
import LoadingIndicator from "util/LoadingIndicator.svelte";
import Breadcrumbs from "filesystem/Breadcrumbs.svelte" import Breadcrumbs from "filesystem/Breadcrumbs.svelte"
import { FSNavigator } from "filesystem/FSNavigator"; import { FSNavigator } from "filesystem/FSNavigator";
import type { FSNode } from "filesystem/FilesystemAPI"; import type { FSNode } from "lib/FilesystemAPI";
import { FileAction, type FileEvent } from "./FileManagerLib"; import { FileAction, type FileEvent } from "./FileManagerLib";
let nav = new FSNavigator(false) let nav = new FSNavigator(false)
let modal: Modal let modal: Modal
let dispatch = createEventDispatcher() let dispatch = createEventDispatcher()
let directory_view = "" let directory_view = ""
let loading = false
let large_icons = false let large_icons = false
let show_hidden = false let show_hidden = false
export let select_multiple = false export let select_multiple = false
@@ -194,8 +192,6 @@ onMount(() => {
on:file={file_event} on:file={file_event}
/> />
{/if} {/if}
<LoadingIndicator loading={loading}/>
</Modal> </Modal>
<style> <style>

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { fs_node_icon, fs_node_type, fs_encode_path } from "filesystem/FilesystemAPI"; import { fs_node_icon, fs_node_type, fs_encode_path } from "lib/FilesystemAPI";
import type { FSNavigator } from "filesystem/FSNavigator"; import type { FSNavigator } from "filesystem/FSNavigator";
import { FileAction } from "./FileManagerLib"; import { FileAction } from "./FileManagerLib";
let dispatch = createEventDispatcher() let dispatch = createEventDispatcher()
@@ -46,15 +46,15 @@ export let large_icons = false
width: 150px; width: 150px;
height: 150px; height: 150px;
overflow: hidden; overflow: hidden;
border-radius: 8px; background: var(--shaded_background);
background: var(--input_background); backdrop-filter: blur(4px);
border-radius: 4px;
color: var(--input_text); color: var(--input_text);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
transition: background 0.2s; transition: background 0.2s;
text-decoration: none; text-decoration: none;
padding: 3px; padding: 3px;
box-shadow: 1px 1px 0px 0px var(--shadow_color);
} }
.file.large_icons { .file.large_icons {
width: 200px; width: 200px;
@@ -87,7 +87,7 @@ export let large_icons = false
} }
.node_icon { .node_icon {
flex: 1 1 0; flex: 1 1 0;
border-radius: 6px; border-radius: 4px;
background-position: center; background-position: center;
background-size: contain; background-size: contain;
background-repeat: no-repeat; background-repeat: no-repeat;

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import { formatDataVolume } from "util/Formatting"; import { formatDataVolume } from "util/Formatting";
import { fs_encode_path, fs_node_icon, node_is_shared } from "filesystem/FilesystemAPI" import { fs_encode_path, fs_node_icon, node_is_shared } from "lib/FilesystemAPI"
import type { FSNavigator } from "filesystem/FSNavigator"; import type { FSNavigator } from "filesystem/FSNavigator";
import SortButton from "layout/SortButton.svelte"; import SortButton from "layout/SortButton.svelte";
import { FileAction } from "./FileManagerLib"; import { FileAction } from "./FileManagerLib";
@@ -19,7 +19,7 @@ export let hide_branding = false
<tr> <tr>
<td></td> <td></td>
<td><SortButton active_field={$nav.sort_last_field} asc={$nav.sort_asc} sort_func={nav.sort_children} field="name">Name</SortButton></td> <td><SortButton active_field={$nav.sort_last_field} asc={$nav.sort_asc} sort_func={nav.sort_children} field="name">Name</SortButton></td>
<td><SortButton active_field={$nav.sort_last_field} asc={$nav.sort_asc} sort_func={nav.sort_children} field="file_size">Size</SortButton></td> <td class="hide_small"><SortButton active_field={$nav.sort_last_field} asc={$nav.sort_asc} sort_func={nav.sort_children} field="file_size">Size</SortButton></td>
<td></td> <td></td>
</tr> </tr>
{#each $nav.children as child, index (child.path)} {#each $nav.children as child, index (child.path)}
@@ -60,14 +60,11 @@ export let hide_branding = false
<i class="icon">palette</i> <i class="icon">palette</i>
</button> </button>
{/if} {/if}
{#if $nav.permissions.write && !hide_edit} {#if !hide_edit}
<button class="action_button" on:click={e => dispatch("file", {index: index, action: FileAction.Edit, original: e})}> <button class="action_button" on:click={e => dispatch("file", {index: index, action: FileAction.Menu, original: e})}>
<i class="icon">edit</i> <i class="icon">menu</i>
</button> </button>
{/if} {/if}
<button class="action_button" on:click={e => dispatch("file", {index: index, action: FileAction.Download, original: e})}>
<i class="icon">save</i>
</button>
</div> </div>
</td> </td>
</a> </a>
@@ -77,13 +74,11 @@ export let hide_branding = false
<style> <style>
.directory { .directory {
display: table; display: table;
margin: 8px auto 16px auto;
background: var(--shaded_background); background: var(--shaded_background);
border-collapse: collapse; border-collapse: collapse;
border-radius: 8px; backdrop-filter: blur(4px);
max-width: 1200px;
max-width: 99%; margin: auto; /* center */
width: 1200px;
} }
.directory > * { .directory > * {
display: table-row; display: table-row;
@@ -116,7 +111,7 @@ td {
height: 32px; height: 32px;
width: 32px; width: 32px;
vertical-align: middle; vertical-align: middle;
border-radius: 4px; /* border-radius: 4px; */
margin: 2px; margin: 2px;
} }
.node_name { .node_name {

View File

@@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
import { fs_search, fs_encode_path, fs_thumbnail_url } from "filesystem/FilesystemAPI"; import { fs_search, fs_encode_path, fs_thumbnail_url } from "lib/FilesystemAPI";
import type { FSNavigator } from "filesystem/FSNavigator"; import type { FSNavigator } from "filesystem/FSNavigator";
import { loading_finish, loading_start } from "lib/Loading";
export let nav: FSNavigator export let nav: FSNavigator
@@ -43,9 +44,9 @@ const search = async (limit = 10) => {
last_limit = limit last_limit = limit
searching = true searching = true
nav.set_loading(true)
try { try {
loading_start()
search_results = await fs_search(nav.base.path, search_term, limit) search_results = await fs_search(nav.base.path, search_term, limit)
} catch (err) { } catch (err) {
if (err.value) { if (err.value) {
@@ -54,6 +55,8 @@ const search = async (limit = 10) => {
alert(err) alert(err)
console.error(err) console.error(err)
} }
} finally {
loading_finish()
} }
if (search_results.length > 0 && selected_result > search_results.length-1) { if (search_results.length > 0 && selected_result > search_results.length-1) {
@@ -61,7 +64,6 @@ const search = async (limit = 10) => {
} }
searching = false searching = false
nav.set_loading(false)
// It's possible that the user entered another letter while we were // It's possible that the user entered another letter while we were
// performing the search reqeust. If this happens we run the search function // performing the search reqeust. If this happens we run the search function
@@ -218,7 +220,6 @@ const window_keydown = (e: KeyboardEvent) => {
max-width: 100%; max-width: 100%;
padding-top: 2px; padding-top: 2px;
padding-bottom: 2px; padding-bottom: 2px;
border-bottom: 1px solid var(--separator);
} }
.search_form { .search_form {

View File

@@ -11,14 +11,11 @@ export type UploadJob = {
</script> </script>
<script lang="ts"> <script lang="ts">
import { tick } from "svelte"; import { tick } from "svelte";
import { fade } from "svelte/transition";
import UploadProgress from "./UploadProgress.svelte"; import UploadProgress from "./UploadProgress.svelte";
import type { FSNavigator } from "filesystem/FSNavigator"; import type { FSNavigator } from "filesystem/FSNavigator";
export let nav: FSNavigator export let nav: FSNavigator
const max_concurrent_uploads = 5
let file_input_field: HTMLInputElement let file_input_field: HTMLInputElement
let file_input_change = (e: Event) => { let file_input_change = (e: Event) => {
// Start uploading the files async // Start uploading the files async
@@ -91,22 +88,29 @@ let active_uploads = 0
let state = "idle" let state = "idle"
const start_upload = async () => { const start_upload = async () => {
// Count the number of active uploads so we can know how many new uploads we active_uploads = 0
// can start let uploading_size = 0
active_uploads = upload_queue.reduce((acc, val) => { for (let i = 0; i < upload_queue.length; i++) {
if (val.status === "uploading") {
acc++
}
return acc
}, 0)
for (let i = 0; i < upload_queue.length && active_uploads < max_concurrent_uploads; i++) {
if (upload_queue[i]) { if (upload_queue[i]) {
// If this file is queued, start the upload
if (upload_queue[i].status === "queued") { if (upload_queue[i].status === "queued") {
active_uploads++
upload_queue[i].component.start() upload_queue[i].component.start()
upload_queue[i].status = "uploading" upload_queue[i].status = "uploading"
} }
// If this file is already uploading (or just started), count it
if (upload_queue[i].status === "uploading") {
uploading_size += upload_queue[i].total_size
active_uploads++
}
// If the size threshold or the concurrent upload limit is reached
// we break the loop. The system tries to keep an upload queue of
// 100 MB and a minimum of two concurrent uploads.
if ((uploading_size >= 100e6 && active_uploads >= 2) || active_uploads >= 10) {
console.debug("Current uploads", active_uploads, "uploads size", uploading_size)
break
}
} }
} }
@@ -153,7 +157,7 @@ const leave_confirmation = (e: BeforeUnloadEvent) => {
/> />
{#if visible} {#if visible}
<div class="upload_widget" transition:fade={{duration: 200}}> <div class="upload_widget">
<div class="header"> <div class="header">
{#if state === "idle"} {#if state === "idle"}
Waiting for files Waiting for files
@@ -184,7 +188,8 @@ const leave_confirmation = (e: BeforeUnloadEvent) => {
position: fixed; position: fixed;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 500px; width: auto;
min-width: 400px;
max-width: 80%; max-width: 80%;
height: auto; height: auto;
max-height: 50%; max-height: 50%;

View File

@@ -9,7 +9,7 @@
// //
// on_error is called when the upload has failed. The parameters are the error // on_error is called when the upload has failed. The parameters are the error
import { fs_path_url, type GenericResponse } from "filesystem/FilesystemAPI" import { fs_path_url, type GenericResponse } from "lib/FilesystemAPI"
// code and an error message // code and an error message
export const upload_file = ( export const upload_file = (

View File

@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import { fade } from "svelte/transition";
import { upload_file } from "./UploadFunc"; import { upload_file } from "./UploadFunc";
import ProgressBar from "util/ProgressBar.svelte"; import ProgressBar from "util/ProgressBar.svelte";
import Button from "layout/Button.svelte" import Button from "layout/Button.svelte"
@@ -56,14 +55,14 @@ const cancel = () => {
} }
</script> </script>
<div class="prog" transition:fade={{duration: 200}} class:error={job.status === "error"}> <div class="prog" class:error={job.status === "error"}>
<div class="bar"> <div class="bar">
{job.file.name}<br/> {job.file.name}<br/>
{#if error_code !== ""} {#if error_code !== ""}
{error_message}<br/> {error_message}<br/>
{error_code}<br/> {error_code}<br/>
{/if} {/if}
<ProgressBar total={total} used={loaded}/> <ProgressBar total={total} used={loaded} speed={500}/>
</div> </div>
<div class="cancel"> <div class="cancel">
<Button icon="cancel" click={cancel}/> <Button icon="cancel" click={cancel}/>

View File

@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { fs_path_url, fs_encode_path, fs_node_icon } from "filesystem/FilesystemAPI" import { fs_path_url, fs_encode_path, fs_node_icon } from "lib/FilesystemAPI"
import FileTitle from "layout/FileTitle.svelte";
import TextBlock from "layout/TextBlock.svelte" import TextBlock from "layout/TextBlock.svelte"
import type { FSNavigator } from 'filesystem/FSNavigator'; import type { FSNavigator } from 'filesystem/FSNavigator';
@@ -50,8 +49,6 @@ onMount(() => {
<slot></slot> <slot></slot>
<FileTitle title={$nav.base.name}/>
<TextBlock width="1000px"> <TextBlock width="1000px">
<audio <audio
bind:this={player} bind:this={player}

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import IconBlock from "layout/IconBlock.svelte"; import IconBlock from "layout/IconBlock.svelte";
import { fs_thumbnail_url } from "filesystem/FilesystemAPI"; import { fs_thumbnail_url } from "lib/FilesystemAPI";
import TextBlock from "layout/TextBlock.svelte" import TextBlock from "layout/TextBlock.svelte"
import { formatDataVolume, formatDate } from "util/Formatting"; import { formatDataVolume, formatDate } from "util/Formatting";
import type { FSNavigator } from "filesystem/FSNavigator"; import type { FSNavigator } from "filesystem/FSNavigator";

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { onMount, tick } from "svelte"; import { onMount, tick } from "svelte";
import Spinner from "util/Spinner.svelte"; import Spinner from "util/Spinner.svelte";
import { fs_node_type } from "filesystem/FilesystemAPI"; import { fs_node_type } from "lib/FilesystemAPI";
import FileManager from "filesystem/filemanager/FileManager.svelte"; import FileManager from "filesystem/filemanager/FileManager.svelte";
import Audio from "./Audio.svelte"; import Audio from "./Audio.svelte";
import File from "./File.svelte"; import File from "./File.svelte";
@@ -74,7 +74,7 @@ export const seek = (delta: number) => {
{#if viewer_type === ""} {#if viewer_type === ""}
<div class="center"> <div class="center">
<Spinner></Spinner> <Spinner/>
</div> </div>
{:else if viewer_type === "dir"} {:else if viewer_type === "dir"}
<FileManager nav={nav} upload_widget={upload_widget} edit_window={edit_window}> <FileManager nav={nav} upload_widget={upload_widget} edit_window={edit_window}>

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import { swipe_nav } from "lib/SwipeNavigate"; import { swipe_nav } from "lib/SwipeNavigate";
import { fs_path_url } from "filesystem/FilesystemAPI"; import { fs_path_url } from "lib/FilesystemAPI";
import type { FSNavigator } from "filesystem/FSNavigator"; import type { FSNavigator } from "filesystem/FSNavigator";
let dispatch = createEventDispatcher(); let dispatch = createEventDispatcher();

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { fs_path_url } from "filesystem/FilesystemAPI"; import { fs_path_url } from "lib/FilesystemAPI";
import type { FSNavigator } from "filesystem/FSNavigator"; import type { FSNavigator } from "filesystem/FSNavigator";
export let nav: FSNavigator export let nav: FSNavigator

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { tick } from "svelte"; import { tick } from "svelte";
import { fs_path_url, type FSNode } from "filesystem/FilesystemAPI"; import { fs_path_url, type FSNode } from "lib/FilesystemAPI";
import type { FSNavigator } from "filesystem/FSNavigator"; import type { FSNavigator } from "filesystem/FSNavigator";
export let nav: FSNavigator export let nav: FSNavigator

View File

@@ -19,9 +19,10 @@ import { formatDate } from "util/Formatting"
import TorrentItem from "./TorrentItem.svelte" import TorrentItem from "./TorrentItem.svelte"
import IconBlock from "layout/IconBlock.svelte"; import IconBlock from "layout/IconBlock.svelte";
import TextBlock from "layout/TextBlock.svelte" import TextBlock from "layout/TextBlock.svelte"
import { fs_node_icon, fs_path_url } from "filesystem/FilesystemAPI"; import { fs_node_icon, fs_path_url } from "lib/FilesystemAPI";
import CopyButton from "layout/CopyButton.svelte"; import CopyButton from "layout/CopyButton.svelte";
import type { FSNavigator } from "filesystem/FSNavigator"; import type { FSNavigator } from "filesystem/FSNavigator";
import { loading_finish, loading_start } from "lib/Loading";
let dispatch = createEventDispatcher() let dispatch = createEventDispatcher()
@@ -31,7 +32,7 @@ let status = "loading"
export const update = async () => { export const update = async () => {
try { try {
nav.set_loading(true) loading_start()
let resp = await fetch(fs_path_url(nav.base.path)+"?torrent_info") let resp = await fetch(fs_path_url(nav.base.path)+"?torrent_info")
if (resp.status >= 400) { if (resp.status >= 400) {
@@ -58,7 +59,7 @@ export const update = async () => {
} catch (err) { } catch (err) {
console.error(err) console.error(err)
} finally { } finally {
nav.set_loading(false) loading_finish()
status = "finished" status = "finished"
} }
} }

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { onMount, createEventDispatcher, tick } from "svelte"; import { onMount, createEventDispatcher, tick } from "svelte";
import { video_position } from "lib/VideoPosition"; import { video_position } from "lib/VideoPosition";
import { fs_path_url } from "filesystem/FilesystemAPI"; import { fs_path_url } from "lib/FilesystemAPI";
import type { FSNavigator } from "filesystem/FSNavigator"; import type { FSNavigator } from "filesystem/FSNavigator";
let dispatch = createEventDispatcher() let dispatch = createEventDispatcher()

View File

@@ -13,8 +13,9 @@ import { formatDataVolume, formatDate } from "util/Formatting"
import ZipItem from "filesystem/viewers/ZipItem.svelte"; import ZipItem from "filesystem/viewers/ZipItem.svelte";
import IconBlock from "layout/IconBlock.svelte"; import IconBlock from "layout/IconBlock.svelte";
import TextBlock from "layout/TextBlock.svelte" import TextBlock from "layout/TextBlock.svelte"
import { fs_node_icon, fs_path_url } from "filesystem/FilesystemAPI"; import { fs_node_icon, fs_path_url } from "lib/FilesystemAPI";
import type { FSNavigator } from "filesystem/FSNavigator"; import type { FSNavigator } from "filesystem/FSNavigator";
import { loading_finish, loading_start } from "lib/Loading";
let dispatch = createEventDispatcher() let dispatch = createEventDispatcher()
@@ -36,8 +37,8 @@ export const update = async () => {
} }
try { try {
loading_start()
status = "loading" status = "loading"
nav.set_loading(true)
let resp = await fetch(fs_path_url(nav.base.path)+"?zip_info") let resp = await fetch(fs_path_url(nav.base.path)+"?zip_info")
if (resp.status >= 400) { if (resp.status >= 400) {
@@ -64,7 +65,7 @@ export const update = async () => {
console.error(err) console.error(err)
status = "parse_failed" status = "parse_failed"
} finally { } finally {
nav.set_loading(false) loading_finish()
} }
} }

View File

@@ -1,8 +0,0 @@
import App from './home_page/HomePage.svelte';
const app = new App({
target: document.getElementById("page_body"),
props: {}
});
export default app;

View File

@@ -15,7 +15,7 @@ import OtherPlans from "./OtherPlans.svelte";
</p> </p>
</section> </section>
<div class="vertical_scroll"> <div class="horizontal_scroll">
<div class="grid"> <div class="grid">
<div></div> <div></div>
<div class="top_row free_feat"> <div class="top_row free_feat">
@@ -271,20 +271,21 @@ import OtherPlans from "./OtherPlans.svelte";
font-weight: bold; font-weight: bold;
} }
.vertical_scroll { .horizontal_scroll {
overflow-x: scroll; overflow-x: scroll;
overflow-y: hidden; overflow-y: hidden;
width: 100%; width: 100%;
padding: 10px;
} }
.grid { .grid {
display: inline-grid; display: grid;
grid-auto-flow: row; grid-auto-flow: row;
grid-template-columns: 9em 1fr 1fr 1fr; grid-template-columns: 9em 1fr 1fr 1fr;
min-width: 40em; min-width: 50em;
max-width: 70em; max-width: 70em;
gap: 5px; gap: 5px;
margin: 10px; margin: auto;
} }
.grid > div { .grid > div {
justify-content: center; justify-content: center;

View File

@@ -5,6 +5,7 @@ import { drop_target } from "lib/DropTarget";
import AddressReputation from "./AddressReputation.svelte"; import AddressReputation from "./AddressReputation.svelte";
import FeatureTable from "./FeatureTable.svelte"; import FeatureTable from "./FeatureTable.svelte";
import GetStarted from "./GetStarted.svelte"; import GetStarted from "./GetStarted.svelte";
import Pricing from "./Pricing.svelte";
let upload_widget let upload_widget
</script> </script>
@@ -50,6 +51,7 @@ let upload_widget
Bullet lists Bullet lists
</li> </li>
</ul> </ul>
<Pricing/>
</section> </section>
</div> </div>
@@ -141,26 +143,18 @@ let upload_widget
<FeatureTable/> <FeatureTable/>
</div> </div>
<Footer nobg/> <svelte:head>
<style> <style>
:global(.page_body) { body {
background-image: url("/res/img/inflating_star.webp"); background-image: url("/res/img/catspaw.webp");
background-repeat: no-repeat; background-repeat: no-repeat;
background-attachment: fixed; background-attachment: fixed;
background-position: center; background-position: center;
background-size: cover; background-size: cover;
} }
.page_content { </style>
margin-top: 16px; </svelte:head>
margin-bottom: 16px; <style>
}
@media (max-width: 1100px) {
.page_content {
margin-top: 0;
}
}
header { header {
padding-top: 20px; padding-top: 20px;
padding-bottom: 20px; padding-bottom: 20px;

View File

@@ -54,6 +54,7 @@ let click_int = (e: MouseEvent) => {
</button> </button>
{:else} {:else}
<a <a
on:click={click_int}
href="{link_href}" href="{link_href}"
target={link_target} target={link_target}
class="button" class="button"

View File

@@ -22,6 +22,10 @@ export const open = (button_rect: DOMRect) => {
dialog.style.top = Math.round(Math.min(min_top, max_top)) + "px" dialog.style.top = Math.round(Math.min(min_top, max_top)) + "px"
} }
export const close = () => {
dialog.close()
}
// Attach a click handler so that the dialog is closed when the user clicks // Attach a click handler so that the dialog is closed when the user clicks
// outside the dialog. The way this check works is that there is usually an // outside the dialog. The way this check works is that there is usually an
// element inside the dialog with all the content. When the click target is // element inside the dialog with all the content. When the click target is

View File

@@ -39,7 +39,6 @@ export let width = "750px"
overflow-wrap: anywhere; overflow-wrap: anywhere;
background-color: var(--shaded_background); background-color: var(--shaded_background);
backdrop-filter: blur(4px); backdrop-filter: blur(4px);
border-radius: 8px;
padding: 8px; padding: 8px;
} }
</style> </style>

View File

@@ -5,7 +5,7 @@ export let asc = true
export let sort_func: (field: string) => void export let sort_func: (field: string) => void
</script> </script>
<button class:button_highlight={active_field === field} on:click={() => sort_func(field)}> <button on:click={() => sort_func(field)}>
{#if active_field === field} {#if active_field === field}
{#if asc}{:else}{/if} {#if asc}{:else}{/if}
{/if} {/if}
@@ -18,6 +18,6 @@ button {
margin: 0; margin: 0;
line-height: 1em; line-height: 1em;
width: 100%; width: 100%;
text-align: center; text-align: initial;
} }
</style> </style>

View File

@@ -11,13 +11,12 @@ export let center = false
.block { .block {
display: block; display: block;
text-align: initial; text-align: initial;
max-width: 99%; max-width: 100%;
overflow-wrap: anywhere; overflow-wrap: anywhere;
margin: 8px auto; margin: auto;
background-color: var(--shaded_background); background-color: var(--shaded_background);
backdrop-filter: blur(4px); backdrop-filter: blur(4px);
border-radius: 8px;
padding: 8px; padding: 8px;
} }
.center { .center {

View File

@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
import LoadingIndicator from "util/LoadingIndicator.svelte";
import PickAmount from "./PickAmount.svelte"; import PickAmount from "./PickAmount.svelte";
import PickCountry from "./PickCountry.svelte"; import PickCountry from "./PickCountry.svelte";
import { payment_providers, type CheckoutState } from "./CheckoutLib"; import { payment_providers, type CheckoutState } from "./CheckoutLib";
@@ -9,7 +8,6 @@ import { get_misc_vat_rate, get_user } from "lib/PixeldrainAPI";
import { countries } from "country-data-list"; import { countries } from "country-data-list";
import PickProvider from "./PickProvider.svelte"; import PickProvider from "./PickProvider.svelte";
let loading = false
let state: CheckoutState = {country: null, provider: null, amount: 0, vat: 0, name: ""} let state: CheckoutState = {country: null, provider: null, amount: 0, vat: 0, name: ""}
onMount(async () => { onMount(async () => {
@@ -42,8 +40,6 @@ onMount(async () => {
</script> </script>
<div class="highlight_border"> <div class="highlight_border">
<LoadingIndicator loading={loading}/>
{#if state.country !== null} {#if state.country !== null}
<div class="navbar"> <div class="navbar">
{#if state.country !== null} {#if state.country !== null}
@@ -76,13 +72,13 @@ onMount(async () => {
{/if} {/if}
{#if state.country === null} {#if state.country === null}
<PickCountry bind:state bind:loading={loading}/> <PickCountry bind:state/>
{:else if state.provider === null} {:else if state.provider === null}
<PickProvider bind:state/> <PickProvider bind:state/>
{:else if state.provider.need_name === true && state.name === ""} {:else if state.provider.need_name === true && state.name === ""}
<PickName bind:state/> <PickName bind:state/>
{:else} {:else}
<PickAmount bind:state bind:loading/> <PickAmount bind:state/>
{/if} {/if}
</div> </div>

View File

@@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import Euro from "util/Euro.svelte"; import Euro from "util/Euro.svelte";
import { checkout, type CheckoutState } from "./CheckoutLib"; import { checkout, type CheckoutState } from "./CheckoutLib";
import { loading_finish, loading_start } from "lib/Loading";
export let state: CheckoutState export let state: CheckoutState
export let loading: boolean
const amounts = [10, 20, 50, 100, 200, 500, 1000, 2000, 5000] const amounts = [10, 20, 50, 100, 200, 500, 1000, 2000, 5000]
let amount = 20 let amount = 20
@@ -11,10 +11,13 @@ let amount = 20
const submit = async (e: SubmitEvent) => { const submit = async (e: SubmitEvent) => {
e.preventDefault() e.preventDefault()
loading = true
state.amount = amount state.amount = amount
loading_start()
try {
await checkout(state) await checkout(state)
loading = false }finally {
loading_finish()
}
} }
</script> </script>

View File

@@ -2,13 +2,12 @@
import { countries } from "country-data-list"; import { countries } from "country-data-list";
import { get_misc_vat_rate, put_user } from "lib/PixeldrainAPI"; import { get_misc_vat_rate, put_user } from "lib/PixeldrainAPI";
import { payment_providers, type CheckoutState } from "./CheckoutLib"; import { payment_providers, type CheckoutState } from "./CheckoutLib";
import { loading_finish, loading_start } from "lib/Loading";
export let state: CheckoutState export let state: CheckoutState
export let loading: boolean
let country_input = "" let country_input = ""
const set_country = async (e?: Event) => { const set_country = async (e?: Event) => {
loading = true
if (e !== undefined) { if (e !== undefined) {
e.preventDefault() e.preventDefault()
} }
@@ -24,6 +23,7 @@ const set_country = async (e?: Event) => {
// with an invalid combination // with an invalid combination
state.provider = null state.provider = null
loading_start()
try { try {
// Save the user's country for later use // Save the user's country for later use
await put_user({checkout_country: c.alpha3}) await put_user({checkout_country: c.alpha3})
@@ -34,7 +34,7 @@ const set_country = async (e?: Event) => {
} catch (err) { } catch (err) {
alert(err) alert(err)
} finally { } finally {
loading = false loading_finish()
} }
} }

View File

@@ -0,0 +1,96 @@
import { writable } from "svelte/store"
import { fs_check_response, fs_path_url } from "./FilesystemAPI"
import { loading_finish, loading_start } from "lib/Loading"
const bookmarks_file = "/me/.fnx/bookmarks.json"
export type Bookmark = {
id: string,
icon: string,
label: string,
}
export let bookmarks_store = writable<Bookmark[]>(
[],
(set: (value: Bookmark[]) => void) => {
bookmarks_get().then((bm: Bookmark[]) => {
set(bm)
}).catch((err: any) => {
alert("Could not fetch bookmarks:\n" + JSON.stringify(err))
})
},
)
export const bookmarks_get = async (): Promise<Bookmark[]> => {
let bookmarks: Bookmark[] = []
try {
bookmarks = await fs_check_response(
await fetch(fs_path_url(bookmarks_file), { cache: "no-store" })
)
} catch (err) {
// If the bookmarks were not found when we return an empty bookmarks
// list
if (err.value !== "path_not_found") {
throw err
}
}
console.debug("Fetched", bookmarks.length, "bookmarks:", bookmarks)
bookmarks_store.set(bookmarks)
return bookmarks
}
export const bookmarks_save = async (bookmarks: Bookmark[]) => {
await fs_check_response(
await fetch(
fs_path_url(bookmarks_file) + "?make_parents=true",
{ method: "PUT", body: JSON.stringify(bookmarks) },
)
)
console.debug("Saved", bookmarks.length, "bookmarks:", bookmarks)
bookmarks_store.set(bookmarks)
}
export const bookmark_add = async (bm: Bookmark): Promise<Bookmark[]> => {
let bookmarks: Bookmark[] = []
try {
loading_start()
// Get bookmarks
bookmarks = await bookmarks_get()
// Add new bookmark
bookmarks.push(bm)
// Save new bookmarks
await bookmarks_save(bookmarks)
} finally {
loading_finish()
}
return bookmarks
}
export const bookmark_del = async (id: string): Promise<Bookmark[]> => {
let bookmarks: Bookmark[] = []
try {
loading_start()
// Get bookmarks
bookmarks = await bookmarks_get()
// Delete bookmark
for (let i = 0; i < bookmarks.length; i++) {
if (bookmarks[i].id === id) {
console.debug("Deleting bookmark at index", i, bookmarks[i])
bookmarks.splice(i, 1)
break
}
}
// Save new bookmarks
await bookmarks_save(bookmarks)
} finally {
loading_finish()
}
return bookmarks
}

View File

@@ -46,7 +46,11 @@ export type FSNode = {
} }
export const node_is_shared = (node: FSNode): boolean => { export const node_is_shared = (node: FSNode): boolean => {
if (node.link_permissions !== undefined && node.link_permissions.read) { if (
(node.link_permissions !== undefined && node.link_permissions.read === true) ||
(node.user_permissions !== undefined && Object.keys(node.user_permissions).length > 0) ||
(node.password_permissions !== undefined && Object.keys(node.password_permissions).length > 0)
) {
return true return true
} }
return false return false

View File

@@ -0,0 +1,22 @@
import { current_page_store, type Tab } from "wrap/RouterStore"
export const highlight_current_page = (node: HTMLAnchorElement) => {
const set_highlight = () => {
if (window.location.pathname === URL.parse(node.href).pathname) {
node.classList.add("button_highlight")
} else {
node.classList.remove("button_highlight")
}
}
// Initialize the status with the current page path
set_highlight()
// Set up a listener with the page router to catch navigation events
const unsub = current_page_store.subscribe((page: Tab) => { set_highlight() })
return {
destroy() {
unsub()
}
}
}

20
svelte/src/lib/Loading.ts Normal file
View File

@@ -0,0 +1,20 @@
import { get, writable } from "svelte/store";
export const loading_store = writable(0)
export const loading_start = () => {
loading_store.set(get(loading_store) + 1)
}
export const loading_finish = () => {
loading_store.set(get(loading_store) - 1)
}
export const loading_run = async <T>(fn: () => Promise<T>): Promise<T> => {
try {
loading_start()
return await fn()
} finally {
loading_finish()
}
}

View File

@@ -106,14 +106,18 @@ export type UserSession = {
valid_domains: string[], valid_domains: string[],
} }
export const get_user = async () => { let cached_user: User = null
export const get_user = async (): Promise<User> => {
if ((window as any).user !== undefined) { if ((window as any).user !== undefined) {
return (window as any).user as User return (window as any).user as User
} else if (cached_user !== null) {
return cached_user
} }
console.warn("user property is not defined on window") console.warn("user property is not defined on window")
return await check_response(await fetch(get_endpoint() + "/user")) as User cached_user = await check_response(await fetch(get_endpoint() + "/user")) as User
return cached_user
} }
export const put_user = async (data: Object) => { export const put_user = async (data: Object) => {
@@ -128,7 +132,6 @@ export const put_user = async (data: Object) => {
} }
} }
export type VATRate = { export type VATRate = {
name: string, name: string,
vat: number, vat: number,

View File

@@ -1,8 +0,0 @@
import App from './login/Router.svelte';
const app = new App({
target: document.getElementById("page_body"),
props: {}
});
export default app;

View File

@@ -27,14 +27,14 @@ onMount(async () => {
params.get("link_login_id") === null && params.get("link_login_id") === null &&
user.username !== "" user.username !== ""
) { ) {
console.debug("User is already logged in, redirecting to user dashboard") console.debug("User is already logged in, redirecting to user dashboard", user)
let login_redirect = params.get("redirect") let login_redirect = params.get("redirect")
if (typeof login_redirect === "string" && login_redirect.startsWith("/")) { if (typeof login_redirect === "string" && login_redirect.startsWith("/")) {
console.debug("redirecting user to requested path", login_redirect) console.debug("redirecting user to requested path", login_redirect)
window.location.href = window.location.protocol+"//"+window.location.host+login_redirect window.location.pathname = login_redirect
} else { } else {
window.location.href = "/user" window.location.pathname = "/user"
} }
} }
}) })

View File

@@ -0,0 +1,145 @@
<script lang="ts">
import { onMount } from "svelte";
const get_cookie = (cname: string) => {
let name = cname + "=";
let decodedCookie = decodeURIComponent(document.cookie);
let ca = decodedCookie.split(';');
for(let i = 0; i <ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
let style = get_cookie("style")
let hue = +get_cookie("hue")
// Style selector
onMount(() => {
document.getElementsByName("style").forEach(function(elem) {
elem.addEventListener("change", e => {
style = (e.target as HTMLInputElement).id.substring(6)
var date = new Date();
date.setTime(date.getTime() + (10 * 365 * 24 * 60 * 60 * 1000));
document.cookie = "style="+style+"; expires=" + date.toUTCString() + "; path=/"
reload_sheet()
})
});
document.getElementsByName("hue").forEach(function(elem) {
elem.addEventListener("change", e => {
hue = +(e.target as HTMLInputElement).id.substring(4)
if (hue === -1) {
document.cookie = "hue=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/"
}
var date = new Date();
date.setTime(date.getTime() + (10 * 365 * 24 * 60 * 60 * 1000));
document.cookie = "hue="+hue+"; expires=" + date.toUTCString() + "; path=/"
reload_sheet()
})
});
})
let stylesheet_2: HTMLLinkElement
const reload_sheet = () => {
let stylesheet1 = document.getElementById("stylesheet_theme") as HTMLLinkElement
// First load the sheet in the secondary tag, wait for it to load,
// and replace the original sheet when it has finished loading
stylesheet_2.href= "/theme.css?style="+style+"&hue="+hue
stylesheet_2.onload = e => {
stylesheet1.href= "/theme.css?style="+style+"&hue="+hue
}
}
</script>
<svelte:head>
<link bind:this={stylesheet_2} id="stylesheet_theme_2" rel="stylesheet" type="text/css" href="/theme.css"/>
</svelte:head>
<div id="page_content" class="page_content">
<section>
<p>
You can change how pixeldrain looks! Your theme choice will
be saved in a cookie.
</p>
<h2>Theme</h2>
<input type="radio" id="style_nord" name="style"><label for="style_nord">Nord</label>
(Inspired by <a href="https://www.nordtheme.com/" target="_blank">Nord</a>)
<br/>
Dynamic theme, changes based on operating system settings. Here you can choose a specific variant:
<br/>
<input type="radio" id="style_nord_dark" name="style"><label for="style_nord_dark">Nord dark</label>
<br/>
<input type="radio" id="style_nord_light" name="style"><label for="style_nord_light">Nord light</label>
<br/>
<br/>
<input type="radio" id="style_solarized" name="style"><label for="style_solarized">Solarized</label>
(Inspired by <a href="https://ethanschoonover.com/solarized/" target="_blank">Solarized</a>)
<br/>
Dynamic theme, changes based on operating system settings. Here you can choose a specific variant:
<br/>
<input type="radio" id="style_solarized_dark" name="style"><label for="style_solarized_dark">Solarized dark</label>
<br/>
<input type="radio" id="style_solarized_light" name="style"><label for="style_solarized_light">Solarized light</label>
<br/>
<!-- <br/> -->
<!-- <input type="radio" id="style_adwaita" name="style"><label for="style_adwaita">Adwaita</label><br/> -->
<br/>
<input type="radio" id="style_purple_drain" name="style"><label for="style_purple_drain">Purple drain</label>
<br/>
Classic 2022 style, with purple gradients
<br/>
<br/>
<input type="radio" id="style_classic" name="style"><label for="style_classic">Pixeldrain classic (gray)</label>
<br/>
Classic pre-2020 pixeldrain style, dark gray
<br/>
<br/>
Other (experimental) themes
<br/>
<input type="radio" id="style_maroon" name="style"><label for="style_maroon">Maroon Style</label>
<br/>
<input type="radio" id="style_hacker" name="style"><label for="style_hacker">Hacker Style</label>
<br/>
<input type="radio" id="style_canta" name="style"><label for="style_canta">Canta Style</label>
(Inspired by <a href="https://github.com/vinceliuice/Canta-theme" target="_blank">Canta GTK</a>)
<br/>
<input type="radio" id="style_skeuos" name="style"><label for="style_skeuos">Skeuos Style</label>
(Inspired by <a href="https://www.gnome-look.org/p/1441725/" target="_blank">Skeuos GTK</a>)
<br/>
<input type="radio" id="style_sweet" name="style"><label for="style_sweet">Sweet</label>
<br/>
<br/>
<input type="radio" id="style_adwaita" name="style"><label for="style_adwaita">Adwaita (dynamic)</label>
<br/>
<input type="radio" id="style_adwaita_dark" name="style"><label for="style_adwaita_dark">Adwaita dark</label>
<br/>
<input type="radio" id="style_adwaita_light" name="style"><label for="style_adwaita_light">Adwaita light</label>
<br/><br/>
<input type="radio" id="style_pixeldrain98" name="style"><label for="style_pixeldrain98">Pixeldrain 98</label>
<br/>
<input type="radio" id="style_pixeldrain98_dark" name="style"><label for="style_pixeldrain98_dark">Pixeldrain 98 (dark)</label>
<h2>Hue</h2>
<p>
Many themes support custom hues. The hue does not change the
contrast of the theme, only the color itself.
</p>
<input type="radio" id="hue_-1" name="hue"><label for="hue_-1">Default</label><br/>
<input type="radio" id="hue_354" name="hue"><label for="hue_354">Red</label><br/>
<input type="radio" id="hue_14" name="hue"><label for="hue_14">Orange</label><br/>
<input type="radio" id="hue_40" name="hue"><label for="hue_40">Yellow</label><br/>
<input type="radio" id="hue_92" name="hue"><label for="hue_92">Green</label><br/>
<input type="radio" id="hue_311" name="hue"><label for="hue_311">Purple</label><br/>
</section>
</div>

View File

@@ -1,8 +0,0 @@
import App from './speedtest/Index.svelte';
const app = new App({
target: document.getElementById("page_body"),
props: {}
});
export default app;

View File

@@ -1,13 +1,8 @@
<script> <script>
import Footer from "layout/Footer.svelte"; import Footer from "layout/Footer.svelte";
import Speedtest from "./Speedtest.svelte"; import Speedtest from "./Speedtest.svelte";
</script> </script>
<header>
<h1>Pixeldrain Speedtest</h1>
</header>
<div class="page_content"> <div class="page_content">
<Speedtest/> <Speedtest/>
@@ -106,11 +101,3 @@ import Speedtest from "./Speedtest.svelte";
</p> </p>
</section> </section>
</div> </div>
<Footer/>
<style>
.page_content {
margin-bottom: 16px;
}
</style>

View File

@@ -1,8 +0,0 @@
import App from './user_home/Router.svelte';
const app = new App({
target: document.getElementById("page_body"),
props: {}
});
export default app;

View File

@@ -1,13 +1,12 @@
<script> <script>
import { loading_finish, loading_start } from "lib/Loading";
import { formatDate } from "util/Formatting"; import { formatDate } from "util/Formatting";
import LoadingIndicator from "util/LoadingIndicator.svelte";
let loading = false
let loaded = false let loaded = false
let rows = [] let rows = []
const load_keys = async () => { const load_keys = async () => {
loading = true loading_start()
try { try {
const resp = await fetch(window.api_endpoint+"/user/session") const resp = await fetch(window.api_endpoint+"/user/session")
if(resp.status >= 400) { if(resp.status >= 400) {
@@ -30,13 +29,13 @@ const load_keys = async () => {
} catch (err) { } catch (err) {
alert(err) alert(err)
} finally { } finally {
loading = false loading_finish()
loaded = true loaded = true
} }
}; };
const create_key = async () => { const create_key = async () => {
loading = true loading_start()
try { try {
let form = new FormData() let form = new FormData()
form.append("app_name", "website keys page") form.append("app_name", "website keys page")
@@ -53,14 +52,15 @@ const create_key = async () => {
} }
} catch (err) { } catch (err) {
alert("Failed to create new API key! "+err) alert("Failed to create new API key! "+err)
} finally {
loading_finish()
} }
load_keys(); load_keys();
} }
const logout = async (key) => { const logout = async (key) => {
loading = true loading_start()
try { try {
const resp = await fetch( const resp = await fetch(
window.api_endpoint+"/user/session", window.api_endpoint+"/user/session",
@@ -76,14 +76,14 @@ const logout = async (key) => {
} }
} catch (err) { } catch (err) {
alert("Failed to delete key: "+err) alert("Failed to delete key: "+err)
} finally {
loading_finish()
} }
load_keys(); load_keys();
} }
</script> </script>
<LoadingIndicator loading={loading}/>
<section> <section>
{#if !loaded} {#if !loaded}
<div class="highlight_yellow"> <div class="highlight_yellow">

View File

@@ -1,9 +1,7 @@
<script> <script>
import { loading_finish, loading_start } from "lib/Loading";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { formatDate } from "util/Formatting"; import { formatDate } from "util/Formatting";
import LoadingIndicator from "util/LoadingIndicator.svelte";
let loading = false
let year = 0 let year = 0
let month = 0 let month = 0
@@ -11,7 +9,7 @@ let month_str = ""
let data = [] let data = []
const load_activity = async () => { const load_activity = async () => {
loading = true loading_start()
month_str = year + "-" + ("00"+(month)).slice(-2) month_str = year + "-" + ("00"+(month)).slice(-2)
try { try {
const resp = await fetch(window.api_endpoint+"/user/activity/" + month_str) const resp = await fetch(window.api_endpoint+"/user/activity/" + month_str)
@@ -29,7 +27,7 @@ const load_activity = async () => {
} catch (err) { } catch (err) {
alert(err) alert(err)
} finally { } finally {
loading = false loading_finish()
} }
}; };
const last_month = () => { const last_month = () => {
@@ -59,8 +57,6 @@ onMount(() => {
}) })
</script> </script>
<LoadingIndicator loading={loading}/>
<section> <section>
<h2>Account activity log</h2> <h2>Account activity log</h2>
<p> <p>

View File

@@ -1,14 +1,13 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
import Modal from "util/Modal.svelte"; import Modal from "util/Modal.svelte";
import LoadingIndicator from "util/LoadingIndicator.svelte";
import { get_user, put_user } from "lib/PixeldrainAPI"; import { get_user, put_user } from "lib/PixeldrainAPI";
import { loading_finish, loading_start } from "lib/Loading";
// When the always flag is set then the pop-up will also show if the user // When the always flag is set then the pop-up will also show if the user
// already has an affiliate ID set // already has an affiliate ID set
export let always = false export let always = false
let modal: Modal let modal: Modal
let loading: boolean
let referral: string let referral: string
let shown = false let shown = false
@@ -50,7 +49,7 @@ export const prompt = async (ref: string) => {
onMount(() => prompt(new URLSearchParams(document.location.search).get("ref"))) onMount(() => prompt(new URLSearchParams(document.location.search).get("ref")))
const allow = async () => { const allow = async () => {
loading = true loading_start()
try { try {
await put_user({affiliate_user_name: referral}) await put_user({affiliate_user_name: referral})
@@ -59,7 +58,7 @@ const allow = async () => {
} catch (err) { } catch (err) {
alert(err) alert(err)
} finally { } finally {
loading = false loading_finish()
} }
} }
@@ -70,7 +69,6 @@ const deny = () => {
</script> </script>
<Modal bind:this={modal} title="Affiliate sponsoring request" width="700px"> <Modal bind:this={modal} title="Affiliate sponsoring request" width="700px">
<LoadingIndicator bind:loading={loading} />
<section> <section>
<p> <p>
Hi! {referral} wants you to sponsor their pixeldrain account. This Hi! {referral} wants you to sponsor their pixeldrain account. This

View File

@@ -2,24 +2,22 @@
import { onMount } from "svelte"; import { onMount } from "svelte";
import Pro from "icons/Pro.svelte"; import Pro from "icons/Pro.svelte";
import { formatDataVolume } from "util/Formatting"; import { formatDataVolume } from "util/Formatting";
import LoadingIndicator from "util/LoadingIndicator.svelte";
import ProgressBar from "util/ProgressBar.svelte"; import ProgressBar from "util/ProgressBar.svelte";
import SuccessMessage from "util/SuccessMessage.svelte"; import SuccessMessage from "util/SuccessMessage.svelte";
import { loading_finish, loading_start } from "lib/Loading";
let loading = false
let success_message let success_message
let hotlinking = window.user.hotlinking_enabled let hotlinking = window.user.hotlinking_enabled
let transfer_cap = window.user.monthly_transfer_cap / 1e12 let transfer_cap = window.user.monthly_transfer_cap / 1e12
let skip_viewer = window.user.skip_file_viewer let skip_viewer = window.user.skip_file_viewer
const update = async () => { const update = async () => {
loading = true
const form = new FormData() const form = new FormData()
form.append("hotlinking_enabled", hotlinking) form.append("hotlinking_enabled", hotlinking)
form.append("transfer_cap", transfer_cap*1e12) form.append("transfer_cap", transfer_cap*1e12)
form.append("skip_file_viewer", skip_viewer) form.append("skip_file_viewer", skip_viewer)
loading_start()
try { try {
const resp = await fetch( const resp = await fetch(
window.api_endpoint+"/user", window.api_endpoint+"/user",
@@ -37,7 +35,7 @@ const update = async () => {
} catch (err) { } catch (err) {
success_message.set(false, "Failed to update subscription: "+err) success_message.set(false, "Failed to update subscription: "+err)
} finally { } finally {
loading = false loading_finish()
} }
} }
@@ -78,8 +76,6 @@ onMount(() => {
</script> </script>
<LoadingIndicator loading={loading}/>
<section> <section>
<h2><Pro/>Hotlinking (bandwidth sharing)</h2> <h2><Pro/>Hotlinking (bandwidth sharing)</h2>
<SuccessMessage bind:this={success_message}></SuccessMessage> <SuccessMessage bind:this={success_message}></SuccessMessage>

View File

@@ -1,13 +1,12 @@
<script> <script>
import { onMount } from "svelte"; import { onMount } from "svelte";
import LoadingIndicator from "util/LoadingIndicator.svelte";
import CopyButton from "layout/CopyButton.svelte"; import CopyButton from "layout/CopyButton.svelte";
import { loading_finish, loading_start } from "lib/Loading";
let loading = false
let app_name = "" let app_name = ""
let api_key = "" let api_key = ""
const create_key = async () => { const create_key = async () => {
loading = true loading_start()
try { try {
let form = new FormData() let form = new FormData()
form.append("app_name", app_name) form.append("app_name", app_name)
@@ -27,7 +26,7 @@ const create_key = async () => {
} catch (err) { } catch (err) {
alert("Failed to create new API key! "+err) alert("Failed to create new API key! "+err)
} finally { } finally {
loading = false loading_finish()
} }
} }
@@ -48,8 +47,6 @@ onMount(() => {
}) })
</script> </script>
<LoadingIndicator loading={loading}/>
<section> <section>
<!-- Show a back button if an app is selected --> <!-- Show a back button if an app is selected -->
{#if app_name} {#if app_name}

View File

@@ -1,16 +1,13 @@
<script> <script>
import Checkout from "layout/checkout/Checkout.svelte"; import Checkout from "layout/checkout/Checkout.svelte";
import { loading_finish, loading_start } from "lib/Loading";
import { onMount } from "svelte"; import { onMount } from "svelte";
import Euro from "util/Euro.svelte"; import Euro from "util/Euro.svelte";
import { formatDate } from "util/Formatting"; import { formatDate } from "util/Formatting";
import LoadingIndicator from "util/LoadingIndicator.svelte";
let loading = false
let credit_amount = 10 let credit_amount = 10
const checkout = async (network = "", amount = 0, country = "") => { const checkout = async (network = "", amount = 0, country = "") => {
loading = true
if (amount < 10) { if (amount < 10) {
alert("Amount needs to be at least €10") alert("Amount needs to be at least €10")
return return
@@ -21,6 +18,7 @@ const checkout = async (network = "", amount = 0, country = "") => {
form.set("network", network) form.set("network", network)
form.set("country", country) form.set("country", country)
loading_start()
try { try {
const resp = await fetch( const resp = await fetch(
window.api_endpoint+"/user/invoice", window.api_endpoint+"/user/invoice",
@@ -35,14 +33,14 @@ const checkout = async (network = "", amount = 0, country = "") => {
} catch (err) { } catch (err) {
alert(err) alert(err)
} finally { } finally {
loading = false loading_finish()
} }
} }
let invoices = [] let invoices = []
let unpaid_invoice = 0 let unpaid_invoice = 0
const load_invoices = async () => { const load_invoices = async () => {
loading = true loading_start()
try { try {
const resp = await fetch(window.api_endpoint+"/user/invoice") const resp = await fetch(window.api_endpoint+"/user/invoice")
if(resp.status >= 400) { if(resp.status >= 400) {
@@ -66,7 +64,7 @@ const load_invoices = async () => {
console.error(err) console.error(err)
alert(err) alert(err)
} finally { } finally {
loading = false loading_finish()
} }
}; };
@@ -75,8 +73,6 @@ onMount(() => {
}) })
</script> </script>
<LoadingIndicator loading={loading}/>
<section> <section>
<h2 id="deposit">Deposit credit</h2> <h2 id="deposit">Deposit credit</h2>
<p> <p>

View File

@@ -1,20 +1,19 @@
<script> <script>
import { onMount } from "svelte"; import { onMount } from "svelte";
import Persistence from "icons/Persistence.svelte"; import Persistence from "icons/Persistence.svelte";
import LoadingIndicator from "util/LoadingIndicator.svelte";
import SuccessMessage from "util/SuccessMessage.svelte"; import SuccessMessage from "util/SuccessMessage.svelte";
import { loading_finish, loading_start } from "lib/Loading";
let loading = false
let success_message let success_message
// Embedding settings // Embedding settings
let embed_domains = "" let embed_domains = ""
const save_embed = async () => { const save_embed = async () => {
loading = true
const form = new FormData() const form = new FormData()
form.append("embed_domains", embed_domains) form.append("embed_domains", embed_domains)
loading_start()
try { try {
const resp = await fetch( const resp = await fetch(
window.api_endpoint+"/user", window.api_endpoint+"/user",
@@ -30,7 +29,7 @@ const save_embed = async () => {
} catch(err) { } catch(err) {
success_message.set(false, err) success_message.set(false, err)
} finally { } finally {
loading = false loading_finish()
} }
} }
@@ -39,8 +38,6 @@ onMount(() => {
}) })
</script> </script>
<LoadingIndicator loading={loading}/>
<section> <section>
<h2><Persistence/>Embedding controls</h2> <h2><Persistence/>Embedding controls</h2>
<SuccessMessage bind:this={success_message}></SuccessMessage> <SuccessMessage bind:this={success_message}></SuccessMessage>

View File

@@ -1,20 +1,18 @@
<script> <script>
import Euro from "util/Euro.svelte" import Euro from "util/Euro.svelte"
import LoadingIndicator from "util/LoadingIndicator.svelte";
import SuccessMessage from "util/SuccessMessage.svelte"; import SuccessMessage from "util/SuccessMessage.svelte";
import PatreonActivationResult from "./PatreonActivationResult.svelte"; import PatreonActivationResult from "./PatreonActivationResult.svelte";
import { loading_finish, loading_start } from "lib/Loading";
let loading = false
let subscription = window.user.subscription.id let subscription = window.user.subscription.id
let subscription_type = window.user.subscription.type let subscription_type = window.user.subscription.type
let success_message let success_message
const update = async (plan) => { const update = async (plan) => {
loading = true
const form = new FormData() const form = new FormData()
form.append("subscription", plan) form.append("subscription", plan)
loading_start()
try { try {
{ {
const resp = await fetch( const resp = await fetch(
@@ -43,13 +41,11 @@ const update = async (plan) => {
} catch (err) { } catch (err) {
success_message.set(false, "Failed to update subscription: "+err) success_message.set(false, "Failed to update subscription: "+err)
} finally { } finally {
loading = false loading_finish()
} }
} }
</script> </script>
<LoadingIndicator loading={loading}/>
<section> <section>
{#if window.location.hash === "#checkout_complete"} {#if window.location.hash === "#checkout_complete"}
<div class="highlight_green"> <div class="highlight_green">

View File

@@ -2,9 +2,7 @@
import { onMount } from "svelte"; import { onMount } from "svelte";
import { formatDataVolume, formatDate } from "util/Formatting"; import { formatDataVolume, formatDate } from "util/Formatting";
import Euro from "util/Euro.svelte" import Euro from "util/Euro.svelte"
import LoadingIndicator from "util/LoadingIndicator.svelte"; import { loading_finish, loading_start } from "lib/Loading";
let loading = false
let year = 0 let year = 0
let month = 0 let month = 0
@@ -22,7 +20,7 @@ let transactions = {
} }
const load_transactions = async () => { const load_transactions = async () => {
loading = true loading_start()
month_str = year + "-" + ("00"+(month)).slice(-2) month_str = year + "-" + ("00"+(month)).slice(-2)
try { try {
const resp = await fetch(window.api_endpoint+"/user/transactions/"+month_str) const resp = await fetch(window.api_endpoint+"/user/transactions/"+month_str)
@@ -67,7 +65,7 @@ const load_transactions = async () => {
} catch (err) { } catch (err) {
alert(err) alert(err)
} finally { } finally {
loading = false loading_finish()
} }
}; };
const last_month = () => { const last_month = () => {
@@ -97,8 +95,6 @@ onMount(() => {
}) })
</script> </script>
<LoadingIndicator loading={loading}/>
<section> <section>
<h2>Transaction log</h2> <h2>Transaction log</h2>
<p> <p>

View File

@@ -1,10 +1,8 @@
<script> <script>
import { onMount } from "svelte"; import { onMount } from "svelte";
import { formatDate } from "util/Formatting"; import { formatDate } from "util/Formatting";
import LoadingIndicator from "util/LoadingIndicator.svelte";
import Button from "layout/Button.svelte"; import Button from "layout/Button.svelte";
import { loading_finish, loading_start } from "lib/Loading";
let loading = false
let year = 0 let year = 0
let month = 0 let month = 0
@@ -12,8 +10,8 @@ let month_str = ""
let data = [] let data = []
const load_activity = async () => { const load_activity = async () => {
loading = true
month_str = year + "-" + ("00"+(month)).slice(-2) month_str = year + "-" + ("00"+(month)).slice(-2)
loading_start()
try { try {
const resp = await fetch(window.api_endpoint+"/user/activity/" + month_str) const resp = await fetch(window.api_endpoint+"/user/activity/" + month_str)
if(resp.status >= 400) { if(resp.status >= 400) {
@@ -30,7 +28,7 @@ const load_activity = async () => {
} catch (err) { } catch (err) {
alert(err) alert(err)
} finally { } finally {
loading = false loading_finish()
} }
}; };
const last_month = () => { const last_month = () => {
@@ -60,8 +58,6 @@ onMount(() => {
}) })
</script> </script>
<LoadingIndicator loading={loading}/>
<div class="toolbar"> <div class="toolbar">
<Button click={last_month} icon="chevron_left"/> <Button click={last_month} icon="chevron_left"/>
<div class="toolbar_spacer"> <div class="toolbar_spacer">

View File

@@ -1,7 +1,7 @@
<script> <script>
import { onMount } from "svelte"; import { onMount } from "svelte";
import { FSNavigator } from "filesystem/FSNavigator.ts" import { FSNavigator } from "filesystem/FSNavigator.ts"
import { fs_encode_path, fs_node_icon, node_is_shared } from "filesystem/FilesystemAPI"; import { fs_encode_path, fs_node_icon, node_is_shared } from "lib/FilesystemAPI";
import Button from "layout/Button.svelte"; import Button from "layout/Button.svelte";
import CreateDirectory from "filesystem/filemanager/CreateDirectory.svelte"; import CreateDirectory from "filesystem/filemanager/CreateDirectory.svelte";
import FSUploadWidget from "filesystem/upload_widget/FSUploadWidget.svelte"; import FSUploadWidget from "filesystem/upload_widget/FSUploadWidget.svelte";

View File

@@ -2,10 +2,8 @@
import { onMount } from "svelte"; import { onMount } from "svelte";
import { formatDataVolume, formatDate } from "util/Formatting"; import { formatDataVolume, formatDate } from "util/Formatting";
import Euro from "util/Euro.svelte" import Euro from "util/Euro.svelte"
import LoadingIndicator from "util/LoadingIndicator.svelte";
import Button from "layout/Button.svelte"; import Button from "layout/Button.svelte";
import { loading_finish, loading_start } from "lib/Loading";
let loading = false
let year = 0 let year = 0
let month = 0 let month = 0
@@ -25,8 +23,8 @@ let transactions = {
} }
const load_transactions = async () => { const load_transactions = async () => {
loading = true
month_str = year + "-" + ("00"+(month)).slice(-2) month_str = year + "-" + ("00"+(month)).slice(-2)
loading_start()
try { try {
const resp = await fetch(window.api_endpoint+"/user/transactions/"+month_str) const resp = await fetch(window.api_endpoint+"/user/transactions/"+month_str)
if(resp.status >= 400) { if(resp.status >= 400) {
@@ -79,7 +77,7 @@ const load_transactions = async () => {
} catch (err) { } catch (err) {
alert(err) alert(err)
} finally { } finally {
loading = false loading_finish()
} }
}; };
const last_month = () => { const last_month = () => {
@@ -109,8 +107,6 @@ onMount(() => {
}) })
</script> </script>
<LoadingIndicator loading={loading}/>
<div class="toolbar"> <div class="toolbar">
<Button click={last_month} icon="chevron_left"/> <Button click={last_month} icon="chevron_left"/>
<div class="toolbar_spacer"> <div class="toolbar_spacer">

View File

@@ -1,21 +0,0 @@
<script>
import Spinner from "./Spinner.svelte";
export let loading = false
</script>
{#if loading}
<div class="container">
<Spinner/>
</div>
{/if}
<style>
.container {
position: fixed;
top: 10px;
right: 10px;
height: 120px;
width: 120px;
}
</style>

Some files were not shown because too many files have changed in this diff Show More