UI overhaul, import fixes, fix user store

This commit is contained in:
2026-07-30 17:07:46 +02:00
parent 773149169b
commit 4b8bd11a4c
75 changed files with 1106 additions and 744 deletions

12
go.mod
View File

@@ -3,6 +3,7 @@ module fornaxian.tech/nova_web
go 1.26
replace (
fornaxian.tech/nova_server => ../nova_server
fornaxian.tech/pixeldrain_api_client => ../pixeldrain_api_client
fornaxian.tech/util => ../util
)
@@ -10,7 +11,7 @@ replace (
require (
fornaxian.tech/config v0.0.0-20211108212237-6133aed90586
fornaxian.tech/log v0.0.0-20211102185326-552e9b1f8640
fornaxian.tech/pixeldrain_api_client v0.0.0-20260124133150-a94861b84e11
fornaxian.tech/pixeldrain_api_client v0.0.0-20260701091325-dc9629676a79
fornaxian.tech/util v0.0.0-20260123171803-02d15be63d16
github.com/julienschmidt/httprouter v1.3.0
github.com/russross/blackfriday/v2 v2.1.0
@@ -18,8 +19,13 @@ require (
require (
github.com/BurntSushi/toml v1.6.0 // indirect
github.com/apache/cassandra-gocql-driver/v2 v2.1.1 // indirect
github.com/apache/cassandra-gocql-driver/v2 v2.1.2 // indirect
github.com/golang/snappy v1.0.0 // indirect
golang.org/x/crypto v0.51.0 // indirect
github.com/mattn/go-colorable v0.1.15 // indirect
github.com/mattn/go-isatty v0.0.24 // indirect
github.com/pierrec/lz4/v4 v4.1.27 // indirect
github.com/stretchr/testify v1.11.1 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.54.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
)

32
go.sum
View File

@@ -4,36 +4,36 @@ fornaxian.tech/log v0.0.0-20211102185326-552e9b1f8640 h1:UPDxJwLRCfh/cv80UMSanzm
fornaxian.tech/log v0.0.0-20211102185326-552e9b1f8640/go.mod h1:sN82qMToeHhP2u3ehvrcE8y1IudRZJAZO9yG5OBYblo=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/apache/cassandra-gocql-driver/v2 v2.1.1 h1:DjmtmSRoUuIFswczA8LPBppRb8T9vxqNP5Txc/lQxhE=
github.com/apache/cassandra-gocql-driver/v2 v2.1.1/go.mod h1:QH/asJjB3mHvY6Dot6ZKMMpTcOrWJ8i9GhsvG1g0PK4=
github.com/apache/cassandra-gocql-driver/v2 v2.1.2 h1:lu/p0Db2av18enHJvWJQoChLssI0P+AR06STq4VdvCc=
github.com/apache/cassandra-gocql-driver/v2 v2.1.2/go.mod h1:QH/asJjB3mHvY6Dot6ZKMMpTcOrWJ8i9GhsvG1g0PK4=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pierrec/lz4/v4 v4.1.8 h1:ieHkV+i2BRzngO4Wd/3HGowuZStgq6QkPsD1eolNAO4=
github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
github.com/pierrec/lz4/v4 v4.1.27 h1:+PhzhWDrjRj89TH2sw43nE3+4+W8lSxIuQadEHZyjUk=
github.com/pierrec/lz4/v4 v4.1.27/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 410 KiB

View File

@@ -78,9 +78,7 @@ footer {
}
footer {
background-color: var(--shaded_background);
backdrop-filter: blur(4px);
border-top: 1px solid var(--separator);
}
footer>.footer_content {
@@ -137,6 +135,8 @@ pre>code {
.page_content {
background: var(--body_background);
text-align: center;
margin: 4px;
border-radius: 8px;
}
@media (max-width: 1000px) {
@@ -163,6 +163,14 @@ section {
clear: both;
}
.bubble {
background: var(--body_background);
margin: 4px;
border-radius: 8px;
border-top: 1px solid var(--separator);
border-left: 1px solid var(--separator);
}
/* Page contents */
.page_navigation a {
@@ -678,11 +686,11 @@ option:checked {
}
::-webkit-scrollbar-corner {
background-color: var(--body_color);
background-color: var(--background_color);
}
::-webkit-scrollbar-button:single-button {
background-color: var(--body_color);
background-color: var(--background_color);
display: block;
border-style: solid;
height: 10px;
@@ -692,5 +700,5 @@ option:checked {
/* Firefox Scrollbar */
* {
scrollbar-color: var(--scrollbar_foreground_color) var(--body_color);
scrollbar-color: var(--scrollbar_foreground_color) var(--background_color);
}

View File

@@ -24,7 +24,6 @@
{{ template "opengraph" .OGData }}
<script>
window.user = {{.User}};
window.api_endpoint = '{{.APIEndpoint}}';
window.server_hostname = "{{.Hostname}}";
</script>

View File

@@ -2494,9 +2494,9 @@
]
},
"node_modules/@sveltejs/acorn-typescript": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.6.tgz",
"integrity": "sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ==",
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.11.tgz",
"integrity": "sha512-LFuZUkjJ9iF7JZye/aG5XM0SFcQ5VyL0oVX4WJ9dc0Va3R3s0OauX1BESVCb+YN/ol8TAfqGDDAQsTG627Y5kw==",
"license": "MIT",
"peerDependencies": {
"acorn": "^8.9.0"
@@ -2569,6 +2569,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT"
},
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
@@ -2635,9 +2641,9 @@
}
},
"node_modules/aria-query": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
"integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==",
"license": "Apache-2.0",
"engines": {
"node": ">= 0.4"
@@ -2966,6 +2972,12 @@
"node": ">=0.10.0"
}
},
"node_modules/devalue": {
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.2.tgz",
"integrity": "sha512-DObPPAfdtFbXjxLqK8s2Xk9ZuWz5+ZoFEhC7J76es4GU/rEiXwHTmbImoCdyoCOcBH1UF3+Cz6Z2sYD4hyl5TA==",
"license": "MIT"
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -3045,12 +3057,20 @@
"license": "MIT"
},
"node_modules/esrap": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.0.tgz",
"integrity": "sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.3.0.tgz",
"integrity": "sha512-GQ/7RN8uOtEfNpzZzBMTzW9JBcX42oaSVtPzdF+6cEL8pqIL094iUpr9jzYGn4O4P/1S60dJ6izyT8F4LYARng==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15"
},
"peerDependencies": {
"@typescript-eslint/types": "^8.2.0"
},
"peerDependenciesMeta": {
"@typescript-eslint/types": {
"optional": true
}
}
},
"node_modules/estree-walker": {
@@ -4037,21 +4057,23 @@
}
},
"node_modules/svelte": {
"version": "5.39.11",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.39.11.tgz",
"integrity": "sha512-8MxWVm2+3YwrFbPaxOlT1bbMi6OTenrAgks6soZfiaS8Fptk4EVyRIFhJc3RpO264EeSNwgjWAdki0ufg4zkGw==",
"version": "5.56.8",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.8.tgz",
"integrity": "sha512-PY8LOw7xP6c8IOiVqdo0sbbZVYhXRSfklOQLAUyGBKqjTX0wx/z4l/9J+PmBpmlLnxzEb1NqltxQ5/wZme/Cmg==",
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.4",
"@jridgewell/sourcemap-codec": "^1.5.0",
"@sveltejs/acorn-typescript": "^1.0.5",
"@sveltejs/acorn-typescript": "^1.0.10",
"@types/estree": "^1.0.5",
"@types/trusted-types": "^2.0.7",
"acorn": "^8.12.1",
"aria-query": "^5.3.1",
"aria-query": "5.3.1",
"axobject-query": "^4.1.0",
"clsx": "^2.1.1",
"devalue": "^5.8.1",
"esm-env": "^1.2.1",
"esrap": "^2.1.0",
"esrap": "^2.2.12",
"is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",

View File

@@ -33,7 +33,7 @@ const update_chart = async (timestamps: string[], metrics: {[key: string]: numbe
chart.data().datasets[0] = {
label: "sum",
data: create_sum_dataset(metrics),
borderWidth: 1.5,
borderWidth: 1,
pointRadius: 0,
borderColor: "#ff0000",
backgroundColor: "#ff0000",
@@ -41,7 +41,7 @@ const update_chart = async (timestamps: string[], metrics: {[key: string]: numbe
chart.data().datasets[1] = {
label: "average",
data: create_avg_dataset(metrics),
borderWidth: 1.5,
borderWidth: 1,
pointRadius: 0,
borderColor: "#00ff00",
backgroundColor: "#00ff00",

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { fs_encode_path } from "lib/FilesystemAPI.svelte";
import { fs_encode_path } from "lib/FilesystemAPI";
import { path_link, type FSNavigator } from "./FSNavigator";
import FileMenu from "./filemanager/FileMenu.svelte";
import EditWindow from "./edit_window/EditWindow.svelte";

View File

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

View File

@@ -1,6 +1,6 @@
import { loading_finish, loading_start } from "lib/Loading";
import { fs_get_node, fs_encode_path, fs_split_path } from "../lib/FilesystemAPI.svelte";
import type { FSNode, FSPath, FSPermissions, FSContext } from "../lib/FilesystemAPI.svelte";
import { fs_get_node, fs_encode_path, fs_split_path } from "lib/FilesystemAPI";
import type { FSNode, FSPath, FSPermissions, FSContext } from "lib/FilesystemAPI";
export class FSNavigator {
// Parts of the raw API response

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { onMount } from "svelte";
import { formatDataVolume, formatThousands } from "util/Formatting"
import { fs_path_url } from "lib/FilesystemAPI.svelte";
import { fs_path_url } from "lib/FilesystemAPI";
import type { FSNavigator } from "./FSNavigator";
let {

View File

@@ -35,15 +35,16 @@ const open = () => dialog.open(button.getBoundingClientRect())
<NovaLogo style="height: 1.6em; width: 1.6em;"/>
{/if}
<span class="button_username" class:hide_name>
{#if $user !== null}
{$user.username === "" ? no_login_label : $user.username}
{/if}
</span>
</button>
</div>
<Dialog bind:this={dialog}>
<div class="menu">
{#if $user.username !== undefined && $user.username !== ""}
{#if $user !== null}
<Button link_href="/user" link_target={target} icon="dashboard" label="Dashboard" />
<div class="separator"></div>
@@ -63,12 +64,7 @@ const open = () => dialog.open(button.getBoundingClientRect())
</div>
<div class="separator"></div>
{#if $user.subscription.filesystem_access}
<Button link_href="/d/me" link_target={target} icon="folder" label="My Filesystem"/>
{:else}
<Button link_href="/#pro" link_target={target} icon="star" label="Get Premium"/>
{/if}
<Button link_href="/filesystem" link_target={target} icon="description" label="Filesystem Guide"/>
<div class="separator"></div>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { onMount, tick } from "svelte";
import { fs_search, fs_encode_path, fs_thumbnail_url, FSNode } from "lib/FilesystemAPI.svelte";
import { fs_search, fs_encode_path, fs_thumbnail_url, FSNode } from "lib/FilesystemAPI";
import type { FSNavigator } from "filesystem/FSNavigator";
import { loading_finish, loading_start } from "lib/Loading";
import Modal from "util/Modal.svelte";

View File

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

View File

@@ -3,7 +3,7 @@ import { copy_text } from "util/Util";
import FileStats from "./FileStats.svelte";
import type { FSNavigator } from "./FSNavigator";
import EditWindow from "./edit_window/EditWindow.svelte";
import { fs_share_url, path_is_shared } from "lib/FilesystemAPI.svelte";
import { fs_share_url, path_is_shared } from "lib/FilesystemAPI";
import ShareDialog from "./ShareDialog.svelte";
import { bookmark_add, bookmark_del, bookmarks_store, is_bookmark } from "lib/Bookmarks";
@@ -36,7 +36,7 @@ export const copy_link = () => {
}
</script>
<div class="toolbar">
<div class="toolbar bubble">
<FileStats nav={nav} bind:expanded={toolbar_expanded}/>
<div class="grid" class:hide={!toolbar_expanded}>
@@ -110,11 +110,10 @@ export const copy_link = () => {
overflow-x: hidden;
overflow-y: hidden;
transition: max-height 0.3s;
border-top: 1px solid var(--separator);
background: var(--shaded_background);
position: sticky;
bottom: 0;
bottom: 4px;
}
.grid {
display: grid;

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import Button from "layout/Button.svelte";
import type { FSPermissions, NodeOptions } from "lib/FilesystemAPI.svelte";
import type { FSPermissions, NodeOptions } from "lib/FilesystemAPI";
import PermissionButton from "./PermissionButton.svelte";
let {
@@ -60,7 +60,6 @@ const del_password = (pass: string) => {
</div>
</fieldset>
<fieldset>
<legend>User permissions</legend>
<p>

View File

@@ -2,7 +2,7 @@ import parse from "pure-color/parse";
import rgb2hsl from "pure-color/convert/rgb2hsl";
import hsl2rgb from "pure-color/convert/hsl2rgb";
import rgb2hex from "pure-color/convert/rgb2hex";
import type { FSNode, FSNodeProperties } from "lib/FilesystemAPI.svelte";
import type { FSNode, FSNodeProperties } from "lib/FilesystemAPI";
const text_contrast = 85
const body_alpha = 0.9

View File

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

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { fs_rename, fs_update, type FSNode, type FSPermissions, type NodeOptions } from "lib/FilesystemAPI.svelte";
import { fs_rename, fs_update, type FSNode, type FSPermissions, type NodeOptions } from "lib/FilesystemAPI";
import Modal from "util/Modal.svelte";
import BrandingOptions from "./BrandingOptions.svelte";
import { branding_from_props } from "./Branding";

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ import { run } from 'svelte/legacy';
import { domain_url } from "util/Util";
import CopyButton from "layout/CopyButton.svelte";
import { formatDate } from "util/Formatting";
import { type FSNode, type NodeOptions } from "lib/FilesystemAPI.svelte";
import { type FSNode, type NodeOptions } from "lib/FilesystemAPI";
import AccessControl from "./AccessControl.svelte";
let {

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import type { FSNodeProperties } from "lib/FilesystemAPI.svelte";
import type { FSNodeProperties } from "lib/FilesystemAPI";
let {
properties = $bindable({} as FSNodeProperties)
@@ -88,7 +88,7 @@ const themes = [
brand_card_color: "#313244",
}, {
name: "Nova",
brand_input_color: "#170025",
brand_input_color: "#28033f",
brand_highlight_color: "#57e389",
brand_danger_color: "#ed333b",
brand_background_color: "#0d0015",

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { fs_encode_path, fs_node_icon } from "lib/FilesystemAPI.svelte"
import { fs_encode_path, fs_node_icon } from "lib/FilesystemAPI"
import type { FSNavigator } from "filesystem/FSNavigator";
import { FileAction, type FileActionHandler } from "./FileManagerLib";

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { onMount } from "svelte";
import { fs_mkdir } from "lib/FilesystemAPI.svelte";
import { fs_mkdir } from "lib/FilesystemAPI";
import Button from "layout/Button.svelte";
import type { FSNavigator } from "filesystem/FSNavigator";
import { loading_finish, loading_start } from "lib/Loading";

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { fs_rename, fs_trash, FSNode } from "lib/FilesystemAPI.svelte"
import { fs_rename, fs_trash, FSNode } from "lib/FilesystemAPI"
import { onMount } from "svelte"
import CreateDirectory from "./CreateDirectory.svelte"
import ListView from "./ListView.svelte"
@@ -305,7 +305,7 @@ onMount(() => {
shadow: "var(--highlight_color) 0 0 10px 2px inset",
}}
>
<div class="width_container">
<div class="width_container bubble">
{#if mode === "viewing"}
<div class="toolbar">
<div class="toolbar_left">
@@ -430,11 +430,9 @@ onMount(() => {
position: sticky;
top: 0;
display: block;
width: 100%;
margin: auto;
margin-top: 0;
padding: 0;
background: var(--body_background);
/* backdrop-filter: blur(4px); */
}
.toolbar {
display: flex;

View File

@@ -4,7 +4,7 @@ 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_trash, type FSNode } from "lib/FilesystemAPI.svelte";
import { fs_trash, type FSNode } from "lib/FilesystemAPI";
import { loading_finish, loading_start } from "lib/Loading";
import { tick } from "svelte";

View File

@@ -5,7 +5,7 @@ import GalleryView from "./GalleryView.svelte"
import CompactView from "./CompactView.svelte"
import Modal from "util/Modal.svelte";
import { FSNavigator, path_link } from "filesystem/FSNavigator";
import { fs_encode_path, type FSNode } from "lib/FilesystemAPI.svelte";
import { fs_encode_path, type FSNode } from "lib/FilesystemAPI";
import { FileAction, type FileActionHandler } from "./FileManagerLib";
let nav = $state(new FSNavigator(false))

View File

@@ -1,5 +1,5 @@
<script lang="ts">
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 "lib/FilesystemAPI";
import type { FSNavigator } from "filesystem/FSNavigator";
import { FileAction, type FileActionHandler } from "./FileManagerLib";

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { formatDataVolume } from "util/Formatting";
import { fs_encode_path, fs_node_icon } from "lib/FilesystemAPI.svelte"
import { fs_encode_path, fs_node_icon } from "lib/FilesystemAPI"
import type { FSNavigator } from "filesystem/FSNavigator";
import SortButton from "layout/SortButton.svelte";
import { FileAction, type FileActionHandler } from "./FileManagerLib";

View File

@@ -0,0 +1,260 @@
<script lang="ts">
import { formatDataVolume, formatDate } from "util/Formatting";
import { fs_check_response, fs_encode_path, fs_get_trash, fs_node_icon, fs_path_url, type TrashEntry } from "lib/FilesystemAPI";
import { loading_finish, loading_start } from "lib/Loading";
import { onMount } from "svelte";
let { root_path = "/me" }: { root_path?: string } = $props();
let entries: TrashEntry[] = $state([]);
let error: string = $state("");
let loaded = $state(false);
const load = async () => {
loaded = false;
entries = [];
try {
loading_start();
entries = await fs_get_trash(root_path)
error = "";
} catch (err: any) {
error = err?.message ?? JSON.stringify(err);
} finally {
loaded = true;
loading_finish();
}
};
const delete_entry = async (entry: TrashEntry) => {
if (!confirm(`Permanently delete "${entry.node.name}"?`)) return;
try {
loading_start();
await fs_check_response(
await fetch(fs_path_url(entry.node.path) + "?trash", { method: "DELETE" })
);
entries = entries.filter(e => e.node.id !== entry.node.id);
} catch (err: any) {
alert(err?.message ?? JSON.stringify(err));
} finally {
loading_finish();
}
};
const restore_entry = async (entry: TrashEntry) => {
if (!entry.original_path) {
alert("Original path is not known, cannot restore this file.");
return;
}
try {
loading_start();
const form = new FormData();
form.append("action", "rename");
form.append("target", "/me" + entry.original_path);
await fs_check_response(
await fetch(fs_path_url(entry.node.path), { method: "POST", body: form })
);
entries = entries.filter(e => e.node.id !== entry.node.id);
} catch (err: any) {
alert(err?.message ?? JSON.stringify(err));
} finally {
loading_finish();
}
};
const empty_trash = async () => {
if (!confirm("Permanently delete all items in the trash?")) return;
try {
loading_start();
await fs_check_response(
await fetch(fs_path_url(root_path) + "?empty_trash", { method: "DELETE" })
);
entries = [];
} catch (err: any) {
alert(err?.message ?? JSON.stringify(err));
} finally {
loading_finish();
}
};
onMount(load);
</script>
<div class="trash_container">
<div class="header">
<h2 class="title"><i class="icon">delete</i> Trash</h2>
<div class="actions">
<button onclick={load}>
<i class="icon">refresh</i>
<span>Refresh</span>
</button>
{#if entries.length > 0}
<button class="danger" onclick={empty_trash}>
<i class="icon">delete_forever</i>
<span>Empty trash</span>
</button>
{/if}
</div>
</div>
{#if error}
<p class="error">{error}</p>
{:else if loaded && entries.length === 0}
<p class="empty">The trash is empty.</p>
{:else if entries.length > 0}
<table class="trash_table">
<thead>
<tr>
<td></td>
<td>Name</td>
<td class="hide_small">Original location</td>
<td class="hide_small">Deleted</td>
<td class="hide_small">Size</td>
<td></td>
</tr>
</thead>
<tbody>
{#each entries as entry (entry.node.id)}
<tr class="entry">
<td class="icon_cell">
<img
src={fs_node_icon(entry.node, 32, 32)}
class="node_icon"
alt="icon"
/>
</td>
<td class="name_cell">
<a class="entry_link" href={"/d" + fs_encode_path(entry.node.path)}>
{entry.node.name}
</a>
</td>
<td class="hide_small original_path">
{entry.original_path || "—"}
</td>
<td class="hide_small date_cell">
{entry.deletion_date ? formatDate(entry.deletion_date) : "—"}
</td>
<td class="hide_small size_cell">
{#if entry.node.type === "file"}
{formatDataVolume(entry.node.file_size, 3)}
{/if}
</td>
<td class="button_cell">
<div class="row_actions">
<button title="Restore to original location" onclick={() => restore_entry(entry)}>
<i class="icon">restore_from_trash</i>
</button>
<button class="danger" title="Delete permanently" onclick={() => delete_entry(entry)}>
<i class="icon">delete_forever</i>
</button>
</div>
</td>
</tr>
{/each}
</tbody>
</table>
{/if}
</div>
<style>
.trash_container {
max-width: 1200px;
margin: auto;
padding: 1em;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.5em;
margin-bottom: 1em;
}
.title {
display: flex;
align-items: center;
gap: 0.3em;
margin: 0;
font-size: 1.2em;
border: none;
}
.actions {
display: flex;
gap: 0.5em;
}
.error {
color: var(--danger_color, #c0392b);
}
.empty {
color: var(--body_text_color);
opacity: 0.6;
}
.trash_table {
width: 100%;
border-collapse: collapse;
background: var(--body_background);
}
.trash_table td {
padding: 0;
}
.entry {
cursor: default;
border-top: 1px solid var(--separator);
}
.entry:hover {
background: var(--input_hover_background);
}
.icon_cell {
width: 2.5em;
padding: 0.3em 0.3em 0.3em 0.5em;
}
.node_icon {
display: block;
width: 2em;
height: 2em;
object-fit: contain;
}
.name_cell {
padding: 0.3em 0.5em;
word-break: break-all;
}
.entry_link {
color: var(--body_text_color);
text-decoration: none;
}
.entry_link:hover {
text-decoration: underline;
}
.original_path {
padding: 0.3em 0.5em;
color: var(--body_text_color);
opacity: 0.7;
font-size: 0.85em;
word-break: break-all;
}
.date_cell {
padding: 0.3em 0.5em;
white-space: nowrap;
font-size: 0.85em;
}
.size_cell {
padding: 0.3em 0.5em;
white-space: nowrap;
font-size: 0.85em;
text-align: right;
}
.button_cell {
padding: 0.3em 0.5em;
}
.row_actions {
display: flex;
gap: 0.25em;
}
.danger {
color: var(--danger_color, #c0392b);
}
@media (max-width: 600px) {
.hide_small {
display: none;
}
}
</style>

View File

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

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { onMount } from 'svelte'
import { fs_path_url, fs_encode_path, fs_node_icon, FSNode } from "lib/FilesystemAPI.svelte"
import { fs_path_url, fs_encode_path, fs_node_icon, FSNode } from "lib/FilesystemAPI"
import TextBlock from "layout/TextBlock.svelte"
import type { FSNavigator } from 'filesystem/FSNavigator';
@@ -64,7 +64,7 @@ onMount(() => {
{@render children?.()}
<div bind:this={background_div} class="background_div">
<TextBlock width="auto">
<TextBlock>
<audio
bind:this={player}
class="player"
@@ -111,6 +111,7 @@ onMount(() => {
background-repeat: no-repeat;
background-size: cover;
min-height: 100%;
border-radius: 8px;
}
.player {
width: 100%;

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import type { FSNode } from 'lib/FilesystemAPI.svelte';
import type { FSNode } from 'lib/FilesystemAPI';
import { run } from 'svelte/legacy';
let { path = [] }: {path: FSNode[]} = $props();

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import IconBlock from "layout/IconBlock.svelte";
import { fs_thumbnail_url, FSNode } from "lib/FilesystemAPI.svelte";
import { fs_thumbnail_url, FSNode } from "lib/FilesystemAPI";
import TextBlock from "layout/TextBlock.svelte"
import { formatDataVolume, formatDate } from "util/Formatting";
@@ -17,12 +17,24 @@ let {
{@render children?.()}
<h1>{node.name}</h1>
<IconBlock icon_href={fs_thumbnail_url(node.path, 256, 256)}>
Type: {node.file_type}<br/>
Size: {formatDataVolume(node.file_size, 3)}<br/>
Upload date: {formatDate(node.created, true, true, false)}
{node.name}<br/>
<table>
<tbody>
<tr>
<td>Type</td>
<td>{node.file_type}</td>
</tr>
<tr>
<td>Size</td>
<td>{formatDataVolume(node.file_size, 3)}</td>
</tr>
<tr>
<td>Upload date</td>
<td>{formatDate(node.created, true, true, false)}</td>
</tr>
</tbody>
</table>
<hr/>
<button class="button_highlight" onclick={() => node.download()}>
<i class="icon">download</i>
@@ -79,9 +91,12 @@ let {
{/if}
<style>
h1 {
text-shadow: 1px 1px 3px var(--shadow_color);
line-break: anywhere;
table {
width: auto;
}
td {
padding: 0;
padding-right: 1em;
}
.icon {
display: inline-block;

View File

@@ -1,8 +1,9 @@
<script lang="ts">
import { onMount, tick } from "svelte";
import Spinner from "util/Spinner.svelte";
import { fs_node_type } from "lib/FilesystemAPI.svelte";
import { fs_node_type } from "lib/FilesystemAPI";
import FileManager from "filesystem/filemanager/FileManager.svelte";
import TrashBin from "filesystem/filemanager/TrashBin.svelte";
import Audio from "./Audio.svelte";
import File from "./File.svelte";
import Image from "./Image.svelte";
@@ -88,6 +89,8 @@ export const seek = (delta: number) => {
<div class="center">
<Spinner/>
</div>
{:else if viewer_type === "dir" && $nav.base.name === ".Trash"}
<TrashBin root_path={$nav.path[$nav.path.length - 2].path}/>
{:else if viewer_type === "dir"}
<FileManager nav={nav} upload_widget={upload_widget} edit_window={edit_window} search_bar={search_bar}>
<CustomBanner path={$nav.path}/>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { swipe_nav } from "lib/SwipeNavigate";
import { fs_path_url } from "lib/FilesystemAPI.svelte";
import { fs_path_url } from "lib/FilesystemAPI";
import type { FSNavigator } from "filesystem/FSNavigator";
import { loading_finish, loading_start } from "lib/Loading";

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { fs_path_url, FSNode } from "lib/FilesystemAPI.svelte";
import { fs_path_url, FSNode } from "lib/FilesystemAPI";
let { node }: { node: FSNode } = $props();
</script>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { tick } from "svelte";
import { fs_path_url, type FSNode } from "lib/FilesystemAPI.svelte";
import { fs_path_url, type FSNode } from "lib/FilesystemAPI";
let {
node,

View File

@@ -18,7 +18,7 @@ import { formatDate } from "util/Formatting"
import TorrentItem from "./TorrentItem.svelte"
import IconBlock from "layout/IconBlock.svelte";
import TextBlock from "layout/TextBlock.svelte"
import { fs_node_icon, fs_path_url, FSNode } from "lib/FilesystemAPI.svelte";
import { fs_node_icon, fs_path_url, FSNode } from "lib/FilesystemAPI";
import CopyButton from "layout/CopyButton.svelte";
import { loading_finish, loading_start } from "lib/Loading";

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { onMount, tick } from "svelte";
import { video_position } from "lib/VideoPosition";
import { fs_path_url, FSNode } from "lib/FilesystemAPI.svelte";
import { fs_path_url, FSNode } from "lib/FilesystemAPI";
let {
node,

View File

@@ -12,7 +12,7 @@ import { formatDataVolume, formatDate } from "util/Formatting"
import ZipItem from "filesystem/viewers/ZipItem.svelte";
import IconBlock from "layout/IconBlock.svelte";
import TextBlock from "layout/TextBlock.svelte"
import { fs_node_icon, fs_path_url, FSNode } from "lib/FilesystemAPI.svelte";
import { fs_node_icon, fs_path_url, FSNode } from "lib/FilesystemAPI";
import { loading_finish, loading_start } from "lib/Loading";
let {
@@ -26,7 +26,7 @@ let {
let status = $state("loading")
let zip: ZipEntry = $state({size: 0} as ZipEntry)
let uncomp_size = 0
let uncomp_size = $state(0)
let comp_ratio = $state(0)
let archive_type = $state("")
let truncated = $state(false)
@@ -97,8 +97,6 @@ const recursive_size = (file: ZipEntry) => {
{@render children?.()}
<h1>{node.name}</h1>
<IconBlock icon_href={fs_node_icon(node, 256, 256)}>
{#if archive_type === "7z"}
This is a 7-zip archive. You will need
@@ -106,12 +104,26 @@ const recursive_size = (file: ZipEntry) => {
extract it<br/>
{/if}
Compressed size: {formatDataVolume(node.file_size, 3)}<br/>
{node.name}<br/>
<table>
<tbody>
<tr>
<td>Compressed size</td>
<td>{formatDataVolume(node.file_size, 3)}</td>
</tr>
{#if !truncated}
Uncompressed size: {formatDataVolume(zip.size, 3)} (Ratio: {comp_ratio.toFixed(2)}x)<br/>
<tr>
<td>Uncompressed size</td>
<td>{formatDataVolume(uncomp_size, 3)} (Ratio: {comp_ratio.toFixed(2)}x)</td>
</tr>
{/if}
Uploaded on: {formatDate(node.created, true, true, true)}
<br/>
<tr>
<td>Upload date</td>
<td>{formatDate(node.created, true, true, true)}</td>
</tr>
</tbody>
</table>
<hr/>
<button class="button_highlight" onclick={() => node.download()}>
<i class="icon">download</i>
<span>Download</span>
@@ -140,8 +152,11 @@ const recursive_size = (file: ZipEntry) => {
{/if}
<style>
h1 {
text-shadow: 1px 1px 3px var(--shadow_color);
line-break: anywhere;
table {
width: auto;
}
td {
padding: 0;
padding-right: 1em;
}
</style>

View File

@@ -1,4 +1,6 @@
<script>
<script lang="ts">
import { get_endpoint } from "lib/NovaAPI";
import { user } from "lib/UserStore";
import { onMount } from "svelte";
import Expandable from "util/Expandable.svelte";
import { formatDate } from "util/Formatting";
@@ -7,9 +9,9 @@ let result = $state(null);
onMount(async () => {
try {
const resp = await fetch(window.api_endpoint+"/misc/ip_address_reputation");
const resp = await fetch(get_endpoint()+"/misc/ip_address_reputation");
if(resp.status >= 400) {
throw new Error(resp.text());
throw new Error(await resp.text());
}
result = await resp.json()
} catch (err) {
@@ -66,7 +68,7 @@ onMount(async () => {
support@nova.storage. Please do not mail unless you have a
good reason. If you do not provide a valid reason why the ban
should be reversed your e-mail will be ignored. And do not
forget to put your username ({window.user.username}) in the
forget to put your username ({$user === null ? "" : $user.username}) in the
e-mail.
</p>
</Expandable>

View File

@@ -1,7 +1,8 @@
<script>
import Tooltip from "layout/Tooltip.svelte";
import Euro from "util/Euro.svelte";
import OtherPlans from "./OtherPlans.svelte";
import OtherPlans from "./SubscriptionPlans.svelte";
import { user } from "lib/UserStore";
</script>
<section>
@@ -222,7 +223,7 @@ import OtherPlans from "./OtherPlans.svelte";
Free
</div>
<div class="bottom_row pro_feat">
{#if window.user.subscription.id === "patreon_1"}
{#if $user !== null && $user.subscription.id === "patreon_1"}
You have this plan<br/>
Thank you for supporting Nova!
{:else}
@@ -240,7 +241,7 @@ import OtherPlans from "./OtherPlans.svelte";
{/if}
</div>
<div class="bottom_row prepaid_feat">
{#if window.user.username === ""}
{#if $user === null}
<!-- User is not logged in -->
Account required<br/>
<a href="/login?redirect=/user/prepaid/deposit" class="button button_highlight round">
@@ -257,16 +258,16 @@ import OtherPlans from "./OtherPlans.svelte";
No recurring payments
{:else}
<!-- User is logged in -->
{#if window.user.subscription.type === ""}
{#if $user.subscription.type === ""}
<a href="/user/prepaid/deposit#deposit" class="button button_highlight round">
Deposit credit
</a>
to activate Prepaid
{:else if window.user.subscription.type === "patreon"}
{:else if $user.subscription.type === "patreon"}
Patreon subscription active. Prepaid cannot be activated
{:else if window.user.subscription.type === "prepaid"}
{:else if $user.subscription.type === "prepaid"}
Prepaid subscription is active<br/>
Current balance <Euro amount={window.user.balance_micro_eur}/>
Current balance <Euro amount={$user.balance_micro_eur}/>
<br/>
<a href="/user/prepaid/deposit#deposit" class="button button_highlight round">
Top up my credit

View File

@@ -1,28 +1,28 @@
<script>
<script lang="ts">
import Checkout from "layout/checkout/Checkout.svelte";
import { user } from "lib/UserStore";
import LoginRegister from "login/LoginRegister.svelte";
import Euro from "util/Euro.svelte";
</script>
{#if window.user.username !== ""}
{#if $user !== null}
<div class="page_content">
<section>
{#if window.user.subscription.type === "patreon"}
{#if $user.subscription.type === "patreon"}
<p>
You already have a Patreon subscription active. You cannot use
Prepaid while that subscription is active.
</p>
{:else if window.user.subscription.type === "prepaid"}
{:else if $user.subscription.type === "prepaid"}
<p>
You already have a Prepaid subscription active. Your account
balance is <Euro amount={window.user.balance_micro_eur}/>. Use
balance is <Euro amount={$user.balance_micro_eur}/>. Use
the form below to top up your balance.
</p>
<Checkout/>
{:else}
<p>
You are currently logged in as '{window.user.username}'. Use the
You are currently logged in as '{$user.username}'. Use the
form below to activate prepaid on this account.
</p>
<Checkout/>

View File

@@ -3,9 +3,9 @@ import Menu from "filesystem/Menu.svelte";
import Footer from "layout/Footer.svelte";
import { drop_target } from "lib/DropTarget";
import AddressReputation from "./AddressReputation.svelte";
import FeatureTable from "./FeatureTable.svelte";
import GetStarted from "./GetStarted.svelte";
import Pricing from "./Pricing.svelte";
import SubscriptionPlans from "./SubscriptionPlans.svelte";
let upload_widget
</script>
@@ -28,26 +28,6 @@ let upload_widget
content delivery. We will store and serve your files at an extremely
competitive rate.
</p>
<div class="prices">
<div>
<div>Storage pricing</div>
<div>€ 4 / TB / month</div>
</div>
<div>
<div>Egress pricing</div>
<div>€ 1 / TB</div>
</div>
</div>
<h2>What Nova is good at</h2>
<ul>
<li>
Serving large files to millions of people worldwide
</li>
<li>
Storing files for less money than all the competition
</li>
</ul>
<Pricing/>
</section>
</div>
@@ -107,6 +87,11 @@ let upload_widget
</section>
</div>
<header>
<h1>Subscription plans</h1>
</header>
<SubscriptionPlans/>
<header>
<h1>Get started</h1>
</header>

View File

@@ -1,146 +0,0 @@
<div class="feat_table">
<div>
<div class="cell_background" style="background-image: url('/res/img/benefit_2.webp');">
Persistence<br/>
{#if window.user.subscription.id === "patreon_2"}
You have this plan<br/>
Thank you for supporting Nova!
{:else}
<a href="https://www.patreon.com/join/nova/checkout?rid=5291482" class="button button_highlight round">
€ 8
</a>
/ month
{/if}
</div>
<div class="feat_pro features_cell round_tr">
<div><span class="text_highlight">100 GB</span> max file size</div>
<div><span class="text_highlight">Files never expire</span></div>
<div><span class="text_highlight">8 TB</span> transfer limit</div>
<div><span class="text_highlight">4 TB</span> storage space</div>
<div>
<span class="text_highlight">File viewer
branding</span>: Set a custom theme and header,
footer and background images on the download pages
for your files
</div>
</div>
</div>
<div>
<div class="cell_background" style="background-image: url('/res/img/benefit_3.webp');">
Tenacity<br/>
{#if window.user.subscription.id === "patreon_3"}
You have this plan<br/>
Thank you for supporting Nova!
{:else}
<a href="https://www.patreon.com/join/nova/checkout?rid=5291516" class="button button_highlight round">
€ 16
</a>
/ month
{/if}
</div>
<div class="feat_pro features_cell">
<div><span class="text_highlight">100 GB</span> max file size</div>
<div><span class="text_highlight">Files never expire</span></div>
<div><span class="text_highlight">16 TB</span> transfer limit</div>
<div><span class="text_highlight">8 TB</span> storage space</div>
<div><span class="text_highlight">File viewer branding</span></div>
</div>
</div>
<div>
<div class="cell_background" style="background-image: url('/res/img/benefit_4.webp');">
Eternity<br/>
{#if window.user.subscription.id === "patreon_4"}
You have this plan<br/>
Thank you for supporting Nova!
{:else}
<a href="https://www.patreon.com/join/nova/checkout?rid=5291528" class="button button_highlight round">
€ 32
</a>
/ month
{/if}
</div>
<div class="feat_pro features_cell">
<div><span class="text_highlight">100 GB</span> max file size</div>
<div><span class="text_highlight">Files never expire</span></div>
<div><span class="text_highlight">32 TB</span> transfer limit</div>
<div><span class="text_highlight">16 TB</span> storage space</div>
<div><span class="text_highlight">File viewer branding</span></div>
</div>
</div>
<div>
<div class="cell_background" style="background-image: url('/res/img/benefit_6.webp');">
Infinity<br/>
{#if window.user.subscription.id === "patreon_6"}
You have this plan<br/>
Thank you for supporting Nova!
{:else}
<a href="https://www.patreon.com/join/nova/checkout?rid=6573749" class="button button_highlight round">
€ 64
</a>
/ month
{/if}
</div>
<div class="feat_pro features_cell round_br">
<div><span class="text_highlight">100 GB</span> max file size</div>
<div><span class="text_highlight">Files never expire</span></div>
<div><span class="text_highlight">64 TB</span> transfer limit</div>
<div><span class="text_highlight">32 TB</span> storage space</div>
<div><span class="text_highlight">File viewer branding</span></div>
</div>
</div>
</div>
<style>
.feat_table {
display: flex;
flex-direction: column;
}
.feat_table > div {
display: flex;
flex-direction: row;
min-height: 8em;
}
.feat_table > div > div:first-child {
flex: 0 0 20%;
max-width: 20%;
}
.feat_table > div > div {
flex: 1 1 0;
margin: 0.2em;
padding: 0.5em;
text-align: center;
word-wrap: break-word;
hyphens: auto;
}
.feat_table > div > .feat_pro {
background-color: var(--card_color);
border: 1px solid var(--highlight_color);
}
.text_highlight {
font-weight: bold;
}
.feat_table > div > .cell_background {
flex: 0 0 33%;
min-width: 33%;
border-top-left-radius: 0.5em;
border-bottom-left-radius: 0.5em;
background-position: center;
background-size: cover;
text-align: left;
font-size: 1.2em;
color: #ffffff;
text-shadow: 1px 1px 2px #000000;
}
.feat_table > div > div.round_tr { border-top-right-radius: 0.5em; }
.feat_table > div > div.round_br { border-bottom-right-radius: 0.5em; }
.features_cell {
display: flex;
flex-wrap: wrap;
}
.features_cell > div {
flex: 1 1 50%;
min-width: 220px;
}
</style>

View File

@@ -0,0 +1,226 @@
<script lang="ts">
import Euro from "util/Euro.svelte";
import { formatDataVolume } from "util/Formatting";
type Plan = {
title: string
monthly: number
egress: number
storage: number
checkout_cadence_1?: string
checkout_cadence_6?: string
checkout_cadence_12?: string
}
const gb = 1e9
const tb = gb*1000
const euro = 1e6
const plans: Plan[] = [
{
title: "Luna",
monthly: 2*euro,
egress: 2*tb,
storage: 500*gb,
// checkout_cadence_1: "https://example.com",
checkout_cadence_6: "https://www.creem.io/test/payment/prod_4WIbZB54NgOTVSGEe8kwpA",
checkout_cadence_12: "https://www.creem.io/test/payment/prod_4A8depiWgKgJ5h5tJD2Sv7",
}, {
title: "Io",
monthly: 4*euro,
egress: 4*tb,
storage: 1*tb,
// checkout_cadence_1: "https://example.com",
checkout_cadence_6: "https://example.com",
checkout_cadence_12: "https://example.com",
}, {
title: "Callisto",
monthly: 8*euro,
egress: 8*tb,
storage: 2*tb,
checkout_cadence_1: "https://example.com",
checkout_cadence_6: "https://example.com",
checkout_cadence_12: "https://example.com",
}, {
title: "Mercury",
monthly: 16*euro,
egress: 16*tb,
storage: 4*tb,
checkout_cadence_1: "https://example.com",
checkout_cadence_6: "https://example.com",
checkout_cadence_12: "https://example.com",
}, {
title: "Titan",
monthly: 32*euro,
egress: 32*tb,
storage: 8*tb,
checkout_cadence_1: "https://example.com",
checkout_cadence_6: "https://example.com",
checkout_cadence_12: "https://example.com",
}, {
title: "Ganymede",
monthly: 64*euro,
egress: 64*tb,
storage: 16*tb,
checkout_cadence_1: "https://example.com",
checkout_cadence_6: "https://example.com",
checkout_cadence_12: "https://example.com",
}, {
title: "Mars",
monthly: 128*euro,
egress: 128*tb,
storage: 32*tb,
checkout_cadence_1: "https://example.com",
checkout_cadence_6: "https://example.com",
checkout_cadence_12: "https://example.com",
}, {
title: "Venus",
monthly: 256*euro,
egress: 256*tb,
storage: 64*tb,
checkout_cadence_1: "https://example.com",
checkout_cadence_6: "https://example.com",
checkout_cadence_12: "https://example.com",
}, {
title: "Terra",
monthly: 512*euro,
egress: 512*tb,
storage: 128*tb,
checkout_cadence_1: "https://example.com",
checkout_cadence_6: "https://example.com",
checkout_cadence_12: "https://example.com",
}, {
title: "Neptunus",
monthly: 1024*euro,
egress: 1024*tb,
storage: 256*tb,
checkout_cadence_1: "https://example.com",
checkout_cadence_6: "https://example.com",
checkout_cadence_12: "https://example.com",
}, {
title: "Uranus",
monthly: 2048*euro,
egress: 2048*tb,
storage: 512*tb,
checkout_cadence_1: "https://example.com",
checkout_cadence_6: "https://example.com",
checkout_cadence_12: "https://example.com",
}, {
title: "Saturnus",
monthly: 4096*euro,
egress: 4096*tb,
storage: 1024*tb,
checkout_cadence_1: "https://example.com",
checkout_cadence_6: "https://example.com",
checkout_cadence_12: "https://example.com",
}, {
title: "Jupiter",
monthly: 8192*euro,
egress: 8192*tb,
storage: 2048*tb,
checkout_cadence_1: "https://example.com",
checkout_cadence_6: "https://example.com",
checkout_cadence_12: "https://example.com",
}, {
title: "Sol",
monthly: 16384*euro,
egress: 16384*tb,
storage: 4096*tb,
checkout_cadence_1: "https://example.com",
checkout_cadence_6: "https://example.com",
checkout_cadence_12: "https://example.com",
}
]
</script>
<div class="horizontal_scroll">
<div class="feat_table">
{#each plans as plan (plan.title)}
<div class="plan">
<div class="title">
<span class="bold">{plan.title}</span>
<br/>
<Euro amount={plan.monthly}/> / month
</div>
<div class="features">
<div>
<span class="bold">{formatDataVolume(plan.storage, 0)}</span>
storage space
</div>
<div>
<span class="bold">{formatDataVolume(plan.egress, 0)}</span>
egress traffic per month
</div>
</div>
<div class="order">
{#if plan.checkout_cadence_1 !== undefined}
<a href="{plan.checkout_cadence_1}" class="button">
<div class="bold">Monthly</div>
<div><Euro amount={plan.monthly}/> / month</div>
</a>
{:else}
<span>Monthly plan not available</span>
{/if}
{#if plan.checkout_cadence_6 !== undefined}
<a href="{plan.checkout_cadence_6}" class="button">
<div class="bold">Bi-yearly (save 10%)</div>
<div><Euro amount={plan.monthly*6*.9}/> / 6 months</div>
</a>
{:else}
<span>Bi-yearly plan not available</span>
{/if}
{#if plan.checkout_cadence_12 !== undefined}
<a href="{plan.checkout_cadence_12}" class="button">
<div class="bold">Yearly (save 20%)</div>
<div><Euro amount={plan.monthly*12*.8}/> / year</div>
</a>
{:else}
<span>Yearly plan not available</span>
{/if}
</div>
</div>
{/each}
</div>
</div>
<style>
.horizontal_scroll {
overflow-x: scroll;
overflow-y: hidden;
width: 100%;
}
.feat_table {
display: flex;
flex-direction: row;
gap: 8px;
padding: 8px;
}
.plan {
flex: 0 0 18em;
display: flex;
width: 18em;
flex-direction: column;
border: 1px solid var(--highlight_color);
border-radius: 8px;
background-color: var(--body_color);
}
.plan > div {
flex: 1 1 0;
margin: 0.2em;
padding: 0.5em;
text-align: center;
word-wrap: break-word;
hyphens: auto;
}
.plan > .order {
flex: 1 1 auto;
display: flex;
flex-direction: column;
}
.plan > .order > .button {
flex-direction: column;
}
.bold {
font-weight: bold;
}
</style>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
let {
icon_href = "",
width = "750px",
width = "800px",
children
}: {
icon_href?: string;
@@ -12,7 +12,7 @@ let {
<div class="block" style="width: {width}; max-width: 100%">
<img src={icon_href} alt="File icon" class="icon">
<div class="description">
<div class="description bubble">
{@render children?.()}
</div>
</div>
@@ -47,5 +47,6 @@ let {
background-color: var(--shaded_background);
backdrop-filter: blur(4px);
padding: 8px;
margin: 0;
}
</style>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
let {
width = "750px",
width = "800px",
center = false,
children
}: {
@@ -10,17 +10,25 @@ let {
} = $props();
</script>
<div class="block" class:center style="width: {width};">
<div class="width_container">
<div class="block bubble" class:center style="width: {width};">
{@render children?.()}
</div>
</div>
<style>
.width_container {
display: flex;
justify-content: center;
width: 100%;
margin: auto;
}
.block {
display: block;
text-align: initial;
max-width: 100%;
overflow-wrap: anywhere;
margin: auto;
/* margin: auto; */
background-color: var(--shaded_background);
backdrop-filter: blur(4px);

View File

@@ -1,5 +1,5 @@
import { writable } from "svelte/store"
import { fs_check_response, fs_path_url, type FSNode } from "./FilesystemAPI.svelte"
import { fs_check_response, fs_path_url, type FSNode } from "./FilesystemAPI"
import { loading_finish, loading_start } from "lib/Loading"
import { get_user } from "lib/NovaAPI"
@@ -27,12 +27,7 @@ export const bookmarks_get = async (): Promise<Bookmark[]> => {
try {
loading_start()
const user = await get_user()
if (
user.username === undefined ||
user.username === "" ||
user.subscription.filesystem_access === undefined ||
user.subscription.filesystem_access === false
) {
if (user === null) {
return []
}

View File

@@ -103,7 +103,8 @@ export type FSPermissions = {
}
export type FSContext = {
premium_transfer: boolean,
premium_transfer: boolean
can_share: boolean
}
// API parameters

View File

@@ -1,6 +1,8 @@
// Response types
// ==============
import { user } from "./UserStore"
export type GenericResponse = {
value: string,
message: string,
@@ -35,14 +37,11 @@ export type Subscription = {
name: string,
type: string,
file_size_limit: number,
file_expiry_days: number,
storage_space: number,
storage_limit: number,
price_per_tb_storage: number,
price_per_tb_bandwidth: number,
monthly_transfer_cap: number,
file_viewer_branding: boolean,
filesystem_access: boolean,
filesystem_storage_limit: number,
file_sharing: boolean,
}
// Utility funcs
@@ -107,34 +106,66 @@ export type UserSession = {
valid_domains: string[],
}
let cached_user: User = null
// If cached_user is undefined it means that the value is not initialized yet,
// null means the user is not logged in
let cached_user: User = undefined
// The request which is currently in flight. Callers which arrive while the
// request is still running share this promise, so the user is only requested
// from the server once
let user_request: Promise<User> | null = null
export const get_user = async (): Promise<User> => {
if ((window as any).user !== undefined) {
return (window as any).user as User
} else if (cached_user !== null) {
if (cached_user !== undefined) {
return cached_user
}
if (user_request === null) {
user_request = request_user()
}
console.warn("user property is not defined on window")
const u = await user_request
if (u === undefined) {
// The request failed. Clear it so the next caller tries again
user_request = null
}
cached_user = await check_response(await fetch(get_endpoint() + "/user")) as User
return u
}
const request_user = async (): Promise<User> => {
try {
let resp = await fetch(get_endpoint() + "/user")
if (resp.status === 401) {
// User is not authenticated
cached_user = null
} else if (resp.status >= 400) {
throw await resp.text()
} else {
cached_user = (await resp.json()) as User
}
} catch (err) {
alert("failed to fetch user:\n" + err)
}
user.set(cached_user)
return cached_user
}
export const put_user = async (data: Object) => {
check_response(await fetch(
await check_response(await fetch(
get_endpoint() + "/user",
{ method: "PUT", body: dict_to_form(data) },
))
// Update the window.user variable
// Update the user store
for (let key of Object.keys(data)) {
((window as any).user as User)[key] = data[key]
cached_user[key] = data[key]
}
user.set(cached_user)
}
export const logout_user = async (redirect_path: string) => {
check_response(await fetch(
await check_response(await fetch(
get_endpoint() + "/user/session",
{ method: "DELETE" },
))

View File

@@ -1,8 +1,8 @@
import { writable } from "svelte/store";
import { get_user, type Subscription, type User } from "./NovaAPI";
import { get_user, type User } from "./NovaAPI";
export const user = writable(
{ subscription: {} as Subscription } as User,
null as User | null,
(set: (value: User) => void) => {
get_user().then((u: User) => {
set(u)

View File

@@ -1,12 +1,16 @@
<script>
<script lang="ts">
import { onMount } from "svelte";
import CopyButton from "layout/CopyButton.svelte";
import Form from "util/Form.svelte";
import Form, { type FormConfig } from "util/Form.svelte";
import Button from "layout/Button.svelte";
import OtpSetup from "./OTPSetup.svelte";
import { put_user } from "lib/NovaAPI";
import { get_endpoint, put_user } from "lib/NovaAPI";
import { user } from "lib/UserStore";
let affiliate_link = window.location.protocol+"//"+window.location.host + "?ref=" + encodeURIComponent(window.user.username)
let affiliate_link = $derived(
window.location.protocol+"//"+window.location.host +
"?ref=" + encodeURIComponent($user.username)
)
let affiliate_deny = $state(false)
onMount(() => {
affiliate_deny = localStorage.getItem("affiliate_deny") === "1"
@@ -16,14 +20,13 @@ const enable_affiliate_prompt = () => {
localStorage.removeItem("affiliate_deny")
}
let account_settings = {
name: "account_settings",
let account_settings: FormConfig = $derived({
fields: [
{
name: "email",
label: "E-mail address",
type: "email",
default_value: window.user.email,
default_value: $user.email,
description: `We will send an e-mail to the new address to verify
that it's real. The address will be saved once the link in the
message is clicked. If the e-mail doesn't arrive right away
@@ -48,7 +51,7 @@ let account_settings = {
name: "username",
label: "Name",
type: "username",
default_value: window.user.username,
default_value: $user.username,
description: `Changing your username also changes the name used to
log in. If you forget your username you can still log in using
your e-mail address if you have one configured`,
@@ -57,17 +60,17 @@ let account_settings = {
name: "checkout_country",
label: "Country code used at checkout",
type: "text",
default_value: window.user.checkout_country,
default_value: $user.checkout_country,
}, {
name: "checkout_provider",
label: "Default payment provider used at checkout",
type: "text",
default_value: window.user.checkout_provider,
default_value: $user.checkout_provider,
}, {
name: "checkout_name",
label: "Full name used at checkout",
type: "text",
default_value: window.user.checkout_name,
default_value: $user.checkout_name,
separator: true,
},
],
@@ -98,21 +101,20 @@ let account_settings = {
checkout_name: fields.checkout_name,
})
} catch (err) {
return {error_json: err}
return {success: false, error_json: err}
}
return {success: true, message: "Success! Your changes have been saved"}
},
}
})
const affiliate_settings = {
name: "affiliate_settings",
let affiliate_settings: FormConfig = $derived({
fields: [
{
name: "affiliate_user_name",
label: "Affiliate user name",
type: "text",
default_value: window.user.affiliate_user_name,
default_value: $user.affiliate_user_name,
description: `The affiliate user name can be the name of a
Nova account you wish to support with your subscription.
The account will receive a fee of €0.50 for every month that
@@ -125,16 +127,15 @@ const affiliate_settings = {
const form = new FormData()
form.append("affiliate_user_name", fields.affiliate_user_name)
const resp = await fetch(window.api_endpoint+"/user", { method: "PUT", body: form });
const resp = await fetch(get_endpoint()+"/user", { method: "PUT", body: form });
if(resp.status >= 400) {
return {error_json: await resp.json()}
return {success: false, error_json: await resp.json()}
}
return {success: true, message: "Success! Your changes have been saved"}
},
}
})
let delete_account = {
name: "delete_account",
let delete_account: FormConfig = {
fields: [
{
name: "description",
@@ -169,13 +170,13 @@ let delete_account = {
submit_label: `<i class="icon">delete</i> Delete`,
on_submit: async fields => {
const resp = await fetch(
window.api_endpoint+"/user",
get_endpoint()+"/user",
{ method: "DELETE" }
);
if(resp.status >= 400) {
return {error_json: await resp.json()}
return {success: false, error_json: await resp.json()}
}
setTimeout(() => { window.location = "/" }, 6000)
setTimeout(() => { window.location.assign("/") }, 6000)
return {success: true, message: "Success! Your account has been scheduled for deletion in 7 days"}
},
}
@@ -201,7 +202,7 @@ let delete_account = {
<a href="{affiliate_link}">{affiliate_link}</a>
<CopyButton small_icon text={affiliate_link}/>. Share this link
with premium Nova users to gain commissions. You can use
the "?ref={encodeURIComponent(window.user.username)}" referral
the "?ref={encodeURIComponent($user.username)}" referral
code on download pages too. For a detailed description of the
affiliate program please check out the <a
href="/about#toc_12">Q&A page</a>.

View File

@@ -6,31 +6,22 @@ import { formatDataVolume } from "util/Formatting";
import ProgressBar from "util/ProgressBar.svelte";
import SuccessMessage from "util/SuccessMessage.svelte";
import { loading_finish, loading_start } from "lib/Loading";
import { put_user } from 'lib/NovaAPI';
import { user } from 'lib/UserStore';
let success_message = $state()
let hotlinking = $state(window.user.hotlinking_enabled)
let transfer_cap = $state(window.user.monthly_transfer_cap / 1e12)
let skip_viewer = $state(window.user.skip_file_viewer)
let hotlinking = $state($user === null ? false : $user.hotlinking_enabled)
let transfer_cap = $state($user === null ? 0 : $user.monthly_transfer_cap / 1e12)
let skip_viewer = $state($user === null ? false : $user.skip_file_viewer)
const update = async () => {
const form = new FormData()
form.append("hotlinking_enabled", hotlinking)
form.append("transfer_cap", transfer_cap*1e12)
form.append("skip_file_viewer", skip_viewer)
loading_start()
try {
const resp = await fetch(
window.api_endpoint+"/user",
{ method: "PUT", body: form },
)
if(resp.status >= 400) {
let json = await resp.json()
throw json.message
}
window.user.hotlinking_enabled = hotlinking
window.user.monthly_transfer_cap = transfer_cap*1e12
await put_user({
hotlinking_enabled: hotlinking,
transfer_cap: transfer_cap*1e12,
skip_file_viewer: skip_viewer,
})
success_message.set(true, "Sharing settings updated")
} catch (err) {

View File

@@ -1,48 +1,18 @@
<script>
<script lang="ts">
import Checkout from "layout/checkout/Checkout.svelte";
import { loading_finish, loading_start } from "lib/Loading";
import { loading_finish, loading_start } from "lib/Loading";
import { get_endpoint } from "lib/NovaAPI";
import { user } from "lib/UserStore";
import { onMount } from "svelte";
import Euro from "util/Euro.svelte";
import { formatDate } from "util/Formatting";
let credit_amount = 10
const checkout = async (network = "", amount = 0, country = "") => {
if (amount < 10) {
alert("Amount needs to be at least €10")
return
}
const form = new FormData()
form.set("amount", amount*1e6)
form.set("network", network)
form.set("country", country)
loading_start()
try {
const resp = await fetch(
window.api_endpoint+"/user/invoice",
{method: "POST", body: form},
)
if(resp.status >= 400) {
let json = await resp.json()
throw json.message
}
window.location = (await resp.json()).checkout_url
} catch (err) {
alert(err)
} finally {
loading_finish()
}
}
let invoices = $state([])
let unpaid_invoice = $state(0)
const load_invoices = async () => {
loading_start()
try {
const resp = await fetch(window.api_endpoint+"/user/invoice")
const resp = await fetch(get_endpoint()+"/user/invoice")
if(resp.status >= 400) {
throw new Error((await resp.json()).message)
}
@@ -81,7 +51,7 @@ onMount(() => {
Prepaid there are no limits. You pay for what you use, at a rate of €4
per TB per month for storage and €1 per TB for data transfer. Your
current account balance is <Euro
amount={window.user.balance_micro_eur}/>
amount={$user === null ? 0 : $user.balance_micro_eur}/>
</p>
{#if unpaid_invoice > 1}

View File

@@ -1,23 +1,23 @@
<script>
import { preventDefault } from 'svelte/legacy';
<script lang="ts">
import { onMount } from "svelte";
import Persistence from "icons/Persistence.svelte";
import SuccessMessage from "util/SuccessMessage.svelte";
import { loading_finish, loading_start } from "lib/Loading";
import { get_endpoint, get_user, type User } from "lib/NovaAPI";
import { loading_run } from "lib/Loading";
let success_message = $state()
let success_message: SuccessMessage
// Embedding settings
let embed_domains = $state("")
let embed_domains: string[] = []
const save_embed = async () => {
await loading_run(async () => {
const domain_list = embed_domains.join(" ")
const form = new FormData()
form.append("embed_domains", embed_domains)
form.append("embed_domains", domain_list)
loading_start()
try {
const resp = await fetch(
window.api_endpoint+"/user",
get_endpoint()+"/user",
{ method: "PUT", body: form }
);
if(resp.status >= 400) {
@@ -29,25 +29,37 @@ const save_embed = async () => {
success_message.set(true, "Changes saved")
} catch(err) {
success_message.set(false, err)
} finally {
loading_finish()
}
})
}
onMount(() => {
embed_domains = window.user.file_embed_domains
let new_domain: string = ""
const add = () => {
console.debug("Adding domain", new_domain)
embed_domains.push(new_domain)
embed_domains = embed_domains
new_domain = ""
save_embed()
}
const remove = (index: number) => {
embed_domains.splice(index, 1)
embed_domains = embed_domains
save_embed()
}
let user: User
onMount(async () => {
user = await get_user()
if (user.file_embed_domains !== "") {
embed_domains = user.file_embed_domains.split(" ")
}
})
</script>
<section>
<h2><Persistence/>Embedding controls</h2>
<h2>Embedding controls</h2>
<SuccessMessage bind:this={success_message}></SuccessMessage>
{#if !window.user.subscription.file_viewer_branding}
<div class="highlight_yellow">
Sharing settings are not available for your account. Subscribe to
the Persistence plan or higher to enable these features.
</div>
{:else if !window.user.hotlinking_enabled}
{#if user !== undefined && !user.hotlinking_enabled}
<div class="highlight_yellow">
To use embedding restrictions hotlinking needs to be enabled.
Enable hotlinking on the
@@ -57,25 +69,45 @@ onMount(() => {
<p>
Here you can control which websites are allowed to embed your files in
their web pages. If a website that is not on this list tries to embed
one of your files the request will be blocked.
one of your files the request will be blocked. This applies to both
hotlink and iframe embeds. If the list is empty, all domains are allowed
to embed your files.
</p>
<p>
The list should be formatted as a list of domain names separated by a
space. Like this: 'nova.storage google.com twitter.com'
</p>
Domain names:<br/>
<form class="form_row" onsubmit={preventDefault(save_embed)}>
<input class="grow" bind:value={embed_domains} type="text"/>
<button class="shrink" action="submit"><i class="icon">save</i> Save</button>
<div class="highlight_border">
<form on:submit|preventDefault={() => add()}>
<div class="form_row">
<span>Add domain name</span>
<input class="grow" bind:value={new_domain} type="text"/>
<button class="shrink" type="submit"><i class="icon">add</i> Add</button>
</div>
</form>
<hr/>
{#if embed_domains.length === 0}
<span>
No domains specified. All websites are allowed to embed your files
</span>
{/if}
{#each embed_domains as domain, index}
<div class="form_row">
<button class="shrink" type="button" on:click={() => remove(index)}>
<i class="icon">delete</i>
</button>
<div>{domain}</div>
</div>
{/each}
</div>
</section>
<style>
.highlight_border {
text-align: initial;
}
.form_row {
display: inline-flex;
flex-direction: row;
width: 100%;
align-items: center;
gap: 0.5em;
}
.grow {
flex: 1 1 auto;

View File

@@ -34,7 +34,7 @@ let frac = $derived(used / total)
files have a daily download limit and hotlinking is disabled.
</p>
{#if $user.monthly_transfer_cap > 0}
{#if $user !== null && $user.monthly_transfer_cap > 0}
<p>
You have a billshock limit configured. <a
href="/user/sharing/bandwidth">increase or disable the limit</a> to
@@ -56,7 +56,7 @@ let frac = $derived(used / total)
and hotlinking is disabled.
</p>
{#if $user.monthly_transfer_cap > 0}
{#if $user !== null && $user.monthly_transfer_cap > 0}
<p>
You have a billshock limit configured. <a
href="/user/sharing/bandwidth">increase or disable the limit</a> to

View File

@@ -8,7 +8,7 @@ let { total = 0, used = 0, disable_warnings = false }: {
disable_warnings?: boolean;
} = $props();
let frac = $derived(used / total)
let frac = $derived(total <= 0 ? 0 : used / total)
</script>
<ProgressBar total={total} used={used}></ProgressBar>

View File

@@ -6,8 +6,8 @@ import { loading_finish, loading_start } from "lib/Loading";
import { user } from "lib/UserStore";
import { put_user } from "lib/NovaAPI";
let subscription = $state($user.subscription.id)
let subscription_type = $state($user.subscription.type)
let subscription: string = $state($user === null ? "" : $user.subscription.id)
let subscription_type: string = $state($user === null ? "" : $user.subscription.type)
let success_message: SuccessMessage = $state()
const update = async (plan: string) => {

View File

@@ -1,10 +1,12 @@
<script>
import Button from "layout/Button.svelte";
<script lang="ts">
import { logout_user } from "lib/NovaAPI";
import { user } from "lib/UserStore";
</script>
<ul>
<li>Username: {window.user.username}</li>
{#if window.user.email === ""}
{#if $user !== null}
<ul>
<li>Username: {$user.username}</li>
{#if $user.email === ""}
<li class="highlight_blue" style="text-align: initial;">
No e-mail address configured. You will not be able to recover your
account if you lose your password. Set an e-mail address on the <a
@@ -12,24 +14,23 @@ import { logout_user } from "lib/NovaAPI";
</li>
{:else}
<li>
E-mail address: {window.user.email}
E-mail address: {$user.email}
</li>
{/if}
<li>
<i class="icon">settings</i>
<a href="/user/settings">Account settings</a>
</li>
</ul>
</ul>
{/if}
<h3>Quick navigation</h3>
<div class="button_row">
{#if window.user.subscription.filesystem_access}
<a href="/d/me" class="button">
<i class="icon">folder</i>
Filesystem
</a>
{/if}
<button onclick={()=> logout_user("/")}>
<i class="icon">logout</i>
Log out

View File

@@ -1,20 +1,22 @@
<script>
<script lang="ts">
import { user } from "lib/UserStore";
import Euro from "util/Euro.svelte";
import { formatDataVolume } from "util/Formatting";
</script>
<ul>
{#if $user !== null}
<ul>
<li>
Supporter level: {window.user.subscription.name}<br/>
Supporter level: {$user.subscription.name}<br/>
<i class="icon">shopping_cart</i>
<a href="/user/subscription">Manage subscriptions</a><br/>
<i class="icon">add_link</i>
<a href="/api/patreon_auth/start">Link Patreon subscription</a>
</li>
{#if window.user.balance_micro_eur !== 0}
{#if $user.balance_micro_eur !== 0}
<li>
Current account balance: <Euro amount={window.user.balance_micro_eur}></Euro><br/>
Current account balance: <Euro amount={$user.balance_micro_eur}></Euro><br/>
<i class="icon">account_balance_wallet</i>
<a href="/user/prepaid/deposit">Deposit credit</a><br/>
@@ -22,7 +24,7 @@ import { formatDataVolume } from "util/Formatting";
<i class="icon">receipt</i>
<a href="/user/prepaid/transactions">Transaction log</a>
{#if window.user.balance_micro_eur > 0 && window.user.subscription.id === ""}
{#if $user.balance_micro_eur > 0 && $user.subscription.id === ""}
<br/>
You have account credit but no active subscription. Activate
a subscription on the <a href="/user/subscription">subscriptions page</a>
@@ -31,29 +33,37 @@ import { formatDataVolume } from "util/Formatting";
{/if}
<li>
Max file size: {formatDataVolume(window.user.subscription.file_size_limit, 3)}
Max file size: {formatDataVolume($user.subscription.file_size_limit, 3)}
</li>
<li>
{#if window.user.subscription.storage_space > 0}
Storage limit: {formatDataVolume(window.user.subscription.storage_space, 3)}
{#if $user.subscription.storage_limit > 0}
Storage limit: {formatDataVolume($user.subscription.storage_limit, 3)}
{:else}
No storage limit
{/if}
</li>
<li>
{#if window.user.subscription.monthly_transfer_cap > 0}
Data transfer limit: {formatDataVolume(window.user.subscription.monthly_transfer_cap, 3)}
{#if $user.subscription.monthly_transfer_cap > 0}
Data transfer limit: {formatDataVolume($user.subscription.monthly_transfer_cap, 3)}
{:else}
No data transfer limit
{/if}
</li>
{#if window.user.subscription.id !== ""}
<li>
{#if $user.subscription.file_sharing === true}
File sharing enabled
{:else}
File sharing not allowed
{/if}
</li>
{#if $user.subscription.id !== ""}
<li>
Support: For questions related to your account you can send a
message to support@nova.storage
</li>
{/if}
</ul>
</ul>
{/if}
<style>
ul {

View File

@@ -42,7 +42,7 @@ onMount(async () => {
transfer_cap = -1
}
storage_limit = user.subscription.storage_space
storage_limit = user.subscription.storage_limit
load_direct_bw()
})

View File

@@ -127,7 +127,7 @@ onMount(() => {
{#each cards as card, i (card.id)}
<div
animate:flip={{duration: 250}}
class="card"
class="card bubble"
class:size_1={card.size === 1}
class:size_2={card.size === 2}
class:size_3={card.size === 3}
@@ -175,17 +175,15 @@ onMount(() => {
flex-wrap: wrap;
gap: 8px;
padding: 8px 0;
margin: 0 8px;
margin: 0 4px;
}
.card {
flex: 1 0 auto;
display: flex;
flex-direction: column;
max-width: 100%;
background: var(--body_background);
border-radius: 8px;
border: 1px solid var(--separator);
text-align: initial;
margin: 0;
}
.card_component {
flex: 1 1 auto;

View File

@@ -14,7 +14,6 @@ export type FormField = {
separator?: boolean,
radio_values?: string[], // Options to choose from when type is "radio"
pattern?: string, // Used for pattern matching on input fields
binding?: any
};
export type SubmitResult = {
success: boolean,
@@ -24,44 +23,53 @@ export type SubmitResult = {
};
</script>
<script lang="ts">
import { onMount } from "svelte";
import Spinner from "./Spinner.svelte";
import { loading_run } from "lib/Loading";
import type { GenericResponse } from "lib/NovaAPI";
let { config }: {
let { config = $bindable() }: {
config: FormConfig;
} = $props();
onMount(() => {
// The current value of each input, keyed by field name. The values live here
// instead of on the config object because the config is usually a plain object
// which svelte can't track changes on
const initial_values = () => {
let values: {[key: string]: any} = {}
config.fields.forEach(field => {
if(field.default_value === undefined) {
field.default_value = ""
if (field.name !== undefined) {
values[field.name] = field.default_value ?? ""
}
})
return values
}
let values: {[key: string]: any} = $state(initial_values())
// When the parent swaps out the form (or rebuilds its config) the inputs are
// reset to the new default values
let rendered_fields = config.fields
$effect(() => {
if (config.fields !== rendered_fields) {
rendered_fields = config.fields
values = initial_values()
}
})
let loading = $state(false)
let submitted = $state(false)
let submit_result: SubmitResult = $state()
const submit = async (event: SubmitEvent) => {
loading = true
return await loading_run(async () => {
event.preventDefault()
let field_values = {}
config.fields.forEach(field => {
if (field.type === "radio") {
if (field.binding === undefined) {
field_values[field.name] = ""
} else {
field_values[field.name] = field.binding
}
} else if (field.type === "description") {
field_values[field.name] = ""
} else {
field_values[field.name] = field.binding.value
if (field.name === undefined) {
return
}
const value = values[field.name]
field_values[field.name] = value === undefined || value === null ? "" : String(value)
})
submit_result = await config.on_submit(field_values)
@@ -69,9 +77,8 @@ const submit = async (event: SubmitEvent) => {
submit_result = handle_errors(submit_result.error_json)
}
submitted = true
loading = false
return false
})
}
const field_label = (field: string) => {
@@ -144,94 +151,85 @@ const handle_errors = (response: GenericResponse) => {
{field.label}
</label>
{#if field.type === "text"}
<input bind:this={field.binding}
<input bind:value={values[field.name]}
id="input_{field.name}"
name="{field.name}"
value="{field.default_value}"
pattern={field.pattern}
type="text"
class="form_input"
/>
{:else if field.type === "text_area"}
<textarea bind:this={field.binding}
<textarea bind:value={values[field.name]}
id="input_{field.name}"
name="{field.name}"
class="form_input"
style="width: 100%; height: 10em; resize: vertical;"
>{field.default_value}</textarea>
></textarea>
{:else if field.type === "number"}
<input bind:this={field.binding}
<input bind:value={values[field.name]}
id="input_{field.name}"
name="{field.name}"
value="{field.default_value}"
pattern={field.pattern}
type="number"
class="form_input"
/>
{:else if field.type === "decimal"}
<input bind:this={field.binding}
<input bind:value={values[field.name]}
id="input_{field.name}"
name="{field.name}"
value="{field.default_value}"
pattern={field.pattern}
type="number"
step="0.1"
class="form_input"
/>
{:else if field.type === "datetime-local"}
<input bind:this={field.binding}
<input bind:value={values[field.name]}
id="input_{field.name}"
name="{field.name}"
value="{field.default_value}"
pattern={field.pattern}
type="datetime-local"
class="form_input"
/>
{:else if field.type === "username"}
<input bind:this={field.binding}
<input bind:value={values[field.name]}
id="input_{field.name}"
name="{field.name}"
value="{field.default_value}"
pattern={field.pattern}
type="text"
autocomplete="username"
class="form_input"
/>
{:else if field.type === "email"}
<input bind:this={field.binding}
<input bind:value={values[field.name]}
id="input_{field.name}"
name="{field.name}"
value="{field.default_value}"
pattern={field.pattern}
type="email"
autocomplete="email"
class="form_input"
/>
{:else if field.type === "current_password"}
<input bind:this={field.binding}
<input bind:value={values[field.name]}
id="input_{field.name}"
name="{field.name}"
value="{field.default_value}"
pattern={field.pattern}
type="password"
autocomplete="current-password"
class="form_input"
/>
{:else if field.type === "new_password"}
<input bind:this={field.binding}
<input bind:value={values[field.name]}
id="input_{field.name}"
name="{field.name}"
value="{field.default_value}"
pattern={field.pattern}
type="password"
autocomplete="new-password"
class="form_input"
/>
{:else if field.type === "totp"}
<input bind:this={field.binding}
<input bind:value={values[field.name]}
id="input_{field.name}"
name="{field.name}"
value="{field.default_value?field.default_value:""}"
type="text"
autocomplete="one-time-code"
pattern={"[0-9]{6}"}
@@ -240,12 +238,11 @@ const handle_errors = (response: GenericResponse) => {
{:else if field.type === "radio"}
<div>
{#each field.radio_values as val}
<input bind:group={field.binding}
<input bind:group={values[field.name]}
id="input_{field.name}_choice_{val}"
name="{field.name}"
value={val}
type="radio"
checked={val === field.default_value}/>
type="radio"/>
<label for="input_{field.name}_choice_{val}">{val}</label><br/>
{/each}
</div>
@@ -268,20 +265,4 @@ const handle_errors = (response: GenericResponse) => {
<button type="submit" class="button_highlight">{@html config.submit_label}</button>
{/if}
</div>
{#if loading}
<div class="spinner_container">
<Spinner></Spinner>
</div>
{/if}
</form>
<style>
.spinner_container {
position: absolute;
top: 10px;
right: 10px;
height: 100px;
width: 100px;
}
</style>

View File

@@ -1,6 +1,4 @@
<script lang="ts">
import { run } from 'svelte/legacy';
let {
total = $bindable(0),
used = 0,
@@ -20,7 +18,7 @@ let {
} = $props();
let percent = $state(0)
run(() => {
$effect(() => {
// Avoid division by 0
if (total === 0) {
total = 1

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { bookmarks_save, bookmarks_store } from "lib/Bookmarks";
import { fs_encode_path, fs_thumbnail_url } from "lib/FilesystemAPI.svelte";
import { fs_encode_path, fs_thumbnail_url } from "lib/FilesystemAPI";
import { highlight_current_page } from "lib/HighlightCurrentPage";
import MenuEntry from "./MenuEntry.svelte";
import { flip } from "svelte/animate";

View File

@@ -9,7 +9,7 @@ import { formatDataVolume } from "util/Formatting";
import Router from "wrap/Router.svelte";
import Bookmarks from "./Bookmarks.svelte";
import { onMount } from "svelte";
import { fs_get_node } from "lib/FilesystemAPI.svelte";
import { fs_get_node } from "lib/FilesystemAPI";
import { css_from_path } from "filesystem/edit_window/Branding";
import { loading_run, loading_store } from "lib/Loading";
import Spinner from "util/Spinner.svelte";
@@ -32,12 +32,12 @@ const min_screen_size_fullscreen_menu = 500
// If the screen is smaller than this the menu will be closed by default
const min_screen_size_menu_open = 800
onMount(() => {
onMount(async () => {
if (document.documentElement.clientWidth < min_screen_size_menu_open) {
menu_close()
}
loading_run(async () => {
await loading_run(async () => {
const user = await get_user()
if (user.username === undefined || user.username === "") {
return
@@ -79,11 +79,15 @@ const touchmove = (e: TouchEvent) => {
// The cursor must have moved at least swipe_dead_zone pixels and twice as
// much on the x axis than the y axis for it to count as a swipe
if (abs_x > swipe_dead_zone && abs_x / 2 > abs_y) {
if (abs_x > swipe_dead_zone) {
if (abs_x > abs_y) {
e.preventDefault()
e.stopPropagation()
set_offset(initial_offset+(x*2))
} else {
touchend(e)
}
}
}
const touchend = (e: TouchEvent) => {
@@ -151,7 +155,7 @@ const set_offset = (off: number) => {
<div class="wrap">
<div class="nav_container" bind:this={menu}>
<div class="scroll_container">
<nav class="nav" bind:this={nav}>
<nav class="nav bubble" bind:this={nav}>
{#if document.documentElement.clientWidth < min_screen_size_menu_open}
<button class="button" onclick={toggle_menu}>
<i class="icon">menu</i>
@@ -159,26 +163,12 @@ const set_offset = (off: number) => {
</button>
{/if}
<MenuEntry id="home" collapsed={false} no_border>
{#snippet title()}
<a class="button" style="flex: 1 1 auto;" href="/" use:highlight_current_page>
<span style="flex: 1 1 auto;">Nova.storage</span>
<img src="/res/img/nova_64.png" style="width: 1.5em; height: 1.5em; flex: 0 0 auto;" alt="Nova.storage logo">
</a>
{/snippet}
{#snippet body()}
<a class="button" href="/speedtest" use:highlight_current_page>
<i class="icon">speed</i>
<span>Speedtest</span>
</a>
<a class="button" href="/appearance" use:highlight_current_page>
<i class="icon">palette</i>
<span>Themes</span>
</a>
{/snippet}
</MenuEntry>
{#if $user.username !== undefined && $user.username !== ""}
{#if $user !== null}
<MenuEntry id="subscription_info" collapsed={false}>
{#snippet title()}
<div class="username">{$user.username}</div>
@@ -202,6 +192,10 @@ const set_offset = (off: number) => {
{/snippet}
</MenuEntry>
<a class="button" href="/user" use:highlight_current_page>
<i class="icon">person</i>
<span>Account</span>
</a>
<a class="button" href="/d/me" use:highlight_current_page>
<i class="icon">folder</i>
<span>Filesystem</span>
@@ -210,10 +204,6 @@ const set_offset = (off: number) => {
<i class="icon">delete</i>
<span>Trash</span>
</a>
<a class="button" href="/user" use:highlight_current_page>
<i class="icon">person</i>
<span>Account</span>
</a>
{#if $user.is_admin}
<a class="button" href="/admin" use:highlight_current_page>
<i class="icon">admin_panel_settings</i>
@@ -231,6 +221,24 @@ const set_offset = (off: number) => {
</a>
{/if}
<MenuEntry id="other_pages" collapsed={false}>
{#snippet title()}
<span style="flex: 1 1 auto;">
<span style="flex: 1 1 auto;">Other pages</span>
</span>
{/snippet}
{#snippet body()}
<a class="button" href="/speedtest" use:highlight_current_page>
<i class="icon">speed</i>
<span>Speedtest</span>
</a>
<a class="button" href="/appearance" use:highlight_current_page>
<i class="icon">palette</i>
<span>Themes</span>
</a>
{/snippet}
</MenuEntry>
<Bookmarks/>
<Tree/>
</nav>
@@ -238,7 +246,7 @@ const set_offset = (off: number) => {
</div>
<div class="page">
<div class="header">
<div class="header bubble">
<button class="menu_button" onclick={toggle_menu}>
<i class="icon">menu</i>
</button>
@@ -253,7 +261,6 @@ const set_offset = (off: number) => {
<Router/>
</div>
{#if $loading_store !== 0}
<div class="spinner">
<Spinner/>
@@ -279,8 +286,6 @@ const set_offset = (off: number) => {
}
.nav_container {
flex: 0 0 auto;
border-right: 1px solid var(--separator);
background: var(--body_background);
backdrop-filter: blur(3px);
z-index: 9;
}
@@ -291,11 +296,15 @@ const set_offset = (off: number) => {
overflow-y: auto;
max-height: 100vh;
height: 100vh;
padding: 4px;
scrollbar-gutter: stable;
}
.nav {
display: flex;
flex-direction: column;
width: 15em;
margin: 0;
padding: 4px;
}
.button {
background: none;
@@ -326,11 +335,9 @@ const set_offset = (off: number) => {
flex-direction: row;
align-items: center;
justify-content: left;
background: var(--body_background);
border-bottom: 1px solid var(--separator);
backdrop-filter: blur(3px);
position: sticky;
top: 0;
top: 4px;
z-index: 8; /* below navigation, on top of most other things */
}
.menu_button {
@@ -344,6 +351,7 @@ const set_offset = (off: number) => {
flex-wrap: wrap;
flex-direction: row;
align-items: center;
overflow-x: hidden;
}
.spinner {
@@ -352,5 +360,6 @@ const set_offset = (off: number) => {
right: 10px;
height: 120px;
width: 120px;
z-index: 11; /* One above the menu */
}
</style>

View File

@@ -64,6 +64,8 @@ const toggle = (e: MouseEvent) => {
flex-direction: row;
align-items: center;
border-top: 1px solid var(--separator);
margin-top: 4px;
padding-top: 4px;
}
.no_border {
border-top: none;

View File

@@ -56,21 +56,26 @@ let pages: Tab[] = [
},
]
let user: User = null
onMount(async () => {
user = await get_user()
load_page(window.location.pathname, false)
await load_page(window.location.pathname, false)
})
let current_page: Tab = $state(null)
const load_page = (pathname: string, history: boolean): boolean => {
const load_page = async (pathname: string, history: boolean): Promise<boolean> => {
console.debug(
"Page router: Navigating to page", pathname,
"current path", window.location.pathname,
"log history:", history,
)
let user: User
try {
user = await get_user()
} catch (err) {
alert("Failed to get user details: " + err)
}
const path_decoded = decodeURIComponent(pathname)
let page_by_path: Tab = null
let page_by_prefix: Tab = null
@@ -98,7 +103,7 @@ const load_page = (pathname: string, history: boolean): boolean => {
// If this page requires login, and the user is not logged in, then we
// redirect the user to the login page
if (current_page.login === true && (user.username === "" || user.username === undefined)) {
if (current_page.login === true && (user === null || user === undefined)) {
console.debug("User is not logged in, redirecting to login page", user)
return load_page("/login", true)
}

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { global_navigator } from "filesystem/FSNavigator";
import { fs_encode_path, fs_node_icon, FSNode } from "lib/FilesystemAPI.svelte";
import { fs_encode_path, fs_node_icon, FSNode } from "lib/FilesystemAPI";
import { highlight_current_page } from "lib/HighlightCurrentPage";
import { onMount } from "svelte";
import MenuEntry from "./MenuEntry.svelte";

View File

@@ -14,7 +14,7 @@ import (
func (wc *WebController) serveDirectory(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
var err error
var td = wc.newTemplateData(w, r)
var td = wc.newTemplateData(r)
var path = strings.TrimPrefix(p.ByName("path"), "/")
// Prevent search engines from indexing this page for privacy reasons
@@ -25,7 +25,9 @@ func (wc *WebController) serveDirectory(w http.ResponseWriter, r *http.Request,
return
}
node, err := td.PixelAPI.GetFilesystemPath(path)
pdapi := wc.api.RealIP(util.RemoteAddress(r)).RealAgent(r.UserAgent())
node, err := pdapi.GetFilesystemPath(path)
if err != nil {
if apiErr, ok := errors.AsType[pixelapi.Error](err); ok {
switch apiErr.StatusCode {
@@ -59,15 +61,17 @@ func (wc *WebController) serveDirectory(w http.ResponseWriter, r *http.Request,
func (wc *WebController) serveForbidden(w http.ResponseWriter, r *http.Request) {
log.Debug("Forbidden: %s", r.URL)
w.WriteHeader(http.StatusForbidden)
wc.templates.Run(w, r, "403", wc.newTemplateData(w, r))
if err := wc.templates.Run(w, r, "wrap", wc.newTemplateData(r)); err != nil {
log.Error("Failed to run template 403: %s", err)
}
}
func (wc *WebController) serveNotFound(w http.ResponseWriter, r *http.Request) {
log.Debug("Not Found: %s", r.URL)
w.WriteHeader(http.StatusNotFound)
wc.templates.Run(w, r, "404", wc.newTemplateData(w, r))
wc.templates.Run(w, r, "404", wc.newTemplateData(r))
}
func (wc *WebController) serveUnavailableForLegalReasons(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnavailableForLegalReasons)
wc.templates.Run(w, r, "451", wc.newTemplateData(w, r))
wc.templates.Run(w, r, "451", wc.newTemplateData(r))
}

View File

@@ -14,7 +14,6 @@ import (
"time"
"fornaxian.tech/log"
"fornaxian.tech/pixeldrain_api_client/pixelapi"
"fornaxian.tech/util"
)
@@ -22,11 +21,7 @@ import (
// the field Other you can pass your own template-specific variables.
type TemplateData struct {
tpm *TemplateManager
Authenticated bool
User pixelapi.UserInfo
UserAgent string
APIEndpoint template.URL
PixelAPI pixelapi.PixelAPI
Hostname template.HTML
// Only used on file viewer page
@@ -37,58 +32,14 @@ type TemplateData struct {
URLQuery url.Values
}
func (wc *WebController) newTemplateData(w http.ResponseWriter, r *http.Request) (t *TemplateData) {
func (wc *WebController) newTemplateData(r *http.Request) (t *TemplateData) {
t = &TemplateData{
tpm: wc.templates,
Authenticated: false,
UserAgent: r.UserAgent(),
APIEndpoint: template.URL(wc.config.APIURLExternal),
// Use the user's IP address for making requests
PixelAPI: wc.api.RealIP(util.RemoteAddress(r)).RealAgent(r.UserAgent()),
Hostname: template.HTML(wc.hostname),
URLQuery: r.URL.Query(),
}
// If the user is authenticated we'll indentify him and put the user info
// into the templatedata. This is used for putting the username in the menu
// and stuff like that
if key, err := wc.getAPIKey(r); err == nil {
t.PixelAPI = t.PixelAPI.Login(key) // Use the user's API key for all requests
if t.User, err = t.PixelAPI.GetUser(); err != nil {
// This session key doesn't work, or the backend is down, user
// cannot be authenticated
log.Debug("Session check for key '%s' failed: %s", key, err)
if err.Error() == "authentication_required" || err.Error() == "authentication_failed" {
// Disable API authentication
t.PixelAPI = wc.api.RealIP(util.RemoteAddress(r)).RealAgent(r.UserAgent())
// Remove the authentication cookie
log.Debug("Deleting invalid API key")
http.SetCookie(w, &http.Cookie{
Name: "pd_auth_key",
Value: "",
Path: "/",
Expires: time.Unix(0, 0),
Domain: wc.config.SessionCookieDomain,
})
http.SetCookie(w, &http.Cookie{
Name: "pd_auth_key",
Value: "",
Path: "/",
Expires: time.Unix(0, 0),
Domain: ".nova.storage",
})
}
return t
}
// Authentication succeeded
t.Authenticated = true
}
return t
}

View File

@@ -87,7 +87,7 @@ func New(r *httprouter.Router, prefix string, conf Config) (wc *WebController) {
if conf.MaintenanceMode {
r.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
wc.templates.Run(w, r, "maintenance", wc.newTemplateData(w, r))
wc.templates.Run(w, r, "maintenance", wc.newTemplateData(r))
})
return wc
}
@@ -118,7 +118,7 @@ func New(r *httprouter.Router, prefix string, conf Config) (wc *WebController) {
}
r.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
wc.serveTemplate("wrap", handlerOpts{})(w, r, nil)
wc.serveTemplate("wrap")(w, r, nil)
})
// Request method shorthands. These help keep the array of handlers aligned
@@ -130,10 +130,10 @@ func New(r *httprouter.Router, prefix string, conf Config) (wc *WebController) {
path string // The URL path this API will be registered on
handler httprouter.Handle // The function to run when this API is called
}{
{GET, "api" /* */, wc.serveMarkdown("api.md", handlerOpts{})},
{GET, "api" /* */, wc.serveMarkdown("api.md")},
{GET, "d/*path" /* */, wc.serveDirectory},
{GET, "widgets" /* */, wc.serveTemplate("widgets", handlerOpts{})},
{GET, "status" /* */, wc.serveTemplate("status", handlerOpts{})},
{GET, "widgets" /* */, wc.serveTemplate("widgets")},
{GET, "status" /* */, wc.serveTemplate("status")},
{GET, "theme.css", wc.themeHandler},
} {
r.Handle(h.method, prefix+"/"+h.path, middleware(h.handler))
@@ -165,43 +165,27 @@ func middleware(handle httprouter.Handle) httprouter.Handle {
}
}
type handlerOpts struct {
Auth bool
NoEmbed bool
NoExec bool
}
func (wc *WebController) serveTemplate(tpl string, opts handlerOpts) httprouter.Handle {
func (wc *WebController) serveTemplate(tpl string) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
if opts.NoEmbed {
// if opts.NoEmbed {
w.Header().Set("X-Frame-Options", "DENY")
}
// }
var td = wc.newTemplateData(w, r)
if opts.Auth && !td.Authenticated {
http.Redirect(w, r, "/login?redirect="+url.QueryEscape(r.URL.Path), http.StatusSeeOther)
return
}
err := wc.templates.Run(w, r, tpl, td)
err := wc.templates.Run(w, r, tpl, wc.newTemplateData(r))
if err != nil && !util.IsNetError(err) {
log.Error("Error executing template '%s': %s", tpl, err)
}
}
}
func (wc *WebController) serveMarkdown(tpl string, opts handlerOpts) httprouter.Handle {
func (wc *WebController) serveMarkdown(tpl string) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
var err error
if opts.NoEmbed {
// if opts.NoEmbed {
w.Header().Set("X-Frame-Options", "DENY")
}
// }
var tpld = wc.newTemplateData(w, r)
if opts.Auth && !tpld.Authenticated {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
var tpld = wc.newTemplateData(r)
// Execute the raw markdown template and save the result in a buffer
var tplBuf bytes.Buffer