Compare commits

..

3 Commits

Author SHA1 Message Date
99ab34c76d Fix error popup when canceling a sharing action
Fix navigator not reloading when saving an edited file
Remove dead links
Remove references to Patreon
2026-08-05 22:18:57 +02:00
4c57a363b2 Fix:
* Breadcrumbs too wide in deeply nested directories
* Sharedialog copies link and calls navigator.share at the same time
* Link copy feedback is not clear
* Show embed code when public read
* Hide hidden files from audio playlist
2026-08-05 21:54:49 +02:00
00b40ab900 Add filesystem indexing metrics 2026-08-05 21:52:28 +02:00
12 changed files with 104 additions and 135 deletions

View File

@@ -26,7 +26,26 @@ const groups: {
{metric: "api_error_400", data_type: "number"}, {metric: "api_error_400", data_type: "number"},
{metric: "api_error_500", data_type: "number"}, {metric: "api_error_500", data_type: "number"},
{metric: "api_panic", data_type: "number"}, {metric: "api_panic", data_type: "number"},
{metric: "api_user_register", data_type: "number"},
{metric: "api_user_login", data_type: "number"},
{metric: "api_user_delete", data_type: "number"},
], ],
}, {
title: "Filesystem API", expanded: false,
graphs: [
{metric: "filesystem_write", data_type: "number"},
{metric: "filesystem_write_size", data_type: "bytes"},
{metric: "filesystem_read", data_type: "number"},
{metric: "filesystem_read_size", data_type: "bytes"},
{metric: "filesystem_index_update", data_type: "number"},
{metric: "filesystem_index_update_duration", data_type: "duration"},
{
metric: "filesystem_index_update_duration_avg",
agg_base: "filesystem_index_update_duration",
agg_divisor: "filesystem_index_update",
data_type: "duration",
},
]
}, { }, {
title: "Task scheduler", expanded: false, title: "Task scheduler", expanded: false,
graphs: [ graphs: [

View File

@@ -36,7 +36,7 @@ onMount(() => {
{:else if node.is_shared()} {:else if node.is_shared()}
<i class="icon small">share</i> <i class="icon small">share</i>
{/if} {/if}
<div class="node_name" class:base={$nav.base_index === i}> <div class="node_name" class:base={$nav.base_index === i} class:small={$nav.path.length > 10}>
{node.name} {node.name}
</div> </div>
</a> </a>
@@ -64,6 +64,9 @@ onMount(() => {
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.small {
max-width: 10vw;
}
.base { .base {
/* The base name uses all available space */ /* The base name uses all available space */
max-width: unset; max-width: unset;

View File

@@ -1,11 +1,11 @@
<script lang="ts"> <script lang="ts">
import type { FSNavigator } from "./FSNavigator"; import type { FSNavigator } from "./FSNavigator";
import { fs_check_is_error, fs_node_icon, fs_share_hotlink_url, fs_share_url, fs_update, type FSNode, type FSPermissions } from "lib/FilesystemAPI.svelte"; import { fs_check_is_error, fs_node_icon, fs_share_hotlink_url, fs_share_url, fs_update, type FSNode, type FSPermissions } from "lib/FilesystemAPI.svelte";
import { copy_text } from "util/Util";
import CopyButton from "layout/CopyButton.svelte"; import CopyButton from "layout/CopyButton.svelte";
import Dialog from "layout/Dialog.svelte"; import Dialog from "layout/Dialog.svelte";
import { fade } from "svelte/transition"; import { fade } from "svelte/transition";
import { get_user } from "lib/NovaAPI"; import { get_user } from "lib/NovaAPI";
import Button from "layout/Button.svelte";
let { nav }: { let { nav }: {
nav: FSNavigator; nav: FSNavigator;
@@ -13,10 +13,8 @@ let { nav }: {
let path: FSNode[] let path: FSNode[]
let base: FSNode = $state() let base: FSNode = $state()
let toast = $state("")
let share_url = $state("") let share_url = $state("")
let direct_share_url = $state("") let direct_share_url = $state("")
let is_parent = $state(false)
let parent_node: FSNode = $state() let parent_node: FSNode = $state()
let dialog: Dialog = $state() let dialog: Dialog = $state()
@@ -71,15 +69,19 @@ const make_public = async () => {
} }
} }
let share_parent = $state(false)
let navigator_share = $state(false)
const share = async () => { const share = async () => {
// If the base node does not have a public ID then the file is shared // If the base node is not shared then the file is shared through a parent
// through a parent node. In that case we ask the user if it was their // node. In that case we ask the user if it was their intention to share the
// intention to share the parent directory // parent directory
is_parent = base.id === undefined share_parent = !base.is_shared()
if (is_parent) { navigator_share = navigator.share !== undefined
if (!base.is_shared()) {
// Walk path backwards looking for the last public ID // Walk path backwards looking for the last public ID
for (let i = path.length - 1; i >= 0; i--) { for (let i = path.length - 1; i >= 0; i--) {
if (path[i].id !== undefined && path[i].id !== "me") { if (path[i].is_shared()) {
parent_node = path[i] parent_node = path[i]
break break
} }
@@ -88,25 +90,17 @@ const share = async () => {
share_url = fs_share_url(path) share_url = fs_share_url(path)
direct_share_url = fs_share_hotlink_url(path) direct_share_url = fs_share_hotlink_url(path)
}
try { const nav_share = async (url: string) => {
await navigator.share({ await navigator.share({
title: base.name, title: base.name,
text: "I would like to share '" + base.name + "' with you", text: "I would like to share '" + base.name + "' with you",
url: share_url, url: url,
}) })
} catch(_) {
if (copy_text(share_url)) {
toast = "Link copied to clipboard"
setTimeout(() => {toast = ""}, 10000)
} else {
alert("Could not copy text")
}
}
} }
</script> </script>
<Dialog bind:this={dialog_not_allowed}> <Dialog bind:this={dialog_not_allowed}>
<div class="dialog_inner"> <div class="dialog_inner">
<div class="highlight_yellow" transition:fade> <div class="highlight_yellow" transition:fade>
@@ -118,15 +112,13 @@ const share = async () => {
<Dialog bind:this={dialog}> <Dialog bind:this={dialog}>
<div class="dialog_inner"> <div class="dialog_inner">
{#if toast !== ""}
<div class="highlight_green" transition:fade>{toast}</div>
<div class="separator" transition:fade></div>
{/if}
<div>Sharing link</div> <div>Sharing link</div>
<div class="link_copy"> <div class="link_copy">
<div class="button_container"> <div class="button_container">
<CopyButton text={share_url}>Copy</CopyButton> <CopyButton text={share_url}>Copy</CopyButton>
{#if navigator_share}
<Button icon="share" label="Share" click={() => {nav_share(share_url)}}/>
{/if}
</div> </div>
<a href="{share_url}">{share_url}</a> <a href="{share_url}">{share_url}</a>
</div> </div>
@@ -135,11 +127,14 @@ const share = async () => {
<div class="link_copy"> <div class="link_copy">
<div class="button_container"> <div class="button_container">
<CopyButton text={direct_share_url}>Copy</CopyButton> <CopyButton text={direct_share_url}>Copy</CopyButton>
{#if navigator_share}
<Button icon="share" label="Share" click={() => {nav_share(direct_share_url)}}/>
{/if}
</div> </div>
<a href="{direct_share_url}">{direct_share_url}</a> <a href="{direct_share_url}">{direct_share_url}</a>
</div> </div>
{#if is_parent} {#if share_parent}
<div class="separator"></div> <div class="separator"></div>
<div> <div>
This link also gives access to This link also gives access to

View File

@@ -40,7 +40,6 @@ export const copy_link = () => {
<FileStats nav={nav} bind:expanded={toolbar_expanded}/> <FileStats nav={nav} bind:expanded={toolbar_expanded}/>
<div class="grid" class:hide={!toolbar_expanded}> <div class="grid" class:hide={!toolbar_expanded}>
<div class="button_row"> <div class="button_row">
<button onclick={() => {nav.open_sibling(-1)}}> <button onclick={() => {nav.open_sibling(-1)}}>
<i class="icon">skip_previous</i> <i class="icon">skip_previous</i>
@@ -73,7 +72,11 @@ export const copy_link = () => {
{#if path_is_shared($nav.path)} {#if path_is_shared($nav.path)}
<button onclick={copy_link} class:button_highlight={link_copied}> <button onclick={copy_link} class:button_highlight={link_copied}>
<i class="icon">content_copy</i> <i class="icon">content_copy</i>
{#if link_copied}
<span>Link copied!</span>
{:else}
<span><u>C</u>opy link</span> <span><u>C</u>opy link</span>
{/if}
</button> </button>
{/if} {/if}

View File

@@ -6,7 +6,7 @@ import { branding_from_props } from "./Branding";
import FileOptions from "./FileOptions.svelte"; import FileOptions from "./FileOptions.svelte";
import SharingOptions from "./SharingOptions.svelte"; import SharingOptions from "./SharingOptions.svelte";
import type { FSNavigator } from "filesystem/FSNavigator"; import type { FSNavigator } from "filesystem/FSNavigator";
import { loading_finish, loading_start } from "lib/Loading"; import { loading_run } from "lib/Loading";
let file: FSNode = $state({} as FSNode) let file: FSNode = $state({} as FSNode)
let options: NodeOptions = $state({} as NodeOptions) let options: NodeOptions = $state({} as NodeOptions)
@@ -72,23 +72,22 @@ const save = async (e: SubmitEvent) => {
e.preventDefault() e.preventDefault()
console.debug("Saving file", file.path) console.debug("Saving file", file.path)
let new_file: FSNode
try { try {
loading_start() await loading_run(async () => {
options.branding_enabled = JSON.stringify(branding_enabled) options.branding_enabled = JSON.stringify(branding_enabled)
new_file = await fs_update(file.path, options) await fs_update(file.path, options)
// If the file name has changed we need to call fs_rename. This is a
// separate operation because a rename also moves the file
if (new_name !== file.name) { if (new_name !== file.name) {
let parent = file.path.slice(0, -file.name.length) let parent = file.path.slice(0, -file.name.length)
console.log("Moving", file.path, "to", parent+new_name) console.log("Moving", file.path, "to", parent+new_name)
await fs_rename(file.path, parent+new_name) await fs_rename(file.path, parent+new_name)
file.path = parent+new_name file.path = parent+new_name
new_file.name = new_name
new_file.path = file.path
} }
})
} catch (err) { } catch (err) {
if (err.message) { if (err.message) {
alert(err.message) alert(err.message)
@@ -97,11 +96,11 @@ const save = async (e: SubmitEvent) => {
alert(err) alert(err)
} }
return return
} finally {
loading_finish()
} }
if (open_after_edit) { // The navigate call will be debounced if the new path is equal to the
// current path. So we call reload() instead if that's the case
if (open_after_edit && $nav.base.path !== file.path) {
nav.navigate(file.path, false) nav.navigate(file.path, false)
} else { } else {
nav.reload() nav.reload()

View File

@@ -4,7 +4,6 @@ import CopyButton from "layout/CopyButton.svelte";
import { formatDate } from "util/Formatting"; import { formatDate } from "util/Formatting";
import { type FSNode, type NodeOptions } from "lib/FilesystemAPI.svelte"; import { type FSNode, type NodeOptions } from "lib/FilesystemAPI.svelte";
import AccessControl from "./AccessControl.svelte"; import AccessControl from "./AccessControl.svelte";
import { onMount } from 'svelte';
import { user } from "lib/UserStore"; import { user } from "lib/UserStore";
let { let {
@@ -15,27 +14,24 @@ let {
options: NodeOptions; options: NodeOptions;
} = $props(); } = $props();
let embed_html: string = $state() let is_shared = $derived(file.is_shared() || options.link_permissions?.read)
let preview_area: HTMLDivElement = $state() let embed_html: string = $derived.by(() => {
if (!is_shared) {
const embed_iframe = (file: FSNode, options: NodeOptions) => { return "File is not shared, can't generate embed code"
if (!file.is_shared()) {
example = false
embed_html = "File is not shared, can't generate embed code"
return
} }
let url = domain_url()+"/d/"+file.id let url = domain_url()+"/d/"+file.id
embed_html = `<iframe ` + return `<iframe ` +
`src="${url}" ` + `src="${url}" ` +
`style="border: none; width: 100%; max-width 90vw; height: 800px; max-height: 75vh; border-radius: 6px;" ` + `style="border: none; width: 100%; max-width 90vw; height: 800px; max-height: 75vh; border-radius: 6px;" ` +
`allowfullscreen` + `allowfullscreen` +
`></iframe>` `></iframe>`
} })
let preview_area: HTMLDivElement = $state()
let example = $state(false) let example = $state(false)
const toggle_example = () => { const toggle_example = () => {
if (file.is_shared()) { if (is_shared) {
example = !example example = !example
if (example) { if (example) {
preview_area.innerHTML = embed_html preview_area.innerHTML = embed_html
@@ -46,9 +42,6 @@ const toggle_example = () => {
} }
let share_link = $derived(window.location.protocol+"//"+window.location.host+"/d/"+file.id) let share_link = $derived(window.location.protocol+"//"+window.location.host+"/d/"+file.id)
onMount(() => {
embed_iframe(file, options)
});
</script> </script>
{#if $user.subscription.file_sharing === false} {#if $user.subscription.file_sharing === false}
@@ -93,8 +86,8 @@ onMount(() => {
<div class="center"> <div class="center">
<textarea bind:value={embed_html} style="width: 100%; height: 4em;"></textarea> <textarea bind:value={embed_html} style="width: 100%; height: 4em;"></textarea>
<br/> <br/>
<CopyButton text={embed_html}>Copy HTML</CopyButton> <CopyButton text={embed_html} disabled={!is_shared}>Copy HTML</CopyButton>
<button onclick={toggle_example} class:button_highlight={example} disabled={!file.is_shared()}> <button onclick={toggle_example} class:button_highlight={example} disabled={!is_shared}>
<i class="icon">visibility</i> Show example <i class="icon">visibility</i> Show example
</button> </button>
</div> </div>

View File

@@ -91,7 +91,8 @@ onMount(() => {
</div> </div>
<h2>Tracklist</h2> <h2>Tracklist</h2>
{#each siblings as sibling (sibling.path)} {#each siblings as sibling (sibling.id)}
{#if !sibling.is_hidden()}
<a href={"/d"+fs_encode_path(sibling.path)} class="node" class:playing={sibling.path === $nav.base.path}> <a href={"/d"+fs_encode_path(sibling.path)} class="node" class:playing={sibling.path === $nav.base.path}>
{#if sibling.path === $nav.base.path} {#if sibling.path === $nav.base.path}
<i class="play_arrow icon">play_arrow</i> <i class="play_arrow icon">play_arrow</i>
@@ -101,6 +102,7 @@ onMount(() => {
<span>{sibling.name}</span> <span>{sibling.name}</span>
<br/> <br/>
</a> </a>
{/if}
{/each} {/each}
</TextBlock> </TextBlock>
</div> </div>

View File

@@ -6,6 +6,7 @@ let {
style = "", style = "",
large_icon = false, large_icon = false,
small_icon = false, small_icon = false,
disabled = false,
children, children,
onclick, onclick,
}: { }: {
@@ -13,6 +14,7 @@ let {
style?: string; style?: string;
large_icon?: boolean; large_icon?: boolean;
small_icon?: boolean; small_icon?: boolean;
disabled?: boolean;
children?: import('svelte').Snippet; children?: import('svelte').Snippet;
onclick?: (e: MouseEvent) => void; onclick?: (e: MouseEvent) => void;
} = $props(); } = $props();
@@ -50,7 +52,7 @@ export const copy = (e: MouseEvent) => {
class:large_icon class:large_icon
class:small_icon class:small_icon
title="Copy text to clipboard" title="Copy text to clipboard"
disabled={text === ""} disabled={disabled}
> >
<i class="icon">content_copy</i> <i class="icon">content_copy</i>
<span> <span>

View File

@@ -190,7 +190,7 @@ export const fs_get_node = async (path: string) => {
// - shared, boolean. If true the node will receive a public ID // - shared, boolean. If true the node will receive a public ID
// //
// Returns the modified filesystem node object // Returns the modified filesystem node object
export const fs_update = async (path: string, opts: NodeOptions) => { export const fs_update = async (path: string, opts: NodeOptions): Promise<FSNode> => {
const form = new FormData() const form = new FormData()
form.append("action", "update") form.append("action", "update")

View File

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

View File

@@ -7,7 +7,6 @@ import { user } from "lib/UserStore";
import { put_user } from "lib/NovaAPI"; import { put_user } from "lib/NovaAPI";
let subscription: string = $state($user === null ? "" : $user.subscription.id) let subscription: string = $state($user === null ? "" : $user.subscription.id)
let subscription_type: string = $state($user === null ? "" : $user.subscription.type)
let success_message: SuccessMessage = $state() let success_message: SuccessMessage = $state()
const update = async (plan: string) => { const update = async (plan: string) => {
@@ -71,13 +70,6 @@ const update = async (plan: string) => {
<p> <p>
Here you can switch between different subscription plans. Here you can switch between different subscription plans.
</p> </p>
<p>
The Patreon subscription is managed by Patreon. Nova cannot modify
or end your subsciption. If you would like to cancel your Patreon plan
you can do that <a
href="https://www.patreon.com/settings/memberships/nova"
target="_blank">on Patreon</a>.
</p>
<p> <p>
The Prepaid subscription is charged daily based on usage. When you reach The Prepaid subscription is charged daily based on usage. When you reach
negative balance the subscription will automatically end. You need at negative balance the subscription will automatically end. You need at
@@ -91,41 +83,6 @@ const update = async (plan: string) => {
<SuccessMessage bind:this={success_message}/> <SuccessMessage bind:this={success_message}/>
<div class="feat_table"> <div class="feat_table">
<div>
<div class="feat_label" class:feat_highlight={subscription_type === "patreon"}>
Patreon<br/>
{#if subscription_type === "patreon"}
Currently active<br/>
<a class="button" href="https://www.patreon.com/settings/memberships/nova" target="_blank">
<i class="icon">settings</i>
Manage
</a>
{:else}
<a class="button" href="/api/patreon_auth/start">
<i class="icon">add_link</i>
Link Patreon
</a>
{/if}
</div>
<div class="feat_normal round_tr" class:feat_highlight={subscription_type === "patreon"}>
<p>
This subscription is managed by Patreon. You will need to <a
href="https://www.patreon.com/nova/membership"
target="_blank">purchase a plan on Patreon</a> before you
can activate this subscription. After your purchase you can
click the "Link Patreon" button and your account will be
upgraded.
</p>
<ul>
<li>€4 per month</li>
<li>No storage limit for file sharing</li>
<li>4 TB transfer limit (higher plans available)</li>
<li>Access to the <a href="/filesystem">filesystem</a></li>
<li>2 TB filesytem storage limit (higher plans available)</li>
</ul>
</div>
</div>
<div> <div>
<div class="feat_label" class:feat_highlight={subscription === "prepaid"}> <div class="feat_label" class:feat_highlight={subscription === "prepaid"}>
Prepaid<br/> Prepaid<br/>
@@ -141,11 +98,7 @@ const update = async (plan: string) => {
<div class="feat_normal round_tr" class:feat_highlight={subscription === "prepaid"}> <div class="feat_normal round_tr" class:feat_highlight={subscription === "prepaid"}>
<p> <p>
You will need a positive account balance to activate this You will need a positive account balance to activate this
plan. If you currently have a Patreon subscription active, plan. The plan deactivates when your credit runs out.
then enabling prepaid will not cancel that subscription. You
can end your subscription <a
href="https://www.patreon.com/settings/memberships/nova"
target="_blank">on Patreon.com</a>.
</p> </p>
<ul> <ul>
<li> <li>
@@ -157,12 +110,12 @@ const update = async (plan: string) => {
</li> </li>
<li> <li>
€1 per TB for data transfer (with <a €1 per TB for data transfer (with <a
href="/user/sharing/bandwidth">hotlinking</a> href="/user/sharing">hotlinking</a>
enabled) enabled)
</li> </li>
<li> <li>
File <a href="/user/sharing/embedding">embedding File <a href="/user/sharing">embedding control</a>
control</a> options options
</li> </li>
</ul> </ul>
</div> </div>
@@ -177,8 +130,8 @@ const update = async (plan: string) => {
</div> </div>
<div class="feat_normal round_br" class:feat_highlight={subscription === ""}> <div class="feat_normal round_br" class:feat_highlight={subscription === ""}>
<ul> <ul>
<li>Standard free plan, files expire after 120 days.</li> <li>Storage limit: 50 GB</li>
<li>Download limit of 6 GB per day</li> <li>Download limit: 50 GB per 30 days (sliding window)</li>
</ul> </ul>
</div> </div>
</div> </div>

View File

@@ -58,6 +58,6 @@ onMount(async () => {
<br/><br/> <br/><br/>
Egress used (30 days): Egress used (30 days):
(<a href="/user/sharing/bandwidth">set custom limit</a>) (<a href="/user/sharing">set custom limit</a>)
<HotlinkProgressBar used={transfer_used} total={transfer_cap}></HotlinkProgressBar> <HotlinkProgressBar used={transfer_used} total={transfer_cap}></HotlinkProgressBar>
{/if} {/if}