Overhaul user settings page

This commit is contained in:
2022-10-18 14:30:50 +02:00
parent f1c6b3adf8
commit 71a34fc881
15 changed files with 751 additions and 635 deletions

View File

@@ -178,6 +178,7 @@ onMount(get_reporters);
on:delete={e => delete_reporter(e.detail)}> on:delete={e => delete_reporter(e.detail)}>
</AbuseReporterTable> </AbuseReporterTable>
</div> </div>
<br/>
<style> <style>
.toolbar { .toolbar {

View File

@@ -187,6 +187,10 @@ onDestroy(() => {
<br/> <br/>
<ServerDiagnostics running_since={status.cpu_profile_running_since} on:refresh={() => getStats(lastOrder)}/> <ServerDiagnostics running_since={status.cpu_profile_running_since} on:refresh={() => getStats(lastOrder)}/>
<br/> <br/>
<a class="button" href="/admin/globals">
<i class="icon">edit</i>
Global settings
</a>
<section> <section>
<table> <table>

View File

@@ -1,98 +1,45 @@
<script> <script>
import AbuseReporters from "./AbuseReporters.svelte"
import AbuseReports from "./AbuseReports.svelte" import AbuseReports from "./AbuseReports.svelte"
import IpBans from "./IPBans.svelte" import IPBans from "./IPBans.svelte"
import Home from "./Home.svelte" import Home from "./Home.svelte"
import { onMount } from "svelte";
import BlockFiles from "./BlockFiles.svelte"; import BlockFiles from "./BlockFiles.svelte";
import Subscriptions from "./Subscriptions.svelte"; import TabMenu from "../util/TabMenu.svelte";
import Footer from "../layout/Footer.svelte"; import UserManagement from "./UserManagement.svelte";
import EmailReporters from "./EmailReporters.svelte";
let page = "" let pages = [
{
let navigate = (path, title) => { path: "/admin",
page = path title: "Status",
window.document.title = title+" ~ pixeldrain" icon: "home",
window.history.pushState( component: Home,
{}, window.document.title, "/admin/"+path }, {
) path: "/admin/block_files",
} title: "Block Files",
icon: "block",
onMount(() => { component: BlockFiles,
let newpage = window.location.pathname.substring(window.location.pathname.lastIndexOf("/")+1) }, {
if (newpage === "admin") { path: "/admin/abuse_reports",
newpage = "status" title: "User Reports",
} icon: "flag",
page = newpage component: AbuseReports,
}) }, {
path: "/admin/email_reporters",
title: "E-mail Reporters",
icon: "email",
component: EmailReporters,
}, {
path: "/admin/ip_bans",
title: "IP Bans",
icon: "remove_circle",
component: IPBans,
}, {
path: "/admin/user_management",
title: "User Management",
icon: "person",
component: UserManagement,
},
]
</script> </script>
<header> <TabMenu pages={pages} title="Admin Panel"/>
<h1>Admin Panel</h1>
<div class="tab_bar">
<a class="button"
href="/admin"
class:button_highlight={page === "status"}
on:click|preventDefault={() => {navigate("status", "Status")}}>
<i class="icon">home</i><br/>
Status
</a>
<a class="button"
href="/admin/block_files"
class:button_highlight={page === "block_files"}
on:click|preventDefault={() => {navigate("block_files", "Block files")}}>
<i class="icon">block</i><br/>
Block files
</a>
<a class="button"
href="/admin/abuse_reports"
class:button_highlight={page === "abuse_reports"}
on:click|preventDefault={() => {navigate("abuse_reports", "Abuse reports")}}>
<i class="icon">flag</i><br/>
User reports
</a>
<a class="button"
href="/admin/abuse_reporters"
class:button_highlight={page === "abuse_reporters"}
on:click|preventDefault={() => {navigate("abuse_reporters", "Abuse reporters")}}>
<i class="icon">email</i><br/>
E-mail reporters
</a>
<a class="button"
href="/admin/ip_bans"
class:button_highlight={page === "ip_bans"}
on:click|preventDefault={() => {navigate("ip_bans", "IP bans")}}>
<i class="icon">remove_circle</i><br/>
IP bans
</a>
<a class="button"
href="/admin/subscriptions"
class:button_highlight={page === "subscriptions"}
on:click|preventDefault={() => {navigate("subscriptions", "Subscriptions")}}>
<i class="icon">receipt_long</i><br/>
Subscriptions
</a>
<a class="button" href="/admin/globals">
<i class="icon">edit</i><br/>
Global settings
</a>
</div>
</header>
<div id="page_content" class="page_content">
{#if page === "status"}
<Home></Home>
{:else if page === "block_files"}
<BlockFiles></BlockFiles>
{:else if page === "abuse_reports"}
<AbuseReports></AbuseReports>
{:else if page === "abuse_reporters"}
<AbuseReporters></AbuseReporters>
{:else if page === "ip_bans"}
<IpBans></IpBans>
{:else if page === "subscriptions"}
<Subscriptions></Subscriptions>
{/if}
</div>
<Footer/>

View File

@@ -1,7 +1,7 @@
<script> <script>
import { onMount } from "svelte"; import { onMount } from "svelte";
import Euro from "../util/Euro.svelte"; import Euro from "../util/Euro.svelte";
import Form from "./../util/Form.svelte"; import Form from "../util/Form.svelte";
import LoadingIndicator from "../util/LoadingIndicator.svelte"; import LoadingIndicator from "../util/LoadingIndicator.svelte";
let loading = true let loading = true
@@ -176,6 +176,11 @@ onMount(get_coupons)
<LoadingIndicator loading={loading}/> <LoadingIndicator loading={loading}/>
<section> <section>
<h2>Impersonate user</h2>
<div class="highlight_shaded">
<Form config={impersonate_form}></Form>
</div>
<h2>Give user credit</h2> <h2>Give user credit</h2>
<p> <p>
This adds credit to a user's account. You only need to enter one of This adds credit to a user's account. You only need to enter one of
@@ -185,11 +190,6 @@ onMount(get_coupons)
<Form config={credit_form}></Form> <Form config={credit_form}></Form>
</div> </div>
<h2>Impersonate user</h2>
<div class="highlight_shaded">
<Form config={impersonate_form}></Form>
</div>
<h2>Create coupon codes</h2> <h2>Create coupon codes</h2>
<div class="highlight_shaded"> <div class="highlight_shaded">
<Form config={coupon_form}></Form> <Form config={coupon_form}></Form>

View File

@@ -302,11 +302,9 @@ const node_click = (index) => {
<div id="directory_element"> <div id="directory_element">
<div bind:this={directorySorters} id="sorters" class="directory_sorters"> <div bind:this={directorySorters} id="sorters" class="directory_sorters">
{#each tableColumns as col} {#each tableColumns as col}
<!-- <div style="min-width: {col.width}"> --> <button style="min-width: {col.width}" on:click={sortBy(col.field)} class="sorter_button">
<button style="min-width: {col.width}" on:click={sortBy(col.field)} class="sorter_button"> {col.name}
{col.name} </button>
</button>
<!-- </div> -->
{/each} {/each}
</div> </div>
<div bind:this={directoryArea} on:scroll={onScroll} id="directory_area" class="directory_area"> <div bind:this={directoryArea} on:scroll={onScroll} id="directory_area" class="directory_area">

View File

@@ -0,0 +1,135 @@
<script>
import { onMount } from "svelte";
import { formatDataVolume } from "../util/Formatting.svelte";
import LoadingIndicator from "../util/LoadingIndicator.svelte";
import ProgressBar from "../util/ProgressBar.svelte";
import SuccessMessage from "../util/SuccessMessage.svelte";
let loading = false
let success_message
const update = async () => {
loading = true
const form = new FormData()
form.append("update", "limits")
form.append("hotlinking_enabled", hotlinking)
form.append("transfer_cap", transfer_cap*1e9)
try {
const resp = await fetch(
window.api_endpoint+"/user/subscription",
{ method: "PUT", body: form },
)
if(resp.status >= 400) {
let json = await resp.json()
throw json.message
}
window.user.hotlinking_enabled = hotlinking
window.user.monthly_transfer_cap = transfer_cap*1e9
success_message.set(true, "Sharing settings updated")
} catch (err) {
success_message.set(false, "Failed to update subscription: "+err)
} finally {
loading = false
}
}
let hotlinking = window.user.hotlinking_enabled
let toggle_hotlinking = () => {
hotlinking = !hotlinking
update()
}
let transfer_cap = window.user.monthly_transfer_cap / 1e9
let transfer_used = 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/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 => {
transfer_used = resp.amounts.reduce((acc, cur) => { return acc + cur }, 0)
}).catch(e => {
console.error("Error requesting time series: " + e);
})
}
onMount(() => {
load_transfer_used()
})
</script>
<LoadingIndicator loading={loading}/>
<section>
<h2>Bandwidth sharing</h2>
<SuccessMessage bind:this={success_message}></SuccessMessage>
<button on:click={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 bandwidth sharing 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 bandwidth sharing is on. You can directly embed your
file's download link anywhere, you don't need to use the file viewer
page.
</p>
<h2>Bill shock limit</h2>
<p>
Billshock limit in gigabytes per month (1 TB = 1000 GB). Set to 0 to disable.
</p>
<form on:submit|preventDefault={update} class="billshock_container">
<input type="number" bind:value={transfer_cap} step="100" min="0"/>
<div style="margin: 0.5em;">GB</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*1e9, 3)}
</p>
<ProgressBar used={transfer_used} total={transfer_cap*1e9}></ProgressBar>
<p>
The billshock limit limits how much bandwidth your account can use
in a 30 day window. When this limit is reached files will show ads
again and can only be downloaded from the file viewer page. This is
mostly useful for prepaid plans, but it works for patreon plans too.
Set to 0 to disable the limit.
</p>
</section>
<style>
.green {
color: var(--highlight_color);
}
.red {
color: var(--danger_color);
}
.billshock_container {
display: flex;
flex-direction: row;
align-items: center;
}
</style>

View File

@@ -0,0 +1,154 @@
<script>
import { onMount } from "svelte";
import Euro from "../util/Euro.svelte";
import { formatDate } from "../util/Formatting.svelte";
import LoadingIndicator from "../util/LoadingIndicator.svelte";
let loading = false
let credit_amount = 10
const checkout = async (network) => {
loading = true
const form = new FormData()
form.set("amount", credit_amount*1e6)
form.set("network", network)
try {
const resp = await fetch(
window.api_endpoint+"/btcpay/deposit",
{ method: "POST", body: form },
)
if(resp.status >= 400) {
let json = await resp.json()
throw json.message
}
window.location = (await resp.json()).checkout_url
} catch (err) {
alert(err)
} finally {
loading = false
}
}
let show_expired = false
let invoices = []
const load_invoices = async () => {
loading = true
try {
const resp = await fetch(window.api_endpoint+"/btcpay/invoice")
if(resp.status >= 400) {
throw new Error((await resp.json()).message)
}
let invoices_tmp = await resp.json()
invoices_tmp.forEach(row => {
row.time = new Date(row.time)
})
invoices_tmp.sort((a, b) => {
return b.time - a.time
})
invoices = invoices_tmp
} catch (err) {
console.error(err)
alert(err)
} finally {
loading = false
}
};
onMount(() => {
load_invoices()
})
</script>
<LoadingIndicator loading={loading}/>
<section>
<h2>Deposit credits</h2>
<p>
You can deposit credit on your pixeldrain account with Bitcoin,
Lightning network (<a
href="https://btcpay.pixeldrain.com/embed/uS2mbWjXUuaAqMh8XLjkjwi8oehFuxeBZxekMxv68LN/BTC/ln"
target="_blank">node info</a>) and Dogecoin. You must pay the full
amount as stated on the invoice, else your payment will fail.
</p>
<p>
Do note that it is not possible to withdraw coins from your
pixeldrain account. It's not a wallet. Any amount of money you
deposit has to be used up.
</p>
<div style="text-align: center;">
Deposit amount €
<input type="number" bind:value={credit_amount} min="1"/>
<br/>
Pay with:<br/>
<button on:click={() => {checkout("btc")}}>
<span class="icon_unicode"></span> Bitcoin
</button>
<button on:click={() => {checkout("btc_lightning")}}>
<i class="icon">bolt</i> Lightning network
</button>
<button on:click={() => {checkout("doge")}}>
<span class="icon_unicode">Ð</span> Dogecoin
</button>
</div>
<h3>Open invoices</h3>
<div class="table_scroll">
<table style="text-align: left;">
<thead>
<tr>
<td>Created</td>
<td>Amount</td>
<td>Status</td>
<td></td>
</tr>
</thead>
<tbody>
{#each invoices as row (row.id)}
{#if row.status === "New" ||
row.status === "InvoiceCreated" ||
row.status === "InvoiceProcessing" ||
show_expired
}
<tr>
<td>{formatDate(row.time, true, true, false)}</td>
<td><Euro amount={row.amount}></Euro></td>
<td>
{#if row.status === "InvoiceCreated"}
New (waiting for payment)
{:else if row.status === "InvoiceProcessing"}
Payment received, waiting for confirmations
{:else if row.status === "InvoiceSettled"}
Paid
{:else if row.status === "InvoiceExpired"}
Expired
{:else}
{row.status}
{/if}
</td>
<td>
{#if row.status === "New" || row.status === "InvoiceCreated"}
<a href={row.checkout_url} class="button button_highlight">
<i class="icon">paid</i> Pay
</a>
{/if}
</td>
</tr>
{/if}
{/each}
</tbody>
</table>
<div style="text-align: center;">
<button on:click={() => {show_expired = !show_expired}}>
{#if show_expired}
Hide
{:else}
Show
{/if}
expired and settled invoices
</button>
</div>
</div>
</section>

View File

@@ -0,0 +1,88 @@
<script>
import { onMount } from "svelte";
import LoadingIndicator from "../util/LoadingIndicator.svelte";
import SuccessMessage from "../util/SuccessMessage.svelte";
let loading = false
let success_message
// Embedding settings
let embed_domains = ""
const save_embed = async () => {
loading = true
const form = new FormData()
form.append("domains", embed_domains)
try {
const resp = await fetch(
window.api_endpoint+"/user/file_embed",
{ 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)
} finally {
loading = false
}
}
onMount(() => {
embed_domains = window.user.file_embed_domains
})
</script>
<LoadingIndicator loading={loading}/>
<section>
<h2>Embedding controls</h2>
<SuccessMessage bind:this={success_message}></SuccessMessage>
{#if !window.user.subscription.file_viewer_branding}
<div class="highlight_red">
Sharing settings are not available for your account. Subscribe to
the Persistence plan or higher to enable these features.
</div>
{:else if !window.user.hotlinking_enabled}
<div class="highlight_red">
To use embedding restrictions bandwidth sharing needs to be enabled.
Enable bandwidth sharing on the
<a href="/user/sharing/bandwidth">bandwidth sharing 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.
</p>
<p>
The list should be formatted as a list of domain names separated by a
space. Like this: 'pixeldrain.com google.com twitter.com'
</p>
<form class="form_row" on:submit|preventDefault={save_embed}>
<div class="shrink">Domain names:</div>
<input class="grow" bind:value={embed_domains} type="text"/>
<button class="shrink" action="submit"><i class="icon">save</i> Save</button>
</form>
</section>
<style>
.form_row {
display: inline-flex;
flex-direction: row;
width: 100%;
align-items: center;
}
.grow {
flex: 1 1 auto;
}
.shrink {
flex: 0 0 auto;
}
</style>

View File

@@ -1,10 +1,10 @@
<script> <script>
import { onMount } from "svelte";
import FilePicker from "../file_viewer/FilePicker.svelte"; import FilePicker from "../file_viewer/FilePicker.svelte";
import CustomBanner from "../file_viewer/CustomBanner.svelte"; import CustomBanner from "../file_viewer/CustomBanner.svelte";
import LoadingIndicator from "../util/LoadingIndicator.svelte"; import LoadingIndicator from "../util/LoadingIndicator.svelte";
import SuccessMessage from "../util/SuccessMessage.svelte"; import SuccessMessage from "../util/SuccessMessage.svelte";
import ThemePicker from "../util/ThemePicker.svelte"; import ThemePicker from "../util/ThemePicker.svelte";
import { onMount } from "svelte";
let loading = false let loading = false
let success_message let success_message
@@ -74,33 +74,6 @@ let save = async () => {
} }
} }
// Embedding settings
let embed_domains = ""
const save_embed = async () => {
loading = true
const form = new FormData()
form.append("domains", embed_domains)
try {
const resp = await fetch(
window.api_endpoint+"/user/file_embed",
{ 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)
} finally {
loading = false
}
}
onMount(() => { onMount(() => {
// The fields are undefined when they're empty. So we need to check if each // The fields are undefined when they're empty. So we need to check if each
// field is defined before converting to a string // field is defined before converting to a string
@@ -113,16 +86,13 @@ onMount(() => {
footer_image = b.footer_image ? b.footer_image : "" footer_image = b.footer_image ? b.footer_image : ""
footer_link = b.footer_link ? b.footer_link : "" footer_link = b.footer_link ? b.footer_link : ""
} }
embed_domains = window.user.file_embed_domains
}) })
</script> </script>
<LoadingIndicator loading={loading}/> <LoadingIndicator loading={loading}/>
<section> <section>
<h2>Sharing settings</h2> <h2>File viewer branding</h2>
{#if !window.user.subscription.file_viewer_branding} {#if !window.user.subscription.file_viewer_branding}
<div class="highlight_red"> <div class="highlight_red">
Sharing settings are not available for your account. Subscribe to Sharing settings are not available for your account. Subscribe to
@@ -130,13 +100,13 @@ onMount(() => {
</div> </div>
{:else if !window.user.hotlinking_enabled} {:else if !window.user.hotlinking_enabled}
<div class="highlight_red"> <div class="highlight_red">
To use the sharing settings bandwidth sharing needs to be enabled. To use custom file viewer branding bandwidth sharing needs to be
Enable bandwidth sharing on the enabled. Enable bandwidth sharing on the
<a href="/user/subscription">subscription page</a>. <a href="/user/sharing/bandwidth">bandwidth sharing page</a>.
</div> </div>
{/if} {/if}
<h3>File viewer branding</h3>
<SuccessMessage bind:this={success_message}></SuccessMessage> <SuccessMessage bind:this={success_message}></SuccessMessage>
<p> <p>
You can change the appearance of your file viewer pages. The images you You can change the appearance of your file viewer pages. The images you
choose here will be loaded each time someone visits one of your files. choose here will be loaded each time someone visits one of your files.
@@ -148,7 +118,7 @@ onMount(() => {
should use APNG or WebP. Avoid using animated GIFs as they are very slow should use APNG or WebP. Avoid using animated GIFs as they are very slow
to load. to load.
</p> </p>
<h4>Theme</h4> <h3>Theme</h3>
<p> <p>
Choose a theme for your download pages. This theme will override the Choose a theme for your download pages. This theme will override the
theme preference of the person viewing the file. Set to 'None' to let theme preference of the person viewing the file. Set to 'None' to let
@@ -159,7 +129,7 @@ onMount(() => {
on:theme_change={e => {theme = e.detail; save()}}> on:theme_change={e => {theme = e.detail; save()}}>
</ThemePicker> </ThemePicker>
<h4>Header image</h4> <h3>Header image</h3>
<p> <p>
Will be shown above the file. Maximum height is 90px. Will be shrunk if Will be shown above the file. Maximum height is 90px. Will be shrunk if
larger. You can also add a link to open when the visitor clicks the larger. You can also add a link to open when the visitor clicks the
@@ -174,8 +144,8 @@ onMount(() => {
Remove Remove
</button> </button>
<br/> <br/>
Header image link:<br/>
<form class="form_row" on:submit|preventDefault={save}> <form class="form_row" on:submit|preventDefault={save}>
<div class="shrink">Header image link:</div>
<input class="grow" bind:value={header_link} type="text" placeholder="https://"/> <input class="grow" bind:value={header_link} type="text" placeholder="https://"/>
<button class="shrink" action="submit"><i class="icon">save</i> Save</button> <button class="shrink" action="submit"><i class="icon">save</i> Save</button>
</form> </form>
@@ -186,7 +156,7 @@ onMount(() => {
</div> </div>
{/if} {/if}
<h4>Background image</h4> <h3>Background image</h3>
<p> <p>
This image will be shown behind the file which is being viewed. I This image will be shown behind the file which is being viewed. I
recommend choosing something dark and not too distracting. Try to keep recommend choosing something dark and not too distracting. Try to keep
@@ -207,7 +177,7 @@ onMount(() => {
</div> </div>
{/if} {/if}
<h4>Footer image</h4> <h3>Footer image</h3>
<p> <p>
Will be shown below the file. Maximum height is 90px. Will be shrunk if Will be shown below the file. Maximum height is 90px. Will be shrunk if
larger. larger.
@@ -221,8 +191,8 @@ onMount(() => {
Remove Remove
</button> </button>
<br/> <br/>
Footer image link:<br/>
<form class="form_row" on:submit|preventDefault={save}> <form class="form_row" on:submit|preventDefault={save}>
<div class="shrink">Footer image link:</div>
<input class="grow" bind:value={footer_link} type="text" placeholder="https://"/> <input class="grow" bind:value={footer_link} type="text" placeholder="https://"/>
<button class="shrink" action="submit"><i class="icon">save</i> Save</button> <button class="shrink" action="submit"><i class="icon">save</i> Save</button>
</form> </form>
@@ -231,22 +201,6 @@ onMount(() => {
<CustomBanner src={"/api/file/"+footer_image} link={footer_link}></CustomBanner> <CustomBanner src={"/api/file/"+footer_image} link={footer_link}></CustomBanner>
</div> </div>
{/if} {/if}
<br/>
<h3>Embedding controls</h3>
<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.
</p>
<p>
The list should be formatted as a list of domain names separated by a
space. Like this: 'pixeldrain.com google.com twitter.com'
</p>
<form class="form_row" on:submit|preventDefault={save_embed}>
<div class="shrink">Domain names:</div>
<input class="grow" bind:value={embed_domains} type="text"/>
<button class="shrink" action="submit"><i class="icon">save</i> Save</button>
</form>
</section> </section>
<FilePicker <FilePicker

View File

@@ -1,5 +1,4 @@
<script> <script>
import { onMount } from "svelte";
import Home from "./Home.svelte"; 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";
@@ -7,116 +6,85 @@ import Transactions from "./Transactions.svelte";
import Subscription from "./Subscription.svelte"; import Subscription from "./Subscription.svelte";
import ConnectApp from "./ConnectApp.svelte"; import ConnectApp from "./ConnectApp.svelte";
import ActivityLog from "./ActivityLog.svelte"; import ActivityLog from "./ActivityLog.svelte";
import SharingSettings from "./SharingSettings.svelte"; import DepositCredit from "./DepositCredit.svelte";
import Footer from "../layout/Footer.svelte"; import TabMenu from "../util/TabMenu.svelte";
import BandwidthSharing from "./BandwidthSharing.svelte";
import EmbeddingControls from "./EmbeddingControls.svelte";
import PageBranding from "./PageBranding.svelte";
let page = "" let pages = [
{
let navigate = (path, title) => { path: "/user/home",
page = path title: "My Home",
window.document.title = title+" ~ pixeldrain" icon: "home",
window.history.pushState( component: Home,
{}, window.document.title, "/user/"+path }, {
) path: "/user/settings",
} title: "Settings",
icon: "settings",
let get_page = () => { component: AccountSettings,
let newpage = window.location.pathname.substring(window.location.pathname.lastIndexOf("/")+1) }, {
if (newpage === "user") { path: "/user/sharing",
newpage = "home" title: "Sharing",
} icon: "share",
subpages: [
page = newpage {
} path: "/user/sharing/bandwidth",
title: "Bandwidth Sharing",
onMount(() => { icon: "share",
get_page() component: BandwidthSharing,
}) }, {
path: "/user/sharing/branding",
title: "Page Branding",
icon: "palette",
component: PageBranding,
}, {
path: "/user/sharing/embedding",
title: "Embedding Controls",
icon: "code",
component: EmbeddingControls,
},
],
}, {
path: "/user/connect_app",
title: "Apps",
icon: "app_registration",
component: ConnectApp,
}, {
path: "/user/api_keys",
title: "API Keys",
icon: "vpn_key",
component: APIKeys,
}, {
path: "/user/activity",
title: "Activity Log",
icon: "list",
component: ActivityLog,
}, {
path: "/user/prepaid",
title: "Prepaid",
icon: "receipt_long",
hidden: window.user.subscription.type === "patreon",
subpages: [
{
path: "/user/prepaid/deposit",
title: "Deposit credit",
icon: "account_balance_wallet",
component: DepositCredit,
}, {
path: "/user/prepaid/subscriptions",
title: "Subscriptions",
icon: "shopping_cart",
component: Subscription,
}, {
path: "/user/prepaid/transactions",
title: "Transactions",
icon: "receipt",
component: Transactions,
},
],
},
]
</script> </script>
<svelte:window on:popstate={get_page} /> <TabMenu pages={pages} title="Welcome home, {window.user.username}!"/>
<header>
<h1>Welcome home, {window.user.username}!</h1>
<div class="tab_bar">
<a class="button"
href="/user"
class:button_highlight={page === "home"}
on:click|preventDefault={() => {navigate("home", "My home")}}>
<i class="icon">home</i><br/>
My home
</a>
<a class="button"
href="/user/settings"
class:button_highlight={page === "settings"}
on:click|preventDefault={() => {navigate("settings", "Settings")}}>
<i class="icon">settings</i><br/>
Settings
</a>
<a class="button"
href="/user/sharing"
class:button_highlight={page === "sharing"}
on:click|preventDefault={() => {navigate("sharing", "Sharing")}}>
<i class="icon">share</i><br/>
Sharing
</a>
<a class="button"
href="/user/connect_app"
class:button_highlight={page === "connect_app"}
on:click|preventDefault={() => {navigate("connect_app", "Apps")}}>
<i class="icon">app_registration</i><br/>
Apps
</a>
<a class="button"
href="/user/api_keys"
class:button_highlight={page === "api_keys"}
on:click|preventDefault={() => {navigate("api_keys", "API keys")}}>
<i class="icon">vpn_key</i><br/>
Keys
</a>
<a class="button"
href="/user/activity"
class:button_highlight={page === "activity"}
on:click|preventDefault={() => {navigate("activity", "Activity log")}}>
<i class="icon">list</i><br/>
Activity log
</a>
<a class="button"
href="/user/subscription"
class:button_highlight={page === "subscription"}
on:click|preventDefault={() => {navigate("subscription", "Subscription")}}>
<i class="icon">shopping_cart</i><br/>
Subscription
</a>
{#if window.user.subscription.type !== "patreon"}
<a class="button"
href="/user/transactions"
class:button_highlight={page === "transactions"}
on:click|preventDefault={() => {navigate("transactions", "Transactions")}}>
<i class="icon">receipt_long</i><br/>
Transactions
</a>
{/if}
</div>
</header>
<div id="page_content" class="page_content">
{#if page === "home"}
<Home/>
{:else if page === "settings"}
<AccountSettings/>
{:else if page === "sharing"}
<SharingSettings/>
{:else if page === "api_keys"}
<APIKeys/>
{:else if page === "activity"}
<ActivityLog/>
{:else if page === "connect_app"}
<ConnectApp/>
{:else if page === "transactions"}
<Transactions/>
{:else if page === "subscription"}
<Subscription/>
{/if}
</div>
<Footer/>

View File

@@ -1,31 +1,18 @@
<script> <script>
import Euro from "../util/Euro.svelte" import Euro from "../util/Euro.svelte"
import ProgressBar from "../util/ProgressBar.svelte";
import { onMount } from "svelte";
import { formatDataVolume } from "../util/Formatting.svelte";
import LoadingIndicator from "../util/LoadingIndicator.svelte"; import LoadingIndicator from "../util/LoadingIndicator.svelte";
import SuccessMessage from "../util/SuccessMessage.svelte";
let loading = false let loading = false
let subscription = window.user.subscription.id let subscription = window.user.subscription.id
let success_message
let result = "" const update = async () => {
let result_success = false
const update = async (update_field) => {
loading = true loading = true
const form = new FormData() const form = new FormData()
if (update_field === "subscription") { form.append("update", "subscription")
form.append("update", "subscription") form.append("subscription", subscription)
form.append("subscription", subscription)
} else if (update_field === "limits") {
form.append("update", "limits")
form.append("hotlinking_enabled", hotlinking)
form.append("transfer_cap", transfer_cap*1e9)
} else {
console.error("Invalid update type", update_field)
return
}
try { try {
const resp = await fetch( const resp = await fetch(
@@ -37,217 +24,98 @@ const update = async (update_field) => {
throw json.message throw json.message
} }
result_success = true success_message.set(true, "Subscription updated")
result = "Subscription updated"
} catch (err) { } catch (err) {
result_success = false success_message.set(false, "Failed to update subscription: "+err)
result = "Failed to update subscription: "+err
} finally { } finally {
loading = false loading = false
} }
} }
let hotlinking = window.user.hotlinking_enabled
let transfer_cap = window.user.monthly_transfer_cap / 1e9
let transfer_used = 0
let load_tranfer_used = () => {
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 => {
transfer_used = resp.amounts.reduce((acc, cur) => { return acc + cur }, 0)
}).catch(e => {
console.error("Error requesting time series: " + e);
})
}
onMount(() => {
load_tranfer_used()
})
</script> </script>
<LoadingIndicator loading={loading}/> <LoadingIndicator loading={loading}/>
<section> <section>
<h2>Manage subscription</h2> <h2>Manage subscription</h2>
{#if window.user.subscription.type !== "patreon"} <p>
<p> Current account balance: <Euro amount={window.user.balance_micro_eur}></Euro>
Current account balance: <Euro amount={window.user.balance_micro_eur}></Euro> </p>
</p> <p>
<p> When your prepaid subscription is active you will be charged daily based
When your prepaid subscription is active you will be charged daily on usage. The prepaid subscription will stay active for as long as you
based on usage. Hotlink bandwidth is charged per TB based on the have credit on your account. When you reach negative balance the
usage of the previous day. The amount charged for storage each day subscription will automatically end. You will need a positive balance to
is your storage usage at the end of the day multiplied by the activate the subscription again.
storage price (€4 / TB) and divided by the average number of days in </p>
a month (30.4375). So if you have exactly 1 TB on your account you
will be charged €0.131416838 per day.
</p>
<p>
The prepaid subscription will stay active for as long as you have
credit on your account. When you reach negative balance the
subscription will automatically end. You will need a positive
balance to activate the subscription again.
</p>
<h3>Prepaid plans</h3> <h3>Prepaid plans</h3>
{#if result !== ""} <SuccessMessage bind:this={success_message}/>
<div class:highlight_green={result_success} class:highlight_red={!result_success}>
{result}
</div>
{/if}
<div class="feat_table"> <div class="feat_table">
<div> <div>
<div class="feat_label" class:feat_highlight={subscription === "prepaid"}> <div class="feat_label" class:feat_highlight={subscription === "prepaid"}>
Prepaid<br/> Prepaid<br/>
{#if subscription === "prepaid"} {#if subscription === "prepaid"}
Currently active Currently active
{:else} {:else}
<button on:click={() => {subscription = "prepaid"; update("subscription")}}> <button on:click={() => {subscription = "prepaid"; update("subscription")}}>
<i class="icon">attach_money</i> <i class="icon">attach_money</i>
Activate Activate
</button> </button>
{/if} {/if}
</div>
<div class="feat_normal round_tr" class:feat_highlight={subscription === "prepaid"}>
<ul>
<li>Base price of €1 per month</li>
<li>€4 per TB per month for storage</li>
<li>€2 per TB for data transfer</li>
<li>Files never expire as long as subscription is active</li>
</ul>
</div>
</div> </div>
<div> <div class="feat_normal round_tr" class:feat_highlight={subscription === "prepaid"}>
<div class="feat_label" class:feat_highlight={subscription === "prepaid_temp_storage_120d"}> <ul>
120 days storage<br/> <li>Base price of €1 per month</li>
{#if subscription === "prepaid_temp_storage_120d"} <li>€3 per TB per month for storage</li>
Currently active <li>€2 per TB for data transfer</li>
{:else} <li>Files never expire as long as subscription is active</li>
<button on:click={() => {subscription = "prepaid_temp_storage_120d"; update("subscription")}}> </ul>
<i class="icon">attach_money</i>
Activate
</button>
{/if}
</div>
<div class="feat_normal" class:feat_highlight={subscription === "prepaid_temp_storage_120d"}>
<ul>
<li>Base price of €1 per month</li>
<li>€1 per TB per month for storage</li>
<li>€2 per TB for data transfer</li>
<li>Files expire 120 days after the last time they're viewed</li>
</ul>
</div>
</div>
<div>
<div class="feat_label" class:feat_highlight={subscription === "prepaid_temp_storage_60d"}>
60 days storage<br/>
{#if subscription === "prepaid_temp_storage_60d"}
Currently active
{:else}
<button on:click={() => {subscription = "prepaid_temp_storage_60d"; update("subscription")}}>
<i class="icon">attach_money</i>
Activate
</button>
{/if}
</div>
<div class="feat_normal" class:feat_highlight={subscription === "prepaid_temp_storage_60d"}>
<ul>
<li>Base price of €1 per month</li>
<li>€0.50 per TB per month for storage</li>
<li>€2 per TB for data transfer</li>
<li>Files expire 60 days after the last time they're viewed</li>
</ul>
</div>
</div>
<div>
<div class="feat_label" class:feat_highlight={subscription === ""}>
Free<br/>
{#if subscription === ""}
Currently active
{:else}
<button on:click={() => {subscription = ""; update("subscription")}}>
<i class="icon">money_off</i>
Activate
</button>
{/if}
</div>
<div class="feat_normal round_br" class:feat_highlight={subscription === ""}>
<ul>
<li>Standard free plan, files expire after 60 days.</li>
</ul>
</div>
</div> </div>
</div> </div>
{/if} <div>
<div class="feat_label" class:feat_highlight={subscription === "prepaid_temp_storage_120d"}>
<h3>Bandwidth sharing</h3> 120 days storage<br/>
{#if hotlinking} {#if subscription === "prepaid_temp_storage_120d"}
<button on:click={() => { hotlinking = false; update("limits") }}> Currently active
<i class="icon green">check</i> ON (click to turn off) {:else}
</button> <button on:click={() => {subscription = "prepaid_temp_storage_120d"; update("subscription")}}>
{:else} <i class="icon">attach_money</i>
<button on:click={() => { hotlinking = true; update("limits") }}> Activate
<i class="icon red">close</i> OFF (click to turn on) </button>
</button> {/if}
{/if} </div>
<p> <div class="feat_normal" class:feat_highlight={subscription === "prepaid_temp_storage_120d"}>
When bandwidth sharing is enabled all the bandwidth that your files <ul>
use will be subtracted from your data cap. Advertisements will be <li>Base price of €1 per month</li>
disabled on the download pages for your files and download speed <li>€0.50 per TB per month for storage</li>
will be unlimited. The rate limiting captcha for files is also <li>€2 per TB for data transfer</li>
disabled when bandwidth sharing is on. You can directly embed your <li>Files expire 120 days after the last time they're viewed</li>
file's download link anywhere, you don't need to use the file viewer </ul>
page. </div>
</p> </div>
<div>
<h3>Bill shock limit</h3> <div class="feat_label" class:feat_highlight={subscription === ""}>
<p> Free<br/>
Billshock limit in gigabytes per month (1 TB = 1000 GB). Set to 0 to disable. {#if subscription === ""}
</p> Currently active
<form on:submit|preventDefault={() => {update("limits")}} class="billshock_container"> {:else}
<input type="number" bind:value={transfer_cap} step="100" min="0"/> <button on:click={() => {subscription = ""; update("subscription")}}>
<div style="margin: 0.5em;">GB</div> <i class="icon">money_off</i>
<button type="submit"> Activate
<i class="icon">save</i> Save </button>
</button> {/if}
</form> </div>
<div class="feat_normal round_br" class:feat_highlight={subscription === ""}>
Bandwidth used in the last 30 days: {formatDataVolume(transfer_used, 3)}, <ul>
new limit: {formatDataVolume(transfer_cap*1e9, 3)} <li>Standard free plan, files expire after 60 days.</li>
<ProgressBar used={transfer_used} total={transfer_cap*1e9}></ProgressBar> </ul>
<p> </div>
The billshock limit limits how much bandwidth your account can use </div>
in a 30 day window. When this limit is reached files will show ads </div>
again and can only be downloaded from the file viewer page. This is
mostly useful for prepaid plans, but it works for patreon plans too.
Set to 0 to disable the limit.
</p>
</section> </section>
<style> <style>
.green {
color: var(--highlight_color);
}
.red {
color: var(--danger_color);
}
.billshock_container {
display: flex;
flex-direction: row;
align-items: center;
}
.feat_table { .feat_table {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -31,7 +31,9 @@ const load_transactions = async () => {
let month = { let month = {
rows: await resp.json(), rows: await resp.json(),
total_subscription_charge: 0, total_subscription_charge: 0,
total_storage_used: 0,
total_storage_charge: 0, total_storage_charge: 0,
total_bandwidth_used: 0,
total_bandwidth_charge: 0, total_bandwidth_charge: 0,
total_deposited: 0, total_deposited: 0,
total_deducted: 0, total_deducted: 0,
@@ -41,10 +43,15 @@ const load_transactions = async () => {
row.time = new Date(row.time) row.time = new Date(row.time)
month.total_deposited += row.deposit_amount month.total_deposited += row.deposit_amount
month.total_subscription_charge += row.subscription_charge month.total_subscription_charge += row.subscription_charge
month.total_storage_used += row.storage_used
month.total_storage_charge += row.storage_charge month.total_storage_charge += row.storage_charge
month.total_bandwidth_used += row.bandwidth_used
month.total_bandwidth_charge += row.bandwidth_charge month.total_bandwidth_charge += row.bandwidth_charge
month.total_deducted += row.subscription_charge + row.storage_charge + row.bandwidth_charge month.total_deducted += row.subscription_charge + row.storage_charge + row.bandwidth_charge
}) })
month.total_storage_used /= month.rows.length
transactions = month transactions = month
} catch (err) { } catch (err) {
alert(err) alert(err)
@@ -71,162 +78,33 @@ const next_month = () => {
load_transactions() load_transactions()
} }
let credit_amount = 10
const checkout = async (network) => {
loading = true
const form = new FormData()
form.set("amount", credit_amount*1e6)
form.set("network", network)
try {
const resp = await fetch(
window.api_endpoint+"/btcpay/deposit",
{ method: "POST", body: form },
)
if(resp.status >= 400) {
let json = await resp.json()
throw json.message
}
window.location = (await resp.json()).checkout_url
} catch (err) {
alert(err)
} finally {
loading = false
}
}
let show_expired = false
let invoices = []
const load_invoices = async () => {
loading = true
try {
const resp = await fetch(window.api_endpoint+"/btcpay/invoice")
if(resp.status >= 400) {
throw new Error((await resp.json()).message)
}
let invoices_tmp = await resp.json()
invoices_tmp.forEach(row => {
row.time = new Date(row.time)
})
invoices_tmp.sort((a, b) => {
return b.time - a.time
})
invoices = invoices_tmp
} catch (err) {
console.error(err)
alert(err)
} finally {
loading = false
}
};
onMount(() => { onMount(() => {
let now = new Date() let now = new Date()
year = now.getFullYear() year = now.getFullYear()
month = now.getMonth()+1 month = now.getMonth()+1
load_transactions() load_transactions()
load_invoices()
}) })
</script> </script>
<LoadingIndicator loading={loading}/> <LoadingIndicator loading={loading}/>
<section> <section>
<h2>Deposit credits</h2>
<p>
You can deposit credit on your pixeldrain account with Bitcoin,
Lightning network (<a
href="https://btcpay.pixeldrain.com/embed/uS2mbWjXUuaAqMh8XLjkjwi8oehFuxeBZxekMxv68LN/BTC/ln"
target="_blank">node info</a>) and Dogecoin. You must pay the full
amount as stated on the invoice, else your payment will fail.
</p>
<p>
Do note that it is not possible to withdraw coins from your
pixeldrain account. It's not a wallet. Any amount of money you
deposit has to be used up.
</p>
<div style="text-align: center;">
Deposit amount €
<input type="number" bind:value={credit_amount} min="1"/>
<br/>
Pay with:<br/>
<button on:click={() => {checkout("btc")}}>
<span class="icon_unicode"></span> Bitcoin
</button>
<button on:click={() => {checkout("btc_lightning")}}>
<i class="icon">bolt</i> Lightning network
</button>
<button on:click={() => {checkout("doge")}}>
<span class="icon_unicode">Ð</span> Dogecoin
</button>
</div>
<h3>Open invoices</h3>
<div class="table_scroll">
<table style="text-align: left;">
<thead>
<tr>
<td>Created</td>
<td>Amount</td>
<td>Status</td>
<td></td>
</tr>
</thead>
<tbody>
{#each invoices as row (row.id)}
{#if row.status === "New" ||
row.status === "InvoiceCreated" ||
row.status === "InvoiceProcessing" ||
show_expired
}
<tr>
<td>{formatDate(row.time, true, true, false)}</td>
<td><Euro amount={row.amount}></Euro></td>
<td>
{#if row.status === "InvoiceCreated"}
New (waiting for payment)
{:else if row.status === "InvoiceProcessing"}
Payment received, waiting for confirmations
{:else if row.status === "InvoiceSettled"}
Paid
{:else if row.status === "InvoiceExpired"}
Expired
{:else}
{row.status}
{/if}
</td>
<td>
{#if row.status === "New" || row.status === "InvoiceCreated"}
<a href={row.checkout_url} class="button button_highlight">
<i class="icon">paid</i> Pay
</a>
{/if}
</td>
</tr>
{/if}
{/each}
</tbody>
</table>
<div style="text-align: center;">
<button on:click={() => {show_expired = !show_expired}}>
{#if show_expired}
Hide
{:else}
Show
{/if}
expired and settled invoices
</button>
</div>
</div>
<h2>Transaction log</h2> <h2>Transaction log</h2>
<p> <p>
Here is a log of all transactions on your account balance. Here is a log of all transactions on your account balance. Usage is
calculated per day. The storage charge is divided by the average number
of months in a day (30.4375).
</p>
<p>
Example: If you have 2 TB stored on your pixeldrain account at €3 per TB
then the daily charge will be:<br/>
2 TB * € 3 = € 6<br/>
€ 6 / 30.4375 = € 0.197125 per day<br/>
Similarly the subscription charge of €1 per month is also charged at € 1
/ 30.4375 = € 0.032854 per day.
</p> </p>
<h3>{month_str}</h3> <h3>{month_str}</h3>
@@ -242,11 +120,23 @@ onMount(() => {
</button> </button>
</div> </div>
<ul> <ul>
<li>Subscription charge: <Euro amount={transactions.total_subscription_charge}></Euro></li> <li>
<li>Storage charge: <Euro amount={transactions.total_storage_charge}></Euro></li> Subscription charge: <Euro amount={transactions.total_subscription_charge} precision="4"/>
<li>Bandwidth charge: <Euro amount={transactions.total_bandwidth_charge}></Euro></li> </li>
<li>Total charge: <Euro amount={transactions.total_deducted}></Euro></li> <li>
<li>Deposited: <Euro amount={transactions.total_deposited}></Euro></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>
Total charge: <Euro amount={transactions.total_deducted} precision="4"/>
</li>
<li>
Deposited: <Euro amount={transactions.total_deposited} precision="4"/>
</li>
</ul> </ul>
<div class="table_scroll"> <div class="table_scroll">
@@ -276,10 +166,10 @@ onMount(() => {
<tr> <tr>
<td>{formatDate(row.time, true, true, false)}</td> <td>{formatDate(row.time, true, true, false)}</td>
<td><Euro amount={row.new_balance}></Euro></td> <td><Euro amount={row.new_balance}></Euro></td>
<td><Euro amount={row.subscription_charge} precision="4"></Euro></td> <td><Euro amount={row.subscription_charge} precision="6"></Euro></td>
<td><Euro amount={row.storage_charge} precision="4"></Euro></td> <td><Euro amount={row.storage_charge} precision="6"></Euro></td>
<td>{formatDataVolume(row.storage_used, 3)}</td> <td>{formatDataVolume(row.storage_used, 3)}</td>
<td><Euro amount={row.bandwidth_charge} precision="4"></Euro></td> <td><Euro amount={row.bandwidth_charge} precision="6"></Euro></td>
<td>{formatDataVolume(row.bandwidth_used, 3)}</td> <td>{formatDataVolume(row.bandwidth_used, 3)}</td>
<td><Euro amount={row.deposit_amount}></Euro></td> <td><Euro amount={row.deposit_amount}></Euro></td>
</tr> </tr>

View File

@@ -50,7 +50,6 @@ Chart.defaults.animation.duration = 500;
Chart.defaults.animation.easing = "linear"; Chart.defaults.animation.easing = "linear";
onMount(() => { onMount(() => {
console.log(legend)
chart_object = new Chart( chart_object = new Chart(
chart_element.getContext("2d"), chart_element.getContext("2d"),
{ {

View File

@@ -0,0 +1,107 @@
<script>
import { onMount } from "svelte";
import Footer from "../layout/Footer.svelte";
export let title = ""
export let pages = []
let navigate = (path, title) => {
window.document.title = title+" ~ pixeldrain"
window.history.pushState({}, window.document.title, path)
get_page()
}
let get_page = () => {
current_page = null
current_subpage = null
pages.forEach(page => {
if (window.location.pathname.endsWith(page.path)) {
current_page = page
}
if (page.subpages) {
page.subpages.forEach(subpage => {
if (window.location.pathname.endsWith(subpage.path)) {
current_page = page
current_subpage = subpage
}
})
}
})
// If no page is active, default to home
if (!current_page) {
current_page = pages[0]
}
if (!current_subpage && current_page.subpages) {
current_subpage = current_page.subpages[0]
}
console.log("Page", current_page)
console.log("Subpage", current_subpage)
pages = pages
}
let current_page = null
let current_subpage = null
onMount(() => get_page())
</script>
<svelte:window on:popstate={get_page} />
<header>
<h1>{title}</h1>
<div class="tab_bar">
{#each pages as page}
{#if !page.hidden}
<a class="button"
href="{page.path}"
class:button_highlight={current_page && page.path === current_page.path}
on:click|preventDefault={() => {navigate(page.path, page.title)}}>
<i class="icon">{page.icon}</i><br/>
{page.title}
</a>
{/if}
{/each}
</div>
</header>
<div id="page_content" class="page_content">
{#if current_page}
{#if current_page.subpages}
<div class="tab_bar submenu">
{#each current_page.subpages as page}
{#if !page.hidden}
<a class="button"
href="{page.path}"
class:button_highlight={current_subpage && page.path === current_subpage.path}
on:click|preventDefault={() => {navigate(page.path, page.title)}}>
<i class="icon">{page.icon}</i><br/>
{page.title}
</a>
{/if}
{/each}
</div>
{#if current_subpage}
<svelte:component this={current_subpage.component} />
{/if}
{:else}
<svelte:component this={current_page.component} />
{/if}
{/if}
</div>
<Footer/>
<style>
.submenu{
border-bottom: 2px solid var(--separator);
}
</style>

View File

@@ -178,11 +178,14 @@ func New(
{GET, "user/home" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true})}, {GET, "user/home" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true})},
{GET, "user/settings" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true})}, {GET, "user/settings" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true})},
{GET, "user/sharing" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true})}, {GET, "user/sharing" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true})},
{GET, "user/sharing/*p" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true})},
{GET, "user/api_keys" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true})}, {GET, "user/api_keys" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true})},
{GET, "user/activity" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true})}, {GET, "user/activity" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true})},
{GET, "user/connect_app" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true})}, {GET, "user/connect_app" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true})},
{GET, "user/transactions" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true})}, {GET, "user/transactions" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true})},
{GET, "user/subscription" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true})}, {GET, "user/subscription" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true})},
{GET, "user/prepaid" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true})},
{GET, "user/prepaid/*p" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true})},
{GET, "user/confirm_email" /* */, wc.serveEmailConfirm}, {GET, "user/confirm_email" /* */, wc.serveEmailConfirm},
{GET, "user/password_reset_confirm" /**/, wc.serveForm(wc.passwordResetConfirmForm, handlerOpts{NoEmbed: true})}, {GET, "user/password_reset_confirm" /**/, wc.serveForm(wc.passwordResetConfirmForm, handlerOpts{NoEmbed: true})},
{PST, "user/password_reset_confirm" /**/, wc.serveForm(wc.passwordResetConfirmForm, handlerOpts{NoEmbed: true})}, {PST, "user/password_reset_confirm" /**/, wc.serveForm(wc.passwordResetConfirmForm, handlerOpts{NoEmbed: true})},
@@ -200,10 +203,10 @@ func New(
{GET, "admin" /* */, wc.serveTemplate("admin", handlerOpts{Auth: true})}, {GET, "admin" /* */, wc.serveTemplate("admin", handlerOpts{Auth: true})},
{GET, "admin/status" /* */, wc.serveTemplate("admin", handlerOpts{Auth: true})}, {GET, "admin/status" /* */, wc.serveTemplate("admin", handlerOpts{Auth: true})},
{GET, "admin/block_files" /* */, wc.serveTemplate("admin", handlerOpts{Auth: true})}, {GET, "admin/block_files" /* */, wc.serveTemplate("admin", handlerOpts{Auth: true})},
{GET, "admin/abuse_reporters" /**/, wc.serveTemplate("admin", handlerOpts{Auth: true})}, {GET, "admin/email_reporters" /**/, wc.serveTemplate("admin", handlerOpts{Auth: true})},
{GET, "admin/abuse_reports" /* */, wc.serveTemplate("admin", handlerOpts{Auth: true})}, {GET, "admin/abuse_reports" /* */, wc.serveTemplate("admin", handlerOpts{Auth: true})},
{GET, "admin/ip_bans" /* */, wc.serveTemplate("admin", handlerOpts{Auth: true})}, {GET, "admin/ip_bans" /* */, wc.serveTemplate("admin", handlerOpts{Auth: true})},
{GET, "admin/subscriptions" /* */, wc.serveTemplate("admin", handlerOpts{Auth: true})}, {GET, "admin/user_management" /**/, wc.serveTemplate("admin", handlerOpts{Auth: true})},
{GET, "admin/globals" /* */, wc.serveForm(wc.adminGlobalsForm, handlerOpts{Auth: true})}, {GET, "admin/globals" /* */, wc.serveForm(wc.adminGlobalsForm, handlerOpts{Auth: true})},
{PST, "admin/globals" /* */, wc.serveForm(wc.adminGlobalsForm, handlerOpts{Auth: true})}, {PST, "admin/globals" /* */, wc.serveForm(wc.adminGlobalsForm, handlerOpts{Auth: true})},