Compare commits

...

9 Commits

192 changed files with 4046 additions and 4592 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

@@ -17,7 +17,6 @@
</section> </section>
</div> </div>
{{template "page_bottom" .}} {{template "page_bottom" .}}
{{template "analytics"}}
</body> </body>
</html> </html>
{{end}} {{end}}

View File

@@ -21,7 +21,6 @@
</section> </section>
</div> </div>
{{template "page_bottom" .}} {{template "page_bottom" .}}
{{template "analytics"}}
</body> </body>
</html> </html>
{{end}} {{end}}

View File

@@ -18,7 +18,6 @@
</section> </section>
</div> </div>
{{template "page_bottom" .}} {{template "page_bottom" .}}
{{template "analytics"}}
</body> </body>
</html> </html>
{{end}} {{end}}

View File

@@ -27,7 +27,6 @@
</section> </section>
</div> </div>
{{template "page_bottom" .}} {{template "page_bottom" .}}
{{template "analytics"}}
</body> </body>
</html> </html>
{{end}} {{end}}

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,153 +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" .}}
{{template "analytics"}}
</body>
</html>
{{end}}

View File

@@ -140,7 +140,6 @@
</section> </section>
</div> </div>
{{template "page_bottom" .}} {{template "page_bottom" .}}
{{template "analytics"}}
</body> </body>
</html> </html>
{{ end }} {{ end }}

View File

@@ -45,7 +45,6 @@
{{end}} {{end}}
{{template "page_bottom" .}} {{template "page_bottom" .}}
{{template "analytics"}}
</body> </body>
</html> </html>
{{end}} {{end}}

View File

@@ -1 +0,0 @@
{{define "analytics"}}<script defer data-domain="pixeldrain.com" src="https://stats.pixeldrain.com/js/script.js"></script>{{end}}

View File

@@ -1,108 +0,0 @@
{{define "form"}}
{{.PreFormHTML}}
<form class="highlight_border" method="POST">
{{if eq .Submitted true}}
{{if eq .SubmitSuccess true}}
<div id="submit_result" class="highlight_green">
{{index .SubmitMessages 0}}
</div>
{{else}}
<div id="submit_result" class="highlight_red">
<ul>
{{range $msg := .SubmitMessages}}
<li>{{$msg}}</li>
{{end}}
</ul>
</div>
{{end}}
{{end}}
<input type="text" name="form" value="{{.Name}}" style="display: none;" readonly="readonly"/>
{{if ne .Username ""}}
<!-- The invisible username field is so browsers know which user the form was for -->
<input type="text" autocomplete="username" value="{{.Username}}" style="display: none;" readonly="readonly"/>
{{end}}
<div class="form">
{{range $field := .Fields}}
<label for="input_{{$field.Name}}">
{{$field.Label}}
</label>
{{if eq $field.Type "text"}}
<input id="input_{{$field.Name}}" name="{{$field.Name}}" value="{{$field.DefaultValue}}" type="text" class="form_input"/>
{{else if eq $field.Type "number"}}
<input id="input_{{$field.Name}}" name="{{$field.Name}}" value="{{$field.DefaultValue}}" type="number" class="form_input"/>
{{else if eq $field.Type "username"}}
<input id="input_{{$field.Name}}" name="{{$field.Name}}" value="{{$field.DefaultValue}}" type="text" autocomplete="username" class="form_input"/>
{{else if eq $field.Type "email"}}
<input id="input_{{$field.Name}}" name="{{$field.Name}}" value="{{$field.DefaultValue}}" type="email" autocomplete="email" class="form_input"/>
{{else if eq $field.Type "current-password"}}
<input id="input_{{$field.Name}}" name="{{$field.Name}}" value="{{$field.DefaultValue}}" type="password" autocomplete="current-password" class="form_input"/>
{{else if eq $field.Type "new-password"}}
<input id="input_{{$field.Name}}" name="{{$field.Name}}" value="{{$field.DefaultValue}}" type="password" autocomplete="new-password" class="form_input"/>
{{else if eq $field.Type "textarea"}}
<textarea id="input_{{$field.Name}}" name="{{$field.Name}}" class="form_input" style="width: 100%; height: 10em; resize: vertical;">{{$field.DefaultValue}}</textarea>
{{else if eq $field.Type "captcha"}}
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
<div class="g-recaptcha" data-theme="dark" data-sitekey="{{$field.CaptchaSiteKey}}"></div>
{{else if eq $field.Type "radio"}}
{{ range $val := $field.RadioValues}}
<input
id="input_{{$field.Name}}_choice_{{$val}}"
name="{{$field.Name}}"
value="{{$val}}"
type="radio"
{{if eq $val $field.DefaultValue}}checked="checked"{{end}}/>
<label for="input_{{$field.Name}}_choice_{{$val}}">{{$val}}</label><br/>
{{ end }}
{{else if eq $field.Type "description"}}
{{$field.DefaultValue}}
{{end}}
{{if ne $field.Description ""}}
<div>
{{$field.Description}}
</div>
{{end}}
{{end}}
{{if eq .SubmitRed true}}
<button type="submit" class="button_red">
<i class="icon">send</i>
{{.SubmitLabel}}
</button>
{{else}}
<button type="submit" class="button_highlight">
<i class="icon">send</i>
{{.SubmitLabel}}
</button>
{{end}}
</div>
</form>
{{.PostFormHTML}}
{{end}}
{{define "form_page"}}
<!DOCTYPE html>
<html lang="en">
<head>
{{template "meta_tags" .Title}}
<script>var apiEndpoint = '{{.APIEndpoint}}';</script>
</head>
<body>
<div id='body' class="body">
{{template "page_top" .}}
<header>
<h1>{{.Form.Title}}</h1>
</header>
<div id="page_content" class="page_content">
<br/>
<section>
{{template "form" .Form}}
<br/>
</section>
</div>
{{template "page_bottom" .}}
</div>
{{template "analytics"}}
</body>
</html>
{{end}}

View File

@@ -15,7 +15,6 @@
</section> </section>
</div> </div>
{{template "page_bottom" .}} {{template "page_bottom" .}}
{{template "analytics"}}
</body> </body>
</html> </html>
{{end}} {{end}}

View File

@@ -1,20 +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>
{{template "analytics"}}
</body>
</html>
{{end}}

View File

@@ -1,20 +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>
{{template "analytics"}}
</body>
</html>
{{end}}

View File

@@ -1,20 +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>
{{template "analytics"}}
</body>
</html>
{{end}}

View File

@@ -19,7 +19,6 @@
></iframe> ></iframe>
</div> </div>
{{template "page_bottom" .}} {{template "page_bottom" .}}
{{template "analytics"}}
</body> </body>
</html> </html>
{{ end }} {{ end }}

View File

@@ -1,20 +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>
{{template "analytics"}}
</body>
</html>
{{end}}

View File

@@ -35,7 +35,6 @@
</section> </section>
</div> </div>
{{template "page_bottom" .}} {{template "page_bottom" .}}
{{template "analytics"}}
</body> </body>
</html> </html>
{{end}} {{end}}

View File

@@ -1,4 +1,4 @@
{{define "filesystem"}} {{define "wrap"}}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
@@ -23,13 +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>
{{template "analytics"}}
</head> </head>
<body></body> <body></body>
</html> </html>

