Replace user home page with user dashboard

This commit is contained in:
2024-07-09 18:18:26 +02:00
parent a6c8ee4263
commit ff38a54ae4
19 changed files with 1178 additions and 384 deletions

View File

@@ -78,41 +78,49 @@ const stats_update = () => {
<div class="stats_box"> <div class="stats_box">
<div> <div>
Size {formatDataVolume(total_size, 3)} <div>
Size {formatDataVolume(total_size, 3)}
</div>
<div>
Progress {(total_progress*100).toPrecision(3)}%
</div>
</div> </div>
<div> <div>
Progress {(total_progress*100).toPrecision(3)}% {#if finished}
<div>
Time {formatDuration(elapsed_time, 0)}
</div>
<div>
Rate {formatDataVolume(total_loaded / (elapsed_time/1000), 3)}/s
</div>
{:else}
<div>
ETA {formatDuration(remaining_time, 0)}
</div>
<div>
Rate {formatDataVolume(total_rate, 3)}/s
</div>
{/if}
</div> </div>
{#if finished}
<div>
Time {formatDuration(elapsed_time, 0)}
</div>
<div>
Rate {formatDataVolume(total_loaded / (elapsed_time/1000), 3)}/s
</div>
{:else}
<div>
ETA {formatDuration(remaining_time, 0)}
</div>
<div>
Rate {formatDataVolume(total_rate, 3)}/s
</div>
{/if}
</div> </div>
<ProgressBar total={total_size} used={total_loaded} animation="linear" speed={stats_interval_ms}/> <ProgressBar total={total_size} used={total_loaded} animation="linear" speed={stats_interval_ms}/>
<style> <style>
.stats_box { .stats_box {
display: inline-grid; display: inline-flex;
grid-template-columns: 25% 25% 25% 25%; flex-direction: row;
flex-wrap: wrap;
width: 100%; width: 100%;
text-align: center; text-align: center;
font-family: sans-serif, monospace;
} }
@media (max-width: 1000px) { .stats_box > div {
.stats_box { flex: 1 1 auto;
grid-template-columns: 50% 50%; display: flex;
} flex-direction: row;
}
.stats_box > div > div {
flex: 1 1 auto;
min-width: 150px;
} }
</style> </style>

View File

@@ -82,5 +82,6 @@ let click_int = e => {
background: none; background: none;
margin: 0; margin: 0;
color: var(--body_text_color); color: var(--body_text_color);
box-shadow: none;
} }
</style> </style>

View File

@@ -22,7 +22,7 @@ let form = {
const form = new FormData() const form = new FormData()
form.append("username", fields.username) form.append("username", fields.username)
form.append("password", fields.password) form.append("password", fields.password)
form.append("app_name", "website_login") form.append("app_name", "website login")
const resp = await fetch( const resp = await fetch(
window.api_endpoint+"/user/login", window.api_endpoint+"/user/login",

View File

@@ -66,7 +66,7 @@ let form = {
const login_form = new FormData() const login_form = new FormData()
login_form.append("username", fields.username) login_form.append("username", fields.username)
login_form.append("password", fields.password) login_form.append("password", fields.password)
login_form.append("app_name", "website_login") login_form.append("app_name", "website login")
const login_resp = await fetch( const login_resp = await fetch(
window.api_endpoint+"/user/login", window.api_endpoint+"/user/login",

View File

@@ -1,341 +0,0 @@
<script>
import { onMount } from "svelte";
import { formatDataVolume, formatThousands } from "../util/Formatting.svelte";
import Chart from "../util/Chart.svelte";
import StorageProgressBar from "./StorageProgressBar.svelte";
import HotlinkProgressBar from "./HotlinkProgressBar.svelte";
import Euro from "../util/Euro.svelte"
import { color_by_name } from "../util/Util.svelte";
let graph_views_downloads = null
let graph_bandwidth = null
let time_start = ""
let time_end = ""
let load_graphs = async (minutes, interval) => {
let end = new Date()
let start = new Date()
start.setMinutes(start.getMinutes() - minutes)
try {
let views = get_graph_data("views", start, end, interval);
let downloads = get_graph_data("downloads", start, end, interval);
let bandwidth = get_graph_data("bandwidth", start, end, interval);
let transfer_paid = get_graph_data("transfer_paid", start, end, interval);
views = await views
downloads = await downloads
bandwidth = await bandwidth
transfer_paid = await transfer_paid
graph_views_downloads.data().labels = views.timestamps;
graph_views_downloads.data().datasets[0].data = views.amounts
graph_views_downloads.data().datasets[1].data = downloads.amounts
graph_bandwidth.data().labels = bandwidth.timestamps;
graph_bandwidth.data().datasets[0].data = bandwidth.amounts
graph_bandwidth.data().datasets[1].data = transfer_paid.amounts
graph_views_downloads.update()
graph_bandwidth.update()
time_start = views.timestamps[0];
time_end = views.timestamps.slice(-1)[0];
} catch (err) {
console.error("Failed to update graphs", err)
return
}
}
let total_views = 0
let total_downloads = 0
let total_bandwidth = 0
let total_transfer_paid = 0
let get_graph_data = async (stat, start, end, interval) => {
let resp = await fetch(
window.api_endpoint + "/user/time_series/" + stat +
"?start=" + start.toISOString() +
"&end=" + end.toISOString() +
"&interval=" + interval
)
resp = await resp.json()
// Convert the timestamps to a human-friendly format
resp.timestamps.forEach((val, idx) => {
let date = new Date(val);
let str = date.getFullYear();
str += "-" + ("00" + (date.getMonth() + 1)).slice(-2);
str += "-" + ("00" + date.getDate()).slice(-2);
str += " " + ("00" + date.getHours()).slice(-2);
str += ":" + ("00" + date.getMinutes()).slice(-2);
resp.timestamps[idx] = " " + str + " "; // Poor man's padding
});
// Add up the total amount and save it in the correct place
let total = resp.amounts.reduce((acc, cur) => { return acc + cur }, 0)
if (stat == "views") {
total_views = total;
} else if (stat == "downloads") {
total_downloads = total;
graph_views_downloads.update()
} else if (stat == "bandwidth") {
total_bandwidth = total;
} else if (stat == "transfer_paid") {
total_transfer_paid = total;
}
return resp
}
let graph_timeout = null
let graph_timespan = 0
let update_graphs = (minutes, interval, live) => {
if (graph_timeout !== null) { clearTimeout(graph_timeout) }
if (live) {
graph_timeout = setTimeout(() => { update_graphs(minutes, interval, true) }, 6000)
}
graph_timespan = minutes
load_graphs(minutes, interval)
load_direct_bw()
}
let transfer_cap = 0
let transfer_used = 0
let storage_space_used = 0
let load_direct_bw = () => {
let today = new Date()
let start = new Date()
start.setDate(start.getDate() - 30)
fetch(
window.api_endpoint + "/user/time_series/transfer_paid" +
"?start=" + start.toISOString() +
"&end=" + today.toISOString() +
"&interval=60"
).then(resp => {
if (!resp.ok) { return Promise.reject("Error: " + resp.status); }
return resp.json();
}).then(resp => {
let total = resp.amounts.reduce((accum, val) => accum += val, 0);
transfer_used = total
storage_space_used = window.user.storage_space_used
}).catch(e => {
console.error("Error requesting time series: " + e);
})
}
onMount(() => {
if (window.user.monthly_transfer_cap > 0) {
transfer_cap = window.user.monthly_transfer_cap
} else if (window.user.subscription.monthly_transfer_cap > 0) {
transfer_cap = window.user.subscription.monthly_transfer_cap
} else {
transfer_cap = -1
}
graph_views_downloads.data().datasets = [
{
label: "Views",
borderWidth: 2,
pointRadius: 0,
borderColor: color_by_name("highlight_color"),
backgroundColor: color_by_name("highlight_color"),
},
{
label: "Downloads",
borderWidth: 2,
pointRadius: 0,
borderColor: color_by_name("danger_color"),
backgroundColor: color_by_name("danger_color"),
},
];
graph_bandwidth.data().datasets = [
{
label: "Total bandwidth",
borderWidth: 2,
pointRadius: 0,
borderColor: color_by_name("highlight_color"),
backgroundColor: color_by_name("highlight_color"),
},
{
label: "Premium bandwidth",
borderWidth: 2,
pointRadius: 0,
borderColor: color_by_name("danger_color"),
backgroundColor: color_by_name("danger_color"),
}
];
update_graphs(43200, 1440, true);
return () => {
if (graph_timeout !== null) {
clearTimeout(graph_timeout)
}
}
})
</script>
<section>
<h2>Account information</h2>
<ul>
<li>Username: {window.user.username}</li>
<li>
{#if window.user.email === ""}
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 page</a>.
{:else}
E-mail address: {window.user.email}
(<a href="/user/settings">configure</a>)
{/if}
</li>
<li>
Supporter level: {window.user.subscription.name}
(<a href="/user/subscription">manage subscriptions</a> /
<a href="/api/patreon_auth/start">link patreon subscription</a>)
<ul>
<li>
Max file size: {formatDataVolume(window.user.subscription.file_size_limit, 3)}
</li>
{#if window.user.subscription.file_expiry_days > 0}
<li>Files expire after {window.user.subscription.file_expiry_days} days</li>
{:else}
<li>Files never expire</li>
{/if}
</ul>
</li>
<li>
Current account balance: <Euro amount={window.user.balance_micro_eur}></Euro>
(<a href="/user/prepaid/deposit">deposit credit</a> /
<a href="/user/prepaid/transactions">transaction log</a>)
{#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>
{/if}
</li>
</ul>
{#if window.user.subscription.storage_space === -1}
Storage space used: {formatDataVolume(storage_space_used, 3)}<br/>
{:else}
<StorageProgressBar used={storage_space_used} total={window.user.subscription.storage_space}></StorageProgressBar>
{/if}
{#if transfer_cap === -1}
Premium transfer used in the last 30 days: {formatDataVolume(transfer_used, 3)}
(<a href="/user/sharing/bandwidth">configure limit</a>)
{:else}
Transfer cap:
{formatDataVolume(transfer_used, 3)}
out of
{formatDataVolume(transfer_cap, 3)}
(<a href="/user/sharing/bandwidth">Set your transfer limit on the sharing page</a>)
<HotlinkProgressBar used={transfer_used} total={transfer_cap}></HotlinkProgressBar>
{/if}
<h3>Exports</h3>
<div style="text-align: center;">
<a href="/user/export/files" class="button">
<i class="icon">list</i>
Export uploaded files to CSV
</a>
<a href="/user/export/lists" class="button">
<i class="icon">list</i>
Export created lists to CSV
</a>
</div>
<h2>Statistics</h2>
<p>
Here you can see how often your files are viewed, downloaded and how
much bandwidth they consume. The buttons below can be pressed to adjust
the timeframe.
</p>
<div class="highlight_border">
<button
on:click={() => update_graphs(1440, 1, true)}
class:button_highlight={graph_timespan == 1440}>
Day (1m)
</button>
<button
on:click={() => update_graphs(10080, 60, true)}
class:button_highlight={graph_timespan == 10080}>
Week (1h)
</button>
<button
on:click={() => update_graphs(20160, 60, true)}
class:button_highlight={graph_timespan == 20160}>
Two Weeks (1h)
</button>
<button
on:click={() => update_graphs(43200, 1440, false)}
class:button_highlight={graph_timespan == 43200}>
Month (1d)
</button>
<button
on:click={() => update_graphs(131400, 1440, false)}
class:button_highlight={graph_timespan == 131400}>
Quarter (1d)
</button>
<button
on:click={() => update_graphs(525600, 1440, false)}
class:button_highlight={graph_timespan == 525600}>
Year (1d)
</button>
<button
on:click={() => update_graphs(1051200, 1440, false)}
class:button_highlight={graph_timespan == 1051200}>
Two Years (1d)
</button>
</div>
<h3>Premium transfers and total bandwidth usage</h3>
<p>
Total bandwidth usage is the combined bandwidth usage of all the files
on your account. This includes paid transfers.
</p>
<p>
A premium transfer is when a file is downloaded using the data cap on
your subscription plan. These can be files you downloaded from other
people, or other people downloading your files if you have bandwidth
sharing enabled. Bandwidth sharing can be changed on
<a href="/user/sharing/bandwidth">the sharing page</a>.
</p>
</section>
<Chart bind:this={graph_bandwidth} data_type="bytes"/>
<section>
<div class="highlight_border">
Total usage from {time_start} to {time_end}<br/>
{formatDataVolume(total_bandwidth, 3)} bandwidth,
{formatDataVolume(total_transfer_paid, 3)} paid transfers
</div>
<h3>Views and downloads</h3>
<p>
A view is counted when someone visits the download page of one of
your files. Views are unique per user per file.
</p>
<p>
Downloads are counted when a user clicks the download button on one
of your files. It does not matter whether the download is completed
or not, only the start of the download is counted.
</p>
</section>
<Chart bind:this={graph_views_downloads} data_type="number"/>
<section>
<div class="highlight_border">
Total usage from {time_start} to {time_end}<br/>
{formatThousands(total_views)} views and
{formatThousands(total_downloads)} downloads
</div>
</section>

View File

@@ -1,5 +1,6 @@
<script> <script>
import ProgressBar from "../util/ProgressBar.svelte"; import ProgressBar from "../util/ProgressBar.svelte";
import { formatDataVolume } from "../util/Formatting.svelte"
export let total = 0 export let total = 0
export let used = 0 export let used = 0
@@ -8,6 +9,18 @@ $: frac = used / total
</script> </script>
<ProgressBar total={total} used={used}></ProgressBar> <ProgressBar total={total} used={used}></ProgressBar>
<div class="gauge_labels">
<div>
{formatDataVolume(used, 3)}
</div>
<div>
{#if total <= 0}
No limit
{:else}
{formatDataVolume(total, 3)}
{/if}
</div>
</div>
{#if frac > 1} {#if frac > 1}
<div class="highlight_yellow"> <div class="highlight_yellow">
@@ -53,3 +66,13 @@ $: frac = used / total
</a> </a>
</div> </div>
{/if} {/if}
<style>
.gauge_labels {
display: flex;
justify-content: space-between;
}
.gauge_labels > div {
flex: 0 0 auto;
}
</style>

View File

@@ -1,5 +1,4 @@
<script> <script>
import Home from "./Home.svelte";
import AccountSettings from "./AccountSettings.svelte"; import AccountSettings from "./AccountSettings.svelte";
import APIKeys from "./APIKeys.svelte"; import APIKeys from "./APIKeys.svelte";
import Transactions from "./Transactions.svelte"; import Transactions from "./Transactions.svelte";
@@ -11,13 +10,15 @@ import TabMenu from "../util/TabMenu.svelte";
import BandwidthSharing from "./BandwidthSharing.svelte"; import BandwidthSharing from "./BandwidthSharing.svelte";
import EmbeddingControls from "./EmbeddingControls.svelte"; import EmbeddingControls from "./EmbeddingControls.svelte";
import PageBranding from "./PageBranding.svelte"; import PageBranding from "./PageBranding.svelte";
import Dashboard from "./dashboard/Dashboard.svelte";
let pages = [ let pages = [
{ {
path: "/user/home", path: "/user",
title: "My Home", title: "Dashboard",
icon: "home", icon: "dashboard",
component: Home, component: Dashboard,
hide_background: true,
}, { }, {
path: "/user/settings", path: "/user/settings",
title: "Settings", title: "Settings",

View File

@@ -7,12 +7,19 @@ export let used = 0
$: frac = used / total $: frac = used / total
</script> </script>
Storage:
{formatDataVolume(used, 3)}
out of
{formatDataVolume(total, 3)}
<br/>
<ProgressBar total={total} used={used}></ProgressBar> <ProgressBar total={total} used={used}></ProgressBar>
<div class="gauge_labels">
<div>
{formatDataVolume(used, 3)}
</div>
<div>
{#if total <= 0}
No limit
{:else}
{formatDataVolume(total, 3)}
{/if}
</div>
</div>
{#if frac > 2.0} {#if frac > 2.0}
<div class="highlight_red"> <div class="highlight_red">
@@ -61,4 +68,13 @@ out of
font-weight: bold; font-weight: bold;
font-size: 1.5em; font-size: 1.5em;
} }
.gauge_labels {
display: flex;
justify-content: space-between;
line-height: 1em;
}
.gauge_labels > div {
flex: 0 0 auto;
}
</style> </style>

View File

@@ -0,0 +1,67 @@
<ul>
<li>Username: {window.user.username}</li>
<li>
{#if window.user.email === ""}
No e-mail address configured. You will not be able to recover
your account if you lose your password.
{:else}
E-mail address: {window.user.email}
{/if}
</li>
<li>
<i class="icon">settings</i>
<a href="/user/settings">Account settings</a>
</li>
</ul>
<h3>Quick navigation</h3>
<div class="button_row">
<a href="/user/filemanager#files" class="button">
<i class="icon">image</i>
My Files
</a>
<a href="/user/filemanager#lists" class="button">
<i class="icon">photo_library</i>
My Albums
</a>
{#if window.user.subscription.filesystem_access}
<a href="/d/me" class="button">
<i class="icon">folder</i>
Filesystem
</a>
{/if}
<a href="/logout" class="button">
<i class="icon">logout</i>
Log out
</a>
</div>
<h3>Exports</h3>
<div class="button_row">
<a href="/user/export/files" class="button">
<i class="icon">list</i>
Export files to CSV
</a>
<a href="/user/export/lists" class="button">
<i class="icon">list</i>
Export albums to CSV
</a>
</div>
<style>
.button_row {
display: flex;
flex-wrap: wrap;
}
.button_row > a {
flex: 1 1 auto;
}
ul {
margin: 0;
}
h3 {
font-size: 1.3em;
}
</style>

View File

@@ -0,0 +1,138 @@
<script>
import { onMount } from "svelte";
import { formatDate } from "../../util/Formatting.svelte";
import LoadingIndicator from "../../util/LoadingIndicator.svelte";
import Button from "../../layout/Button.svelte";
let loading = false
let year = 0
let month = 0
let month_str = ""
let data = []
const load_activity = async () => {
loading = true
month_str = year + "-" + ("00"+(month)).slice(-2)
try {
const resp = await fetch(window.api_endpoint+"/user/activity/" + month_str)
if(resp.status >= 400) {
let json = await resp.json()
if (json.value === "authentication_failed") {
window.location = "/login"
return
} else {
throw new Error(json.message)
}
}
data = await resp.json()
} catch (err) {
alert(err)
} finally {
loading = false
}
};
const last_month = () => {
month--
if (month === 0) {
month = 12
year--
}
load_activity()
}
const next_month = () => {
month++
if (month === 13) {
month = 1
year++
}
load_activity()
}
onMount(() => {
let now = new Date()
year = now.getFullYear()
month = now.getMonth()+1
load_activity()
})
</script>
<LoadingIndicator loading={loading}/>
<div class="toolbar">
<Button click={last_month} icon="chevron_left"/>
<div class="toolbar_spacer">
{month_str}
</div>
<Button click={next_month} icon="chevron_right"/>
</div>
{#if data.length === 0}
<div class="center">
Removed or expired files will show up here
</div>
{:else}
<div class="table_scroll">
<table style="text-align: left;">
<thead>
<tr>
<td>Time</td>
<td>File name</td>
<td>Event</td>
</tr>
</thead>
<tbody>
{#each data as row}
<tr>
<td>
{formatDate(row.time, true, true, false)}
</td>
<td>
{#if row.event === "file_instance_blocked"}
<a href="/u/{row.file_id}">{row.file_name}</a>
{:else if row.event === "filesystem_node_blocked"}
<a href="/d/{row.file_id}">{row.file_name}</a>
{:else}
{row.file_name}
{/if}
</td>
<td>
{#if row.event === "file_instance_blocked" || row.event === "filesystem_node_blocked"}
Blocked for abuse
{:else if row.event === "file_instance_expired"}
Expired
{:else if row.event === "file_instance_lost"}
File has been lost
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
<style>
.toolbar {
display: flex;
flex-direction: row;
width: 100%;
margin-top: 4px;
}
.toolbar > * {
flex: 0 0 auto;
}
.toolbar_spacer {
flex: 1 1 auto;
text-align: center;
align-content: center;
font-size: 1.2em;
}
.center {
text-align: center;
}
</style>

View File

@@ -0,0 +1,142 @@
<script>
import { onMount } from "svelte";
import { formatDataVolume, formatDate } from "../../util/Formatting.svelte";
import Euro from "../../util/Euro.svelte"
import LoadingIndicator from "../../util/LoadingIndicator.svelte";
import Button from "../../layout/Button.svelte";
let loading = false
let year = 0
let month = 0
let month_str = ""
let transactions = {
rows: [],
total_subscription_charge: 0,
total_storage_used: 0,
total_storage_charge: 0,
total_bandwidth_used: 0,
total_bandwidth_charge: 0,
total_deposited: 0,
total_deducted: 0,
}
const load_transactions = async () => {
loading = true
month_str = year + "-" + ("00"+(month)).slice(-2)
try {
const resp = await fetch(window.api_endpoint+"/user/transactions/"+month_str)
if(resp.status >= 400) {
let json = await resp.json()
if (json.value === "authentication_failed") {
window.location = "/login"
return
} else {
throw new Error(json.message)
}
}
let month = {
rows: await resp.json(),
total_subscription_charge: 0,
total_storage_used: 0,
total_storage_charge: 0,
total_bandwidth_used: 0,
total_bandwidth_charge: 0,
total_deposited: 0,
total_deducted: 0,
}
month.rows.forEach(row => {
row.time = new Date(row.time)
month.total_deposited += row.deposit_amount
month.total_subscription_charge += row.subscription_charge
month.total_storage_used += row.storage_used
month.total_storage_charge += row.storage_charge
month.total_bandwidth_used += row.bandwidth_used
month.total_bandwidth_charge += row.bandwidth_charge
month.total_deducted += row.subscription_charge + row.storage_charge + row.bandwidth_charge
})
// Divide the total storage usage by the number of days in a month
month.total_storage_used /= 30.4375
transactions = month
} catch (err) {
alert(err)
} finally {
loading = false
}
};
const last_month = () => {
month--
if (month === 0) {
month = 12
year--
}
load_transactions()
}
const next_month = () => {
month++
if (month === 13) {
month = 1
year++
}
load_transactions()
}
onMount(() => {
let now = new Date()
year = now.getFullYear()
month = now.getMonth()+1
load_transactions()
})
</script>
<LoadingIndicator loading={loading}/>
<div class="toolbar">
<Button click={last_month} icon="chevron_left"/>
<div class="toolbar_spacer">
{month_str}
</div>
<Button click={next_month} icon="chevron_right"/>
</div>
<ul>
<li>
Total charge: <Euro amount={transactions.total_deducted} precision="4"/>
</li>
<li>
Subscription charge: <Euro amount={transactions.total_subscription_charge} precision="4"/>
</li>
<li>
Storage charge: <Euro amount={transactions.total_storage_charge} precision="4"/>
(used {formatDataVolume(transactions.total_storage_used, 3)})
</li>
<li>
Bandwidth charge: <Euro amount={transactions.total_bandwidth_charge} precision="4"/>
(used {formatDataVolume(transactions.total_bandwidth_used, 3)})
</li>
<li>
Deposited: <Euro amount={transactions.total_deposited} precision="4"/>
</li>
</ul>
<style>
.toolbar {
display: flex;
flex-direction: row;
width: 100%;
margin-top: 4px;
}
.toolbar > * { flex: 0 0 auto; }
.toolbar_spacer {
flex: 1 1 auto;
text-align: center;
align-content: center;
font-size: 1.2em;
}
</style>

View File

@@ -0,0 +1,180 @@
<script>
import { onMount } from "svelte";
import Chart from "../../util/Chart.svelte";
import { color_by_name } from "../../util/Util.svelte";
import { formatDataVolume, formatThousands } from "../../util/Formatting.svelte";
let graph_views_downloads = null
let graph_bandwidth = null
let load_graphs = async (minutes, interval) => {
let end = new Date()
let start = new Date()
start.setMinutes(start.getMinutes() - minutes)
try {
let views = get_graph_data("views", start, end, interval);
let downloads = get_graph_data("downloads", start, end, interval);
let bandwidth = get_graph_data("bandwidth", start, end, interval);
let transfer_paid = get_graph_data("transfer_paid", start, end, interval);
views = await views
downloads = await downloads
bandwidth = await bandwidth
transfer_paid = await transfer_paid
graph_views_downloads.data().labels = views.timestamps;
graph_views_downloads.data().datasets[0].data = views.amounts
graph_views_downloads.data().datasets[1].data = downloads.amounts
graph_bandwidth.data().labels = bandwidth.timestamps;
graph_bandwidth.data().datasets[0].data = bandwidth.amounts
graph_bandwidth.data().datasets[1].data = transfer_paid.amounts
graph_views_downloads.update()
graph_bandwidth.update()
} catch (err) {
console.error("Failed to update graphs", err)
return
}
}
let total_views = 0
let total_downloads = 0
let total_bandwidth = 0
let total_transfer_paid = 0
let get_graph_data = async (stat, start, end, interval) => {
let resp = await fetch(
window.api_endpoint + "/user/time_series/" + stat +
"?start=" + start.toISOString() +
"&end=" + end.toISOString() +
"&interval=" + interval
)
resp = await resp.json()
// Convert the timestamps to a human-friendly format
resp.timestamps.forEach((val, idx) => {
let date = new Date(val);
let str = date.getFullYear();
str += "-" + ("00" + (date.getMonth() + 1)).slice(-2);
str += "-" + ("00" + date.getDate()).slice(-2);
str += " " + ("00" + date.getHours()).slice(-2);
str += ":" + ("00" + date.getMinutes()).slice(-2);
resp.timestamps[idx] = " " + str + " "; // Poor man's padding
});
// Add up the total amount and save it in the correct place
let total = resp.amounts.reduce((acc, cur) => { return acc + cur }, 0)
if (stat == "views") {
total_views = total;
} else if (stat == "downloads") {
total_downloads = total;
graph_views_downloads.update()
} else if (stat == "bandwidth") {
total_bandwidth = total;
} else if (stat == "transfer_paid") {
total_transfer_paid = total;
}
return resp
}
let graph_timespan = 0
let update_graphs = (minutes, interval) => {
graph_timespan = minutes
load_graphs(minutes, interval)
}
onMount(() => {
graph_views_downloads.data().datasets = [
{
label: "Views",
borderWidth: 2,
pointRadius: 0,
borderColor: color_by_name("highlight_color"),
backgroundColor: color_by_name("highlight_color"),
},
{
label: "Downloads",
borderWidth: 2,
pointRadius: 0,
borderColor: color_by_name("danger_color"),
backgroundColor: color_by_name("danger_color"),
},
];
graph_bandwidth.data().datasets = [
{
label: "Total bandwidth",
borderWidth: 2,
pointRadius: 0,
borderColor: color_by_name("highlight_color"),
backgroundColor: color_by_name("highlight_color"),
},
{
label: "Premium bandwidth",
borderWidth: 2,
pointRadius: 0,
borderColor: color_by_name("danger_color"),
backgroundColor: color_by_name("danger_color"),
}
];
update_graphs(43200, 1440);
})
</script>
<div style="text-align: center">
<button
on:click={() => update_graphs(1440, 1)}
class:button_highlight={graph_timespan == 1440}>
Day (1m)
</button>
<button
on:click={() => update_graphs(10080, 60)}
class:button_highlight={graph_timespan == 10080}>
Week (1h)
</button>
<button
on:click={() => update_graphs(20160, 60)}
class:button_highlight={graph_timespan == 20160}>
Two Weeks (1h)
</button>
<button
on:click={() => update_graphs(43200, 1440)}
class:button_highlight={graph_timespan == 43200}>
Month (1d)
</button>
<button
on:click={() => update_graphs(131400, 1440)}
class:button_highlight={graph_timespan == 131400}>
Quarter (1d)
</button>
<button
on:click={() => update_graphs(525600, 1440)}
class:button_highlight={graph_timespan == 525600}>
Year (1d)
</button>
<button
on:click={() => update_graphs(1051200, 1440)}
class:button_highlight={graph_timespan == 1051200}>
Two Years (1d)
</button>
</div>
<Chart bind:this={graph_bandwidth} data_type="bytes" height="200px" ticks={false}/>
<div class="center">
{formatDataVolume(total_bandwidth, 3)} free downloads and
{formatDataVolume(total_transfer_paid, 3)} paid downloads
</div>
<Chart bind:this={graph_views_downloads} data_type="number" height="200px" ticks={false}/>
<div class="center">
{formatThousands(total_views)} views and
{formatThousands(total_downloads)} downloads
</div>
<style>
.center {
text-align: center;
}
</style>

View File

@@ -0,0 +1,64 @@
<script>
import Euro from "../../util/Euro.svelte";
import { formatDataVolume } from "../../util/Formatting.svelte";
</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}
<li>
Current account balance: <Euro amount={window.user.balance_micro_eur}></Euro><br/>
<i class="icon">account_balance_wallet</i>
<a href="/user/prepaid/deposit">Deposit credit</a><br/>
<i class="icon">receipt</i>
<a href="/user/prepaid/transactions">Transaction log</a>
{#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>
{/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>
<li>
{#if window.user.subscription.file_expiry_days > 0}
Files expire after {window.user.subscription.file_expiry_days} days
{:else}
Files never expire
{/if}
</li>
</ul>
<style>
ul {
margin: 0;
}
</style>

View File

@@ -0,0 +1,44 @@
<script>
import UploadLib from "./UploadLib.svelte";
let upload_widget
</script>
<div class="upload_buttons">
<button on:click={() => upload_widget.pick_files() } class="big_button button_highlight">
<i class="icon small">cloud_upload</i>
<span><u>U</u>pload Files</span>
</button>
<a href="/t" id="upload_text_button" class="button big_button button_highlight">
<i class="icon small">text_fields</i>
<span>Upload <u>T</u>ext</span>
</a>
</div>
<div class="center">
<UploadLib bind:this={upload_widget}/>
</div>
<style>
.center {
text-align: center;
}
.upload_buttons {
display: flex;
flex-direction: row;
justify-content: space-around;
gap: 8px;
margin-top: 8px;
}
.upload_buttons > * {
flex: 1 1 auto;
}
.big_button {
margin: 0;
max-width: 300px;
font-size: 1.4em;
justify-content: center;
}
</style>

View File

@@ -0,0 +1,53 @@
<script>
import { onMount } from "svelte";
import HotlinkProgressBar from "../HotlinkProgressBar.svelte";
import StorageProgressBar from "../StorageProgressBar.svelte";
let transfer_cap = 0
let transfer_used = 0
let storage_used = window.user.storage_space_used
let storage_limit = window.user.subscription.storage_space
let load_direct_bw = () => {
let today = new Date()
let start = new Date()
start.setDate(start.getDate() - 30)
fetch(
window.api_endpoint + "/user/time_series/transfer_paid" +
"?start=" + start.toISOString() +
"&end=" + today.toISOString() +
"&interval=60"
).then(resp => {
if (!resp.ok) { return Promise.reject("Error: " + resp.status); }
return resp.json();
}).then(resp => {
let total = resp.amounts.reduce((accum, val) => accum += val, 0);
transfer_used = total
}).catch(e => {
console.error("Error requesting time series: " + e);
})
}
onMount(() => {
if (window.user.monthly_transfer_cap > 0) {
transfer_cap = window.user.monthly_transfer_cap
} else if (window.user.subscription.monthly_transfer_cap > 0) {
transfer_cap = window.user.subscription.monthly_transfer_cap
} else {
transfer_cap = -1
}
load_direct_bw()
})
</script>
Storage space used:
<StorageProgressBar used={storage_used} total={storage_limit}/>
<br/>
Premium data transfer:
(<a href="/user/sharing/bandwidth">set custom limit</a>)
<HotlinkProgressBar used={transfer_used} total={transfer_cap}></HotlinkProgressBar>

View File

@@ -0,0 +1,163 @@
<script>
import Button from "../../layout/Button.svelte"
import { onMount } from "svelte";
import CardAccount from "./CardAccount.svelte";
import CardStatistics from "./CardStatistics.svelte";
import CardSubscription from "./CardSubscription.svelte";
import CardUsage from "./CardUsage.svelte";
import CardActivity from "./CardActivity.svelte";
import CardUpload from "./CardUpload.svelte";
import CardPrepaidTransactions from "./CardPrepaidTransactions.svelte";
let cards = [
{
id: "upload",
elem: CardUpload,
title: "Quick upload",
},{
id: "account",
elem: CardAccount,
title: "Account",
link: "/user/settings",
}, {
id: "subscription",
elem: CardSubscription,
title: "Subscription",
link: "/user/subscription",
}, {
id: "prepaid_transactions",
elem: CardPrepaidTransactions,
title: "Prepaid transactions",
link: "/user/prepaid/transactions",
hidden: window.user.subscription.type !== "prepaid"
}, {
id: "usage",
elem: CardUsage,
title: "Usage",
}, {
id: "activiy",
elem: CardActivity,
title: "Activity",
link: "/user/activity",
}, {
id: "statistics",
elem: CardStatistics,
title: "Statistics",
},
]
const save = () => {
let storage = {
expanded: [],
}
for (const card of cards) {
if (card.expanded === true) {
storage.expanded.push(card.id)
}
}
window.localStorage.setItem(
"dashboard_layout",
JSON.stringify(storage),
)
}
const load = () => {
const str = window.localStorage.getItem("dashboard_layout")
if (str === null) {
return
}
const storage = JSON.parse(str)
if (storage.expanded) {
for (const card of cards) {
if (storage.expanded.includes(card.id)) {
card.expanded = true
}
}
}
// Update the view
cards = cards
}
onMount(() => {
load()
})
</script>
<div class="cards">
{#each cards as card (card.id)}{#if !card.hidden}
<div class="card" class:card_wide={card.expanded}>
<div class="title_box">
<h2>
{card.title}
</h2>
{#if card.link}
<Button
link_href={card.link}
icon="link"
flat
/>
{/if}
<Button
click={() => {card.expanded = !card.expanded; save()}}
icon={card.expanded ? "fullscreen_exit" : "fullscreen"}
flat
/>
</div>
<div class="card_component">
<svelte:component this={card.elem}/>
</div>
</div>
{/if}{/each}
</div>
<style>
.cards {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 8px 0;
margin: 0 8px;
border-top: 1px solid var(--separator);
}
.card {
flex: 1 0 auto;
display: flex;
flex-direction: column;
width: 25em;
max-width: 100%;
background: var(--body_background);
border-radius: 8px;
padding: 8px;
text-align: initial;
max-height: 600px;
}
.card_component {
flex: 1 1 auto;
overflow: auto;
}
.card_wide {
flex-basis: auto;
width: 100%;
max-height: none;
}
.title_box {
flex: 0 0 auto;
display: flex;
flex-direction: row;
align-items: flex-start;
border-bottom: 1px solid var(--separator);
}
.title_box > h2 {
flex: 1 1 auto;
margin: 0;
font-size: 1.5em;
border-bottom: none;
}
</style>

View File

@@ -0,0 +1,233 @@
<script>
import UploadProgressBar from "../../home_page/UploadProgressBar.svelte"
import { tick } from "svelte"
import UploadStats from "../../home_page/UploadStats.svelte";
export const pick_files = () => {
file_input_field.click()
}
// === UPLOAD LOGIC ===
let file_input_field
const file_input_change = (event) => {
// Start uploading the files async
upload_files(event.target.files)
// This resets the file input field
file_input_field.nodeValue = ""
}
let dragging = false
const drop = (e) => {
dragging = false;
if (e.dataTransfer && e.dataTransfer.items.length > 0) {
e.preventDefault()
e.stopPropagation()
upload_files(e.dataTransfer.files)
}
}
const paste = (e) => {
if (e.clipboardData.files[0]) {
e.preventDefault();
e.stopPropagation();
upload_files(e.clipboardData.files)
}
}
let active_uploads = 0
let upload_queue = []
let state = "idle" // idle, uploading, finished
let upload_stats
const upload_files = async (files) => {
if (files.length === 0) {
return
}
// Add files to the queue
for (let i = 0; i < files.length; i++) {
if (files[i].type === "" && files[i].size === 0) {
continue
}
upload_queue.push({
file: files[i],
name: files[i].name,
status: "queued",
component: null,
id: "",
total_size: files[i].size,
loaded_size: 0,
on_finished: finish_upload,
})
}
// Reassign array and wait for tick to complete. After the tick is completed
// each upload progress bar will have bound itself to its array item
upload_queue = upload_queue
await tick()
start_upload()
}
const start_upload = () => {
let finished_count = 0
for (let i = 0; i < upload_queue.length && active_uploads < 3; i++) {
if (upload_queue[i].status == "queued") {
active_uploads++
upload_queue[i].component.start()
} else if (
upload_queue[i].status == "finished" ||
upload_queue[i].status == "error"
) {
finished_count++
}
}
if (active_uploads === 0 && finished_count != 0) {
state = "finished"
upload_stats.finish()
// uploads_finished()
} else {
state = "uploading"
upload_stats.start()
}
}
const finish_upload = (file) => {
active_uploads--
start_upload()
}
const leave_confirmation = e => {
if (state === "uploading") {
e.preventDefault()
e.returnValue = "If you close the page your files will stop uploading. Do you want to continue?"
return e.returnValue
} else {
return null
}
}
// === SHARING BUTTONS ===
let share_link = ""
let input_album_name = ""
let btn_create_list
const create_list = async (title, anonymous) => {
let files = upload_queue.reduce(
(acc, curr) => {
if (curr.status === "finished") {
acc.push({"id": curr.id})
}
return acc
},
[],
)
const resp = await fetch(
window.api_endpoint+"/list",
{
method: "POST",
headers: { "Content-Type": "application/json; charset=UTF-8" },
body: JSON.stringify({
"title": title,
"anonymous": anonymous,
"files": files
})
}
)
if(!resp.ok) {
return Promise.reject("HTTP error: "+resp.status)
}
return await resp.json()
}
const share_mail = () => window.open("mailto:please@set.address?subject=File%20on%20pixeldrain&body=" + share_link)
const share_twitter = () => window.open("https://twitter.com/share?url=" + share_link)
const share_facebook = () => window.open('https://www.facebook.com/sharer.php?u=' + share_link)
const share_reddit = () => window.open('https://www.reddit.com/submit?url=' + share_link)
const share_tumblr = () => window.open('https://www.tumblr.com/share/link?url=' + share_link)
const create_album = () => {
if (!input_album_name) {
return
}
create_list(input_album_name, false).then(resp => {
window.location = '/l/' + resp.id
}).catch(err => {
alert("Failed to create list. Server says this:\n"+err)
})
}
const keydown = (e) => {
if (e.ctrlKey || e.altKey || e.metaKey) {
return // prevent custom shortcuts from interfering with system shortcuts
}
if (document.activeElement.type && document.activeElement.type === "text") {
return // Prevent shortcuts from interfering with input fields
}
switch (e.key) {
case "u": file_input_field.click(); break
case "l": btn_create_list.click(); break
case "e": share_mail(); break
case "w": share_twitter(); break
case "f": share_facebook(); break
case "r": share_reddit(); break
case "m": share_tumblr(); break
}
}
</script>
<svelte:window
on:dragover|preventDefault|stopPropagation={() => { dragging = true }}
on:dragenter|preventDefault|stopPropagation={() => { dragging = true }}
on:dragleave|preventDefault|stopPropagation={() => { dragging = false }}
on:drop={drop}
on:paste={paste}
on:keydown={keydown}
on:beforeunload={leave_confirmation} />
<input bind:this={file_input_field} on:change={file_input_change} type="file" name="file" multiple="multiple" class="hide"/>
<UploadStats bind:this={upload_stats} upload_queue={upload_queue}/>
{#if upload_queue.length > 1}
<div class="album_widget">
Create an album<br/>
<form class="album_name_form" on:submit|preventDefault={create_album}>
<div>Name:</div>
<input bind:value={input_album_name} type="text" placeholder="My album"/>
<button type="submit" disabled={state !== "finished"}>
<i class="icon">create_new_folder</i> Create
</button>
</form>
</div>
{/if}
<div id="file_drop_highlight" class="highlight_green" class:hide={!dragging}>
Drop your files to upload them
</div>
{#each upload_queue as file}
<UploadProgressBar bind:this={file.component} job={file}></UploadProgressBar>
{/each}
<style>
.album_widget {
display: block;
border-bottom: 1px solid var(--separator);
}
.album_name_form {
display: inline-flex;
flex-direction: row;
align-items: center;
}
.hide {
display: none;
}
</style>

View File

@@ -29,6 +29,8 @@ let chart_object
export let data_type = "" export let data_type = ""
export let legend = true export let legend = true
export let tooltips = true export let tooltips = true
export let ticks = true
export let height = "300px"
export const chart = () => { export const chart = () => {
return chart_object return chart_object
@@ -80,7 +82,7 @@ onMount(() => {
layout: { layout: {
padding: { padding: {
left: 4, left: 4,
right: 10, right: 4,
} }
}, },
scales: { scales: {
@@ -106,6 +108,7 @@ onMount(() => {
x: { x: {
display: true, display: true,
ticks: { ticks: {
display: ticks,
sampleSize: 1, sampleSize: 1,
padding: 4, padding: 4,
minRotation: 0, minRotation: 0,
@@ -123,7 +126,7 @@ onMount(() => {
}) })
</script> </script>
<div class="chart-container"> <div class="chart-container" style="height: {height};">
<canvas bind:this={chart_element}></canvas> <canvas bind:this={chart_element}></canvas>
</div> </div>
@@ -131,6 +134,5 @@ onMount(() => {
.chart-container { .chart-container {
position: relative; position: relative;
width: 100%; width: 100%;
height: 300px;
} }
</style> </style>

View File

@@ -72,8 +72,8 @@ onMount(() => get_page())
</div> </div>
</header> </header>
<div id="page_content" class="page_content"> {#if current_page}
{#if current_page} <div id="page_content" class:page_content={current_page.hide_background !== true}>
{#if current_page.subpages} {#if current_page.subpages}
<div class="tab_bar submenu"> <div class="tab_bar submenu">
{#each current_page.subpages as page} {#each current_page.subpages as page}
@@ -95,8 +95,8 @@ onMount(() => get_page())
{:else} {:else}
<svelte:component this={current_page.component} /> <svelte:component this={current_page.component} />
{/if} {/if}
{/if} </div>
</div> {/if}
<Footer/> <Footer/>