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

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,53 +1,65 @@
<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 () => {
const form = new FormData()
form.append("embed_domains", embed_domains)
await loading_run(async () => {
const domain_list = embed_domains.join(" ")
const form = new FormData()
form.append("embed_domains", domain_list)
loading_start()
try {
const resp = await fetch(
window.api_endpoint+"/user",
{ method: "PUT", body: form }
);
if(resp.status >= 400) {
let json = await resp.json()
console.debug(json)
throw json.message
try {
const resp = await fetch(
get_endpoint()+"/user",
{ method: "PUT", body: form }
);
if(resp.status >= 400) {
let json = await resp.json()
console.debug(json)
throw json.message
}
success_message.set(true, "Changes saved")
} catch(err) {
success_message.set(false, err)
}
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>
</form>
<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,35 +1,36 @@
<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 === ""}
<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
href="/user/settings">Settings</a> page.
</li>
{:else}
{#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
href="/user/settings">Settings</a> page.
</li>
{:else}
<li>
E-mail address: {$user.email}
</li>
{/if}
<li>
E-mail address: {window.user.email}
<i class="icon">settings</i>
<a href="/user/settings">Account settings</a>
</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}
<a href="/d/me" class="button">
<i class="icon">folder</i>
Filesystem
</a>
<button onclick={()=> logout_user("/")}>
<i class="icon">logout</i>
Log out

View File

@@ -1,59 +1,69 @@
<script>
<script lang="ts">
import { user } from "lib/UserStore";
import Euro from "util/Euro.svelte";
import { formatDataVolume } from "util/Formatting";
</script>
<ul>
<li>
Supporter level: {window.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 !== null}
<ul>
<li>
Current account balance: <Euro amount={window.user.balance_micro_eur}></Euro><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>
<i class="icon">account_balance_wallet</i>
<a href="/user/prepaid/deposit">Deposit credit</a><br/>
{#if $user.balance_micro_eur !== 0}
<li>
Current account balance: <Euro amount={$user.balance_micro_eur}></Euro><br/>
<i class="icon">receipt</i>
<a href="/user/prepaid/transactions">Transaction log</a>
<i class="icon">account_balance_wallet</i>
<a href="/user/prepaid/deposit">Deposit credit</a><br/>
{#if window.user.balance_micro_eur > 0 && window.user.subscription.id === ""}
<br/>
You have account credit but no active subscription. Activate
a subscription on the <a href="/user/subscription">subscriptions page</a>
<i class="icon">receipt</i>
<a href="/user/prepaid/transactions">Transaction log</a>
{#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>
{/if}
</li>
{/if}
<li>
Max file size: {formatDataVolume($user.subscription.file_size_limit, 3)}
</li>
<li>
{#if $user.subscription.storage_limit > 0}
Storage limit: {formatDataVolume($user.subscription.storage_limit, 3)}
{:else}
No storage limit
{/if}
</li>
{/if}
<li>
Max file size: {formatDataVolume(window.user.subscription.file_size_limit, 3)}
</li>
<li>
{#if window.user.subscription.storage_space > 0}
Storage limit: {formatDataVolume(window.user.subscription.storage_space, 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)}
{:else}
No data transfer limit
{/if}
</li>
{#if window.user.subscription.id !== ""}
<li>
Support: For questions related to your account you can send a
message to support@nova.storage
{#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}
</ul>
<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>
{/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;