1921
svelte/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -18,7 +18,7 @@
"rollup": "^4.24.4", "rollup": "^4.24.4",
"rollup-plugin-livereload": "^2.0.5", "rollup-plugin-livereload": "^2.0.5",
"rollup-plugin-svelte": "^7.2.2", "rollup-plugin-svelte": "^7.2.2",
"svelte": "^4.2.19" "svelte": "^5.0.0"
}, },
"dependencies": { "dependencies": {
"behave-js": "^1.5.0", "behave-js": "^1.5.0",

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,23 +1,26 @@
<script> <script lang="ts">
import { formatDate, formatNumber } from "util/Formatting"; import { formatDate, formatNumber } from "util/Formatting";
import Expandable from "util/Expandable.svelte"; import Expandable from "util/Expandable.svelte";
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
let dispatch = createEventDispatcher() let dispatch = createEventDispatcher()
export let report let {
export let ip_report_count report,
let preview = false ip_report_count
} = $props();
let preview = $state(false)
$: can_grant = report.status !== "granted" let can_grant = $derived(report.status !== "granted")
$: can_reject = report.status !== "rejected" let can_reject = $derived(report.status !== "rejected")
let set_status = async (action, report_type) => { let set_status = async (action: string, report_type: string) => {
dispatch("resolve_report", {action: action, report_type: report_type}) dispatch("resolve_report", {action: action, report_type: report_type})
} }
</script> </script>
<Expandable expanded={report.status === "pending"} click_expand> <Expandable expanded={report.status === "pending"} click_expand>
<div slot="header" class="header"> {#snippet header()}
<div class="header">
<div class="icon_cell"> <div class="icon_cell">
<img class="file_icon" src={"/api/file/"+report.file.id+"/thumbnail"} alt="File thumbnail"/> <img class="file_icon" src={"/api/file/"+report.file.id+"/thumbnail"} alt="File thumbnail"/>
</div> </div>
@@ -39,35 +42,36 @@ let set_status = async (action, report_type) => {
<div class="stats">V<br/>{formatNumber(report.file.views, 3)}</div> <div class="stats">V<br/>{formatNumber(report.file.views, 3)}</div>
<div class="stats">DL<br/>{formatNumber(report.file.bandwidth_used / report.file.size, 3)}</div> <div class="stats">DL<br/>{formatNumber(report.file.bandwidth_used / report.file.size, 3)}</div>
</div> </div>
{/snippet}
<div class="details"> <div class="details">
<div class="toolbar"> <div class="toolbar">
<div class="action_list"> <div class="action_list">
<a class="button" target="_blank" href={"/u/"+report.file.id} rel="noreferrer"> <a class="button" target="_blank" href={"/u/"+report.file.id} rel="noreferrer">
<i class="icon">open_in_new</i> Open file <i class="icon">open_in_new</i> Open file
</a> </a>
<button class:button_highlight={preview} on:click={() => {preview = !preview}}> <button class:button_highlight={preview} onclick={() => {preview = !preview}}>
<i class="icon">visibility</i> Preview <i class="icon">visibility</i> Preview
</button> </button>
{#if can_grant} {#if can_grant}
<button class="button_highlight" on:click={() => {set_status("grant", report.type)}}> <button class="button_highlight" onclick={() => {set_status("grant", report.type)}}>
<i class="icon">done</i> Block ({report.type}) <i class="icon">done</i> Block ({report.type})
</button> </button>
{/if} {/if}
{#if can_reject} {#if can_reject}
<button class="button_red" on:click={() => {set_status("reject", "")}}> <button class="button_red" onclick={() => {set_status("reject", "")}}>
<i class="icon">delete</i> Ignore <i class="icon">delete</i> Ignore
</button> </button>
{/if} {/if}
</div> </div>
<div class="type_list"> <div class="type_list">
<button on:click={() => {set_status("grant", "copyright")}}>copyright</button> <button onclick={() => {set_status("grant", "copyright")}}>copyright</button>
<button on:click={() => {set_status("grant", "terrorism")}}>terrorism</button> <button onclick={() => {set_status("grant", "terrorism")}}>terrorism</button>
<button on:click={() => {set_status("grant", "gore")}}>gore</button> <button onclick={() => {set_status("grant", "gore")}}>gore</button>
<button on:click={() => {set_status("grant", "child_abuse")}}>child_abuse</button> <button onclick={() => {set_status("grant", "child_abuse")}}>child_abuse</button>
<button on:click={() => {set_status("grant", "zoophilia")}}>zoophilia</button> <button onclick={() => {set_status("grant", "zoophilia")}}>zoophilia</button>
<button on:click={() => {set_status("grant", "malware")}}>malware</button> <button onclick={() => {set_status("grant", "malware")}}>malware</button>
<button on:click={() => {set_status("grant", "doxing")}}>doxing</button> <button onclick={() => {set_status("grant", "doxing")}}>doxing</button>
<button on:click={() => {set_status("grant", "revenge_porn")}}>revenge_porn</button> <button onclick={() => {set_status("grant", "revenge_porn")}}>revenge_porn</button>
</div> </div>
</div> </div>
<div style="text-align: center;"> <div style="text-align: center;">
@@ -101,12 +105,12 @@ let set_status = async (action, report_type) => {
<td>{ip_report_count[user_report.ip_address]}</td> <td>{ip_report_count[user_report.ip_address]}</td>
<td> <td>
{#if can_grant} {#if can_grant}
<button on:click={() => dispatch("resolve_by_ip", {ip: user_report.ip_address, action: "grant"})}> <button onclick={() => dispatch("resolve_by_ip", {ip: user_report.ip_address, action: "grant"})}>
Accept all Accept all
</button> </button>
{/if} {/if}
{#if can_reject} {#if can_reject}
<button on:click={() => dispatch("resolve_by_ip", {ip: user_report.ip_address, action: "reject"})}> <button onclick={() => dispatch("resolve_by_ip", {ip: user_report.ip_address, action: "reject"})}>
Ignore all Ignore all
</button> </button>
{/if} {/if}

View File

@@ -1,18 +1,18 @@
<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 = $state([])
let startPicker let startPicker = $state()
let endPicker let endPicker = $state()
let tab = "pending" let tab = $state("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,11 +70,11 @@ const get_reports = async () => {
} catch (err) { } catch (err) {
alert(err); alert(err);
} finally { } finally {
loading = false; loading_finish()
} }
}; };
let ip_report_count = {} let ip_report_count = $state({})
const count_ip_reports = () => { const count_ip_reports = () => {
ip_report_count = {} ip_report_count = {}
reports.forEach(v => { reports.forEach(v => {
@@ -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>
@@ -165,19 +163,19 @@ onMount(() => {
<div>Range:</div> <div>Range:</div>
<input type="date" bind:this={startPicker}/> <input type="date" bind:this={startPicker}/>
<input type="date" bind:this={endPicker}/> <input type="date" bind:this={endPicker}/>
<button on:click={get_reports}>Go</button> <button onclick={get_reports}>Go</button>
</div> </div>
<div class="tab_bar"> <div class="tab_bar">
<button on:click={() => {tab = "pending"; get_reports()}} class:button_highlight={tab === "pending"}> <button onclick={() => {tab = "pending"; get_reports()}} class:button_highlight={tab === "pending"}>
<i class="icon">flag</i> <i class="icon">flag</i>
Pending Pending
</button> </button>
<button on:click={() => {tab = "granted"; get_reports()}} class:button_highlight={tab === "granted"}> <button onclick={() => {tab = "granted"; get_reports()}} class:button_highlight={tab === "granted"}>
<i class="icon">flag</i> <i class="icon">flag</i>
Granted Granted
</button> </button>
<button on:click={() => {tab = "rejected"; get_reports()}} class:button_highlight={tab === "rejected"}> <button onclick={() => {tab = "rejected"; get_reports()}} class:button_highlight={tab === "rejected"}>
<i class="icon">flag</i> <i class="icon">flag</i>
Rejected Rejected
</button> </button>

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,29 +17,28 @@ type Reporter = {
last_message_html: string, last_message_html: string,
} }
let loading = true let reporters: Reporter[] = $state([])
let reporters: Reporter[] = [] let reporters_pending = $derived(reporters.reduce((acc, val) => {
$: reporters_pending = reporters.reduce((acc, val) => {
if (val.status === "pending") { if (val.status === "pending") {
acc.push(val) acc.push(val)
} }
return acc return acc
}, []) }, []))
$: reporters_trusted = reporters.reduce((acc, val) => { let reporters_trusted = $derived(reporters.reduce((acc, val) => {
if (val.status === "trusted") { if (val.status === "trusted") {
acc.push(val) acc.push(val)
} }
return acc return acc
}, []) }, []))
$: reporters_rejected = reporters.reduce((acc, val) => { let reporters_rejected = $derived(reporters.reduce((acc, val) => {
if (val.status === "rejected") { if (val.status === "rejected") {
acc.push(val) acc.push(val)
} }
return acc return acc
}, []) }, []))
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,17 +48,18 @@ const get_reporters = async () => {
} catch (err) { } catch (err) {
alert(err); alert(err);
} finally { } finally {
loading = false; loading_finish()
} }
}; };
let edit_button: HTMLButtonElement let edit_button: HTMLButtonElement = $state()
let creating = false let creating = $state(false)
let new_reporter_from_address: HTMLInputElement let new_reporter_from_address: HTMLInputElement = $state()
let new_reporter_name: HTMLInputElement let new_reporter_name: HTMLInputElement = $state()
let new_reporter_status = "trusted" let new_reporter_status = $state("trusted")
const create_reporter = async () => { const create_reporter = async (e: SubmitEvent) => {
e.preventDefault()
if (!new_reporter_from_address.value) { if (!new_reporter_from_address.value) {
alert("Please enter an e-mail address") alert("Please enter an e-mail address")
return return
@@ -137,21 +137,19 @@ 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>
<button on:click={() => get_reporters()}> <button onclick={() => get_reporters()}>
<i class="icon">refresh</i> <i class="icon">refresh</i>
</button> </button>
<button bind:this={edit_button} class:button_highlight={creating} on:click={() => {creating = !creating}}> <button bind:this={edit_button} class:button_highlight={creating} onclick={() => {creating = !creating}}>
<i class="icon">create</i> Add abuse reporter <i class="icon">create</i> Add abuse reporter
</button> </button>
</div> </div>
{#if creating} {#if creating}
<div class="highlight_shaded"> <div class="highlight_shaded">
<form on:submit|preventDefault={create_reporter}> <form onsubmit={create_reporter}>
<div class="form"> <div class="form">
<label for="field_from_address">E-mail address</label> <label for="field_from_address">E-mail address</label>
<input id="field_from_address" type="text" bind:this={new_reporter_from_address}/> <input id="field_from_address" type="text" bind:this={new_reporter_from_address}/>

View File

@@ -1,4 +1,5 @@
<script> <script lang="ts">
import { run, preventDefault } from 'svelte/legacy';
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import { formatDate } from "util/Formatting"; import { formatDate } from "util/Formatting";
import Modal from "util/Modal.svelte" import Modal from "util/Modal.svelte"
@@ -7,13 +8,12 @@ import { flip } from "svelte/animate";
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
export let reporters = [] let { reporters = $bindable([]) } = $props();
$: update_table(reporters)
const update_table = (reporters) => sort("") const update_table = (reporters) => sort("")
let sort_field = "last_used" let sort_field = $state("last_used")
let asc = false let asc = $state(false)
const sort = (field) => { const sort = (field) => {
if (field !== "" && field === sort_field) asc = !asc if (field !== "" && field === sort_field) asc = !asc
if (field === "") field = sort_field if (field === "") field = sort_field
@@ -40,16 +40,19 @@ const sort = (field) => {
reporters = reporters reporters = reporters
} }
let modal let modal: Modal = $state()
let preview_subject = "" let preview_subject = $state("")
let preview_html = "" let preview_html = $state("")
let preview_text = "" let preview_text = $state("")
const toggle_preview = (rep) => { const toggle_preview = (rep) => {
preview_subject = rep.last_message_subject preview_subject = rep.last_message_subject
preview_text = rep.last_message_text preview_text = rep.last_message_text
preview_html = rep.last_message_html preview_html = rep.last_message_html
modal.show() modal.show()
} }
run(() => {
update_table(reporters)
});
</script> </script>
<div class="table_scroll"> <div class="table_scroll">
@@ -75,23 +78,23 @@ const toggle_preview = (rep) => {
<td>{formatDate(rep.last_used, true, true, false)}</td> <td>{formatDate(rep.last_used, true, true, false)}</td>
<td>{formatDate(rep.created, false, false, false)}</td> <td>{formatDate(rep.created, false, false, false)}</td>
<td> <td>
<button on:click|preventDefault={() => toggle_preview(rep)} class="button round"> <button onclick={preventDefault(() => toggle_preview(rep))} class="button round">
<i class="icon">email</i> <i class="icon">email</i>
</button> </button>
<button on:click|preventDefault={() => {dispatch("edit", rep)}} class="button round"> <button onclick={preventDefault(() => {dispatch("edit", rep)})} class="button round">
<i class="icon">edit</i> <i class="icon">edit</i>
</button> </button>
{#if rep.status !== "trusted"} {#if rep.status !== "trusted"}
<button on:click|preventDefault={() => {dispatch("approve", rep)}} class="button button_highlight round"> <button onclick={preventDefault(() => {dispatch("approve", rep)})} class="button button_highlight round">
<i class="icon">check</i> <i class="icon">check</i>
</button> </button>
{/if} {/if}
{#if rep.status !== "rejected"} {#if rep.status !== "rejected"}
<button on:click|preventDefault={() => {dispatch("spam", rep)}} class="button button_red round"> <button onclick={preventDefault(() => {dispatch("spam", rep)})} class="button button_red round">
<i class="icon">block</i> <i class="icon">block</i>
</button> </button>
{/if} {/if}
<button on:click|preventDefault={() => {dispatch("delete", rep)}} class="button button_red round"> <button onclick={preventDefault(() => {dispatch("delete", rep)})} class="button button_red round">
<i class="icon">delete</i> <i class="icon">delete</i>
</button> </button>
</td> </td>

View File

@@ -2,20 +2,20 @@
import { onDestroy, onMount } from "svelte"; import { onDestroy, onMount } from "svelte";
import { formatDataVolume, formatThousands, formatDate, formatNumber, formatDuration } from "util/Formatting"; import { formatDataVolume, formatThousands, formatDate, formatNumber, formatDuration } from "util/Formatting";
import Chart from "util/Chart.svelte"; import Chart from "util/Chart.svelte";
import { color_by_name } from "util/Util.svelte"; import { color_by_name } from "util/Util";
import ServerDiagnostics from "./ServerDiagnostics.svelte"; import ServerDiagnostics from "./ServerDiagnostics.svelte";
import PeerTable from "./PeerTable.svelte"; import PeerTable from "./PeerTable.svelte";
let graphViews let graphViews = $state()
let graphBandwidth let graphBandwidth = $state()
let graphTimeout = null let graphTimeout = null
let start_time = "" let start_time = $state("")
let end_time = "" let end_time = $state("")
let total_bandwidth = 0 let total_bandwidth = $state(0)
let total_bandwidth_paid = 0 let total_bandwidth_paid = $state(0)
let total_views = 0 let total_views = $state(0)
let total_downloads = 0 let total_downloads = $state(0)
const loadGraph = (minutes, interval, live) => { const loadGraph = (minutes, interval, live) => {
if (graphTimeout !== null) { clearTimeout(graphTimeout) } if (graphTimeout !== null) { clearTimeout(graphTimeout) }
if (live) { if (live) {
@@ -64,8 +64,8 @@ const loadGraph = (minutes, interval, live) => {
// Load performance statistics // Load performance statistics
let lastOrder; let lastOrder = $state();
let status = { let status = $state({
cpu_profile_running_since: "", cpu_profile_running_since: "",
db_latency: 0, db_latency: 0,
db_time: "", db_time: "",
@@ -93,9 +93,9 @@ let status = {
rate_limit_watcher_listeners: 0, rate_limit_watcher_listeners: 0,
download_clients: 0, download_clients: 0,
download_connections: 0, download_connections: 0,
} })
$: total_reads = status.local_reads + status.neighbour_reads + status.remote_reads let total_reads = $derived(status.local_reads + status.neighbour_reads + status.remote_reads)
$: total_read_size = status.local_read_size + status.neighbour_read_size + status.remote_read_size let total_read_size = $derived(status.local_read_size + status.neighbour_read_size + status.remote_read_size)
function getStats(order) { function getStats(order) {
lastOrder = order lastOrder = order
@@ -179,14 +179,14 @@ onDestroy(() => {
<h3>Bandwidth usage and file views</h3> <h3>Bandwidth usage and file views</h3>
</section> </section>
<div class="highlight_border" style="margin-bottom: 6px;"> <div class="highlight_border" style="margin-bottom: 6px;">
<button on:click={() => loadGraph(1440, 1, true)}>Day 1m</button> <button onclick={() => loadGraph(1440, 1, true)}>Day 1m</button>
<button on:click={() => loadGraph(10080, 10, true)}>Week 10m</button> <button onclick={() => loadGraph(10080, 10, true)}>Week 10m</button>
<button on:click={() => loadGraph(43200, 60, true)}>Month 1h</button> <button onclick={() => loadGraph(43200, 60, true)}>Month 1h</button>
<button on:click={() => loadGraph(131400, 1440, false)}>Quarter 1d</button> <button onclick={() => loadGraph(131400, 1440, false)}>Quarter 1d</button>
<button on:click={() => loadGraph(262800, 1440, false)}>Half-year 1d</button> <button onclick={() => loadGraph(262800, 1440, false)}>Half-year 1d</button>
<button on:click={() => loadGraph(525600, 1440, false)}>Year 1d</button> <button onclick={() => loadGraph(525600, 1440, false)}>Year 1d</button>
<button on:click={() => loadGraph(1051200, 1440, false)}>Two Years 1d</button> <button onclick={() => loadGraph(1051200, 1440, false)}>Two Years 1d</button>
<button on:click={() => loadGraph(2628000, 1440, false)}>Five Years 1d</button> <button onclick={() => loadGraph(2628000, 1440, false)}>Five Years 1d</button>
</div> </div>
<Chart bind:this={graphBandwidth} data_type="bytes" /> <Chart bind:this={graphBandwidth} data_type="bytes" />
<Chart bind:this={graphViews} data_type="number" /> <Chart bind:this={graphViews} data_type="number" />
@@ -199,7 +199,7 @@ onDestroy(() => {
</div> </div>
<br/> <br/>
<ServerDiagnostics running_since={status.cpu_profile_running_since} on:refresh={() => getStats(lastOrder)}/> <ServerDiagnostics running_since={status.cpu_profile_running_since} refresh={() => getStats(lastOrder)}/>
<section> <section>
<h3>Process stats</h3> <h3>Process stats</h3>
@@ -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>
@@ -318,22 +306,22 @@ onDestroy(() => {
<thead> <thead>
<tr> <tr>
<td> <td>
<button on:click={() => { getStats('query_name') }}> <button onclick={() => { getStats('query_name') }}>
Query Query
</button> </button>
</td> </td>
<td> <td>
<button style="cursor: pointer;" on:click={() => { getStats('calls') }}> <button style="cursor: pointer;" onclick={() => { getStats('calls') }}>
Calls Calls
</button> </button>
</td> </td>
<td> <td>
<button style="cursor: pointer;" on:click={() => { getStats('average_duration') }}> <button style="cursor: pointer;" onclick={() => { getStats('average_duration') }}>
Avg Avg
</button> </button>
</td> </td>
<td> <td>
<button style="cursor: pointer;" on:click={() => { getStats('total_duration') }}> <button style="cursor: pointer;" onclick={() => { getStats('total_duration') }}>
Total Total
</button> </button>
</td> </td>

View File

@@ -1,8 +1,9 @@
<script> <script>
import { preventDefault, stopPropagation } from 'svelte/legacy';
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,17 +16,16 @@ const abuse_types = [
"revenge_porn", "revenge_porn",
] ]
let loading = true let rows = $state([])
let rows = [] let total_offences = $state(0)
let total_offences = 0
let expanded = false let expanded = $state(false)
let creating = false let creating = $state(false)
let new_ban_address let new_ban_address = $state()
let new_ban_reason = abuse_types[0] let new_ban_reason = $state(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 +39,7 @@ const get_bans = async () => {
} catch (err) { } catch (err) {
alert(err); alert(err);
} finally { } finally {
loading = false; loading_finish();
} }
}; };
@@ -95,8 +95,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">
@@ -106,20 +104,20 @@ onMount(get_bans);
Offences {total_offences} Offences {total_offences}
</div> </div>
<div class="toolbar_spacer"></div> <div class="toolbar_spacer"></div>
<button class:button_highlight={expanded} on:click={() => {expanded = !expanded}}> <button class:button_highlight={expanded} onclick={() => {expanded = !expanded}}>
{#if expanded} {#if expanded}
<i class="icon">unfold_less</i> Collapse all <i class="icon">unfold_less</i> Collapse all
{:else} {:else}
<i class="icon">unfold_more</i> Expand all <i class="icon">unfold_more</i> Expand all
{/if} {/if}
</button> </button>
<button class:button_highlight={creating} on:click={() => {creating = !creating}}> <button class:button_highlight={creating} onclick={() => {creating = !creating}}>
<i class="icon">create</i> Add IP ban <i class="icon">create</i> Add IP ban
</button> </button>
</div> </div>
{#if creating} {#if creating}
<div class="highlight_shaded"> <div class="highlight_shaded">
<form on:submit|preventDefault={create_ban}> <form onsubmit={preventDefault(create_ban)}>
<div class="form"> <div class="form">
<label for="field_address">IP address</label> <label for="field_address">IP address</label>
<input id="field_address" type="text" bind:this={new_ban_address}/> <input id="field_address" type="text" bind:this={new_ban_address}/>
@@ -141,7 +139,8 @@ onMount(get_bans);
{#each rows as row (row.address)} {#each rows as row (row.address)}
<Expandable expanded={expanded} click_expand> <Expandable expanded={expanded} click_expand>
<div slot="header" class="header"> {#snippet header()}
<div class="header">
<div class="title">{row.address}</div> <div class="title">{row.address}</div>
<div class="stats"> <div class="stats">
Offences<br/> Offences<br/>
@@ -151,10 +150,11 @@ onMount(get_bans);
Date<br/> Date<br/>
{formatDate(row.offences[0].ban_time, false, false, false)} {formatDate(row.offences[0].ban_time, false, false, false)}
</div> </div>
<button on:click|stopPropagation={() => {delete_ban(row.address)}} class="button button_red" style="align-self: center;"> <button onclick={stopPropagation(() => {delete_ban(row.address)})} class="button button_red" style="align-self: center;">
<i class="icon">delete</i> <i class="icon">delete</i>
</button> </button>
</div> </div>
{/snippet}
<div class="table_scroll"> <div class="table_scroll">
<table> <table>
<thead> <thead>

View File

@@ -1,17 +1,16 @@
<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[] = $state([])
let invoices: Invoice[] = []
let year = 0 let year = $state(0)
let month = 0 let month = $state(0)
type Total = { type Total = {
count: number count: number
@@ -19,8 +18,8 @@ type Total = {
vat: number vat: number
fee: number fee: number
} }
let totals_provider: { [id: string]: Total } = {} let totals_provider: { [id: string]: Total } = $state({})
let totals_country: { [id: string]: Total } = {} let totals_country: { [id: string]: Total } = $state({})
const add_total = (i: Invoice) => { const add_total = (i: Invoice) => {
if (totals_provider[i.payment_method] === undefined) { if (totals_provider[i.payment_method] === undefined) {
totals_provider[i.payment_method] = {count: 0, amount: 0, vat: 0, fee: 0} totals_provider[i.payment_method] = {count: 0, amount: 0, vat: 0, fee: 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()
} }
}; };
@@ -120,14 +119,14 @@ onMount(() => {
get_invoices() get_invoices()
}) })
let status_filter = { let status_filter = $state({
canceled: {checked: false}, canceled: {checked: false},
expired: {checked: false}, expired: {checked: false},
open: {checked: false}, open: {checked: false},
paid: {checked: true}, paid: {checked: true},
} })
let gateway_filter = {} let gateway_filter = $state({})
let method_filter = {} let method_filter = $state({})
const filter_invoices = () => { const filter_invoices = () => {
records_hidden = 0 records_hidden = 0
@@ -154,28 +153,28 @@ const filter_invoices = () => {
return false return false
}) })
} }
let records_hidden = 0 let records_hidden = $state(0)
let invoices_filtered: Invoice[] = [] let invoices_filtered: Invoice[] = $state([])
</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">
<button on:click={last_month}> <button onclick={last_month}>
<i class="icon">chevron_left</i> <i class="icon">chevron_left</i>
Previous month Previous month
</button> </button>
<div class="toolbar_spacer"></div> <div class="toolbar_spacer"></div>
<button on:click={next_month}> <button onclick={next_month}>
Next month Next month
<i class="icon">chevron_right</i> <i class="icon">chevron_right</i>
</button> </button>
</div> </div>
<Expandable click_expand> <Expandable click_expand>
<div slot="header" class="header">Per payment processor</div> {#snippet header()}
<div class="header">Per payment processor</div>
{/snippet}
<SortableTable <SortableTable
index_field="id" index_field="id"
rows={obj_to_list(totals_provider)} rows={obj_to_list(totals_provider)}
@@ -191,7 +190,9 @@ let invoices_filtered: Invoice[] = []
</Expandable> </Expandable>
<Expandable click_expand> <Expandable click_expand>
<div slot="header" class="header">Per country</div> {#snippet header()}
<div class="header">Per country</div>
{/snippet}
<SortableTable <SortableTable
index_field="id" index_field="id"
rows={obj_to_list(totals_country)} rows={obj_to_list(totals_country)}
@@ -207,7 +208,9 @@ let invoices_filtered: Invoice[] = []
</Expandable> </Expandable>
<Expandable click_expand> <Expandable click_expand>
<div slot="header" class="header">In European Union</div> {#snippet header()}
<div class="header">In European Union</div>
{/snippet}
<SortableTable <SortableTable
index_field="id" index_field="id"
rows={obj_to_list_eu(totals_country)} rows={obj_to_list_eu(totals_country)}
@@ -223,7 +226,9 @@ let invoices_filtered: Invoice[] = []
</Expandable> </Expandable>
<Expandable click_expand> <Expandable click_expand>
<div slot="header" class="header">PayPal VAT</div> {#snippet header()}
<div class="header">PayPal VAT</div>
{/snippet}
<PayPalVat invoices={invoices}/> <PayPalVat invoices={invoices}/>
</Expandable> </Expandable>
@@ -236,7 +241,7 @@ let invoices_filtered: Invoice[] = []
type="checkbox" type="checkbox"
id="status_{filter}" id="status_{filter}"
bind:checked={status_filter[filter].checked} bind:checked={status_filter[filter].checked}
on:change={filter_invoices}> onchange={filter_invoices}>
<label for="status_{filter}">{filter}</label> <label for="status_{filter}">{filter}</label>
<br/> <br/>
{/each} {/each}
@@ -248,7 +253,7 @@ let invoices_filtered: Invoice[] = []
type="checkbox" type="checkbox"
id="gateway_{filter}" id="gateway_{filter}"
bind:checked={gateway_filter[filter].checked} bind:checked={gateway_filter[filter].checked}
on:change={filter_invoices}> onchange={filter_invoices}>
<label for="gateway_{filter}">{filter}</label> <label for="gateway_{filter}">{filter}</label>
<br/> <br/>
{/each} {/each}
@@ -260,7 +265,7 @@ let invoices_filtered: Invoice[] = []
type="checkbox" type="checkbox"
id="method_{filter}" id="method_{filter}"
bind:checked={method_filter[filter].checked} bind:checked={method_filter[filter].checked}
on:change={filter_invoices}> onchange={filter_invoices}>
<label for="method_{filter}">{filter}</label> <label for="method_{filter}">{filter}</label>
<br/> <br/>
{/each} {/each}

View File

@@ -1,20 +1,19 @@
<script> <script lang="ts">
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_finish, loading_start } from "lib/Loading";
export let settlement = {} let { settlement = {} } = $props();
let loading = true let payments = $state([])
let payments = []
let per_country = {} let per_country = $state({})
let totals = { let totals = $state({
count: 0, count: 0,
vat: 0, vat: 0,
amount: 0, amount: 0,
} })
const load_all_payments = async (settlement_id) => { const load_all_payments = async (settlement_id) => {
let payments = [] let payments = []
@@ -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 = $state([])
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,19 +22,18 @@ 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>
<div slot="header" class="header"> {#snippet header()}
<div class="header">
<div class="title">{row.id}</div> <div class="title">{row.id}</div>
<div class="stats"> <div class="stats">
Date<br/> Date<br/>
@@ -50,6 +48,7 @@ onMount(get_settlements);
{row.status} {row.status}
</div> </div>
</div> </div>
{/snippet}
<MollieSettlement settlement={row}/> <MollieSettlement settlement={row}/>
</Expandable> </Expandable>

View File

@@ -2,20 +2,19 @@
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 = $state([])
let per_country = {} let per_country = $state({})
let totals = {} let totals = $state({})
let datePicker let datePicker = $state()
let rangeMonths = 1 let rangeMonths = $state(1)
let startDate = 0 let startDate = $state(0)
let endDate = 0 let endDate = $state(0)
const get_payments = async () => { const get_payments = async () => {
if (!datePicker.valueAsDate) { if (!datePicker.valueAsDate) {
@@ -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>
@@ -128,7 +125,7 @@ onMount(() => {
<input type="date" bind:this={datePicker}/> <input type="date" bind:this={datePicker}/>
<div>Months</div> <div>Months</div>
<input type="number" bind:value={rangeMonths}/> <input type="number" bind:value={rangeMonths}/>
<button on:click={get_payments}>Go</button> <button onclick={get_payments}>Go</button>
</div> </div>
<div> <div>
@@ -230,7 +227,8 @@ onMount(() => {
<h2>Payments</h2> <h2>Payments</h2>
{#each payments as row (row.id)} {#each payments as row (row.id)}
<Expandable click_expand> <Expandable click_expand>
<div slot="header" class="header"> {#snippet header()}
<div class="header">
<div class="title">{row.id}</div> <div class="title">{row.id}</div>
<div class="stats"> <div class="stats">
Date<br/> Date<br/>
@@ -253,6 +251,7 @@ onMount(() => {
{row.status} {row.status}
</div> </div>
</div> </div>
{/snippet}
<div> <div>
Amount: <Euro amount={row.metadata.amount} /><br/> Amount: <Euro amount={row.metadata.amount} /><br/>
VAT: <Euro amount={row.metadata.vat} /><br/> VAT: <Euro amount={row.metadata.vat} /><br/>

View File

@@ -78,6 +78,7 @@ const update_countries = (invoices: Invoice[]) => {
{#if per_country["NL"] && totals} {#if per_country["NL"] && totals}
<h2>Summary</h2> <h2>Summary</h2>
<table style="width: auto;"> <table style="width: auto;">
<tbody>
<tr> <tr>
<td>Total PayPal earnings -fees</td> <td>Total PayPal earnings -fees</td>
<td><Euro amount={totals.vat+totals.amount-totals.fee}/></td> <td><Euro amount={totals.vat+totals.amount-totals.fee}/></td>
@@ -90,6 +91,7 @@ const update_countries = (invoices: Invoice[]) => {
<td>Total VAT OSS</td> <td>Total VAT OSS</td>
<td><Euro amount={totals.vat-per_country["NL"].vat}/></td> <td><Euro amount={totals.vat-per_country["NL"].vat}/></td>
</tr> </tr>
</tbody>
</table> </table>
<h2>Accounting information</h2> <h2>Accounting information</h2>

View File

@@ -1,25 +1,22 @@
<script> <script lang="ts">
import { run } from 'svelte/legacy';
import { flip } from "svelte/animate"; import { flip } from "svelte/animate";
import { formatDataVolume } from "util/Formatting"; import { formatDataVolume } from "util/Formatting";
import SortButton from "layout/SortButton.svelte"; import SortButton from "layout/SortButton.svelte";
export let peers = []; let { peers = $bindable([]) } = $props();
$: update_peers(peers)
let update_peers = (peers) => { let update_peers = (peers) => {
for (let peer of peers) { for (let peer of peers) {
peer.avg_network_total = peer.avg_network_tx + peer.avg_network_rx peer.avg_network_total = peer.avg_network_tx + peer.avg_network_rx
peer.usage_percent = (peer.avg_network_tx / peer.port_speed) * 100 peer.usage_percent = (peer.avg_network_tx / peer.port_speed) * 100
peer.network_ratio = Math.max(peer.avg_network_tx, peer.avg_network_rx) / Math.min(peer.avg_network_tx, peer.avg_network_rx) peer.network_ratio = Math.max(peer.avg_network_tx, peer.avg_network_rx) / Math.min(peer.avg_network_tx, peer.avg_network_rx)
if (peer.network_ratio === NaN) {
peer.network_ratio = 1
}
} }
sort("") sort("")
} }
let sort_field = "hostname" let sort_field = $state("hostname")
let asc = true let asc = $state(true)
let sort = (field) => { let sort = (field) => {
if (field !== "" && field === sort_field) { if (field !== "" && field === sort_field) {
asc = !asc asc = !asc
@@ -49,6 +46,9 @@ let sort = (field) => {
}) })
peers = peers peers = peers
} }
run(() => {
update_peers(peers)
});
</script> </script>
<div class="table_scroll"> <div class="table_scroll">

View File

@@ -1,35 +1,43 @@
<script> <script lang="ts">
import { createEventDispatcher, onMount } from "svelte"; import { get_endpoint } from "lib/PixeldrainAPI";
import { onMount } from "svelte";
import { formatDuration } from "util/Formatting"; import { formatDuration } from "util/Formatting";
let dispatch = createEventDispatcher()
export let running_since = "" let {
running_since = "",
refresh,
}: {
running_since?: string
refresh?: () => void
} = $props();
$: profile_running = running_since != "0001-01-01T00:00:00Z" && running_since != "" let profile_running = $derived(running_since != "0001-01-01T00:00:00Z" && running_since != "")
const start = async () => { const start = async () => {
if (!profile_running) { if (!profile_running) {
const resp = await fetch( const resp = await fetch(
window.api_endpoint+"/admin/cpu_profile", get_endpoint()+"/admin/cpu_profile",
{ method: "POST" } { method: "POST" }
); );
if(resp.status >= 400) { if(resp.status >= 400) {
throw new Error(await resp.text()); throw new Error(await resp.text());
} }
} else { } else {
window.open(window.api_endpoint+"/admin/cpu_profile") window.open(get_endpoint()+"/admin/cpu_profile")
} }
dispatch("refresh") if (refresh !== undefined) {
refresh()
}
} }
let interval let interval: number
let running_time = "0s" let running_time = $state("0s")
onMount(() => { onMount(() => {
interval = setInterval(() => { interval = setInterval(() => {
if (profile_running) { if (profile_running) {
running_time = formatDuration( running_time = formatDuration(
(new Date()).getTime() - Date.parse(running_since), (new Date()).getTime() - Date.parse(running_since), 3
) )
} }
}, 1000) }, 1000)
@@ -43,7 +51,7 @@ onMount(() => {
<a class="button" href="/api/admin/call_stack">Call stack</a> <a class="button" href="/api/admin/call_stack">Call stack</a>
<a class="button" href="/api/admin/heap_profile">Heap profile</a> <a class="button" href="/api/admin/heap_profile">Heap profile</a>
<button on:click={start} class:button_red={profile_running}> <button onclick={start} class:button_red={profile_running}>
{#if profile_running} {#if profile_running}
Stop CPU profiling (running for {running_time}) Stop CPU profiling (running for {running_time})
{:else} {:else}

View File

@@ -1,8 +1,8 @@
<script> <script lang="ts">
import Euro from "util/Euro.svelte"; import Euro from "util/Euro.svelte";
import { formatDataVolume, formatDate } from "util/Formatting"; import { formatDataVolume, formatDate } from "util/Formatting";
export let row = {} let { row = {} } = $props();
</script> </script>
<table> <table>

View File

@@ -1,20 +1,20 @@
<script> <script>
import { stopPropagation } from 'svelte/legacy';
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 = $state([])
let rows = [] let total_offences = $state(0)
let total_offences = 0 let expanded = $state(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 +28,7 @@ const get_bans = async () => {
} catch (err) { } catch (err) {
alert(err); alert(err);
} finally { } finally {
loading = false; loading_finish()
} }
}; };
@@ -105,8 +105,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">
@@ -116,7 +114,7 @@ onMount(get_bans);
Offences {total_offences} Offences {total_offences}
</div> </div>
<div class="toolbar_spacer"></div> <div class="toolbar_spacer"></div>
<button class:button_highlight={expanded} on:click={() => {expanded = !expanded}}> <button class:button_highlight={expanded} onclick={() => {expanded = !expanded}}>
{#if expanded} {#if expanded}
<i class="icon">unfold_less</i> Collapse all <i class="icon">unfold_less</i> Collapse all
{:else} {:else}
@@ -127,7 +125,8 @@ onMount(get_bans);
{#each rows as row (row.user_id)} {#each rows as row (row.user_id)}
<Expandable expanded={expanded} click_expand> <Expandable expanded={expanded} click_expand>
<div slot="header" class="header"> {#snippet header()}
<div class="header">
<div class="title"> <div class="title">
{row.user.username} {row.user.username}
</div> </div>
@@ -143,10 +142,11 @@ onMount(get_bans);
Date<br/> Date<br/>
{formatDate(row.offences[0].ban_time, false, false, false)} {formatDate(row.offences[0].ban_time, false, false, false)}
</div> </div>
<button on:click|stopPropagation={() => {delete_ban(row.user_id)}} class="button button_red" style="align-self: center;"> <button onclick={stopPropagation(() => {delete_ban(row.user_id)})} class="button button_red" style="align-self: center;">
<i class="icon">delete</i> <i class="icon">delete</i>
</button> </button>
</div> </div>
{/snippet}
<div class="toolbar"> <div class="toolbar">
<Button click={() => impersonate(row.user_id)} icon="login" label="Impersonate user"/> <Button click={() => impersonate(row.user_id)} icon="login" label="Impersonate user"/>

View File

@@ -1,19 +1,24 @@
<script> <script lang="ts">
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";
import { get_endpoint } from "lib/PixeldrainAPI";
export let user_id = "" interface Props {
let files = [] user_id?: string;
let loading = true }
let { user_id = "" }: Props = $props();
let files = $state([])
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", get_endpoint()+"/user/files",
{ {
headers: { headers: {
"Admin-User-Override": user_id, "Admin-User-Override": user_id,
@@ -30,12 +35,12 @@ export const reload = async () => {
} catch (err) { } catch (err) {
alert(err); alert(err);
} finally { } finally {
loading = false; loading_finish()
} }
} }
let sort_field = "date_upload" let sort_field = $state("date_upload")
let asc = false let asc = $state(false)
const sort = (field) => { const sort = (field) => {
if (field !== "" && field === sort_field) { if (field !== "" && field === sort_field) {
asc = !asc asc = !asc
@@ -67,8 +72,6 @@ const sort = (field) => {
} }
</script> </script>
<LoadingIndicator loading={loading}/>
<div class="table_scroll"> <div class="table_scroll">
<table> <table>
<thead> <thead>
@@ -86,7 +89,7 @@ const sort = (field) => {
{#each files as file (file.id)} {#each files as file (file.id)}
<tr> <tr>
<td style="padding: 0; line-height: 1em;"> <td style="padding: 0; line-height: 1em;">
<img src="{window.api_endpoint+file.thumbnail_href}?height=48&width=48" alt="icon" class="thumbnail" /> <img src="{get_endpoint()+file.thumbnail_href}?height=48&width=48" alt="icon" class="thumbnail" />
</td> </td>
<td> <td>
<a href="/u/{file.id}" target="_blank">{file.name}</a> <a href="/u/{file.id}" target="_blank">{file.name}</a>

View File

@@ -1,18 +1,23 @@
<script> <script lang="ts">
import { loading_finish, loading_start } from "lib/Loading";
import { get_endpoint } from "lib/PixeldrainAPI";
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 = "" interface Props {
let lists = [] user_id?: string;
let loading = true }
let { user_id = "" }: Props = $props();
let lists = $state([])
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", get_endpoint()+"/user/lists",
{ {
headers: { headers: {
"Admin-User-Override": user_id, "Admin-User-Override": user_id,
@@ -28,13 +33,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>
@@ -49,7 +52,7 @@ export const reload = async () => {
{#each lists as list (list.id)} {#each lists as list (list.id)}
<tr> <tr>
<td style="padding: 0; line-height: 1em;"> <td style="padding: 0; line-height: 1em;">
<img src="{window.api_endpoint}/list/{list.id}/thumbnail?height=48&width=48" alt="icon" class="thumbnail" /> <img src="{get_endpoint()}/list/{list.id}/thumbnail?height=48&width=48" alt="icon" class="thumbnail" />
</td> </td>
<td> <td>
<a href="/l/{list.id}" target="_blank">{list.title}</a> <a href="/l/{list.id}" target="_blank">{list.title}</a>

View File

@@ -1,8 +0,0 @@
import App from './filesystem/Filesystem.svelte';
const app = new App({
target: document.body,
props: {}
});
export default app;

View File

@@ -1,40 +1,47 @@
<script lang="ts"> <script lang="ts">
import { fs_encode_path, node_is_shared } from "./FilesystemAPI"; import { preventDefault } from 'svelte/legacy';
import { fs_encode_path } from "lib/FilesystemAPI.svelte";
import type { FSNavigator } from "./FSNavigator"; import type { FSNavigator } from "./FSNavigator";
export let nav: FSNavigator let { nav }: {
nav: FSNavigator;
} = $props();
</script> </script>
<div class="breadcrumbs"> <div class="breadcrumbs">
{#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} onclick={preventDefault(() => {nav.navigate(node.path, true)})}
on:click|preventDefault={() => {nav.navigate(node.path, true)}}
> >
{#if node.abuse_type !== undefined} {#if node.abuse_type !== undefined}
<i class="icon small">block</i> <i class="icon small">block</i>
{:else if node_is_shared(node)} {:else if node.is_shared()}
<i class="icon small">share</i> <i class="icon small">share</i>
{/if} {/if}
<div class="node_name" class:base={$nav.base_index === i}> <div class="node_name" class:base={$nav.base_index === i}>
{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 +49,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

@@ -1,31 +1,34 @@
<script lang="ts"> <script lang="ts">
import { run } from 'svelte/legacy';
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.svelte";
import { color_by_name } from "util/Util.svelte"; import { color_by_name } from "util/Util";
import { tick } from "svelte"; import { tick } from "svelte";
import CopyButton from "layout/CopyButton.svelte"; import CopyButton from "layout/CopyButton.svelte";
import type { FSNavigator } from "./FSNavigator"; import type { FSNavigator } from "./FSNavigator";
export let nav: FSNavigator let {
export let visible = false nav,
visible = $bindable(false)
}: {
nav: FSNavigator;
visible?: boolean;
} = $props();
export const toggle = () => visible = !visible export const toggle = () => visible = !visible
$: visibility_change(visible)
const visibility_change = visible => { const visibility_change = visible => {
if (visible) { if (visible) {
update_chart(nav.base, 0, 0) update_chart(nav.base, 0, 0)
} }
} }
$: direct_url = $nav.base.path ? window.location.origin+fs_path_url($nav.base.path) : ""
$: share_url = fs_share_url($nav.path)
$: direct_share_url = fs_share_hotlink_url($nav.path)
let chart let chart: Chart = $state()
let chart_timespan = 0 let chart_timespan = $state(0)
let chart_interval = 0 let chart_interval = $state(0)
let chart_timespans = [ let chart_timespans = [
{label: "Day (1m)", span: 1440, interval: 1}, {label: "Day (1m)", span: 1440, interval: 1},
{label: "Week (1h)", span: 10080, interval: 60}, {label: "Week (1h)", span: 10080, interval: 60},
@@ -36,10 +39,9 @@ let chart_timespans = [
{label: "Five Years (1d)", span: 2628000, interval: 1440}, {label: "Five Years (1d)", span: 2628000, interval: 1440},
] ]
let total_downloads = 0 let total_downloads = $state(0)
let total_transfer = 0 let total_transfer = $state(0)
$: update_chart($nav.base, chart_timespan, chart_interval)
let update_chart = async (base: FSNode, timespan: number, interval: number) => { let update_chart = async (base: FSNode, timespan: number, interval: number) => {
if (chart === undefined) { if (chart === undefined) {
// Wait for the chart element to render, if it's not rendered already // Wait for the chart element to render, if it's not rendered already
@@ -84,7 +86,7 @@ let update_chart = async (base: FSNode, timespan: number, interval: number) => {
display: true, display: true,
position: "right", position: "right",
ticks: { ticks: {
callback: function (value, index, values) { callback: function (value: number, index, values) {
return formatDataVolume(value, 3); return formatDataVolume(value, 3);
}, },
}, },
@@ -141,6 +143,15 @@ let update_chart = async (base: FSNode, timespan: number, interval: number) => {
console.error("Failed to get time series data:", err) console.error("Failed to get time series data:", err)
} }
} }
run(() => {
visibility_change(visible)
});
let direct_url = $derived($nav.base.path ? window.location.origin+fs_path_url($nav.base.path) : "")
let share_url = $derived(fs_share_url($nav.path))
let direct_share_url = $derived(fs_share_hotlink_url($nav.path))
run(() => {
update_chart($nav.base, chart_timespan, chart_interval)
});
</script> </script>
<Modal bind:visible={visible} title="Details" width={($nav.base.type === "file" ? 1000 : 750) + "px"}> <Modal bind:visible={visible} title="Details" width={($nav.base.type === "file" ? 1000 : 750) + "px"}>
@@ -231,7 +242,7 @@ let update_chart = async (base: FSNode, timespan: number, interval: number) => {
<div class="button_bar"> <div class="button_bar">
{#each chart_timespans as ts} {#each chart_timespans as ts}
<button <button
on:click={() => update_chart($nav.base, ts.span, ts.interval)} onclick={() => update_chart($nav.base, ts.span, ts.interval)}
class:button_highlight={chart_timespan == ts.span}> class:button_highlight={chart_timespan == ts.span}>
{ts.label} {ts.label}
</button> </button>

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.svelte";
import type { Writable } from "svelte/store" import type { FSNode, FSPath, FSPermissions, FSContext } from "../lib/FilesystemAPI.svelte";
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,16 +1,18 @@
<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.svelte";
import type { FSNavigator } from "./FSNavigator"; import type { FSNavigator } from "./FSNavigator";
export let nav: FSNavigator let { nav }: {
nav: FSNavigator;
} = $props();
let loading = true let loading = $state(true)
let downloads = 0 let downloads = $state(0)
let transfer_used = 0 let transfer_used = $state(0)
let socket = null let socket = null
let error_msg = "" let error_msg = $state("")
let connected_to = "" let connected_to = ""
@@ -22,9 +24,9 @@ onMount(() => {
} }
}) })
let total_directories = 0 let total_directories = $state(0)
let total_files = 0 let total_files = $state(0)
let total_file_size = 0 let total_file_size = $state(0)
const update_base = async () => { const update_base = async () => {
if (!nav.initialized) { if (!nav.initialized) {
@@ -104,7 +106,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 +142,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,46 +1,56 @@
<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 { type FSPath } from "lib/FilesystemAPI.svelte";
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 = $state()
let file_preview: FilePreview let toolbar: Toolbar = $state()
let toolbar: Toolbar let upload_widget: FSUploadWidget = $state()
let upload_widget: FSUploadWidget let details_visible = $state(false)
let details_visible = false let edit_window: EditWindow = $state()
let edit_window: EditWindow let edit_visible = $state(false)
let edit_visible = false let details_window: DetailsWindow = $state()
const loading = writable(true) const nav = $state(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) => {
@@ -66,16 +76,11 @@ const keydown = (e: KeyboardEvent) => {
} }
break; break;
case "s": case "s":
fs_download(nav.base) nav.base.download()
break; break;
case "r": case "r":
nav.shuffle = !nav.shuffle nav.shuffle = !nav.shuffle
break; break;
case "f": // F fullscreen
if (toolbar) {
toolbar.toggle_fullscreen()
}
break
case "a": case "a":
case "ArrowLeft": case "ArrowLeft":
nav.open_sibling(-1) nav.open_sibling(-1)
@@ -125,25 +130,10 @@ const keydown = (e: KeyboardEvent) => {
}; };
</script> </script>
<svelte:window on:keydown={keydown} /> <svelte:window onkeydown={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
@@ -151,26 +141,29 @@ const keydown = (e: KeyboardEvent) => {
nav={nav} nav={nav}
upload_widget={upload_widget} upload_widget={upload_widget}
edit_window={edit_window} edit_window={edit_window}
on:open_sibling={e => nav.open_sibling(e.detail)} details_window={details_window}
on:download={() => fs_download(nav.base)}
on:details={() => details_visible = !details_visible}
/> />
</div> </div>
</div>
<DetailsWindow nav={nav} bind:visible={details_visible} /> <Toolbar
bind:this={toolbar}
<EditWindow nav={nav} bind:this={edit_window} bind:visible={edit_visible} /> nav={nav}
bind:details_visible={details_visible}
<!-- This one is included at the highest level so uploads can keep running edit_window={edit_window}
even when the user navigates to a different directory --> bind:edit_visible={edit_visible}
<FSUploadWidget nav={nav} bind:this={upload_widget} /> />
<AffiliatePrompt/>
<LoadingIndicator loading={$loading}/>
</div> </div>
<DetailsWindow nav={nav} bind:this={details_window} bind:visible={details_visible} />
<EditWindow nav={nav} bind:this={edit_window} bind:visible={edit_visible} />
<!-- This one is included at the highest level so uploads can keep running
even when the user navigates to a different directory -->
<FSUploadWidget nav={nav} bind:this={upload_widget} />
<AffiliatePrompt/>
<style> <style>
:global(*) { :global(*) {
transition: background-color 0.2s, transition: background-color 0.2s,
@@ -183,56 +176,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

@@ -6,23 +6,31 @@ import { formatDataVolume } from "util/Formatting";
import { user } from "lib/UserStore"; import { user } from "lib/UserStore";
import Dialog from "layout/Dialog.svelte"; import Dialog from "layout/Dialog.svelte";
let button: HTMLButtonElement let button: HTMLButtonElement = $state()
let dialog: Dialog let dialog: Dialog = $state()
export let no_login_label = "Pixeldrain" let {
no_login_label = "Pixeldrain",
hide_name = true,
hide_logo = false,
style = "",
embedded = false
}: {
no_login_label?: string;
// Hide the label if the screen is smaller than 800px
hide_name?: boolean;
hide_logo?: boolean;
style?: string;
embedded?: boolean;
} = $props();
// Hide the label if the screen is smaller than 800px let target = $derived(embedded ? "_blank" : "_self")
export let hide_name = true
export let hide_logo = false
export let style = ""
export let embedded = false
$: target = embedded ? "_blank" : "_self"
const open = () => dialog.open(button.getBoundingClientRect()) const open = () => dialog.open(button.getBoundingClientRect())
</script> </script>
<div class="wrapper"> <div class="wrapper">
<button bind:this={button} on:click={open} class="button round" title="Menu" style={style}> <button bind:this={button} onclick={open} class="button round" title="Menu" style={style}>
{#if !hide_logo} {#if !hide_logo}
<PixeldrainLogo style="height: 1.6em; width: 1.6em;"/> <PixeldrainLogo style="height: 1.6em; width: 1.6em;"/>
{/if} {/if}

View File

@@ -1,22 +1,24 @@
<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, type FSNode, type FSPermissions } from "lib/FilesystemAPI.svelte";
import { copy_text } from "util/Util.svelte"; import { copy_text } from "util/Util";
import CopyButton from "layout/CopyButton.svelte"; import CopyButton from "layout/CopyButton.svelte";
import Dialog from "layout/Dialog.svelte"; import Dialog from "layout/Dialog.svelte";
import { fade } from "svelte/transition"; import { fade } from "svelte/transition";
export let nav: FSNavigator let { nav }: {
nav: FSNavigator;
} = $props();
let path: FSNode[] let path: FSNode[]
let base: FSNode let base: FSNode = $state()
let toast = "" let toast = $state("")
let share_url = "" let share_url = $state("")
let direct_share_url = "" let direct_share_url = $state("")
let is_parent = false let is_parent = $state(false)
let parent_node: FSNode let parent_node: FSNode = $state()
let dialog: Dialog let dialog: Dialog = $state()
export const open = async (e: MouseEvent, p: FSNode[]) => { export const open = async (e: MouseEvent, p: FSNode[]) => {
path = p path = p
base = path[path.length-1] base = path[path.length-1]
@@ -36,7 +38,7 @@ export const open = async (e: MouseEvent, p: FSNode[]) => {
} }
const make_public = async () => { const make_public = async () => {
if (!node_is_shared(base)) { if (!base.is_shared()) {
base = await fs_update( base = await fs_update(
base.path, base.path,
{link_permissions: {read: true} as FSPermissions}, {link_permissions: {read: true} as FSPermissions},
@@ -113,7 +115,7 @@ const share = async () => {
<img src={fs_node_icon(parent_node, 64, 64)} class="node_icon" alt="icon"/> <img src={fs_node_icon(parent_node, 64, 64)} class="node_icon" alt="icon"/>
{parent_node.name} {parent_node.name}
<br/> <br/>
<button on:click={async e => {await make_public(); await share()}} style="display: inline;"> <button onclick={async e => {await make_public(); await share()}} style="display: inline;">
Only share Only share
<img src={fs_node_icon(base, 64, 64)} class="node_icon" alt="icon"/> <img src={fs_node_icon(base, 64, 64)} class="node_icon" alt="icon"/>
{base.name} {base.name}

View File

@@ -1,26 +1,28 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from "svelte"; import { copy_text } from "util/Util";
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, path_is_shared } from "lib/FilesystemAPI.svelte";
import { fs_share_url } from "./FilesystemAPI";
import ShareDialog from "./ShareDialog.svelte"; import ShareDialog from "./ShareDialog.svelte";
import { bookmark_add, bookmark_del, bookmarks_store, is_bookmark } from "lib/Bookmarks";
let dispatch = createEventDispatcher() let {
nav = $bindable(),
details_visible = $bindable(false),
edit_window,
edit_visible = $bindable(false)
}: {
nav: FSNavigator;
details_visible?: boolean;
edit_window: EditWindow;
edit_visible?: boolean;
} = $props();
export let nav: FSNavigator let share_dialog: ShareDialog = $state()
export let details_visible = false let link_copied = $state(false)
export let edit_window: EditWindow
export let edit_visible = false
export let file_viewer: HTMLDivElement
export let file_preview: FilePreview
let share_dialog: ShareDialog
$: share_url = fs_share_url($nav.path)
let link_copied = false
export const copy_link = () => { export const copy_link = () => {
const share_url = fs_share_url($nav.path)
if (share_url === "") { if (share_url === "") {
edit_window.edit(nav.base, true, "share") edit_window.edit(nav.base, true, "share")
return return
@@ -30,103 +32,63 @@ 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 onclick={() => {nav.open_sibling(-1)}}>
<i class="icon">skip_previous</i> <i class="icon">skip_previous</i>
</button> </button>
<button on:click={() => {nav.shuffle = !nav.shuffle}} class:button_highlight={nav.shuffle}> <button onclick={() => {nav.shuffle = !nav.shuffle}} class:button_highlight={nav.shuffle}>
<i class="icon">shuffle</i> <i class="icon">shuffle</i>
</button> </button>
<button on:click={() => {nav.open_sibling(1)}}> <button onclick={() => {nav.open_sibling(1)}}>
<i class="icon">skip_next</i> <i class="icon">skip_next</i>
</button> </button>
</div> </div>
<div class="separator hidden_horizontal"></div> <button onclick={() => $nav.base.download()}>
<button on:click={() => dispatch("download")}>
<i class="icon">save</i> <i class="icon">save</i>
<span>Download</span> <span>Download</span>
</button> </button>
{#if share_url !== ""} {#if is_bookmark($bookmarks_store, $nav.base.id)}
<button on:click={copy_link} class:button_highlight={link_copied}> <button onclick={() => bookmark_del($nav.base.id)}>
<i class="icon">bookmark_remove</i>
<span>Bookmark</span>
</button>
{:else}
<button onclick={() => bookmark_add($nav.base)}>
<i class="icon">bookmark_add</i>
<span>Bookmark</span>
</button>
{/if}
{#if path_is_shared($nav.path)}
<button onclick={copy_link} class:button_highlight={link_copied}>
<i class="icon">content_copy</i> <i class="icon">content_copy</i>
<span><u>C</u>opy link</span> <span><u>C</u>opy link</span>
</button> </button>
{/if} {/if}
<!-- Share button is enabled when: The browser has a sharing API, or the user can edit the file (to enable sharing)--> <!-- Share button is enabled when: The browser has a sharing API, or the user can edit the file (to enable sharing)-->
{#if $nav.base.id !== "me" && (navigator.share !== undefined || $nav.permissions.write === true)} {#if navigator.share !== undefined || $nav.permissions.write === true}
<button on:click={(e) => share_dialog.open(e, nav.path)}> <button onclick={(e) => share_dialog.open(e, nav.path)}>
<i class="icon">share</i> <i class="icon">share</i>
<span>Share</span> <span>Share</span>
</button> </button>
{/if} {/if}
<button <button onclick={() => details_visible = !details_visible} class:button_highlight={details_visible}>
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}>
<i class="icon">help</i> <i class="icon">help</i>
<span>Deta<u>i</u>ls</span> <span>Deta<u>i</u>ls</span>
</button> </button>
{#if $nav.base.id !== "me" && $nav.permissions.write === true} {#if $nav.base.id !== "me" && $nav.permissions.write === true}
<button on:click={() => edit_window.edit(nav.base, true, "file")} class:button_highlight={edit_visible}> <button onclick={() => edit_window.edit(nav.base, true, "file")} class:button_highlight={edit_visible}>
<i class="icon">edit</i> <i class="icon">edit</i>
<span><u>E</u>dit</span> <span><u>E</u>dit</span>
</button> </button>
@@ -140,22 +102,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 +121,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,12 +1,16 @@
<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.svelte";
import PermissionButton from "./PermissionButton.svelte"; import PermissionButton from "./PermissionButton.svelte";
export let options: NodeOptions let {
options = $bindable()
}: {
options: NodeOptions;
} = $props();
let new_user_id = "" let new_user_id = $state("")
let new_user_perms = <FSPermissions>{read: true} let new_user_perms = $state(<FSPermissions>{read: true})
const add_user = (e: SubmitEvent) => { const add_user = (e: SubmitEvent) => {
e.preventDefault() e.preventDefault()
if (options.user_permissions === undefined) { if (options.user_permissions === undefined) {
@@ -19,8 +23,8 @@ const del_user = (id: string) => {
options.user_permissions = options.user_permissions options.user_permissions = options.user_permissions
} }
let new_password = "" let new_password = $state("")
let new_password_perms = <FSPermissions>{read: true} let new_password_perms = $state(<FSPermissions>{read: true})
const add_password = (e: SubmitEvent) => { const add_password = (e: SubmitEvent) => {
e.preventDefault() e.preventDefault()
if (options.password_permissions === undefined) { if (options.password_permissions === undefined) {
@@ -64,7 +68,7 @@ const del_password = (pass: string) => {
not receive an e-mail invite. Giving write access to a user without giving not receive an e-mail invite. Giving write access to a user without giving
read access as well does not actually allow them to write anything. read access as well does not actually allow them to write anything.
</p> </p>
<form on:submit={add_user} class="row"> <form onsubmit={add_user} class="row">
<input type="text" bind:value={new_user_id} placeholder="Username" class="grow" size="1"> <input type="text" bind:value={new_user_id} placeholder="Username" class="grow" size="1">
<Button type="submit" icon="add" label="Add"/> <Button type="submit" icon="add" label="Add"/>
<div class="perms"> <div class="perms">
@@ -94,7 +98,7 @@ const del_password = (pass: string) => {
<p> <p>
<b>This feature is not implemented currently!</b> <b>This feature is not implemented currently!</b>
</p> </p>
<form on:submit={add_password} class="row"> <form onsubmit={add_password} class="row">
<input type="text" bind:value={new_password} placeholder="Password" class="grow" size="1"> <input type="text" bind:value={new_password} placeholder="Password" class="grow" size="1">
<Button type="submit" icon="add" label="Add"/> <Button type="submit" icon="add" label="Add"/>
<div class="perms"> <div class="perms">

View File

@@ -2,11 +2,39 @@ 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.svelte";
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 = {} const style = {}
for (let node of path) { for (const node of path) {
add_styles(style, node.properties) add_styles(style, node.properties)
} }
last_generated_style = style last_generated_style = style
@@ -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_props = (props: FSNodeProperties) => {
add_styles(last_generated_style, node.properties) add_styles(last_generated_style, props)
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,38 +1,44 @@
<script lang="ts"> <script lang="ts">
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, type FSPermissions } from "lib/FilesystemAPI.svelte";
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";
let dispatch = createEventDispatcher() import { branding_from_props } from './Branding';
export let file: FSNode let {
export let options: NodeOptions file = $bindable(),
export let enabled: boolean options = $bindable(),
enabled = $bindable(),
custom_css = $bindable(),
}: {
file: FSNode
options: NodeOptions
enabled: boolean
custom_css: string
} = $props();
$: update_colors(options) $effect(() => {
const update_colors = (options: NodeOptions) => {
if (enabled) { if (enabled) {
options.branding_enabled = "true" options.branding_enabled = "true"
dispatch("style_change") custom_css = branding_from_props(options)
} else { } else {
options.branding_enabled = "" options.branding_enabled = ""
} }
} })
let picker: FilePicker let picker: FilePicker = $state()
let picking = "" let picking = ""
const pick_image = (type: string) => { const pick_image = (type: string) => {
picking = type picking = type
picker.open(file.path) picker.open(file.path)
} }
const handle_picker = async (e: CustomEvent<FSNode[]>) => { const handle_picker = async (nodes: FSNode[]) => {
if (e.detail.length !== 1) { if (nodes.length !== 1) {
alert("Please select one file") alert("Please select one file")
return return
} }
let f = e.detail[0] let f = nodes[0]
if (fs_node_type(f) !== "image") { if (fs_node_type(f) !== "image") {
alert("Please select an image file") alert("Please select an image file")
@@ -43,10 +49,10 @@ const handle_picker = async (e: CustomEvent<FSNode[]>) => {
} }
// If this image is not public, it will be made public // If this image is not public, it will be made public
if (!node_is_shared(f)) { if (!f.is_shared()) {
try { try {
f = await fs_update( f = await fs_update(
e.detail[0].path, nodes[0].path,
{link_permissions: {read: true} as FSPermissions}, {link_permissions: {read: true} as FSPermissions},
) )
} catch (err) { } catch (err) {
@@ -61,7 +67,7 @@ const handle_picker = async (e: CustomEvent<FSNode[]>) => {
} }
} }
let highlight_info = false let highlight_info = $state(false)
</script> </script>
<fieldset> <fieldset>
@@ -90,7 +96,7 @@ let highlight_info = false
<div style="display: inline-block">Highlight</div> <div style="display: inline-block">Highlight</div>
<HelpButton bind:toggle={highlight_info}/> <HelpButton bind:toggle={highlight_info}/>
</div> </div>
<input type="color" bind:value={options.brand_highlight_color}/> <input type="color" value={options.brand_highlight_color} onchange={e => options.brand_highlight_color = (e.target as HTMLInputElement).value}/>
<input type="text" bind:value={options.brand_highlight_color}/> <input type="text" bind:value={options.brand_highlight_color}/>
{#if highlight_info} {#if highlight_info}
<p class="span3"> <p class="span3">
@@ -101,19 +107,19 @@ let highlight_info = false
</p> </p>
{/if} {/if}
<div>Button and input</div> <div>Button and input</div>
<input type="color" bind:value={options.brand_input_color}/> <input type="color" value={options.brand_input_color} onchange={e => options.brand_input_color = (e.target as HTMLInputElement).value}/>
<input type="text" bind:value={options.brand_input_color}/> <input type="text" bind:value={options.brand_input_color}/>
<div>Delete button</div> <div>Delete button</div>
<input type="color" bind:value={options.brand_danger_color}/> <input type="color" value={options.brand_danger_color} onchange={e => options.brand_danger_color = (e.target as HTMLInputElement).value}/>
<input type="text" bind:value={options.brand_danger_color}/> <input type="text" bind:value={options.brand_danger_color}/>
<div>Background</div> <div>Background</div>
<input type="color" bind:value={options.brand_background_color}/> <input type="color" value={options.brand_background_color} onchange={e => options.brand_background_color = (e.target as HTMLInputElement).value}/>
<input type="text" bind:value={options.brand_background_color}/> <input type="text" bind:value={options.brand_background_color}/>
<div>Body</div> <div>Body</div>
<input type="color" bind:value={options.brand_body_color}/> <input type="color" value={options.brand_body_color} onchange={e => options.brand_body_color = (e.target as HTMLInputElement).value}/>
<input type="text" bind:value={options.brand_body_color}/> <input type="text" bind:value={options.brand_body_color}/>
<div>Card</div> <div>Card</div>
<input type="color" bind:value={options.brand_card_color}/> <input type="color" value={options.brand_card_color} onchange={e => options.brand_card_color = (e.target as HTMLInputElement).value}/>
<input type="text" bind:value={options.brand_card_color}/> <input type="text" bind:value={options.brand_card_color}/>
</fieldset> </fieldset>
@@ -127,7 +133,7 @@ let highlight_info = false
working. Recommended dimensions for the header image are 1000x90 px. working. Recommended dimensions for the header image are 1000x90 px.
</p> </p>
<div>Header image ID</div> <div>Header image ID</div>
<button on:click={() => pick_image("brand_header_image")}> <button onclick={() => pick_image("brand_header_image")}>
<i class="icon">folder_open</i> <i class="icon">folder_open</i>
Pick Pick
</button> </button>
@@ -135,7 +141,7 @@ let highlight_info = false
<div>Header image link</div> <div>Header image link</div>
<input class="span2" type="text" bind:value={options.brand_header_link}/> <input class="span2" type="text" bind:value={options.brand_header_link}/>
<div>Background image ID</div> <div>Background image ID</div>
<button on:click={() => pick_image("brand_background_image")}> <button onclick={() => pick_image("brand_background_image")}>
<i class="icon">folder_open</i> <i class="icon">folder_open</i>
Pick Pick
</button> </button>
@@ -177,7 +183,7 @@ let highlight_info = false
</div> </div>
</fieldset> </fieldset>
<FilePicker bind:this={picker} on:files={handle_picker}/> <FilePicker bind:this={picker} callback={handle_picker}/>
<style> <style>
input[type="color"] { input[type="color"] {

View File

@@ -1,20 +1,26 @@
<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.svelte";
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_props } from "./Branding";
import FileOptions from "./FileOptions.svelte"; import FileOptions from "./FileOptions.svelte";
import SharingOptions from "./SharingOptions.svelte"; import SharingOptions from "./SharingOptions.svelte";
import AccessControl from "./AccessControl.svelte"; import AccessControl from "./AccessControl.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 let file: FSNode = $state({} as FSNode)
let file: FSNode = {} as FSNode let options: NodeOptions = $state({} as NodeOptions)
let options: NodeOptions = {} as NodeOptions
let custom_css = "" let custom_css = $state("")
export let visible: boolean let {
nav,
visible = $bindable()
}: {
nav: FSNavigator;
visible: boolean;
} = $props();
// Open the edit window. Argument 1 is the file to edit, 2 is whether the file // Open the edit window. Argument 1 is the file to edit, 2 is whether the file
// should be opened after the user finishes editing and 3 is the default tab // should be opened after the user finishes editing and 3 is the default tab
@@ -39,22 +45,13 @@ 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) {
custom_css = branding_from_node(file) custom_css = branding_from_props(options)
} else { } else {
custom_css = "" custom_css = ""
} }
@@ -62,18 +59,19 @@ export const edit = (f: FSNode, oae = false, open_tab = "") => {
visible = true visible = true
} }
let tab = "file" let tab = $state("file")
let open_after_edit = false let open_after_edit = $state(false)
let new_name = "" let new_name = $state("")
let branding_enabled = false let branding_enabled = $state(false)
const save = async (keep_editing = false) => { const save = async (e: SubmitEvent) => {
e.preventDefault()
console.debug("Saving file", file.path) console.debug("Saving file", file.path)
let new_file: FSNode let new_file: FSNode
try { try {
nav.set_loading(true) loading_start()
options.branding_enabled = JSON.stringify(branding_enabled) options.branding_enabled = JSON.stringify(branding_enabled)
new_file = await fs_update(file.path, options) new_file = await fs_update(file.path, options)
@@ -97,7 +95,7 @@ const save = async (keep_editing = false) => {
} }
return return
} finally { } finally {
nav.set_loading(false) loading_finish()
} }
if (open_after_edit) { if (open_after_edit) {
@@ -105,36 +103,32 @@ const save = async (keep_editing = false) => {
} else { } else {
nav.reload() nav.reload()
} }
if (keep_editing) {
edit(new_file, open_after_edit)
}
} }
</script> </script>
<Modal bind:visible={visible} title="Edit {file.name}" width="800px" form="edit_form" style="color: var(--body_text_color); {custom_css}"> <Modal bind:visible={visible} title="Edit {file.name}" width="800px" form="edit_form" style="color: var(--body_text_color); {custom_css}">
<div class="tab_bar"> <div class="tab_bar">
<button class:button_highlight={tab === "file"} on:click={() => tab = "file"}> <button class:button_highlight={tab === "file"} onclick={() => tab = "file"}>
<i class="icon">edit</i> <i class="icon">edit</i>
Properties Properties
</button> </button>
<button class:button_highlight={tab === "share"} on:click={() => tab = "share"}> <button class:button_highlight={tab === "share"} onclick={() => tab = "share"}>
<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"} onclick={() => tab = "access"}>
<i class="icon">key</i> <i class="icon">key</i>
Access control Access control
</button> </button>
{/if} {/if}
<button class:button_highlight={tab === "branding"} on:click={() => tab = "branding"}> <button class:button_highlight={tab === "branding"} onclick={() => tab = "branding"}>
<i class="icon">palette</i> <i class="icon">palette</i>
Branding Branding
</button> </button>
</div> </div>
<form id="edit_form" on:submit|preventDefault={() => save(false)}></form> <form id="edit_form" onsubmit={save}></form>
<div class="tab_content"> <div class="tab_content">
{#if tab === "file"} {#if tab === "file"}
@@ -146,7 +140,10 @@ const save = async (keep_editing = false) => {
bind:open_after_edit bind:open_after_edit
/> />
{:else if tab === "share"} {:else if tab === "share"}
<SharingOptions bind:file bind:options on:save={() => save(true)} /> <SharingOptions
bind:file
bind:options
/>
{:else if tab === "access"} {:else if tab === "access"}
<AccessControl bind:options /> <AccessControl bind:options />
{:else if tab === "branding"} {:else if tab === "branding"}
@@ -154,7 +151,7 @@ const save = async (keep_editing = false) => {
bind:enabled={branding_enabled} bind:enabled={branding_enabled}
bind:options={options} bind:options={options}
bind:file bind:file
on:style_change={e => custom_css = branding_from_node(file)} bind:custom_css
/> />
{/if} {/if}
</div> </div>

View File

@@ -1,29 +1,38 @@
<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.svelte";
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";
import { loading_finish, loading_start } from "lib/Loading";
export let nav: FSNavigator let {
export let file: FSNode = {} as FSNode nav,
export let new_name: string file = $bindable({} as FSNode),
export let visible: boolean new_name = $bindable(),
export let open_after_edit: boolean visible = $bindable(),
open_after_edit = $bindable(false)
}: {
nav: FSNavigator;
file?: FSNode;
new_name: string;
visible: boolean;
open_after_edit: boolean;
} = $props();
$: is_root_dir = file.path === "/"+file.id let is_root_dir = $derived(file.path === "/"+file.id)
const delete_file = async (e: MouseEvent) => { const delete_file = async (e: MouseEvent) => {
e.preventDefault() e.preventDefault()
try { try {
nav.set_loading(true) loading_start()
await fs_delete_all(file.path) await fs_delete_all(file.path)
} catch (err) { } catch (err) {
console.error(err) console.error(err)
alert(err) alert(err)
return return
} finally { } finally {
nav.set_loading(false) loading_finish()
} }
if (open_after_edit) { if (open_after_edit) {

View File

@@ -1,10 +1,16 @@
<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.svelte";
export let permissions = <FSPermissions>{} let {
permissions = $bindable()
}: {
permissions: FSPermissions
} = $props();
</script> </script>
<ToggleButton group_first bind:on={permissions.read}>Read</ToggleButton> {#if permissions !== undefined}
<ToggleButton group_middle bind:on={permissions.write}>Write</ToggleButton> <ToggleButton group_first bind:on={permissions.read}>Read</ToggleButton>
<ToggleButton group_last bind:on={permissions.delete}>Delete</ToggleButton> <ToggleButton group_middle bind:on={permissions.write}>Write</ToggleButton>
<ToggleButton group_last bind:on={permissions.delete}>Delete</ToggleButton>
{/if}

View File

@@ -1,21 +1,24 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from "svelte"; import { run } from 'svelte/legacy';
import { domain_url } from "util/Util.svelte"; import { domain_url } from "util/Util";
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 { type FSNode, type NodeOptions } from "lib/FilesystemAPI.svelte";
import AccessControl from "./AccessControl.svelte";
let dispatch = createEventDispatcher() let {
export let file: FSNode = {} as FSNode file = $bindable(),
export let options: NodeOptions options = $bindable(),
}: {
file?: FSNode;
options: NodeOptions;
} = $props();
let embed_html: string let embed_html: string = $state()
let preview_area: HTMLDivElement let preview_area: HTMLDivElement = $state()
$: share_link = window.location.protocol+"//"+window.location.host+"/d/"+file.id const embed_iframe = (file: FSNode, options: NodeOptions) => {
$: embed_iframe(file, options) if (!file.is_shared()) {
let embed_iframe = (file: FSNode, options: NodeOptions) => {
if (!options.shared) {
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
@@ -24,14 +27,14 @@ let embed_iframe = (file: FSNode, options: NodeOptions) => {
let url = domain_url()+"/d/"+file.id let url = domain_url()+"/d/"+file.id
embed_html = `<iframe ` + embed_html = `<iframe ` +
`src="${url}" ` + `src="${url}" ` +
`style="border: none; width: 100%; max-width 90vw; height: 800px; max-height: 75vh; border-radius: 6px; "` + `style="border: none; width: 100%; max-width 90vw; height: 800px; max-height: 75vh; border-radius: 6px;" ` +
`allowfullscreen` + `allowfullscreen` +
`></iframe>` `></iframe>`
} }
let example = false let example = $state(false)
const toggle_example = () => { const toggle_example = () => {
if (options.shared) { if (file.is_shared()) {
example = !example example = !example
if (example) { if (example) {
preview_area.innerHTML = embed_html preview_area.innerHTML = embed_html
@@ -41,15 +44,10 @@ const toggle_example = () => {
} }
} }
const update_shared = () => { let share_link = $derived(window.location.protocol+"//"+window.location.host+"/d/"+file.id)
// If sharing is enabled we automatically save the file so the user can copy run(() => {
// the sharing link. But if the user disables sharing we don't automatically embed_iframe(file, options)
// 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 +62,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 +86,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 onclick={toggle_example} class:button_highlight={example} disabled={!file.is_shared()}>
<i class="icon">visibility</i> Show example <i class="icon">visibility</i> Show example
</button> </button>
</div> </div>

View File

@@ -1,9 +1,13 @@
<script lang="ts"> <script lang="ts">
import type { FSNodeProperties } from "filesystem/FilesystemAPI"; import type { FSNodeProperties } from "lib/FilesystemAPI.svelte";
export let properties: FSNodeProperties = {} as FSNodeProperties let {
properties = $bindable({} as FSNodeProperties)
}: {
properties?: FSNodeProperties;
} = $props();
let current_theme = -1 let current_theme = $state(-1)
const set_theme = (index: number) => { const set_theme = (index: number) => {
current_theme = index current_theme = index
@@ -71,7 +75,7 @@ const themes = [
</script> </script>
{#each themes as theme, index (theme.name)} {#each themes as theme, index (theme.name)}
<button class:button_highlight={current_theme === index} on:click={() => {set_theme(index)}}> <button class:button_highlight={current_theme === index} onclick={() => {set_theme(index)}}>
{theme.name} {theme.name}
</button> </button>
{/each} {/each}

View File

@@ -1,23 +1,29 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from "svelte"; import { fs_encode_path, fs_node_icon } from "lib/FilesystemAPI.svelte"
import { fs_encode_path, fs_node_icon, node_is_shared } from "filesystem/FilesystemAPI"
import type { FSNavigator } from "filesystem/FSNavigator"; import type { FSNavigator } from "filesystem/FSNavigator";
import { FileAction } from "./FileManagerLib"; import { FileAction, type FileActionHandler } from "./FileManagerLib";
let dispatch = createEventDispatcher() let {
nav,
export let nav: FSNavigator file_event,
export let show_hidden = false show_hidden = false,
export let large_icons = false large_icons = false,
export let hide_edit = false hide_edit = false
}: {
nav: FSNavigator;
file_event: FileActionHandler
show_hidden?: boolean
large_icons?: boolean
hide_edit?: boolean
} = $props();
</script> </script>
<div class="directory"> <div class="directory">
{#each $nav.children as child, index (child.path)} {#each $nav.children as child, index (child.path)}
<a <a
href={"/d"+fs_encode_path(child.path)} href={"/d"+fs_encode_path(child.path)}
on:click={e => dispatch("file", {index: index, action: FileAction.Click, original: e})} onclick={e => file_event(FileAction.Click, index, e)}
on:contextmenu={e => dispatch("file", {index: index, action: FileAction.Context, original: e})} oncontextmenu={e => file_event(FileAction.Context, index, e)}
class="node" class="node"
class:node_selected={child.fm_selected} class:node_selected={child.fm_selected}
class:hidden={child.name.startsWith(".") && !show_hidden} class:hidden={child.name.startsWith(".") && !show_hidden}
@@ -26,31 +32,19 @@ export let hide_edit = false
<div class="node_name"> <div class="node_name">
{child.name} {child.name}
</div> </div>
{#if node_is_shared(child)}
<a {#if child.is_shared()}
href="/d/{child.id}"
on:click={e => dispatch("file", {index: index, action: FileAction.Share, original: e})}
class="button flat action_button"
>
<i class="icon" title="This file / directory is shared. Click to open public link">share</i> <i class="icon" title="This file / directory is shared. Click to open public link">share</i>
</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})} onclick={e => file_event(FileAction.Menu, index, 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 +65,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(--body_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 +86,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,20 +1,22 @@
<script lang="ts"> <script lang="ts">
import { preventDefault } from 'svelte/legacy';
import { onMount } from "svelte"; import { onMount } from "svelte";
import { fs_mkdir } from "filesystem/FilesystemAPI"; import { fs_mkdir } from "lib/FilesystemAPI.svelte";
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 let { nav }: { nav: FSNavigator } = $props();
let name_input: HTMLInputElement; let name_input: HTMLInputElement = $state();
let new_dir_name = "" let new_dir_name = $state("")
let error_msg = "" let error_msg = $state("")
let create_dir = async () => { let create_dir = async () => {
let form = new FormData() let form = new FormData()
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 +28,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()
} }
} }
@@ -41,7 +43,7 @@ onMount(() => {
</div> </div>
{/if} {/if}
<form id="create_dir_form" class="create_dir" on:submit|preventDefault={create_dir}> <form id="create_dir_form" class="create_dir" onsubmit={preventDefault(create_dir)}>
<img src="/res/img/mime/folder.png" class="icon" alt="icon"/> <img src="/res/img/mime/folder.png" class="icon" alt="icon"/>
<input class="dirname" type="text" bind:this={name_input} bind:value={new_dir_name} /> <input class="dirname" type="text" bind:this={name_input} bind:value={new_dir_name} />
<Button form="create_dir_form" type="submit" icon="create_new_folder" label="Create"/> <Button form="create_dir_form" type="submit" icon="create_new_folder" label="Create"/>

View File

@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { fs_delete_all, fs_download, fs_rename, type FSNode } from "filesystem/FilesystemAPI" import { run } from 'svelte/legacy';
import { fs_delete_all, fs_rename, type FSNode } from "lib/FilesystemAPI.svelte"
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"
@@ -12,30 +13,42 @@ import SearchBar from "./SearchBar.svelte";
import type { FSNavigator } from "filesystem/FSNavigator"; 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 FileActionHandler } from "./FileManagerLib";
import FileMenu from "./FileMenu.svelte";
import { loading_finish, loading_start } from "lib/Loading";
export let nav: FSNavigator let {
export let upload_widget: FsUploadWidget nav = $bindable(),
export let edit_window: EditWindow upload_widget,
export let directory_view = "" edit_window = $bindable(),
let large_icons = false directory_view = $bindable(""),
children
}: {
nav: FSNavigator;
upload_widget: FsUploadWidget;
edit_window: EditWindow;
directory_view?: string;
children?: import('svelte').Snippet;
} = $props();
let large_icons = $state(false)
let uploader: FsUploadWidget let uploader: FsUploadWidget
let mode = "viewing" let mode = $state("viewing")
let creating_dir = false let creating_dir = $state(false)
let show_hidden = false let show_hidden = $state(false)
let file_menu: FileMenu = $state()
export const upload = (files: File[]) => { export const upload = (files: File[]) => {
return uploader.upload(files) return uploader.upload_files(files)
} }
// Navigation functions // Navigation functions
const file_event = (e: CustomEvent<FileEvent>) => { const file_event: FileActionHandler = (action: FileAction, index: number, orig: Event) => {
const index = e.detail.index orig.preventDefault()
orig.stopPropagation()
switch (e.detail.action) { switch (action) {
case FileAction.Click: case FileAction.Click:
e.detail.original.preventDefault()
e.detail.original.stopPropagation()
creating_dir = false creating_dir = false
if (mode === "viewing") { if (mode === "viewing") {
@@ -54,35 +67,26 @@ const file_event = (e: CustomEvent<FileEvent>) => {
case FileAction.Context: case FileAction.Context:
// If this is a touch event we will select the item // If this is a touch event we will select the item
if (navigator.maxTouchPoints && navigator.maxTouchPoints > 0) { if (navigator.maxTouchPoints && navigator.maxTouchPoints > 0) {
e.detail.original.preventDefault()
select_node(index) select_node(index)
} else {
file_menu.open(nav.children[index], orig.target)
} }
break break
case FileAction.Edit: case FileAction.Edit:
e.detail.original.preventDefault()
e.detail.original.stopPropagation()
edit_window.edit(nav.children[index], false, "file") edit_window.edit(nav.children[index], false, "file")
break break
case FileAction.Share: case FileAction.Share:
e.detail.original.preventDefault()
e.detail.original.stopPropagation()
creating_dir = false creating_dir = false
edit_window.edit(nav.children[index], false, "share") edit_window.edit(nav.children[index], false, "share")
break break
case FileAction.Branding: case FileAction.Branding:
e.detail.original.preventDefault()
e.detail.original.stopPropagation()
edit_window.edit(nav.children[index], false, "branding") edit_window.edit(nav.children[index], false, "branding")
break break
case FileAction.Select: case FileAction.Select:
e.detail.original.preventDefault()
e.detail.original.stopPropagation()
select_node(index) select_node(index)
break break
case FileAction.Download: case FileAction.Menu:
e.detail.original.preventDefault() file_menu.open(nav.children[index], orig.target)
e.detail.original.stopPropagation()
fs_download(nav.children[index])
break break
} }
} }
@@ -110,9 +114,9 @@ const delete_selected = async () => {
return return
} }
nav.set_loading(true)
try { try {
loading_start()
// Save all promises with deletion requests in an array // Save all promises with deletion requests in an array
let promises = [] let promises = []
nav.children.forEach(child => { nav.children.forEach(child => {
@@ -129,7 +133,7 @@ const delete_selected = async () => {
alert("Delete failed: " + err.message + " ("+err.value+")") alert("Delete failed: " + err.message + " ("+err.value+")")
} finally { } finally {
viewing_mode() viewing_mode()
nav.reload() loading_finish()
} }
} }
@@ -220,10 +224,6 @@ const select_node = (index: number) => {
last_selected_node = index last_selected_node = index
} }
// When the directory is reloaded we want to keep our selection, so this
// function watches the children array for changes and updates the selection
// when it changes
$: update($nav.children)
const update = (children: FSNode[]) => { const update = (children: FSNode[]) => {
creating_dir = false creating_dir = false
@@ -237,8 +237,8 @@ const update = (children: FSNode[]) => {
} }
} }
let moving_files = 0 let moving_files = $state(0)
let moving_directories = 0 let moving_directories = $state(0)
const move_start = () => { const move_start = () => {
moving_files = 0 moving_files = 0
moving_directories = 0 moving_directories = 0
@@ -257,11 +257,10 @@ const move_start = () => {
} }
const move_here = async () => { const move_here = async () => {
nav.set_loading(true) const target_dir = nav.base.path + "/"
let target_dir = nav.base.path + "/"
try { try {
loading_start()
let promises = [] let promises = []
moving_items.forEach(item => { moving_items.forEach(item => {
console.log("moving", item.path, "to", target_dir + item.name) console.log("moving", item.path, "to", target_dir + item.name)
@@ -276,6 +275,7 @@ const move_here = async () => {
} finally { } finally {
viewing_mode() viewing_mode()
nav.reload() nav.reload()
loading_finish()
} }
} }
@@ -288,9 +288,15 @@ onMount(() => {
directory_view = "list" directory_view = "list"
} }
}) })
// When the directory is reloaded we want to keep our selection, so this
// function watches the children array for changes and updates the selection
// when it changes
run(() => {
update($nav.children)
});
</script> </script>
<svelte:window on:keydown={keypress} on:keyup={keypress} /> <svelte:window onkeydown={keypress} onkeyup={keypress} />
<div <div
class="container" class="container"
@@ -303,25 +309,25 @@ onMount(() => {
{#if mode === "viewing"} {#if mode === "viewing"}
<div class="toolbar"> <div class="toolbar">
<div class="toolbar_left"> <div class="toolbar_left">
<button on:click={navigate_back} title="Back"> <button onclick={navigate_back} title="Back">
<i class="icon">arrow_back</i> <i class="icon">arrow_back</i>
</button> </button>
<button on:click={() => nav.navigate_up()} disabled={$nav.path.length <= 1} title="Up"> <button onclick={() => nav.navigate_up()} disabled={$nav.path.length <= 1} title="Up">
<i class="icon">north</i> <i class="icon">north</i>
</button> </button>
<button on:click={() => nav.reload()} title="Refresh directory listing"> <button onclick={() => nav.reload()} title="Refresh directory listing">
<i class="icon">refresh</i> <i class="icon">refresh</i>
</button> </button>
</div> </div>
<div class="toolbar_middle"> <div class="toolbar_middle">
<button on:click={() => toggle_view()} title="Switch between gallery, list and compact view"> <button onclick={() => toggle_view()} title="Switch between gallery, list and compact view">
<i class="icon" class:button_highlight={directory_view === "list"}>list</i> <i class="icon" class:button_highlight={directory_view === "list"}>list</i>
<i class="icon" class:button_highlight={directory_view === "gallery"}>collections</i> <i class="icon" class:button_highlight={directory_view === "gallery"}>collections</i>
<i class="icon" class:button_highlight={directory_view === "compact"}>view_compact</i> <i class="icon" class:button_highlight={directory_view === "compact"}>view_compact</i>
</button> </button>
<button class="button_large_icons" on:click={() => toggle_large_icons()} title="Switch between large and small icons"> <button class="button_large_icons" onclick={() => toggle_large_icons()} title="Switch between large and small icons">
{#if large_icons} {#if large_icons}
<i class="icon">zoom_out</i> <i class="icon">zoom_out</i>
{:else} {:else}
@@ -329,7 +335,7 @@ onMount(() => {
{/if} {/if}
</button> </button>
<button on:click={() => {show_hidden = !show_hidden}} title="Toggle hidden files"> <button onclick={() => {show_hidden = !show_hidden}} title="Toggle hidden files">
{#if show_hidden} {#if show_hidden}
<i class="icon">visibility_off</i> <i class="icon">visibility_off</i>
{:else} {:else}
@@ -340,13 +346,13 @@ onMount(() => {
<div class="toolbar_right"> <div class="toolbar_right">
{#if $nav.permissions.write} {#if $nav.permissions.write}
<button on:click={() => upload_widget.pick_files()} title="Upload files to this directory"> <button onclick={() => upload_widget.pick_files()} title="Upload files to this directory">
<i class="icon">cloud_upload</i> <i class="icon">cloud_upload</i>
</button> </button>
<Button click={() => {creating_dir = !creating_dir}} highlight={creating_dir} icon="create_new_folder" title="Make folder"/> <Button click={() => {creating_dir = !creating_dir}} highlight={creating_dir} icon="create_new_folder" title="Make folder"/>
<button on:click={selecting_mode} title="Select and delete files"> <button onclick={selecting_mode} title="Select and delete files">
<i class="icon">select_all</i> <i class="icon">select_all</i>
</button> </button>
{/if} {/if}
@@ -360,7 +366,7 @@ onMount(() => {
<Button click={viewing_mode} icon="close"/> <Button click={viewing_mode} icon="close"/>
<div class="toolbar_spacer">Selecting files</div> <div class="toolbar_spacer">Selecting files</div>
<Button click={move_start} icon="drive_file_move" label="Move"/> <Button click={move_start} icon="drive_file_move" label="Move"/>
<button on:click={delete_selected} class="button_red"> <button onclick={delete_selected} class="button_red">
<i class="icon">delete</i> <i class="icon">delete</i>
Delete Delete
</button> </button>
@@ -399,24 +405,25 @@ onMount(() => {
</div> </div>
{/if} {/if}
<slot></slot> {@render children?.()}
{#if directory_view === "list"} {#if directory_view === "list"}
<ListView nav={nav} show_hidden={show_hidden} large_icons={large_icons} on:file={file_event} /> <ListView nav={nav} file_event={file_event} show_hidden={show_hidden} large_icons={large_icons}/>
{:else if directory_view === "gallery"} {:else if directory_view === "gallery"}
<GalleryView nav={nav} show_hidden={show_hidden} large_icons={large_icons} on:file={file_event} /> <GalleryView nav={nav} file_event={file_event} show_hidden={show_hidden} large_icons={large_icons}/>
{:else if directory_view === "compact"} {:else if directory_view === "compact"}
<CompactView nav={nav} show_hidden={show_hidden} large_icons={large_icons} on:file={file_event} /> <CompactView nav={nav} file_event={file_event} show_hidden={show_hidden} large_icons={large_icons}/>
{/if} {/if}
</div> </div>
<FileMenu bind:this={file_menu} nav={nav} edit_window={edit_window} />
<style> <style>
.container { .container {
height: 100%;
width: 100%;
padding: 0; padding: 0;
overflow: auto; overflow: auto;
display: block; display: block;
height: 100%; /* Used for drop target */
} }
.width_container { .width_container {
position: sticky; position: sticky;
@@ -425,8 +432,8 @@ onMount(() => {
width: 100%; width: 100%;
margin: auto; margin: auto;
padding: 0; padding: 0;
background: var(--shaded_background); background: var(--body_background);
backdrop-filter: blur(4px); /* backdrop-filter: blur(4px); */
} }
.toolbar { .toolbar {
display: flex; display: flex;

View File

@@ -3,4 +3,18 @@ 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,
}
export type FileActionHandler = (
action: FileAction,
file_index: number,
original_event: Event,
) => void

View File

@@ -0,0 +1,70 @@
<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, is_bookmark } from "lib/Bookmarks";
import { fs_delete, type FSNode } from "lib/FilesystemAPI.svelte";
import { loading_finish, loading_start } from "lib/Loading";
import { tick } from "svelte";
let {
nav,
edit_window
}: {
nav: FSNavigator;
edit_window: EditWindow;
} = $props();
let dialog: Dialog = $state()
let node: FSNode = $state(null)
export const open = async (n: FSNode, target: EventTarget) => {
node = n
let el: HTMLElement = (target as Element).closest("button")
if (el === null) {
el = (target as Element).closest("a")
}
// Wait for the view to update, so the dialog gets the proper measurements
await tick()
dialog.open(el.getBoundingClientRect())
}
const delete_node = async () => {
try {
loading_start()
await fs_delete(node.path)
nav.reload()
} catch (err) {
alert(JSON.stringify(err))
} finally {
loading_finish()
}
}
</script>
<Dialog bind:this={dialog}>
<div class="menu">
<Button click={() => {dialog.close(); node.download()}} icon="save" label="Download"/>
{#if node !== null && is_bookmark($bookmarks_store, node.id)}
<Button click={() => {dialog.close(); bookmark_del(node.id)}} icon="bookmark_remove" label="Remove bookmark"/>
{:else}
<Button click={() => {dialog.close(); bookmark_add(node)}} icon="bookmark_add" label="Add bookmark"/>
{/if}
{#if $nav.permissions.write}
<Button click={() => {dialog.close(); delete_node()}} icon="delete" label="Delete"/>
<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

@@ -1,44 +1,46 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher, onMount } from "svelte" import { onMount } from "svelte"
import ListView from "./ListView.svelte" 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.svelte";
import { FileAction, type FileEvent } from "./FileManagerLib"; import { FileAction, type FileActionHandler } from "./FileManagerLib";
let nav = new FSNavigator(false) let nav = $state(new FSNavigator(false))
let modal: Modal let modal: Modal = $state()
let dispatch = createEventDispatcher() let directory_view = $state("")
let directory_view = "" let large_icons = $state(false)
let loading = false let show_hidden = $state(false)
let large_icons = false
let show_hidden = false let {
export let select_multiple = false callback,
select_multiple = false
}: {
callback: (files: FSNode[]) => void
select_multiple?: boolean
} = $props();
export const open = (path: string) => { export const open = (path: string) => {
modal.show() modal.show()
nav.navigate(path, false) nav.navigate(path, false)
} }
$: selected_files = $nav.children.reduce((acc, file) => { let selected_files = $derived($nav.children.reduce((acc, file) => {
if (file.fm_selected) { if (file.fm_selected) {
acc++ acc++
} }
return acc return acc
}, 0) }, 0))
// Navigation functions // Navigation functions
const file_event = (e: CustomEvent<FileEvent>) => { const file_event: FileActionHandler = (action: FileAction, index: number, orig: Event) => {
const index = e.detail.index switch (action) {
switch (e.detail.action) {
case FileAction.Click: case FileAction.Click:
e.detail.original.preventDefault() orig.preventDefault()
if (nav.children[index].type === "dir") { if (nav.children[index].type === "dir") {
nav.navigate(nav.children[index].path, true) nav.navigate(nav.children[index].path, true)
} else { } else {
@@ -48,12 +50,12 @@ const file_event = (e: CustomEvent<FileEvent>) => {
case FileAction.Context: case FileAction.Context:
// If this is a touch event we will select the item // If this is a touch event we will select the item
if (navigator.maxTouchPoints && navigator.maxTouchPoints > 0) { if (navigator.maxTouchPoints && navigator.maxTouchPoints > 0) {
e.detail.original.preventDefault() orig.preventDefault()
select_node(index) select_node(index)
} }
break break
case FileAction.Select: case FileAction.Select:
e.detail.original.preventDefault() orig.preventDefault()
nav.children[index].fm_selected = !nav.children[index].fm_selected nav.children[index].fm_selected = !nav.children[index].fm_selected
break break
} }
@@ -114,7 +116,7 @@ let done = () => {
} }
if (selected_files.length > 0) { if (selected_files.length > 0) {
dispatch("files", selected_files) callback(selected_files)
} }
modal.hide() modal.hide()
} }
@@ -130,17 +132,18 @@ onMount(() => {
}) })
</script> </script>
<svelte:window on:keydown={detect_shift} on:keyup={detect_shift} /> <svelte:window onkeydown={detect_shift} onkeyup={detect_shift} />
<Modal bind:this={modal} width="900px"> <Modal bind:this={modal} width="900px">
<div class="header" slot="title"> {#snippet header()}
<button class="button round" on:click={modal.hide}> <div class="header" >
<button class="button round" onclick={modal.hide}>
<i class="icon">close</i> <i class="icon">close</i>
</button> </button>
<button on:click={() => nav.navigate_up()} disabled={$nav.path.length <= 1} title="Up"> <button onclick={() => nav.navigate_up()} disabled={$nav.path.length <= 1} title="Up">
<i class="icon">north</i> <i class="icon">north</i>
</button> </button>
<button on:click={() => nav.reload()} title="Refresh directory listing"> <button onclick={() => nav.reload()} title="Refresh directory listing">
<i class="icon">refresh</i> <i class="icon">refresh</i>
</button> </button>
@@ -148,7 +151,7 @@ onMount(() => {
Selected {selected_files} files Selected {selected_files} files
</div> </div>
<button on:click={() => {show_hidden = !show_hidden}} title="Toggle hidden files"> <button onclick={() => {show_hidden = !show_hidden}} title="Toggle hidden files">
{#if show_hidden} {#if show_hidden}
<i class="icon">visibility_off</i> <i class="icon">visibility_off</i>
{:else} {:else}
@@ -156,46 +159,45 @@ onMount(() => {
{/if} {/if}
</button> </button>
<button on:click={() => toggle_view()} title="Switch between gallery, list and compact view"> <button onclick={() => toggle_view()} title="Switch between gallery, list and compact view">
<i class="icon" class:button_highlight={directory_view === "list"}>list</i> <i class="icon" class:button_highlight={directory_view === "list"}>list</i>
<i class="icon" class:button_highlight={directory_view === "gallery"}>collections</i> <i class="icon" class:button_highlight={directory_view === "gallery"}>collections</i>
<i class="icon" class:button_highlight={directory_view === "compact"}>view_compact</i> <i class="icon" class:button_highlight={directory_view === "compact"}>view_compact</i>
</button> </button>
<button class="button button_highlight round" on:click={done}> <button class="button button_highlight round" onclick={done}>
<i class="icon">done</i> Pick <i class="icon">done</i> Pick
</button> </button>
</div> </div>
{/snippet}
<Breadcrumbs nav={nav}/> <Breadcrumbs nav={nav}/>
{#if directory_view === "list"} {#if directory_view === "list"}
<ListView <ListView
nav={nav} nav={nav}
file_event={file_event}
show_hidden={show_hidden} show_hidden={show_hidden}
large_icons={large_icons} large_icons={large_icons}
hide_edit hide_edit
hide_branding hide_branding
on:file={file_event}
/> />
{:else if directory_view === "gallery"} {:else if directory_view === "gallery"}
<GalleryView <GalleryView
nav={nav} nav={nav}
file_event={file_event}
show_hidden={show_hidden} show_hidden={show_hidden}
large_icons={large_icons} large_icons={large_icons}
on:file={file_event}
/> />
{:else if directory_view === "compact"} {:else if directory_view === "compact"}
<CompactView <CompactView
nav={nav} nav={nav}
file_event={file_event}
show_hidden={show_hidden} show_hidden={show_hidden}
large_icons={large_icons} large_icons={large_icons}
hide_edit hide_edit
on:file={file_event}
/> />
{/if} {/if}
<LoadingIndicator loading={loading}/>
</Modal> </Modal>
<style> <style>

View File

@@ -1,21 +1,27 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from "svelte" import { fs_node_icon, fs_node_type, fs_encode_path } from "lib/FilesystemAPI.svelte";
import { fs_node_icon, fs_node_type, fs_encode_path } from "filesystem/FilesystemAPI";
import type { FSNavigator } from "filesystem/FSNavigator"; import type { FSNavigator } from "filesystem/FSNavigator";
import { FileAction } from "./FileManagerLib"; import { FileAction, type FileActionHandler } from "./FileManagerLib";
let dispatch = createEventDispatcher()
export let nav: FSNavigator let {
export let show_hidden = false nav,
export let large_icons = false file_event,
show_hidden = false,
large_icons = false
}: {
nav: FSNavigator
file_event: FileActionHandler
show_hidden?: boolean
large_icons?: boolean
} = $props();
</script> </script>
<div class="gallery"> <div class="gallery">
{#each $nav.children as child, index (child.path)} {#each $nav.children as child, index (child.path)}
<a class="file" <a class="file"
href={"/d"+fs_encode_path(child.path)} href={"/d"+fs_encode_path(child.path)}
on:click={e => dispatch("file", {index: index, action: FileAction.Click, original: e})} onclick={e => file_event(FileAction.Click, index, e)}
on:contextmenu={e => dispatch("file", {index: index, action: FileAction.Context, original: e})} oncontextmenu={e => file_event(FileAction.Context, index, e)}
class:selected={child.fm_selected} class:selected={child.fm_selected}
class:hidden={child.name.startsWith(".") && !show_hidden} class:hidden={child.name.startsWith(".") && !show_hidden}
class:large_icons class:large_icons
@@ -46,15 +52,15 @@ export let large_icons = false
width: 150px; width: 150px;
height: 150px; height: 150px;
overflow: hidden; overflow: hidden;
border-radius: 8px; background: var(--body_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 +93,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,32 +1,41 @@
<script lang="ts"> <script lang="ts">
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 } from "lib/FilesystemAPI.svelte"
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, type FileActionHandler } from "./FileManagerLib";
let dispatch = createEventDispatcher() let {
nav,
export let nav: FSNavigator file_event,
export let show_hidden = false show_hidden = false,
export let large_icons = false large_icons = false,
export let hide_edit = false hide_edit = false,
export let hide_branding = false hide_branding = false
}: {
nav: FSNavigator
file_event: FileActionHandler
show_hidden?: boolean
large_icons?: boolean
hide_edit?: boolean
hide_branding?: boolean
} = $props();
</script> </script>
<div class="directory"> <table class="directory">
<thead>
<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>
</thead>
<tbody>
{#each $nav.children as child, index (child.path)} {#each $nav.children as child, index (child.path)}
<a <tr
href={"/d"+fs_encode_path(child.path)} onclick={e => file_event(FileAction.Click, index, e)}
on:click={e => dispatch("file", {index: index, action: FileAction.Click, original: e})} oncontextmenu={e => file_event(FileAction.Context, index, e)}
on:contextmenu={e => dispatch("file", {index: index, action: FileAction.Context, original: e})}
class="node" class="node"
class:node_selected={child.fm_selected} class:node_selected={child.fm_selected}
class:hidden={child.name.startsWith(".") && !show_hidden} class:hidden={child.name.startsWith(".") && !show_hidden}
@@ -35,7 +44,9 @@ export let hide_branding = false
<img src={fs_node_icon(child, 64, 64)} class="node_icon" class:large_icons alt="icon"/> <img src={fs_node_icon(child, 64, 64)} class="node_icon" class:large_icons alt="icon"/>
</td> </td>
<td class="node_name"> <td class="node_name">
<a class="title_link" href={"/d"+fs_encode_path(child.path)}>
{child.name} {child.name}
</a>
</td> </td>
<td class="node_size hide_small"> <td class="node_size hide_small">
{#if child.type === "file"} {#if child.type === "file"}
@@ -46,44 +57,40 @@ export let hide_branding = false
<div class="icons_wrap"> <div class="icons_wrap">
{#if child.abuse_type !== undefined} {#if child.abuse_type !== undefined}
<i class="icon" title="This file / directory has received an abuse report. It cannot be shared">block</i> <i class="icon" title="This file / directory has received an abuse report. It cannot be shared">block</i>
{:else if node_is_shared(child)} {:else if child.is_shared()}
<a <a
href="/d/{child.id}" href="/d/{child.id}"
on:click={e => dispatch("file", {index: index, action: FileAction.Share, original: e})} onclick={e => file_event(FileAction.Share, index, e)}
class="button action_button" class="button action_button"
> >
<i class="icon" title="This file / directory is shared. Click to open public link">share</i> <i class="icon" title="This file / directory is shared. Click to open public link">share</i>
</a> </a>
{/if} {/if}
{#if child.properties !== undefined && child.properties.branding_enabled === "true" && !hide_branding} {#if child.properties !== undefined && child.properties.branding_enabled === "true" && !hide_branding}
<button class="action_button" on:click={e => dispatch("file", {index: index, action: FileAction.Branding, original: e})}> <button class="action_button" onclick={e => file_event(FileAction.Branding, index, e)}>
<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" onclick={e => file_event(FileAction.Menu, index, 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> </tr>
{/each} {/each}
</div> </tbody>
</table>
<style> <style>
.directory { .directory {
display: table; display: table;
margin: 8px auto 16px auto; background: var(--body_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;
@@ -91,11 +98,14 @@ export let hide_branding = false
.directory > * > * { .directory > * > * {
display: table-cell; display: table-cell;
} }
td {
padding: 0;
}
.node { .node {
display: table-row; display: table-row;
text-decoration: none; text-decoration: none;
color: var(--body_text-color); color: var(--body_text-color);
padding: 6px; cursor: pointer;
} }
.node:not(:last-child) { .node:not(:last-child) {
border-bottom: 1px solid var(--separator); border-bottom: 1px solid var(--separator);
@@ -110,13 +120,14 @@ export let hide_branding = false
color: var(--highlight_text_color); color: var(--highlight_text_color);
} }
td { td {
padding: 2px;
vertical-align: middle; vertical-align: middle;
} }
.node_icon { .node_icon {
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 {
@@ -124,6 +135,10 @@ td {
line-height: 1.2em; line-height: 1.2em;
word-break: break-all; word-break: break-all;
} }
.title_link {
text-decoration: none;
color: var(--body_text_color);
}
.node_size { .node_size {
min-width: 5em; min-width: 5em;
white-space: nowrap; white-space: nowrap;

View File

@@ -1,18 +1,21 @@
<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.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 let { nav }: {
nav: FSNavigator;
} = $props();
let search_bar: HTMLInputElement let search_bar: HTMLInputElement = $state()
let error = "" let error = $state("")
let search_term = "" let search_term = $state("")
let search_results: string[] = [] let search_results: string[] = $state([])
let selected_result = 0 let selected_result = $state(0)
let searching = false let searching = false
let last_searched_term = "" let last_searched_term = ""
let last_limit = 10 let last_limit = $state(10)
onMount(() => { onMount(() => {
// Clear results when the user moves to a new directory // Clear results when the user moves to a new directory
@@ -43,9 +46,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 +57,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 +66,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
@@ -112,13 +116,17 @@ const input_keyup = (e: KeyboardEvent) => {
} }
// Submitting opens the selected result // Submitting opens the selected result
const submit_search = () => { const submit_search = (e: Event) => {
e.preventDefault()
if (search_results.length !== 0) { if (search_results.length !== 0) {
open_result(selected_result) open_result(selected_result)
} }
} }
const open_result = (index: number) => { const open_result = (index: number, e?: MouseEvent) => {
if (e !== undefined) {
e.preventDefault()
}
nav.navigate(search_results[index], true) nav.navigate(search_results[index], true)
clear_search(false) clear_search(false)
} }
@@ -142,7 +150,7 @@ const window_keydown = (e: KeyboardEvent) => {
} }
</script> </script>
<svelte:window on:keydown={window_keydown} /> <svelte:window onkeydown={window_keydown} />
{#if error === "path_not_found" || error === "node_is_a_directory"} {#if error === "path_not_found" || error === "node_is_a_directory"}
<div class="highlight_yellow center"> <div class="highlight_yellow center">
@@ -158,7 +166,7 @@ const window_keydown = (e: KeyboardEvent) => {
{/if} {/if}
<div class="center"> <div class="center">
<form class="search_form" on:submit|preventDefault={submit_search}> <form class="search_form" onsubmit={submit_search}>
<i class="icon">search</i> <i class="icon">search</i>
<input <input
bind:this={search_bar} bind:this={search_bar}
@@ -167,12 +175,12 @@ const window_keydown = (e: KeyboardEvent) => {
placeholder="Press / to search in {$nav.base.name}" placeholder="Press / to search in {$nav.base.name}"
style="width: 100%;" style="width: 100%;"
bind:value={search_term} bind:value={search_term}
on:keydown={input_keydown} onkeydown={input_keydown}
on:keyup={input_keyup} onkeyup={input_keyup}
/> />
{#if search_term !== ""} {#if search_term !== ""}
<!-- Button needs to be of button type in order to not submit the form --> <!-- Button needs to be of button type in order to not submit the form -->
<button on:click={() => clear_search(false)} type="button"> <button onclick={() => clear_search(false)} type="button">
<i class="icon">close</i> <i class="icon">close</i>
</button> </button>
{/if} {/if}
@@ -186,7 +194,7 @@ const window_keydown = (e: KeyboardEvent) => {
{#each search_results as result, index} {#each search_results as result, index}
<a <a
href={"/d"+fs_encode_path(result)} href={"/d"+fs_encode_path(result)}
on:click|preventDefault={() => open_result(index)} onclick={(e) => open_result(index, e)}
class="node" class="node"
class:node_selected={selected_result === index} class:node_selected={selected_result === index}
> >
@@ -201,7 +209,7 @@ const window_keydown = (e: KeyboardEvent) => {
{#if search_results.length === last_limit} {#if search_results.length === last_limit}
<div class="node"> <div class="node">
<div class="node_name" style="text-align: center;"> <div class="node_name" style="text-align: center;">
<button on:click={() => {search(last_limit + 100)}}> <button onclick={() => {search(last_limit + 100)}}>
<i class="icon">expand_more</i> <i class="icon">expand_more</i>
More results More results
</button> </button>
@@ -218,7 +226,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

@@ -1,4 +1,4 @@
<script lang="ts" context="module"> <script lang="ts" module>
export type UploadJob = { export type UploadJob = {
task_id: number, task_id: number,
file: File, file: File,
@@ -11,15 +11,14 @@ 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 let { nav }: {
nav: FSNavigator;
} = $props();
const max_concurrent_uploads = 5 let file_input_field: HTMLInputElement = $state()
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
upload_files((e.target as HTMLInputElement).files) upload_files((e.target as HTMLInputElement).files)
@@ -31,8 +30,8 @@ export const pick_files = () => {
file_input_field.click() file_input_field.click()
} }
let visible = false let visible = $state(false)
let upload_queue: UploadJob[] = []; let upload_queue: UploadJob[] = $state([]);
let task_id_counter = 0 let task_id_counter = 0
export const upload_files = async (files: File[]|FileList) => { export const upload_files = async (files: File[]|FileList) => {
@@ -79,8 +78,8 @@ export const upload_file = async (file: File) => {
// each upload progress bar will have bound itself to its array item // each upload progress bar will have bound itself to its array item
upload_queue = upload_queue upload_queue = upload_queue
if (active_uploads === 0 && state !== "uploading") { if (active_uploads === 0 && status !== "uploading") {
state = "uploading" status = "uploading"
visible = true visible = true
await tick() await tick()
await start_upload() await start_upload()
@@ -88,30 +87,37 @@ export const upload_file = async (file: File) => {
} }
let active_uploads = 0 let active_uploads = 0
let state = "idle" let status = $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
}
} }
} }
if (active_uploads === 0) { if (active_uploads === 0) {
state = "finished" status = "finished"
nav.reload() nav.reload()
// Empty the queue to free any references to lingering components // Empty the queue to free any references to lingering components
@@ -119,12 +125,12 @@ const start_upload = async () => {
// In ten seconds we close the popup // In ten seconds we close the popup
setTimeout(() => { setTimeout(() => {
if (state === "finished") { if (status === "finished") {
visible = false visible = false
} }
}, 10000) }, 10000)
} else { } else {
state = "uploading" status = "uploading"
} }
} }
@@ -135,7 +141,7 @@ const finish_upload = () => {
} }
const leave_confirmation = (e: BeforeUnloadEvent) => { const leave_confirmation = (e: BeforeUnloadEvent) => {
if (state === "uploading") { if (status === "uploading") {
e.preventDefault() e.preventDefault()
return "If you close this page your files will stop uploading. Do you want to continue?" return "If you close this page your files will stop uploading. Do you want to continue?"
} else { } else {
@@ -144,29 +150,29 @@ const leave_confirmation = (e: BeforeUnloadEvent) => {
} }
</script> </script>
<svelte:window on:beforeunload={leave_confirmation} /> <svelte:window onbeforeunload={leave_confirmation} />
<input <input
bind:this={file_input_field} bind:this={file_input_field}
on:change={file_input_change} onchange={file_input_change}
class="upload_input" type="file" name="file" multiple class="upload_input" type="file" name="file" multiple
/> />
{#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 status === "idle"}
Waiting for files Waiting for files
{:else if state === "uploading"} {:else if status === "uploading"}
Uploading files... Uploading files...
{:else if state === "finished"} {:else if status === "finished"}
Done Done
{/if} {/if}
</div> </div>
<div class="body"> <div class="body">
{#each upload_queue as job} {#each upload_queue as job}
{#if job.status !== "finished"} {#if job.status !== "finished"}
<UploadProgress bind:this={job.component} job={job} on:finished={finish_upload}/> <UploadProgress bind:this={job.component} job={job} finish={finish_upload}/>
{/if} {/if}
{/each} {/each}
</div> </div>
@@ -184,7 +190,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.svelte"
// code and an error message // code and an error message
export const upload_file = ( export const upload_file = (

View File

@@ -1,17 +1,23 @@
<script lang="ts"> <script lang="ts">
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"
import type { UploadJob } from "./FSUploadWidget.svelte"; import type { UploadJob } from "./FSUploadWidget.svelte";
let dispatch = createEventDispatcher() let {
export let job: UploadJob job = $bindable(),
export let total = 0 total = $bindable(0),
export let loaded = 0 loaded = $bindable(0),
let error_code = "" finish,
let error_message = "" }: {
job: UploadJob
total?: number
loaded?: number
finish: () => void
} = $props();
let error_code = $state("")
let error_message = $state("")
let xhr: XMLHttpRequest = null let xhr: XMLHttpRequest = null
export const start = () => { export const start = () => {
@@ -24,7 +30,7 @@ export const start = () => {
}, },
async () => { async () => {
job.status = "finished" job.status = "finished"
dispatch("finished") finish()
}, },
(code, message) => { (code, message) => {
if (job.status === "finished") { if (job.status === "finished") {
@@ -52,18 +58,18 @@ const cancel = () => {
} }
xhr = null xhr = null
dispatch("finished") finish()
} }
</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,10 +1,18 @@
<script lang="ts"> <script lang="ts">
import { preventDefault } from 'svelte/legacy';
import type { FSNavigator } from "filesystem/FSNavigator"; import type { FSNavigator } from "filesystem/FSNavigator";
export let nav: FSNavigator let {
export let path = "" nav,
path = "",
children
}: {
nav: FSNavigator;
path?: string;
children?: import('svelte').Snippet;
} = $props();
</script> </script>
<a href={"/d"+path} on:click|preventDefault={() => {nav.navigate(path, true)}}> <a href={"/d"+path} onclick={preventDefault(() => {nav.navigate(path, true)})}>
<slot></slot> {@render children?.()}
</a> </a>

View File

@@ -1,15 +1,19 @@
<script lang="ts"> <script lang="ts">
import { preventDefault } from 'svelte/legacy';
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.svelte"
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';
export let nav: FSNavigator let { nav, children }: {
let player: HTMLAudioElement nav: FSNavigator;
let playing = false children?: import('svelte').Snippet;
} = $props();
let player: HTMLAudioElement = $state()
let playing = $state(false)
let media_session = false let media_session = false
let siblings = [] let siblings = $state([])
export const toggle_playback = () => playing ? player.pause() : player.play() export const toggle_playback = () => playing ? player.pause() : player.play()
export const toggle_mute = () => player.muted = !player.muted export const toggle_mute = () => player.muted = !player.muted
@@ -48,9 +52,7 @@ onMount(() => {
}) })
</script> </script>
<slot></slot> {@render children?.()}
<FileTitle title={$nav.base.name}/>
<TextBlock width="1000px"> <TextBlock width="1000px">
<audio <audio
@@ -59,30 +61,30 @@ onMount(() => {
src={fs_path_url($nav.base.path)} src={fs_path_url($nav.base.path)}
autoplay autoplay
controls controls
on:pause={() => playing = false } onpause={() => playing = false}
on:play={() => playing = true } onplay={() => playing = true}
on:ended={() => nav.open_sibling(1) }> onended={() => nav.open_sibling(1)}>
<track kind="captions"/> <track kind="captions"/>
</audio> </audio>
<div style="text-align: center;"> <div style="text-align: center;">
<button on:click={() => nav.open_sibling(-1) }><i class="icon">skip_previous</i></button> <button onclick={() => nav.open_sibling(-1)}><i class="icon">skip_previous</i></button>
<button on:click={() => seek(-10) }><i class="icon">replay_10</i></button> <button onclick={() => seek(-10)}><i class="icon">replay_10</i></button>
<button on:click={toggle_playback}> <button onclick={toggle_playback}>
{#if playing} {#if playing}
<i class="icon">pause</i> <i class="icon">pause</i>
{:else} {:else}
<i class="icon">play_arrow</i> <i class="icon">play_arrow</i>
{/if} {/if}
</button> </button>
<button on:click={() => seek(10) }><i class="icon">forward_10</i></button> <button onclick={() => seek(10)}><i class="icon">forward_10</i></button>
<button on:click={() => nav.open_sibling(1) }><i class="icon">skip_next</i></button> <button onclick={() => nav.open_sibling(1)}><i class="icon">skip_next</i></button>
</div> </div>
<h2>Tracklist</h2> <h2>Tracklist</h2>
{#each siblings as sibling (sibling.path)} {#each siblings as sibling (sibling.path)}
<a <a
href={"/d"+fs_encode_path(sibling.path)} href={"/d"+fs_encode_path(sibling.path)}
on:click|preventDefault={() => nav.navigate(sibling.path, true)} onclick={preventDefault(() => nav.navigate(sibling.path, true))}
class="node" class="node"
> >
{#if sibling.path === $nav.base.path} {#if sibling.path === $nav.base.path}

View File

@@ -1,10 +1,12 @@
<script lang="ts"> <script lang="ts">
export let path = [] import type { FSNode } from 'lib/FilesystemAPI.svelte';
import { run } from 'svelte/legacy';
let image_uri: string let { path = [] }: {path: FSNode[]} = $props();
let image_link: string
$: update_links(path) let image_uri: string = $state()
const update_links = (path) => { let image_link: string = $state()
const update_links = (path: FSNode[]) => {
image_uri = null image_uri = null
image_link = null image_link = null
for (let node of path) { for (let node of path) {
@@ -18,6 +20,9 @@ const update_links = (path) => {
} }
} }
} }
run(() => {
update_links(path)
});
</script> </script>
{#if image_uri} {#if image_uri}

View File

@@ -1,36 +1,40 @@
<script lang="ts"> <script lang="ts">
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, FSNode } from "lib/FilesystemAPI.svelte";
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";
let dispatch = createEventDispatcher() let {
node,
export let nav: FSNavigator open_details,
children,
}: {
node: FSNode
open_details: () => void
children?: import('svelte').Snippet
} = $props();
</script> </script>
<slot></slot> {@render children?.()}
<h1>{$nav.base.name}</h1> <h1>{node.name}</h1>
<IconBlock icon_href={fs_thumbnail_url($nav.base.path, 256, 256)}> <IconBlock icon_href={fs_thumbnail_url(node.path, 256, 256)}>
Type: {$nav.base.file_type}<br/> Type: {node.file_type}<br/>
Size: {formatDataVolume($nav.base.file_size, 3)}<br/> Size: {formatDataVolume(node.file_size, 3)}<br/>
Upload date: {formatDate($nav.base.created, true, true, false)} Upload date: {formatDate(node.created, true, true, false)}
<hr/> <hr/>
<button class="button_highlight" on:click={() => {dispatch("download")}}> <button class="button_highlight" onclick={() => node.download()}>
<i class="icon">download</i> <i class="icon">download</i>
<span>Download</span> <span>Download</span>
</button> </button>
<button on:click={() => {dispatch("details")}}> <button onclick={() => open_details()}>
<i class="icon">help</i> <i class="icon">help</i>
<span>Details</span> <span>Details</span>
</button> </button>
</IconBlock> </IconBlock>
{#if $nav.base.name === ".search_index.gz"} {#if node.name === ".search_index.gz"}
<TextBlock> <TextBlock>
<p> <p>
Congratulations! You have found the search index. One of the Congratulations! You have found the search index. One of the

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.svelte";
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";
@@ -15,13 +15,22 @@ import CustomBanner from "./CustomBanner.svelte";
import type { FSNavigator } from "filesystem/FSNavigator"; 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 DetailsWindow from "filesystem/DetailsWindow.svelte";
export let nav: FSNavigator let {
export let upload_widget: FsUploadWidget nav,
export let edit_window: EditWindow upload_widget,
edit_window,
details_window,
}: {
nav: FSNavigator
upload_widget: FsUploadWidget
edit_window: EditWindow
details_window: DetailsWindow
} = $props();
let viewer: any let viewer: any = $state()
let viewer_type = "" let viewer_type = $state("")
let last_path = "" let last_path = ""
onMount(() => nav.subscribe(state_update)) onMount(() => nav.subscribe(state_update))
@@ -74,7 +83,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}>
@@ -87,23 +96,23 @@ export const seek = (delta: number) => {
{:else if viewer_type === "image"} {:else if viewer_type === "image"}
<Image nav={nav} bind:this={viewer}/> <Image nav={nav} bind:this={viewer}/>
{:else if viewer_type === "video"} {:else if viewer_type === "video"}
<Video nav={nav} bind:this={viewer} on:open_sibling/> <Video node={$nav.base} bind:this={viewer} open_sibling={(d) => nav.open_sibling(d)}/>
{:else if viewer_type === "pdf"} {:else if viewer_type === "pdf"}
<Pdf nav={nav} bind:this={viewer}/> <Pdf node={$nav.base} bind:this={viewer}/>
{:else if viewer_type === "text"} {:else if viewer_type === "text"}
<Text nav={nav} bind:this={viewer}> <Text node={$nav.base} bind:this={viewer}>
<CustomBanner path={$nav.path}/> <CustomBanner path={$nav.path}/>
</Text> </Text>
{:else if viewer_type === "torrent"} {:else if viewer_type === "torrent"}
<Torrent nav={nav} bind:this={viewer} on:download> <Torrent node={$nav.base} bind:this={viewer}>
<CustomBanner path={$nav.path}/> <CustomBanner path={$nav.path}/>
</Torrent> </Torrent>
{:else if viewer_type === "zip"} {:else if viewer_type === "zip"}
<Zip nav={nav} bind:this={viewer} on:download> <Zip node={$nav.base} bind:this={viewer}>
<CustomBanner path={$nav.path}/> <CustomBanner path={$nav.path}/>
</Zip> </Zip>
{:else} {:else}
<File nav={nav} on:download on:details> <File node={$nav.base} open_details={() => details_window.toggle()}>
<CustomBanner path={$nav.path}/> <CustomBanner path={$nav.path}/>
</File> </File>
{/if} {/if}

View File

@@ -1,21 +1,22 @@
<script lang="ts"> <script lang="ts">
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.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 { nav }: {
nav: FSNavigator;
} = $props();
export let nav: FSNavigator let container: HTMLDivElement = $state()
let container: HTMLDivElement let zoom = $state(false)
let zoom = false
let x = 0, y = 0 let x = 0, y = 0
let dragging = false let dragging = false
let swipe_prev = true let swipe_prev = $state(true)
let swipe_next = true let swipe_next = $state(true)
export const update = async () => { export const update = async () => {
dispatch("loading", true) loading_start()
// Figure out if there are previous or next files. If not then we disable // Figure out if there are previous or next files. If not then we disable
// swiping controls in that direction // swiping controls in that direction
@@ -29,7 +30,7 @@ export const update = async () => {
} }
} }
const on_load = () => dispatch("loading", false) const on_load = () => loading_finish()
const mousedown = (e: MouseEvent) => { const mousedown = (e: MouseEvent) => {
if (!dragging && e.which === 1 && zoom) { if (!dragging && e.which === 1 && zoom) {
@@ -66,7 +67,7 @@ const mouseup = (e: MouseEvent) => {
} }
</script> </script>
<svelte:window on:mousemove={mousemove} on:mouseup={mouseup} /> <svelte:window onmousemove={mousemove} onmouseup={mouseup} />
<div <div
bind:this={container} bind:this={container}
@@ -80,12 +81,12 @@ const mouseup = (e: MouseEvent) => {
on_next: () => nav.open_sibling(1), on_next: () => nav.open_sibling(1),
}} }}
> >
<!-- svelte-ignore a11y-no-noninteractive-element-interactions --> <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<img <img
on:dblclick={() => {zoom = !zoom}} ondblclick={() => {zoom = !zoom}}
on:mousedown={mousedown} onmousedown={mousedown}
on:load={on_load} onload={on_load}
on:error={on_load} onerror={on_load}
class="image" class="image"
class:zoom class:zoom
src={fs_path_url($nav.base.path)} src={fs_path_url($nav.base.path)}

View File

@@ -1,13 +1,12 @@
<script lang="ts"> <script lang="ts">
import { fs_path_url } from "filesystem/FilesystemAPI"; import { fs_path_url, FSNode } from "lib/FilesystemAPI.svelte";
import type { FSNavigator } from "filesystem/FSNavigator";
export let nav: FSNavigator let { node }: { node: FSNode } = $props();
</script> </script>
<iframe <iframe
class="container" class="container"
src={"/res/misc/pdf-viewer/web/viewer.html?file="+fs_path_url($nav.base.path)} src={"/res/misc/pdf-viewer/web/viewer.html?file="+fs_path_url(node.path)}
title="PDF viewer"> title="PDF viewer">
</iframe> </iframe>

View File

@@ -1,31 +1,37 @@
<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.svelte";
import type { FSNavigator } from "filesystem/FSNavigator";
export let nav: FSNavigator let {
let text_type = "text" node,
children
}: {
node: FSNode
children?: import('svelte').Snippet;
} = $props();
let text_type = $state("text")
export const update = () => { export const update = () => {
console.debug("Loading text file", nav.base.name) console.debug("Loading text file", node.name)
if (nav.base.file_size > 1 << 21) { // File larger than 2 MiB if (node.file_size > 1 << 21) { // File larger than 2 MiB
text_pre.innerText = "File is too large to view online.\nPlease download and view it locally." text_pre.innerText = "File is too large to view online.\nPlease download and view it locally."
return return
} }
if ( if (
nav.base.file_type.startsWith("text/markdown") || node.file_type.startsWith("text/markdown") ||
nav.base.name.endsWith(".md") || node.name.endsWith(".md") ||
nav.base.name.endsWith(".markdown") node.name.endsWith(".markdown")
) { ) {
markdown(nav.base) markdown(node)
} else { } else {
text(nav.base) text(node)
} }
} }
let text_pre: HTMLPreElement let text_pre: HTMLPreElement = $state()
const text = async (file: FSNode) => { const text = async (file: FSNode) => {
text_type = "text" text_type = "text"
await tick() await tick()
@@ -42,7 +48,7 @@ const text = async (file: FSNode) => {
}) })
} }
let md_container: HTMLElement let md_container: HTMLElement = $state()
const markdown = async (file: FSNode) => { const markdown = async (file: FSNode) => {
text_type = "markdown" text_type = "markdown"
await tick() await tick()
@@ -61,7 +67,7 @@ const markdown = async (file: FSNode) => {
</script> </script>
<div class="container"> <div class="container">
<slot></slot> {@render children?.()}
{#if text_type === "markdown"} {#if text_type === "markdown"}
<section bind:this={md_container} class="md"> <section bind:this={md_container} class="md">

View File

@@ -1,4 +1,4 @@
<script lang="ts" context="module"> <script lang="ts" module>
export type TorrentInfo = { export type TorrentInfo = {
trackers: string[] trackers: string[]
comment: string, comment: string,
@@ -13,26 +13,29 @@ export type TorrentFile = {
}; };
</script> </script>
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from "svelte";
import Magnet from "icons/Magnet.svelte"; import Magnet from "icons/Magnet.svelte";
import { formatDate } from "util/Formatting" 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, FSNode } from "lib/FilesystemAPI.svelte";
import CopyButton from "layout/CopyButton.svelte"; import CopyButton from "layout/CopyButton.svelte";
import type { FSNavigator } from "filesystem/FSNavigator"; import { loading_finish, loading_start } from "lib/Loading";
let dispatch = createEventDispatcher() let {
node,
children
}: {
node: FSNode;
children?: import('svelte').Snippet;
} = $props();
export let nav: FSNavigator let status = $state("loading")
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(node.path)+"?torrent_info")
if (resp.status >= 400) { if (resp.status >= 400) {
let json = await resp.json() let json = await resp.json()
@@ -58,20 +61,20 @@ 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"
} }
} }
let torrent: TorrentInfo = {} as TorrentInfo let torrent: TorrentInfo = $state({} as TorrentInfo)
let magnet = "" let magnet = $state("")
</script> </script>
<slot></slot> {@render children?.()}
<h1>{$nav.base.name}</h1> <h1>{node.name}</h1>
<IconBlock icon_href={fs_node_icon($nav.base, 256, 256)}> <IconBlock icon_href={fs_node_icon(node, 256, 256)}>
{#if status === "finished"} {#if status === "finished"}
Created by: {torrent.created_by}<br/> Created by: {torrent.created_by}<br/>
Comment: {torrent.comment}<br/> Comment: {torrent.comment}<br/>
@@ -92,7 +95,7 @@ let magnet = ""
Torrent file could not be parsed. It may be corrupted. Torrent file could not be parsed. It may be corrupted.
</p> </p>
{/if} {/if}
<button on:click={() => {dispatch("download")}} class="button"> <button onclick={() => node.download()} class="button">
<i class="icon">download</i> <i class="icon">download</i>
<span>Download torrent file</span> <span>Download torrent file</span>
</button> </button>

View File

@@ -1,8 +1,11 @@
<script lang="ts"> <script lang="ts">
import TorrentItem from './TorrentItem.svelte';
import { formatDataVolume } from "util/Formatting"; import { formatDataVolume } from "util/Formatting";
import type { TorrentFile } from "./Torrent.svelte"; import type { TorrentFile } from "./Torrent.svelte";
export let item: TorrentFile = {} as TorrentFile let { item = {} as TorrentFile }: {
item?: TorrentFile;
} = $props();
</script> </script>
<ul class="list_open"> <ul class="list_open">
@@ -10,7 +13,7 @@ export let item: TorrentFile = {} as TorrentFile
<li class:list_closed={!child.children}> <li class:list_closed={!child.children}>
{name} ({formatDataVolume(child.size, 3)})<br/> {name} ({formatDataVolume(child.size, 3)})<br/>
{#if child.children} {#if child.children}
<svelte:self item={child}></svelte:self> <TorrentItem item={child}></TorrentItem>
{/if} {/if}
</li> </li>
{/each} {/each}

View File

@@ -1,38 +1,42 @@
<script lang="ts"> <script lang="ts">
import { onMount, createEventDispatcher, tick } from "svelte"; import { onMount, 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, FSNode } from "lib/FilesystemAPI.svelte";
import type { FSNavigator } from "filesystem/FSNavigator";
let dispatch = createEventDispatcher()
export let nav: FSNavigator let {
node,
open_sibling,
}: {
node: FSNode
open_sibling: (delta: number) => void
} = $props();
// Used to detect when the file path changes // Used to detect when the file path changes
let last_path = "" let last_path = ""
let loaded = false let loaded = $state(false)
let player: HTMLVideoElement let player: HTMLVideoElement = $state()
let playing = false let playing = $state(false)
let media_session = false let media_session = false
let loop = false let loop = $state(false)
export const update = async () => { export const update = async () => {
if (media_session) { if (media_session) {
navigator.mediaSession.metadata = new MediaMetadata({ navigator.mediaSession.metadata = new MediaMetadata({
title: nav.base.name, title: node.name,
artist: "pixeldrain", artist: "pixeldrain",
album: "unknown", album: "unknown",
}); });
console.debug("Updating media session") console.debug("Updating media session")
} }
loop = nav.base.name.includes(".loop.") loop = node.name.includes(".loop.")
// When the component receives a new ID the video track does not // When the component receives a new ID the video track does not
// automatically start playing the new video. So we use this little hack to // automatically start playing the new video. So we use this little hack to
// make sure that the video is unloaded and loaded when the ID changes // make sure that the video is unloaded and loaded when the ID changes
if (nav.base.path != last_path) { if (node.path != last_path) {
last_path = nav.base.path last_path = node.path
loaded = false loaded = false
await tick() await tick()
loaded = true loaded = true
@@ -49,8 +53,7 @@ export const toggle_fullscreen = () => {
} }
} }
export const seek = (delta: number) => {
export const seek = delta => {
// fastseek can be pretty imprecise, so we don't use it for small seeks // fastseek can be pretty imprecise, so we don't use it for small seeks
// below 5 seconds // below 5 seconds
if (player.fastSeek && delta > 5) { if (player.fastSeek && delta > 5) {
@@ -66,8 +69,8 @@ onMount(() => {
navigator.mediaSession.setActionHandler('play', () => player.play()); navigator.mediaSession.setActionHandler('play', () => player.play());
navigator.mediaSession.setActionHandler('pause', () => player.pause()); navigator.mediaSession.setActionHandler('pause', () => player.pause());
navigator.mediaSession.setActionHandler('stop', () => player.pause()); navigator.mediaSession.setActionHandler('stop', () => player.pause());
navigator.mediaSession.setActionHandler('previoustrack', () => dispatch("open_sibling", -1)); navigator.mediaSession.setActionHandler('previoustrack', () => open_sibling(-1));
navigator.mediaSession.setActionHandler('nexttrack', () => dispatch("open_sibling", 1)); navigator.mediaSession.setActionHandler('nexttrack', () => open_sibling(1));
} }
}) })
@@ -83,10 +86,9 @@ const video_keydown = (e: KeyboardEvent) => {
<div class="container"> <div class="container">
{#if {#if
$nav.base.file_type === "video/x-matroska" || node.file_type === "video/x-matroska" ||
$nav.base.file_type === "video/quicktime" || node.file_type === "video/quicktime" ||
$nav.base.file_type === "video/x-ms-asf" node.file_type === "video/x-ms-asf"}
}
<div class="compatibility_warning"> <div class="compatibility_warning">
This video file type is not compatible with every web This video file type is not compatible with every web
browser. If the video fails to play you can try downloading browser. If the video fails to play you can try downloading
@@ -96,7 +98,7 @@ const video_keydown = (e: KeyboardEvent) => {
<div class="player_and_controls"> <div class="player_and_controls">
<div class="player"> <div class="player">
{#if loaded} {#if loaded}
<!-- svelte-ignore a11y-media-has-caption --> <!-- svelte-ignore a11y_media_has_caption -->
<video <video
bind:this={player} bind:this={player}
controls controls
@@ -104,46 +106,46 @@ const video_keydown = (e: KeyboardEvent) => {
autoplay autoplay
loop={loop} loop={loop}
class="video" class="video"
on:pause={() => playing = false } onpause={() => playing = false}
on:play={() => playing = true } onplay={() => playing = true}
on:keydown={video_keydown} onkeydown={video_keydown}
use:video_position={() => $nav.base.sha256_sum.substring(0, 8)} use:video_position={() => node.sha256_sum.substring(0, 8)}
> >
<source src={fs_path_url($nav.base.path)} type={$nav.base.file_type} /> <source src={fs_path_url(node.path)} type={node.file_type} />
</video> </video>
{/if} {/if}
</div> </div>
<div class="controls"> <div class="controls">
<div class="spacer"></div> <div class="spacer"></div>
<button on:click={() => dispatch("open_sibling", -1) }> <button onclick={() => open_sibling(-1)}>
<i class="icon">skip_previous</i> <i class="icon">skip_previous</i>
</button> </button>
<button on:click={() => seek(-10)}> <button onclick={() => seek(-10)}>
<i class="icon">replay_10</i> <i class="icon">replay_10</i>
</button> </button>
<button on:click={toggle_playback} class="button_highlight"> <button onclick={toggle_playback} class="button_highlight">
{#if playing} {#if playing}
<i class="icon">pause</i> <i class="icon">pause</i>
{:else} {:else}
<i class="icon">play_arrow</i> <i class="icon">play_arrow</i>
{/if} {/if}
</button> </button>
<button on:click={() => seek(10)}> <button onclick={() => seek(10)}>
<i class="icon">forward_10</i> <i class="icon">forward_10</i>
</button> </button>
<button on:click={() => dispatch("open_sibling", 1) }> <button onclick={() => open_sibling(1)}>
<i class="icon">skip_next</i> <i class="icon">skip_next</i>
</button> </button>
<div style="width: 16px; height: 8px;"></div> <div style="width: 16px; height: 8px;"></div>
<button on:click={toggle_mute} class:button_red={player && player.muted}> <button onclick={toggle_mute} class:button_red={player && player.muted}>
{#if player && player.muted} {#if player && player.muted}
<i class="icon">volume_off</i> <i class="icon">volume_off</i>
{:else} {:else}
<i class="icon">volume_up</i> <i class="icon">volume_up</i>
{/if} {/if}
</button> </button>
<button on:click={toggle_fullscreen}> <button onclick={toggle_fullscreen}>
<i class="icon">fullscreen</i> <i class="icon">fullscreen</i>
</button> </button>
<div class="spacer"></div> <div class="spacer"></div>

View File

@@ -1,4 +1,4 @@
<script lang="ts" context="module"> <script lang="ts" module>
export type ZipEntry = { export type ZipEntry = {
size: number, size: number,
children?: {[index: string]: ZipEntry}, children?: {[index: string]: ZipEntry},
@@ -8,37 +8,40 @@ export type ZipEntry = {
}; };
</script> </script>
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from "svelte";
import { formatDataVolume, formatDate } from "util/Formatting" 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, FSNode } from "lib/FilesystemAPI.svelte";
import type { FSNavigator } from "filesystem/FSNavigator"; import { loading_finish, loading_start } from "lib/Loading";
let dispatch = createEventDispatcher() let {
node,
children,
}: {
node: FSNode;
children?: import('svelte').Snippet;
} = $props();
export let nav: FSNavigator let status = $state("loading")
let status = "loading" let zip: ZipEntry = $state({size: 0} as ZipEntry)
let zip: ZipEntry = {size: 0} as ZipEntry
let uncomp_size = 0 let uncomp_size = 0
let comp_ratio = 0 let comp_ratio = $state(0)
let archive_type = "" let archive_type = $state("")
let truncated = false let truncated = $state(false)
export const update = async () => { export const update = async () => {
if (nav.base.file_type === "application/x-7z-compressed") { if (node.file_type === "application/x-7z-compressed") {
archive_type = "7z" archive_type = "7z"
} else { } else {
archive_type = "" archive_type = ""
} }
try { try {
loading_start()
status = "loading" status = "loading"
nav.set_loading(true) let resp = await fetch(fs_path_url(node.path)+"?zip_info")
let resp = await fetch(fs_path_url(nav.base.path)+"?zip_info")
if (resp.status >= 400) { if (resp.status >= 400) {
status = "parse_failed" status = "parse_failed"
@@ -52,19 +55,19 @@ export const update = async () => {
if (zip.properties !== undefined) { if (zip.properties !== undefined) {
if (zip.properties.includes("read_individual_files")) { if (zip.properties.includes("read_individual_files")) {
// Set the download URL for each file in the zip // Set the download URL for each file in the zip
recursive_set_url(fs_path_url(nav.base.path)+"?zip_file=", zip) recursive_set_url(fs_path_url(node.path)+"?zip_file=", zip)
} }
truncated = zip.properties.includes("truncated") truncated = zip.properties.includes("truncated")
} }
uncomp_size = recursive_size(zip) uncomp_size = recursive_size(zip)
comp_ratio = (uncomp_size / nav.base.file_size) comp_ratio = (uncomp_size / node.file_size)
status = "finished" status = "finished"
} catch (err) { } catch (err) {
console.error(err) console.error(err)
status = "parse_failed" status = "parse_failed"
} finally { } finally {
nav.set_loading(false) loading_finish()
} }
} }
@@ -92,24 +95,24 @@ const recursive_size = (file: ZipEntry) => {
} }
</script> </script>
<slot></slot> {@render children?.()}
<h1>{$nav.base.name}</h1> <h1>{node.name}</h1>
<IconBlock icon_href={fs_node_icon($nav.base, 256, 256)}> <IconBlock icon_href={fs_node_icon(node, 256, 256)}>
{#if archive_type === "7z"} {#if archive_type === "7z"}
This is a 7-zip archive. You will need This is a 7-zip archive. You will need
<a href="https://www.7-zip.org/">7-zip</a> or compatible software to <a href="https://www.7-zip.org/">7-zip</a> or compatible software to
extract it<br/> extract it<br/>
{/if} {/if}
Compressed size: {formatDataVolume($nav.base.file_size, 3)}<br/> Compressed size: {formatDataVolume(node.file_size, 3)}<br/>
{#if !truncated} {#if !truncated}
Uncompressed size: {formatDataVolume(zip.size, 3)} (Ratio: {comp_ratio.toFixed(2)}x)<br/> Uncompressed size: {formatDataVolume(zip.size, 3)} (Ratio: {comp_ratio.toFixed(2)}x)<br/>
{/if} {/if}
Uploaded on: {formatDate($nav.base.created, true, true, true)} Uploaded on: {formatDate(node.created, true, true, true)}
<br/> <br/>
<button class="button_highlight" on:click={() => {dispatch("download")}}> <button class="button_highlight" onclick={() => node.download()}>
<i class="icon">download</i> <i class="icon">download</i>
<span>Download</span> <span>Download</span>
</button> </button>

View File

@@ -1,8 +1,11 @@
<script lang="ts"> <script lang="ts">
import ZipItem from './ZipItem.svelte';
import type { ZipEntry } from "filesystem/viewers/Zip.svelte"; import type { ZipEntry } from "filesystem/viewers/Zip.svelte";
import { formatDataVolume } from "util/Formatting"; import { formatDataVolume } from "util/Formatting";
export let item: ZipEntry = {} as ZipEntry let { item = {} as ZipEntry }: {
item?: ZipEntry;
} = $props();
</script> </script>
<!-- First get directories and render them as details collapsibles --> <!-- First get directories and render them as details collapsibles -->
@@ -23,7 +26,7 @@ export let item: ZipEntry = {} as ZipEntry
<!-- Performance optimization, only render children if details is expanded --> <!-- Performance optimization, only render children if details is expanded -->
{#if child.details_open} {#if child.details_open}
<svelte:self item={child}></svelte:self> <ZipItem item={child}></ZipItem>
{/if} {/if}
</details> </details>
{/if} {/if}

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

@@ -3,7 +3,7 @@ import { onMount } from "svelte";
import Expandable from "util/Expandable.svelte"; import Expandable from "util/Expandable.svelte";
import { formatDate } from "util/Formatting"; import { formatDate } from "util/Formatting";
let result = null; let result = $state(null);
onMount(async () => { onMount(async () => {
try { try {
@@ -21,9 +21,11 @@ onMount(async () => {
{#if result !== null && result.user_banned} {#if result !== null && result.user_banned}
<section> <section>
<Expandable click_expand> <Expandable click_expand>
<div slot="header" class="header red"> {#snippet header()}
<div class="header red">
Your account has been banned, click for details Your account has been banned, click for details
</div> </div>
{/snippet}
<p> <p>
Your user account has been banned from uploading to Your user account has been banned from uploading to
pixeldrain due to violation of the pixeldrain due to violation of the
@@ -72,13 +74,16 @@ onMount(async () => {
{:else if result !== null && result.ip_offences.length > 0} {:else if result !== null && result.ip_offences.length > 0}
<section> <section>
<Expandable click_expand> <Expandable click_expand>
<div slot="header" class="header" class:red={result.ip_banned} class:yellow={!result.ip_banned}> {#snippet header()}
<div class="header" class:red={result.ip_banned} class:yellow={!result.ip_banned}>
{#if result.ip_banned} {#if result.ip_banned}
Your IP address has been banned, click for details Your IP address has been banned, click for details
{:else} {:else}
Your IP address has received a copyright strike, click for details Your IP address has received a copyright strike, click for details
{/if} {/if}
</div> </div>
{/snippet}
{#if result.ip_banned} {#if result.ip_banned}
<p> <p>
Your IP address ({result.address}) has been banned from Your IP address ({result.address}) has been banned from

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">
@@ -36,10 +36,12 @@ import OtherPlans from "./OtherPlans.svelte";
</div> </div>
<div class="feature_cell pro_feat"> <div class="feature_cell pro_feat">
<Tooltip> <Tooltip>
<span slot="label"> {#snippet label()}
<span>
<span class="bold">€4 / month</span> or <span class="bold">€4 / month</span> or
<span class="bold">€40 / year</span> <span class="bold">€40 / year</span>
</span> </span>
{/snippet}
The Pro subscription is managed by Patreon. Patreon's own fees The Pro subscription is managed by Patreon. Patreon's own fees
and sales tax will be added to this price. After paying you need and sales tax will be added to this price. After paying you need
to link your pixeldrain account to Patreon to activate the plan. to link your pixeldrain account to Patreon to activate the plan.
@@ -47,7 +49,9 @@ import OtherPlans from "./OtherPlans.svelte";
</div> </div>
<div class="feature_cell prepaid_feat"> <div class="feature_cell prepaid_feat">
<Tooltip> <Tooltip>
<span slot="label" class="bold">€1 / month minimum</span> {#snippet label()}
<span class="bold">€1 / month minimum</span>
{/snippet}
<p> <p>
The minimum fee is only charged when usage is less than €1. The minimum fee is only charged when usage is less than €1.
This calculation is per day, the €1 amount is divided by the This calculation is per day, the €1 amount is divided by the
@@ -62,7 +66,9 @@ import OtherPlans from "./OtherPlans.svelte";
</div> </div>
<div class="feature_cell free_feat"> <div class="feature_cell free_feat">
<Tooltip> <Tooltip>
<span slot="label" class="bold">6 GB per day</span> {#snippet label()}
<span class="bold">6 GB per day</span>
{/snippet}
<p> <p>
Free users are limited to downloading 6 GB per day, this Free users are limited to downloading 6 GB per day, this
limit is linked to your IP address, even if you are logged limit is linked to your IP address, even if you are logged
@@ -77,7 +83,9 @@ import OtherPlans from "./OtherPlans.svelte";
</div> </div>
<div class="feature_cell pro_feat"> <div class="feature_cell pro_feat">
<Tooltip> <Tooltip>
<span slot="label" class="bold">4 TB per 30 days</span> {#snippet label()}
<span class="bold">4 TB per 30 days</span>
{/snippet}
<p> <p>
The transfer limit is used for downloading, sharing and The transfer limit is used for downloading, sharing and
hotlinking files. hotlinking files.
@@ -89,7 +97,9 @@ import OtherPlans from "./OtherPlans.svelte";
</div> </div>
<div class="feature_cell prepaid_feat"> <div class="feature_cell prepaid_feat">
<Tooltip> <Tooltip>
<span slot="label" class="bold">€1 per TB transferred</span> {#snippet label()}
<span class="bold">€1 per TB transferred</span>
{/snippet}
<p> <p>
Prepaid does not have a transfer limit, instead you are Prepaid does not have a transfer limit, instead you are
charged for what you use at a rate of €1 per terabyte charged for what you use at a rate of €1 per terabyte
@@ -132,7 +142,9 @@ import OtherPlans from "./OtherPlans.svelte";
</div> </div>
<div class="feature_cell free_feat"> <div class="feature_cell free_feat">
<Tooltip> <Tooltip>
<span slot="label" class="bold">120 days (4 months)</span> {#snippet label()}
<span class="bold">120 days (4 months)</span>
{/snippet}
<p> <p>
Files expire when they have not been downloaded in the last Files expire when they have not been downloaded in the last
120 days. A download is counted when more than 10% of the 120 days. A download is counted when more than 10% of the
@@ -142,7 +154,9 @@ import OtherPlans from "./OtherPlans.svelte";
</div> </div>
<div class="feature_cell pro_feat"> <div class="feature_cell pro_feat">
<Tooltip> <Tooltip>
<span slot="label" class="bold">240 days (8 months)</span> {#snippet label()}
<span class="bold">240 days (8 months)</span>
{/snippet}
<p> <p>
The Pro plan has 240 day file expiry. The same rules apply The Pro plan has 240 day file expiry. The same rules apply
as the free plan. Higher Patreon subscription plans are as the free plan. Higher Patreon subscription plans are
@@ -152,7 +166,9 @@ import OtherPlans from "./OtherPlans.svelte";
</div> </div>
<div class="feature_cell prepaid_feat"> <div class="feature_cell prepaid_feat">
<Tooltip> <Tooltip>
<span slot="label" class="bold">Files do not expire</span> {#snippet label()}
<span class="bold">Files do not expire</span>
{/snippet}
<p> <p>
Files don't expire while your Prepaid plan is active. If Files don't expire while your Prepaid plan is active. If
your credit runs out you have one week to top up your your credit runs out you have one week to top up your
@@ -271,20 +287,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,7 +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 UploadWidget from "./UploadWidget.svelte"; import Pricing from "./Pricing.svelte";
let upload_widget let upload_widget
</script> </script>
@@ -51,6 +51,7 @@ let upload_widget
Bullet lists Bullet lists
</li> </li>
</ul> </ul>
<Pricing/>
</section> </section>
</div> </div>
@@ -142,26 +143,18 @@ let upload_widget
<FeatureTable/> <FeatureTable/>
</div> </div>
<Footer nobg/> <svelte:head>
<style>
<style> body {
:global(.page_body) { background-image: url("/res/img/catspaw.webp");
background-image: url("/res/img/inflating_star.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 {
margin-top: 16px;
margin-bottom: 16px;
}
@media (max-width: 1100px) {
.page_content {
margin-top: 0;
} }
} </style>
</svelte:head>
<style>
header { header {
padding-top: 20px; padding-top: 20px;
padding-bottom: 20px; padding-bottom: 20px;

View File

@@ -1,63 +0,0 @@
<script>
import Expandable from "util/Expandable.svelte";
</script>
<section>
<Expandable click_expand>
<div slot="header" class="header">
Important policy change! Click to expand
</div>
<p>
Sometime this summer pixeldrain will start requiring you to be
logged in to an account in order to upload new files. This is
necessary due to the large amount of regional blockings that have
been implemented against pixeldrain recently.
</p>
<p>
None of the countries that have blocked pixeldrain have specified a
valid reason, nor have I been able to contact them to ask what's
going on. But I can only guess that it has something to do with
abusive content being uploaded. Pixeldrain currently uses an IP
address banning system for restricting uploads from users who have
violated the content policy, but at the scale the site operates at
now that has proven to not be effective anymore. For that reason
pixeldrain will be switching to an account ban system.
</p>
<p>
The account ban system will restrict file uploads to your account
for a certain amount of time which depends on the type of the
offence. Serious violations might get your account banned for up to
a year, minor violations maybe a day or a week per file. It will
depend on the amount of abuse that slips through. Once the account
ban system is activated the IP ban system will be disabled.
</p>
<p>
Account banning is only effective if there is some system in place
to prevent the automated creation of accounts. There is already a
CAPTCHA in place on the registration page. I might start requiring
e-mail addresses for new accounts as well. The lack of an e-mail
address requirement has caused many issues with lost accounts as
well, so it had to happen anyway. I will also be looking into
integrating with OAuth providers (Google, Microsoft, Facebook,
Patreon, etc) to make the login flow simpler for newcomers.
</p>
<p>
After the site has been cleaned up it might take a long time before
all the regional blocking issues are resolved. Because of that I
will be adding alternative domain names for premium subscribers to
use. Most of the blocks are only on DNS level, which means they can
be circumvented by using a different domain name.
</p>
</Expandable>
</section>
<style>
.header {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
border-radius: 6px;
background-color: rgba(0, 0, 255, 0.2);
}
</style>

View File

@@ -1,27 +1,28 @@
<script> <script>
import { run } from 'svelte/legacy';
import { onMount } from "svelte"; import { onMount } from "svelte";
import Euro from "util/Euro.svelte"; import Euro from "util/Euro.svelte";
import ProgressBar from "util/ProgressBar.svelte"; import ProgressBar from "util/ProgressBar.svelte";
let pixeldrain_storage = 0 let pixeldrain_storage = $state(0)
let pixeldrain_egress = 0 let pixeldrain_egress = $state(0)
let pixeldrain_total = 0 let pixeldrain_total = $state(0)
let backblaze_storage = 0 let backblaze_storage = $state(0)
let backblaze_egress = 0 let backblaze_egress = $state(0)
let backblaze_api = 0 let backblaze_api = $state(0)
let backblaze_total = 0 let backblaze_total = $state(0)
let wasabi_storage = 0 let wasabi_storage = $state(0)
let wasabi_total = 0 let wasabi_total = $state(0)
let price_amazon = 0 let price_amazon = 0
let price_azure = 0 let price_azure = 0
let price_google = 0 let price_google = 0
let price_max = 0 let price_max = $state(0)
let storage = 10 // TB let storage = $state(10) // TB
let egress = 10 // TB let egress = $state(10) // TB
let avg_file_size = 1000 // kB let avg_file_size = $state(1000) // kB
$: { run(() => {
pixeldrain_storage = storage * 4 pixeldrain_storage = storage * 4
pixeldrain_egress = egress * 1 pixeldrain_egress = egress * 1
pixeldrain_total = pixeldrain_storage + pixeldrain_egress pixeldrain_total = pixeldrain_storage + pixeldrain_egress
@@ -43,7 +44,7 @@ $: {
// price_google = (storage * 20) + (egress * 20) // price_google = (storage * 20) + (egress * 20)
price_max = Math.max(pixeldrain_total, backblaze_total, wasabi_total, price_amazon, price_azure, price_google) price_max = Math.max(pixeldrain_total, backblaze_total, wasabi_total, price_amazon, price_azure, price_google)
} });
onMount(() => {}) onMount(() => {})
</script> </script>

View File

@@ -1,4 +1,8 @@
<script>export let style;</script> <script lang="ts">
let { style }: {
style: string;
} = $props();
</script>
<svg style={style} width="24" height="24" viewBox="0 -28.5 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid"> <svg style={style} width="24" height="24" viewBox="0 -28.5 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<g> <g>

View File

@@ -1,5 +0,0 @@
<script>export let style;</script>
<svg style={style} xmlns="http://www.w3.org/2000/svg" version="1.1" width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor" d="M5,3H19A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V5A2,2 0 0,1 5,3M18,5H15.5A3.5,3.5 0 0,0 12,8.5V11H10V14H12V21H15V14H18V11H15V9A1,1 0 0,1 16,8H18V5Z" />
</svg>

View File

@@ -1,4 +1,8 @@
<script>export let style;</script> <script lang="ts">
let { style = "" }: {
style: string;
} = $props();
</script>
<svg style={style} role="img" viewBox="0 0 24 24" width="24" height="24" xmlns="http://www.w3.org/2000/svg"> <svg style={style} role="img" viewBox="0 0 24 24" width="24" height="24" xmlns="http://www.w3.org/2000/svg">
<title>GitHub</title> <title>GitHub</title>

View File

@@ -1,79 +0,0 @@
<script>export let style;</script>
<svg
style={style}
width="24"
height="24"
viewBox="0 0 1024 1024"
version="1.1"
id="svg8"
enable-background="new"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-26.066658)"
style="display:inline">
<path
fill="currentColor"
stroke="currentColor"
id="path1087"
style="display:inline;opacity:1;fill:none;fill-opacity:1;stroke-width:28;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 716.85595,362.96478 c 15.29075,-21.36763 35.36198,-41.10921 56.50979,-53.31749 66.66377,-38.48393 137.02617,-33.22172 170.08018,22.43043 33.09493,55.72093 14.98656,117.48866 -47.64399,159.85496 -31.95554,19.26819 -62.93318,30.92309 -97.22892,35.54473 M 307.14407,362.96478 C 291.85332,341.59715 271.78209,321.85557 250.63429,309.64729 183.97051,271.16336 113.60811,276.42557 80.554051,332.07772 47.459131,387.79865 65.56752,449.56638 128.19809,491.93268 c 31.95554,19.26819 62.93319,30.92309 97.22893,35.54473"
inkscape:connector-curvature="0"
inkscape:label="ears"
sodipodi:insensitive="true" />
<path
fill="currentColor"
stroke="currentColor"
style="display:inline;opacity:1;fill:none;fill-opacity:1;stroke-width:28;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 801.23205,576.8699 C 812.73478,427.06971 720.58431,321.98291 511.99999,325.38859 303.41568,328.79426 213.71393,428.0311 222.76794,576.8699 c 8.64289,142.08048 176.80223,246.40388 288.12038,246.40388 111.31815,0 279.45076,-104.5447 290.34373,-246.40388 z"
id="path969"
inkscape:connector-curvature="0"
sodipodi:nodetypes="szszs"
inkscape:label="head"
sodipodi:insensitive="true" />
<path
fill="currentColor"
id="path1084"
style="display:inline;opacity:1;fill-opacity:1;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 610.4991,644.28932 c 0,23.11198 18.70595,41.84795 41.78091,41.84795 23.07495,0 41.7809,-18.73597 41.7809,-41.84795 0,-23.112 -18.70594,-41.84796 -41.7809,-41.84796 -23.07496,0 -41.78091,18.73596 -41.78091,41.84796 z m -280.56002,0 c 0,23.32492 18.87829,42.23352 42.16586,42.23352 23.28755,0 42.16585,-18.9086 42.16585,-42.23352 0,-23.32494 -18.87829,-42.23353 -42.16585,-42.23353 -23.28757,0 -42.16586,18.90859 -42.16586,42.23353 z"
inkscape:connector-curvature="0"
inkscape:label="eyes"
sodipodi:nodetypes="ssssssssss"
sodipodi:insensitive="true" />
<path
fill="currentColor"
id="path1008"
style="display:inline;opacity:1;fill:none;stroke-width:32;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 339.72919,769.2467 -54.54422,72.22481 m 399.08582,-72.22481 54.54423,72.22481 M 263.68341,697.82002 175.92752,733.64353 m 579.85765,-35.82351 87.7559,35.82351"
inkscape:connector-curvature="0"
inkscape:label="whiskers"
sodipodi:nodetypes="cccccccc"
sodipodi:insensitive="true" />
<path
fill="currentColor"
style="display:inline;opacity:1;fill-opacity:1;stroke-width:28;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;fill-rule:nonzero"
d="m 512.00082,713.08977 c -45.86417,0 -75.13006,31.84485 -74.14159,71.10084 1.07048,42.51275 32.46865,71.10323 74.14159,71.10323 41.67296,0 74.05118,-32.99608 74.14161,-71.10323 0.0932,-39.26839 -28.27742,-71.10084 -74.14161,-71.10084 z"
id="path1115"
inkscape:connector-curvature="0"
inkscape:label="nose"
sodipodi:nodetypes="zszsz"
sodipodi:insensitive="true" />
</g>
</svg>
<style>
svg, path {
fill: currentColor;
border: currentColor;
stroke: currentColor;
}
</style>

View File

@@ -1,4 +1,10 @@
<script>export let style = "";</script> <script lang="ts">
let {
style = ""
}: {
style?: string;
} = $props();
</script>
<svg style={style} xmlns="http://www.w3.org/2000/svg" version="1.1" width="24" height="24" viewBox="0 0 987.525 987.525"> <svg style={style} xmlns="http://www.w3.org/2000/svg" version="1.1" width="24" height="24" viewBox="0 0 987.525 987.525">
<path fill="currentColor" d="M132.138,855.425c43,43,93.2,76.301,149.3,99.101c54.1,21.899,111.1,33,169.6,33s115.601-11.101,169.601-33 <path fill="currentColor" d="M132.138,855.425c43,43,93.2,76.301,149.3,99.101c54.1,21.899,111.1,33,169.6,33s115.601-11.101,169.601-33

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