Compare commits
3 Commits
5b25f21f72
...
f3522f370f
| Author | SHA1 | Date | |
|---|---|---|---|
| f3522f370f | |||
| 912fe06ff6 | |||
| 0acc6a91bb |
@@ -1,28 +1,31 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { formatDataVolume, formatDate } from "util/Formatting";
|
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.svelte";
|
import { fs_check_response, fs_encode_path, fs_get_trash, fs_node_icon, fs_path_url, type TrashEntry } from "lib/FilesystemAPI.svelte";
|
||||||
import { loading_finish, loading_start } from "lib/Loading";
|
import { loading_finish, loading_run, loading_start, loading_store } from "lib/Loading";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
|
import { breadcrumbs_store } from "wrap/BreadcrumbStore";
|
||||||
|
import { FSNavigator } from "filesystem/FSNavigator";
|
||||||
|
|
||||||
let { root_path = "/me" }: { root_path?: string } = $props();
|
let {
|
||||||
|
nav,
|
||||||
|
root_path = "/me",
|
||||||
|
}: {
|
||||||
|
nav: FSNavigator
|
||||||
|
root_path?: string
|
||||||
|
} = $props();
|
||||||
|
|
||||||
let entries: TrashEntry[] = $state([]);
|
let entries: TrashEntry[] = $state([]);
|
||||||
let error: string = $state("");
|
let error: string = $state("");
|
||||||
let loaded = $state(false);
|
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
loaded = false;
|
loading_run(async () => {
|
||||||
entries = [];
|
try {
|
||||||
try {
|
entries = await fs_get_trash(root_path)
|
||||||
loading_start();
|
error = "";
|
||||||
entries = await fs_get_trash(root_path)
|
} catch (err: any) {
|
||||||
error = "";
|
error = err?.message ?? JSON.stringify(err);
|
||||||
} catch (err: any) {
|
}
|
||||||
error = err?.message ?? JSON.stringify(err);
|
})
|
||||||
} finally {
|
|
||||||
loaded = true;
|
|
||||||
loading_finish();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const delete_entry = async (entry: TrashEntry) => {
|
const delete_entry = async (entry: TrashEntry) => {
|
||||||
@@ -76,122 +79,112 @@ const empty_trash = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(load);
|
onMount(() => {
|
||||||
|
breadcrumbs_store.set(breadcrumbs)
|
||||||
|
|
||||||
|
load()
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="trash_container">
|
|
||||||
<div class="header">
|
{#snippet breadcrumbs()}
|
||||||
<h2 class="title"><i class="icon">delete</i> Trash</h2>
|
<i class="icon">delete</i>
|
||||||
<div class="actions">
|
<div>
|
||||||
<button onclick={load}>
|
Trash bin
|
||||||
<i class="icon">refresh</i>
|
{#if $loading_store === 0 && entries.length === 0}
|
||||||
<span>Refresh</span>
|
(empty)
|
||||||
</button>
|
{:else if entries.length !== 0}
|
||||||
{#if entries.length > 0}
|
({formatDataVolume(entries.reduce(
|
||||||
<button class="danger" onclick={empty_trash}>
|
(acc, entry) => acc + entry.node.file_size, 0
|
||||||
<i class="icon">delete_forever</i>
|
), 3)})
|
||||||
<span>Empty trash</span>
|
|
||||||
</button>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="separator"></div>
|
||||||
|
<button onclick={() => window.history.back()} title="Back">
|
||||||
|
<i class="icon">arrow_back</i>
|
||||||
|
</button>
|
||||||
|
<button onclick={() => nav.navigate_up()} disabled={$nav.path.length <= 1} title="Up">
|
||||||
|
<i class="icon">north</i>
|
||||||
|
</button>
|
||||||
|
<button onclick={() => nav.reload()} title="Refresh directory listing">
|
||||||
|
<i class="icon">refresh</i>
|
||||||
|
</button>
|
||||||
|
{#if entries.length !== 0}
|
||||||
|
<button class="button button_red" onclick={empty_trash}>
|
||||||
|
<i class="icon">delete_forever</i>
|
||||||
|
<span>Delete all</span>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
{#if error}
|
{#if error}
|
||||||
<p class="error">{error}</p>
|
<p class="error">{error}</p>
|
||||||
{:else if loaded && entries.length === 0}
|
{:else if entries.length > 0}
|
||||||
<p class="empty">The trash is empty.</p>
|
<table class="trash_table">
|
||||||
{:else if entries.length > 0}
|
<thead>
|
||||||
<table class="trash_table">
|
<tr>
|
||||||
<thead>
|
<td></td>
|
||||||
<tr>
|
<td>Name</td>
|
||||||
<td></td>
|
<td class="hide_small">Original location</td>
|
||||||
<td>Name</td>
|
<td class="hide_small">Deleted at</td>
|
||||||
<td class="hide_small">Original location</td>
|
<td class="hide_small">Size</td>
|
||||||
<td class="hide_small">Deleted</td>
|
<td></td>
|
||||||
<td class="hide_small">Size</td>
|
</tr>
|
||||||
<td></td>
|
</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">
|
||||||
|
{entry.original_path ? entry.original_path.slice(0, -entry.node.name.length) : "—"}
|
||||||
|
</td>
|
||||||
|
<td class="hide_small">
|
||||||
|
{entry.deletion_date ? formatDate(entry.deletion_date) : "—"}
|
||||||
|
</td>
|
||||||
|
<td class="hide_small">
|
||||||
|
{#if entry.node.type === "file"}
|
||||||
|
{formatDataVolume(entry.node.file_size, 3)}
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="row_actions">
|
||||||
|
<button title="Restore to original location" onclick={() => restore_entry(entry)}>
|
||||||
|
<i class="icon">restore_from_trash</i>
|
||||||
|
</button>
|
||||||
|
<button class="button button_red" title="Delete permanently" onclick={() => delete_entry(entry)}>
|
||||||
|
<i class="icon">delete_forever</i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
{/each}
|
||||||
<tbody>
|
</tbody>
|
||||||
{#each entries as entry (entry.node.id)}
|
</table>
|
||||||
<tr class="entry">
|
{/if}
|
||||||
<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>
|
<style>
|
||||||
.trash_container {
|
.separator {
|
||||||
max-width: 1200px;
|
flex: 1 1 auto;
|
||||||
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 {
|
.error {
|
||||||
color: var(--danger_color, #c0392b);
|
color: var(--danger_color, #c0392b);
|
||||||
}
|
}
|
||||||
.empty {
|
|
||||||
color: var(--body_text_color);
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
.trash_table {
|
.trash_table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
background: var(--body_background);
|
background: var(--body_background);
|
||||||
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
.trash_table td {
|
.trash_table td {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -205,7 +198,6 @@ onMount(load);
|
|||||||
}
|
}
|
||||||
.icon_cell {
|
.icon_cell {
|
||||||
width: 2.5em;
|
width: 2.5em;
|
||||||
padding: 0.3em 0.3em 0.3em 0.5em;
|
|
||||||
}
|
}
|
||||||
.node_icon {
|
.node_icon {
|
||||||
display: block;
|
display: block;
|
||||||
@@ -214,7 +206,6 @@ onMount(load);
|
|||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
.name_cell {
|
.name_cell {
|
||||||
padding: 0.3em 0.5em;
|
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
.entry_link {
|
.entry_link {
|
||||||
@@ -224,34 +215,10 @@ onMount(load);
|
|||||||
.entry_link:hover {
|
.entry_link:hover {
|
||||||
text-decoration: underline;
|
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 {
|
.row_actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.25em;
|
gap: 0.25em;
|
||||||
}
|
}
|
||||||
.danger {
|
|
||||||
color: var(--danger_color, #c0392b);
|
|
||||||
}
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.hide_small {
|
.hide_small {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export const seek = (delta: number) => {
|
|||||||
<Spinner/>
|
<Spinner/>
|
||||||
</div>
|
</div>
|
||||||
{:else if viewer_type === "dir" && $nav.base.name === ".Trash"}
|
{:else if viewer_type === "dir" && $nav.base.name === ".Trash"}
|
||||||
<TrashBin root_path={$nav.path[$nav.path.length - 2].path}/>
|
<TrashBin nav={nav} root_path={$nav.path[$nav.path.length - 2].path}/>
|
||||||
{:else if viewer_type === "dir"}
|
{:else if viewer_type === "dir"}
|
||||||
<FileManager nav={nav} upload_widget={upload_widget} edit_window={edit_window} search_bar={search_bar}>
|
<FileManager nav={nav} upload_widget={upload_widget} edit_window={edit_window} search_bar={search_bar}>
|
||||||
<CustomBanner path={$nav.path}/>
|
<CustomBanner path={$nav.path}/>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export type GenericResponse = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
|
id: string,
|
||||||
username: string,
|
username: string,
|
||||||
email: string,
|
email: string,
|
||||||
otp_enabled: boolean,
|
otp_enabled: boolean,
|
||||||
@@ -20,12 +21,10 @@ export type User = {
|
|||||||
filesystem_node_count: number,
|
filesystem_node_count: number,
|
||||||
is_admin: boolean,
|
is_admin: boolean,
|
||||||
balance_micro_eur: number,
|
balance_micro_eur: number,
|
||||||
hotlinking_enabled: boolean,
|
|
||||||
monthly_transfer_cap: number,
|
monthly_transfer_cap: number,
|
||||||
monthly_transfer_used: number,
|
monthly_transfer_used: number,
|
||||||
file_viewer_branding: Map<string, string>,
|
file_viewer_branding: Map<string, string>,
|
||||||
file_embed_domains: string,
|
embed_domains: string,
|
||||||
skip_file_viewer: boolean,
|
|
||||||
affiliate_user_name: string,
|
affiliate_user_name: string,
|
||||||
checkout_country: string,
|
checkout_country: string,
|
||||||
checkout_name: string,
|
checkout_name: string,
|
||||||
@@ -158,10 +157,12 @@ export const put_user = async (data: Object) => {
|
|||||||
))
|
))
|
||||||
|
|
||||||
// Update the user store
|
// Update the user store
|
||||||
for (let key of Object.keys(data)) {
|
user.update((user: User) => {
|
||||||
cached_user[key] = data[key]
|
for (let key of Object.keys(data)) {
|
||||||
}
|
user[key] = data[key]
|
||||||
user.set(cached_user)
|
}
|
||||||
|
return user
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const logout_user = async (redirect_path: string) => {
|
export const logout_user = async (redirect_path: string) => {
|
||||||
|
|||||||
@@ -1,148 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { preventDefault } from 'svelte/legacy';
|
|
||||||
import { onMount } from "svelte";
|
|
||||||
import Pro from "icons/Pro.svelte";
|
|
||||||
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($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 () => {
|
|
||||||
loading_start()
|
|
||||||
try {
|
|
||||||
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) {
|
|
||||||
success_message.set(false, "Failed to update subscription: "+err)
|
|
||||||
} finally {
|
|
||||||
loading_finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let toggle_hotlinking = () => {
|
|
||||||
hotlinking = !hotlinking
|
|
||||||
update()
|
|
||||||
}
|
|
||||||
|
|
||||||
let transfer_used = $state(0)
|
|
||||||
let load_transfer_used = () => {
|
|
||||||
let today = new Date()
|
|
||||||
let start = new Date()
|
|
||||||
start.setDate(start.getDate() - 30)
|
|
||||||
|
|
||||||
fetch(
|
|
||||||
window.api_endpoint + "/user/time_series/egress" +
|
|
||||||
"?start=" + start.toISOString() +
|
|
||||||
"&end=" + today.toISOString() +
|
|
||||||
"&interval=60"
|
|
||||||
).then(resp => {
|
|
||||||
if (!resp.ok) { return Promise.reject("Error: " + resp.status); }
|
|
||||||
return resp.json();
|
|
||||||
}).then(resp => {
|
|
||||||
transfer_used = resp.amounts.reduce((acc, cur) => { return acc + cur }, 0)
|
|
||||||
}).catch(e => {
|
|
||||||
console.error("Error requesting time series: " + e);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let toggle_skip_viewer = () => {
|
|
||||||
skip_viewer = !skip_viewer
|
|
||||||
update()
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
load_transfer_used()
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<h2><Pro/>Hotlinking (bandwidth sharing)</h2>
|
|
||||||
<SuccessMessage bind:this={success_message}></SuccessMessage>
|
|
||||||
|
|
||||||
<button onclick={toggle_hotlinking}>
|
|
||||||
{#if hotlinking}
|
|
||||||
<i class="icon green">check</i> ON (click to turn off)
|
|
||||||
{:else}
|
|
||||||
<i class="icon red">close</i> OFF (click to turn on)
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
<p>
|
|
||||||
When hotlinking is enabled all the bandwidth that your files use will be
|
|
||||||
subtracted from your data cap. Advertisements will be disabled on the
|
|
||||||
download pages for your files and download speed will be unlimited. The
|
|
||||||
rate limiting captcha for files is also disabled when hotlinking is on.
|
|
||||||
You can directly embed your file's download link anywhere, you don't
|
|
||||||
need to use the file viewer page.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2><Pro/>Bill shock limit</h2>
|
|
||||||
<p>
|
|
||||||
Billshock limit in terabytes per month (1 TB = 1000 GB). Set to 0 to
|
|
||||||
disable.
|
|
||||||
</p>
|
|
||||||
<form onsubmit={preventDefault(update)} class="billshock_container">
|
|
||||||
<input type="number" bind:value={transfer_cap} step="0.1" min="0" style="width: 5em;"/>
|
|
||||||
<div style="margin: 0.5em;">TB</div>
|
|
||||||
<button type="submit">
|
|
||||||
<i class="icon">save</i> Save
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Bandwidth used in the last 30 days: {formatDataVolume(transfer_used, 3)},
|
|
||||||
new limit: {formatDataVolume(transfer_cap*1e12, 3)}
|
|
||||||
</p>
|
|
||||||
<ProgressBar used={transfer_used} total={transfer_cap*1e12}></ProgressBar>
|
|
||||||
<p>
|
|
||||||
The billshock limit limits how much bandwidth your account can use in a
|
|
||||||
30 day window. When this limit is reached hotlinking will be disabled
|
|
||||||
and you will no longer be charged for bandwidth usage. This is mostly
|
|
||||||
useful for the Prepaid subscription, but it works for Patreon plans too.
|
|
||||||
Set to 0 to disable the limit.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2><Pro/>Skip download page</h2>
|
|
||||||
<button onclick={toggle_skip_viewer}>
|
|
||||||
{#if skip_viewer}
|
|
||||||
<i class="icon green">check</i> ON (click to turn off)
|
|
||||||
{:else}
|
|
||||||
<i class="icon red">close</i> OFF (click to turn on)
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
<p>
|
|
||||||
This setting only applies to your account, others will still see the
|
|
||||||
download page when visiting your files. When this is enabled you will be
|
|
||||||
redirected to the file download API when clicking a Nova link.
|
|
||||||
This means that images, videos and PDF files will be opened directly in
|
|
||||||
your browser, and other files are immediately downloaded to your device.
|
|
||||||
This only works for single file links, not albums. You will need a Pro
|
|
||||||
subscription to use this feature.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.green {
|
|
||||||
color: var(--highlight_color);
|
|
||||||
}
|
|
||||||
.red {
|
|
||||||
color: var(--danger_color);
|
|
||||||
}
|
|
||||||
.billshock_container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { onMount } from "svelte";
|
|
||||||
import SuccessMessage from "util/SuccessMessage.svelte";
|
|
||||||
import { get_endpoint, get_user, type User } from "lib/NovaAPI";
|
|
||||||
import { loading_run } from "lib/Loading";
|
|
||||||
|
|
||||||
let success_message: SuccessMessage
|
|
||||||
|
|
||||||
// Embedding settings
|
|
||||||
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", domain_list)
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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>Embedding controls</h2>
|
|
||||||
<SuccessMessage bind:this={success_message}></SuccessMessage>
|
|
||||||
{#if user !== undefined && !user.hotlinking_enabled}
|
|
||||||
<div class="highlight_yellow">
|
|
||||||
To use embedding restrictions hotlinking needs to be enabled.
|
|
||||||
Enable hotlinking on the
|
|
||||||
<a href="/user/sharing/bandwidth">sharing settings page</a>.
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<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. This applies to both
|
|
||||||
hotlink and iframe embeds. If the list is empty, all domains are allowed
|
|
||||||
to embed your files.
|
|
||||||
</p>
|
|
||||||
<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;
|
|
||||||
}
|
|
||||||
.shrink {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -7,12 +7,11 @@ import ConnectApp from "./ConnectApp.svelte";
|
|||||||
import ActivityLog from "./ActivityLog.svelte";
|
import ActivityLog from "./ActivityLog.svelte";
|
||||||
import DepositCredit from "./DepositCredit.svelte";
|
import DepositCredit from "./DepositCredit.svelte";
|
||||||
import TabMenu, { type Tab } from "util/TabMenu.svelte";
|
import TabMenu, { type Tab } from "util/TabMenu.svelte";
|
||||||
import BandwidthSharing from "./BandwidthSharing.svelte";
|
|
||||||
import EmbeddingControls from "./EmbeddingControls.svelte";
|
|
||||||
import Dashboard from "./dashboard/Dashboard.svelte";
|
import Dashboard from "./dashboard/Dashboard.svelte";
|
||||||
import AffiliatePrompt from "./AffiliatePrompt.svelte";
|
import AffiliatePrompt from "./AffiliatePrompt.svelte";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { get_user, type User } from "lib/NovaAPI";
|
import { get_user, type User } from "lib/NovaAPI";
|
||||||
|
import SharingSettings from "./SharingSettings.svelte";
|
||||||
|
|
||||||
let pages: Tab[] = [
|
let pages: Tab[] = [
|
||||||
{
|
{
|
||||||
@@ -50,21 +49,9 @@ let pages: Tab[] = [
|
|||||||
],
|
],
|
||||||
}, {
|
}, {
|
||||||
path: "/user/sharing",
|
path: "/user/sharing",
|
||||||
title: "Sharing",
|
title: "Sharing settings",
|
||||||
icon: "share",
|
icon: "share",
|
||||||
subpages: [
|
component: SharingSettings,
|
||||||
{
|
|
||||||
path: "/user/sharing/bandwidth",
|
|
||||||
title: "Sharing settings",
|
|
||||||
icon: "share",
|
|
||||||
component: BandwidthSharing,
|
|
||||||
}, {
|
|
||||||
path: "/user/sharing/embedding",
|
|
||||||
title: "Embedding Controls",
|
|
||||||
icon: "code",
|
|
||||||
component: EmbeddingControls,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}, {
|
}, {
|
||||||
path: "/user/connect_app",
|
path: "/user/connect_app",
|
||||||
title: "Apps",
|
title: "Apps",
|
||||||
|
|||||||
160
svelte/src/user_home/SharingSettings.svelte
Normal file
160
svelte/src/user_home/SharingSettings.svelte
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { formatDataVolume } from "util/Formatting";
|
||||||
|
import ProgressBar from "util/ProgressBar.svelte";
|
||||||
|
import { loading_run } from "lib/Loading";
|
||||||
|
import { get_endpoint, put_user } from 'lib/NovaAPI';
|
||||||
|
import SuccessMessage from 'util/SuccessMessage.svelte';
|
||||||
|
import { user } from "lib/UserStore";
|
||||||
|
|
||||||
|
let success_message: SuccessMessage = $state()
|
||||||
|
let transfer_cap = $state($user === null ? 0 : $user.monthly_transfer_cap / 1e12)
|
||||||
|
|
||||||
|
const update_user = async (e: SubmitEvent) => {
|
||||||
|
if (e !== null) {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
loading_run(async () => {
|
||||||
|
try {
|
||||||
|
await put_user({
|
||||||
|
transfer_cap: transfer_cap*1e12,
|
||||||
|
embed_domains: embed_domains.join(" "),
|
||||||
|
})
|
||||||
|
|
||||||
|
success_message.set(true, "Sharing settings updated")
|
||||||
|
} catch (err) {
|
||||||
|
success_message.set(false, "Failed to update user: "+err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let transfer_used = $state(0)
|
||||||
|
let load_transfer_used = () => {
|
||||||
|
let today = new Date()
|
||||||
|
let start = new Date()
|
||||||
|
start.setDate(start.getDate() - 30)
|
||||||
|
|
||||||
|
fetch(
|
||||||
|
get_endpoint() + "/user/time_series/egress" +
|
||||||
|
"?start=" + start.toISOString() +
|
||||||
|
"&end=" + today.toISOString() +
|
||||||
|
"&interval=60"
|
||||||
|
).then(resp => {
|
||||||
|
if (!resp.ok) { return Promise.reject("Error: " + resp.status); }
|
||||||
|
return resp.json();
|
||||||
|
}).then(resp => {
|
||||||
|
transfer_used = resp.amounts.reduce((acc, cur) => { return acc + cur }, 0)
|
||||||
|
}).catch(e => {
|
||||||
|
console.error("Error requesting time series: " + e);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Embedding settings
|
||||||
|
let embed_domains: string[] = $state($user === null ? [] : $user.embed_domains.split(" "))
|
||||||
|
let new_domain: string = $state("")
|
||||||
|
|
||||||
|
const add_domain = (e: SubmitEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
console.debug("Adding domain", new_domain)
|
||||||
|
embed_domains.push(new_domain)
|
||||||
|
embed_domains = embed_domains
|
||||||
|
new_domain = ""
|
||||||
|
update_user(e)
|
||||||
|
}
|
||||||
|
const del_domain = (index: number) => {
|
||||||
|
embed_domains.splice(index, 1)
|
||||||
|
embed_domains = embed_domains
|
||||||
|
update_user(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
load_transfer_used()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<SuccessMessage bind:this={success_message}/>
|
||||||
|
|
||||||
|
<h2>Bill shock limit</h2>
|
||||||
|
<p>
|
||||||
|
Billshock limit in terabytes per month (1 TB = 1000 GB). Set to 0 to
|
||||||
|
disable.
|
||||||
|
</p>
|
||||||
|
<form onsubmit={update_user} class="billshock_container">
|
||||||
|
<input type="number" bind:value={transfer_cap} step="0.1" min="0" style="width: 5em;"/>
|
||||||
|
<div style="margin: 0.5em;">TB</div>
|
||||||
|
<button type="submit">
|
||||||
|
<i class="icon">save</i> Save
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Bandwidth used in the last 30 days: {formatDataVolume(transfer_used, 3)},
|
||||||
|
new limit: {formatDataVolume(transfer_cap*1e12, 3)}
|
||||||
|
</p>
|
||||||
|
<ProgressBar used={transfer_used} total={transfer_cap*1e12}></ProgressBar>
|
||||||
|
<p>
|
||||||
|
The billshock limit limits how much bandwidth your account can use in a
|
||||||
|
30 day window. When this limit is reached hotlinking will be disabled
|
||||||
|
and you will no longer be charged for bandwidth usage. This is mostly
|
||||||
|
useful for the Prepaid subscription, but it works for Patreon plans too.
|
||||||
|
Set to 0 to disable the limit.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Embedding controls</h2>
|
||||||
|
<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. This applies to both
|
||||||
|
hotlink and iframe embeds. If the list is empty, all domains are allowed
|
||||||
|
to embed your files.
|
||||||
|
</p>
|
||||||
|
<div class="highlight_border">
|
||||||
|
<form onsubmit={add_domain}>
|
||||||
|
<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" onclick={() => del_domain(index)}>
|
||||||
|
<i class="icon">delete</i>
|
||||||
|
</button>
|
||||||
|
<div>{domain}</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.billshock_container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
.shrink {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -236,6 +236,10 @@ const set_offset = (off: number) => {
|
|||||||
<i class="icon">palette</i>
|
<i class="icon">palette</i>
|
||||||
<span>Themes</span>
|
<span>Themes</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a class="button" href="https://docs.nova.storage" use:highlight_current_page>
|
||||||
|
<i class="icon">text_snippet</i>
|
||||||
|
<span>Documentation</span>
|
||||||
|
</a>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</MenuEntry>
|
</MenuEntry>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user