Replace user home page with user dashboard

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,64 @@
<script>
import Euro from "../../util/Euro.svelte";
import { formatDataVolume } from "../../util/Formatting.svelte";
</script>
<ul>
<li>
Supporter level: {window.user.subscription.name}<br/>
<i class="icon">shopping_cart</i>
<a href="/user/subscription">Manage subscriptions</a><br/>
<i class="icon">add_link</i>
<a href="/api/patreon_auth/start">Link Patreon subscription</a>
</li>
{#if window.user.balance_micro_eur !== 0}
<li>
Current account balance: <Euro amount={window.user.balance_micro_eur}></Euro><br/>
<i class="icon">account_balance_wallet</i>
<a href="/user/prepaid/deposit">Deposit credit</a><br/>
<i class="icon">receipt</i>
<a href="/user/prepaid/transactions">Transaction log</a>
{#if window.user.balance_micro_eur > 0 && window.user.subscription.id === ""}
<br/>
You have account credit but no active subscription. Activate
a subscription on the <a href="/user/subscription">subscriptions page</a>
{/if}
</li>
{/if}
<li>
Max file size: {formatDataVolume(window.user.subscription.file_size_limit, 3)}
</li>
<li>
{#if window.user.subscription.storage_space > 0}
Storage limit: {formatDataVolume(window.user.subscription.storage_space, 3)}
{:else}
No storage limit
{/if}
</li>
<li>
{#if window.user.subscription.monthly_transfer_cap > 0}
Data transfer limit: {formatDataVolume(window.user.subscription.monthly_transfer_cap, 3)}
{:else}
No data transfer limit
{/if}
</li>
<li>
{#if window.user.subscription.file_expiry_days > 0}
Files expire after {window.user.subscription.file_expiry_days} days
{:else}
Files never expire
{/if}
</li>
</ul>
<style>
ul {
margin: 0;
}
</style>

View File

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

View File

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

View File

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

View File

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