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;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
/* This makes sure that no scrollbar shows up when the menu is open on small screens*/
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: system-ui, sans-serif;
|
font-family: system-ui, sans-serif;
|
||||||
@@ -76,12 +70,6 @@ body {
|
|||||||
transition: opacity 0.2s;
|
transition: opacity 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkers {
|
|
||||||
background-image: var(--background_pattern);
|
|
||||||
background-color: var(--background_pattern_color);
|
|
||||||
background-repeat: repeat;
|
|
||||||
}
|
|
||||||
|
|
||||||
header,
|
header,
|
||||||
footer {
|
footer {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -90,24 +78,16 @@ footer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
background-image: url("/res/img/nebula.webp");
|
background-color: var(--shaded_background);
|
||||||
background-color: var(--background_color);
|
backdrop-filter: blur(4px);
|
||||||
background-blend-mode: luminosity;
|
border-top: 1px solid var(--separator);
|
||||||
box-shadow: inset 0 0 10px -4px var(--shadow_color);
|
|
||||||
border-radius: 8px;
|
|
||||||
margin: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
footer>.footer_content {
|
footer>.footer_content {
|
||||||
background: var(--body_background);
|
|
||||||
color: var(--body_text_color);
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 1000px;
|
width: 100%;
|
||||||
max-width: 100%;
|
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
border-radius: 8px;
|
|
||||||
margin: 120px 0 60px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
header>h1 {
|
header>h1 {
|
||||||
@@ -173,52 +153,17 @@ pre>code {
|
|||||||
transition: left 0.5s;
|
transition: left 0.5s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page_body {
|
|
||||||
position: relative;
|
|
||||||
right: 0;
|
|
||||||
height: auto;
|
|
||||||
left: 0;
|
|
||||||
margin-left: 300px;
|
|
||||||
min-width: 300px;
|
|
||||||
display: block;
|
|
||||||
text-align: center;
|
|
||||||
/* Center the header and body */
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: hidden;
|
|
||||||
transition: margin 0.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page_content {
|
.page_content {
|
||||||
background: var(--body_background);
|
background: var(--shaded_background);
|
||||||
border-radius: 6px;
|
backdrop-filter: blur(4px);
|
||||||
overflow: hidden;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page_content,
|
@media (max-width: 1000px) {
|
||||||
.page_margins,
|
|
||||||
footer {
|
|
||||||
margin-right: 20px;
|
|
||||||
margin-left: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1100px) {
|
|
||||||
.page_navigation {
|
.page_navigation {
|
||||||
left: -300px;
|
left: -300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page_body {
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page_content,
|
|
||||||
.page_margins,
|
|
||||||
footer {
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
header>h1 {
|
header>h1 {
|
||||||
/* We want the header text to appear below the menu button, so the top
|
/* We want the header text to appear below the menu button, so the top
|
||||||
margin needs to be fairly large when the screen is small */
|
margin needs to be fairly large when the screen is small */
|
||||||
@@ -537,6 +482,12 @@ input[type="color"] {
|
|||||||
line-height: 1.3em;
|
line-height: 1.3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button.flat {
|
||||||
|
background: none;
|
||||||
|
color: var(--body_text_color);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
button:hover,
|
button:hover,
|
||||||
.button:hover,
|
.button:hover,
|
||||||
input[type="submit"]:hover,
|
input[type="submit"]:hover,
|
||||||
|
@@ -17,7 +17,6 @@
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
{{template "page_bottom" .}}
|
{{template "page_bottom" .}}
|
||||||
{{template "analytics"}}
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
@@ -21,7 +21,6 @@
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
{{template "page_bottom" .}}
|
{{template "page_bottom" .}}
|
||||||
{{template "analytics"}}
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
@@ -18,7 +18,6 @@
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
{{template "page_bottom" .}}
|
{{template "page_bottom" .}}
|
||||||
{{template "analytics"}}
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
@@ -27,7 +27,6 @@
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
{{template "page_bottom" .}}
|
{{template "page_bottom" .}}
|
||||||
{{template "analytics"}}
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
{{end}}
|
{{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>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
{{template "page_bottom" .}}
|
{{template "page_bottom" .}}
|
||||||
{{template "analytics"}}
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
@@ -45,7 +45,6 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{template "page_bottom" .}}
|
{{template "page_bottom" .}}
|
||||||
{{template "analytics"}}
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
{{end}}
|
{{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>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
{{template "page_bottom" .}}
|
{{template "page_bottom" .}}
|
||||||
{{template "analytics"}}
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
{{end}}
|
{{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>
|
></iframe>
|
||||||
</div>
|
</div>
|
||||||
{{template "page_bottom" .}}
|
{{template "page_bottom" .}}
|
||||||
{{template "analytics"}}
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
{{ end }}
|
{{ 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>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
{{template "page_bottom" .}}
|
{{template "page_bottom" .}}
|
||||||
{{template "analytics"}}
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
{{define "filesystem"}}
|
{{define "wrap"}}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
@@ -23,13 +23,12 @@
|
|||||||
|
|
||||||
{{ template "opengraph" .OGData }}
|
{{ template "opengraph" .OGData }}
|
||||||
<script>
|
<script>
|
||||||
window.initial_node = {{.Other}};
|
|
||||||
window.user = {{.User}};
|
window.user = {{.User}};
|
||||||
window.api_endpoint = '{{.APIEndpoint}}';
|
window.api_endpoint = '{{.APIEndpoint}}';
|
||||||
|
window.server_hostname = "{{.Hostname}}";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script defer src='/res/svelte/filesystem.js?v{{cacheID}}'></script>
|
<script defer src='/res/svelte/wrap.js?v{{cacheID}}'></script>
|
||||||
{{template "analytics"}}
|
|
||||||
</head>
|
</head>
|
||||||
<body></body>
|
<body></body>
|
||||||
</html>
|
</html>
|
1921
svelte/package-lock.json
generated
@@ -18,7 +18,7 @@
|
|||||||
"rollup": "^4.24.4",
|
"rollup": "^4.24.4",
|
||||||
"rollup-plugin-livereload": "^2.0.5",
|
"rollup-plugin-livereload": "^2.0.5",
|
||||||
"rollup-plugin-svelte": "^7.2.2",
|
"rollup-plugin-svelte": "^7.2.2",
|
||||||
"svelte": "^4.2.19"
|
"svelte": "^5.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"behave-js": "^1.5.0",
|
"behave-js": "^1.5.0",
|
||||||
|
@@ -11,12 +11,7 @@ const production = !process.env.ROLLUP_WATCH;
|
|||||||
|
|
||||||
const builddir = "../res/static/svelte"
|
const builddir = "../res/static/svelte"
|
||||||
export default [
|
export default [
|
||||||
"filesystem",
|
"wrap",
|
||||||
"user_home",
|
|
||||||
"admin_panel",
|
|
||||||
"home_page",
|
|
||||||
"speedtest",
|
|
||||||
"login",
|
|
||||||
].map((name, index) => ({
|
].map((name, index) => ({
|
||||||
input: `src/${name}.js`,
|
input: `src/${name}.js`,
|
||||||
output: {
|
output: {
|
||||||
|
@@ -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,23 +1,26 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
import { formatDate, formatNumber } from "util/Formatting";
|
import { formatDate, formatNumber } from "util/Formatting";
|
||||||
import Expandable from "util/Expandable.svelte";
|
import Expandable from "util/Expandable.svelte";
|
||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from "svelte";
|
||||||
let dispatch = createEventDispatcher()
|
let dispatch = createEventDispatcher()
|
||||||
|
|
||||||
export let report
|
let {
|
||||||
export let ip_report_count
|
report,
|
||||||
let preview = false
|
ip_report_count
|
||||||
|
} = $props();
|
||||||
|
let preview = $state(false)
|
||||||
|
|
||||||
$: can_grant = report.status !== "granted"
|
let can_grant = $derived(report.status !== "granted")
|
||||||
$: can_reject = report.status !== "rejected"
|
let can_reject = $derived(report.status !== "rejected")
|
||||||
|
|
||||||
let set_status = async (action, report_type) => {
|
let set_status = async (action: string, report_type: string) => {
|
||||||
dispatch("resolve_report", {action: action, report_type: report_type})
|
dispatch("resolve_report", {action: action, report_type: report_type})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Expandable expanded={report.status === "pending"} click_expand>
|
<Expandable expanded={report.status === "pending"} click_expand>
|
||||||
<div slot="header" class="header">
|
{#snippet header()}
|
||||||
|
<div class="header">
|
||||||
<div class="icon_cell">
|
<div class="icon_cell">
|
||||||
<img class="file_icon" src={"/api/file/"+report.file.id+"/thumbnail"} alt="File thumbnail"/>
|
<img class="file_icon" src={"/api/file/"+report.file.id+"/thumbnail"} alt="File thumbnail"/>
|
||||||
</div>
|
</div>
|
||||||
@@ -39,35 +42,36 @@ let set_status = async (action, report_type) => {
|
|||||||
<div class="stats">V<br/>{formatNumber(report.file.views, 3)}</div>
|
<div class="stats">V<br/>{formatNumber(report.file.views, 3)}</div>
|
||||||
<div class="stats">DL<br/>{formatNumber(report.file.bandwidth_used / report.file.size, 3)}</div>
|
<div class="stats">DL<br/>{formatNumber(report.file.bandwidth_used / report.file.size, 3)}</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/snippet}
|
||||||
<div class="details">
|
<div class="details">
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<div class="action_list">
|
<div class="action_list">
|
||||||
<a class="button" target="_blank" href={"/u/"+report.file.id} rel="noreferrer">
|
<a class="button" target="_blank" href={"/u/"+report.file.id} rel="noreferrer">
|
||||||
<i class="icon">open_in_new</i> Open file
|
<i class="icon">open_in_new</i> Open file
|
||||||
</a>
|
</a>
|
||||||
<button class:button_highlight={preview} on:click={() => {preview = !preview}}>
|
<button class:button_highlight={preview} onclick={() => {preview = !preview}}>
|
||||||
<i class="icon">visibility</i> Preview
|
<i class="icon">visibility</i> Preview
|
||||||
</button>
|
</button>
|
||||||
{#if can_grant}
|
{#if can_grant}
|
||||||
<button class="button_highlight" on:click={() => {set_status("grant", report.type)}}>
|
<button class="button_highlight" onclick={() => {set_status("grant", report.type)}}>
|
||||||
<i class="icon">done</i> Block ({report.type})
|
<i class="icon">done</i> Block ({report.type})
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if can_reject}
|
{#if can_reject}
|
||||||
<button class="button_red" on:click={() => {set_status("reject", "")}}>
|
<button class="button_red" onclick={() => {set_status("reject", "")}}>
|
||||||
<i class="icon">delete</i> Ignore
|
<i class="icon">delete</i> Ignore
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="type_list">
|
<div class="type_list">
|
||||||
<button on:click={() => {set_status("grant", "copyright")}}>copyright</button>
|
<button onclick={() => {set_status("grant", "copyright")}}>copyright</button>
|
||||||
<button on:click={() => {set_status("grant", "terrorism")}}>terrorism</button>
|
<button onclick={() => {set_status("grant", "terrorism")}}>terrorism</button>
|
||||||
<button on:click={() => {set_status("grant", "gore")}}>gore</button>
|
<button onclick={() => {set_status("grant", "gore")}}>gore</button>
|
||||||
<button on:click={() => {set_status("grant", "child_abuse")}}>child_abuse</button>
|
<button onclick={() => {set_status("grant", "child_abuse")}}>child_abuse</button>
|
||||||
<button on:click={() => {set_status("grant", "zoophilia")}}>zoophilia</button>
|
<button onclick={() => {set_status("grant", "zoophilia")}}>zoophilia</button>
|
||||||
<button on:click={() => {set_status("grant", "malware")}}>malware</button>
|
<button onclick={() => {set_status("grant", "malware")}}>malware</button>
|
||||||
<button on:click={() => {set_status("grant", "doxing")}}>doxing</button>
|
<button onclick={() => {set_status("grant", "doxing")}}>doxing</button>
|
||||||
<button on:click={() => {set_status("grant", "revenge_porn")}}>revenge_porn</button>
|
<button onclick={() => {set_status("grant", "revenge_porn")}}>revenge_porn</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="text-align: center;">
|
<div style="text-align: center;">
|
||||||
@@ -101,12 +105,12 @@ let set_status = async (action, report_type) => {
|
|||||||
<td>{ip_report_count[user_report.ip_address]}</td>
|
<td>{ip_report_count[user_report.ip_address]}</td>
|
||||||
<td>
|
<td>
|
||||||
{#if can_grant}
|
{#if can_grant}
|
||||||
<button on:click={() => dispatch("resolve_by_ip", {ip: user_report.ip_address, action: "grant"})}>
|
<button onclick={() => dispatch("resolve_by_ip", {ip: user_report.ip_address, action: "grant"})}>
|
||||||
Accept all
|
Accept all
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if can_reject}
|
{#if can_reject}
|
||||||
<button on:click={() => dispatch("resolve_by_ip", {ip: user_report.ip_address, action: "reject"})}>
|
<button onclick={() => dispatch("resolve_by_ip", {ip: user_report.ip_address, action: "reject"})}>
|
||||||
Ignore all
|
Ignore all
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
@@ -1,18 +1,18 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import LoadingIndicator from "util/LoadingIndicator.svelte";
|
|
||||||
import AbuseReport from "./AbuseReport.svelte";
|
import AbuseReport from "./AbuseReport.svelte";
|
||||||
|
import { loading_finish, loading_start } from "lib/Loading";
|
||||||
|
|
||||||
let loading = true
|
let loading = true
|
||||||
let reports = []
|
let reports = $state([])
|
||||||
|
|
||||||
let startPicker
|
let startPicker = $state()
|
||||||
let endPicker
|
let endPicker = $state()
|
||||||
|
|
||||||
let tab = "pending"
|
let tab = $state("pending")
|
||||||
|
|
||||||
const get_reports = async () => {
|
const get_reports = async () => {
|
||||||
loading = true;
|
loading_start()
|
||||||
|
|
||||||
// Remove refresh timeout if there is one
|
// Remove refresh timeout if there is one
|
||||||
clearTimeout(refresh_timeout)
|
clearTimeout(refresh_timeout)
|
||||||
@@ -70,11 +70,11 @@ const get_reports = async () => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err);
|
alert(err);
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading_finish()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let ip_report_count = {}
|
let ip_report_count = $state({})
|
||||||
const count_ip_reports = () => {
|
const count_ip_reports = () => {
|
||||||
ip_report_count = {}
|
ip_report_count = {}
|
||||||
reports.forEach(v => {
|
reports.forEach(v => {
|
||||||
@@ -156,8 +156,6 @@ onMount(() => {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<LoadingIndicator loading={loading}/>
|
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<div class="toolbar" style="text-align: left;">
|
<div class="toolbar" style="text-align: left;">
|
||||||
<div>Reports: {reports.length}</div>
|
<div>Reports: {reports.length}</div>
|
||||||
@@ -165,19 +163,19 @@ onMount(() => {
|
|||||||
<div>Range:</div>
|
<div>Range:</div>
|
||||||
<input type="date" bind:this={startPicker}/>
|
<input type="date" bind:this={startPicker}/>
|
||||||
<input type="date" bind:this={endPicker}/>
|
<input type="date" bind:this={endPicker}/>
|
||||||
<button on:click={get_reports}>Go</button>
|
<button onclick={get_reports}>Go</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tab_bar">
|
<div class="tab_bar">
|
||||||
<button on:click={() => {tab = "pending"; get_reports()}} class:button_highlight={tab === "pending"}>
|
<button onclick={() => {tab = "pending"; get_reports()}} class:button_highlight={tab === "pending"}>
|
||||||
<i class="icon">flag</i>
|
<i class="icon">flag</i>
|
||||||
Pending
|
Pending
|
||||||
</button>
|
</button>
|
||||||
<button on:click={() => {tab = "granted"; get_reports()}} class:button_highlight={tab === "granted"}>
|
<button onclick={() => {tab = "granted"; get_reports()}} class:button_highlight={tab === "granted"}>
|
||||||
<i class="icon">flag</i>
|
<i class="icon">flag</i>
|
||||||
Granted
|
Granted
|
||||||
</button>
|
</button>
|
||||||
<button on:click={() => {tab = "rejected"; get_reports()}} class:button_highlight={tab === "rejected"}>
|
<button onclick={() => {tab = "rejected"; get_reports()}} class:button_highlight={tab === "rejected"}>
|
||||||
<i class="icon">flag</i>
|
<i class="icon">flag</i>
|
||||||
Rejected
|
Rejected
|
||||||
</button>
|
</button>
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, tick } from "svelte";
|
import { onMount, tick } from "svelte";
|
||||||
import LoadingIndicator from "util/LoadingIndicator.svelte";
|
|
||||||
import EmailReportersTable from "./EmailReportersTable.svelte";
|
import EmailReportersTable from "./EmailReportersTable.svelte";
|
||||||
import { get_endpoint } from "lib/PixeldrainAPI";
|
import { get_endpoint } from "lib/PixeldrainAPI";
|
||||||
|
import { loading_finish, loading_start } from "lib/Loading";
|
||||||
|
|
||||||
type Reporter = {
|
type Reporter = {
|
||||||
from_address: string,
|
from_address: string,
|
||||||
@@ -17,29 +17,28 @@ type Reporter = {
|
|||||||
last_message_html: string,
|
last_message_html: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
let loading = true
|
let reporters: Reporter[] = $state([])
|
||||||
let reporters: Reporter[] = []
|
let reporters_pending = $derived(reporters.reduce((acc, val) => {
|
||||||
$: reporters_pending = reporters.reduce((acc, val) => {
|
|
||||||
if (val.status === "pending") {
|
if (val.status === "pending") {
|
||||||
acc.push(val)
|
acc.push(val)
|
||||||
}
|
}
|
||||||
return acc
|
return acc
|
||||||
}, [])
|
}, []))
|
||||||
$: reporters_trusted = reporters.reduce((acc, val) => {
|
let reporters_trusted = $derived(reporters.reduce((acc, val) => {
|
||||||
if (val.status === "trusted") {
|
if (val.status === "trusted") {
|
||||||
acc.push(val)
|
acc.push(val)
|
||||||
}
|
}
|
||||||
return acc
|
return acc
|
||||||
}, [])
|
}, []))
|
||||||
$: reporters_rejected = reporters.reduce((acc, val) => {
|
let reporters_rejected = $derived(reporters.reduce((acc, val) => {
|
||||||
if (val.status === "rejected") {
|
if (val.status === "rejected") {
|
||||||
acc.push(val)
|
acc.push(val)
|
||||||
}
|
}
|
||||||
return acc
|
return acc
|
||||||
}, [])
|
}, []))
|
||||||
|
|
||||||
const get_reporters = async () => {
|
const get_reporters = async () => {
|
||||||
loading = true;
|
loading_start()
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(get_endpoint()+"/admin/abuse_reporter");
|
const resp = await fetch(get_endpoint()+"/admin/abuse_reporter");
|
||||||
if(resp.status >= 400) {
|
if(resp.status >= 400) {
|
||||||
@@ -49,17 +48,18 @@ const get_reporters = async () => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err);
|
alert(err);
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading_finish()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let edit_button: HTMLButtonElement
|
let edit_button: HTMLButtonElement = $state()
|
||||||
let creating = false
|
let creating = $state(false)
|
||||||
let new_reporter_from_address: HTMLInputElement
|
let new_reporter_from_address: HTMLInputElement = $state()
|
||||||
let new_reporter_name: HTMLInputElement
|
let new_reporter_name: HTMLInputElement = $state()
|
||||||
let new_reporter_status = "trusted"
|
let new_reporter_status = $state("trusted")
|
||||||
|
|
||||||
const create_reporter = async () => {
|
const create_reporter = async (e: SubmitEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
if (!new_reporter_from_address.value) {
|
if (!new_reporter_from_address.value) {
|
||||||
alert("Please enter an e-mail address")
|
alert("Please enter an e-mail address")
|
||||||
return
|
return
|
||||||
@@ -137,21 +137,19 @@ const delete_reporter = async (reporter: Reporter) => {
|
|||||||
onMount(get_reporters);
|
onMount(get_reporters);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<LoadingIndicator loading={loading}/>
|
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<div class="toolbar" style="text-align: left;">
|
<div class="toolbar" style="text-align: left;">
|
||||||
<div class="toolbar_spacer"></div>
|
<div class="toolbar_spacer"></div>
|
||||||
<button on:click={() => get_reporters()}>
|
<button onclick={() => get_reporters()}>
|
||||||
<i class="icon">refresh</i>
|
<i class="icon">refresh</i>
|
||||||
</button>
|
</button>
|
||||||
<button bind:this={edit_button} class:button_highlight={creating} on:click={() => {creating = !creating}}>
|
<button bind:this={edit_button} class:button_highlight={creating} onclick={() => {creating = !creating}}>
|
||||||
<i class="icon">create</i> Add abuse reporter
|
<i class="icon">create</i> Add abuse reporter
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{#if creating}
|
{#if creating}
|
||||||
<div class="highlight_shaded">
|
<div class="highlight_shaded">
|
||||||
<form on:submit|preventDefault={create_reporter}>
|
<form onsubmit={create_reporter}>
|
||||||
<div class="form">
|
<div class="form">
|
||||||
<label for="field_from_address">E-mail address</label>
|
<label for="field_from_address">E-mail address</label>
|
||||||
<input id="field_from_address" type="text" bind:this={new_reporter_from_address}/>
|
<input id="field_from_address" type="text" bind:this={new_reporter_from_address}/>
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
|
import { run, preventDefault } from 'svelte/legacy';
|
||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from "svelte";
|
||||||
import { formatDate } from "util/Formatting";
|
import { formatDate } from "util/Formatting";
|
||||||
import Modal from "util/Modal.svelte"
|
import Modal from "util/Modal.svelte"
|
||||||
@@ -7,13 +8,12 @@ import { flip } from "svelte/animate";
|
|||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
export let reporters = []
|
let { reporters = $bindable([]) } = $props();
|
||||||
|
|
||||||
$: update_table(reporters)
|
|
||||||
const update_table = (reporters) => sort("")
|
const update_table = (reporters) => sort("")
|
||||||
|
|
||||||
let sort_field = "last_used"
|
let sort_field = $state("last_used")
|
||||||
let asc = false
|
let asc = $state(false)
|
||||||
const sort = (field) => {
|
const sort = (field) => {
|
||||||
if (field !== "" && field === sort_field) asc = !asc
|
if (field !== "" && field === sort_field) asc = !asc
|
||||||
if (field === "") field = sort_field
|
if (field === "") field = sort_field
|
||||||
@@ -40,16 +40,19 @@ const sort = (field) => {
|
|||||||
reporters = reporters
|
reporters = reporters
|
||||||
}
|
}
|
||||||
|
|
||||||
let modal
|
let modal: Modal = $state()
|
||||||
let preview_subject = ""
|
let preview_subject = $state("")
|
||||||
let preview_html = ""
|
let preview_html = $state("")
|
||||||
let preview_text = ""
|
let preview_text = $state("")
|
||||||
const toggle_preview = (rep) => {
|
const toggle_preview = (rep) => {
|
||||||
preview_subject = rep.last_message_subject
|
preview_subject = rep.last_message_subject
|
||||||
preview_text = rep.last_message_text
|
preview_text = rep.last_message_text
|
||||||
preview_html = rep.last_message_html
|
preview_html = rep.last_message_html
|
||||||
modal.show()
|
modal.show()
|
||||||
}
|
}
|
||||||
|
run(() => {
|
||||||
|
update_table(reporters)
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="table_scroll">
|
<div class="table_scroll">
|
||||||
@@ -75,23 +78,23 @@ const toggle_preview = (rep) => {
|
|||||||
<td>{formatDate(rep.last_used, true, true, false)}</td>
|
<td>{formatDate(rep.last_used, true, true, false)}</td>
|
||||||
<td>{formatDate(rep.created, false, false, false)}</td>
|
<td>{formatDate(rep.created, false, false, false)}</td>
|
||||||
<td>
|
<td>
|
||||||
<button on:click|preventDefault={() => toggle_preview(rep)} class="button round">
|
<button onclick={preventDefault(() => toggle_preview(rep))} class="button round">
|
||||||
<i class="icon">email</i>
|
<i class="icon">email</i>
|
||||||
</button>
|
</button>
|
||||||
<button on:click|preventDefault={() => {dispatch("edit", rep)}} class="button round">
|
<button onclick={preventDefault(() => {dispatch("edit", rep)})} class="button round">
|
||||||
<i class="icon">edit</i>
|
<i class="icon">edit</i>
|
||||||
</button>
|
</button>
|
||||||
{#if rep.status !== "trusted"}
|
{#if rep.status !== "trusted"}
|
||||||
<button on:click|preventDefault={() => {dispatch("approve", rep)}} class="button button_highlight round">
|
<button onclick={preventDefault(() => {dispatch("approve", rep)})} class="button button_highlight round">
|
||||||
<i class="icon">check</i>
|
<i class="icon">check</i>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if rep.status !== "rejected"}
|
{#if rep.status !== "rejected"}
|
||||||
<button on:click|preventDefault={() => {dispatch("spam", rep)}} class="button button_red round">
|
<button onclick={preventDefault(() => {dispatch("spam", rep)})} class="button button_red round">
|
||||||
<i class="icon">block</i>
|
<i class="icon">block</i>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
<button on:click|preventDefault={() => {dispatch("delete", rep)}} class="button button_red round">
|
<button onclick={preventDefault(() => {dispatch("delete", rep)})} class="button button_red round">
|
||||||
<i class="icon">delete</i>
|
<i class="icon">delete</i>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
|
@@ -2,20 +2,20 @@
|
|||||||
import { onDestroy, onMount } from "svelte";
|
import { onDestroy, onMount } from "svelte";
|
||||||
import { formatDataVolume, formatThousands, formatDate, formatNumber, formatDuration } from "util/Formatting";
|
import { formatDataVolume, formatThousands, formatDate, formatNumber, formatDuration } from "util/Formatting";
|
||||||
import Chart from "util/Chart.svelte";
|
import Chart from "util/Chart.svelte";
|
||||||
import { color_by_name } from "util/Util.svelte";
|
import { color_by_name } from "util/Util";
|
||||||
import ServerDiagnostics from "./ServerDiagnostics.svelte";
|
import ServerDiagnostics from "./ServerDiagnostics.svelte";
|
||||||
import PeerTable from "./PeerTable.svelte";
|
import PeerTable from "./PeerTable.svelte";
|
||||||
|
|
||||||
let graphViews
|
let graphViews = $state()
|
||||||
let graphBandwidth
|
let graphBandwidth = $state()
|
||||||
let graphTimeout = null
|
let graphTimeout = null
|
||||||
|
|
||||||
let start_time = ""
|
let start_time = $state("")
|
||||||
let end_time = ""
|
let end_time = $state("")
|
||||||
let total_bandwidth = 0
|
let total_bandwidth = $state(0)
|
||||||
let total_bandwidth_paid = 0
|
let total_bandwidth_paid = $state(0)
|
||||||
let total_views = 0
|
let total_views = $state(0)
|
||||||
let total_downloads = 0
|
let total_downloads = $state(0)
|
||||||
const loadGraph = (minutes, interval, live) => {
|
const loadGraph = (minutes, interval, live) => {
|
||||||
if (graphTimeout !== null) { clearTimeout(graphTimeout) }
|
if (graphTimeout !== null) { clearTimeout(graphTimeout) }
|
||||||
if (live) {
|
if (live) {
|
||||||
@@ -64,8 +64,8 @@ const loadGraph = (minutes, interval, live) => {
|
|||||||
|
|
||||||
// Load performance statistics
|
// Load performance statistics
|
||||||
|
|
||||||
let lastOrder;
|
let lastOrder = $state();
|
||||||
let status = {
|
let status = $state({
|
||||||
cpu_profile_running_since: "",
|
cpu_profile_running_since: "",
|
||||||
db_latency: 0,
|
db_latency: 0,
|
||||||
db_time: "",
|
db_time: "",
|
||||||
@@ -93,9 +93,9 @@ let status = {
|
|||||||
rate_limit_watcher_listeners: 0,
|
rate_limit_watcher_listeners: 0,
|
||||||
download_clients: 0,
|
download_clients: 0,
|
||||||
download_connections: 0,
|
download_connections: 0,
|
||||||
}
|
})
|
||||||
$: total_reads = status.local_reads + status.neighbour_reads + status.remote_reads
|
let total_reads = $derived(status.local_reads + status.neighbour_reads + status.remote_reads)
|
||||||
$: total_read_size = status.local_read_size + status.neighbour_read_size + status.remote_read_size
|
let total_read_size = $derived(status.local_read_size + status.neighbour_read_size + status.remote_read_size)
|
||||||
|
|
||||||
function getStats(order) {
|
function getStats(order) {
|
||||||
lastOrder = order
|
lastOrder = order
|
||||||
@@ -179,14 +179,14 @@ onDestroy(() => {
|
|||||||
<h3>Bandwidth usage and file views</h3>
|
<h3>Bandwidth usage and file views</h3>
|
||||||
</section>
|
</section>
|
||||||
<div class="highlight_border" style="margin-bottom: 6px;">
|
<div class="highlight_border" style="margin-bottom: 6px;">
|
||||||
<button on:click={() => loadGraph(1440, 1, true)}>Day 1m</button>
|
<button onclick={() => loadGraph(1440, 1, true)}>Day 1m</button>
|
||||||
<button on:click={() => loadGraph(10080, 10, true)}>Week 10m</button>
|
<button onclick={() => loadGraph(10080, 10, true)}>Week 10m</button>
|
||||||
<button on:click={() => loadGraph(43200, 60, true)}>Month 1h</button>
|
<button onclick={() => loadGraph(43200, 60, true)}>Month 1h</button>
|
||||||
<button on:click={() => loadGraph(131400, 1440, false)}>Quarter 1d</button>
|
<button onclick={() => loadGraph(131400, 1440, false)}>Quarter 1d</button>
|
||||||
<button on:click={() => loadGraph(262800, 1440, false)}>Half-year 1d</button>
|
<button onclick={() => loadGraph(262800, 1440, false)}>Half-year 1d</button>
|
||||||
<button on:click={() => loadGraph(525600, 1440, false)}>Year 1d</button>
|
<button onclick={() => loadGraph(525600, 1440, false)}>Year 1d</button>
|
||||||
<button on:click={() => loadGraph(1051200, 1440, false)}>Two Years 1d</button>
|
<button onclick={() => loadGraph(1051200, 1440, false)}>Two Years 1d</button>
|
||||||
<button on:click={() => loadGraph(2628000, 1440, false)}>Five Years 1d</button>
|
<button onclick={() => loadGraph(2628000, 1440, false)}>Five Years 1d</button>
|
||||||
</div>
|
</div>
|
||||||
<Chart bind:this={graphBandwidth} data_type="bytes" />
|
<Chart bind:this={graphBandwidth} data_type="bytes" />
|
||||||
<Chart bind:this={graphViews} data_type="number" />
|
<Chart bind:this={graphViews} data_type="number" />
|
||||||
@@ -199,7 +199,7 @@ onDestroy(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
<ServerDiagnostics running_since={status.cpu_profile_running_since} on:refresh={() => getStats(lastOrder)}/>
|
<ServerDiagnostics running_since={status.cpu_profile_running_since} refresh={() => getStats(lastOrder)}/>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h3>Process stats</h3>
|
<h3>Process stats</h3>
|
||||||
@@ -276,24 +276,12 @@ onDestroy(() => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
|
||||||
<td>File statistics (per file)</td>
|
|
||||||
<td>{status.stats_watcher_threads}</td>
|
|
||||||
<td>{status.stats_watcher_listeners}</td>
|
|
||||||
<td>{(status.stats_watcher_listeners / status.stats_watcher_threads).toPrecision(3)}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>Filesystem statistics (per file)</td>
|
<td>Filesystem statistics (per file)</td>
|
||||||
<td>{status.filesystem_watcher_threads}</td>
|
<td>{status.filesystem_watcher_threads}</td>
|
||||||
<td>{status.filesystem_watcher_listeners}</td>
|
<td>{status.filesystem_watcher_listeners}</td>
|
||||||
<td>{(status.filesystem_watcher_listeners / status.filesystem_watcher_threads).toPrecision(3)}</td>
|
<td>{(status.filesystem_watcher_listeners / status.filesystem_watcher_threads).toPrecision(3)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td>Rate limits (per IP)</td>
|
|
||||||
<td>{status.rate_limit_watcher_threads}</td>
|
|
||||||
<td>{status.rate_limit_watcher_listeners}</td>
|
|
||||||
<td>{(status.rate_limit_watcher_listeners / status.rate_limit_watcher_threads).toPrecision(3)}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>Downloads (per IP)</td>
|
<td>Downloads (per IP)</td>
|
||||||
<td>{status.download_clients}</td>
|
<td>{status.download_clients}</td>
|
||||||
@@ -318,22 +306,22 @@ onDestroy(() => {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<button on:click={() => { getStats('query_name') }}>
|
<button onclick={() => { getStats('query_name') }}>
|
||||||
Query
|
Query
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button style="cursor: pointer;" on:click={() => { getStats('calls') }}>
|
<button style="cursor: pointer;" onclick={() => { getStats('calls') }}>
|
||||||
Calls
|
Calls
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button style="cursor: pointer;" on:click={() => { getStats('average_duration') }}>
|
<button style="cursor: pointer;" onclick={() => { getStats('average_duration') }}>
|
||||||
Avg
|
Avg
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button style="cursor: pointer;" on:click={() => { getStats('total_duration') }}>
|
<button style="cursor: pointer;" onclick={() => { getStats('total_duration') }}>
|
||||||
Total
|
Total
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
|
@@ -1,8 +1,9 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { preventDefault, stopPropagation } from 'svelte/legacy';
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { formatDate } from "util/Formatting";
|
import { formatDate } from "util/Formatting";
|
||||||
import Expandable from "util/Expandable.svelte";
|
import Expandable from "util/Expandable.svelte";
|
||||||
import LoadingIndicator from "util/LoadingIndicator.svelte";
|
import { loading_finish } from "lib/Loading";
|
||||||
|
|
||||||
const abuse_types = [
|
const abuse_types = [
|
||||||
"copyright",
|
"copyright",
|
||||||
@@ -15,17 +16,16 @@ const abuse_types = [
|
|||||||
"revenge_porn",
|
"revenge_porn",
|
||||||
]
|
]
|
||||||
|
|
||||||
let loading = true
|
let rows = $state([])
|
||||||
let rows = []
|
let total_offences = $state(0)
|
||||||
let total_offences = 0
|
|
||||||
|
|
||||||
let expanded = false
|
let expanded = $state(false)
|
||||||
let creating = false
|
let creating = $state(false)
|
||||||
let new_ban_address
|
let new_ban_address = $state()
|
||||||
let new_ban_reason = abuse_types[0]
|
let new_ban_reason = $state(abuse_types[0])
|
||||||
|
|
||||||
const get_bans = async () => {
|
const get_bans = async () => {
|
||||||
loading = true;
|
loading_start()
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(window.api_endpoint+"/admin/ip_ban");
|
const resp = await fetch(window.api_endpoint+"/admin/ip_ban");
|
||||||
if(resp.status >= 400) {
|
if(resp.status >= 400) {
|
||||||
@@ -39,7 +39,7 @@ const get_bans = async () => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err);
|
alert(err);
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading_finish();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -95,8 +95,6 @@ const delete_ban = async (addr) => {
|
|||||||
onMount(get_bans);
|
onMount(get_bans);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<LoadingIndicator loading={loading}/>
|
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<div class="toolbar_label">
|
<div class="toolbar_label">
|
||||||
@@ -106,20 +104,20 @@ onMount(get_bans);
|
|||||||
Offences {total_offences}
|
Offences {total_offences}
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar_spacer"></div>
|
<div class="toolbar_spacer"></div>
|
||||||
<button class:button_highlight={expanded} on:click={() => {expanded = !expanded}}>
|
<button class:button_highlight={expanded} onclick={() => {expanded = !expanded}}>
|
||||||
{#if expanded}
|
{#if expanded}
|
||||||
<i class="icon">unfold_less</i> Collapse all
|
<i class="icon">unfold_less</i> Collapse all
|
||||||
{:else}
|
{:else}
|
||||||
<i class="icon">unfold_more</i> Expand all
|
<i class="icon">unfold_more</i> Expand all
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
<button class:button_highlight={creating} on:click={() => {creating = !creating}}>
|
<button class:button_highlight={creating} onclick={() => {creating = !creating}}>
|
||||||
<i class="icon">create</i> Add IP ban
|
<i class="icon">create</i> Add IP ban
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{#if creating}
|
{#if creating}
|
||||||
<div class="highlight_shaded">
|
<div class="highlight_shaded">
|
||||||
<form on:submit|preventDefault={create_ban}>
|
<form onsubmit={preventDefault(create_ban)}>
|
||||||
<div class="form">
|
<div class="form">
|
||||||
<label for="field_address">IP address</label>
|
<label for="field_address">IP address</label>
|
||||||
<input id="field_address" type="text" bind:this={new_ban_address}/>
|
<input id="field_address" type="text" bind:this={new_ban_address}/>
|
||||||
@@ -141,7 +139,8 @@ onMount(get_bans);
|
|||||||
|
|
||||||
{#each rows as row (row.address)}
|
{#each rows as row (row.address)}
|
||||||
<Expandable expanded={expanded} click_expand>
|
<Expandable expanded={expanded} click_expand>
|
||||||
<div slot="header" class="header">
|
{#snippet header()}
|
||||||
|
<div class="header">
|
||||||
<div class="title">{row.address}</div>
|
<div class="title">{row.address}</div>
|
||||||
<div class="stats">
|
<div class="stats">
|
||||||
Offences<br/>
|
Offences<br/>
|
||||||
@@ -151,10 +150,11 @@ onMount(get_bans);
|
|||||||
Date<br/>
|
Date<br/>
|
||||||
{formatDate(row.offences[0].ban_time, false, false, false)}
|
{formatDate(row.offences[0].ban_time, false, false, false)}
|
||||||
</div>
|
</div>
|
||||||
<button on:click|stopPropagation={() => {delete_ban(row.address)}} class="button button_red" style="align-self: center;">
|
<button onclick={stopPropagation(() => {delete_ban(row.address)})} class="button button_red" style="align-self: center;">
|
||||||
<i class="icon">delete</i>
|
<i class="icon">delete</i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{/snippet}
|
||||||
<div class="table_scroll">
|
<div class="table_scroll">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
|
@@ -1,17 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { formatDate } from "util/Formatting";
|
import { formatDate } from "util/Formatting";
|
||||||
import LoadingIndicator from "util/LoadingIndicator.svelte";
|
|
||||||
import Expandable from "util/Expandable.svelte";
|
import Expandable from "util/Expandable.svelte";
|
||||||
import SortableTable, { FieldType } from "layout/SortableTable.svelte";
|
import SortableTable, { FieldType } from "layout/SortableTable.svelte";
|
||||||
import { country_name, get_admin_invoices, type Invoice } from "lib/AdminAPI";
|
import { country_name, get_admin_invoices, type Invoice } from "lib/AdminAPI";
|
||||||
import PayPalVat from "./PayPalVAT.svelte";
|
import PayPalVat from "./PayPalVAT.svelte";
|
||||||
|
import { loading_finish, loading_start } from "lib/Loading";
|
||||||
|
|
||||||
let loading = true
|
let invoices: Invoice[] = $state([])
|
||||||
let invoices: Invoice[] = []
|
|
||||||
|
|
||||||
let year = 0
|
let year = $state(0)
|
||||||
let month = 0
|
let month = $state(0)
|
||||||
|
|
||||||
type Total = {
|
type Total = {
|
||||||
count: number
|
count: number
|
||||||
@@ -19,8 +18,8 @@ type Total = {
|
|||||||
vat: number
|
vat: number
|
||||||
fee: number
|
fee: number
|
||||||
}
|
}
|
||||||
let totals_provider: { [id: string]: Total } = {}
|
let totals_provider: { [id: string]: Total } = $state({})
|
||||||
let totals_country: { [id: string]: Total } = {}
|
let totals_country: { [id: string]: Total } = $state({})
|
||||||
const add_total = (i: Invoice) => {
|
const add_total = (i: Invoice) => {
|
||||||
if (totals_provider[i.payment_method] === undefined) {
|
if (totals_provider[i.payment_method] === undefined) {
|
||||||
totals_provider[i.payment_method] = {count: 0, amount: 0, vat: 0, fee: 0}
|
totals_provider[i.payment_method] = {count: 0, amount: 0, vat: 0, fee: 0}
|
||||||
@@ -62,7 +61,7 @@ const obj_to_list_eu = (obj: {[id: string]: Total}) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const get_invoices = async () => {
|
const get_invoices = async () => {
|
||||||
loading = true;
|
loading_start()
|
||||||
try {
|
try {
|
||||||
const resp = await get_admin_invoices(year, month)
|
const resp = await get_admin_invoices(year, month)
|
||||||
|
|
||||||
@@ -92,7 +91,7 @@ const get_invoices = async () => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err);
|
alert(err);
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading_finish()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -120,14 +119,14 @@ onMount(() => {
|
|||||||
get_invoices()
|
get_invoices()
|
||||||
})
|
})
|
||||||
|
|
||||||
let status_filter = {
|
let status_filter = $state({
|
||||||
canceled: {checked: false},
|
canceled: {checked: false},
|
||||||
expired: {checked: false},
|
expired: {checked: false},
|
||||||
open: {checked: false},
|
open: {checked: false},
|
||||||
paid: {checked: true},
|
paid: {checked: true},
|
||||||
}
|
})
|
||||||
let gateway_filter = {}
|
let gateway_filter = $state({})
|
||||||
let method_filter = {}
|
let method_filter = $state({})
|
||||||
|
|
||||||
const filter_invoices = () => {
|
const filter_invoices = () => {
|
||||||
records_hidden = 0
|
records_hidden = 0
|
||||||
@@ -154,28 +153,28 @@ const filter_invoices = () => {
|
|||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
let records_hidden = 0
|
let records_hidden = $state(0)
|
||||||
let invoices_filtered: Invoice[] = []
|
let invoices_filtered: Invoice[] = $state([])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<LoadingIndicator loading={loading}/>
|
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h3>{year + "-" + ("00"+(month)).slice(-2)}</h3>
|
<h3>{year + "-" + ("00"+(month)).slice(-2)}</h3>
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<button on:click={last_month}>
|
<button onclick={last_month}>
|
||||||
<i class="icon">chevron_left</i>
|
<i class="icon">chevron_left</i>
|
||||||
Previous month
|
Previous month
|
||||||
</button>
|
</button>
|
||||||
<div class="toolbar_spacer"></div>
|
<div class="toolbar_spacer"></div>
|
||||||
<button on:click={next_month}>
|
<button onclick={next_month}>
|
||||||
Next month
|
Next month
|
||||||
<i class="icon">chevron_right</i>
|
<i class="icon">chevron_right</i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Expandable click_expand>
|
<Expandable click_expand>
|
||||||
<div slot="header" class="header">Per payment processor</div>
|
{#snippet header()}
|
||||||
|
<div class="header">Per payment processor</div>
|
||||||
|
{/snippet}
|
||||||
<SortableTable
|
<SortableTable
|
||||||
index_field="id"
|
index_field="id"
|
||||||
rows={obj_to_list(totals_provider)}
|
rows={obj_to_list(totals_provider)}
|
||||||
@@ -191,7 +190,9 @@ let invoices_filtered: Invoice[] = []
|
|||||||
</Expandable>
|
</Expandable>
|
||||||
|
|
||||||
<Expandable click_expand>
|
<Expandable click_expand>
|
||||||
<div slot="header" class="header">Per country</div>
|
{#snippet header()}
|
||||||
|
<div class="header">Per country</div>
|
||||||
|
{/snippet}
|
||||||
<SortableTable
|
<SortableTable
|
||||||
index_field="id"
|
index_field="id"
|
||||||
rows={obj_to_list(totals_country)}
|
rows={obj_to_list(totals_country)}
|
||||||
@@ -207,7 +208,9 @@ let invoices_filtered: Invoice[] = []
|
|||||||
</Expandable>
|
</Expandable>
|
||||||
|
|
||||||
<Expandable click_expand>
|
<Expandable click_expand>
|
||||||
<div slot="header" class="header">In European Union</div>
|
{#snippet header()}
|
||||||
|
<div class="header">In European Union</div>
|
||||||
|
{/snippet}
|
||||||
<SortableTable
|
<SortableTable
|
||||||
index_field="id"
|
index_field="id"
|
||||||
rows={obj_to_list_eu(totals_country)}
|
rows={obj_to_list_eu(totals_country)}
|
||||||
@@ -223,7 +226,9 @@ let invoices_filtered: Invoice[] = []
|
|||||||
</Expandable>
|
</Expandable>
|
||||||
|
|
||||||
<Expandable click_expand>
|
<Expandable click_expand>
|
||||||
<div slot="header" class="header">PayPal VAT</div>
|
{#snippet header()}
|
||||||
|
<div class="header">PayPal VAT</div>
|
||||||
|
{/snippet}
|
||||||
<PayPalVat invoices={invoices}/>
|
<PayPalVat invoices={invoices}/>
|
||||||
</Expandable>
|
</Expandable>
|
||||||
|
|
||||||
@@ -236,7 +241,7 @@ let invoices_filtered: Invoice[] = []
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="status_{filter}"
|
id="status_{filter}"
|
||||||
bind:checked={status_filter[filter].checked}
|
bind:checked={status_filter[filter].checked}
|
||||||
on:change={filter_invoices}>
|
onchange={filter_invoices}>
|
||||||
<label for="status_{filter}">{filter}</label>
|
<label for="status_{filter}">{filter}</label>
|
||||||
<br/>
|
<br/>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -248,7 +253,7 @@ let invoices_filtered: Invoice[] = []
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="gateway_{filter}"
|
id="gateway_{filter}"
|
||||||
bind:checked={gateway_filter[filter].checked}
|
bind:checked={gateway_filter[filter].checked}
|
||||||
on:change={filter_invoices}>
|
onchange={filter_invoices}>
|
||||||
<label for="gateway_{filter}">{filter}</label>
|
<label for="gateway_{filter}">{filter}</label>
|
||||||
<br/>
|
<br/>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -260,7 +265,7 @@ let invoices_filtered: Invoice[] = []
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="method_{filter}"
|
id="method_{filter}"
|
||||||
bind:checked={method_filter[filter].checked}
|
bind:checked={method_filter[filter].checked}
|
||||||
on:change={filter_invoices}>
|
onchange={filter_invoices}>
|
||||||
<label for="method_{filter}">{filter}</label>
|
<label for="method_{filter}">{filter}</label>
|
||||||
<br/>
|
<br/>
|
||||||
{/each}
|
{/each}
|
||||||
|
@@ -1,20 +1,19 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { formatDate } from "util/Formatting";
|
import { formatDate } from "util/Formatting";
|
||||||
import { mollie_proxy_call } from "./MollieAPI";
|
import { mollie_proxy_call } from "./MollieAPI";
|
||||||
import LoadingIndicator from "util/LoadingIndicator.svelte";
|
|
||||||
import Euro from "util/Euro.svelte";
|
import Euro from "util/Euro.svelte";
|
||||||
|
import { loading_finish, loading_start } from "lib/Loading";
|
||||||
|
|
||||||
export let settlement = {}
|
let { settlement = {} } = $props();
|
||||||
let loading = true
|
let payments = $state([])
|
||||||
let payments = []
|
|
||||||
|
|
||||||
let per_country = {}
|
let per_country = $state({})
|
||||||
let totals = {
|
let totals = $state({
|
||||||
count: 0,
|
count: 0,
|
||||||
vat: 0,
|
vat: 0,
|
||||||
amount: 0,
|
amount: 0,
|
||||||
}
|
})
|
||||||
|
|
||||||
const load_all_payments = async (settlement_id) => {
|
const load_all_payments = async (settlement_id) => {
|
||||||
let payments = []
|
let payments = []
|
||||||
@@ -49,7 +48,7 @@ const load_all_payments = async (settlement_id) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const get_payments = async () => {
|
const get_payments = async () => {
|
||||||
loading = true;
|
loading_start()
|
||||||
try {
|
try {
|
||||||
payments = await load_all_payments(settlement.id)
|
payments = await load_all_payments(settlement.id)
|
||||||
|
|
||||||
@@ -84,15 +83,13 @@ const get_payments = async () => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err);
|
alert(err);
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading_finish()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(get_payments);
|
onMount(get_payments);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<LoadingIndicator loading={loading}/>
|
|
||||||
|
|
||||||
<h3>Accounting information</h3>
|
<h3>Accounting information</h3>
|
||||||
|
|
||||||
{#if per_country.NL}
|
{#if per_country.NL}
|
||||||
|
@@ -2,17 +2,16 @@
|
|||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { formatDate } from "util/Formatting";
|
import { formatDate } from "util/Formatting";
|
||||||
import Expandable from "util/Expandable.svelte";
|
import Expandable from "util/Expandable.svelte";
|
||||||
import LoadingIndicator from "util/LoadingIndicator.svelte";
|
|
||||||
import Euro from "util/Euro.svelte";
|
import Euro from "util/Euro.svelte";
|
||||||
import MollieSettlement from "./MollieSettlement.svelte";
|
import MollieSettlement from "./MollieSettlement.svelte";
|
||||||
import { mollie_proxy_call } from "./MollieAPI";
|
import { mollie_proxy_call } from "./MollieAPI";
|
||||||
|
import { loading_finish, loading_start } from "lib/Loading";
|
||||||
|
|
||||||
let loading = true
|
|
||||||
let response = {}
|
let response = {}
|
||||||
let settlements = []
|
let settlements = $state([])
|
||||||
|
|
||||||
const get_settlements = async () => {
|
const get_settlements = async () => {
|
||||||
loading = true;
|
loading_start()
|
||||||
try {
|
try {
|
||||||
const req = await mollie_proxy_call("settlements?limit=250");
|
const req = await mollie_proxy_call("settlements?limit=250");
|
||||||
if(req.status >= 400) {
|
if(req.status >= 400) {
|
||||||
@@ -23,19 +22,18 @@ const get_settlements = async () => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err);
|
alert(err);
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading_finish()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(get_settlements);
|
onMount(get_settlements);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<LoadingIndicator loading={loading}/>
|
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
{#each settlements as row (row.id)}
|
{#each settlements as row (row.id)}
|
||||||
<Expandable click_expand>
|
<Expandable click_expand>
|
||||||
<div slot="header" class="header">
|
{#snippet header()}
|
||||||
|
<div class="header">
|
||||||
<div class="title">{row.id}</div>
|
<div class="title">{row.id}</div>
|
||||||
<div class="stats">
|
<div class="stats">
|
||||||
Date<br/>
|
Date<br/>
|
||||||
@@ -50,6 +48,7 @@ onMount(get_settlements);
|
|||||||
{row.status}
|
{row.status}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
<MollieSettlement settlement={row}/>
|
<MollieSettlement settlement={row}/>
|
||||||
</Expandable>
|
</Expandable>
|
||||||
|
@@ -2,20 +2,19 @@
|
|||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { formatDate } from "util/Formatting";
|
import { formatDate } from "util/Formatting";
|
||||||
import Expandable from "util/Expandable.svelte";
|
import Expandable from "util/Expandable.svelte";
|
||||||
import LoadingIndicator from "util/LoadingIndicator.svelte";
|
|
||||||
import Euro from "util/Euro.svelte";
|
import Euro from "util/Euro.svelte";
|
||||||
|
import { loading_finish, loading_start } from "lib/Loading";
|
||||||
|
|
||||||
let loading = true
|
|
||||||
let response = {}
|
let response = {}
|
||||||
let payments = []
|
let payments = $state([])
|
||||||
|
|
||||||
let per_country = {}
|
let per_country = $state({})
|
||||||
let totals = {}
|
let totals = $state({})
|
||||||
|
|
||||||
let datePicker
|
let datePicker = $state()
|
||||||
let rangeMonths = 1
|
let rangeMonths = $state(1)
|
||||||
let startDate = 0
|
let startDate = $state(0)
|
||||||
let endDate = 0
|
let endDate = $state(0)
|
||||||
|
|
||||||
const get_payments = async () => {
|
const get_payments = async () => {
|
||||||
if (!datePicker.valueAsDate) {
|
if (!datePicker.valueAsDate) {
|
||||||
@@ -40,8 +39,8 @@ const get_payments = async () => {
|
|||||||
fee: 0,
|
fee: 0,
|
||||||
}
|
}
|
||||||
payments = []
|
payments = []
|
||||||
loading = true;
|
|
||||||
|
|
||||||
|
loading_start()
|
||||||
try {
|
try {
|
||||||
await get_page("https://api.mollie.com/v2/payments?limit=250")
|
await get_page("https://api.mollie.com/v2/payments?limit=250")
|
||||||
|
|
||||||
@@ -76,7 +75,7 @@ const get_payments = async () => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err);
|
alert(err);
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading_finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,8 +117,6 @@ onMount(() => {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<LoadingIndicator loading={loading}/>
|
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<div class="toolbar" style="text-align: left;">
|
<div class="toolbar" style="text-align: left;">
|
||||||
<div>Payments: {payments.length}</div>
|
<div>Payments: {payments.length}</div>
|
||||||
@@ -128,7 +125,7 @@ onMount(() => {
|
|||||||
<input type="date" bind:this={datePicker}/>
|
<input type="date" bind:this={datePicker}/>
|
||||||
<div>Months</div>
|
<div>Months</div>
|
||||||
<input type="number" bind:value={rangeMonths}/>
|
<input type="number" bind:value={rangeMonths}/>
|
||||||
<button on:click={get_payments}>Go</button>
|
<button onclick={get_payments}>Go</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -230,7 +227,8 @@ onMount(() => {
|
|||||||
<h2>Payments</h2>
|
<h2>Payments</h2>
|
||||||
{#each payments as row (row.id)}
|
{#each payments as row (row.id)}
|
||||||
<Expandable click_expand>
|
<Expandable click_expand>
|
||||||
<div slot="header" class="header">
|
{#snippet header()}
|
||||||
|
<div class="header">
|
||||||
<div class="title">{row.id}</div>
|
<div class="title">{row.id}</div>
|
||||||
<div class="stats">
|
<div class="stats">
|
||||||
Date<br/>
|
Date<br/>
|
||||||
@@ -253,6 +251,7 @@ onMount(() => {
|
|||||||
{row.status}
|
{row.status}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/snippet}
|
||||||
<div>
|
<div>
|
||||||
Amount: <Euro amount={row.metadata.amount} /><br/>
|
Amount: <Euro amount={row.metadata.amount} /><br/>
|
||||||
VAT: <Euro amount={row.metadata.vat} /><br/>
|
VAT: <Euro amount={row.metadata.vat} /><br/>
|
||||||
|
@@ -78,6 +78,7 @@ const update_countries = (invoices: Invoice[]) => {
|
|||||||
{#if per_country["NL"] && totals}
|
{#if per_country["NL"] && totals}
|
||||||
<h2>Summary</h2>
|
<h2>Summary</h2>
|
||||||
<table style="width: auto;">
|
<table style="width: auto;">
|
||||||
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Total PayPal earnings -fees</td>
|
<td>Total PayPal earnings -fees</td>
|
||||||
<td><Euro amount={totals.vat+totals.amount-totals.fee}/></td>
|
<td><Euro amount={totals.vat+totals.amount-totals.fee}/></td>
|
||||||
@@ -90,6 +91,7 @@ const update_countries = (invoices: Invoice[]) => {
|
|||||||
<td>Total VAT OSS</td>
|
<td>Total VAT OSS</td>
|
||||||
<td><Euro amount={totals.vat-per_country["NL"].vat}/></td>
|
<td><Euro amount={totals.vat-per_country["NL"].vat}/></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<h2>Accounting information</h2>
|
<h2>Accounting information</h2>
|
||||||
|
@@ -1,25 +1,22 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
|
import { run } from 'svelte/legacy';
|
||||||
import { flip } from "svelte/animate";
|
import { flip } from "svelte/animate";
|
||||||
import { formatDataVolume } from "util/Formatting";
|
import { formatDataVolume } from "util/Formatting";
|
||||||
import SortButton from "layout/SortButton.svelte";
|
import SortButton from "layout/SortButton.svelte";
|
||||||
|
|
||||||
export let peers = [];
|
let { peers = $bindable([]) } = $props();
|
||||||
$: update_peers(peers)
|
|
||||||
let update_peers = (peers) => {
|
let update_peers = (peers) => {
|
||||||
for (let peer of peers) {
|
for (let peer of peers) {
|
||||||
peer.avg_network_total = peer.avg_network_tx + peer.avg_network_rx
|
peer.avg_network_total = peer.avg_network_tx + peer.avg_network_rx
|
||||||
peer.usage_percent = (peer.avg_network_tx / peer.port_speed) * 100
|
peer.usage_percent = (peer.avg_network_tx / peer.port_speed) * 100
|
||||||
peer.network_ratio = Math.max(peer.avg_network_tx, peer.avg_network_rx) / Math.min(peer.avg_network_tx, peer.avg_network_rx)
|
peer.network_ratio = Math.max(peer.avg_network_tx, peer.avg_network_rx) / Math.min(peer.avg_network_tx, peer.avg_network_rx)
|
||||||
if (peer.network_ratio === NaN) {
|
|
||||||
peer.network_ratio = 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sort("")
|
sort("")
|
||||||
}
|
}
|
||||||
|
|
||||||
let sort_field = "hostname"
|
let sort_field = $state("hostname")
|
||||||
let asc = true
|
let asc = $state(true)
|
||||||
let sort = (field) => {
|
let sort = (field) => {
|
||||||
if (field !== "" && field === sort_field) {
|
if (field !== "" && field === sort_field) {
|
||||||
asc = !asc
|
asc = !asc
|
||||||
@@ -49,6 +46,9 @@ let sort = (field) => {
|
|||||||
})
|
})
|
||||||
peers = peers
|
peers = peers
|
||||||
}
|
}
|
||||||
|
run(() => {
|
||||||
|
update_peers(peers)
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="table_scroll">
|
<div class="table_scroll">
|
||||||
|
@@ -1,35 +1,43 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
import { createEventDispatcher, onMount } from "svelte";
|
import { get_endpoint } from "lib/PixeldrainAPI";
|
||||||
|
import { onMount } from "svelte";
|
||||||
import { formatDuration } from "util/Formatting";
|
import { formatDuration } from "util/Formatting";
|
||||||
let dispatch = createEventDispatcher()
|
|
||||||
|
|
||||||
export let running_since = ""
|
let {
|
||||||
|
running_since = "",
|
||||||
|
refresh,
|
||||||
|
}: {
|
||||||
|
running_since?: string
|
||||||
|
refresh?: () => void
|
||||||
|
} = $props();
|
||||||
|
|
||||||
$: profile_running = running_since != "0001-01-01T00:00:00Z" && running_since != ""
|
let profile_running = $derived(running_since != "0001-01-01T00:00:00Z" && running_since != "")
|
||||||
|
|
||||||
const start = async () => {
|
const start = async () => {
|
||||||
if (!profile_running) {
|
if (!profile_running) {
|
||||||
const resp = await fetch(
|
const resp = await fetch(
|
||||||
window.api_endpoint+"/admin/cpu_profile",
|
get_endpoint()+"/admin/cpu_profile",
|
||||||
{ method: "POST" }
|
{ method: "POST" }
|
||||||
);
|
);
|
||||||
if(resp.status >= 400) {
|
if(resp.status >= 400) {
|
||||||
throw new Error(await resp.text());
|
throw new Error(await resp.text());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
window.open(window.api_endpoint+"/admin/cpu_profile")
|
window.open(get_endpoint()+"/admin/cpu_profile")
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch("refresh")
|
if (refresh !== undefined) {
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let interval
|
let interval: number
|
||||||
let running_time = "0s"
|
let running_time = $state("0s")
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
interval = setInterval(() => {
|
interval = setInterval(() => {
|
||||||
if (profile_running) {
|
if (profile_running) {
|
||||||
running_time = formatDuration(
|
running_time = formatDuration(
|
||||||
(new Date()).getTime() - Date.parse(running_since),
|
(new Date()).getTime() - Date.parse(running_since), 3
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}, 1000)
|
}, 1000)
|
||||||
@@ -43,7 +51,7 @@ onMount(() => {
|
|||||||
|
|
||||||
<a class="button" href="/api/admin/call_stack">Call stack</a>
|
<a class="button" href="/api/admin/call_stack">Call stack</a>
|
||||||
<a class="button" href="/api/admin/heap_profile">Heap profile</a>
|
<a class="button" href="/api/admin/heap_profile">Heap profile</a>
|
||||||
<button on:click={start} class:button_red={profile_running}>
|
<button onclick={start} class:button_red={profile_running}>
|
||||||
{#if profile_running}
|
{#if profile_running}
|
||||||
Stop CPU profiling (running for {running_time})
|
Stop CPU profiling (running for {running_time})
|
||||||
{:else}
|
{:else}
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
import Euro from "util/Euro.svelte";
|
import Euro from "util/Euro.svelte";
|
||||||
import { formatDataVolume, formatDate } from "util/Formatting";
|
import { formatDataVolume, formatDate } from "util/Formatting";
|
||||||
|
|
||||||
export let row = {}
|
let { row = {} } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
|
@@ -1,20 +1,20 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { stopPropagation } from 'svelte/legacy';
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { formatDate } from "util/Formatting";
|
import { formatDate } from "util/Formatting";
|
||||||
import Expandable from "util/Expandable.svelte";
|
import Expandable from "util/Expandable.svelte";
|
||||||
import LoadingIndicator from "util/LoadingIndicator.svelte";
|
|
||||||
import Button from "layout/Button.svelte"
|
import Button from "layout/Button.svelte"
|
||||||
import UserFiles from "./UserFiles.svelte";
|
import UserFiles from "./UserFiles.svelte";
|
||||||
import BanDetails from "./BanDetails.svelte";
|
import BanDetails from "./BanDetails.svelte";
|
||||||
import UserLists from "./UserLists.svelte";
|
import UserLists from "./UserLists.svelte";
|
||||||
|
import { loading_finish, loading_start } from "lib/Loading";
|
||||||
|
|
||||||
let loading = true
|
let rows = $state([])
|
||||||
let rows = []
|
let total_offences = $state(0)
|
||||||
let total_offences = 0
|
let expanded = $state(false)
|
||||||
let expanded = false
|
|
||||||
|
|
||||||
const get_bans = async () => {
|
const get_bans = async () => {
|
||||||
loading = true;
|
loading_start()
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(window.api_endpoint+"/admin/user_ban");
|
const resp = await fetch(window.api_endpoint+"/admin/user_ban");
|
||||||
if(resp.status >= 400) {
|
if(resp.status >= 400) {
|
||||||
@@ -28,7 +28,7 @@ const get_bans = async () => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err);
|
alert(err);
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading_finish()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -105,8 +105,6 @@ const block_all_files = async (row, reason) => {
|
|||||||
onMount(get_bans);
|
onMount(get_bans);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<LoadingIndicator loading={loading}/>
|
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<div class="toolbar_label">
|
<div class="toolbar_label">
|
||||||
@@ -116,7 +114,7 @@ onMount(get_bans);
|
|||||||
Offences {total_offences}
|
Offences {total_offences}
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar_spacer"></div>
|
<div class="toolbar_spacer"></div>
|
||||||
<button class:button_highlight={expanded} on:click={() => {expanded = !expanded}}>
|
<button class:button_highlight={expanded} onclick={() => {expanded = !expanded}}>
|
||||||
{#if expanded}
|
{#if expanded}
|
||||||
<i class="icon">unfold_less</i> Collapse all
|
<i class="icon">unfold_less</i> Collapse all
|
||||||
{:else}
|
{:else}
|
||||||
@@ -127,7 +125,8 @@ onMount(get_bans);
|
|||||||
|
|
||||||
{#each rows as row (row.user_id)}
|
{#each rows as row (row.user_id)}
|
||||||
<Expandable expanded={expanded} click_expand>
|
<Expandable expanded={expanded} click_expand>
|
||||||
<div slot="header" class="header">
|
{#snippet header()}
|
||||||
|
<div class="header">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
{row.user.username}
|
{row.user.username}
|
||||||
</div>
|
</div>
|
||||||
@@ -143,10 +142,11 @@ onMount(get_bans);
|
|||||||
Date<br/>
|
Date<br/>
|
||||||
{formatDate(row.offences[0].ban_time, false, false, false)}
|
{formatDate(row.offences[0].ban_time, false, false, false)}
|
||||||
</div>
|
</div>
|
||||||
<button on:click|stopPropagation={() => {delete_ban(row.user_id)}} class="button button_red" style="align-self: center;">
|
<button onclick={stopPropagation(() => {delete_ban(row.user_id)})} class="button button_red" style="align-self: center;">
|
||||||
<i class="icon">delete</i>
|
<i class="icon">delete</i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<Button click={() => impersonate(row.user_id)} icon="login" label="Impersonate user"/>
|
<Button click={() => impersonate(row.user_id)} icon="login" label="Impersonate user"/>
|
||||||
|
@@ -1,19 +1,24 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import LoadingIndicator from "util/LoadingIndicator.svelte";
|
|
||||||
import { formatDataVolume, formatDate } from "util/Formatting";
|
import { formatDataVolume, formatDate } from "util/Formatting";
|
||||||
import SortButton from "layout/SortButton.svelte";
|
import SortButton from "layout/SortButton.svelte";
|
||||||
|
import { loading_finish, loading_start } from "lib/Loading";
|
||||||
|
import { get_endpoint } from "lib/PixeldrainAPI";
|
||||||
|
|
||||||
export let user_id = ""
|
interface Props {
|
||||||
let files = []
|
user_id?: string;
|
||||||
let loading = true
|
}
|
||||||
|
|
||||||
|
let { user_id = "" }: Props = $props();
|
||||||
|
let files = $state([])
|
||||||
|
|
||||||
onMount(() => reload())
|
onMount(() => reload())
|
||||||
|
|
||||||
export const reload = async () => {
|
export const reload = async () => {
|
||||||
|
loading_start()
|
||||||
try {
|
try {
|
||||||
const req = await fetch(
|
const req = await fetch(
|
||||||
window.api_endpoint+"/user/files",
|
get_endpoint()+"/user/files",
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
"Admin-User-Override": user_id,
|
"Admin-User-Override": user_id,
|
||||||
@@ -30,12 +35,12 @@ export const reload = async () => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err);
|
alert(err);
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading_finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let sort_field = "date_upload"
|
let sort_field = $state("date_upload")
|
||||||
let asc = false
|
let asc = $state(false)
|
||||||
const sort = (field) => {
|
const sort = (field) => {
|
||||||
if (field !== "" && field === sort_field) {
|
if (field !== "" && field === sort_field) {
|
||||||
asc = !asc
|
asc = !asc
|
||||||
@@ -67,8 +72,6 @@ const sort = (field) => {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<LoadingIndicator loading={loading}/>
|
|
||||||
|
|
||||||
<div class="table_scroll">
|
<div class="table_scroll">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
@@ -86,7 +89,7 @@ const sort = (field) => {
|
|||||||
{#each files as file (file.id)}
|
{#each files as file (file.id)}
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding: 0; line-height: 1em;">
|
<td style="padding: 0; line-height: 1em;">
|
||||||
<img src="{window.api_endpoint+file.thumbnail_href}?height=48&width=48" alt="icon" class="thumbnail" />
|
<img src="{get_endpoint()+file.thumbnail_href}?height=48&width=48" alt="icon" class="thumbnail" />
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="/u/{file.id}" target="_blank">{file.name}</a>
|
<a href="/u/{file.id}" target="_blank">{file.name}</a>
|
||||||
|
@@ -1,18 +1,23 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
|
import { loading_finish, loading_start } from "lib/Loading";
|
||||||
|
import { get_endpoint } from "lib/PixeldrainAPI";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import LoadingIndicator from "util/LoadingIndicator.svelte";
|
|
||||||
import { formatDate } from "util/Formatting";
|
import { formatDate } from "util/Formatting";
|
||||||
|
|
||||||
export let user_id = ""
|
interface Props {
|
||||||
let lists = []
|
user_id?: string;
|
||||||
let loading = true
|
}
|
||||||
|
|
||||||
|
let { user_id = "" }: Props = $props();
|
||||||
|
let lists = $state([])
|
||||||
|
|
||||||
onMount(() => reload())
|
onMount(() => reload())
|
||||||
|
|
||||||
export const reload = async () => {
|
export const reload = async () => {
|
||||||
|
loading_start()
|
||||||
try {
|
try {
|
||||||
const req = await fetch(
|
const req = await fetch(
|
||||||
window.api_endpoint+"/user/lists",
|
get_endpoint()+"/user/lists",
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
"Admin-User-Override": user_id,
|
"Admin-User-Override": user_id,
|
||||||
@@ -28,13 +33,11 @@ export const reload = async () => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err);
|
alert(err);
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading_finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<LoadingIndicator loading={loading}/>
|
|
||||||
|
|
||||||
<div class="table_scroll">
|
<div class="table_scroll">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
@@ -49,7 +52,7 @@ export const reload = async () => {
|
|||||||
{#each lists as list (list.id)}
|
{#each lists as list (list.id)}
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding: 0; line-height: 1em;">
|
<td style="padding: 0; line-height: 1em;">
|
||||||
<img src="{window.api_endpoint}/list/{list.id}/thumbnail?height=48&width=48" alt="icon" class="thumbnail" />
|
<img src="{get_endpoint()}/list/{list.id}/thumbnail?height=48&width=48" alt="icon" class="thumbnail" />
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="/l/{list.id}" target="_blank">{list.title}</a>
|
<a href="/l/{list.id}" target="_blank">{list.title}</a>
|
||||||
|
@@ -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">
|
<script lang="ts">
|
||||||
import { fs_encode_path, node_is_shared } from "./FilesystemAPI";
|
import { preventDefault } from 'svelte/legacy';
|
||||||
|
import { fs_encode_path } from "lib/FilesystemAPI.svelte";
|
||||||
import type { FSNavigator } from "./FSNavigator";
|
import type { FSNavigator } from "./FSNavigator";
|
||||||
|
|
||||||
export let nav: FSNavigator
|
let { nav }: {
|
||||||
|
nav: FSNavigator;
|
||||||
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="breadcrumbs">
|
<div class="breadcrumbs">
|
||||||
{#each $nav.path as node, i (node.path)}
|
{#each $nav.path as node, i (node.path)}
|
||||||
<a
|
<a
|
||||||
href={"/d"+fs_encode_path(node.path)}
|
href={"/d"+fs_encode_path(node.path)}
|
||||||
class="breadcrumb button"
|
class="breadcrumb button flat"
|
||||||
class:button_highlight={$nav.base_index === i}
|
onclick={preventDefault(() => {nav.navigate(node.path, true)})}
|
||||||
on:click|preventDefault={() => {nav.navigate(node.path, true)}}
|
|
||||||
>
|
>
|
||||||
{#if node.abuse_type !== undefined}
|
{#if node.abuse_type !== undefined}
|
||||||
<i class="icon small">block</i>
|
<i class="icon small">block</i>
|
||||||
{:else if node_is_shared(node)}
|
{:else if node.is_shared()}
|
||||||
<i class="icon small">share</i>
|
<i class="icon small">share</i>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="node_name" class:base={$nav.base_index === i}>
|
<div class="node_name" class:base={$nav.base_index === i}>
|
||||||
{node.name}
|
{node.name}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
{#if $nav.base_index !== i}
|
||||||
|
<i class="icon">chevron_right</i>
|
||||||
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.breadcrumbs {
|
.breadcrumbs {
|
||||||
flex-grow: 1;
|
flex: 0 0 auto;
|
||||||
flex-shrink: 1;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: left;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
background: var(--shaded_background);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
border-bottom: 1px solid var(--separator);
|
||||||
}
|
}
|
||||||
.breadcrumb {
|
.breadcrumb {
|
||||||
min-width: 1em;
|
min-width: 1em;
|
||||||
@@ -42,6 +49,8 @@ export let nav: FSNavigator
|
|||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
background-color: unset;
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
.node_name {
|
.node_name {
|
||||||
max-width: 20vw;
|
max-width: 20vw;
|
||||||
|
@@ -1,31 +1,34 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { run } from 'svelte/legacy';
|
||||||
import Chart from "util/Chart.svelte";
|
import Chart from "util/Chart.svelte";
|
||||||
import { formatDataVolume, formatDate, formatThousands } from "util/Formatting";
|
import { formatDataVolume, formatDate, formatThousands } from "util/Formatting";
|
||||||
import Modal from "util/Modal.svelte";
|
import Modal from "util/Modal.svelte";
|
||||||
import { fs_path_url, fs_share_hotlink_url, fs_share_url, fs_timeseries, type FSNode } from "./FilesystemAPI";
|
import { fs_path_url, fs_share_hotlink_url, fs_share_url, fs_timeseries, type FSNode } from "lib/FilesystemAPI.svelte";
|
||||||
import { color_by_name } from "util/Util.svelte";
|
import { color_by_name } from "util/Util";
|
||||||
import { tick } from "svelte";
|
import { tick } from "svelte";
|
||||||
import CopyButton from "layout/CopyButton.svelte";
|
import CopyButton from "layout/CopyButton.svelte";
|
||||||
import type { FSNavigator } from "./FSNavigator";
|
import type { FSNavigator } from "./FSNavigator";
|
||||||
|
|
||||||
export let nav: FSNavigator
|
let {
|
||||||
export let visible = false
|
nav,
|
||||||
|
visible = $bindable(false)
|
||||||
|
}: {
|
||||||
|
nav: FSNavigator;
|
||||||
|
visible?: boolean;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
export const toggle = () => visible = !visible
|
export const toggle = () => visible = !visible
|
||||||
|
|
||||||
$: visibility_change(visible)
|
|
||||||
const visibility_change = visible => {
|
const visibility_change = visible => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
update_chart(nav.base, 0, 0)
|
update_chart(nav.base, 0, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: direct_url = $nav.base.path ? window.location.origin+fs_path_url($nav.base.path) : ""
|
|
||||||
$: share_url = fs_share_url($nav.path)
|
|
||||||
$: direct_share_url = fs_share_hotlink_url($nav.path)
|
|
||||||
|
|
||||||
let chart
|
let chart: Chart = $state()
|
||||||
let chart_timespan = 0
|
let chart_timespan = $state(0)
|
||||||
let chart_interval = 0
|
let chart_interval = $state(0)
|
||||||
let chart_timespans = [
|
let chart_timespans = [
|
||||||
{label: "Day (1m)", span: 1440, interval: 1},
|
{label: "Day (1m)", span: 1440, interval: 1},
|
||||||
{label: "Week (1h)", span: 10080, interval: 60},
|
{label: "Week (1h)", span: 10080, interval: 60},
|
||||||
@@ -36,10 +39,9 @@ let chart_timespans = [
|
|||||||
{label: "Five Years (1d)", span: 2628000, interval: 1440},
|
{label: "Five Years (1d)", span: 2628000, interval: 1440},
|
||||||
]
|
]
|
||||||
|
|
||||||
let total_downloads = 0
|
let total_downloads = $state(0)
|
||||||
let total_transfer = 0
|
let total_transfer = $state(0)
|
||||||
|
|
||||||
$: update_chart($nav.base, chart_timespan, chart_interval)
|
|
||||||
let update_chart = async (base: FSNode, timespan: number, interval: number) => {
|
let update_chart = async (base: FSNode, timespan: number, interval: number) => {
|
||||||
if (chart === undefined) {
|
if (chart === undefined) {
|
||||||
// Wait for the chart element to render, if it's not rendered already
|
// Wait for the chart element to render, if it's not rendered already
|
||||||
@@ -84,7 +86,7 @@ let update_chart = async (base: FSNode, timespan: number, interval: number) => {
|
|||||||
display: true,
|
display: true,
|
||||||
position: "right",
|
position: "right",
|
||||||
ticks: {
|
ticks: {
|
||||||
callback: function (value, index, values) {
|
callback: function (value: number, index, values) {
|
||||||
return formatDataVolume(value, 3);
|
return formatDataVolume(value, 3);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -141,6 +143,15 @@ let update_chart = async (base: FSNode, timespan: number, interval: number) => {
|
|||||||
console.error("Failed to get time series data:", err)
|
console.error("Failed to get time series data:", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
run(() => {
|
||||||
|
visibility_change(visible)
|
||||||
|
});
|
||||||
|
let direct_url = $derived($nav.base.path ? window.location.origin+fs_path_url($nav.base.path) : "")
|
||||||
|
let share_url = $derived(fs_share_url($nav.path))
|
||||||
|
let direct_share_url = $derived(fs_share_hotlink_url($nav.path))
|
||||||
|
run(() => {
|
||||||
|
update_chart($nav.base, chart_timespan, chart_interval)
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:visible={visible} title="Details" width={($nav.base.type === "file" ? 1000 : 750) + "px"}>
|
<Modal bind:visible={visible} title="Details" width={($nav.base.type === "file" ? 1000 : 750) + "px"}>
|
||||||
@@ -231,7 +242,7 @@ let update_chart = async (base: FSNode, timespan: number, interval: number) => {
|
|||||||
<div class="button_bar">
|
<div class="button_bar">
|
||||||
{#each chart_timespans as ts}
|
{#each chart_timespans as ts}
|
||||||
<button
|
<button
|
||||||
on:click={() => update_chart($nav.base, ts.span, ts.interval)}
|
onclick={() => update_chart($nav.base, ts.span, ts.interval)}
|
||||||
class:button_highlight={chart_timespan == ts.span}>
|
class:button_highlight={chart_timespan == ts.span}>
|
||||||
{ts.label}
|
{ts.label}
|
||||||
</button>
|
</button>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { fs_get_node, fs_encode_path, fs_split_path } from "./FilesystemAPI";
|
import { loading_finish, loading_start } from "lib/Loading";
|
||||||
import type { FSNode, FSPath, FSPermissions, FSContext } from "./FilesystemAPI";
|
import { fs_get_node, fs_encode_path, fs_split_path } from "../lib/FilesystemAPI.svelte";
|
||||||
import type { Writable } from "svelte/store"
|
import type { FSNode, FSPath, FSPermissions, FSContext } from "../lib/FilesystemAPI.svelte";
|
||||||
|
|
||||||
export class FSNavigator {
|
export class FSNavigator {
|
||||||
// Parts of the raw API response
|
// Parts of the raw API response
|
||||||
@@ -22,27 +22,16 @@ export class FSNavigator {
|
|||||||
|
|
||||||
constructor(history_enabled = true) {
|
constructor(history_enabled = true) {
|
||||||
this.history_enabled = history_enabled
|
this.history_enabled = history_enabled
|
||||||
|
}
|
||||||
|
|
||||||
// If history logging is enabled we capture the popstate event, which
|
// The popstate event can be used to listen for navigation events. Register
|
||||||
// fires when the user uses the back and forward buttons in the browser.
|
// this event listener on the <svelte:window> in the parent element. When
|
||||||
// Instead of reloading the page we use the navigator to navigate to the
|
// the user presses the back or forward buttons in the browser we'll catch
|
||||||
// new page
|
// the event and navigate to the proper directory
|
||||||
if (history_enabled) {
|
popstate = (e: PopStateEvent) => {
|
||||||
window.addEventListener("popstate", () => {
|
|
||||||
// Get the part of the URL after the fs root and navigate to it
|
// Get the part of the URL after the fs root and navigate to it
|
||||||
const path = document.location.pathname.replace("/d/", "")
|
const path = window.location.pathname.replace(/^\/d/, "")
|
||||||
this.navigate(decodeURIComponent(path), false)
|
this.navigate(decodeURI(path), false)
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If you set the loading property to a boolean writable store the navigator
|
|
||||||
// will use it to publish its loading states
|
|
||||||
loading: Writable<boolean> | null = null
|
|
||||||
set_loading = (b: boolean) => {
|
|
||||||
if (this.loading !== null) {
|
|
||||||
this.loading.set(b)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The FSNavigator acts as a svelte store. This allows for DOM reactivity.
|
// The FSNavigator acts as a svelte store. This allows for DOM reactivity.
|
||||||
@@ -72,7 +61,7 @@ export class FSNavigator {
|
|||||||
console.debug("Navigating to path", path, push_history)
|
console.debug("Navigating to path", path, push_history)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.set_loading(true)
|
loading_start()
|
||||||
const resp = await fs_get_node(path)
|
const resp = await fs_get_node(path)
|
||||||
this.open_node(resp, push_history)
|
this.open_node(resp, push_history)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -89,7 +78,7 @@ export class FSNavigator {
|
|||||||
alert("Error: " + err)
|
alert("Error: " + err)
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.set_loading(false)
|
loading_finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +97,7 @@ export class FSNavigator {
|
|||||||
// we still replace the URL with replaceState. This way the user is not
|
// we still replace the URL with replaceState. This way the user is not
|
||||||
// greeted to a 404 page when refreshing after renaming a file
|
// greeted to a 404 page when refreshing after renaming a file
|
||||||
if (this.history_enabled) {
|
if (this.history_enabled) {
|
||||||
window.document.title = node.path[node.base_index].name + " ~ pixeldrain"
|
window.document.title = node.path[node.base_index].name + " / FNX"
|
||||||
const url = "/d" + fs_encode_path(node.path[node.base_index].path) + window.location.hash
|
const url = "/d" + fs_encode_path(node.path[node.base_index].path) + window.location.hash
|
||||||
if (push_history) {
|
if (push_history) {
|
||||||
window.history.pushState({}, window.document.title, url)
|
window.history.pushState({}, window.document.title, url)
|
||||||
@@ -189,14 +178,14 @@ export class FSNavigator {
|
|||||||
|
|
||||||
let siblings: Array<FSNode>
|
let siblings: Array<FSNode>
|
||||||
try {
|
try {
|
||||||
this.set_loading(true)
|
loading_start()
|
||||||
siblings = await this.get_siblings()
|
siblings = await this.get_siblings()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
alert(err)
|
alert(err)
|
||||||
return
|
return
|
||||||
} finally {
|
} finally {
|
||||||
this.set_loading(false)
|
loading_finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
let next_sibling: FSNode | null = null
|
let next_sibling: FSNode | null = null
|
||||||
|
@@ -1,16 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { formatDataVolume, formatThousands } from "util/Formatting"
|
import { formatDataVolume, formatThousands } from "util/Formatting"
|
||||||
import { fs_path_url } from "./FilesystemAPI";
|
import { fs_path_url } from "lib/FilesystemAPI.svelte";
|
||||||
import type { FSNavigator } from "./FSNavigator";
|
import type { FSNavigator } from "./FSNavigator";
|
||||||
|
|
||||||
export let nav: FSNavigator
|
let { nav }: {
|
||||||
|
nav: FSNavigator;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
let loading = true
|
let loading = $state(true)
|
||||||
let downloads = 0
|
let downloads = $state(0)
|
||||||
let transfer_used = 0
|
let transfer_used = $state(0)
|
||||||
let socket = null
|
let socket = null
|
||||||
let error_msg = ""
|
let error_msg = $state("")
|
||||||
|
|
||||||
let connected_to = ""
|
let connected_to = ""
|
||||||
|
|
||||||
@@ -22,9 +24,9 @@ onMount(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
let total_directories = 0
|
let total_directories = $state(0)
|
||||||
let total_files = 0
|
let total_files = $state(0)
|
||||||
let total_file_size = 0
|
let total_file_size = $state(0)
|
||||||
|
|
||||||
const update_base = async () => {
|
const update_base = async () => {
|
||||||
if (!nav.initialized) {
|
if (!nav.initialized) {
|
||||||
@@ -104,7 +106,7 @@ const close_socket = () => {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="group">
|
<div class="group">
|
||||||
<div class="label">Transfer used</div>
|
<div class="label">Egress</div>
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
{loading ? "Loading..." : formatDataVolume(transfer_used, 3)}
|
{loading ? "Loading..." : formatDataVolume(transfer_used, 3)}
|
||||||
</div>
|
</div>
|
||||||
@@ -140,18 +142,11 @@ const close_socket = () => {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.label {
|
.label {
|
||||||
padding-left: 0.5em;
|
text-align: center;
|
||||||
text-align: left;
|
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
}
|
}
|
||||||
.stat {
|
.stat {
|
||||||
line-height: 1.2em;
|
line-height: 1.2em;
|
||||||
}
|
}
|
||||||
@media (max-width: 1000px) {
|
|
||||||
.label {
|
|
||||||
text-align: center;
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
@@ -1,46 +1,56 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import LoadingIndicator from "util/LoadingIndicator.svelte";
|
|
||||||
import EditWindow from "./edit_window/EditWindow.svelte";
|
import EditWindow from "./edit_window/EditWindow.svelte";
|
||||||
import Toolbar from "./Toolbar.svelte";
|
import Toolbar from "./Toolbar.svelte";
|
||||||
import Breadcrumbs from "./Breadcrumbs.svelte";
|
import Breadcrumbs from "./Breadcrumbs.svelte";
|
||||||
import DetailsWindow from "./DetailsWindow.svelte";
|
import DetailsWindow from "./DetailsWindow.svelte";
|
||||||
import FilePreview from "./viewers/FilePreview.svelte";
|
import FilePreview from "./viewers/FilePreview.svelte";
|
||||||
import FSUploadWidget from "./upload_widget/FSUploadWidget.svelte";
|
import FSUploadWidget from "./upload_widget/FSUploadWidget.svelte";
|
||||||
import { fs_download, type FSPath } from "./FilesystemAPI";
|
import { type FSPath } from "lib/FilesystemAPI.svelte";
|
||||||
import Menu from "./Menu.svelte";
|
|
||||||
import { FSNavigator } from "./FSNavigator"
|
import { FSNavigator } from "./FSNavigator"
|
||||||
import { writable } from "svelte/store";
|
|
||||||
import { css_from_path } from "filesystem/edit_window/Branding";
|
import { css_from_path } from "filesystem/edit_window/Branding";
|
||||||
import AffiliatePrompt from "user_home/AffiliatePrompt.svelte";
|
import AffiliatePrompt from "user_home/AffiliatePrompt.svelte";
|
||||||
|
import { current_page_store } from "wrap/RouterStore";
|
||||||
|
|
||||||
let file_viewer: HTMLDivElement
|
let file_preview: FilePreview = $state()
|
||||||
let file_preview: FilePreview
|
let toolbar: Toolbar = $state()
|
||||||
let toolbar: Toolbar
|
let upload_widget: FSUploadWidget = $state()
|
||||||
let upload_widget: FSUploadWidget
|
let details_visible = $state(false)
|
||||||
let details_visible = false
|
let edit_window: EditWindow = $state()
|
||||||
let edit_window: EditWindow
|
let edit_visible = $state(false)
|
||||||
let edit_visible = false
|
let details_window: DetailsWindow = $state()
|
||||||
|
|
||||||
const loading = writable(true)
|
const nav = $state(new FSNavigator(true))
|
||||||
const nav = new FSNavigator(true)
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
nav.loading = loading
|
if ((window as any).intial_node !== undefined) {
|
||||||
|
console.debug("Loading initial node")
|
||||||
nav.open_node((window as any).initial_node as FSPath, false)
|
nav.open_node((window as any).initial_node as FSPath, false)
|
||||||
|
} else {
|
||||||
|
console.debug("No initial node, fetching path", window.location.pathname)
|
||||||
|
nav.navigate(decodeURI(window.location.pathname).replace(/^\/d/, ""), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const page_sub = current_page_store.subscribe(() => {
|
||||||
|
console.debug("Caught page transition to", window.location.pathname)
|
||||||
|
nav.navigate(decodeURI(window.location.pathname).replace(/^\/d/, ""), false)
|
||||||
|
})
|
||||||
|
|
||||||
// Subscribe to navigation updates. This function returns a deconstructor
|
// Subscribe to navigation updates. This function returns a deconstructor
|
||||||
// which we can conveniently return from our mount function as well
|
// which we can conveniently return from our mount function as well
|
||||||
return nav.subscribe(nav => {
|
const nav_sub = nav.subscribe(nav => {
|
||||||
if (!nav.initialized) {
|
if (!nav.initialized) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom CSS rules for the whole viewer
|
// Custom CSS rules for the whole viewer
|
||||||
document.documentElement.style = css_from_path(nav.path)
|
document.documentElement.style = css_from_path(nav.path)
|
||||||
|
|
||||||
loading.set(false)
|
|
||||||
})
|
})
|
||||||
|
return () => {
|
||||||
|
page_sub()
|
||||||
|
nav_sub()
|
||||||
|
document.documentElement.style = ""
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const keydown = (e: KeyboardEvent) => {
|
const keydown = (e: KeyboardEvent) => {
|
||||||
@@ -66,16 +76,11 @@ const keydown = (e: KeyboardEvent) => {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "s":
|
case "s":
|
||||||
fs_download(nav.base)
|
nav.base.download()
|
||||||
break;
|
break;
|
||||||
case "r":
|
case "r":
|
||||||
nav.shuffle = !nav.shuffle
|
nav.shuffle = !nav.shuffle
|
||||||
break;
|
break;
|
||||||
case "f": // F fullscreen
|
|
||||||
if (toolbar) {
|
|
||||||
toolbar.toggle_fullscreen()
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case "a":
|
case "a":
|
||||||
case "ArrowLeft":
|
case "ArrowLeft":
|
||||||
nav.open_sibling(-1)
|
nav.open_sibling(-1)
|
||||||
@@ -125,25 +130,10 @@ const keydown = (e: KeyboardEvent) => {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:keydown={keydown} />
|
<svelte:window onkeydown={keydown} />
|
||||||
|
|
||||||
<div bind:this={file_viewer} class="file_viewer">
|
<div class="filesystem">
|
||||||
<div class="headerbar">
|
|
||||||
<Menu/>
|
|
||||||
<Breadcrumbs nav={nav}/>
|
<Breadcrumbs nav={nav}/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="viewer_area">
|
|
||||||
<Toolbar
|
|
||||||
bind:this={toolbar}
|
|
||||||
nav={nav}
|
|
||||||
file_viewer={file_viewer}
|
|
||||||
file_preview={file_preview}
|
|
||||||
bind:details_visible={details_visible}
|
|
||||||
edit_window={edit_window}
|
|
||||||
bind:edit_visible={edit_visible}
|
|
||||||
on:download={() => fs_download(nav.base)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="file_preview">
|
<div class="file_preview">
|
||||||
<FilePreview
|
<FilePreview
|
||||||
@@ -151,26 +141,29 @@ const keydown = (e: KeyboardEvent) => {
|
|||||||
nav={nav}
|
nav={nav}
|
||||||
upload_widget={upload_widget}
|
upload_widget={upload_widget}
|
||||||
edit_window={edit_window}
|
edit_window={edit_window}
|
||||||
on:open_sibling={e => nav.open_sibling(e.detail)}
|
details_window={details_window}
|
||||||
on:download={() => fs_download(nav.base)}
|
|
||||||
on:details={() => details_visible = !details_visible}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<DetailsWindow nav={nav} bind:visible={details_visible} />
|
<Toolbar
|
||||||
|
bind:this={toolbar}
|
||||||
<EditWindow nav={nav} bind:this={edit_window} bind:visible={edit_visible} />
|
nav={nav}
|
||||||
|
bind:details_visible={details_visible}
|
||||||
<!-- This one is included at the highest level so uploads can keep running
|
edit_window={edit_window}
|
||||||
even when the user navigates to a different directory -->
|
bind:edit_visible={edit_visible}
|
||||||
<FSUploadWidget nav={nav} bind:this={upload_widget} />
|
/>
|
||||||
|
|
||||||
<AffiliatePrompt/>
|
|
||||||
|
|
||||||
<LoadingIndicator loading={$loading}/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<DetailsWindow nav={nav} bind:this={details_window} bind:visible={details_visible} />
|
||||||
|
|
||||||
|
<EditWindow nav={nav} bind:this={edit_window} bind:visible={edit_visible} />
|
||||||
|
|
||||||
|
<!-- This one is included at the highest level so uploads can keep running
|
||||||
|
even when the user navigates to a different directory -->
|
||||||
|
<FSUploadWidget nav={nav} bind:this={upload_widget} />
|
||||||
|
|
||||||
|
<AffiliatePrompt/>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
:global(*) {
|
:global(*) {
|
||||||
transition: background-color 0.2s,
|
transition: background-color 0.2s,
|
||||||
@@ -183,56 +176,15 @@ const keydown = (e: KeyboardEvent) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Viewer container */
|
/* Viewer container */
|
||||||
.file_viewer {
|
.filesystem {
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
height: 100vh;
|
||||||
|
width: 100%;
|
||||||
/* Force some variable usage that is normally out of scope */
|
|
||||||
color: var(--body_text_color);
|
|
||||||
|
|
||||||
background-image: var(--background_image, var(--background_pattern));
|
|
||||||
background-color: var(--background_pattern_color);
|
|
||||||
background-size: var(--background_image_size, initial);
|
|
||||||
background-position: var(--background_image_position, initial);
|
|
||||||
background-repeat: var(--background_image_repeat, repeat);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Headerbar (row 1) */
|
|
||||||
.headerbar {
|
|
||||||
flex: 0 0 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
text-align: left;
|
|
||||||
box-shadow: none;
|
|
||||||
background-color: var(--shaded_background);
|
|
||||||
backdrop-filter: blur(4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* File preview area (row 2) */
|
|
||||||
.viewer_area {
|
|
||||||
flex: 1 1 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* This max-width needs to be synced with the .toolbar max-width in
|
|
||||||
Toolbar.svelte and the .label max-width in FileStats.svelte */
|
|
||||||
@media (max-width: 1000px) {
|
|
||||||
.viewer_area {
|
|
||||||
flex-direction: column-reverse;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.file_preview {
|
.file_preview {
|
||||||
flex: 1 1 0;
|
flex: 1 1 auto;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
border: 1px solid var(--separator);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -6,23 +6,31 @@ import { formatDataVolume } from "util/Formatting";
|
|||||||
import { user } from "lib/UserStore";
|
import { user } from "lib/UserStore";
|
||||||
import Dialog from "layout/Dialog.svelte";
|
import Dialog from "layout/Dialog.svelte";
|
||||||
|
|
||||||
let button: HTMLButtonElement
|
let button: HTMLButtonElement = $state()
|
||||||
let dialog: Dialog
|
let dialog: Dialog = $state()
|
||||||
|
|
||||||
export let no_login_label = "Pixeldrain"
|
let {
|
||||||
|
no_login_label = "Pixeldrain",
|
||||||
|
hide_name = true,
|
||||||
|
hide_logo = false,
|
||||||
|
style = "",
|
||||||
|
embedded = false
|
||||||
|
}: {
|
||||||
|
no_login_label?: string;
|
||||||
|
// Hide the label if the screen is smaller than 800px
|
||||||
|
hide_name?: boolean;
|
||||||
|
hide_logo?: boolean;
|
||||||
|
style?: string;
|
||||||
|
embedded?: boolean;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
// Hide the label if the screen is smaller than 800px
|
let target = $derived(embedded ? "_blank" : "_self")
|
||||||
export let hide_name = true
|
|
||||||
export let hide_logo = false
|
|
||||||
export let style = ""
|
|
||||||
export let embedded = false
|
|
||||||
$: target = embedded ? "_blank" : "_self"
|
|
||||||
|
|
||||||
const open = () => dialog.open(button.getBoundingClientRect())
|
const open = () => dialog.open(button.getBoundingClientRect())
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<button bind:this={button} on:click={open} class="button round" title="Menu" style={style}>
|
<button bind:this={button} onclick={open} class="button round" title="Menu" style={style}>
|
||||||
{#if !hide_logo}
|
{#if !hide_logo}
|
||||||
<PixeldrainLogo style="height: 1.6em; width: 1.6em;"/>
|
<PixeldrainLogo style="height: 1.6em; width: 1.6em;"/>
|
||||||
{/if}
|
{/if}
|
||||||
|
@@ -1,22 +1,24 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { FSNavigator } from "./FSNavigator";
|
import type { FSNavigator } from "./FSNavigator";
|
||||||
import { fs_node_icon, fs_share_hotlink_url, fs_share_url, fs_update, node_is_shared, type FSNode, type FSPermissions } from "./FilesystemAPI";
|
import { fs_node_icon, fs_share_hotlink_url, fs_share_url, fs_update, type FSNode, type FSPermissions } from "lib/FilesystemAPI.svelte";
|
||||||
import { copy_text } from "util/Util.svelte";
|
import { copy_text } from "util/Util";
|
||||||
import CopyButton from "layout/CopyButton.svelte";
|
import CopyButton from "layout/CopyButton.svelte";
|
||||||
import Dialog from "layout/Dialog.svelte";
|
import Dialog from "layout/Dialog.svelte";
|
||||||
import { fade } from "svelte/transition";
|
import { fade } from "svelte/transition";
|
||||||
|
|
||||||
export let nav: FSNavigator
|
let { nav }: {
|
||||||
|
nav: FSNavigator;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
let path: FSNode[]
|
let path: FSNode[]
|
||||||
let base: FSNode
|
let base: FSNode = $state()
|
||||||
let toast = ""
|
let toast = $state("")
|
||||||
let share_url = ""
|
let share_url = $state("")
|
||||||
let direct_share_url = ""
|
let direct_share_url = $state("")
|
||||||
let is_parent = false
|
let is_parent = $state(false)
|
||||||
let parent_node: FSNode
|
let parent_node: FSNode = $state()
|
||||||
|
|
||||||
let dialog: Dialog
|
let dialog: Dialog = $state()
|
||||||
export const open = async (e: MouseEvent, p: FSNode[]) => {
|
export const open = async (e: MouseEvent, p: FSNode[]) => {
|
||||||
path = p
|
path = p
|
||||||
base = path[path.length-1]
|
base = path[path.length-1]
|
||||||
@@ -36,7 +38,7 @@ export const open = async (e: MouseEvent, p: FSNode[]) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const make_public = async () => {
|
const make_public = async () => {
|
||||||
if (!node_is_shared(base)) {
|
if (!base.is_shared()) {
|
||||||
base = await fs_update(
|
base = await fs_update(
|
||||||
base.path,
|
base.path,
|
||||||
{link_permissions: {read: true} as FSPermissions},
|
{link_permissions: {read: true} as FSPermissions},
|
||||||
@@ -113,7 +115,7 @@ const share = async () => {
|
|||||||
<img src={fs_node_icon(parent_node, 64, 64)} class="node_icon" alt="icon"/>
|
<img src={fs_node_icon(parent_node, 64, 64)} class="node_icon" alt="icon"/>
|
||||||
{parent_node.name}
|
{parent_node.name}
|
||||||
<br/>
|
<br/>
|
||||||
<button on:click={async e => {await make_public(); await share()}} style="display: inline;">
|
<button onclick={async e => {await make_public(); await share()}} style="display: inline;">
|
||||||
Only share
|
Only share
|
||||||
<img src={fs_node_icon(base, 64, 64)} class="node_icon" alt="icon"/>
|
<img src={fs_node_icon(base, 64, 64)} class="node_icon" alt="icon"/>
|
||||||
{base.name}
|
{base.name}
|
||||||
|
@@ -1,26 +1,28 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher } from "svelte";
|
import { copy_text } from "util/Util";
|
||||||
import { copy_text } from "util/Util.svelte";
|
|
||||||
import FileStats from "./FileStats.svelte";
|
import FileStats from "./FileStats.svelte";
|
||||||
import type { FSNavigator } from "./FSNavigator";
|
import type { FSNavigator } from "./FSNavigator";
|
||||||
import EditWindow from "./edit_window/EditWindow.svelte";
|
import EditWindow from "./edit_window/EditWindow.svelte";
|
||||||
import FilePreview from "./viewers/FilePreview.svelte";
|
import { fs_share_url, path_is_shared } from "lib/FilesystemAPI.svelte";
|
||||||
import { fs_share_url } from "./FilesystemAPI";
|
|
||||||
import ShareDialog from "./ShareDialog.svelte";
|
import ShareDialog from "./ShareDialog.svelte";
|
||||||
|
import { bookmark_add, bookmark_del, bookmarks_store, is_bookmark } from "lib/Bookmarks";
|
||||||
|
|
||||||
let dispatch = createEventDispatcher()
|
let {
|
||||||
|
nav = $bindable(),
|
||||||
|
details_visible = $bindable(false),
|
||||||
|
edit_window,
|
||||||
|
edit_visible = $bindable(false)
|
||||||
|
}: {
|
||||||
|
nav: FSNavigator;
|
||||||
|
details_visible?: boolean;
|
||||||
|
edit_window: EditWindow;
|
||||||
|
edit_visible?: boolean;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
export let nav: FSNavigator
|
let share_dialog: ShareDialog = $state()
|
||||||
export let details_visible = false
|
let link_copied = $state(false)
|
||||||
export let edit_window: EditWindow
|
|
||||||
export let edit_visible = false
|
|
||||||
export let file_viewer: HTMLDivElement
|
|
||||||
export let file_preview: FilePreview
|
|
||||||
let share_dialog: ShareDialog
|
|
||||||
|
|
||||||
$: share_url = fs_share_url($nav.path)
|
|
||||||
let link_copied = false
|
|
||||||
export const copy_link = () => {
|
export const copy_link = () => {
|
||||||
|
const share_url = fs_share_url($nav.path)
|
||||||
if (share_url === "") {
|
if (share_url === "") {
|
||||||
edit_window.edit(nav.base, true, "share")
|
edit_window.edit(nav.base, true, "share")
|
||||||
return
|
return
|
||||||
@@ -30,103 +32,63 @@ export const copy_link = () => {
|
|||||||
link_copied = true
|
link_copied = true
|
||||||
setTimeout(() => {link_copied = false}, 60000)
|
setTimeout(() => {link_copied = false}, 60000)
|
||||||
}
|
}
|
||||||
|
|
||||||
let fullscreen = false
|
|
||||||
export const toggle_fullscreen = () => {
|
|
||||||
if (document.fullscreenElement !== null) {
|
|
||||||
try {
|
|
||||||
document.exitFullscreen()
|
|
||||||
} catch (err) {
|
|
||||||
console.debug("Failed to exit fullscreen", err)
|
|
||||||
}
|
|
||||||
fullscreen = false
|
|
||||||
} else {
|
|
||||||
if (!file_preview.toggle_fullscreen()) {
|
|
||||||
file_viewer.requestFullscreen()
|
|
||||||
}
|
|
||||||
fullscreen = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let expanded = true
|
|
||||||
let expand = (e: Event) => {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
expanded = !expanded
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="toolbar" class:expanded>
|
<div class="toolbar">
|
||||||
<div class="stats_container" on:click={expand} on:keypress={expand} role="button" tabindex="0">
|
|
||||||
<button class="button_expand hidden_vertical" on:click={expand}>
|
|
||||||
{#if expanded}
|
|
||||||
<i class="icon">expand_more</i>
|
|
||||||
{:else}
|
|
||||||
<i class="icon">expand_less</i>
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
<FileStats nav={nav}/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="separator"></div>
|
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
|
<FileStats nav={nav}/>
|
||||||
|
|
||||||
<div class="button_row">
|
<div class="button_row">
|
||||||
<button on:click={() => {nav.open_sibling(-1)}}>
|
<button onclick={() => {nav.open_sibling(-1)}}>
|
||||||
<i class="icon">skip_previous</i>
|
<i class="icon">skip_previous</i>
|
||||||
</button>
|
</button>
|
||||||
<button on:click={() => {nav.shuffle = !nav.shuffle}} class:button_highlight={nav.shuffle}>
|
<button onclick={() => {nav.shuffle = !nav.shuffle}} class:button_highlight={nav.shuffle}>
|
||||||
<i class="icon">shuffle</i>
|
<i class="icon">shuffle</i>
|
||||||
</button>
|
</button>
|
||||||
<button on:click={() => {nav.open_sibling(1)}}>
|
<button onclick={() => {nav.open_sibling(1)}}>
|
||||||
<i class="icon">skip_next</i>
|
<i class="icon">skip_next</i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="separator hidden_horizontal"></div>
|
<button onclick={() => $nav.base.download()}>
|
||||||
|
|
||||||
<button on:click={() => dispatch("download")}>
|
|
||||||
<i class="icon">save</i>
|
<i class="icon">save</i>
|
||||||
<span>Download</span>
|
<span>Download</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if share_url !== ""}
|
{#if is_bookmark($bookmarks_store, $nav.base.id)}
|
||||||
<button on:click={copy_link} class:button_highlight={link_copied}>
|
<button onclick={() => bookmark_del($nav.base.id)}>
|
||||||
|
<i class="icon">bookmark_remove</i>
|
||||||
|
<span>Bookmark</span>
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<button onclick={() => bookmark_add($nav.base)}>
|
||||||
|
<i class="icon">bookmark_add</i>
|
||||||
|
<span>Bookmark</span>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if path_is_shared($nav.path)}
|
||||||
|
<button onclick={copy_link} class:button_highlight={link_copied}>
|
||||||
<i class="icon">content_copy</i>
|
<i class="icon">content_copy</i>
|
||||||
<span><u>C</u>opy link</span>
|
<span><u>C</u>opy link</span>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Share button is enabled when: The browser has a sharing API, or the user can edit the file (to enable sharing)-->
|
<!-- Share button is enabled when: The browser has a sharing API, or the user can edit the file (to enable sharing)-->
|
||||||
{#if $nav.base.id !== "me" && (navigator.share !== undefined || $nav.permissions.write === true)}
|
{#if navigator.share !== undefined || $nav.permissions.write === true}
|
||||||
<button on:click={(e) => share_dialog.open(e, nav.path)}>
|
<button onclick={(e) => share_dialog.open(e, nav.path)}>
|
||||||
<i class="icon">share</i>
|
<i class="icon">share</i>
|
||||||
<span>Share</span>
|
<span>Share</span>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<button
|
<button onclick={() => details_visible = !details_visible} class:button_highlight={details_visible}>
|
||||||
class="toolbar_button"
|
|
||||||
on:click={toggle_fullscreen}
|
|
||||||
class:button_highlight={fullscreen}
|
|
||||||
title="Open page in full screen mode">
|
|
||||||
{#if fullscreen}
|
|
||||||
<i class="icon">fullscreen_exit</i>
|
|
||||||
{:else}
|
|
||||||
<i class="icon">fullscreen</i>
|
|
||||||
{/if}
|
|
||||||
<span>Fullscreen</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="separator hidden_horizontal"></div>
|
|
||||||
|
|
||||||
<button on:click={() => details_visible = !details_visible} class:button_highlight={details_visible}>
|
|
||||||
<i class="icon">help</i>
|
<i class="icon">help</i>
|
||||||
<span>Deta<u>i</u>ls</span>
|
<span>Deta<u>i</u>ls</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if $nav.base.id !== "me" && $nav.permissions.write === true}
|
{#if $nav.base.id !== "me" && $nav.permissions.write === true}
|
||||||
<button on:click={() => edit_window.edit(nav.base, true, "file")} class:button_highlight={edit_visible}>
|
<button onclick={() => edit_window.edit(nav.base, true, "file")} class:button_highlight={edit_visible}>
|
||||||
<i class="icon">edit</i>
|
<i class="icon">edit</i>
|
||||||
<span><u>E</u>dit</span>
|
<span><u>E</u>dit</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -140,22 +102,17 @@ let expand = (e: Event) => {
|
|||||||
.toolbar {
|
.toolbar {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: scroll;
|
overflow-y: hidden;
|
||||||
transition: max-height 0.3s;
|
transition: max-height 0.3s;
|
||||||
background-color: var(--shaded_background);
|
border-top: 1px solid var(--separator);
|
||||||
|
|
||||||
|
background: var(--shaded_background);
|
||||||
backdrop-filter: blur(4px);
|
backdrop-filter: blur(4px);
|
||||||
}
|
}
|
||||||
.grid {
|
.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(7.5em, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(7.5em, 1fr));
|
||||||
}
|
}
|
||||||
.separator {
|
|
||||||
height: 1px;
|
|
||||||
margin: 2px 0;
|
|
||||||
width: 100%;
|
|
||||||
background-color: var(--separator);
|
|
||||||
}
|
|
||||||
|
|
||||||
.button_row {
|
.button_row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -164,46 +121,4 @@ let expand = (e: Event) => {
|
|||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats_container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
.button_expand {
|
|
||||||
line-height: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden_vertical {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.hidden_horizontal {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* This max-width needs to be synced with the .viewer_area max-width in
|
|
||||||
Toolbar.svelte and the .label max-width in FileStats.svelte */
|
|
||||||
@media (max-width: 1000px) {
|
|
||||||
.toolbar {
|
|
||||||
overflow-y: hidden;
|
|
||||||
max-height: 2.1em;
|
|
||||||
}
|
|
||||||
.toolbar.expanded {
|
|
||||||
overflow-y: scroll;
|
|
||||||
max-height: 25vh;
|
|
||||||
}
|
|
||||||
.stats_container {
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
.separator {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden_vertical {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.hidden_horizontal {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
@@ -1,12 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Button from "layout/Button.svelte";
|
import Button from "layout/Button.svelte";
|
||||||
import type { FSPermissions, NodeOptions } from "filesystem/FilesystemAPI";
|
import type { FSPermissions, NodeOptions } from "lib/FilesystemAPI.svelte";
|
||||||
import PermissionButton from "./PermissionButton.svelte";
|
import PermissionButton from "./PermissionButton.svelte";
|
||||||
|
|
||||||
export let options: NodeOptions
|
let {
|
||||||
|
options = $bindable()
|
||||||
|
}: {
|
||||||
|
options: NodeOptions;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
let new_user_id = ""
|
let new_user_id = $state("")
|
||||||
let new_user_perms = <FSPermissions>{read: true}
|
let new_user_perms = $state(<FSPermissions>{read: true})
|
||||||
const add_user = (e: SubmitEvent) => {
|
const add_user = (e: SubmitEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (options.user_permissions === undefined) {
|
if (options.user_permissions === undefined) {
|
||||||
@@ -19,8 +23,8 @@ const del_user = (id: string) => {
|
|||||||
options.user_permissions = options.user_permissions
|
options.user_permissions = options.user_permissions
|
||||||
}
|
}
|
||||||
|
|
||||||
let new_password = ""
|
let new_password = $state("")
|
||||||
let new_password_perms = <FSPermissions>{read: true}
|
let new_password_perms = $state(<FSPermissions>{read: true})
|
||||||
const add_password = (e: SubmitEvent) => {
|
const add_password = (e: SubmitEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (options.password_permissions === undefined) {
|
if (options.password_permissions === undefined) {
|
||||||
@@ -64,7 +68,7 @@ const del_password = (pass: string) => {
|
|||||||
not receive an e-mail invite. Giving write access to a user without giving
|
not receive an e-mail invite. Giving write access to a user without giving
|
||||||
read access as well does not actually allow them to write anything.
|
read access as well does not actually allow them to write anything.
|
||||||
</p>
|
</p>
|
||||||
<form on:submit={add_user} class="row">
|
<form onsubmit={add_user} class="row">
|
||||||
<input type="text" bind:value={new_user_id} placeholder="Username" class="grow" size="1">
|
<input type="text" bind:value={new_user_id} placeholder="Username" class="grow" size="1">
|
||||||
<Button type="submit" icon="add" label="Add"/>
|
<Button type="submit" icon="add" label="Add"/>
|
||||||
<div class="perms">
|
<div class="perms">
|
||||||
@@ -94,7 +98,7 @@ const del_password = (pass: string) => {
|
|||||||
<p>
|
<p>
|
||||||
<b>This feature is not implemented currently!</b>
|
<b>This feature is not implemented currently!</b>
|
||||||
</p>
|
</p>
|
||||||
<form on:submit={add_password} class="row">
|
<form onsubmit={add_password} class="row">
|
||||||
<input type="text" bind:value={new_password} placeholder="Password" class="grow" size="1">
|
<input type="text" bind:value={new_password} placeholder="Password" class="grow" size="1">
|
||||||
<Button type="submit" icon="add" label="Add"/>
|
<Button type="submit" icon="add" label="Add"/>
|
||||||
<div class="perms">
|
<div class="perms">
|
||||||
|
@@ -2,11 +2,39 @@ import parse from "pure-color/parse";
|
|||||||
import rgb2hsl from "pure-color/convert/rgb2hsl";
|
import rgb2hsl from "pure-color/convert/rgb2hsl";
|
||||||
import hsl2rgb from "pure-color/convert/hsl2rgb";
|
import hsl2rgb from "pure-color/convert/hsl2rgb";
|
||||||
import rgb2hex from "pure-color/convert/rgb2hex";
|
import rgb2hex from "pure-color/convert/rgb2hex";
|
||||||
|
import type { FSNode, FSNodeProperties } from "lib/FilesystemAPI.svelte";
|
||||||
|
|
||||||
|
type Style = {
|
||||||
|
input_background?: string,
|
||||||
|
input_hover_background?: string,
|
||||||
|
input_text?: string,
|
||||||
|
highlight_color?: string,
|
||||||
|
highlight_background?: string,
|
||||||
|
highlight_text_color?: string,
|
||||||
|
link_color?: string,
|
||||||
|
danger_color?: string,
|
||||||
|
danger_text_color?: string,
|
||||||
|
background_color?: string,
|
||||||
|
background?: string,
|
||||||
|
background_text_color?: string,
|
||||||
|
background_pattern_color?: string,
|
||||||
|
body_color?: string,
|
||||||
|
body_background?: string,
|
||||||
|
body_text_color?: string,
|
||||||
|
shaded_background?: string,
|
||||||
|
separator?: string,
|
||||||
|
shadow_color?: string,
|
||||||
|
card_color?: string,
|
||||||
|
background_image?: string,
|
||||||
|
background_image_size?: string,
|
||||||
|
background_image_position?: string,
|
||||||
|
background_image_repeat?: string,
|
||||||
|
}
|
||||||
|
|
||||||
// Generate a branding style from a file's properties map
|
// Generate a branding style from a file's properties map
|
||||||
export const branding_from_path = path => {
|
export const branding_from_path = (path: Array<FSNode>) => {
|
||||||
let style = {}
|
const style = {}
|
||||||
for (let node of path) {
|
for (const node of path) {
|
||||||
add_styles(style, node.properties)
|
add_styles(style, node.properties)
|
||||||
}
|
}
|
||||||
last_generated_style = style
|
last_generated_style = style
|
||||||
@@ -15,17 +43,17 @@ export const branding_from_path = path => {
|
|||||||
|
|
||||||
// The last style which was generated is cached, when we don't have a complete
|
// The last style which was generated is cached, when we don't have a complete
|
||||||
// path to generate the style with we will use the cached style as a basis
|
// path to generate the style with we will use the cached style as a basis
|
||||||
let last_generated_style = {}
|
let last_generated_style: Style = {}
|
||||||
export const branding_from_node = node => {
|
export const branding_from_props = (props: FSNodeProperties) => {
|
||||||
add_styles(last_generated_style, node.properties)
|
add_styles(last_generated_style, props)
|
||||||
return gen_css(last_generated_style)
|
return gen_css(last_generated_style)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const css_from_path = path => {
|
export const css_from_path = (path: Array<FSNode>) => {
|
||||||
return gen_css(branding_from_path(path))
|
return gen_css(branding_from_path(path))
|
||||||
}
|
}
|
||||||
|
|
||||||
const gen_css = style => {
|
const gen_css = (style: Style) => {
|
||||||
return Object.entries(style).map(([key, value]) => `--${key}:${value}`).join(';');
|
return Object.entries(style).map(([key, value]) => `--${key}:${value}`).join(';');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,7 +61,7 @@ const gen_css = style => {
|
|||||||
// existing style which is passed as the first argument. When navigating to a
|
// existing style which is passed as the first argument. When navigating to a
|
||||||
// path this function is executed on every member of the path so all the styles
|
// path this function is executed on every member of the path so all the styles
|
||||||
// get combined
|
// get combined
|
||||||
const add_styles = (style, properties) => {
|
const add_styles = (style: Style, properties: FSNodeProperties) => {
|
||||||
if (!properties || !properties.branding_enabled || properties.branding_enabled !== "true") {
|
if (!properties || !properties.branding_enabled || properties.branding_enabled !== "true") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -83,7 +111,7 @@ const add_styles = (style, properties) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const add_contrast = (color, amt) => {
|
const add_contrast = (color: string, amt: number) => {
|
||||||
let hsl = rgb2hsl(parse(color)) // Convert hex to hsl
|
let hsl = rgb2hsl(parse(color)) // Convert hex to hsl
|
||||||
// If the lightness is less than 40 it is considered a dark colour. This
|
// If the lightness is less than 40 it is considered a dark colour. This
|
||||||
// threshold is 40 instead of 50 because overall dark text is more legible
|
// threshold is 40 instead of 50 because overall dark text is more legible
|
||||||
@@ -96,20 +124,20 @@ const add_contrast = (color, amt) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Darken and desaturate. Only used for shadows
|
// Darken and desaturate. Only used for shadows
|
||||||
const darken = (color, percent) => {
|
const darken = (color: string, percent: number) => {
|
||||||
let hsl = rgb2hsl(parse(color)) // Convert hex to hsl
|
let hsl = rgb2hsl(parse(color)) // Convert hex to hsl
|
||||||
hsl[1] = hsl[1] * percent
|
hsl[1] = hsl[1] * percent
|
||||||
hsl[2] = hsl[2] * percent
|
hsl[2] = hsl[2] * percent
|
||||||
return rgb2hex(hsl2rgb(hsl)) // Convert back to hex
|
return rgb2hex(hsl2rgb(hsl)) // Convert back to hex
|
||||||
}
|
}
|
||||||
|
|
||||||
const set_alpha = (color, amt) => {
|
const set_alpha = (color: string, amt: number) => {
|
||||||
let rgb = parse(color)
|
let rgb = parse(color)
|
||||||
rgb.push(amt)
|
rgb.push(amt)
|
||||||
return "rgba(" + rgb.join(", ") + ")"
|
return "rgba(" + rgb.join(", ") + ")"
|
||||||
}
|
}
|
||||||
|
|
||||||
const generate_link_color = (link_color, body_color) => {
|
const generate_link_color = (link_color: string, body_color: string) => {
|
||||||
let link = rgb2hsl(parse(link_color))
|
let link = rgb2hsl(parse(link_color))
|
||||||
let body = rgb2hsl(parse(body_color))
|
let body = rgb2hsl(parse(body_color))
|
||||||
|
|
||||||
|
@@ -1,38 +1,44 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher } from "svelte";
|
|
||||||
import ThemePresets from "./ThemePresets.svelte";
|
import ThemePresets from "./ThemePresets.svelte";
|
||||||
import { fs_update, fs_node_type, type FSNode, type NodeOptions, node_is_shared, type FSPermissions } from "filesystem/FilesystemAPI";
|
import { fs_update, fs_node_type, type FSNode, type NodeOptions, type FSPermissions } from "lib/FilesystemAPI.svelte";
|
||||||
import CustomBanner from "filesystem/viewers/CustomBanner.svelte";
|
import CustomBanner from "filesystem/viewers/CustomBanner.svelte";
|
||||||
import HelpButton from "layout/HelpButton.svelte";
|
import HelpButton from "layout/HelpButton.svelte";
|
||||||
import FilePicker from "filesystem/filemanager/FilePicker.svelte";
|
import FilePicker from "filesystem/filemanager/FilePicker.svelte";
|
||||||
let dispatch = createEventDispatcher()
|
import { branding_from_props } from './Branding';
|
||||||
|
|
||||||
export let file: FSNode
|
let {
|
||||||
export let options: NodeOptions
|
file = $bindable(),
|
||||||
export let enabled: boolean
|
options = $bindable(),
|
||||||
|
enabled = $bindable(),
|
||||||
|
custom_css = $bindable(),
|
||||||
|
}: {
|
||||||
|
file: FSNode
|
||||||
|
options: NodeOptions
|
||||||
|
enabled: boolean
|
||||||
|
custom_css: string
|
||||||
|
} = $props();
|
||||||
|
|
||||||
$: update_colors(options)
|
$effect(() => {
|
||||||
const update_colors = (options: NodeOptions) => {
|
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
options.branding_enabled = "true"
|
options.branding_enabled = "true"
|
||||||
dispatch("style_change")
|
custom_css = branding_from_props(options)
|
||||||
} else {
|
} else {
|
||||||
options.branding_enabled = ""
|
options.branding_enabled = ""
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
let picker: FilePicker
|
let picker: FilePicker = $state()
|
||||||
let picking = ""
|
let picking = ""
|
||||||
const pick_image = (type: string) => {
|
const pick_image = (type: string) => {
|
||||||
picking = type
|
picking = type
|
||||||
picker.open(file.path)
|
picker.open(file.path)
|
||||||
}
|
}
|
||||||
const handle_picker = async (e: CustomEvent<FSNode[]>) => {
|
const handle_picker = async (nodes: FSNode[]) => {
|
||||||
if (e.detail.length !== 1) {
|
if (nodes.length !== 1) {
|
||||||
alert("Please select one file")
|
alert("Please select one file")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let f = e.detail[0]
|
let f = nodes[0]
|
||||||
|
|
||||||
if (fs_node_type(f) !== "image") {
|
if (fs_node_type(f) !== "image") {
|
||||||
alert("Please select an image file")
|
alert("Please select an image file")
|
||||||
@@ -43,10 +49,10 @@ const handle_picker = async (e: CustomEvent<FSNode[]>) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If this image is not public, it will be made public
|
// If this image is not public, it will be made public
|
||||||
if (!node_is_shared(f)) {
|
if (!f.is_shared()) {
|
||||||
try {
|
try {
|
||||||
f = await fs_update(
|
f = await fs_update(
|
||||||
e.detail[0].path,
|
nodes[0].path,
|
||||||
{link_permissions: {read: true} as FSPermissions},
|
{link_permissions: {read: true} as FSPermissions},
|
||||||
)
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -61,7 +67,7 @@ const handle_picker = async (e: CustomEvent<FSNode[]>) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let highlight_info = false
|
let highlight_info = $state(false)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
@@ -90,7 +96,7 @@ let highlight_info = false
|
|||||||
<div style="display: inline-block">Highlight</div>
|
<div style="display: inline-block">Highlight</div>
|
||||||
<HelpButton bind:toggle={highlight_info}/>
|
<HelpButton bind:toggle={highlight_info}/>
|
||||||
</div>
|
</div>
|
||||||
<input type="color" bind:value={options.brand_highlight_color}/>
|
<input type="color" value={options.brand_highlight_color} onchange={e => options.brand_highlight_color = (e.target as HTMLInputElement).value}/>
|
||||||
<input type="text" bind:value={options.brand_highlight_color}/>
|
<input type="text" bind:value={options.brand_highlight_color}/>
|
||||||
{#if highlight_info}
|
{#if highlight_info}
|
||||||
<p class="span3">
|
<p class="span3">
|
||||||
@@ -101,19 +107,19 @@ let highlight_info = false
|
|||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
<div>Button and input</div>
|
<div>Button and input</div>
|
||||||
<input type="color" bind:value={options.brand_input_color}/>
|
<input type="color" value={options.brand_input_color} onchange={e => options.brand_input_color = (e.target as HTMLInputElement).value}/>
|
||||||
<input type="text" bind:value={options.brand_input_color}/>
|
<input type="text" bind:value={options.brand_input_color}/>
|
||||||
<div>Delete button</div>
|
<div>Delete button</div>
|
||||||
<input type="color" bind:value={options.brand_danger_color}/>
|
<input type="color" value={options.brand_danger_color} onchange={e => options.brand_danger_color = (e.target as HTMLInputElement).value}/>
|
||||||
<input type="text" bind:value={options.brand_danger_color}/>
|
<input type="text" bind:value={options.brand_danger_color}/>
|
||||||
<div>Background</div>
|
<div>Background</div>
|
||||||
<input type="color" bind:value={options.brand_background_color}/>
|
<input type="color" value={options.brand_background_color} onchange={e => options.brand_background_color = (e.target as HTMLInputElement).value}/>
|
||||||
<input type="text" bind:value={options.brand_background_color}/>
|
<input type="text" bind:value={options.brand_background_color}/>
|
||||||
<div>Body</div>
|
<div>Body</div>
|
||||||
<input type="color" bind:value={options.brand_body_color}/>
|
<input type="color" value={options.brand_body_color} onchange={e => options.brand_body_color = (e.target as HTMLInputElement).value}/>
|
||||||
<input type="text" bind:value={options.brand_body_color}/>
|
<input type="text" bind:value={options.brand_body_color}/>
|
||||||
<div>Card</div>
|
<div>Card</div>
|
||||||
<input type="color" bind:value={options.brand_card_color}/>
|
<input type="color" value={options.brand_card_color} onchange={e => options.brand_card_color = (e.target as HTMLInputElement).value}/>
|
||||||
<input type="text" bind:value={options.brand_card_color}/>
|
<input type="text" bind:value={options.brand_card_color}/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
@@ -127,7 +133,7 @@ let highlight_info = false
|
|||||||
working. Recommended dimensions for the header image are 1000x90 px.
|
working. Recommended dimensions for the header image are 1000x90 px.
|
||||||
</p>
|
</p>
|
||||||
<div>Header image ID</div>
|
<div>Header image ID</div>
|
||||||
<button on:click={() => pick_image("brand_header_image")}>
|
<button onclick={() => pick_image("brand_header_image")}>
|
||||||
<i class="icon">folder_open</i>
|
<i class="icon">folder_open</i>
|
||||||
Pick
|
Pick
|
||||||
</button>
|
</button>
|
||||||
@@ -135,7 +141,7 @@ let highlight_info = false
|
|||||||
<div>Header image link</div>
|
<div>Header image link</div>
|
||||||
<input class="span2" type="text" bind:value={options.brand_header_link}/>
|
<input class="span2" type="text" bind:value={options.brand_header_link}/>
|
||||||
<div>Background image ID</div>
|
<div>Background image ID</div>
|
||||||
<button on:click={() => pick_image("brand_background_image")}>
|
<button onclick={() => pick_image("brand_background_image")}>
|
||||||
<i class="icon">folder_open</i>
|
<i class="icon">folder_open</i>
|
||||||
Pick
|
Pick
|
||||||
</button>
|
</button>
|
||||||
@@ -177,7 +183,7 @@ let highlight_info = false
|
|||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<FilePicker bind:this={picker} on:files={handle_picker}/>
|
<FilePicker bind:this={picker} callback={handle_picker}/>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
input[type="color"] {
|
input[type="color"] {
|
||||||
|
@@ -1,20 +1,26 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { fs_rename, fs_update, type FSNode, type NodeOptions } from "filesystem/FilesystemAPI";
|
import { fs_rename, fs_update, type FSNode, type NodeOptions } from "lib/FilesystemAPI.svelte";
|
||||||
import Modal from "util/Modal.svelte";
|
import Modal from "util/Modal.svelte";
|
||||||
import BrandingOptions from "./BrandingOptions.svelte";
|
import BrandingOptions from "./BrandingOptions.svelte";
|
||||||
import { branding_from_node } from "./Branding";
|
import { branding_from_props } from "./Branding";
|
||||||
import FileOptions from "./FileOptions.svelte";
|
import FileOptions from "./FileOptions.svelte";
|
||||||
import SharingOptions from "./SharingOptions.svelte";
|
import SharingOptions from "./SharingOptions.svelte";
|
||||||
import AccessControl from "./AccessControl.svelte";
|
import AccessControl from "./AccessControl.svelte";
|
||||||
import type { FSNavigator } from "filesystem/FSNavigator";
|
import type { FSNavigator } from "filesystem/FSNavigator";
|
||||||
|
import { loading_finish, loading_start } from "lib/Loading";
|
||||||
|
|
||||||
export let nav: FSNavigator
|
let file: FSNode = $state({} as FSNode)
|
||||||
let file: FSNode = {} as FSNode
|
let options: NodeOptions = $state({} as NodeOptions)
|
||||||
let options: NodeOptions = {} as NodeOptions
|
|
||||||
|
|
||||||
let custom_css = ""
|
let custom_css = $state("")
|
||||||
|
|
||||||
export let visible: boolean
|
let {
|
||||||
|
nav,
|
||||||
|
visible = $bindable()
|
||||||
|
}: {
|
||||||
|
nav: FSNavigator;
|
||||||
|
visible: boolean;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
// Open the edit window. Argument 1 is the file to edit, 2 is whether the file
|
// Open the edit window. Argument 1 is the file to edit, 2 is whether the file
|
||||||
// should be opened after the user finishes editing and 3 is the default tab
|
// should be opened after the user finishes editing and 3 is the default tab
|
||||||
@@ -39,22 +45,13 @@ export const edit = (f: FSNode, oae = false, open_tab = "") => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
options.custom_domain_name = file.custom_domain_name
|
options.custom_domain_name = file.custom_domain_name
|
||||||
|
|
||||||
options.shared = !(file.id === undefined || file.id === "")
|
|
||||||
if (options.shared) {
|
|
||||||
if (file.link_permissions === undefined) {
|
|
||||||
// Default to read-only for public links
|
|
||||||
file.link_permissions = { owner: false, read: true, write: false, delete: false}
|
|
||||||
} else {
|
|
||||||
options.link_permissions = file.link_permissions
|
options.link_permissions = file.link_permissions
|
||||||
}
|
|
||||||
options.user_permissions = file.user_permissions
|
options.user_permissions = file.user_permissions
|
||||||
options.password_permissions = file.password_permissions
|
options.password_permissions = file.password_permissions
|
||||||
}
|
|
||||||
|
|
||||||
branding_enabled = options.branding_enabled === "true"
|
branding_enabled = options.branding_enabled === "true"
|
||||||
if (branding_enabled) {
|
if (branding_enabled) {
|
||||||
custom_css = branding_from_node(file)
|
custom_css = branding_from_props(options)
|
||||||
} else {
|
} else {
|
||||||
custom_css = ""
|
custom_css = ""
|
||||||
}
|
}
|
||||||
@@ -62,18 +59,19 @@ export const edit = (f: FSNode, oae = false, open_tab = "") => {
|
|||||||
visible = true
|
visible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
let tab = "file"
|
let tab = $state("file")
|
||||||
let open_after_edit = false
|
let open_after_edit = $state(false)
|
||||||
|
|
||||||
let new_name = ""
|
let new_name = $state("")
|
||||||
let branding_enabled = false
|
let branding_enabled = $state(false)
|
||||||
|
|
||||||
const save = async (keep_editing = false) => {
|
const save = async (e: SubmitEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
console.debug("Saving file", file.path)
|
console.debug("Saving file", file.path)
|
||||||
|
|
||||||
let new_file: FSNode
|
let new_file: FSNode
|
||||||
try {
|
try {
|
||||||
nav.set_loading(true)
|
loading_start()
|
||||||
options.branding_enabled = JSON.stringify(branding_enabled)
|
options.branding_enabled = JSON.stringify(branding_enabled)
|
||||||
|
|
||||||
new_file = await fs_update(file.path, options)
|
new_file = await fs_update(file.path, options)
|
||||||
@@ -97,7 +95,7 @@ const save = async (keep_editing = false) => {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
} finally {
|
} finally {
|
||||||
nav.set_loading(false)
|
loading_finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (open_after_edit) {
|
if (open_after_edit) {
|
||||||
@@ -105,36 +103,32 @@ const save = async (keep_editing = false) => {
|
|||||||
} else {
|
} else {
|
||||||
nav.reload()
|
nav.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keep_editing) {
|
|
||||||
edit(new_file, open_after_edit)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:visible={visible} title="Edit {file.name}" width="800px" form="edit_form" style="color: var(--body_text_color); {custom_css}">
|
<Modal bind:visible={visible} title="Edit {file.name}" width="800px" form="edit_form" style="color: var(--body_text_color); {custom_css}">
|
||||||
<div class="tab_bar">
|
<div class="tab_bar">
|
||||||
<button class:button_highlight={tab === "file"} on:click={() => tab = "file"}>
|
<button class:button_highlight={tab === "file"} onclick={() => tab = "file"}>
|
||||||
<i class="icon">edit</i>
|
<i class="icon">edit</i>
|
||||||
Properties
|
Properties
|
||||||
</button>
|
</button>
|
||||||
<button class:button_highlight={tab === "share"} on:click={() => tab = "share"}>
|
<button class:button_highlight={tab === "share"} onclick={() => tab = "share"}>
|
||||||
<i class="icon">share</i>
|
<i class="icon">share</i>
|
||||||
Sharing
|
Sharing
|
||||||
</button>
|
</button>
|
||||||
{#if options.shared && $nav.permissions.owner}
|
{#if $nav.permissions.owner}
|
||||||
<button class:button_highlight={tab === "access"} on:click={() => tab = "access"}>
|
<button class:button_highlight={tab === "access"} onclick={() => tab = "access"}>
|
||||||
<i class="icon">key</i>
|
<i class="icon">key</i>
|
||||||
Access control
|
Access control
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
<button class:button_highlight={tab === "branding"} on:click={() => tab = "branding"}>
|
<button class:button_highlight={tab === "branding"} onclick={() => tab = "branding"}>
|
||||||
<i class="icon">palette</i>
|
<i class="icon">palette</i>
|
||||||
Branding
|
Branding
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form id="edit_form" on:submit|preventDefault={() => save(false)}></form>
|
<form id="edit_form" onsubmit={save}></form>
|
||||||
|
|
||||||
<div class="tab_content">
|
<div class="tab_content">
|
||||||
{#if tab === "file"}
|
{#if tab === "file"}
|
||||||
@@ -146,7 +140,10 @@ const save = async (keep_editing = false) => {
|
|||||||
bind:open_after_edit
|
bind:open_after_edit
|
||||||
/>
|
/>
|
||||||
{:else if tab === "share"}
|
{:else if tab === "share"}
|
||||||
<SharingOptions bind:file bind:options on:save={() => save(true)} />
|
<SharingOptions
|
||||||
|
bind:file
|
||||||
|
bind:options
|
||||||
|
/>
|
||||||
{:else if tab === "access"}
|
{:else if tab === "access"}
|
||||||
<AccessControl bind:options />
|
<AccessControl bind:options />
|
||||||
{:else if tab === "branding"}
|
{:else if tab === "branding"}
|
||||||
@@ -154,7 +151,7 @@ const save = async (keep_editing = false) => {
|
|||||||
bind:enabled={branding_enabled}
|
bind:enabled={branding_enabled}
|
||||||
bind:options={options}
|
bind:options={options}
|
||||||
bind:file
|
bind:file
|
||||||
on:style_change={e => custom_css = branding_from_node(file)}
|
bind:custom_css
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,29 +1,38 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Button from "layout/Button.svelte";
|
import Button from "layout/Button.svelte";
|
||||||
import { fs_delete_all, type FSNode } from "filesystem/FilesystemAPI";
|
import { fs_delete_all, type FSNode } from "lib/FilesystemAPI.svelte";
|
||||||
import PathLink from "filesystem/util/PathLink.svelte";
|
import PathLink from "filesystem/util/PathLink.svelte";
|
||||||
import type { FSNavigator } from "filesystem/FSNavigator";
|
import type { FSNavigator } from "filesystem/FSNavigator";
|
||||||
|
import { loading_finish, loading_start } from "lib/Loading";
|
||||||
|
|
||||||
export let nav: FSNavigator
|
let {
|
||||||
export let file: FSNode = {} as FSNode
|
nav,
|
||||||
export let new_name: string
|
file = $bindable({} as FSNode),
|
||||||
export let visible: boolean
|
new_name = $bindable(),
|
||||||
export let open_after_edit: boolean
|
visible = $bindable(),
|
||||||
|
open_after_edit = $bindable(false)
|
||||||
|
}: {
|
||||||
|
nav: FSNavigator;
|
||||||
|
file?: FSNode;
|
||||||
|
new_name: string;
|
||||||
|
visible: boolean;
|
||||||
|
open_after_edit: boolean;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
$: is_root_dir = file.path === "/"+file.id
|
let is_root_dir = $derived(file.path === "/"+file.id)
|
||||||
|
|
||||||
const delete_file = async (e: MouseEvent) => {
|
const delete_file = async (e: MouseEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
nav.set_loading(true)
|
loading_start()
|
||||||
await fs_delete_all(file.path)
|
await fs_delete_all(file.path)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
alert(err)
|
alert(err)
|
||||||
return
|
return
|
||||||
} finally {
|
} finally {
|
||||||
nav.set_loading(false)
|
loading_finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (open_after_edit) {
|
if (open_after_edit) {
|
||||||
|
@@ -1,10 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ToggleButton from "layout/ToggleButton.svelte";
|
import ToggleButton from "layout/ToggleButton.svelte";
|
||||||
import type { FSPermissions } from "filesystem/FilesystemAPI";
|
import type { FSPermissions } from "lib/FilesystemAPI.svelte";
|
||||||
|
|
||||||
export let permissions = <FSPermissions>{}
|
let {
|
||||||
|
permissions = $bindable()
|
||||||
|
}: {
|
||||||
|
permissions: FSPermissions
|
||||||
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ToggleButton group_first bind:on={permissions.read}>Read</ToggleButton>
|
{#if permissions !== undefined}
|
||||||
<ToggleButton group_middle bind:on={permissions.write}>Write</ToggleButton>
|
<ToggleButton group_first bind:on={permissions.read}>Read</ToggleButton>
|
||||||
<ToggleButton group_last bind:on={permissions.delete}>Delete</ToggleButton>
|
<ToggleButton group_middle bind:on={permissions.write}>Write</ToggleButton>
|
||||||
|
<ToggleButton group_last bind:on={permissions.delete}>Delete</ToggleButton>
|
||||||
|
{/if}
|
||||||
|
@@ -1,21 +1,24 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher } from "svelte";
|
import { run } from 'svelte/legacy';
|
||||||
import { domain_url } from "util/Util.svelte";
|
import { domain_url } from "util/Util";
|
||||||
import CopyButton from "layout/CopyButton.svelte";
|
import CopyButton from "layout/CopyButton.svelte";
|
||||||
import { formatDate } from "util/Formatting";
|
import { formatDate } from "util/Formatting";
|
||||||
import type { FSNode, NodeOptions } from "filesystem/FilesystemAPI";
|
import { type FSNode, type NodeOptions } from "lib/FilesystemAPI.svelte";
|
||||||
|
import AccessControl from "./AccessControl.svelte";
|
||||||
|
|
||||||
let dispatch = createEventDispatcher()
|
let {
|
||||||
export let file: FSNode = {} as FSNode
|
file = $bindable(),
|
||||||
export let options: NodeOptions
|
options = $bindable(),
|
||||||
|
}: {
|
||||||
|
file?: FSNode;
|
||||||
|
options: NodeOptions;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
let embed_html: string
|
let embed_html: string = $state()
|
||||||
let preview_area: HTMLDivElement
|
let preview_area: HTMLDivElement = $state()
|
||||||
|
|
||||||
$: share_link = window.location.protocol+"//"+window.location.host+"/d/"+file.id
|
const embed_iframe = (file: FSNode, options: NodeOptions) => {
|
||||||
$: embed_iframe(file, options)
|
if (!file.is_shared()) {
|
||||||
let embed_iframe = (file: FSNode, options: NodeOptions) => {
|
|
||||||
if (!options.shared) {
|
|
||||||
example = false
|
example = false
|
||||||
embed_html = "File is not shared, can't generate embed code"
|
embed_html = "File is not shared, can't generate embed code"
|
||||||
return
|
return
|
||||||
@@ -24,14 +27,14 @@ let embed_iframe = (file: FSNode, options: NodeOptions) => {
|
|||||||
let url = domain_url()+"/d/"+file.id
|
let url = domain_url()+"/d/"+file.id
|
||||||
embed_html = `<iframe ` +
|
embed_html = `<iframe ` +
|
||||||
`src="${url}" ` +
|
`src="${url}" ` +
|
||||||
`style="border: none; width: 100%; max-width 90vw; height: 800px; max-height: 75vh; border-radius: 6px; "` +
|
`style="border: none; width: 100%; max-width 90vw; height: 800px; max-height: 75vh; border-radius: 6px;" ` +
|
||||||
`allowfullscreen` +
|
`allowfullscreen` +
|
||||||
`></iframe>`
|
`></iframe>`
|
||||||
}
|
}
|
||||||
|
|
||||||
let example = false
|
let example = $state(false)
|
||||||
const toggle_example = () => {
|
const toggle_example = () => {
|
||||||
if (options.shared) {
|
if (file.is_shared()) {
|
||||||
example = !example
|
example = !example
|
||||||
if (example) {
|
if (example) {
|
||||||
preview_area.innerHTML = embed_html
|
preview_area.innerHTML = embed_html
|
||||||
@@ -41,15 +44,10 @@ const toggle_example = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const update_shared = () => {
|
let share_link = $derived(window.location.protocol+"//"+window.location.host+"/d/"+file.id)
|
||||||
// If sharing is enabled we automatically save the file so the user can copy
|
run(() => {
|
||||||
// the sharing link. But if the user disables sharing we don't automatically
|
embed_iframe(file, options)
|
||||||
// save so that the user can't accidentally discard a sharing link that's in
|
});
|
||||||
// use
|
|
||||||
if (options.shared && !file.id) {
|
|
||||||
dispatch("save")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
@@ -64,34 +62,14 @@ const update_shared = () => {
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
form="edit_form"
|
|
||||||
bind:checked={options.shared}
|
|
||||||
on:change={update_shared}
|
|
||||||
id="shared"
|
|
||||||
type="checkbox"
|
|
||||||
class="form_input"
|
|
||||||
/>
|
|
||||||
<label for="shared">Share this file or directory</label>
|
|
||||||
</div>
|
|
||||||
<div class="link_grid">
|
<div class="link_grid">
|
||||||
{#if options.shared}
|
<a href={share_link}>{share_link}</a>
|
||||||
<span>Public link: <a href={share_link}>{share_link}</a></span>
|
|
||||||
<CopyButton text={share_link}>Copy</CopyButton>
|
<CopyButton text={share_link}>Copy</CopyButton>
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>
|
|
||||||
When a file or directory is shared it can be accessed through a
|
|
||||||
unique link. You can get the URL with the 'Copy link' button on
|
|
||||||
the toolbar, or share the link with the 'Share' button. If you
|
|
||||||
share a directory all the files within the directory are also
|
|
||||||
accessible from the link.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
<AccessControl options={options}/>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Embedding</legend>
|
<legend>Embedding</legend>
|
||||||
<p>
|
<p>
|
||||||
@@ -108,7 +86,7 @@ const update_shared = () => {
|
|||||||
<textarea bind:value={embed_html} style="width: 100%; height: 4em;"></textarea>
|
<textarea bind:value={embed_html} style="width: 100%; height: 4em;"></textarea>
|
||||||
<br/>
|
<br/>
|
||||||
<CopyButton text={embed_html}>Copy HTML</CopyButton>
|
<CopyButton text={embed_html}>Copy HTML</CopyButton>
|
||||||
<button on:click={toggle_example} class:button_highlight={example} disabled={!options.shared}>
|
<button onclick={toggle_example} class:button_highlight={example} disabled={!file.is_shared()}>
|
||||||
<i class="icon">visibility</i> Show example
|
<i class="icon">visibility</i> Show example
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,9 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { FSNodeProperties } from "filesystem/FilesystemAPI";
|
import type { FSNodeProperties } from "lib/FilesystemAPI.svelte";
|
||||||
|
|
||||||
export let properties: FSNodeProperties = {} as FSNodeProperties
|
let {
|
||||||
|
properties = $bindable({} as FSNodeProperties)
|
||||||
|
}: {
|
||||||
|
properties?: FSNodeProperties;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
let current_theme = -1
|
let current_theme = $state(-1)
|
||||||
|
|
||||||
const set_theme = (index: number) => {
|
const set_theme = (index: number) => {
|
||||||
current_theme = index
|
current_theme = index
|
||||||
@@ -71,7 +75,7 @@ const themes = [
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#each themes as theme, index (theme.name)}
|
{#each themes as theme, index (theme.name)}
|
||||||
<button class:button_highlight={current_theme === index} on:click={() => {set_theme(index)}}>
|
<button class:button_highlight={current_theme === index} onclick={() => {set_theme(index)}}>
|
||||||
{theme.name}
|
{theme.name}
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
|
@@ -1,23 +1,29 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher } from "svelte";
|
import { fs_encode_path, fs_node_icon } from "lib/FilesystemAPI.svelte"
|
||||||
import { fs_encode_path, fs_node_icon, node_is_shared } from "filesystem/FilesystemAPI"
|
|
||||||
import type { FSNavigator } from "filesystem/FSNavigator";
|
import type { FSNavigator } from "filesystem/FSNavigator";
|
||||||
import { FileAction } from "./FileManagerLib";
|
import { FileAction, type FileActionHandler } from "./FileManagerLib";
|
||||||
|
|
||||||
let dispatch = createEventDispatcher()
|
let {
|
||||||
|
nav,
|
||||||
export let nav: FSNavigator
|
file_event,
|
||||||
export let show_hidden = false
|
show_hidden = false,
|
||||||
export let large_icons = false
|
large_icons = false,
|
||||||
export let hide_edit = false
|
hide_edit = false
|
||||||
|
}: {
|
||||||
|
nav: FSNavigator;
|
||||||
|
file_event: FileActionHandler
|
||||||
|
show_hidden?: boolean
|
||||||
|
large_icons?: boolean
|
||||||
|
hide_edit?: boolean
|
||||||
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="directory">
|
<div class="directory">
|
||||||
{#each $nav.children as child, index (child.path)}
|
{#each $nav.children as child, index (child.path)}
|
||||||
<a
|
<a
|
||||||
href={"/d"+fs_encode_path(child.path)}
|
href={"/d"+fs_encode_path(child.path)}
|
||||||
on:click={e => dispatch("file", {index: index, action: FileAction.Click, original: e})}
|
onclick={e => file_event(FileAction.Click, index, e)}
|
||||||
on:contextmenu={e => dispatch("file", {index: index, action: FileAction.Context, original: e})}
|
oncontextmenu={e => file_event(FileAction.Context, index, e)}
|
||||||
class="node"
|
class="node"
|
||||||
class:node_selected={child.fm_selected}
|
class:node_selected={child.fm_selected}
|
||||||
class:hidden={child.name.startsWith(".") && !show_hidden}
|
class:hidden={child.name.startsWith(".") && !show_hidden}
|
||||||
@@ -26,31 +32,19 @@ export let hide_edit = false
|
|||||||
<div class="node_name">
|
<div class="node_name">
|
||||||
{child.name}
|
{child.name}
|
||||||
</div>
|
</div>
|
||||||
{#if node_is_shared(child)}
|
|
||||||
<a
|
{#if child.is_shared()}
|
||||||
href="/d/{child.id}"
|
|
||||||
on:click={e => dispatch("file", {index: index, action: FileAction.Share, original: e})}
|
|
||||||
class="button flat action_button"
|
|
||||||
>
|
|
||||||
<i class="icon" title="This file / directory is shared. Click to open public link">share</i>
|
<i class="icon" title="This file / directory is shared. Click to open public link">share</i>
|
||||||
</a>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $nav.permissions.write && !hide_edit}
|
{#if !hide_edit}
|
||||||
<button
|
<button
|
||||||
class="action_button flat"
|
class="action_button flat"
|
||||||
on:click={e => dispatch("file", {index: index, action: FileAction.Edit, original: e})}
|
onclick={e => file_event(FileAction.Menu, index, e)}
|
||||||
>
|
>
|
||||||
<i class="icon">edit</i>
|
<i class="icon">menu</i>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<button
|
|
||||||
class="action_button flat"
|
|
||||||
on:click={e => dispatch("file", {index: index, action: FileAction.Download, original: e})}
|
|
||||||
>
|
|
||||||
<i class="icon">save</i>
|
|
||||||
</button>
|
|
||||||
</a>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@@ -71,9 +65,9 @@ export let hide_edit = false
|
|||||||
color: var(--body_text-color);
|
color: var(--body_text-color);
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: var(--input_background);
|
background: var(--body_background);
|
||||||
|
/* backdrop-filter: blur(4px); */
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
box-shadow: 1px 1px 8px 0px var(--shadow_color);
|
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
.node:hover:not(.node_selected) {
|
.node:hover:not(.node_selected) {
|
||||||
@@ -92,7 +86,6 @@ export let hide_edit = false
|
|||||||
height: 2em;
|
height: 2em;
|
||||||
width: 2em;
|
width: 2em;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
border-radius: 4px;
|
|
||||||
}
|
}
|
||||||
.node_name {
|
.node_name {
|
||||||
flex: 1 1 content;
|
flex: 1 1 content;
|
||||||
|
@@ -1,20 +1,22 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { preventDefault } from 'svelte/legacy';
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { fs_mkdir } from "filesystem/FilesystemAPI";
|
import { fs_mkdir } from "lib/FilesystemAPI.svelte";
|
||||||
import Button from "layout/Button.svelte";
|
import Button from "layout/Button.svelte";
|
||||||
import type { FSNavigator } from "filesystem/FSNavigator";
|
import type { FSNavigator } from "filesystem/FSNavigator";
|
||||||
|
import { loading_finish, loading_start } from "lib/Loading";
|
||||||
|
|
||||||
export let nav: FSNavigator
|
let { nav }: { nav: FSNavigator } = $props();
|
||||||
|
|
||||||
let name_input: HTMLInputElement;
|
let name_input: HTMLInputElement = $state();
|
||||||
let new_dir_name = ""
|
let new_dir_name = $state("")
|
||||||
let error_msg = ""
|
let error_msg = $state("")
|
||||||
let create_dir = async () => {
|
let create_dir = async () => {
|
||||||
let form = new FormData()
|
let form = new FormData()
|
||||||
form.append("type", "dir")
|
form.append("type", "dir")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
nav.set_loading(true)
|
loading_start()
|
||||||
await fs_mkdir(nav.base.path+"/"+new_dir_name)
|
await fs_mkdir(nav.base.path+"/"+new_dir_name)
|
||||||
new_dir_name = "" // Clear input field
|
new_dir_name = "" // Clear input field
|
||||||
error_msg = "" // Clear error msg
|
error_msg = "" // Clear error msg
|
||||||
@@ -26,7 +28,7 @@ let create_dir = async () => {
|
|||||||
error_msg = "Server returned an error: code: '"+err.value+"' message: "+err.message
|
error_msg = "Server returned an error: code: '"+err.value+"' message: "+err.message
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
nav.set_loading(false)
|
loading_finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +43,7 @@ onMount(() => {
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<form id="create_dir_form" class="create_dir" on:submit|preventDefault={create_dir}>
|
<form id="create_dir_form" class="create_dir" onsubmit={preventDefault(create_dir)}>
|
||||||
<img src="/res/img/mime/folder.png" class="icon" alt="icon"/>
|
<img src="/res/img/mime/folder.png" class="icon" alt="icon"/>
|
||||||
<input class="dirname" type="text" bind:this={name_input} bind:value={new_dir_name} />
|
<input class="dirname" type="text" bind:this={name_input} bind:value={new_dir_name} />
|
||||||
<Button form="create_dir_form" type="submit" icon="create_new_folder" label="Create"/>
|
<Button form="create_dir_form" type="submit" icon="create_new_folder" label="Create"/>
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { fs_delete_all, fs_download, fs_rename, type FSNode } from "filesystem/FilesystemAPI"
|
import { run } from 'svelte/legacy';
|
||||||
|
import { fs_delete_all, fs_rename, type FSNode } from "lib/FilesystemAPI.svelte"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import CreateDirectory from "./CreateDirectory.svelte"
|
import CreateDirectory from "./CreateDirectory.svelte"
|
||||||
import ListView from "./ListView.svelte"
|
import ListView from "./ListView.svelte"
|
||||||
@@ -12,30 +13,42 @@ import SearchBar from "./SearchBar.svelte";
|
|||||||
import type { FSNavigator } from "filesystem/FSNavigator";
|
import type { FSNavigator } from "filesystem/FSNavigator";
|
||||||
import FsUploadWidget from "filesystem/upload_widget/FSUploadWidget.svelte";
|
import FsUploadWidget from "filesystem/upload_widget/FSUploadWidget.svelte";
|
||||||
import EditWindow from "filesystem/edit_window/EditWindow.svelte";
|
import EditWindow from "filesystem/edit_window/EditWindow.svelte";
|
||||||
import { FileAction, type FileEvent } from "./FileManagerLib";
|
import { FileAction, type FileActionHandler } from "./FileManagerLib";
|
||||||
|
import FileMenu from "./FileMenu.svelte";
|
||||||
|
import { loading_finish, loading_start } from "lib/Loading";
|
||||||
|
|
||||||
export let nav: FSNavigator
|
let {
|
||||||
export let upload_widget: FsUploadWidget
|
nav = $bindable(),
|
||||||
export let edit_window: EditWindow
|
upload_widget,
|
||||||
export let directory_view = ""
|
edit_window = $bindable(),
|
||||||
let large_icons = false
|
directory_view = $bindable(""),
|
||||||
|
children
|
||||||
|
}: {
|
||||||
|
nav: FSNavigator;
|
||||||
|
upload_widget: FsUploadWidget;
|
||||||
|
edit_window: EditWindow;
|
||||||
|
directory_view?: string;
|
||||||
|
children?: import('svelte').Snippet;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let large_icons = $state(false)
|
||||||
let uploader: FsUploadWidget
|
let uploader: FsUploadWidget
|
||||||
let mode = "viewing"
|
let mode = $state("viewing")
|
||||||
let creating_dir = false
|
let creating_dir = $state(false)
|
||||||
let show_hidden = false
|
let show_hidden = $state(false)
|
||||||
|
let file_menu: FileMenu = $state()
|
||||||
|
|
||||||
export const upload = (files: File[]) => {
|
export const upload = (files: File[]) => {
|
||||||
return uploader.upload(files)
|
return uploader.upload_files(files)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigation functions
|
// Navigation functions
|
||||||
const file_event = (e: CustomEvent<FileEvent>) => {
|
const file_event: FileActionHandler = (action: FileAction, index: number, orig: Event) => {
|
||||||
const index = e.detail.index
|
orig.preventDefault()
|
||||||
|
orig.stopPropagation()
|
||||||
|
|
||||||
switch (e.detail.action) {
|
switch (action) {
|
||||||
case FileAction.Click:
|
case FileAction.Click:
|
||||||
e.detail.original.preventDefault()
|
|
||||||
e.detail.original.stopPropagation()
|
|
||||||
creating_dir = false
|
creating_dir = false
|
||||||
|
|
||||||
if (mode === "viewing") {
|
if (mode === "viewing") {
|
||||||
@@ -54,35 +67,26 @@ const file_event = (e: CustomEvent<FileEvent>) => {
|
|||||||
case FileAction.Context:
|
case FileAction.Context:
|
||||||
// If this is a touch event we will select the item
|
// If this is a touch event we will select the item
|
||||||
if (navigator.maxTouchPoints && navigator.maxTouchPoints > 0) {
|
if (navigator.maxTouchPoints && navigator.maxTouchPoints > 0) {
|
||||||
e.detail.original.preventDefault()
|
|
||||||
select_node(index)
|
select_node(index)
|
||||||
|
} else {
|
||||||
|
file_menu.open(nav.children[index], orig.target)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case FileAction.Edit:
|
case FileAction.Edit:
|
||||||
e.detail.original.preventDefault()
|
|
||||||
e.detail.original.stopPropagation()
|
|
||||||
edit_window.edit(nav.children[index], false, "file")
|
edit_window.edit(nav.children[index], false, "file")
|
||||||
break
|
break
|
||||||
case FileAction.Share:
|
case FileAction.Share:
|
||||||
e.detail.original.preventDefault()
|
|
||||||
e.detail.original.stopPropagation()
|
|
||||||
creating_dir = false
|
creating_dir = false
|
||||||
edit_window.edit(nav.children[index], false, "share")
|
edit_window.edit(nav.children[index], false, "share")
|
||||||
break
|
break
|
||||||
case FileAction.Branding:
|
case FileAction.Branding:
|
||||||
e.detail.original.preventDefault()
|
|
||||||
e.detail.original.stopPropagation()
|
|
||||||
edit_window.edit(nav.children[index], false, "branding")
|
edit_window.edit(nav.children[index], false, "branding")
|
||||||
break
|
break
|
||||||
case FileAction.Select:
|
case FileAction.Select:
|
||||||
e.detail.original.preventDefault()
|
|
||||||
e.detail.original.stopPropagation()
|
|
||||||
select_node(index)
|
select_node(index)
|
||||||
break
|
break
|
||||||
case FileAction.Download:
|
case FileAction.Menu:
|
||||||
e.detail.original.preventDefault()
|
file_menu.open(nav.children[index], orig.target)
|
||||||
e.detail.original.stopPropagation()
|
|
||||||
fs_download(nav.children[index])
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,9 +114,9 @@ const delete_selected = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
nav.set_loading(true)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
loading_start()
|
||||||
|
|
||||||
// Save all promises with deletion requests in an array
|
// Save all promises with deletion requests in an array
|
||||||
let promises = []
|
let promises = []
|
||||||
nav.children.forEach(child => {
|
nav.children.forEach(child => {
|
||||||
@@ -129,7 +133,7 @@ const delete_selected = async () => {
|
|||||||
alert("Delete failed: " + err.message + " ("+err.value+")")
|
alert("Delete failed: " + err.message + " ("+err.value+")")
|
||||||
} finally {
|
} finally {
|
||||||
viewing_mode()
|
viewing_mode()
|
||||||
nav.reload()
|
loading_finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,10 +224,6 @@ const select_node = (index: number) => {
|
|||||||
last_selected_node = index
|
last_selected_node = index
|
||||||
}
|
}
|
||||||
|
|
||||||
// When the directory is reloaded we want to keep our selection, so this
|
|
||||||
// function watches the children array for changes and updates the selection
|
|
||||||
// when it changes
|
|
||||||
$: update($nav.children)
|
|
||||||
const update = (children: FSNode[]) => {
|
const update = (children: FSNode[]) => {
|
||||||
creating_dir = false
|
creating_dir = false
|
||||||
|
|
||||||
@@ -237,8 +237,8 @@ const update = (children: FSNode[]) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let moving_files = 0
|
let moving_files = $state(0)
|
||||||
let moving_directories = 0
|
let moving_directories = $state(0)
|
||||||
const move_start = () => {
|
const move_start = () => {
|
||||||
moving_files = 0
|
moving_files = 0
|
||||||
moving_directories = 0
|
moving_directories = 0
|
||||||
@@ -257,11 +257,10 @@ const move_start = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const move_here = async () => {
|
const move_here = async () => {
|
||||||
nav.set_loading(true)
|
const target_dir = nav.base.path + "/"
|
||||||
|
|
||||||
let target_dir = nav.base.path + "/"
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
loading_start()
|
||||||
|
|
||||||
let promises = []
|
let promises = []
|
||||||
moving_items.forEach(item => {
|
moving_items.forEach(item => {
|
||||||
console.log("moving", item.path, "to", target_dir + item.name)
|
console.log("moving", item.path, "to", target_dir + item.name)
|
||||||
@@ -276,6 +275,7 @@ const move_here = async () => {
|
|||||||
} finally {
|
} finally {
|
||||||
viewing_mode()
|
viewing_mode()
|
||||||
nav.reload()
|
nav.reload()
|
||||||
|
loading_finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,9 +288,15 @@ onMount(() => {
|
|||||||
directory_view = "list"
|
directory_view = "list"
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
// When the directory is reloaded we want to keep our selection, so this
|
||||||
|
// function watches the children array for changes and updates the selection
|
||||||
|
// when it changes
|
||||||
|
run(() => {
|
||||||
|
update($nav.children)
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:keydown={keypress} on:keyup={keypress} />
|
<svelte:window onkeydown={keypress} onkeyup={keypress} />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="container"
|
class="container"
|
||||||
@@ -303,25 +309,25 @@ onMount(() => {
|
|||||||
{#if mode === "viewing"}
|
{#if mode === "viewing"}
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<div class="toolbar_left">
|
<div class="toolbar_left">
|
||||||
<button on:click={navigate_back} title="Back">
|
<button onclick={navigate_back} title="Back">
|
||||||
<i class="icon">arrow_back</i>
|
<i class="icon">arrow_back</i>
|
||||||
</button>
|
</button>
|
||||||
<button on:click={() => nav.navigate_up()} disabled={$nav.path.length <= 1} title="Up">
|
<button onclick={() => nav.navigate_up()} disabled={$nav.path.length <= 1} title="Up">
|
||||||
<i class="icon">north</i>
|
<i class="icon">north</i>
|
||||||
</button>
|
</button>
|
||||||
<button on:click={() => nav.reload()} title="Refresh directory listing">
|
<button onclick={() => nav.reload()} title="Refresh directory listing">
|
||||||
<i class="icon">refresh</i>
|
<i class="icon">refresh</i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="toolbar_middle">
|
<div class="toolbar_middle">
|
||||||
<button on:click={() => toggle_view()} title="Switch between gallery, list and compact view">
|
<button onclick={() => toggle_view()} title="Switch between gallery, list and compact view">
|
||||||
<i class="icon" class:button_highlight={directory_view === "list"}>list</i>
|
<i class="icon" class:button_highlight={directory_view === "list"}>list</i>
|
||||||
<i class="icon" class:button_highlight={directory_view === "gallery"}>collections</i>
|
<i class="icon" class:button_highlight={directory_view === "gallery"}>collections</i>
|
||||||
<i class="icon" class:button_highlight={directory_view === "compact"}>view_compact</i>
|
<i class="icon" class:button_highlight={directory_view === "compact"}>view_compact</i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="button_large_icons" on:click={() => toggle_large_icons()} title="Switch between large and small icons">
|
<button class="button_large_icons" onclick={() => toggle_large_icons()} title="Switch between large and small icons">
|
||||||
{#if large_icons}
|
{#if large_icons}
|
||||||
<i class="icon">zoom_out</i>
|
<i class="icon">zoom_out</i>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -329,7 +335,7 @@ onMount(() => {
|
|||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button on:click={() => {show_hidden = !show_hidden}} title="Toggle hidden files">
|
<button onclick={() => {show_hidden = !show_hidden}} title="Toggle hidden files">
|
||||||
{#if show_hidden}
|
{#if show_hidden}
|
||||||
<i class="icon">visibility_off</i>
|
<i class="icon">visibility_off</i>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -340,13 +346,13 @@ onMount(() => {
|
|||||||
|
|
||||||
<div class="toolbar_right">
|
<div class="toolbar_right">
|
||||||
{#if $nav.permissions.write}
|
{#if $nav.permissions.write}
|
||||||
<button on:click={() => upload_widget.pick_files()} title="Upload files to this directory">
|
<button onclick={() => upload_widget.pick_files()} title="Upload files to this directory">
|
||||||
<i class="icon">cloud_upload</i>
|
<i class="icon">cloud_upload</i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Button click={() => {creating_dir = !creating_dir}} highlight={creating_dir} icon="create_new_folder" title="Make folder"/>
|
<Button click={() => {creating_dir = !creating_dir}} highlight={creating_dir} icon="create_new_folder" title="Make folder"/>
|
||||||
|
|
||||||
<button on:click={selecting_mode} title="Select and delete files">
|
<button onclick={selecting_mode} title="Select and delete files">
|
||||||
<i class="icon">select_all</i>
|
<i class="icon">select_all</i>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -360,7 +366,7 @@ onMount(() => {
|
|||||||
<Button click={viewing_mode} icon="close"/>
|
<Button click={viewing_mode} icon="close"/>
|
||||||
<div class="toolbar_spacer">Selecting files</div>
|
<div class="toolbar_spacer">Selecting files</div>
|
||||||
<Button click={move_start} icon="drive_file_move" label="Move"/>
|
<Button click={move_start} icon="drive_file_move" label="Move"/>
|
||||||
<button on:click={delete_selected} class="button_red">
|
<button onclick={delete_selected} class="button_red">
|
||||||
<i class="icon">delete</i>
|
<i class="icon">delete</i>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
@@ -399,24 +405,25 @@ onMount(() => {
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<slot></slot>
|
{@render children?.()}
|
||||||
|
|
||||||
{#if directory_view === "list"}
|
{#if directory_view === "list"}
|
||||||
<ListView nav={nav} show_hidden={show_hidden} large_icons={large_icons} on:file={file_event} />
|
<ListView nav={nav} file_event={file_event} show_hidden={show_hidden} large_icons={large_icons}/>
|
||||||
{:else if directory_view === "gallery"}
|
{:else if directory_view === "gallery"}
|
||||||
<GalleryView nav={nav} show_hidden={show_hidden} large_icons={large_icons} on:file={file_event} />
|
<GalleryView nav={nav} file_event={file_event} show_hidden={show_hidden} large_icons={large_icons}/>
|
||||||
{:else if directory_view === "compact"}
|
{:else if directory_view === "compact"}
|
||||||
<CompactView nav={nav} show_hidden={show_hidden} large_icons={large_icons} on:file={file_event} />
|
<CompactView nav={nav} file_event={file_event} show_hidden={show_hidden} large_icons={large_icons}/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<FileMenu bind:this={file_menu} nav={nav} edit_window={edit_window} />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.container {
|
.container {
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
display: block;
|
display: block;
|
||||||
|
height: 100%; /* Used for drop target */
|
||||||
}
|
}
|
||||||
.width_container {
|
.width_container {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
@@ -425,8 +432,8 @@ onMount(() => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: var(--shaded_background);
|
background: var(--body_background);
|
||||||
backdrop-filter: blur(4px);
|
/* backdrop-filter: blur(4px); */
|
||||||
}
|
}
|
||||||
.toolbar {
|
.toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@@ -3,4 +3,18 @@ export type FileEvent = {
|
|||||||
action: FileAction,
|
action: FileAction,
|
||||||
original: MouseEvent,
|
original: MouseEvent,
|
||||||
}
|
}
|
||||||
export enum FileAction { Click, Context, Edit, Share, Branding, Select, Download }
|
export enum FileAction {
|
||||||
|
Click,
|
||||||
|
Context,
|
||||||
|
Edit,
|
||||||
|
Share,
|
||||||
|
Branding,
|
||||||
|
Select,
|
||||||
|
Download,
|
||||||
|
Menu,
|
||||||
|
}
|
||||||
|
export type FileActionHandler = (
|
||||||
|
action: FileAction,
|
||||||
|
file_index: number,
|
||||||
|
original_event: Event,
|
||||||
|
) => void
|
||||||
|
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">
|
<script lang="ts">
|
||||||
import { createEventDispatcher, onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import ListView from "./ListView.svelte"
|
import ListView from "./ListView.svelte"
|
||||||
import GalleryView from "./GalleryView.svelte"
|
import GalleryView from "./GalleryView.svelte"
|
||||||
import CompactView from "./CompactView.svelte"
|
import CompactView from "./CompactView.svelte"
|
||||||
import Modal from "util/Modal.svelte";
|
import Modal from "util/Modal.svelte";
|
||||||
import LoadingIndicator from "util/LoadingIndicator.svelte";
|
|
||||||
import Breadcrumbs from "filesystem/Breadcrumbs.svelte"
|
import Breadcrumbs from "filesystem/Breadcrumbs.svelte"
|
||||||
import { FSNavigator } from "filesystem/FSNavigator";
|
import { FSNavigator } from "filesystem/FSNavigator";
|
||||||
import type { FSNode } from "filesystem/FilesystemAPI";
|
import type { FSNode } from "lib/FilesystemAPI.svelte";
|
||||||
import { FileAction, type FileEvent } from "./FileManagerLib";
|
import { FileAction, type FileActionHandler } from "./FileManagerLib";
|
||||||
|
|
||||||
let nav = new FSNavigator(false)
|
let nav = $state(new FSNavigator(false))
|
||||||
let modal: Modal
|
let modal: Modal = $state()
|
||||||
let dispatch = createEventDispatcher()
|
let directory_view = $state("")
|
||||||
let directory_view = ""
|
let large_icons = $state(false)
|
||||||
let loading = false
|
let show_hidden = $state(false)
|
||||||
let large_icons = false
|
|
||||||
let show_hidden = false
|
let {
|
||||||
export let select_multiple = false
|
callback,
|
||||||
|
select_multiple = false
|
||||||
|
}: {
|
||||||
|
callback: (files: FSNode[]) => void
|
||||||
|
select_multiple?: boolean
|
||||||
|
} = $props();
|
||||||
|
|
||||||
export const open = (path: string) => {
|
export const open = (path: string) => {
|
||||||
modal.show()
|
modal.show()
|
||||||
nav.navigate(path, false)
|
nav.navigate(path, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
$: selected_files = $nav.children.reduce((acc, file) => {
|
let selected_files = $derived($nav.children.reduce((acc, file) => {
|
||||||
if (file.fm_selected) {
|
if (file.fm_selected) {
|
||||||
acc++
|
acc++
|
||||||
}
|
}
|
||||||
return acc
|
return acc
|
||||||
}, 0)
|
}, 0))
|
||||||
|
|
||||||
// Navigation functions
|
// Navigation functions
|
||||||
|
|
||||||
const file_event = (e: CustomEvent<FileEvent>) => {
|
const file_event: FileActionHandler = (action: FileAction, index: number, orig: Event) => {
|
||||||
const index = e.detail.index
|
switch (action) {
|
||||||
|
|
||||||
switch (e.detail.action) {
|
|
||||||
case FileAction.Click:
|
case FileAction.Click:
|
||||||
e.detail.original.preventDefault()
|
orig.preventDefault()
|
||||||
if (nav.children[index].type === "dir") {
|
if (nav.children[index].type === "dir") {
|
||||||
nav.navigate(nav.children[index].path, true)
|
nav.navigate(nav.children[index].path, true)
|
||||||
} else {
|
} else {
|
||||||
@@ -48,12 +50,12 @@ const file_event = (e: CustomEvent<FileEvent>) => {
|
|||||||
case FileAction.Context:
|
case FileAction.Context:
|
||||||
// If this is a touch event we will select the item
|
// If this is a touch event we will select the item
|
||||||
if (navigator.maxTouchPoints && navigator.maxTouchPoints > 0) {
|
if (navigator.maxTouchPoints && navigator.maxTouchPoints > 0) {
|
||||||
e.detail.original.preventDefault()
|
orig.preventDefault()
|
||||||
select_node(index)
|
select_node(index)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case FileAction.Select:
|
case FileAction.Select:
|
||||||
e.detail.original.preventDefault()
|
orig.preventDefault()
|
||||||
nav.children[index].fm_selected = !nav.children[index].fm_selected
|
nav.children[index].fm_selected = !nav.children[index].fm_selected
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -114,7 +116,7 @@ let done = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (selected_files.length > 0) {
|
if (selected_files.length > 0) {
|
||||||
dispatch("files", selected_files)
|
callback(selected_files)
|
||||||
}
|
}
|
||||||
modal.hide()
|
modal.hide()
|
||||||
}
|
}
|
||||||
@@ -130,17 +132,18 @@ onMount(() => {
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:keydown={detect_shift} on:keyup={detect_shift} />
|
<svelte:window onkeydown={detect_shift} onkeyup={detect_shift} />
|
||||||
|
|
||||||
<Modal bind:this={modal} width="900px">
|
<Modal bind:this={modal} width="900px">
|
||||||
<div class="header" slot="title">
|
{#snippet header()}
|
||||||
<button class="button round" on:click={modal.hide}>
|
<div class="header" >
|
||||||
|
<button class="button round" onclick={modal.hide}>
|
||||||
<i class="icon">close</i>
|
<i class="icon">close</i>
|
||||||
</button>
|
</button>
|
||||||
<button on:click={() => nav.navigate_up()} disabled={$nav.path.length <= 1} title="Up">
|
<button onclick={() => nav.navigate_up()} disabled={$nav.path.length <= 1} title="Up">
|
||||||
<i class="icon">north</i>
|
<i class="icon">north</i>
|
||||||
</button>
|
</button>
|
||||||
<button on:click={() => nav.reload()} title="Refresh directory listing">
|
<button onclick={() => nav.reload()} title="Refresh directory listing">
|
||||||
<i class="icon">refresh</i>
|
<i class="icon">refresh</i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -148,7 +151,7 @@ onMount(() => {
|
|||||||
Selected {selected_files} files
|
Selected {selected_files} files
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button on:click={() => {show_hidden = !show_hidden}} title="Toggle hidden files">
|
<button onclick={() => {show_hidden = !show_hidden}} title="Toggle hidden files">
|
||||||
{#if show_hidden}
|
{#if show_hidden}
|
||||||
<i class="icon">visibility_off</i>
|
<i class="icon">visibility_off</i>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -156,46 +159,45 @@ onMount(() => {
|
|||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button on:click={() => toggle_view()} title="Switch between gallery, list and compact view">
|
<button onclick={() => toggle_view()} title="Switch between gallery, list and compact view">
|
||||||
<i class="icon" class:button_highlight={directory_view === "list"}>list</i>
|
<i class="icon" class:button_highlight={directory_view === "list"}>list</i>
|
||||||
<i class="icon" class:button_highlight={directory_view === "gallery"}>collections</i>
|
<i class="icon" class:button_highlight={directory_view === "gallery"}>collections</i>
|
||||||
<i class="icon" class:button_highlight={directory_view === "compact"}>view_compact</i>
|
<i class="icon" class:button_highlight={directory_view === "compact"}>view_compact</i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="button button_highlight round" on:click={done}>
|
<button class="button button_highlight round" onclick={done}>
|
||||||
<i class="icon">done</i> Pick
|
<i class="icon">done</i> Pick
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
<Breadcrumbs nav={nav}/>
|
<Breadcrumbs nav={nav}/>
|
||||||
|
|
||||||
{#if directory_view === "list"}
|
{#if directory_view === "list"}
|
||||||
<ListView
|
<ListView
|
||||||
nav={nav}
|
nav={nav}
|
||||||
|
file_event={file_event}
|
||||||
show_hidden={show_hidden}
|
show_hidden={show_hidden}
|
||||||
large_icons={large_icons}
|
large_icons={large_icons}
|
||||||
hide_edit
|
hide_edit
|
||||||
hide_branding
|
hide_branding
|
||||||
on:file={file_event}
|
|
||||||
/>
|
/>
|
||||||
{:else if directory_view === "gallery"}
|
{:else if directory_view === "gallery"}
|
||||||
<GalleryView
|
<GalleryView
|
||||||
nav={nav}
|
nav={nav}
|
||||||
|
file_event={file_event}
|
||||||
show_hidden={show_hidden}
|
show_hidden={show_hidden}
|
||||||
large_icons={large_icons}
|
large_icons={large_icons}
|
||||||
on:file={file_event}
|
|
||||||
/>
|
/>
|
||||||
{:else if directory_view === "compact"}
|
{:else if directory_view === "compact"}
|
||||||
<CompactView
|
<CompactView
|
||||||
nav={nav}
|
nav={nav}
|
||||||
|
file_event={file_event}
|
||||||
show_hidden={show_hidden}
|
show_hidden={show_hidden}
|
||||||
large_icons={large_icons}
|
large_icons={large_icons}
|
||||||
hide_edit
|
hide_edit
|
||||||
on:file={file_event}
|
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<LoadingIndicator loading={loading}/>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@@ -1,21 +1,27 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher } from "svelte"
|
import { fs_node_icon, fs_node_type, fs_encode_path } from "lib/FilesystemAPI.svelte";
|
||||||
import { fs_node_icon, fs_node_type, fs_encode_path } from "filesystem/FilesystemAPI";
|
|
||||||
import type { FSNavigator } from "filesystem/FSNavigator";
|
import type { FSNavigator } from "filesystem/FSNavigator";
|
||||||
import { FileAction } from "./FileManagerLib";
|
import { FileAction, type FileActionHandler } from "./FileManagerLib";
|
||||||
let dispatch = createEventDispatcher()
|
|
||||||
|
|
||||||
export let nav: FSNavigator
|
let {
|
||||||
export let show_hidden = false
|
nav,
|
||||||
export let large_icons = false
|
file_event,
|
||||||
|
show_hidden = false,
|
||||||
|
large_icons = false
|
||||||
|
}: {
|
||||||
|
nav: FSNavigator
|
||||||
|
file_event: FileActionHandler
|
||||||
|
show_hidden?: boolean
|
||||||
|
large_icons?: boolean
|
||||||
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="gallery">
|
<div class="gallery">
|
||||||
{#each $nav.children as child, index (child.path)}
|
{#each $nav.children as child, index (child.path)}
|
||||||
<a class="file"
|
<a class="file"
|
||||||
href={"/d"+fs_encode_path(child.path)}
|
href={"/d"+fs_encode_path(child.path)}
|
||||||
on:click={e => dispatch("file", {index: index, action: FileAction.Click, original: e})}
|
onclick={e => file_event(FileAction.Click, index, e)}
|
||||||
on:contextmenu={e => dispatch("file", {index: index, action: FileAction.Context, original: e})}
|
oncontextmenu={e => file_event(FileAction.Context, index, e)}
|
||||||
class:selected={child.fm_selected}
|
class:selected={child.fm_selected}
|
||||||
class:hidden={child.name.startsWith(".") && !show_hidden}
|
class:hidden={child.name.startsWith(".") && !show_hidden}
|
||||||
class:large_icons
|
class:large_icons
|
||||||
@@ -46,15 +52,15 @@ export let large_icons = false
|
|||||||
width: 150px;
|
width: 150px;
|
||||||
height: 150px;
|
height: 150px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 8px;
|
background: var(--body_background);
|
||||||
background: var(--input_background);
|
/* backdrop-filter: blur(4px); */
|
||||||
|
border-radius: 4px;
|
||||||
color: var(--input_text);
|
color: var(--input_text);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
transition: background 0.2s;
|
transition: background 0.2s;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
box-shadow: 1px 1px 0px 0px var(--shadow_color);
|
|
||||||
}
|
}
|
||||||
.file.large_icons {
|
.file.large_icons {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
@@ -87,7 +93,7 @@ export let large_icons = false
|
|||||||
}
|
}
|
||||||
.node_icon {
|
.node_icon {
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
border-radius: 6px;
|
border-radius: 4px;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-size: contain;
|
background-size: contain;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
|
@@ -1,32 +1,41 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher } from "svelte";
|
|
||||||
import { formatDataVolume } from "util/Formatting";
|
import { formatDataVolume } from "util/Formatting";
|
||||||
import { fs_encode_path, fs_node_icon, node_is_shared } from "filesystem/FilesystemAPI"
|
import { fs_encode_path, fs_node_icon } from "lib/FilesystemAPI.svelte"
|
||||||
import type { FSNavigator } from "filesystem/FSNavigator";
|
import type { FSNavigator } from "filesystem/FSNavigator";
|
||||||
import SortButton from "layout/SortButton.svelte";
|
import SortButton from "layout/SortButton.svelte";
|
||||||
import { FileAction } from "./FileManagerLib";
|
import { FileAction, type FileActionHandler } from "./FileManagerLib";
|
||||||
|
|
||||||
let dispatch = createEventDispatcher()
|
let {
|
||||||
|
nav,
|
||||||
export let nav: FSNavigator
|
file_event,
|
||||||
export let show_hidden = false
|
show_hidden = false,
|
||||||
export let large_icons = false
|
large_icons = false,
|
||||||
export let hide_edit = false
|
hide_edit = false,
|
||||||
export let hide_branding = false
|
hide_branding = false
|
||||||
|
}: {
|
||||||
|
nav: FSNavigator
|
||||||
|
file_event: FileActionHandler
|
||||||
|
show_hidden?: boolean
|
||||||
|
large_icons?: boolean
|
||||||
|
hide_edit?: boolean
|
||||||
|
hide_branding?: boolean
|
||||||
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="directory">
|
<table class="directory">
|
||||||
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td><SortButton active_field={$nav.sort_last_field} asc={$nav.sort_asc} sort_func={nav.sort_children} field="name">Name</SortButton></td>
|
<td><SortButton active_field={$nav.sort_last_field} asc={$nav.sort_asc} sort_func={nav.sort_children} field="name">Name</SortButton></td>
|
||||||
<td><SortButton active_field={$nav.sort_last_field} asc={$nav.sort_asc} sort_func={nav.sort_children} field="file_size">Size</SortButton></td>
|
<td class="hide_small"><SortButton active_field={$nav.sort_last_field} asc={$nav.sort_asc} sort_func={nav.sort_children} field="file_size">Size</SortButton></td>
|
||||||
<td></td>
|
<td></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
{#each $nav.children as child, index (child.path)}
|
{#each $nav.children as child, index (child.path)}
|
||||||
<a
|
<tr
|
||||||
href={"/d"+fs_encode_path(child.path)}
|
onclick={e => file_event(FileAction.Click, index, e)}
|
||||||
on:click={e => dispatch("file", {index: index, action: FileAction.Click, original: e})}
|
oncontextmenu={e => file_event(FileAction.Context, index, e)}
|
||||||
on:contextmenu={e => dispatch("file", {index: index, action: FileAction.Context, original: e})}
|
|
||||||
class="node"
|
class="node"
|
||||||
class:node_selected={child.fm_selected}
|
class:node_selected={child.fm_selected}
|
||||||
class:hidden={child.name.startsWith(".") && !show_hidden}
|
class:hidden={child.name.startsWith(".") && !show_hidden}
|
||||||
@@ -35,7 +44,9 @@ export let hide_branding = false
|
|||||||
<img src={fs_node_icon(child, 64, 64)} class="node_icon" class:large_icons alt="icon"/>
|
<img src={fs_node_icon(child, 64, 64)} class="node_icon" class:large_icons alt="icon"/>
|
||||||
</td>
|
</td>
|
||||||
<td class="node_name">
|
<td class="node_name">
|
||||||
|
<a class="title_link" href={"/d"+fs_encode_path(child.path)}>
|
||||||
{child.name}
|
{child.name}
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="node_size hide_small">
|
<td class="node_size hide_small">
|
||||||
{#if child.type === "file"}
|
{#if child.type === "file"}
|
||||||
@@ -46,44 +57,40 @@ export let hide_branding = false
|
|||||||
<div class="icons_wrap">
|
<div class="icons_wrap">
|
||||||
{#if child.abuse_type !== undefined}
|
{#if child.abuse_type !== undefined}
|
||||||
<i class="icon" title="This file / directory has received an abuse report. It cannot be shared">block</i>
|
<i class="icon" title="This file / directory has received an abuse report. It cannot be shared">block</i>
|
||||||
{:else if node_is_shared(child)}
|
{:else if child.is_shared()}
|
||||||
<a
|
<a
|
||||||
href="/d/{child.id}"
|
href="/d/{child.id}"
|
||||||
on:click={e => dispatch("file", {index: index, action: FileAction.Share, original: e})}
|
onclick={e => file_event(FileAction.Share, index, e)}
|
||||||
class="button action_button"
|
class="button action_button"
|
||||||
>
|
>
|
||||||
<i class="icon" title="This file / directory is shared. Click to open public link">share</i>
|
<i class="icon" title="This file / directory is shared. Click to open public link">share</i>
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
{#if child.properties !== undefined && child.properties.branding_enabled === "true" && !hide_branding}
|
{#if child.properties !== undefined && child.properties.branding_enabled === "true" && !hide_branding}
|
||||||
<button class="action_button" on:click={e => dispatch("file", {index: index, action: FileAction.Branding, original: e})}>
|
<button class="action_button" onclick={e => file_event(FileAction.Branding, index, e)}>
|
||||||
<i class="icon">palette</i>
|
<i class="icon">palette</i>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if $nav.permissions.write && !hide_edit}
|
{#if !hide_edit}
|
||||||
<button class="action_button" on:click={e => dispatch("file", {index: index, action: FileAction.Edit, original: e})}>
|
<button class="action_button" onclick={e => file_event(FileAction.Menu, index, e)}>
|
||||||
<i class="icon">edit</i>
|
<i class="icon">menu</i>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
<button class="action_button" on:click={e => dispatch("file", {index: index, action: FileAction.Download, original: e})}>
|
|
||||||
<i class="icon">save</i>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</a>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.directory {
|
.directory {
|
||||||
display: table;
|
display: table;
|
||||||
margin: 8px auto 16px auto;
|
background: var(--body_background);
|
||||||
background: var(--shaded_background);
|
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
border-radius: 8px;
|
/* backdrop-filter: blur(4px); */
|
||||||
|
max-width: 1200px;
|
||||||
max-width: 99%;
|
margin: auto; /* center */
|
||||||
width: 1200px;
|
|
||||||
}
|
}
|
||||||
.directory > * {
|
.directory > * {
|
||||||
display: table-row;
|
display: table-row;
|
||||||
@@ -91,11 +98,14 @@ export let hide_branding = false
|
|||||||
.directory > * > * {
|
.directory > * > * {
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
}
|
}
|
||||||
|
td {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
.node {
|
.node {
|
||||||
display: table-row;
|
display: table-row;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: var(--body_text-color);
|
color: var(--body_text-color);
|
||||||
padding: 6px;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.node:not(:last-child) {
|
.node:not(:last-child) {
|
||||||
border-bottom: 1px solid var(--separator);
|
border-bottom: 1px solid var(--separator);
|
||||||
@@ -110,13 +120,14 @@ export let hide_branding = false
|
|||||||
color: var(--highlight_text_color);
|
color: var(--highlight_text_color);
|
||||||
}
|
}
|
||||||
td {
|
td {
|
||||||
|
padding: 2px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
.node_icon {
|
.node_icon {
|
||||||
height: 32px;
|
height: 32px;
|
||||||
width: 32px;
|
width: 32px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
border-radius: 4px;
|
/* border-radius: 4px; */
|
||||||
margin: 2px;
|
margin: 2px;
|
||||||
}
|
}
|
||||||
.node_name {
|
.node_name {
|
||||||
@@ -124,6 +135,10 @@ td {
|
|||||||
line-height: 1.2em;
|
line-height: 1.2em;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
.title_link {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--body_text_color);
|
||||||
|
}
|
||||||
.node_size {
|
.node_size {
|
||||||
min-width: 5em;
|
min-width: 5em;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
@@ -1,18 +1,21 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { fs_search, fs_encode_path, fs_thumbnail_url } from "filesystem/FilesystemAPI";
|
import { fs_search, fs_encode_path, fs_thumbnail_url } from "lib/FilesystemAPI.svelte";
|
||||||
import type { FSNavigator } from "filesystem/FSNavigator";
|
import type { FSNavigator } from "filesystem/FSNavigator";
|
||||||
|
import { loading_finish, loading_start } from "lib/Loading";
|
||||||
|
|
||||||
export let nav: FSNavigator
|
let { nav }: {
|
||||||
|
nav: FSNavigator;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
let search_bar: HTMLInputElement
|
let search_bar: HTMLInputElement = $state()
|
||||||
let error = ""
|
let error = $state("")
|
||||||
let search_term = ""
|
let search_term = $state("")
|
||||||
let search_results: string[] = []
|
let search_results: string[] = $state([])
|
||||||
let selected_result = 0
|
let selected_result = $state(0)
|
||||||
let searching = false
|
let searching = false
|
||||||
let last_searched_term = ""
|
let last_searched_term = ""
|
||||||
let last_limit = 10
|
let last_limit = $state(10)
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// Clear results when the user moves to a new directory
|
// Clear results when the user moves to a new directory
|
||||||
@@ -43,9 +46,9 @@ const search = async (limit = 10) => {
|
|||||||
last_limit = limit
|
last_limit = limit
|
||||||
|
|
||||||
searching = true
|
searching = true
|
||||||
nav.set_loading(true)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
loading_start()
|
||||||
search_results = await fs_search(nav.base.path, search_term, limit)
|
search_results = await fs_search(nav.base.path, search_term, limit)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.value) {
|
if (err.value) {
|
||||||
@@ -54,6 +57,8 @@ const search = async (limit = 10) => {
|
|||||||
alert(err)
|
alert(err)
|
||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
loading_finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (search_results.length > 0 && selected_result > search_results.length-1) {
|
if (search_results.length > 0 && selected_result > search_results.length-1) {
|
||||||
@@ -61,7 +66,6 @@ const search = async (limit = 10) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
searching = false
|
searching = false
|
||||||
nav.set_loading(false)
|
|
||||||
|
|
||||||
// It's possible that the user entered another letter while we were
|
// It's possible that the user entered another letter while we were
|
||||||
// performing the search reqeust. If this happens we run the search function
|
// performing the search reqeust. If this happens we run the search function
|
||||||
@@ -112,13 +116,17 @@ const input_keyup = (e: KeyboardEvent) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Submitting opens the selected result
|
// Submitting opens the selected result
|
||||||
const submit_search = () => {
|
const submit_search = (e: Event) => {
|
||||||
|
e.preventDefault()
|
||||||
if (search_results.length !== 0) {
|
if (search_results.length !== 0) {
|
||||||
open_result(selected_result)
|
open_result(selected_result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const open_result = (index: number) => {
|
const open_result = (index: number, e?: MouseEvent) => {
|
||||||
|
if (e !== undefined) {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
nav.navigate(search_results[index], true)
|
nav.navigate(search_results[index], true)
|
||||||
clear_search(false)
|
clear_search(false)
|
||||||
}
|
}
|
||||||
@@ -142,7 +150,7 @@ const window_keydown = (e: KeyboardEvent) => {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:keydown={window_keydown} />
|
<svelte:window onkeydown={window_keydown} />
|
||||||
|
|
||||||
{#if error === "path_not_found" || error === "node_is_a_directory"}
|
{#if error === "path_not_found" || error === "node_is_a_directory"}
|
||||||
<div class="highlight_yellow center">
|
<div class="highlight_yellow center">
|
||||||
@@ -158,7 +166,7 @@ const window_keydown = (e: KeyboardEvent) => {
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<form class="search_form" on:submit|preventDefault={submit_search}>
|
<form class="search_form" onsubmit={submit_search}>
|
||||||
<i class="icon">search</i>
|
<i class="icon">search</i>
|
||||||
<input
|
<input
|
||||||
bind:this={search_bar}
|
bind:this={search_bar}
|
||||||
@@ -167,12 +175,12 @@ const window_keydown = (e: KeyboardEvent) => {
|
|||||||
placeholder="Press / to search in {$nav.base.name}"
|
placeholder="Press / to search in {$nav.base.name}"
|
||||||
style="width: 100%;"
|
style="width: 100%;"
|
||||||
bind:value={search_term}
|
bind:value={search_term}
|
||||||
on:keydown={input_keydown}
|
onkeydown={input_keydown}
|
||||||
on:keyup={input_keyup}
|
onkeyup={input_keyup}
|
||||||
/>
|
/>
|
||||||
{#if search_term !== ""}
|
{#if search_term !== ""}
|
||||||
<!-- Button needs to be of button type in order to not submit the form -->
|
<!-- Button needs to be of button type in order to not submit the form -->
|
||||||
<button on:click={() => clear_search(false)} type="button">
|
<button onclick={() => clear_search(false)} type="button">
|
||||||
<i class="icon">close</i>
|
<i class="icon">close</i>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -186,7 +194,7 @@ const window_keydown = (e: KeyboardEvent) => {
|
|||||||
{#each search_results as result, index}
|
{#each search_results as result, index}
|
||||||
<a
|
<a
|
||||||
href={"/d"+fs_encode_path(result)}
|
href={"/d"+fs_encode_path(result)}
|
||||||
on:click|preventDefault={() => open_result(index)}
|
onclick={(e) => open_result(index, e)}
|
||||||
class="node"
|
class="node"
|
||||||
class:node_selected={selected_result === index}
|
class:node_selected={selected_result === index}
|
||||||
>
|
>
|
||||||
@@ -201,7 +209,7 @@ const window_keydown = (e: KeyboardEvent) => {
|
|||||||
{#if search_results.length === last_limit}
|
{#if search_results.length === last_limit}
|
||||||
<div class="node">
|
<div class="node">
|
||||||
<div class="node_name" style="text-align: center;">
|
<div class="node_name" style="text-align: center;">
|
||||||
<button on:click={() => {search(last_limit + 100)}}>
|
<button onclick={() => {search(last_limit + 100)}}>
|
||||||
<i class="icon">expand_more</i>
|
<i class="icon">expand_more</i>
|
||||||
More results
|
More results
|
||||||
</button>
|
</button>
|
||||||
@@ -218,7 +226,6 @@ const window_keydown = (e: KeyboardEvent) => {
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
padding-top: 2px;
|
padding-top: 2px;
|
||||||
padding-bottom: 2px;
|
padding-bottom: 2px;
|
||||||
border-bottom: 1px solid var(--separator);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.search_form {
|
.search_form {
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
<script lang="ts" context="module">
|
<script lang="ts" module>
|
||||||
export type UploadJob = {
|
export type UploadJob = {
|
||||||
task_id: number,
|
task_id: number,
|
||||||
file: File,
|
file: File,
|
||||||
@@ -11,15 +11,14 @@ export type UploadJob = {
|
|||||||
</script>
|
</script>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { tick } from "svelte";
|
import { tick } from "svelte";
|
||||||
import { fade } from "svelte/transition";
|
|
||||||
import UploadProgress from "./UploadProgress.svelte";
|
import UploadProgress from "./UploadProgress.svelte";
|
||||||
import type { FSNavigator } from "filesystem/FSNavigator";
|
import type { FSNavigator } from "filesystem/FSNavigator";
|
||||||
|
|
||||||
export let nav: FSNavigator
|
let { nav }: {
|
||||||
|
nav: FSNavigator;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
const max_concurrent_uploads = 5
|
let file_input_field: HTMLInputElement = $state()
|
||||||
|
|
||||||
let file_input_field: HTMLInputElement
|
|
||||||
let file_input_change = (e: Event) => {
|
let file_input_change = (e: Event) => {
|
||||||
// Start uploading the files async
|
// Start uploading the files async
|
||||||
upload_files((e.target as HTMLInputElement).files)
|
upload_files((e.target as HTMLInputElement).files)
|
||||||
@@ -31,8 +30,8 @@ export const pick_files = () => {
|
|||||||
file_input_field.click()
|
file_input_field.click()
|
||||||
}
|
}
|
||||||
|
|
||||||
let visible = false
|
let visible = $state(false)
|
||||||
let upload_queue: UploadJob[] = [];
|
let upload_queue: UploadJob[] = $state([]);
|
||||||
let task_id_counter = 0
|
let task_id_counter = 0
|
||||||
|
|
||||||
export const upload_files = async (files: File[]|FileList) => {
|
export const upload_files = async (files: File[]|FileList) => {
|
||||||
@@ -79,8 +78,8 @@ export const upload_file = async (file: File) => {
|
|||||||
// each upload progress bar will have bound itself to its array item
|
// each upload progress bar will have bound itself to its array item
|
||||||
upload_queue = upload_queue
|
upload_queue = upload_queue
|
||||||
|
|
||||||
if (active_uploads === 0 && state !== "uploading") {
|
if (active_uploads === 0 && status !== "uploading") {
|
||||||
state = "uploading"
|
status = "uploading"
|
||||||
visible = true
|
visible = true
|
||||||
await tick()
|
await tick()
|
||||||
await start_upload()
|
await start_upload()
|
||||||
@@ -88,30 +87,37 @@ export const upload_file = async (file: File) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let active_uploads = 0
|
let active_uploads = 0
|
||||||
let state = "idle"
|
let status = $state("idle")
|
||||||
|
|
||||||
const start_upload = async () => {
|
const start_upload = async () => {
|
||||||
// Count the number of active uploads so we can know how many new uploads we
|
active_uploads = 0
|
||||||
// can start
|
let uploading_size = 0
|
||||||
active_uploads = upload_queue.reduce((acc, val) => {
|
for (let i = 0; i < upload_queue.length; i++) {
|
||||||
if (val.status === "uploading") {
|
|
||||||
acc++
|
|
||||||
}
|
|
||||||
return acc
|
|
||||||
}, 0)
|
|
||||||
|
|
||||||
for (let i = 0; i < upload_queue.length && active_uploads < max_concurrent_uploads; i++) {
|
|
||||||
if (upload_queue[i]) {
|
if (upload_queue[i]) {
|
||||||
|
// If this file is queued, start the upload
|
||||||
if (upload_queue[i].status === "queued") {
|
if (upload_queue[i].status === "queued") {
|
||||||
active_uploads++
|
|
||||||
upload_queue[i].component.start()
|
upload_queue[i].component.start()
|
||||||
upload_queue[i].status = "uploading"
|
upload_queue[i].status = "uploading"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If this file is already uploading (or just started), count it
|
||||||
|
if (upload_queue[i].status === "uploading") {
|
||||||
|
uploading_size += upload_queue[i].total_size
|
||||||
|
active_uploads++
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the size threshold or the concurrent upload limit is reached
|
||||||
|
// we break the loop. The system tries to keep an upload queue of
|
||||||
|
// 100 MB and a minimum of two concurrent uploads.
|
||||||
|
if ((uploading_size >= 100e6 && active_uploads >= 2) || active_uploads >= 10) {
|
||||||
|
console.debug("Current uploads", active_uploads, "uploads size", uploading_size)
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (active_uploads === 0) {
|
if (active_uploads === 0) {
|
||||||
state = "finished"
|
status = "finished"
|
||||||
nav.reload()
|
nav.reload()
|
||||||
|
|
||||||
// Empty the queue to free any references to lingering components
|
// Empty the queue to free any references to lingering components
|
||||||
@@ -119,12 +125,12 @@ const start_upload = async () => {
|
|||||||
|
|
||||||
// In ten seconds we close the popup
|
// In ten seconds we close the popup
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (state === "finished") {
|
if (status === "finished") {
|
||||||
visible = false
|
visible = false
|
||||||
}
|
}
|
||||||
}, 10000)
|
}, 10000)
|
||||||
} else {
|
} else {
|
||||||
state = "uploading"
|
status = "uploading"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +141,7 @@ const finish_upload = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const leave_confirmation = (e: BeforeUnloadEvent) => {
|
const leave_confirmation = (e: BeforeUnloadEvent) => {
|
||||||
if (state === "uploading") {
|
if (status === "uploading") {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
return "If you close this page your files will stop uploading. Do you want to continue?"
|
return "If you close this page your files will stop uploading. Do you want to continue?"
|
||||||
} else {
|
} else {
|
||||||
@@ -144,29 +150,29 @@ const leave_confirmation = (e: BeforeUnloadEvent) => {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:beforeunload={leave_confirmation} />
|
<svelte:window onbeforeunload={leave_confirmation} />
|
||||||
|
|
||||||
<input
|
<input
|
||||||
bind:this={file_input_field}
|
bind:this={file_input_field}
|
||||||
on:change={file_input_change}
|
onchange={file_input_change}
|
||||||
class="upload_input" type="file" name="file" multiple
|
class="upload_input" type="file" name="file" multiple
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if visible}
|
{#if visible}
|
||||||
<div class="upload_widget" transition:fade={{duration: 200}}>
|
<div class="upload_widget">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
{#if state === "idle"}
|
{#if status === "idle"}
|
||||||
Waiting for files
|
Waiting for files
|
||||||
{:else if state === "uploading"}
|
{:else if status === "uploading"}
|
||||||
Uploading files...
|
Uploading files...
|
||||||
{:else if state === "finished"}
|
{:else if status === "finished"}
|
||||||
Done
|
Done
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
{#each upload_queue as job}
|
{#each upload_queue as job}
|
||||||
{#if job.status !== "finished"}
|
{#if job.status !== "finished"}
|
||||||
<UploadProgress bind:this={job.component} job={job} on:finished={finish_upload}/>
|
<UploadProgress bind:this={job.component} job={job} finish={finish_upload}/>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@@ -184,7 +190,8 @@ const leave_confirmation = (e: BeforeUnloadEvent) => {
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 500px;
|
width: auto;
|
||||||
|
min-width: 400px;
|
||||||
max-width: 80%;
|
max-width: 80%;
|
||||||
height: auto;
|
height: auto;
|
||||||
max-height: 50%;
|
max-height: 50%;
|
||||||
|
@@ -9,7 +9,7 @@
|
|||||||
//
|
//
|
||||||
// on_error is called when the upload has failed. The parameters are the error
|
// on_error is called when the upload has failed. The parameters are the error
|
||||||
|
|
||||||
import { fs_path_url, type GenericResponse } from "filesystem/FilesystemAPI"
|
import { fs_path_url, type GenericResponse } from "lib/FilesystemAPI.svelte"
|
||||||
|
|
||||||
// code and an error message
|
// code and an error message
|
||||||
export const upload_file = (
|
export const upload_file = (
|
||||||
|
@@ -1,17 +1,23 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher } from "svelte";
|
|
||||||
import { fade } from "svelte/transition";
|
|
||||||
import { upload_file } from "./UploadFunc";
|
import { upload_file } from "./UploadFunc";
|
||||||
import ProgressBar from "util/ProgressBar.svelte";
|
import ProgressBar from "util/ProgressBar.svelte";
|
||||||
import Button from "layout/Button.svelte"
|
import Button from "layout/Button.svelte"
|
||||||
import type { UploadJob } from "./FSUploadWidget.svelte";
|
import type { UploadJob } from "./FSUploadWidget.svelte";
|
||||||
|
|
||||||
let dispatch = createEventDispatcher()
|
let {
|
||||||
export let job: UploadJob
|
job = $bindable(),
|
||||||
export let total = 0
|
total = $bindable(0),
|
||||||
export let loaded = 0
|
loaded = $bindable(0),
|
||||||
let error_code = ""
|
finish,
|
||||||
let error_message = ""
|
}: {
|
||||||
|
job: UploadJob
|
||||||
|
total?: number
|
||||||
|
loaded?: number
|
||||||
|
finish: () => void
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let error_code = $state("")
|
||||||
|
let error_message = $state("")
|
||||||
let xhr: XMLHttpRequest = null
|
let xhr: XMLHttpRequest = null
|
||||||
|
|
||||||
export const start = () => {
|
export const start = () => {
|
||||||
@@ -24,7 +30,7 @@ export const start = () => {
|
|||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
job.status = "finished"
|
job.status = "finished"
|
||||||
dispatch("finished")
|
finish()
|
||||||
},
|
},
|
||||||
(code, message) => {
|
(code, message) => {
|
||||||
if (job.status === "finished") {
|
if (job.status === "finished") {
|
||||||
@@ -52,18 +58,18 @@ const cancel = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
xhr = null
|
xhr = null
|
||||||
dispatch("finished")
|
finish()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="prog" transition:fade={{duration: 200}} class:error={job.status === "error"}>
|
<div class="prog" class:error={job.status === "error"}>
|
||||||
<div class="bar">
|
<div class="bar">
|
||||||
{job.file.name}<br/>
|
{job.file.name}<br/>
|
||||||
{#if error_code !== ""}
|
{#if error_code !== ""}
|
||||||
{error_message}<br/>
|
{error_message}<br/>
|
||||||
{error_code}<br/>
|
{error_code}<br/>
|
||||||
{/if}
|
{/if}
|
||||||
<ProgressBar total={total} used={loaded}/>
|
<ProgressBar total={total} used={loaded} speed={500}/>
|
||||||
</div>
|
</div>
|
||||||
<div class="cancel">
|
<div class="cancel">
|
||||||
<Button icon="cancel" click={cancel}/>
|
<Button icon="cancel" click={cancel}/>
|
||||||
|
@@ -1,10 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { preventDefault } from 'svelte/legacy';
|
||||||
import type { FSNavigator } from "filesystem/FSNavigator";
|
import type { FSNavigator } from "filesystem/FSNavigator";
|
||||||
|
|
||||||
export let nav: FSNavigator
|
let {
|
||||||
export let path = ""
|
nav,
|
||||||
|
path = "",
|
||||||
|
children
|
||||||
|
}: {
|
||||||
|
nav: FSNavigator;
|
||||||
|
path?: string;
|
||||||
|
children?: import('svelte').Snippet;
|
||||||
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a href={"/d"+path} on:click|preventDefault={() => {nav.navigate(path, true)}}>
|
<a href={"/d"+path} onclick={preventDefault(() => {nav.navigate(path, true)})}>
|
||||||
<slot></slot>
|
{@render children?.()}
|
||||||
</a>
|
</a>
|
||||||
|
@@ -1,15 +1,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { preventDefault } from 'svelte/legacy';
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
import { fs_path_url, fs_encode_path, fs_node_icon } from "filesystem/FilesystemAPI"
|
import { fs_path_url, fs_encode_path, fs_node_icon } from "lib/FilesystemAPI.svelte"
|
||||||
import FileTitle from "layout/FileTitle.svelte";
|
|
||||||
import TextBlock from "layout/TextBlock.svelte"
|
import TextBlock from "layout/TextBlock.svelte"
|
||||||
import type { FSNavigator } from 'filesystem/FSNavigator';
|
import type { FSNavigator } from 'filesystem/FSNavigator';
|
||||||
|
|
||||||
export let nav: FSNavigator
|
let { nav, children }: {
|
||||||
let player: HTMLAudioElement
|
nav: FSNavigator;
|
||||||
let playing = false
|
children?: import('svelte').Snippet;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let player: HTMLAudioElement = $state()
|
||||||
|
let playing = $state(false)
|
||||||
let media_session = false
|
let media_session = false
|
||||||
let siblings = []
|
let siblings = $state([])
|
||||||
|
|
||||||
export const toggle_playback = () => playing ? player.pause() : player.play()
|
export const toggle_playback = () => playing ? player.pause() : player.play()
|
||||||
export const toggle_mute = () => player.muted = !player.muted
|
export const toggle_mute = () => player.muted = !player.muted
|
||||||
@@ -48,9 +52,7 @@ onMount(() => {
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<slot></slot>
|
{@render children?.()}
|
||||||
|
|
||||||
<FileTitle title={$nav.base.name}/>
|
|
||||||
|
|
||||||
<TextBlock width="1000px">
|
<TextBlock width="1000px">
|
||||||
<audio
|
<audio
|
||||||
@@ -59,30 +61,30 @@ onMount(() => {
|
|||||||
src={fs_path_url($nav.base.path)}
|
src={fs_path_url($nav.base.path)}
|
||||||
autoplay
|
autoplay
|
||||||
controls
|
controls
|
||||||
on:pause={() => playing = false }
|
onpause={() => playing = false}
|
||||||
on:play={() => playing = true }
|
onplay={() => playing = true}
|
||||||
on:ended={() => nav.open_sibling(1) }>
|
onended={() => nav.open_sibling(1)}>
|
||||||
<track kind="captions"/>
|
<track kind="captions"/>
|
||||||
</audio>
|
</audio>
|
||||||
<div style="text-align: center;">
|
<div style="text-align: center;">
|
||||||
<button on:click={() => nav.open_sibling(-1) }><i class="icon">skip_previous</i></button>
|
<button onclick={() => nav.open_sibling(-1)}><i class="icon">skip_previous</i></button>
|
||||||
<button on:click={() => seek(-10) }><i class="icon">replay_10</i></button>
|
<button onclick={() => seek(-10)}><i class="icon">replay_10</i></button>
|
||||||
<button on:click={toggle_playback}>
|
<button onclick={toggle_playback}>
|
||||||
{#if playing}
|
{#if playing}
|
||||||
<i class="icon">pause</i>
|
<i class="icon">pause</i>
|
||||||
{:else}
|
{:else}
|
||||||
<i class="icon">play_arrow</i>
|
<i class="icon">play_arrow</i>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
<button on:click={() => seek(10) }><i class="icon">forward_10</i></button>
|
<button onclick={() => seek(10)}><i class="icon">forward_10</i></button>
|
||||||
<button on:click={() => nav.open_sibling(1) }><i class="icon">skip_next</i></button>
|
<button onclick={() => nav.open_sibling(1)}><i class="icon">skip_next</i></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>Tracklist</h2>
|
<h2>Tracklist</h2>
|
||||||
{#each siblings as sibling (sibling.path)}
|
{#each siblings as sibling (sibling.path)}
|
||||||
<a
|
<a
|
||||||
href={"/d"+fs_encode_path(sibling.path)}
|
href={"/d"+fs_encode_path(sibling.path)}
|
||||||
on:click|preventDefault={() => nav.navigate(sibling.path, true)}
|
onclick={preventDefault(() => nav.navigate(sibling.path, true))}
|
||||||
class="node"
|
class="node"
|
||||||
>
|
>
|
||||||
{#if sibling.path === $nav.base.path}
|
{#if sibling.path === $nav.base.path}
|
||||||
|
@@ -1,10 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let path = []
|
import type { FSNode } from 'lib/FilesystemAPI.svelte';
|
||||||
|
import { run } from 'svelte/legacy';
|
||||||
|
|
||||||
let image_uri: string
|
let { path = [] }: {path: FSNode[]} = $props();
|
||||||
let image_link: string
|
|
||||||
$: update_links(path)
|
let image_uri: string = $state()
|
||||||
const update_links = (path) => {
|
let image_link: string = $state()
|
||||||
|
const update_links = (path: FSNode[]) => {
|
||||||
image_uri = null
|
image_uri = null
|
||||||
image_link = null
|
image_link = null
|
||||||
for (let node of path) {
|
for (let node of path) {
|
||||||
@@ -18,6 +20,9 @@ const update_links = (path) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
run(() => {
|
||||||
|
update_links(path)
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if image_uri}
|
{#if image_uri}
|
||||||
|
@@ -1,36 +1,40 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher } from "svelte";
|
|
||||||
import IconBlock from "layout/IconBlock.svelte";
|
import IconBlock from "layout/IconBlock.svelte";
|
||||||
import { fs_thumbnail_url } from "filesystem/FilesystemAPI";
|
import { fs_thumbnail_url, FSNode } from "lib/FilesystemAPI.svelte";
|
||||||
import TextBlock from "layout/TextBlock.svelte"
|
import TextBlock from "layout/TextBlock.svelte"
|
||||||
import { formatDataVolume, formatDate } from "util/Formatting";
|
import { formatDataVolume, formatDate } from "util/Formatting";
|
||||||
import type { FSNavigator } from "filesystem/FSNavigator";
|
|
||||||
|
|
||||||
let dispatch = createEventDispatcher()
|
let {
|
||||||
|
node,
|
||||||
export let nav: FSNavigator
|
open_details,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
node: FSNode
|
||||||
|
open_details: () => void
|
||||||
|
children?: import('svelte').Snippet
|
||||||
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<slot></slot>
|
{@render children?.()}
|
||||||
|
|
||||||
<h1>{$nav.base.name}</h1>
|
<h1>{node.name}</h1>
|
||||||
|
|
||||||
<IconBlock icon_href={fs_thumbnail_url($nav.base.path, 256, 256)}>
|
<IconBlock icon_href={fs_thumbnail_url(node.path, 256, 256)}>
|
||||||
Type: {$nav.base.file_type}<br/>
|
Type: {node.file_type}<br/>
|
||||||
Size: {formatDataVolume($nav.base.file_size, 3)}<br/>
|
Size: {formatDataVolume(node.file_size, 3)}<br/>
|
||||||
Upload date: {formatDate($nav.base.created, true, true, false)}
|
Upload date: {formatDate(node.created, true, true, false)}
|
||||||
<hr/>
|
<hr/>
|
||||||
<button class="button_highlight" on:click={() => {dispatch("download")}}>
|
<button class="button_highlight" onclick={() => node.download()}>
|
||||||
<i class="icon">download</i>
|
<i class="icon">download</i>
|
||||||
<span>Download</span>
|
<span>Download</span>
|
||||||
</button>
|
</button>
|
||||||
<button on:click={() => {dispatch("details")}}>
|
<button onclick={() => open_details()}>
|
||||||
<i class="icon">help</i>
|
<i class="icon">help</i>
|
||||||
<span>Details</span>
|
<span>Details</span>
|
||||||
</button>
|
</button>
|
||||||
</IconBlock>
|
</IconBlock>
|
||||||
|
|
||||||
{#if $nav.base.name === ".search_index.gz"}
|
{#if node.name === ".search_index.gz"}
|
||||||
<TextBlock>
|
<TextBlock>
|
||||||
<p>
|
<p>
|
||||||
Congratulations! You have found the search index. One of the
|
Congratulations! You have found the search index. One of the
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, tick } from "svelte";
|
import { onMount, tick } from "svelte";
|
||||||
import Spinner from "util/Spinner.svelte";
|
import Spinner from "util/Spinner.svelte";
|
||||||
import { fs_node_type } from "filesystem/FilesystemAPI";
|
import { fs_node_type } from "lib/FilesystemAPI.svelte";
|
||||||
import FileManager from "filesystem/filemanager/FileManager.svelte";
|
import FileManager from "filesystem/filemanager/FileManager.svelte";
|
||||||
import Audio from "./Audio.svelte";
|
import Audio from "./Audio.svelte";
|
||||||
import File from "./File.svelte";
|
import File from "./File.svelte";
|
||||||
@@ -15,13 +15,22 @@ import CustomBanner from "./CustomBanner.svelte";
|
|||||||
import type { FSNavigator } from "filesystem/FSNavigator";
|
import type { FSNavigator } from "filesystem/FSNavigator";
|
||||||
import FsUploadWidget from "filesystem/upload_widget/FSUploadWidget.svelte";
|
import FsUploadWidget from "filesystem/upload_widget/FSUploadWidget.svelte";
|
||||||
import EditWindow from "filesystem/edit_window/EditWindow.svelte";
|
import EditWindow from "filesystem/edit_window/EditWindow.svelte";
|
||||||
|
import DetailsWindow from "filesystem/DetailsWindow.svelte";
|
||||||
|
|
||||||
export let nav: FSNavigator
|
let {
|
||||||
export let upload_widget: FsUploadWidget
|
nav,
|
||||||
export let edit_window: EditWindow
|
upload_widget,
|
||||||
|
edit_window,
|
||||||
|
details_window,
|
||||||
|
}: {
|
||||||
|
nav: FSNavigator
|
||||||
|
upload_widget: FsUploadWidget
|
||||||
|
edit_window: EditWindow
|
||||||
|
details_window: DetailsWindow
|
||||||
|
} = $props();
|
||||||
|
|
||||||
let viewer: any
|
let viewer: any = $state()
|
||||||
let viewer_type = ""
|
let viewer_type = $state("")
|
||||||
let last_path = ""
|
let last_path = ""
|
||||||
|
|
||||||
onMount(() => nav.subscribe(state_update))
|
onMount(() => nav.subscribe(state_update))
|
||||||
@@ -74,7 +83,7 @@ export const seek = (delta: number) => {
|
|||||||
|
|
||||||
{#if viewer_type === ""}
|
{#if viewer_type === ""}
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<Spinner></Spinner>
|
<Spinner/>
|
||||||
</div>
|
</div>
|
||||||
{:else if viewer_type === "dir"}
|
{:else if viewer_type === "dir"}
|
||||||
<FileManager nav={nav} upload_widget={upload_widget} edit_window={edit_window}>
|
<FileManager nav={nav} upload_widget={upload_widget} edit_window={edit_window}>
|
||||||
@@ -87,23 +96,23 @@ export const seek = (delta: number) => {
|
|||||||
{:else if viewer_type === "image"}
|
{:else if viewer_type === "image"}
|
||||||
<Image nav={nav} bind:this={viewer}/>
|
<Image nav={nav} bind:this={viewer}/>
|
||||||
{:else if viewer_type === "video"}
|
{:else if viewer_type === "video"}
|
||||||
<Video nav={nav} bind:this={viewer} on:open_sibling/>
|
<Video node={$nav.base} bind:this={viewer} open_sibling={(d) => nav.open_sibling(d)}/>
|
||||||
{:else if viewer_type === "pdf"}
|
{:else if viewer_type === "pdf"}
|
||||||
<Pdf nav={nav} bind:this={viewer}/>
|
<Pdf node={$nav.base} bind:this={viewer}/>
|
||||||
{:else if viewer_type === "text"}
|
{:else if viewer_type === "text"}
|
||||||
<Text nav={nav} bind:this={viewer}>
|
<Text node={$nav.base} bind:this={viewer}>
|
||||||
<CustomBanner path={$nav.path}/>
|
<CustomBanner path={$nav.path}/>
|
||||||
</Text>
|
</Text>
|
||||||
{:else if viewer_type === "torrent"}
|
{:else if viewer_type === "torrent"}
|
||||||
<Torrent nav={nav} bind:this={viewer} on:download>
|
<Torrent node={$nav.base} bind:this={viewer}>
|
||||||
<CustomBanner path={$nav.path}/>
|
<CustomBanner path={$nav.path}/>
|
||||||
</Torrent>
|
</Torrent>
|
||||||
{:else if viewer_type === "zip"}
|
{:else if viewer_type === "zip"}
|
||||||
<Zip nav={nav} bind:this={viewer} on:download>
|
<Zip node={$nav.base} bind:this={viewer}>
|
||||||
<CustomBanner path={$nav.path}/>
|
<CustomBanner path={$nav.path}/>
|
||||||
</Zip>
|
</Zip>
|
||||||
{:else}
|
{:else}
|
||||||
<File nav={nav} on:download on:details>
|
<File node={$nav.base} open_details={() => details_window.toggle()}>
|
||||||
<CustomBanner path={$nav.path}/>
|
<CustomBanner path={$nav.path}/>
|
||||||
</File>
|
</File>
|
||||||
{/if}
|
{/if}
|
||||||
|
@@ -1,21 +1,22 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher } from "svelte";
|
|
||||||
import { swipe_nav } from "lib/SwipeNavigate";
|
import { swipe_nav } from "lib/SwipeNavigate";
|
||||||
import { fs_path_url } from "filesystem/FilesystemAPI";
|
import { fs_path_url } from "lib/FilesystemAPI.svelte";
|
||||||
import type { FSNavigator } from "filesystem/FSNavigator";
|
import type { FSNavigator } from "filesystem/FSNavigator";
|
||||||
|
import { loading_finish, loading_start } from "lib/Loading";
|
||||||
|
|
||||||
let dispatch = createEventDispatcher();
|
let { nav }: {
|
||||||
|
nav: FSNavigator;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
export let nav: FSNavigator
|
let container: HTMLDivElement = $state()
|
||||||
let container: HTMLDivElement
|
let zoom = $state(false)
|
||||||
let zoom = false
|
|
||||||
let x = 0, y = 0
|
let x = 0, y = 0
|
||||||
let dragging = false
|
let dragging = false
|
||||||
let swipe_prev = true
|
let swipe_prev = $state(true)
|
||||||
let swipe_next = true
|
let swipe_next = $state(true)
|
||||||
|
|
||||||
export const update = async () => {
|
export const update = async () => {
|
||||||
dispatch("loading", true)
|
loading_start()
|
||||||
|
|
||||||
// Figure out if there are previous or next files. If not then we disable
|
// Figure out if there are previous or next files. If not then we disable
|
||||||
// swiping controls in that direction
|
// swiping controls in that direction
|
||||||
@@ -29,7 +30,7 @@ export const update = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const on_load = () => dispatch("loading", false)
|
const on_load = () => loading_finish()
|
||||||
|
|
||||||
const mousedown = (e: MouseEvent) => {
|
const mousedown = (e: MouseEvent) => {
|
||||||
if (!dragging && e.which === 1 && zoom) {
|
if (!dragging && e.which === 1 && zoom) {
|
||||||
@@ -66,7 +67,7 @@ const mouseup = (e: MouseEvent) => {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:mousemove={mousemove} on:mouseup={mouseup} />
|
<svelte:window onmousemove={mousemove} onmouseup={mouseup} />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={container}
|
bind:this={container}
|
||||||
@@ -80,12 +81,12 @@ const mouseup = (e: MouseEvent) => {
|
|||||||
on_next: () => nav.open_sibling(1),
|
on_next: () => nav.open_sibling(1),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||||
<img
|
<img
|
||||||
on:dblclick={() => {zoom = !zoom}}
|
ondblclick={() => {zoom = !zoom}}
|
||||||
on:mousedown={mousedown}
|
onmousedown={mousedown}
|
||||||
on:load={on_load}
|
onload={on_load}
|
||||||
on:error={on_load}
|
onerror={on_load}
|
||||||
class="image"
|
class="image"
|
||||||
class:zoom
|
class:zoom
|
||||||
src={fs_path_url($nav.base.path)}
|
src={fs_path_url($nav.base.path)}
|
||||||
|
@@ -1,13 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { fs_path_url } from "filesystem/FilesystemAPI";
|
import { fs_path_url, FSNode } from "lib/FilesystemAPI.svelte";
|
||||||
import type { FSNavigator } from "filesystem/FSNavigator";
|
|
||||||
|
|
||||||
export let nav: FSNavigator
|
let { node }: { node: FSNode } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<iframe
|
<iframe
|
||||||
class="container"
|
class="container"
|
||||||
src={"/res/misc/pdf-viewer/web/viewer.html?file="+fs_path_url($nav.base.path)}
|
src={"/res/misc/pdf-viewer/web/viewer.html?file="+fs_path_url(node.path)}
|
||||||
title="PDF viewer">
|
title="PDF viewer">
|
||||||
</iframe>
|
</iframe>
|
||||||
|
|
||||||
|
@@ -1,31 +1,37 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { tick } from "svelte";
|
import { tick } from "svelte";
|
||||||
import { fs_path_url, type FSNode } from "filesystem/FilesystemAPI";
|
import { fs_path_url, type FSNode } from "lib/FilesystemAPI.svelte";
|
||||||
import type { FSNavigator } from "filesystem/FSNavigator";
|
|
||||||
|
|
||||||
export let nav: FSNavigator
|
let {
|
||||||
let text_type = "text"
|
node,
|
||||||
|
children
|
||||||
|
}: {
|
||||||
|
node: FSNode
|
||||||
|
children?: import('svelte').Snippet;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let text_type = $state("text")
|
||||||
|
|
||||||
export const update = () => {
|
export const update = () => {
|
||||||
console.debug("Loading text file", nav.base.name)
|
console.debug("Loading text file", node.name)
|
||||||
|
|
||||||
if (nav.base.file_size > 1 << 21) { // File larger than 2 MiB
|
if (node.file_size > 1 << 21) { // File larger than 2 MiB
|
||||||
text_pre.innerText = "File is too large to view online.\nPlease download and view it locally."
|
text_pre.innerText = "File is too large to view online.\nPlease download and view it locally."
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
nav.base.file_type.startsWith("text/markdown") ||
|
node.file_type.startsWith("text/markdown") ||
|
||||||
nav.base.name.endsWith(".md") ||
|
node.name.endsWith(".md") ||
|
||||||
nav.base.name.endsWith(".markdown")
|
node.name.endsWith(".markdown")
|
||||||
) {
|
) {
|
||||||
markdown(nav.base)
|
markdown(node)
|
||||||
} else {
|
} else {
|
||||||
text(nav.base)
|
text(node)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let text_pre: HTMLPreElement
|
let text_pre: HTMLPreElement = $state()
|
||||||
const text = async (file: FSNode) => {
|
const text = async (file: FSNode) => {
|
||||||
text_type = "text"
|
text_type = "text"
|
||||||
await tick()
|
await tick()
|
||||||
@@ -42,7 +48,7 @@ const text = async (file: FSNode) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
let md_container: HTMLElement
|
let md_container: HTMLElement = $state()
|
||||||
const markdown = async (file: FSNode) => {
|
const markdown = async (file: FSNode) => {
|
||||||
text_type = "markdown"
|
text_type = "markdown"
|
||||||
await tick()
|
await tick()
|
||||||
@@ -61,7 +67,7 @@ const markdown = async (file: FSNode) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<slot></slot>
|
{@render children?.()}
|
||||||
|
|
||||||
{#if text_type === "markdown"}
|
{#if text_type === "markdown"}
|
||||||
<section bind:this={md_container} class="md">
|
<section bind:this={md_container} class="md">
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
<script lang="ts" context="module">
|
<script lang="ts" module>
|
||||||
export type TorrentInfo = {
|
export type TorrentInfo = {
|
||||||
trackers: string[]
|
trackers: string[]
|
||||||
comment: string,
|
comment: string,
|
||||||
@@ -13,26 +13,29 @@ export type TorrentFile = {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher } from "svelte";
|
|
||||||
import Magnet from "icons/Magnet.svelte";
|
import Magnet from "icons/Magnet.svelte";
|
||||||
import { formatDate } from "util/Formatting"
|
import { formatDate } from "util/Formatting"
|
||||||
import TorrentItem from "./TorrentItem.svelte"
|
import TorrentItem from "./TorrentItem.svelte"
|
||||||
import IconBlock from "layout/IconBlock.svelte";
|
import IconBlock from "layout/IconBlock.svelte";
|
||||||
import TextBlock from "layout/TextBlock.svelte"
|
import TextBlock from "layout/TextBlock.svelte"
|
||||||
import { fs_node_icon, fs_path_url } from "filesystem/FilesystemAPI";
|
import { fs_node_icon, fs_path_url, FSNode } from "lib/FilesystemAPI.svelte";
|
||||||
import CopyButton from "layout/CopyButton.svelte";
|
import CopyButton from "layout/CopyButton.svelte";
|
||||||
import type { FSNavigator } from "filesystem/FSNavigator";
|
import { loading_finish, loading_start } from "lib/Loading";
|
||||||
|
|
||||||
let dispatch = createEventDispatcher()
|
let {
|
||||||
|
node,
|
||||||
|
children
|
||||||
|
}: {
|
||||||
|
node: FSNode;
|
||||||
|
children?: import('svelte').Snippet;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
export let nav: FSNavigator
|
let status = $state("loading")
|
||||||
|
|
||||||
let status = "loading"
|
|
||||||
|
|
||||||
export const update = async () => {
|
export const update = async () => {
|
||||||
try {
|
try {
|
||||||
nav.set_loading(true)
|
loading_start()
|
||||||
let resp = await fetch(fs_path_url(nav.base.path)+"?torrent_info")
|
let resp = await fetch(fs_path_url(node.path)+"?torrent_info")
|
||||||
|
|
||||||
if (resp.status >= 400) {
|
if (resp.status >= 400) {
|
||||||
let json = await resp.json()
|
let json = await resp.json()
|
||||||
@@ -58,20 +61,20 @@ export const update = async () => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
} finally {
|
} finally {
|
||||||
nav.set_loading(false)
|
loading_finish()
|
||||||
status = "finished"
|
status = "finished"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let torrent: TorrentInfo = {} as TorrentInfo
|
let torrent: TorrentInfo = $state({} as TorrentInfo)
|
||||||
let magnet = ""
|
let magnet = $state("")
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<slot></slot>
|
{@render children?.()}
|
||||||
|
|
||||||
<h1>{$nav.base.name}</h1>
|
<h1>{node.name}</h1>
|
||||||
|
|
||||||
<IconBlock icon_href={fs_node_icon($nav.base, 256, 256)}>
|
<IconBlock icon_href={fs_node_icon(node, 256, 256)}>
|
||||||
{#if status === "finished"}
|
{#if status === "finished"}
|
||||||
Created by: {torrent.created_by}<br/>
|
Created by: {torrent.created_by}<br/>
|
||||||
Comment: {torrent.comment}<br/>
|
Comment: {torrent.comment}<br/>
|
||||||
@@ -92,7 +95,7 @@ let magnet = ""
|
|||||||
Torrent file could not be parsed. It may be corrupted.
|
Torrent file could not be parsed. It may be corrupted.
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
<button on:click={() => {dispatch("download")}} class="button">
|
<button onclick={() => node.download()} class="button">
|
||||||
<i class="icon">download</i>
|
<i class="icon">download</i>
|
||||||
<span>Download torrent file</span>
|
<span>Download torrent file</span>
|
||||||
</button>
|
</button>
|
||||||
|
@@ -1,8 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import TorrentItem from './TorrentItem.svelte';
|
||||||
import { formatDataVolume } from "util/Formatting";
|
import { formatDataVolume } from "util/Formatting";
|
||||||
import type { TorrentFile } from "./Torrent.svelte";
|
import type { TorrentFile } from "./Torrent.svelte";
|
||||||
|
|
||||||
export let item: TorrentFile = {} as TorrentFile
|
let { item = {} as TorrentFile }: {
|
||||||
|
item?: TorrentFile;
|
||||||
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ul class="list_open">
|
<ul class="list_open">
|
||||||
@@ -10,7 +13,7 @@ export let item: TorrentFile = {} as TorrentFile
|
|||||||
<li class:list_closed={!child.children}>
|
<li class:list_closed={!child.children}>
|
||||||
{name} ({formatDataVolume(child.size, 3)})<br/>
|
{name} ({formatDataVolume(child.size, 3)})<br/>
|
||||||
{#if child.children}
|
{#if child.children}
|
||||||
<svelte:self item={child}></svelte:self>
|
<TorrentItem item={child}></TorrentItem>
|
||||||
{/if}
|
{/if}
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
|
@@ -1,38 +1,42 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, createEventDispatcher, tick } from "svelte";
|
import { onMount, tick } from "svelte";
|
||||||
import { video_position } from "lib/VideoPosition";
|
import { video_position } from "lib/VideoPosition";
|
||||||
import { fs_path_url } from "filesystem/FilesystemAPI";
|
import { fs_path_url, FSNode } from "lib/FilesystemAPI.svelte";
|
||||||
import type { FSNavigator } from "filesystem/FSNavigator";
|
|
||||||
let dispatch = createEventDispatcher()
|
|
||||||
|
|
||||||
export let nav: FSNavigator
|
let {
|
||||||
|
node,
|
||||||
|
open_sibling,
|
||||||
|
}: {
|
||||||
|
node: FSNode
|
||||||
|
open_sibling: (delta: number) => void
|
||||||
|
} = $props();
|
||||||
|
|
||||||
// Used to detect when the file path changes
|
// Used to detect when the file path changes
|
||||||
let last_path = ""
|
let last_path = ""
|
||||||
let loaded = false
|
let loaded = $state(false)
|
||||||
|
|
||||||
let player: HTMLVideoElement
|
let player: HTMLVideoElement = $state()
|
||||||
let playing = false
|
let playing = $state(false)
|
||||||
let media_session = false
|
let media_session = false
|
||||||
let loop = false
|
let loop = $state(false)
|
||||||
|
|
||||||
export const update = async () => {
|
export const update = async () => {
|
||||||
if (media_session) {
|
if (media_session) {
|
||||||
navigator.mediaSession.metadata = new MediaMetadata({
|
navigator.mediaSession.metadata = new MediaMetadata({
|
||||||
title: nav.base.name,
|
title: node.name,
|
||||||
artist: "pixeldrain",
|
artist: "pixeldrain",
|
||||||
album: "unknown",
|
album: "unknown",
|
||||||
});
|
});
|
||||||
console.debug("Updating media session")
|
console.debug("Updating media session")
|
||||||
}
|
}
|
||||||
|
|
||||||
loop = nav.base.name.includes(".loop.")
|
loop = node.name.includes(".loop.")
|
||||||
|
|
||||||
// When the component receives a new ID the video track does not
|
// When the component receives a new ID the video track does not
|
||||||
// automatically start playing the new video. So we use this little hack to
|
// automatically start playing the new video. So we use this little hack to
|
||||||
// make sure that the video is unloaded and loaded when the ID changes
|
// make sure that the video is unloaded and loaded when the ID changes
|
||||||
if (nav.base.path != last_path) {
|
if (node.path != last_path) {
|
||||||
last_path = nav.base.path
|
last_path = node.path
|
||||||
loaded = false
|
loaded = false
|
||||||
await tick()
|
await tick()
|
||||||
loaded = true
|
loaded = true
|
||||||
@@ -49,8 +53,7 @@ export const toggle_fullscreen = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const seek = (delta: number) => {
|
||||||
export const seek = delta => {
|
|
||||||
// fastseek can be pretty imprecise, so we don't use it for small seeks
|
// fastseek can be pretty imprecise, so we don't use it for small seeks
|
||||||
// below 5 seconds
|
// below 5 seconds
|
||||||
if (player.fastSeek && delta > 5) {
|
if (player.fastSeek && delta > 5) {
|
||||||
@@ -66,8 +69,8 @@ onMount(() => {
|
|||||||
navigator.mediaSession.setActionHandler('play', () => player.play());
|
navigator.mediaSession.setActionHandler('play', () => player.play());
|
||||||
navigator.mediaSession.setActionHandler('pause', () => player.pause());
|
navigator.mediaSession.setActionHandler('pause', () => player.pause());
|
||||||
navigator.mediaSession.setActionHandler('stop', () => player.pause());
|
navigator.mediaSession.setActionHandler('stop', () => player.pause());
|
||||||
navigator.mediaSession.setActionHandler('previoustrack', () => dispatch("open_sibling", -1));
|
navigator.mediaSession.setActionHandler('previoustrack', () => open_sibling(-1));
|
||||||
navigator.mediaSession.setActionHandler('nexttrack', () => dispatch("open_sibling", 1));
|
navigator.mediaSession.setActionHandler('nexttrack', () => open_sibling(1));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -83,10 +86,9 @@ const video_keydown = (e: KeyboardEvent) => {
|
|||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{#if
|
{#if
|
||||||
$nav.base.file_type === "video/x-matroska" ||
|
node.file_type === "video/x-matroska" ||
|
||||||
$nav.base.file_type === "video/quicktime" ||
|
node.file_type === "video/quicktime" ||
|
||||||
$nav.base.file_type === "video/x-ms-asf"
|
node.file_type === "video/x-ms-asf"}
|
||||||
}
|
|
||||||
<div class="compatibility_warning">
|
<div class="compatibility_warning">
|
||||||
This video file type is not compatible with every web
|
This video file type is not compatible with every web
|
||||||
browser. If the video fails to play you can try downloading
|
browser. If the video fails to play you can try downloading
|
||||||
@@ -96,7 +98,7 @@ const video_keydown = (e: KeyboardEvent) => {
|
|||||||
<div class="player_and_controls">
|
<div class="player_and_controls">
|
||||||
<div class="player">
|
<div class="player">
|
||||||
{#if loaded}
|
{#if loaded}
|
||||||
<!-- svelte-ignore a11y-media-has-caption -->
|
<!-- svelte-ignore a11y_media_has_caption -->
|
||||||
<video
|
<video
|
||||||
bind:this={player}
|
bind:this={player}
|
||||||
controls
|
controls
|
||||||
@@ -104,46 +106,46 @@ const video_keydown = (e: KeyboardEvent) => {
|
|||||||
autoplay
|
autoplay
|
||||||
loop={loop}
|
loop={loop}
|
||||||
class="video"
|
class="video"
|
||||||
on:pause={() => playing = false }
|
onpause={() => playing = false}
|
||||||
on:play={() => playing = true }
|
onplay={() => playing = true}
|
||||||
on:keydown={video_keydown}
|
onkeydown={video_keydown}
|
||||||
use:video_position={() => $nav.base.sha256_sum.substring(0, 8)}
|
use:video_position={() => node.sha256_sum.substring(0, 8)}
|
||||||
>
|
>
|
||||||
<source src={fs_path_url($nav.base.path)} type={$nav.base.file_type} />
|
<source src={fs_path_url(node.path)} type={node.file_type} />
|
||||||
</video>
|
</video>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
<button on:click={() => dispatch("open_sibling", -1) }>
|
<button onclick={() => open_sibling(-1)}>
|
||||||
<i class="icon">skip_previous</i>
|
<i class="icon">skip_previous</i>
|
||||||
</button>
|
</button>
|
||||||
<button on:click={() => seek(-10)}>
|
<button onclick={() => seek(-10)}>
|
||||||
<i class="icon">replay_10</i>
|
<i class="icon">replay_10</i>
|
||||||
</button>
|
</button>
|
||||||
<button on:click={toggle_playback} class="button_highlight">
|
<button onclick={toggle_playback} class="button_highlight">
|
||||||
{#if playing}
|
{#if playing}
|
||||||
<i class="icon">pause</i>
|
<i class="icon">pause</i>
|
||||||
{:else}
|
{:else}
|
||||||
<i class="icon">play_arrow</i>
|
<i class="icon">play_arrow</i>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
<button on:click={() => seek(10)}>
|
<button onclick={() => seek(10)}>
|
||||||
<i class="icon">forward_10</i>
|
<i class="icon">forward_10</i>
|
||||||
</button>
|
</button>
|
||||||
<button on:click={() => dispatch("open_sibling", 1) }>
|
<button onclick={() => open_sibling(1)}>
|
||||||
<i class="icon">skip_next</i>
|
<i class="icon">skip_next</i>
|
||||||
</button>
|
</button>
|
||||||
<div style="width: 16px; height: 8px;"></div>
|
<div style="width: 16px; height: 8px;"></div>
|
||||||
<button on:click={toggle_mute} class:button_red={player && player.muted}>
|
<button onclick={toggle_mute} class:button_red={player && player.muted}>
|
||||||
{#if player && player.muted}
|
{#if player && player.muted}
|
||||||
<i class="icon">volume_off</i>
|
<i class="icon">volume_off</i>
|
||||||
{:else}
|
{:else}
|
||||||
<i class="icon">volume_up</i>
|
<i class="icon">volume_up</i>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
<button on:click={toggle_fullscreen}>
|
<button onclick={toggle_fullscreen}>
|
||||||
<i class="icon">fullscreen</i>
|
<i class="icon">fullscreen</i>
|
||||||
</button>
|
</button>
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
<script lang="ts" context="module">
|
<script lang="ts" module>
|
||||||
export type ZipEntry = {
|
export type ZipEntry = {
|
||||||
size: number,
|
size: number,
|
||||||
children?: {[index: string]: ZipEntry},
|
children?: {[index: string]: ZipEntry},
|
||||||
@@ -8,37 +8,40 @@ export type ZipEntry = {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher } from "svelte";
|
|
||||||
import { formatDataVolume, formatDate } from "util/Formatting"
|
import { formatDataVolume, formatDate } from "util/Formatting"
|
||||||
import ZipItem from "filesystem/viewers/ZipItem.svelte";
|
import ZipItem from "filesystem/viewers/ZipItem.svelte";
|
||||||
import IconBlock from "layout/IconBlock.svelte";
|
import IconBlock from "layout/IconBlock.svelte";
|
||||||
import TextBlock from "layout/TextBlock.svelte"
|
import TextBlock from "layout/TextBlock.svelte"
|
||||||
import { fs_node_icon, fs_path_url } from "filesystem/FilesystemAPI";
|
import { fs_node_icon, fs_path_url, FSNode } from "lib/FilesystemAPI.svelte";
|
||||||
import type { FSNavigator } from "filesystem/FSNavigator";
|
import { loading_finish, loading_start } from "lib/Loading";
|
||||||
|
|
||||||
let dispatch = createEventDispatcher()
|
let {
|
||||||
|
node,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
node: FSNode;
|
||||||
|
children?: import('svelte').Snippet;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
export let nav: FSNavigator
|
let status = $state("loading")
|
||||||
|
|
||||||
let status = "loading"
|
let zip: ZipEntry = $state({size: 0} as ZipEntry)
|
||||||
|
|
||||||
let zip: ZipEntry = {size: 0} as ZipEntry
|
|
||||||
let uncomp_size = 0
|
let uncomp_size = 0
|
||||||
let comp_ratio = 0
|
let comp_ratio = $state(0)
|
||||||
let archive_type = ""
|
let archive_type = $state("")
|
||||||
let truncated = false
|
let truncated = $state(false)
|
||||||
|
|
||||||
export const update = async () => {
|
export const update = async () => {
|
||||||
if (nav.base.file_type === "application/x-7z-compressed") {
|
if (node.file_type === "application/x-7z-compressed") {
|
||||||
archive_type = "7z"
|
archive_type = "7z"
|
||||||
} else {
|
} else {
|
||||||
archive_type = ""
|
archive_type = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
loading_start()
|
||||||
status = "loading"
|
status = "loading"
|
||||||
nav.set_loading(true)
|
let resp = await fetch(fs_path_url(node.path)+"?zip_info")
|
||||||
let resp = await fetch(fs_path_url(nav.base.path)+"?zip_info")
|
|
||||||
|
|
||||||
if (resp.status >= 400) {
|
if (resp.status >= 400) {
|
||||||
status = "parse_failed"
|
status = "parse_failed"
|
||||||
@@ -52,19 +55,19 @@ export const update = async () => {
|
|||||||
if (zip.properties !== undefined) {
|
if (zip.properties !== undefined) {
|
||||||
if (zip.properties.includes("read_individual_files")) {
|
if (zip.properties.includes("read_individual_files")) {
|
||||||
// Set the download URL for each file in the zip
|
// Set the download URL for each file in the zip
|
||||||
recursive_set_url(fs_path_url(nav.base.path)+"?zip_file=", zip)
|
recursive_set_url(fs_path_url(node.path)+"?zip_file=", zip)
|
||||||
}
|
}
|
||||||
truncated = zip.properties.includes("truncated")
|
truncated = zip.properties.includes("truncated")
|
||||||
}
|
}
|
||||||
|
|
||||||
uncomp_size = recursive_size(zip)
|
uncomp_size = recursive_size(zip)
|
||||||
comp_ratio = (uncomp_size / nav.base.file_size)
|
comp_ratio = (uncomp_size / node.file_size)
|
||||||
status = "finished"
|
status = "finished"
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
status = "parse_failed"
|
status = "parse_failed"
|
||||||
} finally {
|
} finally {
|
||||||
nav.set_loading(false)
|
loading_finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,24 +95,24 @@ const recursive_size = (file: ZipEntry) => {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<slot></slot>
|
{@render children?.()}
|
||||||
|
|
||||||
<h1>{$nav.base.name}</h1>
|
<h1>{node.name}</h1>
|
||||||
|
|
||||||
<IconBlock icon_href={fs_node_icon($nav.base, 256, 256)}>
|
<IconBlock icon_href={fs_node_icon(node, 256, 256)}>
|
||||||
{#if archive_type === "7z"}
|
{#if archive_type === "7z"}
|
||||||
This is a 7-zip archive. You will need
|
This is a 7-zip archive. You will need
|
||||||
<a href="https://www.7-zip.org/">7-zip</a> or compatible software to
|
<a href="https://www.7-zip.org/">7-zip</a> or compatible software to
|
||||||
extract it<br/>
|
extract it<br/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
Compressed size: {formatDataVolume($nav.base.file_size, 3)}<br/>
|
Compressed size: {formatDataVolume(node.file_size, 3)}<br/>
|
||||||
{#if !truncated}
|
{#if !truncated}
|
||||||
Uncompressed size: {formatDataVolume(zip.size, 3)} (Ratio: {comp_ratio.toFixed(2)}x)<br/>
|
Uncompressed size: {formatDataVolume(zip.size, 3)} (Ratio: {comp_ratio.toFixed(2)}x)<br/>
|
||||||
{/if}
|
{/if}
|
||||||
Uploaded on: {formatDate($nav.base.created, true, true, true)}
|
Uploaded on: {formatDate(node.created, true, true, true)}
|
||||||
<br/>
|
<br/>
|
||||||
<button class="button_highlight" on:click={() => {dispatch("download")}}>
|
<button class="button_highlight" onclick={() => node.download()}>
|
||||||
<i class="icon">download</i>
|
<i class="icon">download</i>
|
||||||
<span>Download</span>
|
<span>Download</span>
|
||||||
</button>
|
</button>
|
||||||
|
@@ -1,8 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import ZipItem from './ZipItem.svelte';
|
||||||
import type { ZipEntry } from "filesystem/viewers/Zip.svelte";
|
import type { ZipEntry } from "filesystem/viewers/Zip.svelte";
|
||||||
import { formatDataVolume } from "util/Formatting";
|
import { formatDataVolume } from "util/Formatting";
|
||||||
|
|
||||||
export let item: ZipEntry = {} as ZipEntry
|
let { item = {} as ZipEntry }: {
|
||||||
|
item?: ZipEntry;
|
||||||
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- First get directories and render them as details collapsibles -->
|
<!-- First get directories and render them as details collapsibles -->
|
||||||
@@ -23,7 +26,7 @@ export let item: ZipEntry = {} as ZipEntry
|
|||||||
|
|
||||||
<!-- Performance optimization, only render children if details is expanded -->
|
<!-- Performance optimization, only render children if details is expanded -->
|
||||||
{#if child.details_open}
|
{#if child.details_open}
|
||||||
<svelte:self item={child}></svelte:self>
|
<ZipItem item={child}></ZipItem>
|
||||||
{/if}
|
{/if}
|
||||||
</details>
|
</details>
|
||||||
{/if}
|
{/if}
|
||||||
|
@@ -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 Expandable from "util/Expandable.svelte";
|
||||||
import { formatDate } from "util/Formatting";
|
import { formatDate } from "util/Formatting";
|
||||||
|
|
||||||
let result = null;
|
let result = $state(null);
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -21,9 +21,11 @@ onMount(async () => {
|
|||||||
{#if result !== null && result.user_banned}
|
{#if result !== null && result.user_banned}
|
||||||
<section>
|
<section>
|
||||||
<Expandable click_expand>
|
<Expandable click_expand>
|
||||||
<div slot="header" class="header red">
|
{#snippet header()}
|
||||||
|
<div class="header red">
|
||||||
Your account has been banned, click for details
|
Your account has been banned, click for details
|
||||||
</div>
|
</div>
|
||||||
|
{/snippet}
|
||||||
<p>
|
<p>
|
||||||
Your user account has been banned from uploading to
|
Your user account has been banned from uploading to
|
||||||
pixeldrain due to violation of the
|
pixeldrain due to violation of the
|
||||||
@@ -72,13 +74,16 @@ onMount(async () => {
|
|||||||
{:else if result !== null && result.ip_offences.length > 0}
|
{:else if result !== null && result.ip_offences.length > 0}
|
||||||
<section>
|
<section>
|
||||||
<Expandable click_expand>
|
<Expandable click_expand>
|
||||||
<div slot="header" class="header" class:red={result.ip_banned} class:yellow={!result.ip_banned}>
|
{#snippet header()}
|
||||||
|
<div class="header" class:red={result.ip_banned} class:yellow={!result.ip_banned}>
|
||||||
{#if result.ip_banned}
|
{#if result.ip_banned}
|
||||||
Your IP address has been banned, click for details
|
Your IP address has been banned, click for details
|
||||||
{:else}
|
{:else}
|
||||||
Your IP address has received a copyright strike, click for details
|
Your IP address has received a copyright strike, click for details
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
{#if result.ip_banned}
|
{#if result.ip_banned}
|
||||||
<p>
|
<p>
|
||||||
Your IP address ({result.address}) has been banned from
|
Your IP address ({result.address}) has been banned from
|
||||||
|
@@ -15,7 +15,7 @@ import OtherPlans from "./OtherPlans.svelte";
|
|||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="vertical_scroll">
|
<div class="horizontal_scroll">
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<div></div>
|
<div></div>
|
||||||
<div class="top_row free_feat">
|
<div class="top_row free_feat">
|
||||||
@@ -36,10 +36,12 @@ import OtherPlans from "./OtherPlans.svelte";
|
|||||||
</div>
|
</div>
|
||||||
<div class="feature_cell pro_feat">
|
<div class="feature_cell pro_feat">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<span slot="label">
|
{#snippet label()}
|
||||||
|
<span>
|
||||||
<span class="bold">€4 / month</span> or
|
<span class="bold">€4 / month</span> or
|
||||||
<span class="bold">€40 / year</span>
|
<span class="bold">€40 / year</span>
|
||||||
</span>
|
</span>
|
||||||
|
{/snippet}
|
||||||
The Pro subscription is managed by Patreon. Patreon's own fees
|
The Pro subscription is managed by Patreon. Patreon's own fees
|
||||||
and sales tax will be added to this price. After paying you need
|
and sales tax will be added to this price. After paying you need
|
||||||
to link your pixeldrain account to Patreon to activate the plan.
|
to link your pixeldrain account to Patreon to activate the plan.
|
||||||
@@ -47,7 +49,9 @@ import OtherPlans from "./OtherPlans.svelte";
|
|||||||
</div>
|
</div>
|
||||||
<div class="feature_cell prepaid_feat">
|
<div class="feature_cell prepaid_feat">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<span slot="label" class="bold">€1 / month minimum</span>
|
{#snippet label()}
|
||||||
|
<span class="bold">€1 / month minimum</span>
|
||||||
|
{/snippet}
|
||||||
<p>
|
<p>
|
||||||
The minimum fee is only charged when usage is less than €1.
|
The minimum fee is only charged when usage is less than €1.
|
||||||
This calculation is per day, the €1 amount is divided by the
|
This calculation is per day, the €1 amount is divided by the
|
||||||
@@ -62,7 +66,9 @@ import OtherPlans from "./OtherPlans.svelte";
|
|||||||
</div>
|
</div>
|
||||||
<div class="feature_cell free_feat">
|
<div class="feature_cell free_feat">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<span slot="label" class="bold">6 GB per day</span>
|
{#snippet label()}
|
||||||
|
<span class="bold">6 GB per day</span>
|
||||||
|
{/snippet}
|
||||||
<p>
|
<p>
|
||||||
Free users are limited to downloading 6 GB per day, this
|
Free users are limited to downloading 6 GB per day, this
|
||||||
limit is linked to your IP address, even if you are logged
|
limit is linked to your IP address, even if you are logged
|
||||||
@@ -77,7 +83,9 @@ import OtherPlans from "./OtherPlans.svelte";
|
|||||||
</div>
|
</div>
|
||||||
<div class="feature_cell pro_feat">
|
<div class="feature_cell pro_feat">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<span slot="label" class="bold">4 TB per 30 days</span>
|
{#snippet label()}
|
||||||
|
<span class="bold">4 TB per 30 days</span>
|
||||||
|
{/snippet}
|
||||||
<p>
|
<p>
|
||||||
The transfer limit is used for downloading, sharing and
|
The transfer limit is used for downloading, sharing and
|
||||||
hotlinking files.
|
hotlinking files.
|
||||||
@@ -89,7 +97,9 @@ import OtherPlans from "./OtherPlans.svelte";
|
|||||||
</div>
|
</div>
|
||||||
<div class="feature_cell prepaid_feat">
|
<div class="feature_cell prepaid_feat">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<span slot="label" class="bold">€1 per TB transferred</span>
|
{#snippet label()}
|
||||||
|
<span class="bold">€1 per TB transferred</span>
|
||||||
|
{/snippet}
|
||||||
<p>
|
<p>
|
||||||
Prepaid does not have a transfer limit, instead you are
|
Prepaid does not have a transfer limit, instead you are
|
||||||
charged for what you use at a rate of €1 per terabyte
|
charged for what you use at a rate of €1 per terabyte
|
||||||
@@ -132,7 +142,9 @@ import OtherPlans from "./OtherPlans.svelte";
|
|||||||
</div>
|
</div>
|
||||||
<div class="feature_cell free_feat">
|
<div class="feature_cell free_feat">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<span slot="label" class="bold">120 days (4 months)</span>
|
{#snippet label()}
|
||||||
|
<span class="bold">120 days (4 months)</span>
|
||||||
|
{/snippet}
|
||||||
<p>
|
<p>
|
||||||
Files expire when they have not been downloaded in the last
|
Files expire when they have not been downloaded in the last
|
||||||
120 days. A download is counted when more than 10% of the
|
120 days. A download is counted when more than 10% of the
|
||||||
@@ -142,7 +154,9 @@ import OtherPlans from "./OtherPlans.svelte";
|
|||||||
</div>
|
</div>
|
||||||
<div class="feature_cell pro_feat">
|
<div class="feature_cell pro_feat">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<span slot="label" class="bold">240 days (8 months)</span>
|
{#snippet label()}
|
||||||
|
<span class="bold">240 days (8 months)</span>
|
||||||
|
{/snippet}
|
||||||
<p>
|
<p>
|
||||||
The Pro plan has 240 day file expiry. The same rules apply
|
The Pro plan has 240 day file expiry. The same rules apply
|
||||||
as the free plan. Higher Patreon subscription plans are
|
as the free plan. Higher Patreon subscription plans are
|
||||||
@@ -152,7 +166,9 @@ import OtherPlans from "./OtherPlans.svelte";
|
|||||||
</div>
|
</div>
|
||||||
<div class="feature_cell prepaid_feat">
|
<div class="feature_cell prepaid_feat">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<span slot="label" class="bold">Files do not expire</span>
|
{#snippet label()}
|
||||||
|
<span class="bold">Files do not expire</span>
|
||||||
|
{/snippet}
|
||||||
<p>
|
<p>
|
||||||
Files don't expire while your Prepaid plan is active. If
|
Files don't expire while your Prepaid plan is active. If
|
||||||
your credit runs out you have one week to top up your
|
your credit runs out you have one week to top up your
|
||||||
@@ -271,20 +287,21 @@ import OtherPlans from "./OtherPlans.svelte";
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vertical_scroll {
|
.horizontal_scroll {
|
||||||
overflow-x: scroll;
|
overflow-x: scroll;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid {
|
.grid {
|
||||||
display: inline-grid;
|
display: grid;
|
||||||
grid-auto-flow: row;
|
grid-auto-flow: row;
|
||||||
grid-template-columns: 9em 1fr 1fr 1fr;
|
grid-template-columns: 9em 1fr 1fr 1fr;
|
||||||
min-width: 40em;
|
min-width: 50em;
|
||||||
max-width: 70em;
|
max-width: 70em;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
margin: 10px;
|
margin: auto;
|
||||||
}
|
}
|
||||||
.grid > div {
|
.grid > div {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@@ -5,7 +5,7 @@ import { drop_target } from "lib/DropTarget";
|
|||||||
import AddressReputation from "./AddressReputation.svelte";
|
import AddressReputation from "./AddressReputation.svelte";
|
||||||
import FeatureTable from "./FeatureTable.svelte";
|
import FeatureTable from "./FeatureTable.svelte";
|
||||||
import GetStarted from "./GetStarted.svelte";
|
import GetStarted from "./GetStarted.svelte";
|
||||||
import UploadWidget from "./UploadWidget.svelte";
|
import Pricing from "./Pricing.svelte";
|
||||||
|
|
||||||
let upload_widget
|
let upload_widget
|
||||||
</script>
|
</script>
|
||||||
@@ -51,6 +51,7 @@ let upload_widget
|
|||||||
Bullet lists
|
Bullet lists
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<Pricing/>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -142,26 +143,18 @@ let upload_widget
|
|||||||
<FeatureTable/>
|
<FeatureTable/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Footer nobg/>
|
<svelte:head>
|
||||||
|
<style>
|
||||||
<style>
|
body {
|
||||||
:global(.page_body) {
|
background-image: url("/res/img/catspaw.webp");
|
||||||
background-image: url("/res/img/inflating_star.webp");
|
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-attachment: fixed;
|
background-attachment: fixed;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
}
|
|
||||||
.page_content {
|
|
||||||
margin-top: 16px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
@media (max-width: 1100px) {
|
|
||||||
.page_content {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
}
|
||||||
}
|
</style>
|
||||||
|
</svelte:head>
|
||||||
|
<style>
|
||||||
header {
|
header {
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
|
@@ -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>
|
<script>
|
||||||
|
import { run } from 'svelte/legacy';
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import Euro from "util/Euro.svelte";
|
import Euro from "util/Euro.svelte";
|
||||||
import ProgressBar from "util/ProgressBar.svelte";
|
import ProgressBar from "util/ProgressBar.svelte";
|
||||||
|
|
||||||
let pixeldrain_storage = 0
|
let pixeldrain_storage = $state(0)
|
||||||
let pixeldrain_egress = 0
|
let pixeldrain_egress = $state(0)
|
||||||
let pixeldrain_total = 0
|
let pixeldrain_total = $state(0)
|
||||||
let backblaze_storage = 0
|
let backblaze_storage = $state(0)
|
||||||
let backblaze_egress = 0
|
let backblaze_egress = $state(0)
|
||||||
let backblaze_api = 0
|
let backblaze_api = $state(0)
|
||||||
let backblaze_total = 0
|
let backblaze_total = $state(0)
|
||||||
let wasabi_storage = 0
|
let wasabi_storage = $state(0)
|
||||||
let wasabi_total = 0
|
let wasabi_total = $state(0)
|
||||||
let price_amazon = 0
|
let price_amazon = 0
|
||||||
let price_azure = 0
|
let price_azure = 0
|
||||||
let price_google = 0
|
let price_google = 0
|
||||||
let price_max = 0
|
let price_max = $state(0)
|
||||||
|
|
||||||
let storage = 10 // TB
|
let storage = $state(10) // TB
|
||||||
let egress = 10 // TB
|
let egress = $state(10) // TB
|
||||||
let avg_file_size = 1000 // kB
|
let avg_file_size = $state(1000) // kB
|
||||||
|
|
||||||
$: {
|
run(() => {
|
||||||
pixeldrain_storage = storage * 4
|
pixeldrain_storage = storage * 4
|
||||||
pixeldrain_egress = egress * 1
|
pixeldrain_egress = egress * 1
|
||||||
pixeldrain_total = pixeldrain_storage + pixeldrain_egress
|
pixeldrain_total = pixeldrain_storage + pixeldrain_egress
|
||||||
@@ -43,7 +44,7 @@ $: {
|
|||||||
// price_google = (storage * 20) + (egress * 20)
|
// price_google = (storage * 20) + (egress * 20)
|
||||||
|
|
||||||
price_max = Math.max(pixeldrain_total, backblaze_total, wasabi_total, price_amazon, price_azure, price_google)
|
price_max = Math.max(pixeldrain_total, backblaze_total, wasabi_total, price_amazon, price_azure, price_google)
|
||||||
}
|
});
|
||||||
|
|
||||||
onMount(() => {})
|
onMount(() => {})
|
||||||
</script>
|
</script>
|
||||||
|
@@ -1,4 +1,8 @@
|
|||||||
<script>export let style;</script>
|
<script lang="ts">
|
||||||
|
let { style }: {
|
||||||
|
style: string;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
<svg style={style} width="24" height="24" viewBox="0 -28.5 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
<svg style={style} width="24" height="24" viewBox="0 -28.5 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
||||||
<g>
|
<g>
|
||||||
|
@@ -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">
|
<svg style={style} role="img" viewBox="0 0 24 24" width="24" height="24" xmlns="http://www.w3.org/2000/svg">
|
||||||
<title>GitHub</title>
|
<title>GitHub</title>
|
||||||
|
@@ -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">
|
<svg style={style} xmlns="http://www.w3.org/2000/svg" version="1.1" width="24" height="24" viewBox="0 0 987.525 987.525">
|
||||||
<path fill="currentColor" d="M132.138,855.425c43,43,93.2,76.301,149.3,99.101c54.1,21.899,111.1,33,169.6,33s115.601-11.101,169.601-33
|
<path fill="currentColor" d="M132.138,855.425c43,43,93.2,76.301,149.3,99.101c54.1,21.899,111.1,33,169.6,33s115.601-11.101,169.601-33
|
||||||
|