Compare commits
9 Commits
fd5cd0bfd1
...
master
Author | SHA1 | Date | |
---|---|---|---|
4de6331551 | |||
b409ff009d | |||
75d9ed3023 | |||
6d89c5ddd9 | |||
f936e4c0f2 | |||
9a72c85019 | |||
06d04a1abc | |||
c616b2da7f | |||
f4b518edb7 |
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
BIN
res/static/img/carina.webp
Normal file
After Width: | Height: | Size: 195 KiB |
BIN
res/static/img/catspaw.webp
Normal file
After Width: | Height: | Size: 361 KiB |
BIN
res/static/img/fnx_logo.png
Normal file
After Width: | Height: | Size: 777 KiB |
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 77 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 2.3 KiB |
@@ -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,
|
||||
|
@@ -17,7 +17,6 @@
|
||||
</section>
|
||||
</div>
|
||||
{{template "page_bottom" .}}
|
||||
{{template "analytics"}}
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
|
@@ -21,7 +21,6 @@
|
||||
</section>
|
||||
</div>
|
||||
{{template "page_bottom" .}}
|
||||
{{template "analytics"}}
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
|
@@ -18,7 +18,6 @@
|
||||
</section>
|
||||
</div>
|
||||
{{template "page_bottom" .}}
|
||||
{{template "analytics"}}
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
|
@@ -27,7 +27,6 @@
|
||||
</section>
|
||||
</div>
|
||||
{{template "page_bottom" .}}
|
||||
{{template "analytics"}}
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
|
@@ -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}}
|
@@ -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}}
|
@@ -140,7 +140,6 @@
|
||||
</section>
|
||||
</div>
|
||||
{{template "page_bottom" .}}
|
||||
{{template "analytics"}}
|
||||
</body>
|
||||
</html>
|
||||
{{ end }}
|
||||
|
@@ -45,7 +45,6 @@
|
||||
{{end}}
|
||||
|
||||
{{template "page_bottom" .}}
|
||||
{{template "analytics"}}
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
|
@@ -1 +0,0 @@
|
||||
{{define "analytics"}}<script defer data-domain="pixeldrain.com" src="https://stats.pixeldrain.com/js/script.js"></script>{{end}}
|
@@ -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}}
|
@@ -15,7 +15,6 @@
|
||||
</section>
|
||||
</div>
|
||||
{{template "page_bottom" .}}
|
||||
{{template "analytics"}}
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
|
@@ -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}}
|
@@ -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}}
|
@@ -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}}
|
@@ -19,7 +19,6 @@
|
||||
></iframe>
|
||||
</div>
|
||||
{{template "page_bottom" .}}
|
||||
{{template "analytics"}}
|
||||
</body>
|
||||
</html>
|
||||
{{ end }}
|
||||
|
@@ -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}}
|
@@ -35,7 +35,6 @@
|
||||
</section>
|
||||
</div>
|
||||
{{template "page_bottom" .}}
|
||||
{{template "analytics"}}
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
|
@@ -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
@@ -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",
|
||||
|
@@ -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: {
|
||||
|
@@ -1,8 +0,0 @@
|
||||
import App from './admin_panel/Router.svelte';
|
||||
|
||||
const app = new App({
|
||||
target: document.getElementById("page_body"),
|
||||
props: {}
|
||||
});
|
||||
|
||||
export default app;
|
@@ -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}
|
||||
|
@@ -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>
|
||||
|
@@ -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}/>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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}
|
||||
|
@@ -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}
|
||||
|
@@ -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>
|
||||
|
@@ -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/>
|
||||
|
@@ -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>
|
||||
|
@@ -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">
|
||||
|
@@ -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}
|
||||
|
@@ -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>
|
||||
|
@@ -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"/>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -1,8 +0,0 @@
|
||||
import App from './filesystem/Filesystem.svelte';
|
||||
|
||||
const app = new App({
|
||||
target: document.body,
|
||||
props: {}
|
||||
});
|
||||
|
||||
export default app;
|
@@ -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;
|
||||
|
@@ -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>
|
||||
|
@@ -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
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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}
|
||||
|
@@ -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}
|
||||
|
@@ -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>
|
||||
|
@@ -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">
|
||||
|
@@ -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))
|
||||
|
||||
|
@@ -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"] {
|
||||
|
@@ -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>
|
||||
|
@@ -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) {
|
||||
|
@@ -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}
|
||||
|
@@ -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>
|
||||
|
@@ -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}
|
||||
|
@@ -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;
|
||||
|
@@ -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"/>
|
||||
|
@@ -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;
|
||||
|
@@ -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
|
||||
|
70
svelte/src/filesystem/filemanager/FileMenu.svelte
Normal 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>
|
@@ -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>
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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 {
|
||||
|
@@ -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%;
|
||||
|
@@ -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 = (
|
||||
|
@@ -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}/>
|
||||
|
@@ -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>
|
||||
|
@@ -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}
|
||||
|
@@ -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}
|
||||
|
@@ -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
|
||||
|
@@ -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}
|
||||
|
@@ -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)}
|
||||
|
@@ -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>
|
||||
|
||||
|
@@ -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">
|
||||
|
@@ -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>
|
||||
|
@@ -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}
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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}
|
||||
|
@@ -1,8 +0,0 @@
|
||||
import App from './home_page/HomePage.svelte';
|
||||
|
||||
const app = new App({
|
||||
target: document.getElementById("page_body"),
|
||||
props: {}
|
||||
});
|
||||
|
||||
export default app;
|
@@ -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
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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>
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
@@ -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>
|
||||
|
@@ -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>
|
@@ -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
|
||||
|