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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

1923
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-plugin-livereload": "^2.0.5",
"rollup-plugin-svelte": "^7.2.2",
"svelte": "^4.2.19"
"svelte": "^5.0.0"
},
"dependencies": {
"behave-js": "^1.5.0",

View File

@@ -11,12 +11,7 @@ const production = !process.env.ROLLUP_WATCH;
const builddir = "../res/static/svelte"
export default [
"filesystem",
"user_home",
"admin_panel",
"home_page",
"speedtest",
"login",
"wrap",
].map((name, index) => ({
input: `src/${name}.js`,
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,73 +1,77 @@
<script>
<script lang="ts">
import { formatDate, formatNumber } from "util/Formatting";
import Expandable from "util/Expandable.svelte";
import { createEventDispatcher } from "svelte";
let dispatch = createEventDispatcher()
export let report
export let ip_report_count
let preview = false
let {
report,
ip_report_count
} = $props();
let preview = $state(false)
$: can_grant = report.status !== "granted"
$: can_reject = report.status !== "rejected"
let can_grant = $derived(report.status !== "granted")
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})
}
</script>
<Expandable expanded={report.status === "pending"} click_expand>
<div slot="header" class="header">
<div class="icon_cell">
<img class="file_icon" src={"/api/file/"+report.file.id+"/thumbnail"} alt="File thumbnail"/>
</div>
<div class="title">
{report.file.name}
</div>
<div class="stats">
Type<br/>
{report.file.abuse_type === "" ? report.type : report.file.abuse_type}
</div>
{#if report.status !== "pending"}
<div class="stats">
Status<br/>
{report.status}
{#snippet header()}
<div class="header">
<div class="icon_cell">
<img class="file_icon" src={"/api/file/"+report.file.id+"/thumbnail"} alt="File thumbnail"/>
</div>
{/if}
<div class="stats">R<br/>{report.reports.length}</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>
<div class="title">
{report.file.name}
</div>
<div class="stats">
Type<br/>
{report.file.abuse_type === "" ? report.type : report.file.abuse_type}
</div>
{#if report.status !== "pending"}
<div class="stats">
Status<br/>
{report.status}
</div>
{/if}
<div class="stats">R<br/>{report.reports.length}</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>
{/snippet}
<div class="details">
<div class="toolbar">
<div class="action_list">
<a class="button" target="_blank" href={"/u/"+report.file.id} rel="noreferrer">
<i class="icon">open_in_new</i> Open file
</a>
<button class:button_highlight={preview} on:click={() => {preview = !preview}}>
<button class:button_highlight={preview} onclick={() => {preview = !preview}}>
<i class="icon">visibility</i> Preview
</button>
{#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})
</button>
{/if}
{#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
</button>
{/if}
</div>
<div class="type_list">
<button on:click={() => {set_status("grant", "copyright")}}>copyright</button>
<button on:click={() => {set_status("grant", "terrorism")}}>terrorism</button>
<button on:click={() => {set_status("grant", "gore")}}>gore</button>
<button on:click={() => {set_status("grant", "child_abuse")}}>child_abuse</button>
<button on:click={() => {set_status("grant", "zoophilia")}}>zoophilia</button>
<button on:click={() => {set_status("grant", "malware")}}>malware</button>
<button on:click={() => {set_status("grant", "doxing")}}>doxing</button>
<button on:click={() => {set_status("grant", "revenge_porn")}}>revenge_porn</button>
<button onclick={() => {set_status("grant", "copyright")}}>copyright</button>
<button onclick={() => {set_status("grant", "terrorism")}}>terrorism</button>
<button onclick={() => {set_status("grant", "gore")}}>gore</button>
<button onclick={() => {set_status("grant", "child_abuse")}}>child_abuse</button>
<button onclick={() => {set_status("grant", "zoophilia")}}>zoophilia</button>
<button onclick={() => {set_status("grant", "malware")}}>malware</button>
<button onclick={() => {set_status("grant", "doxing")}}>doxing</button>
<button onclick={() => {set_status("grant", "revenge_porn")}}>revenge_porn</button>
</div>
</div>
<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>
{#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
</button>
{/if}
{#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
</button>
{/if}

View File

@@ -1,18 +1,18 @@
<script>
import { onMount } from "svelte";
import LoadingIndicator from "util/LoadingIndicator.svelte";
import AbuseReport from "./AbuseReport.svelte";
import { loading_finish, loading_start } from "lib/Loading";
let loading = true
let reports = []
let reports = $state([])
let startPicker
let endPicker
let startPicker = $state()
let endPicker = $state()
let tab = "pending"
let tab = $state("pending")
const get_reports = async () => {
loading = true;
loading_start()
// Remove refresh timeout if there is one
clearTimeout(refresh_timeout)
@@ -70,11 +70,11 @@ const get_reports = async () => {
} catch (err) {
alert(err);
} finally {
loading = false;
loading_finish()
}
};
let ip_report_count = {}
let ip_report_count = $state({})
const count_ip_reports = () => {
ip_report_count = {}
reports.forEach(v => {
@@ -156,8 +156,6 @@ onMount(() => {
});
</script>
<LoadingIndicator loading={loading}/>
<section>
<div class="toolbar" style="text-align: left;">
<div>Reports: {reports.length}</div>
@@ -165,19 +163,19 @@ onMount(() => {
<div>Range:</div>
<input type="date" bind:this={startPicker}/>
<input type="date" bind:this={endPicker}/>
<button on:click={get_reports}>Go</button>
<button onclick={get_reports}>Go</button>
</div>
<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>
Pending
</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>
Granted
</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>
Rejected
</button>

View File

@@ -1,8 +1,8 @@
<script lang="ts">
import { onMount, tick } from "svelte";
import LoadingIndicator from "util/LoadingIndicator.svelte";
import EmailReportersTable from "./EmailReportersTable.svelte";
import { get_endpoint } from "lib/PixeldrainAPI";
import { loading_finish, loading_start } from "lib/Loading";
type Reporter = {
from_address: string,
@@ -17,29 +17,28 @@ type Reporter = {
last_message_html: string,
}
let loading = true
let reporters: Reporter[] = []
$: reporters_pending = reporters.reduce((acc, val) => {
let reporters: Reporter[] = $state([])
let reporters_pending = $derived(reporters.reduce((acc, val) => {
if (val.status === "pending") {
acc.push(val)
}
return acc
}, [])
$: reporters_trusted = reporters.reduce((acc, val) => {
}, []))
let reporters_trusted = $derived(reporters.reduce((acc, val) => {
if (val.status === "trusted") {
acc.push(val)
}
return acc
}, [])
$: reporters_rejected = reporters.reduce((acc, val) => {
}, []))
let reporters_rejected = $derived(reporters.reduce((acc, val) => {
if (val.status === "rejected") {
acc.push(val)
}
return acc
}, [])
}, []))
const get_reporters = async () => {
loading = true;
loading_start()
try {
const resp = await fetch(get_endpoint()+"/admin/abuse_reporter");
if(resp.status >= 400) {
@@ -49,17 +48,18 @@ const get_reporters = async () => {
} catch (err) {
alert(err);
} finally {
loading = false;
loading_finish()
}
};
let edit_button: HTMLButtonElement
let creating = false
let new_reporter_from_address: HTMLInputElement
let new_reporter_name: HTMLInputElement
let new_reporter_status = "trusted"
let edit_button: HTMLButtonElement = $state()
let creating = $state(false)
let new_reporter_from_address: HTMLInputElement = $state()
let new_reporter_name: HTMLInputElement = $state()
let new_reporter_status = $state("trusted")
const create_reporter = async () => {
const create_reporter = async (e: SubmitEvent) => {
e.preventDefault()
if (!new_reporter_from_address.value) {
alert("Please enter an e-mail address")
return
@@ -137,21 +137,19 @@ const delete_reporter = async (reporter: Reporter) => {
onMount(get_reporters);
</script>
<LoadingIndicator loading={loading}/>
<section>
<div class="toolbar" style="text-align: left;">
<div class="toolbar_spacer"></div>
<button on:click={() => get_reporters()}>
<button onclick={() => get_reporters()}>
<i class="icon">refresh</i>
</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
</button>
</div>
{#if creating}
<div class="highlight_shaded">
<form on:submit|preventDefault={create_reporter}>
<form onsubmit={create_reporter}>
<div class="form">
<label for="field_from_address">E-mail address</label>
<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 { formatDate } from "util/Formatting";
import Modal from "util/Modal.svelte"
@@ -7,13 +8,12 @@ import { flip } from "svelte/animate";
const dispatch = createEventDispatcher()
export let reporters = []
let { reporters = $bindable([]) } = $props();
$: update_table(reporters)
const update_table = (reporters) => sort("")
let sort_field = "last_used"
let asc = false
let sort_field = $state("last_used")
let asc = $state(false)
const sort = (field) => {
if (field !== "" && field === sort_field) asc = !asc
if (field === "") field = sort_field
@@ -40,16 +40,19 @@ const sort = (field) => {
reporters = reporters
}
let modal
let preview_subject = ""
let preview_html = ""
let preview_text = ""
let modal: Modal = $state()
let preview_subject = $state("")
let preview_html = $state("")
let preview_text = $state("")
const toggle_preview = (rep) => {
preview_subject = rep.last_message_subject
preview_text = rep.last_message_text
preview_html = rep.last_message_html
modal.show()
}
run(() => {
update_table(reporters)
});
</script>
<div class="table_scroll">
@@ -75,23 +78,23 @@ const toggle_preview = (rep) => {
<td>{formatDate(rep.last_used, true, true, false)}</td>
<td>{formatDate(rep.created, false, false, false)}</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>
</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>
</button>
{#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>
</button>
{/if}
{#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>
</button>
{/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>
</button>
</td>

View File

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

View File

@@ -1,8 +1,9 @@
<script>
import { preventDefault, stopPropagation } from 'svelte/legacy';
import { onMount } from "svelte";
import { formatDate } from "util/Formatting";
import Expandable from "util/Expandable.svelte";
import LoadingIndicator from "util/LoadingIndicator.svelte";
import { loading_finish } from "lib/Loading";
const abuse_types = [
"copyright",
@@ -15,17 +16,16 @@ const abuse_types = [
"revenge_porn",
]
let loading = true
let rows = []
let total_offences = 0
let rows = $state([])
let total_offences = $state(0)
let expanded = false
let creating = false
let new_ban_address
let new_ban_reason = abuse_types[0]
let expanded = $state(false)
let creating = $state(false)
let new_ban_address = $state()
let new_ban_reason = $state(abuse_types[0])
const get_bans = async () => {
loading = true;
loading_start()
try {
const resp = await fetch(window.api_endpoint+"/admin/ip_ban");
if(resp.status >= 400) {
@@ -39,7 +39,7 @@ const get_bans = async () => {
} catch (err) {
alert(err);
} finally {
loading = false;
loading_finish();
}
};
@@ -95,8 +95,6 @@ const delete_ban = async (addr) => {
onMount(get_bans);
</script>
<LoadingIndicator loading={loading}/>
<section>
<div class="toolbar">
<div class="toolbar_label">
@@ -106,20 +104,20 @@ onMount(get_bans);
Offences {total_offences}
</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}
<i class="icon">unfold_less</i> Collapse all
{:else}
<i class="icon">unfold_more</i> Expand all
{/if}
</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
</button>
</div>
{#if creating}
<div class="highlight_shaded">
<form on:submit|preventDefault={create_ban}>
<form onsubmit={preventDefault(create_ban)}>
<div class="form">
<label for="field_address">IP address</label>
<input id="field_address" type="text" bind:this={new_ban_address}/>
@@ -141,20 +139,22 @@ onMount(get_bans);
{#each rows as row (row.address)}
<Expandable expanded={expanded} click_expand>
<div slot="header" class="header">
<div class="title">{row.address}</div>
<div class="stats">
Offences<br/>
{row.offences.length}
{#snippet header()}
<div class="header">
<div class="title">{row.address}</div>
<div class="stats">
Offences<br/>
{row.offences.length}
</div>
<div class="stats">
Date<br/>
{formatDate(row.offences[0].ban_time, false, false, false)}
</div>
<button onclick={stopPropagation(() => {delete_ban(row.address)})} class="button button_red" style="align-self: center;">
<i class="icon">delete</i>
</button>
</div>
<div class="stats">
Date<br/>
{formatDate(row.offences[0].ban_time, false, false, false)}
</div>
<button on:click|stopPropagation={() => {delete_ban(row.address)}} class="button button_red" style="align-self: center;">
<i class="icon">delete</i>
</button>
</div>
{/snippet}
<div class="table_scroll">
<table>
<thead>

View File

@@ -1,17 +1,16 @@
<script lang="ts">
import { onMount } from "svelte";
import { formatDate } from "util/Formatting";
import LoadingIndicator from "util/LoadingIndicator.svelte";
import Expandable from "util/Expandable.svelte";
import SortableTable, { FieldType } from "layout/SortableTable.svelte";
import { country_name, get_admin_invoices, type Invoice } from "lib/AdminAPI";
import PayPalVat from "./PayPalVAT.svelte";
import { loading_finish, loading_start } from "lib/Loading";
let loading = true
let invoices: Invoice[] = []
let invoices: Invoice[] = $state([])
let year = 0
let month = 0
let year = $state(0)
let month = $state(0)
type Total = {
count: number
@@ -19,8 +18,8 @@ type Total = {
vat: number
fee: number
}
let totals_provider: { [id: string]: Total } = {}
let totals_country: { [id: string]: Total } = {}
let totals_provider: { [id: string]: Total } = $state({})
let totals_country: { [id: string]: Total } = $state({})
const add_total = (i: Invoice) => {
if (totals_provider[i.payment_method] === undefined) {
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 () => {
loading = true;
loading_start()
try {
const resp = await get_admin_invoices(year, month)
@@ -92,7 +91,7 @@ const get_invoices = async () => {
} catch (err) {
alert(err);
} finally {
loading = false;
loading_finish()
}
};
@@ -120,14 +119,14 @@ onMount(() => {
get_invoices()
})
let status_filter = {
let status_filter = $state({
canceled: {checked: false},
expired: {checked: false},
open: {checked: false},
paid: {checked: true},
}
let gateway_filter = {}
let method_filter = {}
})
let gateway_filter = $state({})
let method_filter = $state({})
const filter_invoices = () => {
records_hidden = 0
@@ -154,28 +153,28 @@ const filter_invoices = () => {
return false
})
}
let records_hidden = 0
let invoices_filtered: Invoice[] = []
let records_hidden = $state(0)
let invoices_filtered: Invoice[] = $state([])
</script>
<LoadingIndicator loading={loading}/>
<section>
<h3>{year + "-" + ("00"+(month)).slice(-2)}</h3>
<div class="toolbar">
<button on:click={last_month}>
<button onclick={last_month}>
<i class="icon">chevron_left</i>
Previous month
</button>
<div class="toolbar_spacer"></div>
<button on:click={next_month}>
<button onclick={next_month}>
Next month
<i class="icon">chevron_right</i>
</button>
</div>
<Expandable click_expand>
<div slot="header" class="header">Per payment processor</div>
{#snippet header()}
<div class="header">Per payment processor</div>
{/snippet}
<SortableTable
index_field="id"
rows={obj_to_list(totals_provider)}
@@ -191,7 +190,9 @@ let invoices_filtered: Invoice[] = []
</Expandable>
<Expandable click_expand>
<div slot="header" class="header">Per country</div>
{#snippet header()}
<div class="header">Per country</div>
{/snippet}
<SortableTable
index_field="id"
rows={obj_to_list(totals_country)}
@@ -207,7 +208,9 @@ let invoices_filtered: Invoice[] = []
</Expandable>
<Expandable click_expand>
<div slot="header" class="header">In European Union</div>
{#snippet header()}
<div class="header">In European Union</div>
{/snippet}
<SortableTable
index_field="id"
rows={obj_to_list_eu(totals_country)}
@@ -223,7 +226,9 @@ let invoices_filtered: Invoice[] = []
</Expandable>
<Expandable click_expand>
<div slot="header" class="header">PayPal VAT</div>
{#snippet header()}
<div class="header">PayPal VAT</div>
{/snippet}
<PayPalVat invoices={invoices}/>
</Expandable>
@@ -236,7 +241,7 @@ let invoices_filtered: Invoice[] = []
type="checkbox"
id="status_{filter}"
bind:checked={status_filter[filter].checked}
on:change={filter_invoices}>
onchange={filter_invoices}>
<label for="status_{filter}">{filter}</label>
<br/>
{/each}
@@ -248,7 +253,7 @@ let invoices_filtered: Invoice[] = []
type="checkbox"
id="gateway_{filter}"
bind:checked={gateway_filter[filter].checked}
on:change={filter_invoices}>
onchange={filter_invoices}>
<label for="gateway_{filter}">{filter}</label>
<br/>
{/each}
@@ -260,7 +265,7 @@ let invoices_filtered: Invoice[] = []
type="checkbox"
id="method_{filter}"
bind:checked={method_filter[filter].checked}
on:change={filter_invoices}>
onchange={filter_invoices}>
<label for="method_{filter}">{filter}</label>
<br/>
{/each}

View File

@@ -1,20 +1,19 @@
<script>
<script lang="ts">
import { onMount } from "svelte";
import { formatDate } from "util/Formatting";
import { mollie_proxy_call } from "./MollieAPI";
import LoadingIndicator from "util/LoadingIndicator.svelte";
import Euro from "util/Euro.svelte";
import { loading_finish, loading_start } from "lib/Loading";
export let settlement = {}
let loading = true
let payments = []
let { settlement = {} } = $props();
let payments = $state([])
let per_country = {}
let totals = {
let per_country = $state({})
let totals = $state({
count: 0,
vat: 0,
amount: 0,
}
})
const load_all_payments = async (settlement_id) => {
let payments = []
@@ -49,7 +48,7 @@ const load_all_payments = async (settlement_id) => {
}
const get_payments = async () => {
loading = true;
loading_start()
try {
payments = await load_all_payments(settlement.id)
@@ -84,15 +83,13 @@ const get_payments = async () => {
} catch (err) {
alert(err);
} finally {
loading = false;
loading_finish()
}
};
onMount(get_payments);
</script>
<LoadingIndicator loading={loading}/>
<h3>Accounting information</h3>
{#if per_country.NL}

View File

@@ -2,17 +2,16 @@
import { onMount } from "svelte";
import { formatDate } from "util/Formatting";
import Expandable from "util/Expandable.svelte";
import LoadingIndicator from "util/LoadingIndicator.svelte";
import Euro from "util/Euro.svelte";
import MollieSettlement from "./MollieSettlement.svelte";
import { mollie_proxy_call } from "./MollieAPI";
import { loading_finish, loading_start } from "lib/Loading";
let loading = true
let response = {}
let settlements = []
let settlements = $state([])
const get_settlements = async () => {
loading = true;
loading_start()
try {
const req = await mollie_proxy_call("settlements?limit=250");
if(req.status >= 400) {
@@ -23,33 +22,33 @@ const get_settlements = async () => {
} catch (err) {
alert(err);
} finally {
loading = false;
loading_finish()
}
};
onMount(get_settlements);
</script>
<LoadingIndicator loading={loading}/>
<section>
{#each settlements as row (row.id)}
<Expandable click_expand>
<div slot="header" class="header">
<div class="title">{row.id}</div>
<div class="stats">
Date<br/>
{formatDate(row.createdAt, false, false, false)}
{#snippet header()}
<div class="header">
<div class="title">{row.id}</div>
<div class="stats">
Date<br/>
{formatDate(row.createdAt, false, false, false)}
</div>
<div class="stats">
Amount<br/>
<Euro amount={row.amount.value*1e6}/>
</div>
<div class="stats">
Status<br/>
{row.status}
</div>
</div>
<div class="stats">
Amount<br/>
<Euro amount={row.amount.value*1e6}/>
</div>
<div class="stats">
Status<br/>
{row.status}
</div>
</div>
{/snippet}
<MollieSettlement settlement={row}/>
</Expandable>

View File

@@ -2,20 +2,19 @@
import { onMount } from "svelte";
import { formatDate } from "util/Formatting";
import Expandable from "util/Expandable.svelte";
import LoadingIndicator from "util/LoadingIndicator.svelte";
import Euro from "util/Euro.svelte";
import { loading_finish, loading_start } from "lib/Loading";
let loading = true
let response = {}
let payments = []
let payments = $state([])
let per_country = {}
let totals = {}
let per_country = $state({})
let totals = $state({})
let datePicker
let rangeMonths = 1
let startDate = 0
let endDate = 0
let datePicker = $state()
let rangeMonths = $state(1)
let startDate = $state(0)
let endDate = $state(0)
const get_payments = async () => {
if (!datePicker.valueAsDate) {
@@ -40,8 +39,8 @@ const get_payments = async () => {
fee: 0,
}
payments = []
loading = true;
loading_start()
try {
await get_page("https://api.mollie.com/v2/payments?limit=250")
@@ -76,7 +75,7 @@ const get_payments = async () => {
} catch (err) {
alert(err);
} finally {
loading = false;
loading_finish()
}
}
@@ -118,8 +117,6 @@ onMount(() => {
});
</script>
<LoadingIndicator loading={loading}/>
<section>
<div class="toolbar" style="text-align: left;">
<div>Payments: {payments.length}</div>
@@ -128,7 +125,7 @@ onMount(() => {
<input type="date" bind:this={datePicker}/>
<div>Months</div>
<input type="number" bind:value={rangeMonths}/>
<button on:click={get_payments}>Go</button>
<button onclick={get_payments}>Go</button>
</div>
<div>
@@ -230,29 +227,31 @@ onMount(() => {
<h2>Payments</h2>
{#each payments as row (row.id)}
<Expandable click_expand>
<div slot="header" class="header">
<div class="title">{row.id}</div>
<div class="stats">
Date<br/>
{formatDate(row.createdAt, false, false, false)}
{#snippet header()}
<div class="header">
<div class="title">{row.id}</div>
<div class="stats">
Date<br/>
{formatDate(row.createdAt, false, false, false)}
</div>
<div class="stats">
Total<br/>
<Euro amount={row.amount.value*1e6}/>
</div>
<div class="stats">
Amount<br/>
<Euro amount={row.metadata.amount}/>
</div>
<div class="stats">
VAT<br/>
<Euro amount={row.metadata.vat}/>
</div>
<div class="stats">
Status<br/>
{row.status}
</div>
</div>
<div class="stats">
Total<br/>
<Euro amount={row.amount.value*1e6}/>
</div>
<div class="stats">
Amount<br/>
<Euro amount={row.metadata.amount}/>
</div>
<div class="stats">
VAT<br/>
<Euro amount={row.metadata.vat}/>
</div>
<div class="stats">
Status<br/>
{row.status}
</div>
</div>
{/snippet}
<div>
Amount: <Euro amount={row.metadata.amount} /><br/>
VAT: <Euro amount={row.metadata.vat} /><br/>

View File

@@ -78,18 +78,20 @@ const update_countries = (invoices: Invoice[]) => {
{#if per_country["NL"] && totals}
<h2>Summary</h2>
<table style="width: auto;">
<tr>
<td>Total PayPal earnings -fees</td>
<td><Euro amount={totals.vat+totals.amount-totals.fee}/></td>
</tr>
<tr>
<td>Total VAT NL</td>
<td><Euro amount={per_country["NL"].vat}/></td>
</tr>
<tr>
<td>Total VAT OSS</td>
<td><Euro amount={totals.vat-per_country["NL"].vat}/></td>
</tr>
<tbody>
<tr>
<td>Total PayPal earnings -fees</td>
<td><Euro amount={totals.vat+totals.amount-totals.fee}/></td>
</tr>
<tr>
<td>Total VAT NL</td>
<td><Euro amount={per_country["NL"].vat}/></td>
</tr>
<tr>
<td>Total VAT OSS</td>
<td><Euro amount={totals.vat-per_country["NL"].vat}/></td>
</tr>
</tbody>
</table>
<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 { formatDataVolume } from "util/Formatting";
import SortButton from "layout/SortButton.svelte";
export let peers = [];
$: update_peers(peers)
let { peers = $bindable([]) } = $props();
let update_peers = (peers) => {
for (let peer of peers) {
peer.avg_network_total = peer.avg_network_tx + peer.avg_network_rx
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)
if (peer.network_ratio === NaN) {
peer.network_ratio = 1
}
}
sort("")
}
let sort_field = "hostname"
let asc = true
let sort_field = $state("hostname")
let asc = $state(true)
let sort = (field) => {
if (field !== "" && field === sort_field) {
asc = !asc
@@ -49,6 +46,9 @@ let sort = (field) => {
})
peers = peers
}
run(() => {
update_peers(peers)
});
</script>
<div class="table_scroll">

View File

@@ -1,35 +1,43 @@
<script>
import { createEventDispatcher, onMount } from "svelte";
<script lang="ts">
import { get_endpoint } from "lib/PixeldrainAPI";
import { onMount } from "svelte";
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 () => {
if (!profile_running) {
const resp = await fetch(
window.api_endpoint+"/admin/cpu_profile",
get_endpoint()+"/admin/cpu_profile",
{ method: "POST" }
);
if(resp.status >= 400) {
throw new Error(await resp.text());
}
} 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 running_time = "0s"
let interval: number
let running_time = $state("0s")
onMount(() => {
interval = setInterval(() => {
if (profile_running) {
running_time = formatDuration(
(new Date()).getTime() - Date.parse(running_since),
(new Date()).getTime() - Date.parse(running_since), 3
)
}
}, 1000)
@@ -43,7 +51,7 @@ onMount(() => {
<a class="button" href="/api/admin/call_stack">Call stack</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}
Stop CPU profiling (running for {running_time})
{:else}

View File

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

View File

@@ -1,20 +1,20 @@
<script>
import { stopPropagation } from 'svelte/legacy';
import { onMount } from "svelte";
import { formatDate } from "util/Formatting";
import Expandable from "util/Expandable.svelte";
import LoadingIndicator from "util/LoadingIndicator.svelte";
import Button from "layout/Button.svelte"
import UserFiles from "./UserFiles.svelte";
import BanDetails from "./BanDetails.svelte";
import UserLists from "./UserLists.svelte";
import { loading_finish, loading_start } from "lib/Loading";
let loading = true
let rows = []
let total_offences = 0
let expanded = false
let rows = $state([])
let total_offences = $state(0)
let expanded = $state(false)
const get_bans = async () => {
loading = true;
loading_start()
try {
const resp = await fetch(window.api_endpoint+"/admin/user_ban");
if(resp.status >= 400) {
@@ -28,7 +28,7 @@ const get_bans = async () => {
} catch (err) {
alert(err);
} finally {
loading = false;
loading_finish()
}
};
@@ -105,8 +105,6 @@ const block_all_files = async (row, reason) => {
onMount(get_bans);
</script>
<LoadingIndicator loading={loading}/>
<section>
<div class="toolbar">
<div class="toolbar_label">
@@ -116,7 +114,7 @@ onMount(get_bans);
Offences {total_offences}
</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}
<i class="icon">unfold_less</i> Collapse all
{:else}
@@ -127,26 +125,28 @@ onMount(get_bans);
{#each rows as row (row.user_id)}
<Expandable expanded={expanded} click_expand>
<div slot="header" class="header">
<div class="title">
{row.user.username}
{#snippet header()}
<div class="header">
<div class="title">
{row.user.username}
</div>
<div class="stats">
Type<br/>
{row.offences[0].reason}
</div>
<div class="stats">
Count<br/>
{row.offences.length}
</div>
<div class="stats">
Date<br/>
{formatDate(row.offences[0].ban_time, false, false, false)}
</div>
<button onclick={stopPropagation(() => {delete_ban(row.user_id)})} class="button button_red" style="align-self: center;">
<i class="icon">delete</i>
</button>
</div>
<div class="stats">
Type<br/>
{row.offences[0].reason}
</div>
<div class="stats">
Count<br/>
{row.offences.length}
</div>
<div class="stats">
Date<br/>
{formatDate(row.offences[0].ban_time, false, false, false)}
</div>
<button on:click|stopPropagation={() => {delete_ban(row.user_id)}} class="button button_red" style="align-self: center;">
<i class="icon">delete</i>
</button>
</div>
{/snippet}
<div class="toolbar">
<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 LoadingIndicator from "util/LoadingIndicator.svelte";
import { formatDataVolume, formatDate } from "util/Formatting";
import SortButton from "layout/SortButton.svelte";
import { loading_finish, loading_start } from "lib/Loading";
import { get_endpoint } from "lib/PixeldrainAPI";
export let user_id = ""
let files = []
let loading = true
interface Props {
user_id?: string;
}
let { user_id = "" }: Props = $props();
let files = $state([])
onMount(() => reload())
export const reload = async () => {
loading_start()
try {
const req = await fetch(
window.api_endpoint+"/user/files",
get_endpoint()+"/user/files",
{
headers: {
"Admin-User-Override": user_id,
@@ -30,12 +35,12 @@ export const reload = async () => {
} catch (err) {
alert(err);
} finally {
loading = false;
loading_finish()
}
}
let sort_field = "date_upload"
let asc = false
let sort_field = $state("date_upload")
let asc = $state(false)
const sort = (field) => {
if (field !== "" && field === sort_field) {
asc = !asc
@@ -67,8 +72,6 @@ const sort = (field) => {
}
</script>
<LoadingIndicator loading={loading}/>
<div class="table_scroll">
<table>
<thead>
@@ -86,7 +89,7 @@ const sort = (field) => {
{#each files as file (file.id)}
<tr>
<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>
<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 LoadingIndicator from "util/LoadingIndicator.svelte";
import { formatDate } from "util/Formatting";
export let user_id = ""
let lists = []
let loading = true
interface Props {
user_id?: string;
}
let { user_id = "" }: Props = $props();
let lists = $state([])
onMount(() => reload())
export const reload = async () => {
loading_start()
try {
const req = await fetch(
window.api_endpoint+"/user/lists",
get_endpoint()+"/user/lists",
{
headers: {
"Admin-User-Override": user_id,
@@ -28,13 +33,11 @@ export const reload = async () => {
} catch (err) {
alert(err);
} finally {
loading = false;
loading_finish()
}
}
</script>
<LoadingIndicator loading={loading}/>
<div class="table_scroll">
<table>
<thead>
@@ -49,7 +52,7 @@ export const reload = async () => {
{#each lists as list (list.id)}
<tr>
<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>
<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">
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";
export let nav: FSNavigator
let { nav }: {
nav: FSNavigator;
} = $props();
</script>
<div class="breadcrumbs">
{#each $nav.path as node, i (node.path)}
<a
href={"/d"+fs_encode_path(node.path)}
class="breadcrumb button"
class:button_highlight={$nav.base_index === i}
on:click|preventDefault={() => {nav.navigate(node.path, true)}}
class="breadcrumb button flat"
onclick={preventDefault(() => {nav.navigate(node.path, true)})}
>
{#if node.abuse_type !== undefined}
<i class="icon small">block</i>
{:else if node_is_shared(node)}
{:else if node.is_shared()}
<i class="icon small">share</i>
{/if}
<div class="node_name" class:base={$nav.base_index === i}>
{node.name}
</div>
</a>
{#if $nav.base_index !== i}
<i class="icon">chevron_right</i>
{/if}
{/each}
</div>
<style>
.breadcrumbs {
flex-grow: 1;
flex-shrink: 1;
flex: 0 0 auto;
display: flex;
align-items: center;
justify-content: center;
justify-content: left;
flex-wrap: wrap;
flex-direction: row;
overflow: hidden;
background: var(--shaded_background);
backdrop-filter: blur(4px);
border-bottom: 1px solid var(--separator);
}
.breadcrumb {
min-width: 1em;
@@ -42,6 +49,8 @@ export let nav: FSNavigator
word-break: break-all;
display: inline-flex;
flex-direction: row;
background-color: unset;
box-shadow: none;
}
.node_name {
max-width: 20vw;

View File

@@ -1,31 +1,34 @@
<script lang="ts">
import { run } from 'svelte/legacy';
import Chart from "util/Chart.svelte";
import { formatDataVolume, formatDate, formatThousands } from "util/Formatting";
import Modal from "util/Modal.svelte";
import { fs_path_url, fs_share_hotlink_url, fs_share_url, fs_timeseries, type FSNode } from "./FilesystemAPI";
import { color_by_name } from "util/Util.svelte";
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";
import { tick } from "svelte";
import CopyButton from "layout/CopyButton.svelte";
import type { FSNavigator } from "./FSNavigator";
export let nav: FSNavigator
export let visible = false
let {
nav,
visible = $bindable(false)
}: {
nav: FSNavigator;
visible?: boolean;
} = $props();
export const toggle = () => visible = !visible
$: visibility_change(visible)
const visibility_change = visible => {
if (visible) {
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_timespan = 0
let chart_interval = 0
let chart: Chart = $state()
let chart_timespan = $state(0)
let chart_interval = $state(0)
let chart_timespans = [
{label: "Day (1m)", span: 1440, interval: 1},
{label: "Week (1h)", span: 10080, interval: 60},
@@ -36,10 +39,9 @@ let chart_timespans = [
{label: "Five Years (1d)", span: 2628000, interval: 1440},
]
let total_downloads = 0
let total_transfer = 0
let total_downloads = $state(0)
let total_transfer = $state(0)
$: update_chart($nav.base, chart_timespan, chart_interval)
let update_chart = async (base: FSNode, timespan: number, interval: number) => {
if (chart === undefined) {
// 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,
position: "right",
ticks: {
callback: function (value, index, values) {
callback: function (value: number, index, values) {
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)
}
}
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>
<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">
{#each chart_timespans as ts}
<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}>
{ts.label}
</button>

View File

@@ -1,6 +1,6 @@
import { fs_get_node, fs_encode_path, fs_split_path } from "./FilesystemAPI";
import type { FSNode, FSPath, FSPermissions, FSContext } from "./FilesystemAPI";
import type { Writable } from "svelte/store"
import { loading_finish, loading_start } from "lib/Loading";
import { fs_get_node, fs_encode_path, fs_split_path } from "../lib/FilesystemAPI.svelte";
import type { FSNode, FSPath, FSPermissions, FSContext } from "../lib/FilesystemAPI.svelte";
export class FSNavigator {
// Parts of the raw API response
@@ -22,27 +22,16 @@ export class FSNavigator {
constructor(history_enabled = true) {
this.history_enabled = history_enabled
// If history logging is enabled we capture the popstate event, which
// fires when the user uses the back and forward buttons in the browser.
// Instead of reloading the page we use the navigator to navigate to the
// new page
if (history_enabled) {
window.addEventListener("popstate", () => {
// Get the part of the URL after the fs root and navigate to it
const path = document.location.pathname.replace("/d/", "")
this.navigate(decodeURIComponent(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 popstate event can be used to listen for navigation events. Register
// this event listener on the <svelte:window> in the parent element. When
// the user presses the back or forward buttons in the browser we'll catch
// the event and navigate to the proper directory
popstate = (e: PopStateEvent) => {
// Get the part of the URL after the fs root and navigate to it
const path = window.location.pathname.replace(/^\/d/, "")
this.navigate(decodeURI(path), false)
}
// 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)
try {
this.set_loading(true)
loading_start()
const resp = await fs_get_node(path)
this.open_node(resp, push_history)
} catch (err: any) {
@@ -89,7 +78,7 @@ export class FSNavigator {
alert("Error: " + err)
}
} 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
// greeted to a 404 page when refreshing after renaming a file
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
if (push_history) {
window.history.pushState({}, window.document.title, url)
@@ -189,14 +178,14 @@ export class FSNavigator {
let siblings: Array<FSNode>
try {
this.set_loading(true)
loading_start()
siblings = await this.get_siblings()
} catch (err) {
console.error(err)
alert(err)
return
} finally {
this.set_loading(false)
loading_finish()
}
let next_sibling: FSNode | null = null

View File

@@ -1,16 +1,18 @@
<script lang="ts">
import { onMount } from "svelte";
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";
export let nav: FSNavigator
let { nav }: {
nav: FSNavigator;
} = $props();
let loading = true
let downloads = 0
let transfer_used = 0
let loading = $state(true)
let downloads = $state(0)
let transfer_used = $state(0)
let socket = null
let error_msg = ""
let error_msg = $state("")
let connected_to = ""
@@ -22,9 +24,9 @@ onMount(() => {
}
})
let total_directories = 0
let total_files = 0
let total_file_size = 0
let total_directories = $state(0)
let total_files = $state(0)
let total_file_size = $state(0)
const update_base = async () => {
if (!nav.initialized) {
@@ -104,7 +106,7 @@ const close_socket = () => {
</div>
<div class="group">
<div class="label">Transfer used</div>
<div class="label">Egress</div>
<div class="stat">
{loading ? "Loading..." : formatDataVolume(transfer_used, 3)}
</div>
@@ -140,18 +142,11 @@ const close_socket = () => {
text-align: center;
}
.label {
padding-left: 0.5em;
text-align: left;
text-align: center;
font-size: 0.8em;
line-height: 1em;
}
.stat {
line-height: 1.2em;
}
@media (max-width: 1000px) {
.label {
text-align: center;
padding-left: 0;
}
}
</style>

View File

@@ -1,46 +1,56 @@
<script lang="ts">
import { onMount } from "svelte";
import LoadingIndicator from "util/LoadingIndicator.svelte";
import EditWindow from "./edit_window/EditWindow.svelte";
import Toolbar from "./Toolbar.svelte";
import Breadcrumbs from "./Breadcrumbs.svelte";
import DetailsWindow from "./DetailsWindow.svelte";
import FilePreview from "./viewers/FilePreview.svelte";
import FSUploadWidget from "./upload_widget/FSUploadWidget.svelte";
import { fs_download, type FSPath } from "./FilesystemAPI";
import Menu from "./Menu.svelte";
import { type FSPath } from "lib/FilesystemAPI.svelte";
import { FSNavigator } from "./FSNavigator"
import { writable } from "svelte/store";
import { css_from_path } from "filesystem/edit_window/Branding";
import AffiliatePrompt from "user_home/AffiliatePrompt.svelte";
import { current_page_store } from "wrap/RouterStore";
let file_viewer: HTMLDivElement
let file_preview: FilePreview
let toolbar: Toolbar
let upload_widget: FSUploadWidget
let details_visible = false
let edit_window: EditWindow
let edit_visible = false
let file_preview: FilePreview = $state()
let toolbar: Toolbar = $state()
let upload_widget: FSUploadWidget = $state()
let details_visible = $state(false)
let edit_window: EditWindow = $state()
let edit_visible = $state(false)
let details_window: DetailsWindow = $state()
const loading = writable(true)
const nav = new FSNavigator(true)
const nav = $state(new FSNavigator(true))
onMount(() => {
nav.loading = loading
nav.open_node((window as any).initial_node as FSPath, false)
if ((window as any).intial_node !== undefined) {
console.debug("Loading initial node")
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
// which we can conveniently return from our mount function as well
return nav.subscribe(nav => {
const nav_sub = nav.subscribe(nav => {
if (!nav.initialized) {
return
}
// Custom CSS rules for the whole viewer
document.documentElement.style = css_from_path(nav.path)
loading.set(false)
})
return () => {
page_sub()
nav_sub()
document.documentElement.style = ""
}
})
const keydown = (e: KeyboardEvent) => {
@@ -66,16 +76,11 @@ const keydown = (e: KeyboardEvent) => {
}
break;
case "s":
fs_download(nav.base)
nav.base.download()
break;
case "r":
nav.shuffle = !nav.shuffle
break;
case "f": // F fullscreen
if (toolbar) {
toolbar.toggle_fullscreen()
}
break
case "a":
case "ArrowLeft":
nav.open_sibling(-1)
@@ -125,52 +130,40 @@ const keydown = (e: KeyboardEvent) => {
};
</script>
<svelte:window on:keydown={keydown} />
<svelte:window onkeydown={keydown} />
<div bind:this={file_viewer} class="file_viewer">
<div class="headerbar">
<Menu/>
<Breadcrumbs nav={nav}/>
</div>
<div class="filesystem">
<Breadcrumbs nav={nav}/>
<div class="viewer_area">
<Toolbar
bind:this={toolbar}
<div class="file_preview">
<FilePreview
bind:this={file_preview}
nav={nav}
file_viewer={file_viewer}
file_preview={file_preview}
bind:details_visible={details_visible}
upload_widget={upload_widget}
edit_window={edit_window}
bind:edit_visible={edit_visible}
on:download={() => fs_download(nav.base)}
details_window={details_window}
/>
<div class="file_preview">
<FilePreview
bind:this={file_preview}
nav={nav}
upload_widget={upload_widget}
edit_window={edit_window}
on:open_sibling={e => nav.open_sibling(e.detail)}
on:download={() => fs_download(nav.base)}
on:details={() => details_visible = !details_visible}
/>
</div>
</div>
<DetailsWindow nav={nav} 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/>
<LoadingIndicator loading={$loading}/>
<Toolbar
bind:this={toolbar}
nav={nav}
bind:details_visible={details_visible}
edit_window={edit_window}
bind:edit_visible={edit_visible}
/>
</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>
:global(*) {
transition: background-color 0.2s,
@@ -183,56 +176,15 @@ const keydown = (e: KeyboardEvent) => {
}
/* Viewer container */
.file_viewer {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
.filesystem {
display: flex;
flex-direction: column;
overflow: hidden;
/* 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;
}
height: 100vh;
width: 100%;
}
.file_preview {
flex: 1 1 0;
flex: 1 1 auto;
overflow: auto;
border: 1px solid var(--separator);
}
</style>

View File

@@ -6,23 +6,31 @@ import { formatDataVolume } from "util/Formatting";
import { user } from "lib/UserStore";
import Dialog from "layout/Dialog.svelte";
let button: HTMLButtonElement
let dialog: Dialog
let button: HTMLButtonElement = $state()
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
export let hide_name = true
export let hide_logo = false
export let style = ""
export let embedded = false
$: target = embedded ? "_blank" : "_self"
let target = $derived(embedded ? "_blank" : "_self")
const open = () => dialog.open(button.getBoundingClientRect())
</script>
<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}
<PixeldrainLogo style="height: 1.6em; width: 1.6em;"/>
{/if}

View File

@@ -1,22 +1,24 @@
<script lang="ts">
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 { copy_text } from "util/Util.svelte";
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";
import CopyButton from "layout/CopyButton.svelte";
import Dialog from "layout/Dialog.svelte";
import { fade } from "svelte/transition";
export let nav: FSNavigator
let { nav }: {
nav: FSNavigator;
} = $props();
let path: FSNode[]
let base: FSNode
let toast = ""
let share_url = ""
let direct_share_url = ""
let is_parent = false
let parent_node: FSNode
let base: FSNode = $state()
let toast = $state("")
let share_url = $state("")
let direct_share_url = $state("")
let is_parent = $state(false)
let parent_node: FSNode = $state()
let dialog: Dialog
let dialog: Dialog = $state()
export const open = async (e: MouseEvent, p: FSNode[]) => {
path = p
base = path[path.length-1]
@@ -36,7 +38,7 @@ export const open = async (e: MouseEvent, p: FSNode[]) => {
}
const make_public = async () => {
if (!node_is_shared(base)) {
if (!base.is_shared()) {
base = await fs_update(
base.path,
{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"/>
{parent_node.name}
<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
<img src={fs_node_icon(base, 64, 64)} class="node_icon" alt="icon"/>
{base.name}

View File

@@ -1,26 +1,28 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { copy_text } from "util/Util.svelte";
import { copy_text } from "util/Util";
import FileStats from "./FileStats.svelte";
import type { FSNavigator } from "./FSNavigator";
import EditWindow from "./edit_window/EditWindow.svelte";
import FilePreview from "./viewers/FilePreview.svelte";
import { fs_share_url } from "./FilesystemAPI";
import { fs_share_url, path_is_shared } from "lib/FilesystemAPI.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
export let details_visible = 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
let share_dialog: ShareDialog = $state()
let link_copied = $state(false)
export const copy_link = () => {
const share_url = fs_share_url($nav.path)
if (share_url === "") {
edit_window.edit(nav.base, true, "share")
return
@@ -30,103 +32,63 @@ export const copy_link = () => {
link_copied = true
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>
<div class="toolbar" class:expanded>
<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="toolbar">
<div class="grid">
<FileStats nav={nav}/>
<div class="button_row">
<button on:click={() => {nav.open_sibling(-1)}}>
<button onclick={() => {nav.open_sibling(-1)}}>
<i class="icon">skip_previous</i>
</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>
</button>
<button on:click={() => {nav.open_sibling(1)}}>
<button onclick={() => {nav.open_sibling(1)}}>
<i class="icon">skip_next</i>
</button>
</div>
<div class="separator hidden_horizontal"></div>
<button on:click={() => dispatch("download")}>
<button onclick={() => $nav.base.download()}>
<i class="icon">save</i>
<span>Download</span>
</button>
{#if share_url !== ""}
<button on:click={copy_link} class:button_highlight={link_copied}>
{#if is_bookmark($bookmarks_store, $nav.base.id)}
<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>
<span><u>C</u>opy link</span>
</button>
{/if}
<!-- 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)}
<button on:click={(e) => share_dialog.open(e, nav.path)}>
{#if navigator.share !== undefined || $nav.permissions.write === true}
<button onclick={(e) => share_dialog.open(e, nav.path)}>
<i class="icon">share</i>
<span>Share</span>
</button>
{/if}
<button
class="toolbar_button"
on:click={toggle_fullscreen}
class:button_highlight={fullscreen}
title="Open page in full screen mode">
{#if fullscreen}
<i class="icon">fullscreen_exit</i>
{:else}
<i class="icon">fullscreen</i>
{/if}
<span>Fullscreen</span>
</button>
<div class="separator hidden_horizontal"></div>
<button on:click={() => details_visible = !details_visible} class:button_highlight={details_visible}>
<button onclick={() => details_visible = !details_visible} class:button_highlight={details_visible}>
<i class="icon">help</i>
<span>Deta<u>i</u>ls</span>
</button>
{#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>
<span><u>E</u>dit</span>
</button>
@@ -140,22 +102,17 @@ let expand = (e: Event) => {
.toolbar {
flex: 0 0 auto;
overflow-x: hidden;
overflow-y: scroll;
overflow-y: hidden;
transition: max-height 0.3s;
background-color: var(--shaded_background);
border-top: 1px solid var(--separator);
background: var(--shaded_background);
backdrop-filter: blur(4px);
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(7.5em, 1fr));
}
.separator {
height: 1px;
margin: 2px 0;
width: 100%;
background-color: var(--separator);
}
.button_row {
display: flex;
flex-direction: row;
@@ -164,46 +121,4 @@ let expand = (e: Event) => {
flex: 1 1 auto;
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>

View File

@@ -1,12 +1,16 @@
<script lang="ts">
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";
export let options: NodeOptions
let {
options = $bindable()
}: {
options: NodeOptions;
} = $props();
let new_user_id = ""
let new_user_perms = <FSPermissions>{read: true}
let new_user_id = $state("")
let new_user_perms = $state(<FSPermissions>{read: true})
const add_user = (e: SubmitEvent) => {
e.preventDefault()
if (options.user_permissions === undefined) {
@@ -19,8 +23,8 @@ const del_user = (id: string) => {
options.user_permissions = options.user_permissions
}
let new_password = ""
let new_password_perms = <FSPermissions>{read: true}
let new_password = $state("")
let new_password_perms = $state(<FSPermissions>{read: true})
const add_password = (e: SubmitEvent) => {
e.preventDefault()
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
read access as well does not actually allow them to write anything.
</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">
<Button type="submit" icon="add" label="Add"/>
<div class="perms">
@@ -94,7 +98,7 @@ const del_password = (pass: string) => {
<p>
<b>This feature is not implemented currently!</b>
</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">
<Button type="submit" icon="add" label="Add"/>
<div class="perms">

View File

@@ -2,11 +2,39 @@ import parse from "pure-color/parse";
import rgb2hsl from "pure-color/convert/rgb2hsl";
import hsl2rgb from "pure-color/convert/hsl2rgb";
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
export const branding_from_path = path => {
let style = {}
for (let node of path) {
export const branding_from_path = (path: Array<FSNode>) => {
const style = {}
for (const node of path) {
add_styles(style, node.properties)
}
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
// path to generate the style with we will use the cached style as a basis
let last_generated_style = {}
export const branding_from_node = node => {
add_styles(last_generated_style, node.properties)
let last_generated_style: Style = {}
export const branding_from_props = (props: FSNodeProperties) => {
add_styles(last_generated_style, props)
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))
}
const gen_css = style => {
const gen_css = (style: Style) => {
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
// path this function is executed on every member of the path so all the styles
// get combined
const add_styles = (style, properties) => {
const add_styles = (style: Style, properties: FSNodeProperties) => {
if (!properties || !properties.branding_enabled || properties.branding_enabled !== "true") {
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
// 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
@@ -96,20 +124,20 @@ const add_contrast = (color, amt) => {
}
// 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
hsl[1] = hsl[1] * percent
hsl[2] = hsl[2] * percent
return rgb2hex(hsl2rgb(hsl)) // Convert back to hex
}
const set_alpha = (color, amt) => {
const set_alpha = (color: string, amt: number) => {
let rgb = parse(color)
rgb.push(amt)
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 body = rgb2hsl(parse(body_color))

View File

@@ -1,38 +1,44 @@
<script lang="ts">
import { createEventDispatcher } from "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 HelpButton from "layout/HelpButton.svelte";
import FilePicker from "filesystem/filemanager/FilePicker.svelte";
let dispatch = createEventDispatcher()
import { branding_from_props } from './Branding';
export let file: FSNode
export let options: NodeOptions
export let enabled: boolean
let {
file = $bindable(),
options = $bindable(),
enabled = $bindable(),
custom_css = $bindable(),
}: {
file: FSNode
options: NodeOptions
enabled: boolean
custom_css: string
} = $props();
$: update_colors(options)
const update_colors = (options: NodeOptions) => {
$effect(() => {
if (enabled) {
options.branding_enabled = "true"
dispatch("style_change")
custom_css = branding_from_props(options)
} else {
options.branding_enabled = ""
}
}
})
let picker: FilePicker
let picker: FilePicker = $state()
let picking = ""
const pick_image = (type: string) => {
picking = type
picker.open(file.path)
}
const handle_picker = async (e: CustomEvent<FSNode[]>) => {
if (e.detail.length !== 1) {
const handle_picker = async (nodes: FSNode[]) => {
if (nodes.length !== 1) {
alert("Please select one file")
return
}
let f = e.detail[0]
let f = nodes[0]
if (fs_node_type(f) !== "image") {
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 (!node_is_shared(f)) {
if (!f.is_shared()) {
try {
f = await fs_update(
e.detail[0].path,
nodes[0].path,
{link_permissions: {read: true} as FSPermissions},
)
} catch (err) {
@@ -61,7 +67,7 @@ const handle_picker = async (e: CustomEvent<FSNode[]>) => {
}
}
let highlight_info = false
let highlight_info = $state(false)
</script>
<fieldset>
@@ -90,7 +96,7 @@ let highlight_info = false
<div style="display: inline-block">Highlight</div>
<HelpButton bind:toggle={highlight_info}/>
</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}/>
{#if highlight_info}
<p class="span3">
@@ -101,19 +107,19 @@ let highlight_info = false
</p>
{/if}
<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}/>
<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}/>
<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}/>
<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}/>
<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}/>
</fieldset>
@@ -127,7 +133,7 @@ let highlight_info = false
working. Recommended dimensions for the header image are 1000x90 px.
</p>
<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>
Pick
</button>
@@ -135,7 +141,7 @@ let highlight_info = false
<div>Header image link</div>
<input class="span2" type="text" bind:value={options.brand_header_link}/>
<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>
Pick
</button>
@@ -177,7 +183,7 @@ let highlight_info = false
</div>
</fieldset>
<FilePicker bind:this={picker} on:files={handle_picker}/>
<FilePicker bind:this={picker} callback={handle_picker}/>
<style>
input[type="color"] {

View File

@@ -1,20 +1,26 @@
<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 BrandingOptions from "./BrandingOptions.svelte";
import { branding_from_node } from "./Branding";
import { branding_from_props } from "./Branding";
import FileOptions from "./FileOptions.svelte";
import SharingOptions from "./SharingOptions.svelte";
import AccessControl from "./AccessControl.svelte";
import type { FSNavigator } from "filesystem/FSNavigator";
import { loading_finish, loading_start } from "lib/Loading";
export let nav: FSNavigator
let file: FSNode = {} as FSNode
let options: NodeOptions = {} as NodeOptions
let file: FSNode = $state({} as FSNode)
let options: NodeOptions = $state({} 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
// 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.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.user_permissions = file.user_permissions
options.password_permissions = file.password_permissions
}
options.link_permissions = file.link_permissions
options.user_permissions = file.user_permissions
options.password_permissions = file.password_permissions
branding_enabled = options.branding_enabled === "true"
if (branding_enabled) {
custom_css = branding_from_node(file)
custom_css = branding_from_props(options)
} else {
custom_css = ""
}
@@ -62,18 +59,19 @@ export const edit = (f: FSNode, oae = false, open_tab = "") => {
visible = true
}
let tab = "file"
let open_after_edit = false
let tab = $state("file")
let open_after_edit = $state(false)
let new_name = ""
let branding_enabled = false
let new_name = $state("")
let branding_enabled = $state(false)
const save = async (keep_editing = false) => {
const save = async (e: SubmitEvent) => {
e.preventDefault()
console.debug("Saving file", file.path)
let new_file: FSNode
try {
nav.set_loading(true)
loading_start()
options.branding_enabled = JSON.stringify(branding_enabled)
new_file = await fs_update(file.path, options)
@@ -97,7 +95,7 @@ const save = async (keep_editing = false) => {
}
return
} finally {
nav.set_loading(false)
loading_finish()
}
if (open_after_edit) {
@@ -105,36 +103,32 @@ const save = async (keep_editing = false) => {
} else {
nav.reload()
}
if (keep_editing) {
edit(new_file, open_after_edit)
}
}
</script>
<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">
<button class:button_highlight={tab === "file"} on:click={() => tab = "file"}>
<button class:button_highlight={tab === "file"} onclick={() => tab = "file"}>
<i class="icon">edit</i>
Properties
</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>
Sharing
</button>
{#if options.shared && $nav.permissions.owner}
<button class:button_highlight={tab === "access"} on:click={() => tab = "access"}>
{#if $nav.permissions.owner}
<button class:button_highlight={tab === "access"} onclick={() => tab = "access"}>
<i class="icon">key</i>
Access control
</button>
{/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>
Branding
</button>
</div>
<form id="edit_form" on:submit|preventDefault={() => save(false)}></form>
<form id="edit_form" onsubmit={save}></form>
<div class="tab_content">
{#if tab === "file"}
@@ -146,7 +140,10 @@ const save = async (keep_editing = false) => {
bind:open_after_edit
/>
{:else if tab === "share"}
<SharingOptions bind:file bind:options on:save={() => save(true)} />
<SharingOptions
bind:file
bind:options
/>
{:else if tab === "access"}
<AccessControl bind:options />
{:else if tab === "branding"}
@@ -154,7 +151,7 @@ const save = async (keep_editing = false) => {
bind:enabled={branding_enabled}
bind:options={options}
bind:file
on:style_change={e => custom_css = branding_from_node(file)}
bind:custom_css
/>
{/if}
</div>

View File

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

View File

@@ -1,10 +1,16 @@
<script lang="ts">
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>
<ToggleButton group_first bind:on={permissions.read}>Read</ToggleButton>
<ToggleButton group_middle bind:on={permissions.write}>Write</ToggleButton>
<ToggleButton group_last bind:on={permissions.delete}>Delete</ToggleButton>
{#if permissions !== undefined}
<ToggleButton group_first bind:on={permissions.read}>Read</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">
import { createEventDispatcher } from "svelte";
import { domain_url } from "util/Util.svelte";
import { run } from 'svelte/legacy';
import { domain_url } from "util/Util";
import CopyButton from "layout/CopyButton.svelte";
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()
export let file: FSNode = {} as FSNode
export let options: NodeOptions
let {
file = $bindable(),
options = $bindable(),
}: {
file?: FSNode;
options: NodeOptions;
} = $props();
let embed_html: string
let preview_area: HTMLDivElement
let embed_html: string = $state()
let preview_area: HTMLDivElement = $state()
$: share_link = window.location.protocol+"//"+window.location.host+"/d/"+file.id
$: embed_iframe(file, options)
let embed_iframe = (file: FSNode, options: NodeOptions) => {
if (!options.shared) {
const embed_iframe = (file: FSNode, options: NodeOptions) => {
if (!file.is_shared()) {
example = false
embed_html = "File is not shared, can't generate embed code"
return
@@ -24,14 +27,14 @@ let embed_iframe = (file: FSNode, options: NodeOptions) => {
let url = domain_url()+"/d/"+file.id
embed_html = `<iframe ` +
`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` +
`></iframe>`
}
let example = false
let example = $state(false)
const toggle_example = () => {
if (options.shared) {
if (file.is_shared()) {
example = !example
if (example) {
preview_area.innerHTML = embed_html
@@ -41,15 +44,10 @@ const toggle_example = () => {
}
}
const update_shared = () => {
// If sharing is enabled we automatically save the file so the user can copy
// the sharing link. But if the user disables sharing we don't automatically
// save so that the user can't accidentally discard a sharing link that's in
// use
if (options.shared && !file.id) {
dispatch("save")
}
}
let share_link = $derived(window.location.protocol+"//"+window.location.host+"/d/"+file.id)
run(() => {
embed_iframe(file, options)
});
</script>
<fieldset>
@@ -64,34 +62,14 @@ const update_shared = () => {
</div>
{/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">
{#if options.shared}
<span>Public link: <a href={share_link}>{share_link}</a></span>
<a href={share_link}>{share_link}</a>
<CopyButton text={share_link}>Copy</CopyButton>
{/if}
</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>
<AccessControl options={options}/>
<fieldset>
<legend>Embedding</legend>
<p>
@@ -108,7 +86,7 @@ const update_shared = () => {
<textarea bind:value={embed_html} style="width: 100%; height: 4em;"></textarea>
<br/>
<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
</button>
</div>

View File

@@ -1,9 +1,13 @@
<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) => {
current_theme = index
@@ -71,7 +75,7 @@ const themes = [
</script>
{#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}
</button>
{/each}

View File

@@ -1,23 +1,29 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
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 { FileAction } from "./FileManagerLib";
import { FileAction, type FileActionHandler } from "./FileManagerLib";
let dispatch = createEventDispatcher()
export let nav: FSNavigator
export let show_hidden = false
export let large_icons = false
export let hide_edit = false
let {
nav,
file_event,
show_hidden = false,
large_icons = false,
hide_edit = false
}: {
nav: FSNavigator;
file_event: FileActionHandler
show_hidden?: boolean
large_icons?: boolean
hide_edit?: boolean
} = $props();
</script>
<div class="directory">
{#each $nav.children as child, index (child.path)}
<a
href={"/d"+fs_encode_path(child.path)}
on:click={e => dispatch("file", {index: index, action: FileAction.Click, original: e})}
on:contextmenu={e => dispatch("file", {index: index, action: FileAction.Context, original: e})}
onclick={e => file_event(FileAction.Click, index, e)}
oncontextmenu={e => file_event(FileAction.Context, index, e)}
class="node"
class:node_selected={child.fm_selected}
class:hidden={child.name.startsWith(".") && !show_hidden}
@@ -26,31 +32,19 @@ export let hide_edit = false
<div class="node_name">
{child.name}
</div>
{#if node_is_shared(child)}
<a
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>
</a>
{/if}
{#if $nav.permissions.write && !hide_edit}
<button
class="action_button flat"
on:click={e => dispatch("file", {index: index, action: FileAction.Edit, original: e})}
>
<i class="icon">edit</i>
</button>
{#if child.is_shared()}
<i class="icon" title="This file / directory is shared. Click to open public link">share</i>
{/if}
{#if !hide_edit}
<button
class="action_button flat"
on:click={e => dispatch("file", {index: index, action: FileAction.Download, original: e})}
onclick={e => file_event(FileAction.Menu, index, e)}
>
<i class="icon">save</i>
<i class="icon">menu</i>
</button>
{/if}
</a>
{/each}
</div>
@@ -71,9 +65,9 @@ export let hide_edit = false
color: var(--body_text-color);
padding: 2px;
align-items: center;
background: var(--input_background);
background: var(--body_background);
/* backdrop-filter: blur(4px); */
border-radius: 4px;
box-shadow: 1px 1px 8px 0px var(--shadow_color);
gap: 6px;
}
.node:hover:not(.node_selected) {
@@ -92,7 +86,6 @@ export let hide_edit = false
height: 2em;
width: 2em;
vertical-align: middle;
border-radius: 4px;
}
.node_name {
flex: 1 1 content;

View File

@@ -1,20 +1,22 @@
<script lang="ts">
import { preventDefault } from 'svelte/legacy';
import { onMount } from "svelte";
import { fs_mkdir } from "filesystem/FilesystemAPI";
import { fs_mkdir } from "lib/FilesystemAPI.svelte";
import Button from "layout/Button.svelte";
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 new_dir_name = ""
let error_msg = ""
let name_input: HTMLInputElement = $state();
let new_dir_name = $state("")
let error_msg = $state("")
let create_dir = async () => {
let form = new FormData()
form.append("type", "dir")
try {
nav.set_loading(true)
loading_start()
await fs_mkdir(nav.base.path+"/"+new_dir_name)
new_dir_name = "" // Clear input field
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
}
} finally {
nav.set_loading(false)
loading_finish()
}
}
@@ -41,7 +43,7 @@ onMount(() => {
</div>
{/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"/>
<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"/>

View File

@@ -1,5 +1,6 @@
<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 CreateDirectory from "./CreateDirectory.svelte"
import ListView from "./ListView.svelte"
@@ -12,30 +13,42 @@ import SearchBar from "./SearchBar.svelte";
import type { FSNavigator } from "filesystem/FSNavigator";
import FsUploadWidget from "filesystem/upload_widget/FSUploadWidget.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
export let upload_widget: FsUploadWidget
export let edit_window: EditWindow
export let directory_view = ""
let large_icons = false
let {
nav = $bindable(),
upload_widget,
edit_window = $bindable(),
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 mode = "viewing"
let creating_dir = false
let show_hidden = false
let mode = $state("viewing")
let creating_dir = $state(false)
let show_hidden = $state(false)
let file_menu: FileMenu = $state()
export const upload = (files: File[]) => {
return uploader.upload(files)
return uploader.upload_files(files)
}
// Navigation functions
const file_event = (e: CustomEvent<FileEvent>) => {
const index = e.detail.index
const file_event: FileActionHandler = (action: FileAction, index: number, orig: Event) => {
orig.preventDefault()
orig.stopPropagation()
switch (e.detail.action) {
switch (action) {
case FileAction.Click:
e.detail.original.preventDefault()
e.detail.original.stopPropagation()
creating_dir = false
if (mode === "viewing") {
@@ -54,35 +67,26 @@ const file_event = (e: CustomEvent<FileEvent>) => {
case FileAction.Context:
// If this is a touch event we will select the item
if (navigator.maxTouchPoints && navigator.maxTouchPoints > 0) {
e.detail.original.preventDefault()
select_node(index)
} else {
file_menu.open(nav.children[index], orig.target)
}
break
case FileAction.Edit:
e.detail.original.preventDefault()
e.detail.original.stopPropagation()
edit_window.edit(nav.children[index], false, "file")
break
case FileAction.Share:
e.detail.original.preventDefault()
e.detail.original.stopPropagation()
creating_dir = false
edit_window.edit(nav.children[index], false, "share")
break
case FileAction.Branding:
e.detail.original.preventDefault()
e.detail.original.stopPropagation()
edit_window.edit(nav.children[index], false, "branding")
break
case FileAction.Select:
e.detail.original.preventDefault()
e.detail.original.stopPropagation()
select_node(index)
break
case FileAction.Download:
e.detail.original.preventDefault()
e.detail.original.stopPropagation()
fs_download(nav.children[index])
case FileAction.Menu:
file_menu.open(nav.children[index], orig.target)
break
}
}
@@ -110,9 +114,9 @@ const delete_selected = async () => {
return
}
nav.set_loading(true)
try {
loading_start()
// Save all promises with deletion requests in an array
let promises = []
nav.children.forEach(child => {
@@ -129,7 +133,7 @@ const delete_selected = async () => {
alert("Delete failed: " + err.message + " ("+err.value+")")
} finally {
viewing_mode()
nav.reload()
loading_finish()
}
}
@@ -220,10 +224,6 @@ const select_node = (index: number) => {
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[]) => {
creating_dir = false
@@ -237,8 +237,8 @@ const update = (children: FSNode[]) => {
}
}
let moving_files = 0
let moving_directories = 0
let moving_files = $state(0)
let moving_directories = $state(0)
const move_start = () => {
moving_files = 0
moving_directories = 0
@@ -257,11 +257,10 @@ const move_start = () => {
}
const move_here = async () => {
nav.set_loading(true)
let target_dir = nav.base.path + "/"
const target_dir = nav.base.path + "/"
try {
loading_start()
let promises = []
moving_items.forEach(item => {
console.log("moving", item.path, "to", target_dir + item.name)
@@ -276,6 +275,7 @@ const move_here = async () => {
} finally {
viewing_mode()
nav.reload()
loading_finish()
}
}
@@ -288,9 +288,15 @@ onMount(() => {
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>
<svelte:window on:keydown={keypress} on:keyup={keypress} />
<svelte:window onkeydown={keypress} onkeyup={keypress} />
<div
class="container"
@@ -303,25 +309,25 @@ onMount(() => {
{#if mode === "viewing"}
<div class="toolbar">
<div class="toolbar_left">
<button on:click={navigate_back} title="Back">
<button onclick={navigate_back} title="Back">
<i class="icon">arrow_back</i>
</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>
</button>
<button on:click={() => nav.reload()} title="Refresh directory listing">
<button onclick={() => nav.reload()} title="Refresh directory listing">
<i class="icon">refresh</i>
</button>
</div>
<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 === "gallery"}>collections</i>
<i class="icon" class:button_highlight={directory_view === "compact"}>view_compact</i>
</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}
<i class="icon">zoom_out</i>
{:else}
@@ -329,7 +335,7 @@ onMount(() => {
{/if}
</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}
<i class="icon">visibility_off</i>
{:else}
@@ -340,13 +346,13 @@ onMount(() => {
<div class="toolbar_right">
{#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>
</button>
<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>
</button>
{/if}
@@ -360,7 +366,7 @@ onMount(() => {
<Button click={viewing_mode} icon="close"/>
<div class="toolbar_spacer">Selecting files</div>
<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>
Delete
</button>
@@ -399,24 +405,25 @@ onMount(() => {
</div>
{/if}
<slot></slot>
{@render children?.()}
{#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"}
<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"}
<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}
</div>
<FileMenu bind:this={file_menu} nav={nav} edit_window={edit_window} />
<style>
.container {
height: 100%;
width: 100%;
padding: 0;
overflow: auto;
display: block;
height: 100%; /* Used for drop target */
}
.width_container {
position: sticky;
@@ -425,8 +432,8 @@ onMount(() => {
width: 100%;
margin: auto;
padding: 0;
background: var(--shaded_background);
backdrop-filter: blur(4px);
background: var(--body_background);
/* backdrop-filter: blur(4px); */
}
.toolbar {
display: flex;

View File

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

View File

@@ -1,21 +1,27 @@
<script lang="ts">
import { createEventDispatcher } from "svelte"
import { fs_node_icon, fs_node_type, fs_encode_path } from "filesystem/FilesystemAPI";
import { fs_node_icon, fs_node_type, fs_encode_path } from "lib/FilesystemAPI.svelte";
import type { FSNavigator } from "filesystem/FSNavigator";
import { FileAction } from "./FileManagerLib";
let dispatch = createEventDispatcher()
import { FileAction, type FileActionHandler } from "./FileManagerLib";
export let nav: FSNavigator
export let show_hidden = false
export let large_icons = false
let {
nav,
file_event,
show_hidden = false,
large_icons = false
}: {
nav: FSNavigator
file_event: FileActionHandler
show_hidden?: boolean
large_icons?: boolean
} = $props();
</script>
<div class="gallery">
{#each $nav.children as child, index (child.path)}
<a class="file"
href={"/d"+fs_encode_path(child.path)}
on:click={e => dispatch("file", {index: index, action: FileAction.Click, original: e})}
on:contextmenu={e => dispatch("file", {index: index, action: FileAction.Context, original: e})}
onclick={e => file_event(FileAction.Click, index, e)}
oncontextmenu={e => file_event(FileAction.Context, index, e)}
class:selected={child.fm_selected}
class:hidden={child.name.startsWith(".") && !show_hidden}
class:large_icons
@@ -46,15 +52,15 @@ export let large_icons = false
width: 150px;
height: 150px;
overflow: hidden;
border-radius: 8px;
background: var(--input_background);
background: var(--body_background);
/* backdrop-filter: blur(4px); */
border-radius: 4px;
color: var(--input_text);
display: flex;
flex-direction: column;
transition: background 0.2s;
text-decoration: none;
padding: 3px;
box-shadow: 1px 1px 0px 0px var(--shadow_color);
}
.file.large_icons {
width: 200px;
@@ -87,7 +93,7 @@ export let large_icons = false
}
.node_icon {
flex: 1 1 0;
border-radius: 6px;
border-radius: 4px;
background-position: center;
background-size: contain;
background-repeat: no-repeat;

View File

@@ -1,89 +1,96 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
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 SortButton from "layout/SortButton.svelte";
import { FileAction } from "./FileManagerLib";
import { FileAction, type FileActionHandler } from "./FileManagerLib";
let dispatch = createEventDispatcher()
export let nav: FSNavigator
export let show_hidden = false
export let large_icons = false
export let hide_edit = false
export let hide_branding = false
let {
nav,
file_event,
show_hidden = false,
large_icons = false,
hide_edit = false,
hide_branding = false
}: {
nav: FSNavigator
file_event: FileActionHandler
show_hidden?: boolean
large_icons?: boolean
hide_edit?: boolean
hide_branding?: boolean
} = $props();
</script>
<div class="directory">
<tr>
<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="file_size">Size</SortButton></td>
<td></td>
</tr>
{#each $nav.children as child, index (child.path)}
<a
href={"/d"+fs_encode_path(child.path)}
on:click={e => dispatch("file", {index: index, action: FileAction.Click, original: e})}
on:contextmenu={e => dispatch("file", {index: index, action: FileAction.Context, original: e})}
class="node"
class:node_selected={child.fm_selected}
class:hidden={child.name.startsWith(".") && !show_hidden}
>
<td>
<img src={fs_node_icon(child, 64, 64)} class="node_icon" class:large_icons alt="icon"/>
</td>
<td class="node_name">
{child.name}
</td>
<td class="node_size hide_small">
{#if child.type === "file"}
{formatDataVolume(child.file_size, 3)}
{/if}
</td>
<td class="node_icons">
<div class="icons_wrap">
{#if child.abuse_type !== undefined}
<i class="icon" title="This file / directory has received an abuse report. It cannot be shared">block</i>
{:else if node_is_shared(child)}
<a
href="/d/{child.id}"
on:click={e => dispatch("file", {index: index, action: FileAction.Share, original: e})}
class="button action_button"
>
<i class="icon" title="This file / directory is shared. Click to open public link">share</i>
</a>
<table class="directory">
<thead>
<tr>
<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 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>
</tr>
</thead>
<tbody>
{#each $nav.children as child, index (child.path)}
<tr
onclick={e => file_event(FileAction.Click, index, e)}
oncontextmenu={e => file_event(FileAction.Context, index, e)}
class="node"
class:node_selected={child.fm_selected}
class:hidden={child.name.startsWith(".") && !show_hidden}
>
<td>
<img src={fs_node_icon(child, 64, 64)} class="node_icon" class:large_icons alt="icon"/>
</td>
<td class="node_name">
<a class="title_link" href={"/d"+fs_encode_path(child.path)}>
{child.name}
</a>
</td>
<td class="node_size hide_small">
{#if child.type === "file"}
{formatDataVolume(child.file_size, 3)}
{/if}
{#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})}>
<i class="icon">palette</i>
</button>
{/if}
{#if $nav.permissions.write && !hide_edit}
<button class="action_button" on:click={e => dispatch("file", {index: index, action: FileAction.Edit, original: e})}>
<i class="icon">edit</i>
</button>
{/if}
<button class="action_button" on:click={e => dispatch("file", {index: index, action: FileAction.Download, original: e})}>
<i class="icon">save</i>
</button>
</div>
</td>
</a>
{/each}
</div>
</td>
<td class="node_icons">
<div class="icons_wrap">
{#if child.abuse_type !== undefined}
<i class="icon" title="This file / directory has received an abuse report. It cannot be shared">block</i>
{:else if child.is_shared()}
<a
href="/d/{child.id}"
onclick={e => file_event(FileAction.Share, index, e)}
class="button action_button"
>
<i class="icon" title="This file / directory is shared. Click to open public link">share</i>
</a>
{/if}
{#if child.properties !== undefined && child.properties.branding_enabled === "true" && !hide_branding}
<button class="action_button" onclick={e => file_event(FileAction.Branding, index, e)}>
<i class="icon">palette</i>
</button>
{/if}
{#if !hide_edit}
<button class="action_button" onclick={e => file_event(FileAction.Menu, index, e)}>
<i class="icon">menu</i>
</button>
{/if}
</div>
</td>
</tr>
{/each}
</tbody>
</table>
<style>
.directory {
display: table;
margin: 8px auto 16px auto;
background: var(--shaded_background);
background: var(--body_background);
border-collapse: collapse;
border-radius: 8px;
max-width: 99%;
width: 1200px;
/* backdrop-filter: blur(4px); */
max-width: 1200px;
margin: auto; /* center */
}
.directory > * {
display: table-row;
@@ -91,11 +98,14 @@ export let hide_branding = false
.directory > * > * {
display: table-cell;
}
td {
padding: 0;
}
.node {
display: table-row;
text-decoration: none;
color: var(--body_text-color);
padding: 6px;
cursor: pointer;
}
.node:not(:last-child) {
border-bottom: 1px solid var(--separator);
@@ -110,13 +120,14 @@ export let hide_branding = false
color: var(--highlight_text_color);
}
td {
padding: 2px;
vertical-align: middle;
}
.node_icon {
height: 32px;
width: 32px;
vertical-align: middle;
border-radius: 4px;
/* border-radius: 4px; */
margin: 2px;
}
.node_name {
@@ -124,6 +135,10 @@ td {
line-height: 1.2em;
word-break: break-all;
}
.title_link {
text-decoration: none;
color: var(--body_text_color);
}
.node_size {
min-width: 5em;
white-space: nowrap;

View File

@@ -1,18 +1,21 @@
<script lang="ts">
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 { loading_finish, loading_start } from "lib/Loading";
export let nav: FSNavigator
let { nav }: {
nav: FSNavigator;
} = $props();
let search_bar: HTMLInputElement
let error = ""
let search_term = ""
let search_results: string[] = []
let selected_result = 0
let search_bar: HTMLInputElement = $state()
let error = $state("")
let search_term = $state("")
let search_results: string[] = $state([])
let selected_result = $state(0)
let searching = false
let last_searched_term = ""
let last_limit = 10
let last_limit = $state(10)
onMount(() => {
// Clear results when the user moves to a new directory
@@ -43,9 +46,9 @@ const search = async (limit = 10) => {
last_limit = limit
searching = true
nav.set_loading(true)
try {
loading_start()
search_results = await fs_search(nav.base.path, search_term, limit)
} catch (err) {
if (err.value) {
@@ -54,6 +57,8 @@ const search = async (limit = 10) => {
alert(err)
console.error(err)
}
} finally {
loading_finish()
}
if (search_results.length > 0 && selected_result > search_results.length-1) {
@@ -61,7 +66,6 @@ const search = async (limit = 10) => {
}
searching = false
nav.set_loading(false)
// It's possible that the user entered another letter while we were
// 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
const submit_search = () => {
const submit_search = (e: Event) => {
e.preventDefault()
if (search_results.length !== 0) {
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)
clear_search(false)
}
@@ -142,7 +150,7 @@ const window_keydown = (e: KeyboardEvent) => {
}
</script>
<svelte:window on:keydown={window_keydown} />
<svelte:window onkeydown={window_keydown} />
{#if error === "path_not_found" || error === "node_is_a_directory"}
<div class="highlight_yellow center">
@@ -158,7 +166,7 @@ const window_keydown = (e: KeyboardEvent) => {
{/if}
<div class="center">
<form class="search_form" on:submit|preventDefault={submit_search}>
<form class="search_form" onsubmit={submit_search}>
<i class="icon">search</i>
<input
bind:this={search_bar}
@@ -167,12 +175,12 @@ const window_keydown = (e: KeyboardEvent) => {
placeholder="Press / to search in {$nav.base.name}"
style="width: 100%;"
bind:value={search_term}
on:keydown={input_keydown}
on:keyup={input_keyup}
onkeydown={input_keydown}
onkeyup={input_keyup}
/>
{#if search_term !== ""}
<!-- 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>
</button>
{/if}
@@ -186,7 +194,7 @@ const window_keydown = (e: KeyboardEvent) => {
{#each search_results as result, index}
<a
href={"/d"+fs_encode_path(result)}
on:click|preventDefault={() => open_result(index)}
onclick={(e) => open_result(index, e)}
class="node"
class:node_selected={selected_result === index}
>
@@ -201,7 +209,7 @@ const window_keydown = (e: KeyboardEvent) => {
{#if search_results.length === last_limit}
<div class="node">
<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>
More results
</button>
@@ -218,7 +226,6 @@ const window_keydown = (e: KeyboardEvent) => {
max-width: 100%;
padding-top: 2px;
padding-bottom: 2px;
border-bottom: 1px solid var(--separator);
}
.search_form {

View File

@@ -1,4 +1,4 @@
<script lang="ts" context="module">
<script lang="ts" module>
export type UploadJob = {
task_id: number,
file: File,
@@ -11,15 +11,14 @@ export type UploadJob = {
</script>
<script lang="ts">
import { tick } from "svelte";
import { fade } from "svelte/transition";
import UploadProgress from "./UploadProgress.svelte";
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
let file_input_field: HTMLInputElement = $state()
let file_input_change = (e: Event) => {
// Start uploading the files async
upload_files((e.target as HTMLInputElement).files)
@@ -31,8 +30,8 @@ export const pick_files = () => {
file_input_field.click()
}
let visible = false
let upload_queue: UploadJob[] = [];
let visible = $state(false)
let upload_queue: UploadJob[] = $state([]);
let task_id_counter = 0
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
upload_queue = upload_queue
if (active_uploads === 0 && state !== "uploading") {
state = "uploading"
if (active_uploads === 0 && status !== "uploading") {
status = "uploading"
visible = true
await tick()
await start_upload()
@@ -88,30 +87,37 @@ export const upload_file = async (file: File) => {
}
let active_uploads = 0
let state = "idle"
let status = $state("idle")
const start_upload = async () => {
// Count the number of active uploads so we can know how many new uploads we
// can start
active_uploads = upload_queue.reduce((acc, val) => {
if (val.status === "uploading") {
acc++
}
return acc
}, 0)
for (let i = 0; i < upload_queue.length && active_uploads < max_concurrent_uploads; i++) {
active_uploads = 0
let uploading_size = 0
for (let i = 0; i < upload_queue.length; i++) {
if (upload_queue[i]) {
// If this file is queued, start the upload
if (upload_queue[i].status === "queued") {
active_uploads++
upload_queue[i].component.start()
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) {
state = "finished"
status = "finished"
nav.reload()
// 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
setTimeout(() => {
if (state === "finished") {
if (status === "finished") {
visible = false
}
}, 10000)
} else {
state = "uploading"
status = "uploading"
}
}
@@ -135,7 +141,7 @@ const finish_upload = () => {
}
const leave_confirmation = (e: BeforeUnloadEvent) => {
if (state === "uploading") {
if (status === "uploading") {
e.preventDefault()
return "If you close this page your files will stop uploading. Do you want to continue?"
} else {
@@ -144,29 +150,29 @@ const leave_confirmation = (e: BeforeUnloadEvent) => {
}
</script>
<svelte:window on:beforeunload={leave_confirmation} />
<svelte:window onbeforeunload={leave_confirmation} />
<input
bind:this={file_input_field}
on:change={file_input_change}
onchange={file_input_change}
class="upload_input" type="file" name="file" multiple
/>
{#if visible}
<div class="upload_widget" transition:fade={{duration: 200}}>
<div class="upload_widget">
<div class="header">
{#if state === "idle"}
{#if status === "idle"}
Waiting for files
{:else if state === "uploading"}
{:else if status === "uploading"}
Uploading files...
{:else if state === "finished"}
{:else if status === "finished"}
Done
{/if}
</div>
<div class="body">
{#each upload_queue as job}
{#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}
{/each}
</div>
@@ -184,7 +190,8 @@ const leave_confirmation = (e: BeforeUnloadEvent) => {
position: fixed;
display: flex;
flex-direction: column;
width: 500px;
width: auto;
min-width: 400px;
max-width: 80%;
height: auto;
max-height: 50%;

View File

@@ -9,7 +9,7 @@
//
// 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
export const upload_file = (

View File

@@ -1,17 +1,23 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { fade } from "svelte/transition";
import { upload_file } from "./UploadFunc";
import ProgressBar from "util/ProgressBar.svelte";
import Button from "layout/Button.svelte"
import type { UploadJob } from "./FSUploadWidget.svelte";
let dispatch = createEventDispatcher()
export let job: UploadJob
export let total = 0
export let loaded = 0
let error_code = ""
let error_message = ""
let {
job = $bindable(),
total = $bindable(0),
loaded = $bindable(0),
finish,
}: {
job: UploadJob
total?: number
loaded?: number
finish: () => void
} = $props();
let error_code = $state("")
let error_message = $state("")
let xhr: XMLHttpRequest = null
export const start = () => {
@@ -24,7 +30,7 @@ export const start = () => {
},
async () => {
job.status = "finished"
dispatch("finished")
finish()
},
(code, message) => {
if (job.status === "finished") {
@@ -52,18 +58,18 @@ const cancel = () => {
}
xhr = null
dispatch("finished")
finish()
}
</script>
<div class="prog" transition:fade={{duration: 200}} class:error={job.status === "error"}>
<div class="prog" class:error={job.status === "error"}>
<div class="bar">
{job.file.name}<br/>
{#if error_code !== ""}
{error_message}<br/>
{error_code}<br/>
{/if}
<ProgressBar total={total} used={loaded}/>
<ProgressBar total={total} used={loaded} speed={500}/>
</div>
<div class="cancel">
<Button icon="cancel" click={cancel}/>

View File

@@ -1,10 +1,18 @@
<script lang="ts">
import { preventDefault } from 'svelte/legacy';
import type { FSNavigator } from "filesystem/FSNavigator";
export let nav: FSNavigator
export let path = ""
let {
nav,
path = "",
children
}: {
nav: FSNavigator;
path?: string;
children?: import('svelte').Snippet;
} = $props();
</script>
<a href={"/d"+path} on:click|preventDefault={() => {nav.navigate(path, true)}}>
<slot></slot>
<a href={"/d"+path} onclick={preventDefault(() => {nav.navigate(path, true)})}>
{@render children?.()}
</a>

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { onMount, tick } from "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 Audio from "./Audio.svelte";
import File from "./File.svelte";
@@ -15,13 +15,22 @@ import CustomBanner from "./CustomBanner.svelte";
import type { FSNavigator } from "filesystem/FSNavigator";
import FsUploadWidget from "filesystem/upload_widget/FSUploadWidget.svelte";
import EditWindow from "filesystem/edit_window/EditWindow.svelte";
import DetailsWindow from "filesystem/DetailsWindow.svelte";
export let nav: FSNavigator
export let upload_widget: FsUploadWidget
export let edit_window: EditWindow
let {
nav,
upload_widget,
edit_window,
details_window,
}: {
nav: FSNavigator
upload_widget: FsUploadWidget
edit_window: EditWindow
details_window: DetailsWindow
} = $props();
let viewer: any
let viewer_type = ""
let viewer: any = $state()
let viewer_type = $state("")
let last_path = ""
onMount(() => nav.subscribe(state_update))
@@ -74,7 +83,7 @@ export const seek = (delta: number) => {
{#if viewer_type === ""}
<div class="center">
<Spinner></Spinner>
<Spinner/>
</div>
{:else if viewer_type === "dir"}
<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"}
<Image nav={nav} bind:this={viewer}/>
{: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"}
<Pdf nav={nav} bind:this={viewer}/>
<Pdf node={$nav.base} bind:this={viewer}/>
{:else if viewer_type === "text"}
<Text nav={nav} bind:this={viewer}>
<Text node={$nav.base} bind:this={viewer}>
<CustomBanner path={$nav.path}/>
</Text>
{:else if viewer_type === "torrent"}
<Torrent nav={nav} bind:this={viewer} on:download>
<Torrent node={$nav.base} bind:this={viewer}>
<CustomBanner path={$nav.path}/>
</Torrent>
{:else if viewer_type === "zip"}
<Zip nav={nav} bind:this={viewer} on:download>
<Zip node={$nav.base} bind:this={viewer}>
<CustomBanner path={$nav.path}/>
</Zip>
{:else}
<File nav={nav} on:download on:details>
<File node={$nav.base} open_details={() => details_window.toggle()}>
<CustomBanner path={$nav.path}/>
</File>
{/if}

View File

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

View File

@@ -1,13 +1,12 @@
<script lang="ts">
import { fs_path_url } from "filesystem/FilesystemAPI";
import type { FSNavigator } from "filesystem/FSNavigator";
import { fs_path_url, FSNode } from "lib/FilesystemAPI.svelte";
export let nav: FSNavigator
let { node }: { node: FSNode } = $props();
</script>
<iframe
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">
</iframe>

View File

@@ -1,31 +1,37 @@
<script lang="ts">
import { tick } from "svelte";
import { fs_path_url, type FSNode } from "filesystem/FilesystemAPI";
import type { FSNavigator } from "filesystem/FSNavigator";
import { fs_path_url, type FSNode } from "lib/FilesystemAPI.svelte";
export let nav: FSNavigator
let text_type = "text"
let {
node,
children
}: {
node: FSNode
children?: import('svelte').Snippet;
} = $props();
let text_type = $state("text")
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."
return
}
if (
nav.base.file_type.startsWith("text/markdown") ||
nav.base.name.endsWith(".md") ||
nav.base.name.endsWith(".markdown")
node.file_type.startsWith("text/markdown") ||
node.name.endsWith(".md") ||
node.name.endsWith(".markdown")
) {
markdown(nav.base)
markdown(node)
} else {
text(nav.base)
text(node)
}
}
let text_pre: HTMLPreElement
let text_pre: HTMLPreElement = $state()
const text = async (file: FSNode) => {
text_type = "text"
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) => {
text_type = "markdown"
await tick()
@@ -61,7 +67,7 @@ const markdown = async (file: FSNode) => {
</script>
<div class="container">
<slot></slot>
{@render children?.()}
{#if text_type === "markdown"}
<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 = {
trackers: string[]
comment: string,
@@ -13,26 +13,29 @@ export type TorrentFile = {
};
</script>
<script lang="ts">
import { createEventDispatcher } from "svelte";
import Magnet from "icons/Magnet.svelte";
import { formatDate } from "util/Formatting"
import TorrentItem from "./TorrentItem.svelte"
import IconBlock from "layout/IconBlock.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 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 = "loading"
let status = $state("loading")
export const update = async () => {
try {
nav.set_loading(true)
let resp = await fetch(fs_path_url(nav.base.path)+"?torrent_info")
loading_start()
let resp = await fetch(fs_path_url(node.path)+"?torrent_info")
if (resp.status >= 400) {
let json = await resp.json()
@@ -58,20 +61,20 @@ export const update = async () => {
} catch (err) {
console.error(err)
} finally {
nav.set_loading(false)
loading_finish()
status = "finished"
}
}
let torrent: TorrentInfo = {} as TorrentInfo
let magnet = ""
let torrent: TorrentInfo = $state({} as TorrentInfo)
let magnet = $state("")
</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"}
Created by: {torrent.created_by}<br/>
Comment: {torrent.comment}<br/>
@@ -92,7 +95,7 @@ let magnet = ""
Torrent file could not be parsed. It may be corrupted.
</p>
{/if}
<button on:click={() => {dispatch("download")}} class="button">
<button onclick={() => node.download()} class="button">
<i class="icon">download</i>
<span>Download torrent file</span>
</button>

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,11 @@
<script lang="ts">
import ZipItem from './ZipItem.svelte';
import type { ZipEntry } from "filesystem/viewers/Zip.svelte";
import { formatDataVolume } from "util/Formatting";
export let item: ZipEntry = {} as ZipEntry
let { item = {} as ZipEntry }: {
item?: ZipEntry;
} = $props();
</script>
<!-- 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 -->
{#if child.details_open}
<svelte:self item={child}></svelte:self>
<ZipItem item={child}></ZipItem>
{/if}
</details>
{/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 { formatDate } from "util/Formatting";
let result = null;
let result = $state(null);
onMount(async () => {
try {
@@ -21,9 +21,11 @@ onMount(async () => {
{#if result !== null && result.user_banned}
<section>
<Expandable click_expand>
<div slot="header" class="header red">
Your account has been banned, click for details
</div>
{#snippet header()}
<div class="header red">
Your account has been banned, click for details
</div>
{/snippet}
<p>
Your user account has been banned from uploading to
pixeldrain due to violation of the
@@ -72,13 +74,16 @@ onMount(async () => {
{:else if result !== null && result.ip_offences.length > 0}
<section>
<Expandable click_expand>
<div slot="header" class="header" class:red={result.ip_banned} class:yellow={!result.ip_banned}>
{#if result.ip_banned}
Your IP address has been banned, click for details
{:else}
Your IP address has received a copyright strike, click for details
{/if}
</div>
{#snippet header()}
<div class="header" class:red={result.ip_banned} class:yellow={!result.ip_banned}>
{#if result.ip_banned}
Your IP address has been banned, click for details
{:else}
Your IP address has received a copyright strike, click for details
{/if}
</div>
{/snippet}
{#if result.ip_banned}
<p>
Your IP address ({result.address}) has been banned from

View File

@@ -15,7 +15,7 @@ import OtherPlans from "./OtherPlans.svelte";
</p>
</section>
<div class="vertical_scroll">
<div class="horizontal_scroll">
<div class="grid">
<div></div>
<div class="top_row free_feat">
@@ -36,10 +36,12 @@ import OtherPlans from "./OtherPlans.svelte";
</div>
<div class="feature_cell pro_feat">
<Tooltip>
<span slot="label">
<span class="bold">€4 / month</span> or
<span class="bold">€40 / year</span>
</span>
{#snippet label()}
<span>
<span class="bold">€4 / month</span> or
<span class="bold">€40 / year</span>
</span>
{/snippet}
The Pro subscription is managed by Patreon. Patreon's own fees
and sales tax will be added to this price. After paying you need
to link your pixeldrain account to Patreon to activate the plan.
@@ -47,7 +49,9 @@ import OtherPlans from "./OtherPlans.svelte";
</div>
<div class="feature_cell prepaid_feat">
<Tooltip>
<span slot="label" class="bold">€1 / month minimum</span>
{#snippet label()}
<span class="bold">€1 / month minimum</span>
{/snippet}
<p>
The minimum fee is only charged when usage is less than €1.
This calculation is per day, the €1 amount is divided by the
@@ -62,7 +66,9 @@ import OtherPlans from "./OtherPlans.svelte";
</div>
<div class="feature_cell free_feat">
<Tooltip>
<span slot="label" class="bold">6 GB per day</span>
{#snippet label()}
<span class="bold">6 GB per day</span>
{/snippet}
<p>
Free users are limited to downloading 6 GB per day, this
limit is linked to your IP address, even if you are logged
@@ -77,7 +83,9 @@ import OtherPlans from "./OtherPlans.svelte";
</div>
<div class="feature_cell pro_feat">
<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>
The transfer limit is used for downloading, sharing and
hotlinking files.
@@ -89,7 +97,9 @@ import OtherPlans from "./OtherPlans.svelte";
</div>
<div class="feature_cell prepaid_feat">
<Tooltip>
<span slot="label" class="bold">€1 per TB transferred</span>
{#snippet label()}
<span class="bold">€1 per TB transferred</span>
{/snippet}
<p>
Prepaid does not have a transfer limit, instead you are
charged for what you use at a rate of €1 per terabyte
@@ -132,7 +142,9 @@ import OtherPlans from "./OtherPlans.svelte";
</div>
<div class="feature_cell free_feat">
<Tooltip>
<span slot="label" class="bold">120 days (4 months)</span>
{#snippet label()}
<span class="bold">120 days (4 months)</span>
{/snippet}
<p>
Files expire when they have not been downloaded in the last
120 days. A download is counted when more than 10% of the
@@ -142,7 +154,9 @@ import OtherPlans from "./OtherPlans.svelte";
</div>
<div class="feature_cell pro_feat">
<Tooltip>
<span slot="label" class="bold">240 days (8 months)</span>
{#snippet label()}
<span class="bold">240 days (8 months)</span>
{/snippet}
<p>
The Pro plan has 240 day file expiry. The same rules apply
as the free plan. Higher Patreon subscription plans are
@@ -152,7 +166,9 @@ import OtherPlans from "./OtherPlans.svelte";
</div>
<div class="feature_cell prepaid_feat">
<Tooltip>
<span slot="label" class="bold">Files do not expire</span>
{#snippet label()}
<span class="bold">Files do not expire</span>
{/snippet}
<p>
Files don't expire while your Prepaid plan is active. If
your credit runs out you have one week to top up your
@@ -271,20 +287,21 @@ import OtherPlans from "./OtherPlans.svelte";
font-weight: bold;
}
.vertical_scroll {
.horizontal_scroll {
overflow-x: scroll;
overflow-y: hidden;
width: 100%;
padding: 10px;
}
.grid {
display: inline-grid;
display: grid;
grid-auto-flow: row;
grid-template-columns: 9em 1fr 1fr 1fr;
min-width: 40em;
min-width: 50em;
max-width: 70em;
gap: 5px;
margin: 10px;
margin: auto;
}
.grid > div {
justify-content: center;

View File

@@ -5,7 +5,7 @@ import { drop_target } from "lib/DropTarget";
import AddressReputation from "./AddressReputation.svelte";
import FeatureTable from "./FeatureTable.svelte";
import GetStarted from "./GetStarted.svelte";
import UploadWidget from "./UploadWidget.svelte";
import Pricing from "./Pricing.svelte";
let upload_widget
</script>
@@ -51,6 +51,7 @@ let upload_widget
Bullet lists
</li>
</ul>
<Pricing/>
</section>
</div>
@@ -142,26 +143,18 @@ let upload_widget
<FeatureTable/>
</div>
<Footer nobg/>
<svelte:head>
<style>
body {
background-image: url("/res/img/catspaw.webp");
background-repeat: no-repeat;
background-attachment: fixed;
background-position: center;
background-size: cover;
}
</style>
</svelte:head>
<style>
:global(.page_body) {
background-image: url("/res/img/inflating_star.webp");
background-repeat: no-repeat;
background-attachment: fixed;
background-position: center;
background-size: cover;
}
.page_content {
margin-top: 16px;
margin-bottom: 16px;
}
@media (max-width: 1100px) {
.page_content {
margin-top: 0;
}
}
header {
padding-top: 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>
import { run } from 'svelte/legacy';
import { onMount } from "svelte";
import Euro from "util/Euro.svelte";
import ProgressBar from "util/ProgressBar.svelte";
let pixeldrain_storage = 0
let pixeldrain_egress = 0
let pixeldrain_total = 0
let backblaze_storage = 0
let backblaze_egress = 0
let backblaze_api = 0
let backblaze_total = 0
let wasabi_storage = 0
let wasabi_total = 0
let pixeldrain_storage = $state(0)
let pixeldrain_egress = $state(0)
let pixeldrain_total = $state(0)
let backblaze_storage = $state(0)
let backblaze_egress = $state(0)
let backblaze_api = $state(0)
let backblaze_total = $state(0)
let wasabi_storage = $state(0)
let wasabi_total = $state(0)
let price_amazon = 0
let price_azure = 0
let price_google = 0
let price_max = 0
let price_max = $state(0)
let storage = 10 // TB
let egress = 10 // TB
let avg_file_size = 1000 // kB
let storage = $state(10) // TB
let egress = $state(10) // TB
let avg_file_size = $state(1000) // kB
$: {
run(() => {
pixeldrain_storage = storage * 4
pixeldrain_egress = egress * 1
pixeldrain_total = pixeldrain_storage + pixeldrain_egress
@@ -43,7 +44,7 @@ $: {
// price_google = (storage * 20) + (egress * 20)
price_max = Math.max(pixeldrain_total, backblaze_total, wasabi_total, price_amazon, price_azure, price_google)
}
});
onMount(() => {})
</script>

View File

@@ -1,8 +1,12 @@
<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">
<g>
<g>
<path d="M216.856339,16.5966031 C200.285002,8.84328665 182.566144,3.2084988 164.041564,0 C161.766523,4.11318106 159.108624,9.64549908 157.276099,14.0464379 C137.583995,11.0849896 118.072967,11.0849896 98.7430163,14.0464379 C96.9108417,9.64549908 94.1925838,4.11318106 91.8971895,0 C73.3526068,3.2084988 55.6133949,8.86399117 39.0420583,16.6376612 C5.61752293,67.146514 -3.4433191,116.400813 1.08711069,164.955721 C23.2560196,181.510915 44.7403634,191.567697 65.8621325,198.148576 C71.0772151,190.971126 75.7283628,183.341335 79.7352139,175.300261 C72.104019,172.400575 64.7949724,168.822202 57.8887866,164.667963 C59.7209612,163.310589 61.5131304,161.891452 63.2445898,160.431257 C105.36741,180.133187 151.134928,180.133187 192.754523,160.431257 C194.506336,161.891452 196.298154,163.310589 198.110326,164.667963 C191.183787,168.842556 183.854737,172.420929 176.223542,175.320965 C180.230393,183.341335 184.861538,190.991831 190.096624,198.16893 C211.238746,191.588051 232.743023,181.531619 254.911949,164.955721 C260.227747,108.668201 245.831087,59.8662432 216.856339,16.5966031 Z M85.4738752,135.09489 C72.8290281,135.09489 62.4592217,123.290155 62.4592217,108.914901 C62.4592217,94.5396472 72.607595,82.7145587 85.4738752,82.7145587 C98.3405064,82.7145587 108.709962,94.5189427 108.488529,108.914901 C108.508531,123.290155 98.3405064,135.09489 85.4738752,135.09489 Z M170.525237,135.09489 C157.88039,135.09489 147.510584,123.290155 147.510584,108.914901 C147.510584,94.5396472 157.658606,82.7145587 170.525237,82.7145587 C183.391518,82.7145587 193.761324,94.5189427 193.539891,108.914901 C193.539891,123.290155 183.391518,135.09489 170.525237,135.09489 Z" fill="currentColor" fill-rule="nonzero">
</path>
</g>
</g>
</svg>

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">
<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">
<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