Rewrite user home page in svelte
This commit is contained in:
@@ -145,7 +145,7 @@ body, .checkers {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
padding: 20px 0 20px 0;
|
||||
padding: 0 0 20px 0;
|
||||
background-color: #212121;
|
||||
background-color: var(--layer_2_color);
|
||||
box-shadow: 1px 1px 20px 0 #000000;
|
||||
@@ -498,6 +498,23 @@ select:disabled , select.disabled {
|
||||
.round {
|
||||
border-radius: 32px;
|
||||
}
|
||||
.tab {
|
||||
margin: 0 0 10px 8px;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
.tab:last-child {
|
||||
margin-right: 8px;
|
||||
}
|
||||
.tab_bar {
|
||||
display: block;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
white-space: nowrap;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Dropdown list of the select tag */
|
||||
option{
|
||||
|
27
res/template/account/user_home_svelte.html
Normal file
27
res/template/account/user_home_svelte.html
Normal file
@@ -0,0 +1,27 @@
|
||||
{{define "user_home_svelte"}}<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
{{template "meta_tags" .User.Username}}
|
||||
{{template "user_style" .}}
|
||||
|
||||
<script>
|
||||
window.api_endpoint = '{{.APIEndpoint}}';
|
||||
window.highlight_color = '#{{.Style.HighlightColor.RGB}}';
|
||||
window.user = {{.User}};
|
||||
</script>
|
||||
<link rel='stylesheet' href='/res/svelte/user_home.css'>
|
||||
<script defer src='/res/svelte/user_home.js'></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{{template "page_top" .}}
|
||||
|
||||
<h1>Welcome home, {{.User.Username}}!</h1>
|
||||
|
||||
<div id="page_content" class="page_content"></div>
|
||||
|
||||
{{template "page_bottom" .}}
|
||||
{{template "analytics"}}
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
@@ -15,6 +15,7 @@
|
||||
</head>
|
||||
<body>
|
||||
{{template "page_top" .}}
|
||||
<h1>Admin Panel</h1>
|
||||
<div id="page_content" class="page_content"></div>
|
||||
{{template "page_bottom" .}}
|
||||
</body>
|
||||
|
@@ -55,8 +55,8 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
.feat_table > div > .cell_background {
|
||||
flex: 0 0 30%;
|
||||
min-width: 30%;
|
||||
flex: 0 0 33%;
|
||||
min-width: 33%;
|
||||
border-top-left-radius: 0.5em;
|
||||
border-bottom-left-radius: 0.5em;
|
||||
background-position: center;
|
||||
@@ -101,7 +101,7 @@
|
||||
<br/>
|
||||
|
||||
<!-- Svelte element -->
|
||||
<div id="uploader" class="page_content" style="padding-top: 0; margin-bottom: 50px;"></div>
|
||||
<div id="uploader" class="page_content" style="margin-bottom: 50px;"></div>
|
||||
|
||||
<h1>What is pixeldrain?</h1>
|
||||
<div class="page_content"><div class="limit_width">
|
||||
@@ -212,6 +212,19 @@
|
||||
<span class="text_highlight">1 terabyte</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="feat_label">
|
||||
Bandwidth priority
|
||||
</div>
|
||||
<div class="feat_normal">
|
||||
Download speed will be throttled during busy periods
|
||||
</div>
|
||||
<div class="feat_pro">
|
||||
<span class="text_highlight">High priority</span>
|
||||
bandwidth for files you download and files on your
|
||||
account
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="feat_label">
|
||||
Online file previews
|
||||
|
@@ -32,6 +32,7 @@ export default [
|
||||
"file_viewer",
|
||||
"filesystem",
|
||||
"modal",
|
||||
"user_home",
|
||||
"user_buckets",
|
||||
"user_file_manager",
|
||||
"admin_panel",
|
||||
|
@@ -25,42 +25,44 @@ onMount(() => {
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<a class="button"
|
||||
href="/admin"
|
||||
class:button_highlight={page === "status"}
|
||||
on:click|preventDefault={() => {navigate("status", "Status")}}>
|
||||
<i class="icon">home</i>
|
||||
Status
|
||||
</a>
|
||||
<a class="button" href="/admin/abuse">
|
||||
<i class="icon">block</i>
|
||||
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>
|
||||
User abuse 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">report</i>
|
||||
E-mail abuse 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>
|
||||
IP bans
|
||||
</a>
|
||||
<a class="button" href="/admin/globals">
|
||||
<i class="icon">edit</i>
|
||||
Update global settings
|
||||
</a>
|
||||
<div class="tab_bar">
|
||||
<a class="button tab"
|
||||
href="/admin"
|
||||
class:button_highlight={page === "status"}
|
||||
on:click|preventDefault={() => {navigate("status", "Status")}}>
|
||||
<i class="icon">home</i>
|
||||
Status
|
||||
</a>
|
||||
<a class="button tab" href="/admin/abuse">
|
||||
<i class="icon">block</i>
|
||||
Block files
|
||||
</a>
|
||||
<a class="button tab"
|
||||
href="/admin/abuse_reports"
|
||||
class:button_highlight={page === "abuse_reports"}
|
||||
on:click|preventDefault={() => {navigate("abuse_reports", "Abuse reports")}}>
|
||||
<i class="icon">flag</i>
|
||||
User abuse reports
|
||||
</a>
|
||||
<a class="button tab"
|
||||
href="/admin/abuse_reporters"
|
||||
class:button_highlight={page === "abuse_reporters"}
|
||||
on:click|preventDefault={() => {navigate("abuse_reporters", "Abuse reporters")}}>
|
||||
<i class="icon">report</i>
|
||||
E-mail abuse reporters
|
||||
</a>
|
||||
<a class="button tab"
|
||||
href="/admin/ip_bans"
|
||||
class:button_highlight={page === "ip_bans"}
|
||||
on:click|preventDefault={() => {navigate("ip_bans", "IP bans")}}>
|
||||
<i class="icon">remove_circle</i>
|
||||
IP bans
|
||||
</a>
|
||||
<a class="button tab" href="/admin/globals">
|
||||
<i class="icon">edit</i>
|
||||
Update global settings
|
||||
</a>
|
||||
</div>
|
||||
<hr/>
|
||||
|
||||
{#if page === "status"}
|
||||
|
8
svelte/src/user_home.js
Normal file
8
svelte/src/user_home.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import App from './user_home/Router.svelte';
|
||||
|
||||
const app = new App({
|
||||
target: document.getElementById("page_content"),
|
||||
props: {}
|
||||
});
|
||||
|
||||
export default app;
|
126
svelte/src/user_home/AccountSettings.svelte
Normal file
126
svelte/src/user_home/AccountSettings.svelte
Normal file
@@ -0,0 +1,126 @@
|
||||
<script>
|
||||
import Form from "./../util/Form.svelte";
|
||||
|
||||
let password_change = {
|
||||
name: "password_change",
|
||||
fields: [
|
||||
{
|
||||
name: "old_password",
|
||||
label: "Old password",
|
||||
type: "current_password",
|
||||
}, {
|
||||
name: "new_password",
|
||||
label: "New password",
|
||||
type: "new_password",
|
||||
}, {
|
||||
name: "new_password2",
|
||||
label: "New password again",
|
||||
type: "new_password",
|
||||
description: "we need you to repeat your password so you " +
|
||||
"won't be locked out of your account if you make a " +
|
||||
"typing error"
|
||||
},
|
||||
],
|
||||
submit_label: `<i class="icon">save</i> Save`,
|
||||
on_submit: async fields => {
|
||||
if (fields.new_password != fields.new_password2) {
|
||||
return {success: false, message: "Passwords do not match! Please enter the same password in both fields"}
|
||||
}
|
||||
|
||||
const form = new FormData()
|
||||
form.append("old_password", fields.old_password)
|
||||
form.append("new_password", fields.new_password)
|
||||
|
||||
const resp = await fetch(
|
||||
window.api_endpoint+"/user/password",
|
||||
{ method: "PUT", body: form }
|
||||
);
|
||||
if(resp.status >= 400) {
|
||||
return {error_json: await resp.json()}
|
||||
}
|
||||
return {success: true, message: "Success! Your password has been updated"}
|
||||
},
|
||||
}
|
||||
|
||||
let email_change = {
|
||||
name: "email_change",
|
||||
fields: [
|
||||
{
|
||||
name: "new_email",
|
||||
label: "New e-mail address",
|
||||
type: "email",
|
||||
default_value: window.user.email,
|
||||
description: `we will send an e-mail to the new address to
|
||||
verify that it's real. The address will be saved once the
|
||||
link in the message is clicked. If the e-mail doesn't arrive
|
||||
right away please check your spam box too. Leave the field
|
||||
empty to remove your current e-mail address from your
|
||||
account`,
|
||||
},
|
||||
],
|
||||
submit_label: `<i class="icon">save</i> Save`,
|
||||
on_submit: async fields => {
|
||||
const form = new FormData()
|
||||
form.append("new_email", fields.new_email)
|
||||
|
||||
const resp = await fetch(
|
||||
window.api_endpoint+"/user/email_reset",
|
||||
{ method: "PUT", body: form }
|
||||
);
|
||||
if(resp.status >= 400) {
|
||||
return {error_json: await resp.json()}
|
||||
}
|
||||
return {success: true, message: "Success! E-mail sent. Click the link in the message to verify your new address"}
|
||||
},
|
||||
}
|
||||
|
||||
let name_change = {
|
||||
name: "name_change",
|
||||
fields: [
|
||||
{
|
||||
name: "new_username",
|
||||
label: "New name",
|
||||
type: "username",
|
||||
description: `changing your username also changes the name used to
|
||||
log in. If you forget your username you can still log in using
|
||||
your e-mail address if you have one configured`,
|
||||
},
|
||||
],
|
||||
submit_label: `<i class="icon">save</i> Save`,
|
||||
on_submit: async fields => {
|
||||
const form = new FormData()
|
||||
form.append("new_username", fields.new_username)
|
||||
|
||||
const resp = await fetch(
|
||||
window.api_endpoint+"/user/username",
|
||||
{ method: "PUT", body: form }
|
||||
);
|
||||
if(resp.status >= 400) {
|
||||
return {error_json: await resp.json()}
|
||||
}
|
||||
return {success: true, message: "Success! You are now known as "+fields.new_username}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="limit_width">
|
||||
<h2>Change password</h2>
|
||||
<div class="highlight_dark">
|
||||
<Form config={password_change}></Form>
|
||||
</div>
|
||||
|
||||
<h2>Change e-mail address</h2>
|
||||
<div class="highlight_dark">
|
||||
<Form config={email_change}></Form>
|
||||
</div>
|
||||
|
||||
<h2>Change name</h2>
|
||||
<div class="highlight_dark">
|
||||
<Form config={name_change}></Form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
</style>
|
286
svelte/src/user_home/Home.svelte
Normal file
286
svelte/src/user_home/Home.svelte
Normal file
@@ -0,0 +1,286 @@
|
||||
<script>
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import { formatDataVolume, formatThousands } from "../util/Formatting.svelte";
|
||||
import Chart from "../util/Chart.svelte";
|
||||
|
||||
let graph_view = null
|
||||
let graph_download = null
|
||||
let graph_bandwidth = null
|
||||
let graph_direct_link = null
|
||||
let time_start = ""
|
||||
let time_end = ""
|
||||
let total_views = 0
|
||||
let total_downloads = 0
|
||||
let total_bandwidth = 0
|
||||
let total_direct_link = 0
|
||||
|
||||
let load_graph = (graph, stat, minutes, interval) => {
|
||||
let today = new Date()
|
||||
let start = new Date()
|
||||
start.setMinutes(start.getMinutes() - minutes)
|
||||
|
||||
fetch(
|
||||
window.api_endpoint + "/user/time_series/" + stat +
|
||||
"?start=" + start.toISOString() +
|
||||
"&end=" + today.toISOString() +
|
||||
"&interval=" + interval
|
||||
).then(resp => {
|
||||
if (!resp.ok) { return Promise.reject("Error: " + resp.status); }
|
||||
return resp.json();
|
||||
}).then(resp => {
|
||||
resp.timestamps.forEach((val, idx) => {
|
||||
let date = new Date(val);
|
||||
let dateStr = ("00" + (date.getMonth() + 1)).slice(-2);
|
||||
dateStr += "-" + ("00" + date.getDate()).slice(-2);
|
||||
dateStr += " " + ("00" + date.getHours()).slice(-2);
|
||||
dateStr += ":" + ("00" + date.getMinutes()).slice(-2);
|
||||
resp.timestamps[idx] = " " + dateStr + " "; // Poor man's padding
|
||||
});
|
||||
graph.chart().data.labels = resp.timestamps;
|
||||
graph.chart().data.datasets[0].data = resp.amounts;
|
||||
graph.chart().update();
|
||||
|
||||
time_start = resp.timestamps[0];
|
||||
time_end = resp.timestamps.slice(-1)[0];
|
||||
|
||||
let total = resp.amounts.reduce((acc, cur) => { return acc + cur }, 0)
|
||||
|
||||
if (stat == "views") {
|
||||
total_views = total;
|
||||
} else if (stat == "downloads") {
|
||||
total_downloads = total;
|
||||
} else if (stat == "bandwidth") {
|
||||
total_bandwidth = total;
|
||||
} else if (stat == "direct_bandwidth") {
|
||||
total_direct_link = total;
|
||||
}
|
||||
}).catch(e => {
|
||||
console.error("Error requesting time series: " + e);
|
||||
})
|
||||
}
|
||||
|
||||
let graph_timeout = null
|
||||
let graph_timespan = 0
|
||||
let update_graphs = (minutes, interval, live) => {
|
||||
if (graph_timeout !== null) { clearTimeout(graph_timeout) }
|
||||
if (live) {
|
||||
graph_timeout = setTimeout(() => { update_graphs(minutes, interval, true) }, 10000)
|
||||
}
|
||||
|
||||
graph_timespan = minutes
|
||||
|
||||
load_graph(graph_view, "views", minutes, interval)
|
||||
load_graph(graph_download, "downloads", minutes, interval)
|
||||
load_graph(graph_bandwidth, "bandwidth", minutes, interval)
|
||||
load_graph(graph_direct_link, "direct_bandwidth", minutes, interval)
|
||||
load_direct_bw()
|
||||
}
|
||||
|
||||
let direct_link_bandwidth_used = 0
|
||||
let direct_link_percent = 0
|
||||
let storage_space_used = 0
|
||||
let storage_percent = 0
|
||||
let load_direct_bw = () => {
|
||||
let today = new Date()
|
||||
let start = new Date()
|
||||
start.setDate(start.getDate() - 30)
|
||||
|
||||
fetch(
|
||||
window.api_endpoint + "/user/time_series/direct_bandwidth" +
|
||||
"?start=" + start.toISOString() +
|
||||
"&end=" + today.toISOString() +
|
||||
"&interval=60"
|
||||
).then(resp => {
|
||||
if (!resp.ok) { return Promise.reject("Error: " + resp.status); }
|
||||
return resp.json();
|
||||
}).then(resp => {
|
||||
let total = resp.amounts.reduce((accum, val) => accum += val, 0);
|
||||
direct_link_bandwidth_used = total
|
||||
direct_link_percent = total / window.user.subscription.direct_linking_bandwidth
|
||||
storage_space_used = window.user.storage_space_used
|
||||
storage_percent = window.user.storage_space_used / window.user.subscription.storage_space
|
||||
}).catch(e => {
|
||||
console.error("Error requesting time series: " + e);
|
||||
})
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
update_graphs(1440, 1, true);
|
||||
})
|
||||
onDestroy(() => {
|
||||
if (graph_timeout !== null) {
|
||||
clearTimeout(graph_timeout)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="limit_width">
|
||||
<h2>Account information</h2>
|
||||
<ul>
|
||||
<li>Username: {window.user.username}</li>
|
||||
<li>E-mail address: {window.user.email}</li>
|
||||
<li>
|
||||
Supporter level: {window.user.subscription.name}
|
||||
{#if window.user.subscription.type === "patreon"}
|
||||
(<a href="https://www.patreon.com/join/pixeldrain/checkout?edit=1">Manage subscription</a>)
|
||||
{/if}
|
||||
<ul>
|
||||
<li>
|
||||
Advertisements when viewing files:
|
||||
{#if window.user.subscription.disable_ad_display}No{:else}Yes{/if}
|
||||
</li>
|
||||
<li>
|
||||
Advertisements on your uploaded files:
|
||||
{#if window.user.subscription.disable_ads_on_files}No{:else}Yes{/if}
|
||||
</li>
|
||||
{#if window.user.subscription.file_expiry_days > 0}
|
||||
<li>Files expire after {window.user.subscription.file_expiry_days} days</li>
|
||||
{:else}
|
||||
<li>Files never expire</li>
|
||||
{/if}
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
Storage:
|
||||
{formatDataVolume(storage_space_used, 3)}
|
||||
out of
|
||||
{formatDataVolume(window.user.subscription.storage_space, 3)}
|
||||
<br/>
|
||||
<div class="progress_bar_outer">
|
||||
<div id="storage_progress" class="progress_bar_inner" style="width: {storage_percent*100}%;"></div>
|
||||
</div>
|
||||
<br/>
|
||||
Direct link bandwidth:
|
||||
{formatDataVolume(direct_link_bandwidth_used, 3)}
|
||||
out of
|
||||
{formatDataVolume(window.user.subscription.direct_linking_bandwidth, 3)}
|
||||
(<a href="/#direct_linking">More information about direct linking</a>)
|
||||
<br/>
|
||||
<div class="progress_bar_outer">
|
||||
<div id="direct_bandwidth_progress" class="progress_bar_inner" style="width: {direct_link_percent*100}%;"></div>
|
||||
</div>
|
||||
|
||||
<h3>Exports</h3>
|
||||
<div style="text-align: center;">
|
||||
<a href="/user/export/files" class="button">
|
||||
<i class="icon">list</i>
|
||||
Export uploaded files to CSV
|
||||
</a>
|
||||
<a href="/user/export/lists" class="button">
|
||||
<i class="icon">list</i>
|
||||
Export created lists to CSV
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<h2>Statistics</h2>
|
||||
<p>
|
||||
Here you can see how often your files are viewed, downloaded
|
||||
and how much bandwidth they consume. The buttons at the top
|
||||
can be pressed to adjust the timeframe. If you choose 'Day'
|
||||
the statistics will be updated periodically. No need to
|
||||
refresh the page.
|
||||
</p>
|
||||
</div>
|
||||
<div class="highlight_dark">
|
||||
<button
|
||||
on:click={() => { update_graphs(1440, 1, true) }}
|
||||
class:button_highlight={graph_timespan == 1440}>
|
||||
Day (1m)
|
||||
</button>
|
||||
<button
|
||||
on:click={() => { update_graphs(10080, 10, false) }}
|
||||
class:button_highlight={graph_timespan == 10080}>
|
||||
Week (10m)
|
||||
</button>
|
||||
<button
|
||||
on:click={() => { update_graphs(20160, 60, false) }}
|
||||
class:button_highlight={graph_timespan == 20160}>
|
||||
Two Weeks (1h)
|
||||
</button>
|
||||
<button
|
||||
on:click={() => { update_graphs(43200, 1440, false) }}
|
||||
class:button_highlight={graph_timespan == 43200}>
|
||||
Month (1d)
|
||||
</button>
|
||||
<button
|
||||
on:click={() => { update_graphs(131400, 1440, false) }}
|
||||
class:button_highlight={graph_timespan == 131400}>
|
||||
Quarter (1d)
|
||||
</button>
|
||||
<button
|
||||
on:click={() => { update_graphs(525600, 1440, false) }}
|
||||
class:button_highlight={graph_timespan == 525600}>
|
||||
Year (1d)
|
||||
</button>
|
||||
<button
|
||||
on:click={() => { update_graphs(1051200, 1440, false) }}
|
||||
class:button_highlight={graph_timespan == 1051200}>
|
||||
Two Years (1d)
|
||||
</button>
|
||||
<br/>
|
||||
Total usage from {time_start} to {time_end}<br/>
|
||||
{formatThousands(total_views)} views,
|
||||
{formatThousands(total_downloads)} downloads,
|
||||
{formatDataVolume(total_bandwidth, 3)} bandwidth and
|
||||
{formatDataVolume(total_direct_link, 3)} direct link bandwidth
|
||||
</div>
|
||||
<div class="limit_width">
|
||||
<h3>Views</h3>
|
||||
<p>
|
||||
A view is counted when someone visits the download page of one
|
||||
of your files. Views are unique per user per file.
|
||||
</p>
|
||||
</div>
|
||||
<Chart bind:this={graph_view} dataType="number" label="Views" />
|
||||
<div class="limit_width">
|
||||
<h3>Downloads</h3>
|
||||
<p>
|
||||
Downloads are counted when a user clicks the download button
|
||||
on one of your files. It does not matter whether the
|
||||
download is completed or not, only the start of the download
|
||||
is counted.
|
||||
</p>
|
||||
</div>
|
||||
<Chart bind:this={graph_download} dataType="number" label="Downloads" />
|
||||
<div class="limit_width">
|
||||
<h3>Bandwidth</h3>
|
||||
<p>
|
||||
This is how much bandwidth your files are using in total.
|
||||
Bandwidth is used when a file is tranferred from a
|
||||
pixeldrain server to a user who is downloading the file.
|
||||
When a 5 MB file is downloaded 8 times it has used 40 MB of
|
||||
bandwidth.
|
||||
</p>
|
||||
</div>
|
||||
<Chart bind:this={graph_bandwidth} dataType="bytes" label="Bandwidth" />
|
||||
<div class="limit_width">
|
||||
<h3>Direct link bandwidth</h3>
|
||||
<p>
|
||||
When a file is downloaded without going through pixeldrain's
|
||||
download page it counts as a direct download. Because direct
|
||||
downloads cost us bandwidth and don't generate any ad
|
||||
revenue we have to limit them. When your direct link
|
||||
bandwidth runs out people will be asked to do a test before
|
||||
they can download your files. See our
|
||||
<a href="/#pro">subscription options</a> to get more direct
|
||||
linking bandwidth.
|
||||
</p>
|
||||
</div>
|
||||
<Chart bind:this={graph_direct_link} dataType="bytes" label="Direct link bandwidth" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.progress_bar_outer {
|
||||
background-color: var(--layer_1_color);
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
}
|
||||
.progress_bar_inner {
|
||||
background-color: var(--highlight_color);
|
||||
height: 100%;
|
||||
width: 0;
|
||||
transition: width 1s;
|
||||
}
|
||||
</style>
|
50
svelte/src/user_home/Router.svelte
Normal file
50
svelte/src/user_home/Router.svelte
Normal file
@@ -0,0 +1,50 @@
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import Home from "./Home.svelte";
|
||||
import AccountSettings from "./AccountSettings.svelte";
|
||||
|
||||
let page = ""
|
||||
|
||||
let navigate = (path, title) => {
|
||||
page = path
|
||||
window.document.title = title+" ~ pixeldrain"
|
||||
window.history.pushState(
|
||||
{}, window.document.title, "/user/"+path
|
||||
)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
let newpage = window.location.pathname.substring(window.location.pathname.lastIndexOf("/")+1)
|
||||
if (newpage === "user") {
|
||||
newpage = ""
|
||||
}
|
||||
page = newpage
|
||||
})
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<a class="button tab"
|
||||
href="/user"
|
||||
class:button_highlight={page === ""}
|
||||
on:click|preventDefault={() => {navigate("", "My home")}}>
|
||||
<i class="icon">home</i>
|
||||
My home
|
||||
</a>
|
||||
<a class="button tab"
|
||||
href="/user/settings"
|
||||
class:button_highlight={page === "settings"}
|
||||
on:click|preventDefault={() => {navigate("settings", "Account settings")}}>
|
||||
<i class="icon">settings</i>
|
||||
Account settings
|
||||
</a>
|
||||
<hr/>
|
||||
|
||||
{#if page === ""}
|
||||
<Home></Home>
|
||||
{:else if page === "settings"}
|
||||
<AccountSettings></AccountSettings>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
</style>
|
@@ -15,7 +15,7 @@ export const update = () => {
|
||||
return chart_object.update()
|
||||
}
|
||||
|
||||
Chart.defaults.global.defaultFontColor = "#b3b3b3";
|
||||
Chart.defaults.global.defaultFontColor = "#cccccc";
|
||||
Chart.defaults.global.defaultFontSize = 15;
|
||||
Chart.defaults.global.defaultFontFamily = "system-ui, sans-serif";
|
||||
Chart.defaults.global.maintainAspectRatio = false;
|
||||
|
229
svelte/src/util/Form.svelte
Normal file
229
svelte/src/util/Form.svelte
Normal file
@@ -0,0 +1,229 @@
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import Spinner from "./Spinner.svelte";
|
||||
|
||||
|
||||
export let config = {
|
||||
fields: [
|
||||
{
|
||||
name: "",
|
||||
label: "",
|
||||
type: "",
|
||||
default_value: "",
|
||||
binding: null,
|
||||
}
|
||||
],
|
||||
submit_label: "",
|
||||
submit_red: false,
|
||||
on_submit: async field_values => {},
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
config.fields.forEach(field => {
|
||||
if(field.default_value === undefined) {
|
||||
field.default_value = ""
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
let loading = false
|
||||
let submitted = false
|
||||
let submit_result = {
|
||||
success: false,
|
||||
message: "",
|
||||
messages: null,
|
||||
}
|
||||
|
||||
let submit = async (event) => {
|
||||
loading = true
|
||||
event.preventDefault()
|
||||
|
||||
let field_values = {}
|
||||
|
||||
config.fields.forEach(val => {
|
||||
field_values[val.name] = val.binding.value
|
||||
})
|
||||
|
||||
submit_result = await config.on_submit(field_values)
|
||||
if (submit_result.error_json) {
|
||||
submit_result = handle_errors(submit_result.error_json)
|
||||
}
|
||||
submitted = true
|
||||
|
||||
loading = false
|
||||
return false
|
||||
}
|
||||
let field_label = (field) => {
|
||||
let label = ""
|
||||
config.fields.forEach(val => {
|
||||
if (val.name === field) {
|
||||
label = val.label
|
||||
}
|
||||
})
|
||||
return label
|
||||
}
|
||||
let handle_errors = (response) => {
|
||||
console.log(response)
|
||||
let result = {success: false, message: "", messages: null}
|
||||
|
||||
if (response.value === "multiple_errors") {
|
||||
result.messages = []
|
||||
|
||||
response.errors.forEach(err => {
|
||||
if (err.value === "string_out_of_range") {
|
||||
result.messages.push(
|
||||
`${field_label(err.extra.field)} is too long or too short.
|
||||
It should be between ${err.extra.min_len} and
|
||||
${err.extra.max_len} characters. Current length:
|
||||
${err.extra.len}`
|
||||
)
|
||||
} else if (err.value === "field_contains_illegal_character") {
|
||||
result.messages.push(
|
||||
`Character '${err.extra.char}' is not allowed in ${field_label(err.extra.field)}`
|
||||
)
|
||||
} else if (err.value === "missing_field") {
|
||||
result.messages.push(
|
||||
`${field_label(err.extra.field)} is required`
|
||||
)
|
||||
} else {
|
||||
result.messages.push(err.message)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
result.message = response.message
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
</script>
|
||||
|
||||
<form method="POST" on:submit={submit}>
|
||||
{#if loading}
|
||||
<div class="spinner_container">
|
||||
<Spinner></Spinner>
|
||||
</div>
|
||||
{/if}
|
||||
{#if submitted}
|
||||
{#if submit_result.messages}
|
||||
<div id="submit_result" class:highlight_green={submit_result.success} class:highlight_red={!submit_result.success}>
|
||||
Something went wrong, please correct these errors before continuing:<br/>
|
||||
<ul>
|
||||
{#each submit_result.messages as message}
|
||||
<li>{message}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{:else}
|
||||
<div id="submit_result" class:highlight_green={submit_result.success} class:highlight_red={!submit_result.success}>
|
||||
{submit_result.message}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
|
||||
<table class="form">
|
||||
{#each config.fields as field}
|
||||
<tr class="form">
|
||||
<td>{field.label}</td>
|
||||
{#if field.type === "text"}
|
||||
<input bind:this={field.binding}
|
||||
id="input_{field.name}"
|
||||
name="{field.name}"
|
||||
value="{field.default_value}"
|
||||
type="text"
|
||||
class="form_input"/>
|
||||
{:else if field.type === "number"}
|
||||
<input bind:this={field.binding}
|
||||
id="input_{field.name}"
|
||||
name="{field.name}"
|
||||
value="{field.default_value}"
|
||||
type="number"
|
||||
class="form_input"/>
|
||||
{:else if field.type === "username"}
|
||||
<input bind:this={field.binding}
|
||||
id="input_{field.name}"
|
||||
name="{field.name}"
|
||||
value="{field.default_value}"
|
||||
type="text"
|
||||
autocomplete="username"
|
||||
class="form_input"/>
|
||||
{:else if field.type === "email"}
|
||||
<input bind:this={field.binding}
|
||||
id="input_{field.name}"
|
||||
name="{field.name}"
|
||||
value="{field.default_value}"
|
||||
type="email"
|
||||
autocomplete="email"
|
||||
class="form_input"/>
|
||||
{:else if field.type === "current_password"}
|
||||
<input bind:this={field.binding}
|
||||
id="input_{field.name}"
|
||||
name="{field.name}"
|
||||
value="{field.default_value}"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
class="form_input"/>
|
||||
{:else if field.type === "new_password"}
|
||||
<input bind:this={field.binding}
|
||||
id="input_{field.name}"
|
||||
name="{field.name}"
|
||||
value="{field.default_value}"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
class="form_input"/>
|
||||
{:else if field.type === "captcha"}
|
||||
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
|
||||
<div class="g-recaptcha" data-theme="dark" data-sitekey="{field.captcha_site_key}"></div>
|
||||
{:else if field.type === "radio"}
|
||||
{#each field.radio_values as val}
|
||||
<input
|
||||
id="input_{field.name}_choice_{val}"
|
||||
name="{field.name}"
|
||||
value="{val}"
|
||||
type="radio"
|
||||
checked={val === field.default_value}/>
|
||||
<label for="input_{field.name}_choice_{val}">{val}</label><br/>
|
||||
{/each}
|
||||
{:else if field.type === "description"}
|
||||
{field.default_value}
|
||||
{/if}
|
||||
</tr>
|
||||
{#if field.description}
|
||||
<tr class="form">
|
||||
<td colspan="2">
|
||||
{field.description}
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
{#if field.separator}
|
||||
<tr class="form">
|
||||
<td colspan="2">
|
||||
<hr/>
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
<!-- Submit button -->
|
||||
<tr class="form">
|
||||
<td colspan="2" style="text-align: right;">
|
||||
{#if config.submit_red}
|
||||
<button type="submit" class="button_red" style="float: right;">{@html config.submit_label}</button>
|
||||
{:else}
|
||||
<button type="submit" class="button_highlight" style="float: right;">{@html config.submit_label}</button>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
|
||||
<style>
|
||||
.spinner_container {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
height: 100px;
|
||||
width: 100px;
|
||||
z-index: 1000;
|
||||
}
|
||||
</style>
|
@@ -16,7 +16,7 @@ func (wc *WebController) serveAdClick(w http.ResponseWriter, r *http.Request, p
|
||||
|
||||
// The Real IP is used in the API server to determine that the view is not
|
||||
// fake
|
||||
var api = wc.api.RealIP(util.RemoteAddress(r))
|
||||
var api = wc.api.RealIP(util.RemoteAddress(r)).RealAgent(r.UserAgent())
|
||||
|
||||
// Log a view on the file
|
||||
if err := api.PostFileView(p.ByName("id"), wc.viewTokenOrBust()); err != nil {
|
||||
|
@@ -21,7 +21,7 @@ func (wc *WebController) serveFilePreview(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
|
||||
apiKey, _ := wc.getAPIKey(r)
|
||||
api := wc.api.Login(apiKey).RealIP(util.RemoteAddress(r))
|
||||
api := wc.api.Login(apiKey).RealIP(util.RemoteAddress(r)).RealAgent(r.UserAgent())
|
||||
|
||||
file, err := api.GetFileInfo(p.ByName("id")) // TODO: Error handling
|
||||
if err != nil {
|
||||
|
@@ -78,8 +78,8 @@ func (vd *viewerData) adType(files []pixelapi.ListFile) {
|
||||
adMavenFloat = 3
|
||||
|
||||
// Popunders
|
||||
clickAduPopup = 1
|
||||
propellerPopup = 2
|
||||
// clickAduPopup = 1
|
||||
// propellerPopup = 2
|
||||
)
|
||||
|
||||
// Intn returns a number up to n, but never n itself. So to get a random 0
|
||||
|
@@ -44,7 +44,7 @@ func (wc *WebController) serveShareXConfig(w http.ResponseWriter, r *http.Reques
|
||||
)),
|
||||
)))
|
||||
} else {
|
||||
w.Write([]byte(fmt.Sprintf(
|
||||
w.Write([]byte(
|
||||
`{
|
||||
"Version": "12.4.1",
|
||||
"DestinationType": "ImageUploader, TextUploader, FileUploader",
|
||||
@@ -56,6 +56,6 @@ func (wc *WebController) serveShareXConfig(w http.ResponseWriter, r *http.Reques
|
||||
"ThumbnailURL": "https://pixeldrain.com/api/file/$json:id$/thumbnail"
|
||||
}
|
||||
`,
|
||||
)))
|
||||
))
|
||||
}
|
||||
}
|
||||
|
@@ -52,7 +52,7 @@ func (wc *WebController) newTemplateData(w http.ResponseWriter, r *http.Request)
|
||||
APIEndpoint: template.URL(wc.apiURLExternal),
|
||||
|
||||
// Use the user's IP address for making requests
|
||||
PixelAPI: wc.api.RealIP(util.RemoteAddress(r)),
|
||||
PixelAPI: wc.api.RealIP(util.RemoteAddress(r)).RealAgent(r.UserAgent()),
|
||||
|
||||
Hostname: template.HTML(wc.hostname),
|
||||
URLQuery: r.URL.Query(),
|
||||
@@ -70,7 +70,7 @@ func (wc *WebController) newTemplateData(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
if err.Error() == "authentication_required" || err.Error() == "authentication_failed" {
|
||||
// Disable API authentication
|
||||
t.PixelAPI = wc.api.RealIP(util.RemoteAddress(r))
|
||||
t.PixelAPI = wc.api.RealIP(util.RemoteAddress(r)).RealAgent(r.UserAgent())
|
||||
|
||||
// Remove the authentication cookie
|
||||
log.Debug("Deleting invalid API key")
|
||||
@@ -163,6 +163,9 @@ func (tm *TemplateManager) ParseTemplates(silent bool) {
|
||||
// Parse static resources
|
||||
var file []byte
|
||||
if err = filepath.Walk(tm.resourceDir+"/include", func(path string, f os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("walk err: %w", err)
|
||||
}
|
||||
if f == nil || f.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
@@ -159,7 +159,8 @@ func New(
|
||||
{PST, "password_reset" /* */, wc.serveForm(wc.passwordResetForm, handlerOpts{NoEmbed: true})},
|
||||
{GET, "logout" /* */, wc.serveTemplate("logout", handlerOpts{Auth: true, NoEmbed: true})},
|
||||
{PST, "logout" /* */, wc.serveLogout},
|
||||
{GET, "user" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true})},
|
||||
{GET, "user_old" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true})},
|
||||
{GET, "user" /* */, wc.serveTemplate("user_home_svelte", handlerOpts{Auth: true})},
|
||||
{GET, "user/files" /* */, wc.serveTemplate("user_files", handlerOpts{Auth: true})},
|
||||
{GET, "user/lists" /* */, wc.serveTemplate("user_lists", handlerOpts{Auth: true})},
|
||||
{GET, "user/buckets" /* */, wc.serveTemplate("user_buckets", handlerOpts{Auth: true})},
|
||||
@@ -168,8 +169,9 @@ func New(
|
||||
{GET, "user/export/lists" /**/, wc.serveUserExportLists},
|
||||
|
||||
// User account settings
|
||||
{GET, "user/settings" /* */, wc.serveUserSettings},
|
||||
{PST, "user/settings" /* */, wc.serveUserSettings},
|
||||
{GET, "user/settings" /* */, wc.serveTemplate("user_home_svelte", handlerOpts{Auth: true})},
|
||||
{GET, "user/settings_old" /* */, wc.serveUserSettings},
|
||||
{PST, "user/settings_old" /* */, wc.serveUserSettings},
|
||||
{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})},
|
||||
|
Reference in New Issue
Block a user