Overhaul user settings page
This commit is contained in:
@@ -178,6 +178,7 @@ onMount(get_reporters);
|
||||
on:delete={e => delete_reporter(e.detail)}>
|
||||
</AbuseReporterTable>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<style>
|
||||
.toolbar {
|
@@ -187,6 +187,10 @@ onDestroy(() => {
|
||||
<br/>
|
||||
<ServerDiagnostics running_since={status.cpu_profile_running_since} on:refresh={() => getStats(lastOrder)}/>
|
||||
<br/>
|
||||
<a class="button" href="/admin/globals">
|
||||
<i class="icon">edit</i>
|
||||
Global settings
|
||||
</a>
|
||||
|
||||
<section>
|
||||
<table>
|
||||
|
@@ -1,98 +1,45 @@
|
||||
<script>
|
||||
import AbuseReporters from "./AbuseReporters.svelte"
|
||||
import AbuseReports from "./AbuseReports.svelte"
|
||||
import IpBans from "./IPBans.svelte"
|
||||
import IPBans from "./IPBans.svelte"
|
||||
import Home from "./Home.svelte"
|
||||
import { onMount } from "svelte";
|
||||
import BlockFiles from "./BlockFiles.svelte";
|
||||
import Subscriptions from "./Subscriptions.svelte";
|
||||
import Footer from "../layout/Footer.svelte";
|
||||
import TabMenu from "../util/TabMenu.svelte";
|
||||
import UserManagement from "./UserManagement.svelte";
|
||||
import EmailReporters from "./EmailReporters.svelte";
|
||||
|
||||
let page = ""
|
||||
|
||||
let navigate = (path, title) => {
|
||||
page = path
|
||||
window.document.title = title+" ~ pixeldrain"
|
||||
window.history.pushState(
|
||||
{}, window.document.title, "/admin/"+path
|
||||
)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
let newpage = window.location.pathname.substring(window.location.pathname.lastIndexOf("/")+1)
|
||||
if (newpage === "admin") {
|
||||
newpage = "status"
|
||||
}
|
||||
page = newpage
|
||||
})
|
||||
let pages = [
|
||||
{
|
||||
path: "/admin",
|
||||
title: "Status",
|
||||
icon: "home",
|
||||
component: Home,
|
||||
}, {
|
||||
path: "/admin/block_files",
|
||||
title: "Block Files",
|
||||
icon: "block",
|
||||
component: BlockFiles,
|
||||
}, {
|
||||
path: "/admin/abuse_reports",
|
||||
title: "User Reports",
|
||||
icon: "flag",
|
||||
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>
|
||||
|
||||
<header>
|
||||
<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/>
|
||||
<TabMenu pages={pages} title="Admin Panel"/>
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import { onMount } from "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";
|
||||
|
||||
let loading = true
|
||||
@@ -176,6 +176,11 @@ onMount(get_coupons)
|
||||
<LoadingIndicator loading={loading}/>
|
||||
|
||||
<section>
|
||||
<h2>Impersonate user</h2>
|
||||
<div class="highlight_shaded">
|
||||
<Form config={impersonate_form}></Form>
|
||||
</div>
|
||||
|
||||
<h2>Give user credit</h2>
|
||||
<p>
|
||||
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>
|
||||
</div>
|
||||
|
||||
<h2>Impersonate user</h2>
|
||||
<div class="highlight_shaded">
|
||||
<Form config={impersonate_form}></Form>
|
||||
</div>
|
||||
|
||||
<h2>Create coupon codes</h2>
|
||||
<div class="highlight_shaded">
|
||||
<Form config={coupon_form}></Form>
|
@@ -302,11 +302,9 @@ const node_click = (index) => {
|
||||
<div id="directory_element">
|
||||
<div bind:this={directorySorters} id="sorters" class="directory_sorters">
|
||||
{#each tableColumns as col}
|
||||
<!-- <div style="min-width: {col.width}"> -->
|
||||
<button style="min-width: {col.width}" on:click={sortBy(col.field)} class="sorter_button">
|
||||
{col.name}
|
||||
</button>
|
||||
<!-- </div> -->
|
||||
{/each}
|
||||
</div>
|
||||
<div bind:this={directoryArea} on:scroll={onScroll} id="directory_area" class="directory_area">
|
||||
|
135
svelte/src/user_home/BandwidthSharing.svelte
Normal file
135
svelte/src/user_home/BandwidthSharing.svelte
Normal 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>
|
154
svelte/src/user_home/DepositCredit.svelte
Normal file
154
svelte/src/user_home/DepositCredit.svelte
Normal 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>
|
88
svelte/src/user_home/EmbeddingControls.svelte
Normal file
88
svelte/src/user_home/EmbeddingControls.svelte
Normal 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>
|
@@ -1,10 +1,10 @@
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import FilePicker from "../file_viewer/FilePicker.svelte";
|
||||
import CustomBanner from "../file_viewer/CustomBanner.svelte";
|
||||
import LoadingIndicator from "../util/LoadingIndicator.svelte";
|
||||
import SuccessMessage from "../util/SuccessMessage.svelte";
|
||||
import ThemePicker from "../util/ThemePicker.svelte";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
let loading = false
|
||||
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(() => {
|
||||
// The fields are undefined when they're empty. So we need to check if each
|
||||
// field is defined before converting to a string
|
||||
@@ -113,16 +86,13 @@ onMount(() => {
|
||||
footer_image = b.footer_image ? b.footer_image : ""
|
||||
footer_link = b.footer_link ? b.footer_link : ""
|
||||
}
|
||||
|
||||
embed_domains = window.user.file_embed_domains
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<LoadingIndicator loading={loading}/>
|
||||
|
||||
<section>
|
||||
<h2>Sharing settings</h2>
|
||||
<h2>File viewer branding</h2>
|
||||
{#if !window.user.subscription.file_viewer_branding}
|
||||
<div class="highlight_red">
|
||||
Sharing settings are not available for your account. Subscribe to
|
||||
@@ -130,13 +100,13 @@ onMount(() => {
|
||||
</div>
|
||||
{:else if !window.user.hotlinking_enabled}
|
||||
<div class="highlight_red">
|
||||
To use the sharing settings bandwidth sharing needs to be enabled.
|
||||
Enable bandwidth sharing on the
|
||||
<a href="/user/subscription">subscription page</a>.
|
||||
To use custom file viewer branding bandwidth sharing needs to be
|
||||
enabled. Enable bandwidth sharing on the
|
||||
<a href="/user/sharing/bandwidth">bandwidth sharing page</a>.
|
||||
</div>
|
||||
{/if}
|
||||
<h3>File viewer branding</h3>
|
||||
<SuccessMessage bind:this={success_message}></SuccessMessage>
|
||||
|
||||
<p>
|
||||
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.
|
||||
@@ -148,7 +118,7 @@ onMount(() => {
|
||||
should use APNG or WebP. Avoid using animated GIFs as they are very slow
|
||||
to load.
|
||||
</p>
|
||||
<h4>Theme</h4>
|
||||
<h3>Theme</h3>
|
||||
<p>
|
||||
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
|
||||
@@ -159,7 +129,7 @@ onMount(() => {
|
||||
on:theme_change={e => {theme = e.detail; save()}}>
|
||||
</ThemePicker>
|
||||
|
||||
<h4>Header image</h4>
|
||||
<h3>Header image</h3>
|
||||
<p>
|
||||
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
|
||||
@@ -174,8 +144,8 @@ onMount(() => {
|
||||
Remove
|
||||
</button>
|
||||
<br/>
|
||||
Header image link:<br/>
|
||||
<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://"/>
|
||||
<button class="shrink" action="submit"><i class="icon">save</i> Save</button>
|
||||
</form>
|
||||
@@ -186,7 +156,7 @@ onMount(() => {
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<h4>Background image</h4>
|
||||
<h3>Background image</h3>
|
||||
<p>
|
||||
This image will be shown behind the file which is being viewed. I
|
||||
recommend choosing something dark and not too distracting. Try to keep
|
||||
@@ -207,7 +177,7 @@ onMount(() => {
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<h4>Footer image</h4>
|
||||
<h3>Footer image</h3>
|
||||
<p>
|
||||
Will be shown below the file. Maximum height is 90px. Will be shrunk if
|
||||
larger.
|
||||
@@ -221,8 +191,8 @@ onMount(() => {
|
||||
Remove
|
||||
</button>
|
||||
<br/>
|
||||
Footer image link:<br/>
|
||||
<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://"/>
|
||||
<button class="shrink" action="submit"><i class="icon">save</i> Save</button>
|
||||
</form>
|
||||
@@ -231,22 +201,6 @@ onMount(() => {
|
||||
<CustomBanner src={"/api/file/"+footer_image} link={footer_link}></CustomBanner>
|
||||
</div>
|
||||
{/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>
|
||||
|
||||
<FilePicker
|
@@ -1,5 +1,4 @@
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import Home from "./Home.svelte";
|
||||
import AccountSettings from "./AccountSettings.svelte";
|
||||
import APIKeys from "./APIKeys.svelte";
|
||||
@@ -7,116 +6,85 @@ import Transactions from "./Transactions.svelte";
|
||||
import Subscription from "./Subscription.svelte";
|
||||
import ConnectApp from "./ConnectApp.svelte";
|
||||
import ActivityLog from "./ActivityLog.svelte";
|
||||
import SharingSettings from "./SharingSettings.svelte";
|
||||
import Footer from "../layout/Footer.svelte";
|
||||
import DepositCredit from "./DepositCredit.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 navigate = (path, title) => {
|
||||
page = path
|
||||
window.document.title = title+" ~ pixeldrain"
|
||||
window.history.pushState(
|
||||
{}, window.document.title, "/user/"+path
|
||||
)
|
||||
}
|
||||
|
||||
let get_page = () => {
|
||||
let newpage = window.location.pathname.substring(window.location.pathname.lastIndexOf("/")+1)
|
||||
if (newpage === "user") {
|
||||
newpage = "home"
|
||||
}
|
||||
|
||||
page = newpage
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
get_page()
|
||||
})
|
||||
let pages = [
|
||||
{
|
||||
path: "/user/home",
|
||||
title: "My Home",
|
||||
icon: "home",
|
||||
component: Home,
|
||||
}, {
|
||||
path: "/user/settings",
|
||||
title: "Settings",
|
||||
icon: "settings",
|
||||
component: AccountSettings,
|
||||
}, {
|
||||
path: "/user/sharing",
|
||||
title: "Sharing",
|
||||
icon: "share",
|
||||
subpages: [
|
||||
{
|
||||
path: "/user/sharing/bandwidth",
|
||||
title: "Bandwidth Sharing",
|
||||
icon: "share",
|
||||
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>
|
||||
|
||||
<svelte:window on:popstate={get_page} />
|
||||
|
||||
<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/>
|
||||
<TabMenu pages={pages} title="Welcome home, {window.user.username}!"/>
|
||||
|
@@ -1,31 +1,18 @@
|
||||
<script>
|
||||
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 SuccessMessage from "../util/SuccessMessage.svelte";
|
||||
|
||||
let loading = false
|
||||
let subscription = window.user.subscription.id
|
||||
let success_message
|
||||
|
||||
let result = ""
|
||||
let result_success = false
|
||||
|
||||
const update = async (update_field) => {
|
||||
const update = async () => {
|
||||
loading = true
|
||||
|
||||
const form = new FormData()
|
||||
if (update_field === "subscription") {
|
||||
form.append("update", "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 {
|
||||
const resp = await fetch(
|
||||
@@ -37,75 +24,32 @@ const update = async (update_field) => {
|
||||
throw json.message
|
||||
}
|
||||
|
||||
result_success = true
|
||||
result = "Subscription updated"
|
||||
success_message.set(true, "Subscription updated")
|
||||
} catch (err) {
|
||||
result_success = false
|
||||
result = "Failed to update subscription: "+err
|
||||
success_message.set(false, "Failed to update subscription: "+err)
|
||||
} finally {
|
||||
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>
|
||||
|
||||
<LoadingIndicator loading={loading}/>
|
||||
|
||||
<section>
|
||||
<h2>Manage subscription</h2>
|
||||
{#if window.user.subscription.type !== "patreon"}
|
||||
<p>
|
||||
Current account balance: <Euro amount={window.user.balance_micro_eur}></Euro>
|
||||
</p>
|
||||
<p>
|
||||
When your prepaid subscription is active you will be charged daily
|
||||
based on usage. Hotlink bandwidth is charged per TB based on the
|
||||
usage of the previous day. The amount charged for storage each day
|
||||
is your storage usage at the end of the day multiplied by the
|
||||
storage price (€4 / TB) and divided by the average number of days in
|
||||
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.
|
||||
When your prepaid subscription is active you will be charged daily based
|
||||
on usage. 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>
|
||||
{#if result !== ""}
|
||||
<div class:highlight_green={result_success} class:highlight_red={!result_success}>
|
||||
{result}
|
||||
</div>
|
||||
{/if}
|
||||
<SuccessMessage bind:this={success_message}/>
|
||||
|
||||
<div class="feat_table">
|
||||
<div>
|
||||
@@ -123,7 +67,7 @@ onMount(() => {
|
||||
<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>€3 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>
|
||||
@@ -142,32 +86,11 @@ onMount(() => {
|
||||
{/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>
|
||||
<li>Files expire 120 days after the last time they're viewed</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -190,64 +113,9 @@ onMount(() => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<h3>Bandwidth sharing</h3>
|
||||
{#if hotlinking}
|
||||
<button on:click={() => { hotlinking = false; update("limits") }}>
|
||||
<i class="icon green">check</i> ON (click to turn off)
|
||||
</button>
|
||||
{:else}
|
||||
<button on:click={() => { hotlinking = true; update("limits") }}>
|
||||
<i class="icon red">close</i> OFF (click to turn on)
|
||||
</button>
|
||||
{/if}
|
||||
<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>
|
||||
|
||||
<h3>Bill shock limit</h3>
|
||||
<p>
|
||||
Billshock limit in gigabytes per month (1 TB = 1000 GB). Set to 0 to disable.
|
||||
</p>
|
||||
<form on:submit|preventDefault={() => {update("limits")}} 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>
|
||||
|
||||
Bandwidth used in the last 30 days: {formatDataVolume(transfer_used, 3)},
|
||||
new limit: {formatDataVolume(transfer_cap*1e9, 3)}
|
||||
<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;
|
||||
}
|
||||
.feat_table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@@ -31,7 +31,9 @@ const load_transactions = async () => {
|
||||
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,
|
||||
@@ -41,10 +43,15 @@ const load_transactions = async () => {
|
||||
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
|
||||
})
|
||||
|
||||
month.total_storage_used /= month.rows.length
|
||||
|
||||
transactions = month
|
||||
} catch (err) {
|
||||
alert(err)
|
||||
@@ -71,162 +78,33 @@ const next_month = () => {
|
||||
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(() => {
|
||||
let now = new Date()
|
||||
year = now.getFullYear()
|
||||
month = now.getMonth()+1
|
||||
|
||||
load_transactions()
|
||||
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>
|
||||
|
||||
|
||||
<h2>Transaction log</h2>
|
||||
<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>
|
||||
|
||||
<h3>{month_str}</h3>
|
||||
@@ -242,11 +120,23 @@ onMount(() => {
|
||||
</button>
|
||||
</div>
|
||||
<ul>
|
||||
<li>Subscription charge: <Euro amount={transactions.total_subscription_charge}></Euro></li>
|
||||
<li>Storage charge: <Euro amount={transactions.total_storage_charge}></Euro></li>
|
||||
<li>Bandwidth charge: <Euro amount={transactions.total_bandwidth_charge}></Euro></li>
|
||||
<li>Total charge: <Euro amount={transactions.total_deducted}></Euro></li>
|
||||
<li>Deposited: <Euro amount={transactions.total_deposited}></Euro></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>
|
||||
Total charge: <Euro amount={transactions.total_deducted} precision="4"/>
|
||||
</li>
|
||||
<li>
|
||||
Deposited: <Euro amount={transactions.total_deposited} precision="4"/>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="table_scroll">
|
||||
@@ -276,10 +166,10 @@ onMount(() => {
|
||||
<tr>
|
||||
<td>{formatDate(row.time, true, true, false)}</td>
|
||||
<td><Euro amount={row.new_balance}></Euro></td>
|
||||
<td><Euro amount={row.subscription_charge} precision="4"></Euro></td>
|
||||
<td><Euro amount={row.storage_charge} precision="4"></Euro></td>
|
||||
<td><Euro amount={row.subscription_charge} precision="6"></Euro></td>
|
||||
<td><Euro amount={row.storage_charge} precision="6"></Euro></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><Euro amount={row.deposit_amount}></Euro></td>
|
||||
</tr>
|
||||
|
@@ -50,7 +50,6 @@ Chart.defaults.animation.duration = 500;
|
||||
Chart.defaults.animation.easing = "linear";
|
||||
|
||||
onMount(() => {
|
||||
console.log(legend)
|
||||
chart_object = new Chart(
|
||||
chart_element.getContext("2d"),
|
||||
{
|
||||
|
107
svelte/src/util/TabMenu.svelte
Normal file
107
svelte/src/util/TabMenu.svelte
Normal 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>
|
@@ -178,11 +178,14 @@ func New(
|
||||
{GET, "user/home" /* */, 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/*p" /* */, 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/connect_app" /* */, 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/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/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/status" /* */, 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/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})},
|
||||
{PST, "admin/globals" /* */, wc.serveForm(wc.adminGlobalsForm, handlerOpts{Auth: true})},
|
||||
|
||||
|
Reference in New Issue
Block a user