Update to svelte 5

This commit is contained in:
2025-10-13 16:05:50 +02:00
parent f936e4c0f2
commit 6d89c5ddd9
129 changed files with 2443 additions and 2180 deletions

1712
svelte/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -18,7 +18,7 @@
"rollup": "^4.24.4", "rollup": "^4.24.4",
"rollup-plugin-livereload": "^2.0.5", "rollup-plugin-livereload": "^2.0.5",
"rollup-plugin-svelte": "^7.2.2", "rollup-plugin-svelte": "^7.2.2",
"svelte": "^4.2.19" "svelte": "^5.0.0"
}, },
"dependencies": { "dependencies": {
"behave-js": "^1.5.0", "behave-js": "^1.5.0",

View File

@@ -1,15 +1,14 @@
<script> <script lang="ts">
import { formatDate, formatNumber } from "util/Formatting"; import { formatDate, formatNumber } from "util/Formatting";
import Expandable from "util/Expandable.svelte"; import Expandable from "util/Expandable.svelte";
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
let dispatch = createEventDispatcher() let dispatch = createEventDispatcher()
export let report let { report, ip_report_count } = $props();
export let ip_report_count let preview = $state(false)
let preview = false
$: can_grant = report.status !== "granted" let can_grant = $derived(report.status !== "granted")
$: can_reject = report.status !== "rejected" let can_reject = $derived(report.status !== "rejected")
let set_status = async (action, report_type) => { let set_status = async (action, report_type) => {
dispatch("resolve_report", {action: action, report_type: report_type}) dispatch("resolve_report", {action: action, report_type: report_type})
@@ -17,57 +16,59 @@ let set_status = async (action, report_type) => {
</script> </script>
<Expandable expanded={report.status === "pending"} click_expand> <Expandable expanded={report.status === "pending"} click_expand>
<div slot="header" class="header"> {#snippet header()}
<div class="icon_cell"> <div class="header">
<img class="file_icon" src={"/api/file/"+report.file.id+"/thumbnail"} alt="File thumbnail"/> <div class="icon_cell">
</div> <img class="file_icon" src={"/api/file/"+report.file.id+"/thumbnail"} alt="File thumbnail"/>
<div class="title">
{report.file.name}
</div>
<div class="stats">
Type<br/>
{report.file.abuse_type === "" ? report.type : report.file.abuse_type}
</div>
{#if report.status !== "pending"}
<div class="stats">
Status<br/>
{report.status}
</div> </div>
{/if}
<div class="stats">R<br/>{report.reports.length}</div> <div class="title">
<div class="stats">V<br/>{formatNumber(report.file.views, 3)}</div> {report.file.name}
<div class="stats">DL<br/>{formatNumber(report.file.bandwidth_used / report.file.size, 3)}</div> </div>
</div> <div class="stats">
Type<br/>
{report.file.abuse_type === "" ? report.type : report.file.abuse_type}
</div>
{#if report.status !== "pending"}
<div class="stats">
Status<br/>
{report.status}
</div>
{/if}
<div class="stats">R<br/>{report.reports.length}</div>
<div class="stats">V<br/>{formatNumber(report.file.views, 3)}</div>
<div class="stats">DL<br/>{formatNumber(report.file.bandwidth_used / report.file.size, 3)}</div>
</div>
{/snippet}
<div class="details"> <div class="details">
<div class="toolbar"> <div class="toolbar">
<div class="action_list"> <div class="action_list">
<a class="button" target="_blank" href={"/u/"+report.file.id} rel="noreferrer"> <a class="button" target="_blank" href={"/u/"+report.file.id} rel="noreferrer">
<i class="icon">open_in_new</i> Open file <i class="icon">open_in_new</i> Open file
</a> </a>
<button class:button_highlight={preview} on:click={() => {preview = !preview}}> <button class:button_highlight={preview} onclick={() => {preview = !preview}}>
<i class="icon">visibility</i> Preview <i class="icon">visibility</i> Preview
</button> </button>
{#if can_grant} {#if can_grant}
<button class="button_highlight" on:click={() => {set_status("grant", report.type)}}> <button class="button_highlight" onclick={() => {set_status("grant", report.type)}}>
<i class="icon">done</i> Block ({report.type}) <i class="icon">done</i> Block ({report.type})
</button> </button>
{/if} {/if}
{#if can_reject} {#if can_reject}
<button class="button_red" on:click={() => {set_status("reject", "")}}> <button class="button_red" onclick={() => {set_status("reject", "")}}>
<i class="icon">delete</i> Ignore <i class="icon">delete</i> Ignore
</button> </button>
{/if} {/if}
</div> </div>
<div class="type_list"> <div class="type_list">
<button on:click={() => {set_status("grant", "copyright")}}>copyright</button> <button onclick={() => {set_status("grant", "copyright")}}>copyright</button>
<button on:click={() => {set_status("grant", "terrorism")}}>terrorism</button> <button onclick={() => {set_status("grant", "terrorism")}}>terrorism</button>
<button on:click={() => {set_status("grant", "gore")}}>gore</button> <button onclick={() => {set_status("grant", "gore")}}>gore</button>
<button on:click={() => {set_status("grant", "child_abuse")}}>child_abuse</button> <button onclick={() => {set_status("grant", "child_abuse")}}>child_abuse</button>
<button on:click={() => {set_status("grant", "zoophilia")}}>zoophilia</button> <button onclick={() => {set_status("grant", "zoophilia")}}>zoophilia</button>
<button on:click={() => {set_status("grant", "malware")}}>malware</button> <button onclick={() => {set_status("grant", "malware")}}>malware</button>
<button on:click={() => {set_status("grant", "doxing")}}>doxing</button> <button onclick={() => {set_status("grant", "doxing")}}>doxing</button>
<button on:click={() => {set_status("grant", "revenge_porn")}}>revenge_porn</button> <button onclick={() => {set_status("grant", "revenge_porn")}}>revenge_porn</button>
</div> </div>
</div> </div>
<div style="text-align: center;"> <div style="text-align: center;">
@@ -101,12 +102,12 @@ let set_status = async (action, report_type) => {
<td>{ip_report_count[user_report.ip_address]}</td> <td>{ip_report_count[user_report.ip_address]}</td>
<td> <td>
{#if can_grant} {#if can_grant}
<button on:click={() => dispatch("resolve_by_ip", {ip: user_report.ip_address, action: "grant"})}> <button onclick={() => dispatch("resolve_by_ip", {ip: user_report.ip_address, action: "grant"})}>
Accept all Accept all
</button> </button>
{/if} {/if}
{#if can_reject} {#if can_reject}
<button on:click={() => dispatch("resolve_by_ip", {ip: user_report.ip_address, action: "reject"})}> <button onclick={() => dispatch("resolve_by_ip", {ip: user_report.ip_address, action: "reject"})}>
Ignore all Ignore all
</button> </button>
{/if} {/if}

View File

@@ -4,12 +4,12 @@ import AbuseReport from "./AbuseReport.svelte";
import { loading_finish, loading_start } from "lib/Loading"; import { loading_finish, loading_start } from "lib/Loading";
let loading = true let loading = true
let reports = [] let reports = $state([])
let startPicker let startPicker = $state()
let endPicker let endPicker = $state()
let tab = "pending" let tab = $state("pending")
const get_reports = async () => { const get_reports = async () => {
loading_start() loading_start()
@@ -74,7 +74,7 @@ const get_reports = async () => {
} }
}; };
let ip_report_count = {} let ip_report_count = $state({})
const count_ip_reports = () => { const count_ip_reports = () => {
ip_report_count = {} ip_report_count = {}
reports.forEach(v => { reports.forEach(v => {
@@ -163,19 +163,19 @@ onMount(() => {
<div>Range:</div> <div>Range:</div>
<input type="date" bind:this={startPicker}/> <input type="date" bind:this={startPicker}/>
<input type="date" bind:this={endPicker}/> <input type="date" bind:this={endPicker}/>
<button on:click={get_reports}>Go</button> <button onclick={get_reports}>Go</button>
</div> </div>
<div class="tab_bar"> <div class="tab_bar">
<button on:click={() => {tab = "pending"; get_reports()}} class:button_highlight={tab === "pending"}> <button onclick={() => {tab = "pending"; get_reports()}} class:button_highlight={tab === "pending"}>
<i class="icon">flag</i> <i class="icon">flag</i>
Pending Pending
</button> </button>
<button on:click={() => {tab = "granted"; get_reports()}} class:button_highlight={tab === "granted"}> <button onclick={() => {tab = "granted"; get_reports()}} class:button_highlight={tab === "granted"}>
<i class="icon">flag</i> <i class="icon">flag</i>
Granted Granted
</button> </button>
<button on:click={() => {tab = "rejected"; get_reports()}} class:button_highlight={tab === "rejected"}> <button onclick={() => {tab = "rejected"; get_reports()}} class:button_highlight={tab === "rejected"}>
<i class="icon">flag</i> <i class="icon">flag</i>
Rejected Rejected
</button> </button>

View File

@@ -17,25 +17,25 @@ type Reporter = {
last_message_html: string, last_message_html: string,
} }
let reporters: Reporter[] = [] let reporters: Reporter[] = $state([])
$: reporters_pending = reporters.reduce((acc, val) => { let reporters_pending = $derived(reporters.reduce((acc, val) => {
if (val.status === "pending") { if (val.status === "pending") {
acc.push(val) acc.push(val)
} }
return acc return acc
}, []) }, []))
$: reporters_trusted = reporters.reduce((acc, val) => { let reporters_trusted = $derived(reporters.reduce((acc, val) => {
if (val.status === "trusted") { if (val.status === "trusted") {
acc.push(val) acc.push(val)
} }
return acc return acc
}, []) }, []))
$: reporters_rejected = reporters.reduce((acc, val) => { let reporters_rejected = $derived(reporters.reduce((acc, val) => {
if (val.status === "rejected") { if (val.status === "rejected") {
acc.push(val) acc.push(val)
} }
return acc return acc
}, []) }, []))
const get_reporters = async () => { const get_reporters = async () => {
loading_start() loading_start()
@@ -52,13 +52,14 @@ const get_reporters = async () => {
} }
}; };
let edit_button: HTMLButtonElement let edit_button: HTMLButtonElement = $state()
let creating = false let creating = $state(false)
let new_reporter_from_address: HTMLInputElement let new_reporter_from_address: HTMLInputElement = $state()
let new_reporter_name: HTMLInputElement let new_reporter_name: HTMLInputElement = $state()
let new_reporter_status = "trusted" let new_reporter_status = $state("trusted")
const create_reporter = async () => { const create_reporter = async (e: SubmitEvent) => {
e.preventDefault()
if (!new_reporter_from_address.value) { if (!new_reporter_from_address.value) {
alert("Please enter an e-mail address") alert("Please enter an e-mail address")
return return
@@ -139,16 +140,16 @@ onMount(get_reporters);
<section> <section>
<div class="toolbar" style="text-align: left;"> <div class="toolbar" style="text-align: left;">
<div class="toolbar_spacer"></div> <div class="toolbar_spacer"></div>
<button on:click={() => get_reporters()}> <button onclick={() => get_reporters()}>
<i class="icon">refresh</i> <i class="icon">refresh</i>
</button> </button>
<button bind:this={edit_button} class:button_highlight={creating} on:click={() => {creating = !creating}}> <button bind:this={edit_button} class:button_highlight={creating} onclick={() => {creating = !creating}}>
<i class="icon">create</i> Add abuse reporter <i class="icon">create</i> Add abuse reporter
</button> </button>
</div> </div>
{#if creating} {#if creating}
<div class="highlight_shaded"> <div class="highlight_shaded">
<form on:submit|preventDefault={create_reporter}> <form onsubmit={create_reporter}>
<div class="form"> <div class="form">
<label for="field_from_address">E-mail address</label> <label for="field_from_address">E-mail address</label>
<input id="field_from_address" type="text" bind:this={new_reporter_from_address}/> <input id="field_from_address" type="text" bind:this={new_reporter_from_address}/>

View File

@@ -1,4 +1,5 @@
<script> <script lang="ts">
import { run, preventDefault } from 'svelte/legacy';
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import { formatDate } from "util/Formatting"; import { formatDate } from "util/Formatting";
import Modal from "util/Modal.svelte" import Modal from "util/Modal.svelte"
@@ -7,13 +8,12 @@ import { flip } from "svelte/animate";
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
export let reporters = [] let { reporters = $bindable([]) } = $props();
$: update_table(reporters)
const update_table = (reporters) => sort("") const update_table = (reporters) => sort("")
let sort_field = "last_used" let sort_field = $state("last_used")
let asc = false let asc = $state(false)
const sort = (field) => { const sort = (field) => {
if (field !== "" && field === sort_field) asc = !asc if (field !== "" && field === sort_field) asc = !asc
if (field === "") field = sort_field if (field === "") field = sort_field
@@ -40,16 +40,19 @@ const sort = (field) => {
reporters = reporters reporters = reporters
} }
let modal let modal: Modal = $state()
let preview_subject = "" let preview_subject = $state("")
let preview_html = "" let preview_html = $state("")
let preview_text = "" let preview_text = $state("")
const toggle_preview = (rep) => { const toggle_preview = (rep) => {
preview_subject = rep.last_message_subject preview_subject = rep.last_message_subject
preview_text = rep.last_message_text preview_text = rep.last_message_text
preview_html = rep.last_message_html preview_html = rep.last_message_html
modal.show() modal.show()
} }
run(() => {
update_table(reporters)
});
</script> </script>
<div class="table_scroll"> <div class="table_scroll">
@@ -75,23 +78,23 @@ const toggle_preview = (rep) => {
<td>{formatDate(rep.last_used, true, true, false)}</td> <td>{formatDate(rep.last_used, true, true, false)}</td>
<td>{formatDate(rep.created, false, false, false)}</td> <td>{formatDate(rep.created, false, false, false)}</td>
<td> <td>
<button on:click|preventDefault={() => toggle_preview(rep)} class="button round"> <button onclick={preventDefault(() => toggle_preview(rep))} class="button round">
<i class="icon">email</i> <i class="icon">email</i>
</button> </button>
<button on:click|preventDefault={() => {dispatch("edit", rep)}} class="button round"> <button onclick={preventDefault(() => {dispatch("edit", rep)})} class="button round">
<i class="icon">edit</i> <i class="icon">edit</i>
</button> </button>
{#if rep.status !== "trusted"} {#if rep.status !== "trusted"}
<button on:click|preventDefault={() => {dispatch("approve", rep)}} class="button button_highlight round"> <button onclick={preventDefault(() => {dispatch("approve", rep)})} class="button button_highlight round">
<i class="icon">check</i> <i class="icon">check</i>
</button> </button>
{/if} {/if}
{#if rep.status !== "rejected"} {#if rep.status !== "rejected"}
<button on:click|preventDefault={() => {dispatch("spam", rep)}} class="button button_red round"> <button onclick={preventDefault(() => {dispatch("spam", rep)})} class="button button_red round">
<i class="icon">block</i> <i class="icon">block</i>
</button> </button>
{/if} {/if}
<button on:click|preventDefault={() => {dispatch("delete", rep)}} class="button button_red round"> <button onclick={preventDefault(() => {dispatch("delete", rep)})} class="button button_red round">
<i class="icon">delete</i> <i class="icon">delete</i>
</button> </button>
</td> </td>

View File

@@ -2,20 +2,20 @@
import { onDestroy, onMount } from "svelte"; import { onDestroy, onMount } from "svelte";
import { formatDataVolume, formatThousands, formatDate, formatNumber, formatDuration } from "util/Formatting"; import { formatDataVolume, formatThousands, formatDate, formatNumber, formatDuration } from "util/Formatting";
import Chart from "util/Chart.svelte"; import Chart from "util/Chart.svelte";
import { color_by_name } from "util/Util.svelte"; import { color_by_name } from "util/Util";
import ServerDiagnostics from "./ServerDiagnostics.svelte"; import ServerDiagnostics from "./ServerDiagnostics.svelte";
import PeerTable from "./PeerTable.svelte"; import PeerTable from "./PeerTable.svelte";
let graphViews let graphViews = $state()
let graphBandwidth let graphBandwidth = $state()
let graphTimeout = null let graphTimeout = null
let start_time = "" let start_time = $state("")
let end_time = "" let end_time = $state("")
let total_bandwidth = 0 let total_bandwidth = $state(0)
let total_bandwidth_paid = 0 let total_bandwidth_paid = $state(0)
let total_views = 0 let total_views = $state(0)
let total_downloads = 0 let total_downloads = $state(0)
const loadGraph = (minutes, interval, live) => { const loadGraph = (minutes, interval, live) => {
if (graphTimeout !== null) { clearTimeout(graphTimeout) } if (graphTimeout !== null) { clearTimeout(graphTimeout) }
if (live) { if (live) {
@@ -64,8 +64,8 @@ const loadGraph = (minutes, interval, live) => {
// Load performance statistics // Load performance statistics
let lastOrder; let lastOrder = $state();
let status = { let status = $state({
cpu_profile_running_since: "", cpu_profile_running_since: "",
db_latency: 0, db_latency: 0,
db_time: "", db_time: "",
@@ -93,9 +93,9 @@ let status = {
rate_limit_watcher_listeners: 0, rate_limit_watcher_listeners: 0,
download_clients: 0, download_clients: 0,
download_connections: 0, download_connections: 0,
} })
$: total_reads = status.local_reads + status.neighbour_reads + status.remote_reads let total_reads = $derived(status.local_reads + status.neighbour_reads + status.remote_reads)
$: total_read_size = status.local_read_size + status.neighbour_read_size + status.remote_read_size let total_read_size = $derived(status.local_read_size + status.neighbour_read_size + status.remote_read_size)
function getStats(order) { function getStats(order) {
lastOrder = order lastOrder = order
@@ -179,14 +179,14 @@ onDestroy(() => {
<h3>Bandwidth usage and file views</h3> <h3>Bandwidth usage and file views</h3>
</section> </section>
<div class="highlight_border" style="margin-bottom: 6px;"> <div class="highlight_border" style="margin-bottom: 6px;">
<button on:click={() => loadGraph(1440, 1, true)}>Day 1m</button> <button onclick={() => loadGraph(1440, 1, true)}>Day 1m</button>
<button on:click={() => loadGraph(10080, 10, true)}>Week 10m</button> <button onclick={() => loadGraph(10080, 10, true)}>Week 10m</button>
<button on:click={() => loadGraph(43200, 60, true)}>Month 1h</button> <button onclick={() => loadGraph(43200, 60, true)}>Month 1h</button>
<button on:click={() => loadGraph(131400, 1440, false)}>Quarter 1d</button> <button onclick={() => loadGraph(131400, 1440, false)}>Quarter 1d</button>
<button on:click={() => loadGraph(262800, 1440, false)}>Half-year 1d</button> <button onclick={() => loadGraph(262800, 1440, false)}>Half-year 1d</button>
<button on:click={() => loadGraph(525600, 1440, false)}>Year 1d</button> <button onclick={() => loadGraph(525600, 1440, false)}>Year 1d</button>
<button on:click={() => loadGraph(1051200, 1440, false)}>Two Years 1d</button> <button onclick={() => loadGraph(1051200, 1440, false)}>Two Years 1d</button>
<button on:click={() => loadGraph(2628000, 1440, false)}>Five Years 1d</button> <button onclick={() => loadGraph(2628000, 1440, false)}>Five Years 1d</button>
</div> </div>
<Chart bind:this={graphBandwidth} data_type="bytes" /> <Chart bind:this={graphBandwidth} data_type="bytes" />
<Chart bind:this={graphViews} data_type="number" /> <Chart bind:this={graphViews} data_type="number" />
@@ -306,22 +306,22 @@ onDestroy(() => {
<thead> <thead>
<tr> <tr>
<td> <td>
<button on:click={() => { getStats('query_name') }}> <button onclick={() => { getStats('query_name') }}>
Query Query
</button> </button>
</td> </td>
<td> <td>
<button style="cursor: pointer;" on:click={() => { getStats('calls') }}> <button style="cursor: pointer;" onclick={() => { getStats('calls') }}>
Calls Calls
</button> </button>
</td> </td>
<td> <td>
<button style="cursor: pointer;" on:click={() => { getStats('average_duration') }}> <button style="cursor: pointer;" onclick={() => { getStats('average_duration') }}>
Avg Avg
</button> </button>
</td> </td>
<td> <td>
<button style="cursor: pointer;" on:click={() => { getStats('total_duration') }}> <button style="cursor: pointer;" onclick={() => { getStats('total_duration') }}>
Total Total
</button> </button>
</td> </td>

View File

@@ -1,4 +1,5 @@
<script> <script>
import { preventDefault, stopPropagation } from 'svelte/legacy';
import { onMount } from "svelte"; import { onMount } from "svelte";
import { formatDate } from "util/Formatting"; import { formatDate } from "util/Formatting";
import Expandable from "util/Expandable.svelte"; import Expandable from "util/Expandable.svelte";
@@ -15,13 +16,13 @@ const abuse_types = [
"revenge_porn", "revenge_porn",
] ]
let rows = [] let rows = $state([])
let total_offences = 0 let total_offences = $state(0)
let expanded = false let expanded = $state(false)
let creating = false let creating = $state(false)
let new_ban_address let new_ban_address = $state()
let new_ban_reason = abuse_types[0] let new_ban_reason = $state(abuse_types[0])
const get_bans = async () => { const get_bans = async () => {
loading_start() loading_start()
@@ -103,20 +104,20 @@ onMount(get_bans);
Offences {total_offences} Offences {total_offences}
</div> </div>
<div class="toolbar_spacer"></div> <div class="toolbar_spacer"></div>
<button class:button_highlight={expanded} on:click={() => {expanded = !expanded}}> <button class:button_highlight={expanded} onclick={() => {expanded = !expanded}}>
{#if expanded} {#if expanded}
<i class="icon">unfold_less</i> Collapse all <i class="icon">unfold_less</i> Collapse all
{:else} {:else}
<i class="icon">unfold_more</i> Expand all <i class="icon">unfold_more</i> Expand all
{/if} {/if}
</button> </button>
<button class:button_highlight={creating} on:click={() => {creating = !creating}}> <button class:button_highlight={creating} onclick={() => {creating = !creating}}>
<i class="icon">create</i> Add IP ban <i class="icon">create</i> Add IP ban
</button> </button>
</div> </div>
{#if creating} {#if creating}
<div class="highlight_shaded"> <div class="highlight_shaded">
<form on:submit|preventDefault={create_ban}> <form onsubmit={preventDefault(create_ban)}>
<div class="form"> <div class="form">
<label for="field_address">IP address</label> <label for="field_address">IP address</label>
<input id="field_address" type="text" bind:this={new_ban_address}/> <input id="field_address" type="text" bind:this={new_ban_address}/>
@@ -138,20 +139,22 @@ onMount(get_bans);
{#each rows as row (row.address)} {#each rows as row (row.address)}
<Expandable expanded={expanded} click_expand> <Expandable expanded={expanded} click_expand>
<div slot="header" class="header"> {#snippet header()}
<div class="title">{row.address}</div> <div class="header">
<div class="stats"> <div class="title">{row.address}</div>
Offences<br/> <div class="stats">
{row.offences.length} Offences<br/>
{row.offences.length}
</div>
<div class="stats">
Date<br/>
{formatDate(row.offences[0].ban_time, false, false, false)}
</div>
<button onclick={stopPropagation(() => {delete_ban(row.address)})} class="button button_red" style="align-self: center;">
<i class="icon">delete</i>
</button>
</div> </div>
<div class="stats"> {/snippet}
Date<br/>
{formatDate(row.offences[0].ban_time, false, false, false)}
</div>
<button on:click|stopPropagation={() => {delete_ban(row.address)}} class="button button_red" style="align-self: center;">
<i class="icon">delete</i>
</button>
</div>
<div class="table_scroll"> <div class="table_scroll">
<table> <table>
<thead> <thead>

View File

@@ -7,10 +7,10 @@ import { country_name, get_admin_invoices, type Invoice } from "lib/AdminAPI";
import PayPalVat from "./PayPalVAT.svelte"; import PayPalVat from "./PayPalVAT.svelte";
import { loading_finish, loading_start } from "lib/Loading"; import { loading_finish, loading_start } from "lib/Loading";
let invoices: Invoice[] = [] let invoices: Invoice[] = $state([])
let year = 0 let year = $state(0)
let month = 0 let month = $state(0)
type Total = { type Total = {
count: number count: number
@@ -18,8 +18,8 @@ type Total = {
vat: number vat: number
fee: number fee: number
} }
let totals_provider: { [id: string]: Total } = {} let totals_provider: { [id: string]: Total } = $state({})
let totals_country: { [id: string]: Total } = {} let totals_country: { [id: string]: Total } = $state({})
const add_total = (i: Invoice) => { const add_total = (i: Invoice) => {
if (totals_provider[i.payment_method] === undefined) { if (totals_provider[i.payment_method] === undefined) {
totals_provider[i.payment_method] = {count: 0, amount: 0, vat: 0, fee: 0} totals_provider[i.payment_method] = {count: 0, amount: 0, vat: 0, fee: 0}
@@ -119,14 +119,14 @@ onMount(() => {
get_invoices() get_invoices()
}) })
let status_filter = { let status_filter = $state({
canceled: {checked: false}, canceled: {checked: false},
expired: {checked: false}, expired: {checked: false},
open: {checked: false}, open: {checked: false},
paid: {checked: true}, paid: {checked: true},
} })
let gateway_filter = {} let gateway_filter = $state({})
let method_filter = {} let method_filter = $state({})
const filter_invoices = () => { const filter_invoices = () => {
records_hidden = 0 records_hidden = 0
@@ -153,26 +153,28 @@ const filter_invoices = () => {
return false return false
}) })
} }
let records_hidden = 0 let records_hidden = $state(0)
let invoices_filtered: Invoice[] = [] let invoices_filtered: Invoice[] = $state([])
</script> </script>
<section> <section>
<h3>{year + "-" + ("00"+(month)).slice(-2)}</h3> <h3>{year + "-" + ("00"+(month)).slice(-2)}</h3>
<div class="toolbar"> <div class="toolbar">
<button on:click={last_month}> <button onclick={last_month}>
<i class="icon">chevron_left</i> <i class="icon">chevron_left</i>
Previous month Previous month
</button> </button>
<div class="toolbar_spacer"></div> <div class="toolbar_spacer"></div>
<button on:click={next_month}> <button onclick={next_month}>
Next month Next month
<i class="icon">chevron_right</i> <i class="icon">chevron_right</i>
</button> </button>
</div> </div>
<Expandable click_expand> <Expandable click_expand>
<div slot="header" class="header">Per payment processor</div> {#snippet header()}
<div class="header">Per payment processor</div>
{/snippet}
<SortableTable <SortableTable
index_field="id" index_field="id"
rows={obj_to_list(totals_provider)} rows={obj_to_list(totals_provider)}
@@ -188,7 +190,9 @@ let invoices_filtered: Invoice[] = []
</Expandable> </Expandable>
<Expandable click_expand> <Expandable click_expand>
<div slot="header" class="header">Per country</div> {#snippet header()}
<div class="header">Per country</div>
{/snippet}
<SortableTable <SortableTable
index_field="id" index_field="id"
rows={obj_to_list(totals_country)} rows={obj_to_list(totals_country)}
@@ -204,7 +208,9 @@ let invoices_filtered: Invoice[] = []
</Expandable> </Expandable>
<Expandable click_expand> <Expandable click_expand>
<div slot="header" class="header">In European Union</div> {#snippet header()}
<div class="header">In European Union</div>
{/snippet}
<SortableTable <SortableTable
index_field="id" index_field="id"
rows={obj_to_list_eu(totals_country)} rows={obj_to_list_eu(totals_country)}
@@ -220,7 +226,9 @@ let invoices_filtered: Invoice[] = []
</Expandable> </Expandable>
<Expandable click_expand> <Expandable click_expand>
<div slot="header" class="header">PayPal VAT</div> {#snippet header()}
<div class="header">PayPal VAT</div>
{/snippet}
<PayPalVat invoices={invoices}/> <PayPalVat invoices={invoices}/>
</Expandable> </Expandable>
@@ -233,7 +241,7 @@ let invoices_filtered: Invoice[] = []
type="checkbox" type="checkbox"
id="status_{filter}" id="status_{filter}"
bind:checked={status_filter[filter].checked} bind:checked={status_filter[filter].checked}
on:change={filter_invoices}> onchange={filter_invoices}>
<label for="status_{filter}">{filter}</label> <label for="status_{filter}">{filter}</label>
<br/> <br/>
{/each} {/each}
@@ -245,7 +253,7 @@ let invoices_filtered: Invoice[] = []
type="checkbox" type="checkbox"
id="gateway_{filter}" id="gateway_{filter}"
bind:checked={gateway_filter[filter].checked} bind:checked={gateway_filter[filter].checked}
on:change={filter_invoices}> onchange={filter_invoices}>
<label for="gateway_{filter}">{filter}</label> <label for="gateway_{filter}">{filter}</label>
<br/> <br/>
{/each} {/each}
@@ -257,7 +265,7 @@ let invoices_filtered: Invoice[] = []
type="checkbox" type="checkbox"
id="method_{filter}" id="method_{filter}"
bind:checked={method_filter[filter].checked} bind:checked={method_filter[filter].checked}
on:change={filter_invoices}> onchange={filter_invoices}>
<label for="method_{filter}">{filter}</label> <label for="method_{filter}">{filter}</label>
<br/> <br/>
{/each} {/each}

View File

@@ -1,19 +1,19 @@
<script> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
import { formatDate } from "util/Formatting"; import { formatDate } from "util/Formatting";
import { mollie_proxy_call } from "./MollieAPI"; import { mollie_proxy_call } from "./MollieAPI";
import Euro from "util/Euro.svelte"; import Euro from "util/Euro.svelte";
import { loading_start } from "lib/Loading"; import { loading_finish, loading_start } from "lib/Loading";
export let settlement = {} let { settlement = {} } = $props();
let payments = [] let payments = $state([])
let per_country = {} let per_country = $state({})
let totals = { let totals = $state({
count: 0, count: 0,
vat: 0, vat: 0,
amount: 0, amount: 0,
} })
const load_all_payments = async (settlement_id) => { const load_all_payments = async (settlement_id) => {
let payments = [] let payments = []

View File

@@ -8,7 +8,7 @@ import { mollie_proxy_call } from "./MollieAPI";
import { loading_finish, loading_start } from "lib/Loading"; import { loading_finish, loading_start } from "lib/Loading";
let response = {} let response = {}
let settlements = [] let settlements = $state([])
const get_settlements = async () => { const get_settlements = async () => {
loading_start() loading_start()
@@ -32,21 +32,23 @@ onMount(get_settlements);
<section> <section>
{#each settlements as row (row.id)} {#each settlements as row (row.id)}
<Expandable click_expand> <Expandable click_expand>
<div slot="header" class="header"> {#snippet header()}
<div class="title">{row.id}</div> <div class="header">
<div class="stats"> <div class="title">{row.id}</div>
Date<br/> <div class="stats">
{formatDate(row.createdAt, false, false, false)} Date<br/>
{formatDate(row.createdAt, false, false, false)}
</div>
<div class="stats">
Amount<br/>
<Euro amount={row.amount.value*1e6}/>
</div>
<div class="stats">
Status<br/>
{row.status}
</div>
</div> </div>
<div class="stats"> {/snippet}
Amount<br/>
<Euro amount={row.amount.value*1e6}/>
</div>
<div class="stats">
Status<br/>
{row.status}
</div>
</div>
<MollieSettlement settlement={row}/> <MollieSettlement settlement={row}/>
</Expandable> </Expandable>

View File

@@ -6,15 +6,15 @@ import Euro from "util/Euro.svelte";
import { loading_finish, loading_start } from "lib/Loading"; import { loading_finish, loading_start } from "lib/Loading";
let response = {} let response = {}
let payments = [] let payments = $state([])
let per_country = {} let per_country = $state({})
let totals = {} let totals = $state({})
let datePicker let datePicker = $state()
let rangeMonths = 1 let rangeMonths = $state(1)
let startDate = 0 let startDate = $state(0)
let endDate = 0 let endDate = $state(0)
const get_payments = async () => { const get_payments = async () => {
if (!datePicker.valueAsDate) { if (!datePicker.valueAsDate) {
@@ -125,7 +125,7 @@ onMount(() => {
<input type="date" bind:this={datePicker}/> <input type="date" bind:this={datePicker}/>
<div>Months</div> <div>Months</div>
<input type="number" bind:value={rangeMonths}/> <input type="number" bind:value={rangeMonths}/>
<button on:click={get_payments}>Go</button> <button onclick={get_payments}>Go</button>
</div> </div>
<div> <div>
@@ -227,29 +227,31 @@ onMount(() => {
<h2>Payments</h2> <h2>Payments</h2>
{#each payments as row (row.id)} {#each payments as row (row.id)}
<Expandable click_expand> <Expandable click_expand>
<div slot="header" class="header"> {#snippet header()}
<div class="title">{row.id}</div> <div class="header">
<div class="stats"> <div class="title">{row.id}</div>
Date<br/> <div class="stats">
{formatDate(row.createdAt, false, false, false)} Date<br/>
{formatDate(row.createdAt, false, false, false)}
</div>
<div class="stats">
Total<br/>
<Euro amount={row.amount.value*1e6}/>
</div>
<div class="stats">
Amount<br/>
<Euro amount={row.metadata.amount}/>
</div>
<div class="stats">
VAT<br/>
<Euro amount={row.metadata.vat}/>
</div>
<div class="stats">
Status<br/>
{row.status}
</div>
</div> </div>
<div class="stats"> {/snippet}
Total<br/>
<Euro amount={row.amount.value*1e6}/>
</div>
<div class="stats">
Amount<br/>
<Euro amount={row.metadata.amount}/>
</div>
<div class="stats">
VAT<br/>
<Euro amount={row.metadata.vat}/>
</div>
<div class="stats">
Status<br/>
{row.status}
</div>
</div>
<div> <div>
Amount: <Euro amount={row.metadata.amount} /><br/> Amount: <Euro amount={row.metadata.amount} /><br/>
VAT: <Euro amount={row.metadata.vat} /><br/> VAT: <Euro amount={row.metadata.vat} /><br/>

View File

@@ -78,18 +78,20 @@ const update_countries = (invoices: Invoice[]) => {
{#if per_country["NL"] && totals} {#if per_country["NL"] && totals}
<h2>Summary</h2> <h2>Summary</h2>
<table style="width: auto;"> <table style="width: auto;">
<tr> <tbody>
<td>Total PayPal earnings -fees</td> <tr>
<td><Euro amount={totals.vat+totals.amount-totals.fee}/></td> <td>Total PayPal earnings -fees</td>
</tr> <td><Euro amount={totals.vat+totals.amount-totals.fee}/></td>
<tr> </tr>
<td>Total VAT NL</td> <tr>
<td><Euro amount={per_country["NL"].vat}/></td> <td>Total VAT NL</td>
</tr> <td><Euro amount={per_country["NL"].vat}/></td>
<tr> </tr>
<td>Total VAT OSS</td> <tr>
<td><Euro amount={totals.vat-per_country["NL"].vat}/></td> <td>Total VAT OSS</td>
</tr> <td><Euro amount={totals.vat-per_country["NL"].vat}/></td>
</tr>
</tbody>
</table> </table>
<h2>Accounting information</h2> <h2>Accounting information</h2>

View File

@@ -1,25 +1,22 @@
<script> <script lang="ts">
import { run } from 'svelte/legacy';
import { flip } from "svelte/animate"; import { flip } from "svelte/animate";
import { formatDataVolume } from "util/Formatting"; import { formatDataVolume } from "util/Formatting";
import SortButton from "layout/SortButton.svelte"; import SortButton from "layout/SortButton.svelte";
export let peers = []; let { peers = $bindable([]) } = $props();
$: update_peers(peers)
let update_peers = (peers) => { let update_peers = (peers) => {
for (let peer of peers) { for (let peer of peers) {
peer.avg_network_total = peer.avg_network_tx + peer.avg_network_rx peer.avg_network_total = peer.avg_network_tx + peer.avg_network_rx
peer.usage_percent = (peer.avg_network_tx / peer.port_speed) * 100 peer.usage_percent = (peer.avg_network_tx / peer.port_speed) * 100
peer.network_ratio = Math.max(peer.avg_network_tx, peer.avg_network_rx) / Math.min(peer.avg_network_tx, peer.avg_network_rx) peer.network_ratio = Math.max(peer.avg_network_tx, peer.avg_network_rx) / Math.min(peer.avg_network_tx, peer.avg_network_rx)
if (peer.network_ratio === NaN) {
peer.network_ratio = 1
}
} }
sort("") sort("")
} }
let sort_field = "hostname" let sort_field = $state("hostname")
let asc = true let asc = $state(true)
let sort = (field) => { let sort = (field) => {
if (field !== "" && field === sort_field) { if (field !== "" && field === sort_field) {
asc = !asc asc = !asc
@@ -49,6 +46,9 @@ let sort = (field) => {
}) })
peers = peers peers = peers
} }
run(() => {
update_peers(peers)
});
</script> </script>
<div class="table_scroll"> <div class="table_scroll">

View File

@@ -1,35 +1,40 @@
<script> <script lang="ts">
import { get_endpoint } from "lib/PixeldrainAPI";
import { createEventDispatcher, onMount } from "svelte"; import { createEventDispatcher, onMount } from "svelte";
import { formatDuration } from "util/Formatting"; import { formatDuration } from "util/Formatting";
let dispatch = createEventDispatcher() let dispatch = createEventDispatcher()
export let running_since = "" interface Props {
running_since?: string;
}
$: profile_running = running_since != "0001-01-01T00:00:00Z" && running_since != "" let { running_since = "" }: Props = $props();
let profile_running = $derived(running_since != "0001-01-01T00:00:00Z" && running_since != "")
const start = async () => { const start = async () => {
if (!profile_running) { if (!profile_running) {
const resp = await fetch( const resp = await fetch(
window.api_endpoint+"/admin/cpu_profile", get_endpoint()+"/admin/cpu_profile",
{ method: "POST" } { method: "POST" }
); );
if(resp.status >= 400) { if(resp.status >= 400) {
throw new Error(await resp.text()); throw new Error(await resp.text());
} }
} else { } else {
window.open(window.api_endpoint+"/admin/cpu_profile") window.open(get_endpoint()+"/admin/cpu_profile")
} }
dispatch("refresh") dispatch("refresh")
} }
let interval let interval: number
let running_time = "0s" let running_time = $state("0s")
onMount(() => { onMount(() => {
interval = setInterval(() => { interval = setInterval(() => {
if (profile_running) { if (profile_running) {
running_time = formatDuration( running_time = formatDuration(
(new Date()).getTime() - Date.parse(running_since), (new Date()).getTime() - Date.parse(running_since), 3
) )
} }
}, 1000) }, 1000)
@@ -43,7 +48,7 @@ onMount(() => {
<a class="button" href="/api/admin/call_stack">Call stack</a> <a class="button" href="/api/admin/call_stack">Call stack</a>
<a class="button" href="/api/admin/heap_profile">Heap profile</a> <a class="button" href="/api/admin/heap_profile">Heap profile</a>
<button on:click={start} class:button_red={profile_running}> <button onclick={start} class:button_red={profile_running}>
{#if profile_running} {#if profile_running}
Stop CPU profiling (running for {running_time}) Stop CPU profiling (running for {running_time})
{:else} {:else}

View File

@@ -1,8 +1,8 @@
<script> <script lang="ts">
import Euro from "util/Euro.svelte"; import Euro from "util/Euro.svelte";
import { formatDataVolume, formatDate } from "util/Formatting"; import { formatDataVolume, formatDate } from "util/Formatting";
export let row = {} let { row = {} } = $props();
</script> </script>
<table> <table>

View File

@@ -1,4 +1,5 @@
<script> <script>
import { stopPropagation } from 'svelte/legacy';
import { onMount } from "svelte"; import { onMount } from "svelte";
import { formatDate } from "util/Formatting"; import { formatDate } from "util/Formatting";
import Expandable from "util/Expandable.svelte"; import Expandable from "util/Expandable.svelte";
@@ -8,9 +9,9 @@ import BanDetails from "./BanDetails.svelte";
import UserLists from "./UserLists.svelte"; import UserLists from "./UserLists.svelte";
import { loading_finish, loading_start } from "lib/Loading"; import { loading_finish, loading_start } from "lib/Loading";
let rows = [] let rows = $state([])
let total_offences = 0 let total_offences = $state(0)
let expanded = false let expanded = $state(false)
const get_bans = async () => { const get_bans = async () => {
loading_start() loading_start()
@@ -113,7 +114,7 @@ onMount(get_bans);
Offences {total_offences} Offences {total_offences}
</div> </div>
<div class="toolbar_spacer"></div> <div class="toolbar_spacer"></div>
<button class:button_highlight={expanded} on:click={() => {expanded = !expanded}}> <button class:button_highlight={expanded} onclick={() => {expanded = !expanded}}>
{#if expanded} {#if expanded}
<i class="icon">unfold_less</i> Collapse all <i class="icon">unfold_less</i> Collapse all
{:else} {:else}
@@ -124,26 +125,28 @@ onMount(get_bans);
{#each rows as row (row.user_id)} {#each rows as row (row.user_id)}
<Expandable expanded={expanded} click_expand> <Expandable expanded={expanded} click_expand>
<div slot="header" class="header"> {#snippet header()}
<div class="title"> <div class="header">
{row.user.username} <div class="title">
{row.user.username}
</div>
<div class="stats">
Type<br/>
{row.offences[0].reason}
</div>
<div class="stats">
Count<br/>
{row.offences.length}
</div>
<div class="stats">
Date<br/>
{formatDate(row.offences[0].ban_time, false, false, false)}
</div>
<button onclick={stopPropagation(() => {delete_ban(row.user_id)})} class="button button_red" style="align-self: center;">
<i class="icon">delete</i>
</button>
</div> </div>
<div class="stats"> {/snippet}
Type<br/>
{row.offences[0].reason}
</div>
<div class="stats">
Count<br/>
{row.offences.length}
</div>
<div class="stats">
Date<br/>
{formatDate(row.offences[0].ban_time, false, false, false)}
</div>
<button on:click|stopPropagation={() => {delete_ban(row.user_id)}} class="button button_red" style="align-self: center;">
<i class="icon">delete</i>
</button>
</div>
<div class="toolbar"> <div class="toolbar">
<Button click={() => impersonate(row.user_id)} icon="login" label="Impersonate user"/> <Button click={() => impersonate(row.user_id)} icon="login" label="Impersonate user"/>

View File

@@ -1,11 +1,16 @@
<script> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
import { formatDataVolume, formatDate } from "util/Formatting"; import { formatDataVolume, formatDate } from "util/Formatting";
import SortButton from "layout/SortButton.svelte"; import SortButton from "layout/SortButton.svelte";
import { loading_finish, loading_start } from "lib/Loading"; import { loading_finish, loading_start } from "lib/Loading";
import { get_endpoint } from "lib/PixeldrainAPI";
export let user_id = "" interface Props {
let files = [] user_id?: string;
}
let { user_id = "" }: Props = $props();
let files = $state([])
onMount(() => reload()) onMount(() => reload())
@@ -13,7 +18,7 @@ export const reload = async () => {
loading_start() loading_start()
try { try {
const req = await fetch( const req = await fetch(
window.api_endpoint+"/user/files", get_endpoint()+"/user/files",
{ {
headers: { headers: {
"Admin-User-Override": user_id, "Admin-User-Override": user_id,
@@ -34,8 +39,8 @@ export const reload = async () => {
} }
} }
let sort_field = "date_upload" let sort_field = $state("date_upload")
let asc = false let asc = $state(false)
const sort = (field) => { const sort = (field) => {
if (field !== "" && field === sort_field) { if (field !== "" && field === sort_field) {
asc = !asc asc = !asc
@@ -84,7 +89,7 @@ const sort = (field) => {
{#each files as file (file.id)} {#each files as file (file.id)}
<tr> <tr>
<td style="padding: 0; line-height: 1em;"> <td style="padding: 0; line-height: 1em;">
<img src="{window.api_endpoint+file.thumbnail_href}?height=48&width=48" alt="icon" class="thumbnail" /> <img src="{get_endpoint()+file.thumbnail_href}?height=48&width=48" alt="icon" class="thumbnail" />
</td> </td>
<td> <td>
<a href="/u/{file.id}" target="_blank">{file.name}</a> <a href="/u/{file.id}" target="_blank">{file.name}</a>

View File

@@ -1,10 +1,15 @@
<script> <script lang="ts">
import { loading_finish, loading_start } from "lib/Loading"; import { loading_finish, loading_start } from "lib/Loading";
import { get_endpoint } from "lib/PixeldrainAPI";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { formatDate } from "util/Formatting"; import { formatDate } from "util/Formatting";
export let user_id = "" interface Props {
let lists = [] user_id?: string;
}
let { user_id = "" }: Props = $props();
let lists = $state([])
onMount(() => reload()) onMount(() => reload())
@@ -12,7 +17,7 @@ export const reload = async () => {
loading_start() loading_start()
try { try {
const req = await fetch( const req = await fetch(
window.api_endpoint+"/user/lists", get_endpoint()+"/user/lists",
{ {
headers: { headers: {
"Admin-User-Override": user_id, "Admin-User-Override": user_id,
@@ -47,7 +52,7 @@ export const reload = async () => {
{#each lists as list (list.id)} {#each lists as list (list.id)}
<tr> <tr>
<td style="padding: 0; line-height: 1em;"> <td style="padding: 0; line-height: 1em;">
<img src="{window.api_endpoint}/list/{list.id}/thumbnail?height=48&width=48" alt="icon" class="thumbnail" /> <img src="{get_endpoint()}/list/{list.id}/thumbnail?height=48&width=48" alt="icon" class="thumbnail" />
</td> </td>
<td> <td>
<a href="/l/{list.id}" target="_blank">{list.title}</a> <a href="/l/{list.id}" target="_blank">{list.title}</a>

View File

@@ -1,8 +1,11 @@
<script lang="ts"> <script lang="ts">
import { preventDefault } from 'svelte/legacy';
import { fs_encode_path, node_is_shared } from "lib/FilesystemAPI"; import { fs_encode_path, node_is_shared } from "lib/FilesystemAPI";
import type { FSNavigator } from "./FSNavigator"; import type { FSNavigator } from "./FSNavigator";
export let nav: FSNavigator let { nav }: {
nav: FSNavigator;
} = $props();
</script> </script>
<div class="breadcrumbs"> <div class="breadcrumbs">
@@ -10,7 +13,7 @@ export let nav: FSNavigator
<a <a
href={"/d"+fs_encode_path(node.path)} href={"/d"+fs_encode_path(node.path)}
class="breadcrumb button flat" class="breadcrumb button flat"
on:click|preventDefault={() => {nav.navigate(node.path, true)}} onclick={preventDefault(() => {nav.navigate(node.path, true)})}
> >
{#if node.abuse_type !== undefined} {#if node.abuse_type !== undefined}
<i class="icon small">block</i> <i class="icon small">block</i>

View File

@@ -1,31 +1,34 @@
<script lang="ts"> <script lang="ts">
import { run } from 'svelte/legacy';
import Chart from "util/Chart.svelte"; import Chart from "util/Chart.svelte";
import { formatDataVolume, formatDate, formatThousands } from "util/Formatting"; import { formatDataVolume, formatDate, formatThousands } from "util/Formatting";
import Modal from "util/Modal.svelte"; import Modal from "util/Modal.svelte";
import { fs_path_url, fs_share_hotlink_url, fs_share_url, fs_timeseries, type FSNode } from "lib/FilesystemAPI"; import { fs_path_url, fs_share_hotlink_url, fs_share_url, fs_timeseries, type FSNode } from "lib/FilesystemAPI";
import { color_by_name } from "util/Util.svelte"; import { color_by_name } from "util/Util";
import { tick } from "svelte"; import { tick } from "svelte";
import CopyButton from "layout/CopyButton.svelte"; import CopyButton from "layout/CopyButton.svelte";
import type { FSNavigator } from "./FSNavigator"; import type { FSNavigator } from "./FSNavigator";
export let nav: FSNavigator let {
export let visible = false nav,
visible = $bindable(false)
}: {
nav: FSNavigator;
visible?: boolean;
} = $props();
export const toggle = () => visible = !visible export const toggle = () => visible = !visible
$: visibility_change(visible)
const visibility_change = visible => { const visibility_change = visible => {
if (visible) { if (visible) {
update_chart(nav.base, 0, 0) update_chart(nav.base, 0, 0)
} }
} }
$: direct_url = $nav.base.path ? window.location.origin+fs_path_url($nav.base.path) : ""
$: share_url = fs_share_url($nav.path)
$: direct_share_url = fs_share_hotlink_url($nav.path)
let chart let chart: Chart = $state()
let chart_timespan = 0 let chart_timespan = $state(0)
let chart_interval = 0 let chart_interval = $state(0)
let chart_timespans = [ let chart_timespans = [
{label: "Day (1m)", span: 1440, interval: 1}, {label: "Day (1m)", span: 1440, interval: 1},
{label: "Week (1h)", span: 10080, interval: 60}, {label: "Week (1h)", span: 10080, interval: 60},
@@ -36,10 +39,9 @@ let chart_timespans = [
{label: "Five Years (1d)", span: 2628000, interval: 1440}, {label: "Five Years (1d)", span: 2628000, interval: 1440},
] ]
let total_downloads = 0 let total_downloads = $state(0)
let total_transfer = 0 let total_transfer = $state(0)
$: update_chart($nav.base, chart_timespan, chart_interval)
let update_chart = async (base: FSNode, timespan: number, interval: number) => { let update_chart = async (base: FSNode, timespan: number, interval: number) => {
if (chart === undefined) { if (chart === undefined) {
// Wait for the chart element to render, if it's not rendered already // Wait for the chart element to render, if it's not rendered already
@@ -141,6 +143,15 @@ let update_chart = async (base: FSNode, timespan: number, interval: number) => {
console.error("Failed to get time series data:", err) console.error("Failed to get time series data:", err)
} }
} }
run(() => {
visibility_change(visible)
});
let direct_url = $derived($nav.base.path ? window.location.origin+fs_path_url($nav.base.path) : "")
let share_url = $derived(fs_share_url($nav.path))
let direct_share_url = $derived(fs_share_hotlink_url($nav.path))
run(() => {
update_chart($nav.base, chart_timespan, chart_interval)
});
</script> </script>
<Modal bind:visible={visible} title="Details" width={($nav.base.type === "file" ? 1000 : 750) + "px"}> <Modal bind:visible={visible} title="Details" width={($nav.base.type === "file" ? 1000 : 750) + "px"}>
@@ -231,7 +242,7 @@ let update_chart = async (base: FSNode, timespan: number, interval: number) => {
<div class="button_bar"> <div class="button_bar">
{#each chart_timespans as ts} {#each chart_timespans as ts}
<button <button
on:click={() => update_chart($nav.base, ts.span, ts.interval)} onclick={() => update_chart($nav.base, ts.span, ts.interval)}
class:button_highlight={chart_timespan == ts.span}> class:button_highlight={chart_timespan == ts.span}>
{ts.label} {ts.label}
</button> </button>

View File

@@ -4,13 +4,15 @@ import { formatDataVolume, formatThousands } from "util/Formatting"
import { fs_path_url } from "lib/FilesystemAPI"; import { fs_path_url } from "lib/FilesystemAPI";
import type { FSNavigator } from "./FSNavigator"; import type { FSNavigator } from "./FSNavigator";
export let nav: FSNavigator let { nav }: {
nav: FSNavigator;
} = $props();
let loading = true let loading = $state(true)
let downloads = 0 let downloads = $state(0)
let transfer_used = 0 let transfer_used = $state(0)
let socket = null let socket = null
let error_msg = "" let error_msg = $state("")
let connected_to = "" let connected_to = ""
@@ -22,9 +24,9 @@ onMount(() => {
} }
}) })
let total_directories = 0 let total_directories = $state(0)
let total_files = 0 let total_files = $state(0)
let total_file_size = 0 let total_file_size = $state(0)
const update_base = async () => { const update_base = async () => {
if (!nav.initialized) { if (!nav.initialized) {

View File

@@ -12,14 +12,14 @@ import { css_from_path } from "filesystem/edit_window/Branding";
import AffiliatePrompt from "user_home/AffiliatePrompt.svelte"; import AffiliatePrompt from "user_home/AffiliatePrompt.svelte";
import { current_page_store } from "wrap/RouterStore"; import { current_page_store } from "wrap/RouterStore";
let file_preview: FilePreview let file_preview: FilePreview = $state()
let toolbar: Toolbar let toolbar: Toolbar = $state()
let upload_widget: FSUploadWidget let upload_widget: FSUploadWidget = $state()
let details_visible = false let details_visible = $state(false)
let edit_window: EditWindow let edit_window: EditWindow = $state()
let edit_visible = false let edit_visible = $state(false)
const nav = new FSNavigator(true) const nav = $state(new FSNavigator(true))
onMount(() => { onMount(() => {
if ((window as any).intial_node !== undefined) { if ((window as any).intial_node !== undefined) {
@@ -134,7 +134,7 @@ const keydown = (e: KeyboardEvent) => {
}; };
</script> </script>
<svelte:window on:keydown={keydown} /> <svelte:window onkeydown={keydown} />
<div class="filesystem"> <div class="filesystem">
<Breadcrumbs nav={nav}/> <Breadcrumbs nav={nav}/>

View File

@@ -6,23 +6,31 @@ import { formatDataVolume } from "util/Formatting";
import { user } from "lib/UserStore"; import { user } from "lib/UserStore";
import Dialog from "layout/Dialog.svelte"; import Dialog from "layout/Dialog.svelte";
let button: HTMLButtonElement let button: HTMLButtonElement = $state()
let dialog: Dialog let dialog: Dialog = $state()
export let no_login_label = "Pixeldrain" let {
no_login_label = "Pixeldrain",
hide_name = true,
hide_logo = false,
style = "",
embedded = false
}: {
no_login_label?: string;
// Hide the label if the screen is smaller than 800px
hide_name?: boolean;
hide_logo?: boolean;
style?: string;
embedded?: boolean;
} = $props();
// Hide the label if the screen is smaller than 800px let target = $derived(embedded ? "_blank" : "_self")
export let hide_name = true
export let hide_logo = false
export let style = ""
export let embedded = false
$: target = embedded ? "_blank" : "_self"
const open = () => dialog.open(button.getBoundingClientRect()) const open = () => dialog.open(button.getBoundingClientRect())
</script> </script>
<div class="wrapper"> <div class="wrapper">
<button bind:this={button} on:click={open} class="button round" title="Menu" style={style}> <button bind:this={button} onclick={open} class="button round" title="Menu" style={style}>
{#if !hide_logo} {#if !hide_logo}
<PixeldrainLogo style="height: 1.6em; width: 1.6em;"/> <PixeldrainLogo style="height: 1.6em; width: 1.6em;"/>
{/if} {/if}

View File

@@ -1,22 +1,24 @@
<script lang="ts"> <script lang="ts">
import type { FSNavigator } from "./FSNavigator"; import type { FSNavigator } from "./FSNavigator";
import { fs_node_icon, fs_share_hotlink_url, fs_share_url, fs_update, node_is_shared, type FSNode, type FSPermissions } from "lib/FilesystemAPI"; import { fs_node_icon, fs_share_hotlink_url, fs_share_url, fs_update, node_is_shared, type FSNode, type FSPermissions } from "lib/FilesystemAPI";
import { copy_text } from "util/Util.svelte"; import { copy_text } from "util/Util";
import CopyButton from "layout/CopyButton.svelte"; import CopyButton from "layout/CopyButton.svelte";
import Dialog from "layout/Dialog.svelte"; import Dialog from "layout/Dialog.svelte";
import { fade } from "svelte/transition"; import { fade } from "svelte/transition";
export let nav: FSNavigator let { nav }: {
nav: FSNavigator;
} = $props();
let path: FSNode[] let path: FSNode[]
let base: FSNode let base: FSNode = $state()
let toast = "" let toast = $state("")
let share_url = "" let share_url = $state("")
let direct_share_url = "" let direct_share_url = $state("")
let is_parent = false let is_parent = $state(false)
let parent_node: FSNode let parent_node: FSNode = $state()
let dialog: Dialog let dialog: Dialog = $state()
export const open = async (e: MouseEvent, p: FSNode[]) => { export const open = async (e: MouseEvent, p: FSNode[]) => {
path = p path = p
base = path[path.length-1] base = path[path.length-1]
@@ -113,7 +115,7 @@ const share = async () => {
<img src={fs_node_icon(parent_node, 64, 64)} class="node_icon" alt="icon"/> <img src={fs_node_icon(parent_node, 64, 64)} class="node_icon" alt="icon"/>
{parent_node.name} {parent_node.name}
<br/> <br/>
<button on:click={async e => {await make_public(); await share()}} style="display: inline;"> <button onclick={async e => {await make_public(); await share()}} style="display: inline;">
Only share Only share
<img src={fs_node_icon(base, 64, 64)} class="node_icon" alt="icon"/> <img src={fs_node_icon(base, 64, 64)} class="node_icon" alt="icon"/>
{base.name} {base.name}

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import { copy_text } from "util/Util.svelte"; import { copy_text } from "util/Util";
import FileStats from "./FileStats.svelte"; import FileStats from "./FileStats.svelte";
import type { FSNavigator } from "./FSNavigator"; import type { FSNavigator } from "./FSNavigator";
import EditWindow from "./edit_window/EditWindow.svelte"; import EditWindow from "./edit_window/EditWindow.svelte";
@@ -10,12 +10,20 @@ import { bookmark_add, bookmark_del, bookmarks_store, is_bookmark } from "lib/Bo
let dispatch = createEventDispatcher() let dispatch = createEventDispatcher()
export let nav: FSNavigator let {
export let details_visible = false nav = $bindable(),
export let edit_window: EditWindow details_visible = $bindable(false),
export let edit_visible = false edit_window,
let share_dialog: ShareDialog edit_visible = $bindable(false)
let link_copied = false }: {
nav: FSNavigator;
details_visible?: boolean;
edit_window: EditWindow;
edit_visible?: boolean;
} = $props();
let share_dialog: ShareDialog = $state()
let link_copied = $state(false)
export const copy_link = () => { export const copy_link = () => {
const share_url = fs_share_url($nav.path) const share_url = fs_share_url($nav.path)
if (share_url === "") { if (share_url === "") {
@@ -34,36 +42,36 @@ export const copy_link = () => {
<FileStats nav={nav}/> <FileStats nav={nav}/>
<div class="button_row"> <div class="button_row">
<button on:click={() => {nav.open_sibling(-1)}}> <button onclick={() => {nav.open_sibling(-1)}}>
<i class="icon">skip_previous</i> <i class="icon">skip_previous</i>
</button> </button>
<button on:click={() => {nav.shuffle = !nav.shuffle}} class:button_highlight={nav.shuffle}> <button onclick={() => {nav.shuffle = !nav.shuffle}} class:button_highlight={nav.shuffle}>
<i class="icon">shuffle</i> <i class="icon">shuffle</i>
</button> </button>
<button on:click={() => {nav.open_sibling(1)}}> <button onclick={() => {nav.open_sibling(1)}}>
<i class="icon">skip_next</i> <i class="icon">skip_next</i>
</button> </button>
</div> </div>
<button on:click={() => dispatch("download")}> <button onclick={() => dispatch("download")}>
<i class="icon">save</i> <i class="icon">save</i>
<span>Download</span> <span>Download</span>
</button> </button>
{#if is_bookmark($bookmarks_store, $nav.base.id)} {#if is_bookmark($bookmarks_store, $nav.base.id)}
<button on:click={() => bookmark_del($nav.base.id)}> <button onclick={() => bookmark_del($nav.base.id)}>
<i class="icon">bookmark_remove</i> <i class="icon">bookmark_remove</i>
<span>Bookmark</span> <span>Bookmark</span>
</button> </button>
{:else} {:else}
<button on:click={() => bookmark_add($nav.base)}> <button onclick={() => bookmark_add($nav.base)}>
<i class="icon">bookmark_add</i> <i class="icon">bookmark_add</i>
<span>Bookmark</span> <span>Bookmark</span>
</button> </button>
{/if} {/if}
{#if path_is_shared($nav.path)} {#if path_is_shared($nav.path)}
<button on:click={copy_link} class:button_highlight={link_copied}> <button onclick={copy_link} class:button_highlight={link_copied}>
<i class="icon">content_copy</i> <i class="icon">content_copy</i>
<span><u>C</u>opy link</span> <span><u>C</u>opy link</span>
</button> </button>
@@ -71,19 +79,19 @@ export const copy_link = () => {
<!-- Share button is enabled when: The browser has a sharing API, or the user can edit the file (to enable sharing)--> <!-- Share button is enabled when: The browser has a sharing API, or the user can edit the file (to enable sharing)-->
{#if navigator.share !== undefined || $nav.permissions.write === true} {#if navigator.share !== undefined || $nav.permissions.write === true}
<button on:click={(e) => share_dialog.open(e, nav.path)}> <button onclick={(e) => share_dialog.open(e, nav.path)}>
<i class="icon">share</i> <i class="icon">share</i>
<span>Share</span> <span>Share</span>
</button> </button>
{/if} {/if}
<button on:click={() => details_visible = !details_visible} class:button_highlight={details_visible}> <button onclick={() => details_visible = !details_visible} class:button_highlight={details_visible}>
<i class="icon">help</i> <i class="icon">help</i>
<span>Deta<u>i</u>ls</span> <span>Deta<u>i</u>ls</span>
</button> </button>
{#if $nav.base.id !== "me" && $nav.permissions.write === true} {#if $nav.base.id !== "me" && $nav.permissions.write === true}
<button on:click={() => edit_window.edit(nav.base, true, "file")} class:button_highlight={edit_visible}> <button onclick={() => edit_window.edit(nav.base, true, "file")} class:button_highlight={edit_visible}>
<i class="icon">edit</i> <i class="icon">edit</i>
<span><u>E</u>dit</span> <span><u>E</u>dit</span>
</button> </button>

View File

@@ -3,10 +3,14 @@ import Button from "layout/Button.svelte";
import type { FSPermissions, NodeOptions } from "lib/FilesystemAPI"; import type { FSPermissions, NodeOptions } from "lib/FilesystemAPI";
import PermissionButton from "./PermissionButton.svelte"; import PermissionButton from "./PermissionButton.svelte";
export let options: NodeOptions let {
options = $bindable()
}: {
options: NodeOptions;
} = $props();
let new_user_id = "" let new_user_id = $state("")
let new_user_perms = <FSPermissions>{read: true} let new_user_perms = $state(<FSPermissions>{read: true})
const add_user = (e: SubmitEvent) => { const add_user = (e: SubmitEvent) => {
e.preventDefault() e.preventDefault()
if (options.user_permissions === undefined) { if (options.user_permissions === undefined) {
@@ -19,8 +23,8 @@ const del_user = (id: string) => {
options.user_permissions = options.user_permissions options.user_permissions = options.user_permissions
} }
let new_password = "" let new_password = $state("")
let new_password_perms = <FSPermissions>{read: true} let new_password_perms = $state(<FSPermissions>{read: true})
const add_password = (e: SubmitEvent) => { const add_password = (e: SubmitEvent) => {
e.preventDefault() e.preventDefault()
if (options.password_permissions === undefined) { if (options.password_permissions === undefined) {
@@ -64,7 +68,7 @@ const del_password = (pass: string) => {
not receive an e-mail invite. Giving write access to a user without giving not receive an e-mail invite. Giving write access to a user without giving
read access as well does not actually allow them to write anything. read access as well does not actually allow them to write anything.
</p> </p>
<form on:submit={add_user} class="row"> <form onsubmit={add_user} class="row">
<input type="text" bind:value={new_user_id} placeholder="Username" class="grow" size="1"> <input type="text" bind:value={new_user_id} placeholder="Username" class="grow" size="1">
<Button type="submit" icon="add" label="Add"/> <Button type="submit" icon="add" label="Add"/>
<div class="perms"> <div class="perms">
@@ -94,7 +98,7 @@ const del_password = (pass: string) => {
<p> <p>
<b>This feature is not implemented currently!</b> <b>This feature is not implemented currently!</b>
</p> </p>
<form on:submit={add_password} class="row"> <form onsubmit={add_password} class="row">
<input type="text" bind:value={new_password} placeholder="Password" class="grow" size="1"> <input type="text" bind:value={new_password} placeholder="Password" class="grow" size="1">
<Button type="submit" icon="add" label="Add"/> <Button type="submit" icon="add" label="Add"/>
<div class="perms"> <div class="perms">

View File

@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { run } from 'svelte/legacy';
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import ThemePresets from "./ThemePresets.svelte"; import ThemePresets from "./ThemePresets.svelte";
import { fs_update, fs_node_type, type FSNode, type NodeOptions, node_is_shared, type FSPermissions } from "lib/FilesystemAPI"; import { fs_update, fs_node_type, type FSNode, type NodeOptions, node_is_shared, type FSPermissions } from "lib/FilesystemAPI";
@@ -7,11 +8,16 @@ import HelpButton from "layout/HelpButton.svelte";
import FilePicker from "filesystem/filemanager/FilePicker.svelte"; import FilePicker from "filesystem/filemanager/FilePicker.svelte";
let dispatch = createEventDispatcher() let dispatch = createEventDispatcher()
export let file: FSNode let {
export let options: NodeOptions file = $bindable(),
export let enabled: boolean options = $bindable(),
enabled = $bindable()
}: {
file: FSNode;
options: NodeOptions;
enabled: boolean;
} = $props();
$: update_colors(options)
const update_colors = (options: NodeOptions) => { const update_colors = (options: NodeOptions) => {
if (enabled) { if (enabled) {
options.branding_enabled = "true" options.branding_enabled = "true"
@@ -21,7 +27,7 @@ const update_colors = (options: NodeOptions) => {
} }
} }
let picker: FilePicker let picker: FilePicker = $state()
let picking = "" let picking = ""
const pick_image = (type: string) => { const pick_image = (type: string) => {
picking = type picking = type
@@ -61,7 +67,10 @@ const handle_picker = async (e: CustomEvent<FSNode[]>) => {
} }
} }
let highlight_info = false let highlight_info = $state(false)
run(() => {
update_colors(options)
});
</script> </script>
<fieldset> <fieldset>
@@ -127,7 +136,7 @@ let highlight_info = false
working. Recommended dimensions for the header image are 1000x90 px. working. Recommended dimensions for the header image are 1000x90 px.
</p> </p>
<div>Header image ID</div> <div>Header image ID</div>
<button on:click={() => pick_image("brand_header_image")}> <button onclick={() => pick_image("brand_header_image")}>
<i class="icon">folder_open</i> <i class="icon">folder_open</i>
Pick Pick
</button> </button>
@@ -135,7 +144,7 @@ let highlight_info = false
<div>Header image link</div> <div>Header image link</div>
<input class="span2" type="text" bind:value={options.brand_header_link}/> <input class="span2" type="text" bind:value={options.brand_header_link}/>
<div>Background image ID</div> <div>Background image ID</div>
<button on:click={() => pick_image("brand_background_image")}> <button onclick={() => pick_image("brand_background_image")}>
<i class="icon">folder_open</i> <i class="icon">folder_open</i>
Pick Pick
</button> </button>

View File

@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { preventDefault } from 'svelte/legacy';
import { fs_rename, fs_update, type FSNode, type NodeOptions } from "lib/FilesystemAPI"; import { fs_rename, fs_update, type FSNode, type NodeOptions } from "lib/FilesystemAPI";
import Modal from "util/Modal.svelte"; import Modal from "util/Modal.svelte";
import BrandingOptions from "./BrandingOptions.svelte"; import BrandingOptions from "./BrandingOptions.svelte";
@@ -9,13 +10,18 @@ import AccessControl from "./AccessControl.svelte";
import type { FSNavigator } from "filesystem/FSNavigator"; import type { FSNavigator } from "filesystem/FSNavigator";
import { loading_finish, loading_start } from "lib/Loading"; import { loading_finish, loading_start } from "lib/Loading";
export let nav: FSNavigator let file: FSNode = $state({} as FSNode)
let file: FSNode = {} as FSNode let options: NodeOptions = $state({} as NodeOptions)
let options: NodeOptions = {} as NodeOptions
let custom_css = "" let custom_css = $state("")
export let visible: boolean let {
nav,
visible = $bindable()
}: {
nav: FSNavigator;
visible: boolean;
} = $props();
// Open the edit window. Argument 1 is the file to edit, 2 is whether the file // Open the edit window. Argument 1 is the file to edit, 2 is whether the file
// should be opened after the user finishes editing and 3 is the default tab // should be opened after the user finishes editing and 3 is the default tab
@@ -54,11 +60,11 @@ export const edit = (f: FSNode, oae = false, open_tab = "") => {
visible = true visible = true
} }
let tab = "file" let tab = $state("file")
let open_after_edit = false let open_after_edit = $state(false)
let new_name = "" let new_name = $state("")
let branding_enabled = false let branding_enabled = $state(false)
const save = async (keep_editing = false) => { const save = async (keep_editing = false) => {
console.debug("Saving file", file.path) console.debug("Saving file", file.path)
@@ -106,27 +112,27 @@ const save = async (keep_editing = false) => {
<Modal bind:visible={visible} title="Edit {file.name}" width="800px" form="edit_form" style="color: var(--body_text_color); {custom_css}"> <Modal bind:visible={visible} title="Edit {file.name}" width="800px" form="edit_form" style="color: var(--body_text_color); {custom_css}">
<div class="tab_bar"> <div class="tab_bar">
<button class:button_highlight={tab === "file"} on:click={() => tab = "file"}> <button class:button_highlight={tab === "file"} onclick={() => tab = "file"}>
<i class="icon">edit</i> <i class="icon">edit</i>
Properties Properties
</button> </button>
<button class:button_highlight={tab === "share"} on:click={() => tab = "share"}> <button class:button_highlight={tab === "share"} onclick={() => tab = "share"}>
<i class="icon">share</i> <i class="icon">share</i>
Sharing Sharing
</button> </button>
{#if $nav.permissions.owner} {#if $nav.permissions.owner}
<button class:button_highlight={tab === "access"} on:click={() => tab = "access"}> <button class:button_highlight={tab === "access"} onclick={() => tab = "access"}>
<i class="icon">key</i> <i class="icon">key</i>
Access control Access control
</button> </button>
{/if} {/if}
<button class:button_highlight={tab === "branding"} on:click={() => tab = "branding"}> <button class:button_highlight={tab === "branding"} onclick={() => tab = "branding"}>
<i class="icon">palette</i> <i class="icon">palette</i>
Branding Branding
</button> </button>
</div> </div>
<form id="edit_form" on:submit|preventDefault={() => save(false)}></form> <form id="edit_form" onsubmit={preventDefault(() => save(false))}></form>
<div class="tab_content"> <div class="tab_content">
{#if tab === "file"} {#if tab === "file"}
@@ -138,7 +144,10 @@ const save = async (keep_editing = false) => {
bind:open_after_edit bind:open_after_edit
/> />
{:else if tab === "share"} {:else if tab === "share"}
<SharingOptions bind:file bind:options on:save={() => save(true)} /> <SharingOptions
bind:file
bind:options
/>
{:else if tab === "access"} {:else if tab === "access"}
<AccessControl bind:options /> <AccessControl bind:options />
{:else if tab === "branding"} {:else if tab === "branding"}

View File

@@ -5,13 +5,21 @@ import PathLink from "filesystem/util/PathLink.svelte";
import type { FSNavigator } from "filesystem/FSNavigator"; import type { FSNavigator } from "filesystem/FSNavigator";
import { loading_finish, loading_start } from "lib/Loading"; import { loading_finish, loading_start } from "lib/Loading";
export let nav: FSNavigator let {
export let file: FSNode = {} as FSNode nav,
export let new_name: string file = $bindable({} as FSNode),
export let visible: boolean new_name = $bindable(),
export let open_after_edit: boolean visible = $bindable(),
open_after_edit = $bindable(false)
}: {
nav: FSNavigator;
file?: FSNode;
new_name: string;
visible: boolean;
open_after_edit: boolean;
} = $props();
$: is_root_dir = file.path === "/"+file.id let is_root_dir = $derived(file.path === "/"+file.id)
const delete_file = async (e: MouseEvent) => { const delete_file = async (e: MouseEvent) => {
e.preventDefault() e.preventDefault()

View File

@@ -2,9 +2,15 @@
import ToggleButton from "layout/ToggleButton.svelte"; import ToggleButton from "layout/ToggleButton.svelte";
import type { FSPermissions } from "lib/FilesystemAPI"; import type { FSPermissions } from "lib/FilesystemAPI";
export let permissions = <FSPermissions>{} let {
permissions = $bindable()
}: {
permissions: FSPermissions
} = $props();
</script> </script>
<ToggleButton group_first bind:on={permissions.read}>Read</ToggleButton> {#if permissions !== undefined}
<ToggleButton group_middle bind:on={permissions.write}>Write</ToggleButton> <ToggleButton group_first bind:on={permissions.read}>Read</ToggleButton>
<ToggleButton group_last bind:on={permissions.delete}>Delete</ToggleButton> <ToggleButton group_middle bind:on={permissions.write}>Write</ToggleButton>
<ToggleButton group_last bind:on={permissions.delete}>Delete</ToggleButton>
{/if}

View File

@@ -1,18 +1,22 @@
<script lang="ts"> <script lang="ts">
import { domain_url } from "util/Util.svelte"; import { run } from 'svelte/legacy';
import { domain_url } from "util/Util";
import CopyButton from "layout/CopyButton.svelte"; import CopyButton from "layout/CopyButton.svelte";
import { formatDate } from "util/Formatting"; import { formatDate } from "util/Formatting";
import { node_is_shared, type FSNode, type NodeOptions } from "lib/FilesystemAPI"; import { node_is_shared, type FSNode, type NodeOptions } from "lib/FilesystemAPI";
import AccessControl from "./AccessControl.svelte"; import AccessControl from "./AccessControl.svelte";
export let file: FSNode = {} as FSNode let {
export let options: NodeOptions file = $bindable(),
options = $bindable(),
}: {
file?: FSNode;
options: NodeOptions;
} = $props();
let embed_html: string let embed_html: string = $state()
let preview_area: HTMLDivElement let preview_area: HTMLDivElement = $state()
$: share_link = window.location.protocol+"//"+window.location.host+"/d/"+file.id
$: embed_iframe(file, options)
const embed_iframe = (file: FSNode, options: NodeOptions) => { const embed_iframe = (file: FSNode, options: NodeOptions) => {
if (!node_is_shared(file)) { if (!node_is_shared(file)) {
example = false example = false
@@ -28,7 +32,7 @@ const embed_iframe = (file: FSNode, options: NodeOptions) => {
`></iframe>` `></iframe>`
} }
let example = false let example = $state(false)
const toggle_example = () => { const toggle_example = () => {
if (node_is_shared(file)) { if (node_is_shared(file)) {
example = !example example = !example
@@ -40,6 +44,10 @@ const toggle_example = () => {
} }
} }
let share_link = $derived(window.location.protocol+"//"+window.location.host+"/d/"+file.id)
run(() => {
embed_iframe(file, options)
});
</script> </script>
<fieldset> <fieldset>
@@ -78,7 +86,7 @@ const toggle_example = () => {
<textarea bind:value={embed_html} style="width: 100%; height: 4em;"></textarea> <textarea bind:value={embed_html} style="width: 100%; height: 4em;"></textarea>
<br/> <br/>
<CopyButton text={embed_html}>Copy HTML</CopyButton> <CopyButton text={embed_html}>Copy HTML</CopyButton>
<button on:click={toggle_example} class:button_highlight={example} disabled={!node_is_shared(file)}> <button onclick={toggle_example} class:button_highlight={example} disabled={!node_is_shared(file)}>
<i class="icon">visibility</i> Show example <i class="icon">visibility</i> Show example
</button> </button>
</div> </div>

View File

@@ -1,9 +1,13 @@
<script lang="ts"> <script lang="ts">
import type { FSNodeProperties } from "lib/FilesystemAPI"; import type { FSNodeProperties } from "lib/FilesystemAPI";
export let properties: FSNodeProperties = {} as FSNodeProperties let {
properties = $bindable({} as FSNodeProperties)
}: {
properties?: FSNodeProperties;
} = $props();
let current_theme = -1 let current_theme = $state(-1)
const set_theme = (index: number) => { const set_theme = (index: number) => {
current_theme = index current_theme = index
@@ -71,7 +75,7 @@ const themes = [
</script> </script>
{#each themes as theme, index (theme.name)} {#each themes as theme, index (theme.name)}
<button class:button_highlight={current_theme === index} on:click={() => {set_theme(index)}}> <button class:button_highlight={current_theme === index} onclick={() => {set_theme(index)}}>
{theme.name} {theme.name}
</button> </button>
{/each} {/each}

View File

@@ -6,18 +6,25 @@ import { FileAction } from "./FileManagerLib";
let dispatch = createEventDispatcher() let dispatch = createEventDispatcher()
export let nav: FSNavigator let {
export let show_hidden = false nav,
export let large_icons = false show_hidden = false,
export let hide_edit = false large_icons = false,
hide_edit = false
}: {
nav: FSNavigator;
show_hidden?: boolean;
large_icons?: boolean;
hide_edit?: boolean;
} = $props();
</script> </script>
<div class="directory"> <div class="directory">
{#each $nav.children as child, index (child.path)} {#each $nav.children as child, index (child.path)}
<a <a
href={"/d"+fs_encode_path(child.path)} href={"/d"+fs_encode_path(child.path)}
on:click={e => dispatch("file", {index: index, action: FileAction.Click, original: e})} onclick={e => dispatch("file", {index: index, action: FileAction.Click, original: e})}
on:contextmenu={e => dispatch("file", {index: index, action: FileAction.Context, original: e})} oncontextmenu={e => dispatch("file", {index: index, action: FileAction.Context, original: e})}
class="node" class="node"
class:node_selected={child.fm_selected} class:node_selected={child.fm_selected}
class:hidden={child.name.startsWith(".") && !show_hidden} class:hidden={child.name.startsWith(".") && !show_hidden}
@@ -26,20 +33,15 @@ export let hide_edit = false
<div class="node_name"> <div class="node_name">
{child.name} {child.name}
</div> </div>
{#if node_is_shared(child)} {#if node_is_shared(child)}
<a <i class="icon" title="This file / directory is shared. Click to open public link">share</i>
href="/d/{child.id}"
on:click={e => dispatch("file", {index: index, action: FileAction.Share, original: e})}
class="button flat action_button"
>
<i class="icon" title="This file / directory is shared. Click to open public link">share</i>
</a>
{/if} {/if}
{#if !hide_edit} {#if !hide_edit}
<button <button
class="action_button flat" class="action_button flat"
on:click={e => dispatch("file", {index: index, action: FileAction.Menu, original: e})} onclick={e => dispatch("file", {index: index, action: FileAction.Menu, original: e})}
> >
<i class="icon">menu</i> <i class="icon">menu</i>
</button> </button>

View File

@@ -1,15 +1,16 @@
<script lang="ts"> <script lang="ts">
import { preventDefault } from 'svelte/legacy';
import { onMount } from "svelte"; import { onMount } from "svelte";
import { fs_mkdir } from "lib/FilesystemAPI"; import { fs_mkdir } from "lib/FilesystemAPI";
import Button from "layout/Button.svelte"; import Button from "layout/Button.svelte";
import type { FSNavigator } from "filesystem/FSNavigator"; import type { FSNavigator } from "filesystem/FSNavigator";
import { loading_finish, loading_start } from "lib/Loading"; import { loading_finish, loading_start } from "lib/Loading";
export let nav: FSNavigator let { nav }: { nav: FSNavigator } = $props();
let name_input: HTMLInputElement; let name_input: HTMLInputElement = $state();
let new_dir_name = "" let new_dir_name = $state("")
let error_msg = "" let error_msg = $state("")
let create_dir = async () => { let create_dir = async () => {
let form = new FormData() let form = new FormData()
form.append("type", "dir") form.append("type", "dir")
@@ -42,7 +43,7 @@ onMount(() => {
</div> </div>
{/if} {/if}
<form id="create_dir_form" class="create_dir" on:submit|preventDefault={create_dir}> <form id="create_dir_form" class="create_dir" onsubmit={preventDefault(create_dir)}>
<img src="/res/img/mime/folder.png" class="icon" alt="icon"/> <img src="/res/img/mime/folder.png" class="icon" alt="icon"/>
<input class="dirname" type="text" bind:this={name_input} bind:value={new_dir_name} /> <input class="dirname" type="text" bind:this={name_input} bind:value={new_dir_name} />
<Button form="create_dir_form" type="submit" icon="create_new_folder" label="Create"/> <Button form="create_dir_form" type="submit" icon="create_new_folder" label="Create"/>

View File

@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { run } from 'svelte/legacy';
import { fs_delete_all, fs_download, fs_rename, type FSNode } from "lib/FilesystemAPI" import { fs_delete_all, fs_download, fs_rename, type FSNode } from "lib/FilesystemAPI"
import { onMount } from "svelte" import { onMount } from "svelte"
import CreateDirectory from "./CreateDirectory.svelte" import CreateDirectory from "./CreateDirectory.svelte"
@@ -16,19 +17,29 @@ import { FileAction, type FileEvent } from "./FileManagerLib";
import FileMenu from "./FileMenu.svelte"; import FileMenu from "./FileMenu.svelte";
import { loading_finish, loading_start } from "lib/Loading"; import { loading_finish, loading_start } from "lib/Loading";
export let nav: FSNavigator let {
export let upload_widget: FsUploadWidget nav = $bindable(),
export let edit_window: EditWindow upload_widget,
export let directory_view = "" edit_window = $bindable(),
let large_icons = false directory_view = $bindable(""),
children
}: {
nav: FSNavigator;
upload_widget: FsUploadWidget;
edit_window: EditWindow;
directory_view?: string;
children?: import('svelte').Snippet;
} = $props();
let large_icons = $state(false)
let uploader: FsUploadWidget let uploader: FsUploadWidget
let mode = "viewing" let mode = $state("viewing")
let creating_dir = false let creating_dir = $state(false)
let show_hidden = false let show_hidden = $state(false)
let file_menu: FileMenu let file_menu: FileMenu = $state()
export const upload = (files: File[]) => { export const upload = (files: File[]) => {
return uploader.upload(files) return uploader.upload_files(files)
} }
// Navigation functions // Navigation functions
@@ -228,10 +239,6 @@ const select_node = (index: number) => {
last_selected_node = index last_selected_node = index
} }
// When the directory is reloaded we want to keep our selection, so this
// function watches the children array for changes and updates the selection
// when it changes
$: update($nav.children)
const update = (children: FSNode[]) => { const update = (children: FSNode[]) => {
creating_dir = false creating_dir = false
@@ -245,8 +252,8 @@ const update = (children: FSNode[]) => {
} }
} }
let moving_files = 0 let moving_files = $state(0)
let moving_directories = 0 let moving_directories = $state(0)
const move_start = () => { const move_start = () => {
moving_files = 0 moving_files = 0
moving_directories = 0 moving_directories = 0
@@ -296,9 +303,15 @@ onMount(() => {
directory_view = "list" directory_view = "list"
} }
}) })
// When the directory is reloaded we want to keep our selection, so this
// function watches the children array for changes and updates the selection
// when it changes
run(() => {
update($nav.children)
});
</script> </script>
<svelte:window on:keydown={keypress} on:keyup={keypress} /> <svelte:window onkeydown={keypress} onkeyup={keypress} />
<div <div
class="container" class="container"
@@ -311,25 +324,25 @@ onMount(() => {
{#if mode === "viewing"} {#if mode === "viewing"}
<div class="toolbar"> <div class="toolbar">
<div class="toolbar_left"> <div class="toolbar_left">
<button on:click={navigate_back} title="Back"> <button onclick={navigate_back} title="Back">
<i class="icon">arrow_back</i> <i class="icon">arrow_back</i>
</button> </button>
<button on:click={() => nav.navigate_up()} disabled={$nav.path.length <= 1} title="Up"> <button onclick={() => nav.navigate_up()} disabled={$nav.path.length <= 1} title="Up">
<i class="icon">north</i> <i class="icon">north</i>
</button> </button>
<button on:click={() => nav.reload()} title="Refresh directory listing"> <button onclick={() => nav.reload()} title="Refresh directory listing">
<i class="icon">refresh</i> <i class="icon">refresh</i>
</button> </button>
</div> </div>
<div class="toolbar_middle"> <div class="toolbar_middle">
<button on:click={() => toggle_view()} title="Switch between gallery, list and compact view"> <button onclick={() => toggle_view()} title="Switch between gallery, list and compact view">
<i class="icon" class:button_highlight={directory_view === "list"}>list</i> <i class="icon" class:button_highlight={directory_view === "list"}>list</i>
<i class="icon" class:button_highlight={directory_view === "gallery"}>collections</i> <i class="icon" class:button_highlight={directory_view === "gallery"}>collections</i>
<i class="icon" class:button_highlight={directory_view === "compact"}>view_compact</i> <i class="icon" class:button_highlight={directory_view === "compact"}>view_compact</i>
</button> </button>
<button class="button_large_icons" on:click={() => toggle_large_icons()} title="Switch between large and small icons"> <button class="button_large_icons" onclick={() => toggle_large_icons()} title="Switch between large and small icons">
{#if large_icons} {#if large_icons}
<i class="icon">zoom_out</i> <i class="icon">zoom_out</i>
{:else} {:else}
@@ -337,7 +350,7 @@ onMount(() => {
{/if} {/if}
</button> </button>
<button on:click={() => {show_hidden = !show_hidden}} title="Toggle hidden files"> <button onclick={() => {show_hidden = !show_hidden}} title="Toggle hidden files">
{#if show_hidden} {#if show_hidden}
<i class="icon">visibility_off</i> <i class="icon">visibility_off</i>
{:else} {:else}
@@ -348,13 +361,13 @@ onMount(() => {
<div class="toolbar_right"> <div class="toolbar_right">
{#if $nav.permissions.write} {#if $nav.permissions.write}
<button on:click={() => upload_widget.pick_files()} title="Upload files to this directory"> <button onclick={() => upload_widget.pick_files()} title="Upload files to this directory">
<i class="icon">cloud_upload</i> <i class="icon">cloud_upload</i>
</button> </button>
<Button click={() => {creating_dir = !creating_dir}} highlight={creating_dir} icon="create_new_folder" title="Make folder"/> <Button click={() => {creating_dir = !creating_dir}} highlight={creating_dir} icon="create_new_folder" title="Make folder"/>
<button on:click={selecting_mode} title="Select and delete files"> <button onclick={selecting_mode} title="Select and delete files">
<i class="icon">select_all</i> <i class="icon">select_all</i>
</button> </button>
{/if} {/if}
@@ -368,7 +381,7 @@ onMount(() => {
<Button click={viewing_mode} icon="close"/> <Button click={viewing_mode} icon="close"/>
<div class="toolbar_spacer">Selecting files</div> <div class="toolbar_spacer">Selecting files</div>
<Button click={move_start} icon="drive_file_move" label="Move"/> <Button click={move_start} icon="drive_file_move" label="Move"/>
<button on:click={delete_selected} class="button_red"> <button onclick={delete_selected} class="button_red">
<i class="icon">delete</i> <i class="icon">delete</i>
Delete Delete
</button> </button>
@@ -407,7 +420,7 @@ onMount(() => {
</div> </div>
{/if} {/if}
<slot></slot> {@render children?.()}
{#if directory_view === "list"} {#if directory_view === "list"}
<ListView nav={nav} show_hidden={show_hidden} large_icons={large_icons} on:file={file_event} /> <ListView nav={nav} show_hidden={show_hidden} large_icons={large_icons} on:file={file_event} />
@@ -418,7 +431,7 @@ onMount(() => {
{/if} {/if}
</div> </div>
<FileMenu bind:this={file_menu} bind:nav bind:edit_window /> <FileMenu bind:this={file_menu} nav={nav} edit_window={edit_window} />
<style> <style>
.container { .container {

View File

@@ -7,10 +7,16 @@ import { bookmark_add, bookmark_del, bookmarks_store, is_bookmark } from "lib/Bo
import { fs_download, type FSNode } from "lib/FilesystemAPI"; import { fs_download, type FSNode } from "lib/FilesystemAPI";
import { tick } from "svelte"; import { tick } from "svelte";
export let nav: FSNavigator let {
export let edit_window: EditWindow nav,
let dialog: Dialog edit_window
let node: FSNode = null }: {
nav: FSNavigator;
edit_window: EditWindow;
} = $props();
let dialog: Dialog = $state()
let node: FSNode = $state(null)
export const open = async (n: FSNode, target: EventTarget) => { export const open = async (n: FSNode, target: EventTarget) => {
node = n node = n

View File

@@ -9,25 +9,28 @@ import { FSNavigator } from "filesystem/FSNavigator";
import type { FSNode } from "lib/FilesystemAPI"; import type { FSNode } from "lib/FilesystemAPI";
import { FileAction, type FileEvent } from "./FileManagerLib"; import { FileAction, type FileEvent } from "./FileManagerLib";
let nav = new FSNavigator(false) let nav = $state(new FSNavigator(false))
let modal: Modal let modal: Modal = $state()
let dispatch = createEventDispatcher() let dispatch = createEventDispatcher()
let directory_view = "" let directory_view = $state("")
let large_icons = false let large_icons = $state(false)
let show_hidden = false let show_hidden = $state(false)
export let select_multiple = false
let { select_multiple = false }: {
select_multiple?: boolean;
} = $props();
export const open = (path: string) => { export const open = (path: string) => {
modal.show() modal.show()
nav.navigate(path, false) nav.navigate(path, false)
} }
$: selected_files = $nav.children.reduce((acc, file) => { let selected_files = $derived($nav.children.reduce((acc, file) => {
if (file.fm_selected) { if (file.fm_selected) {
acc++ acc++
} }
return acc return acc
}, 0) }, 0))
// Navigation functions // Navigation functions
@@ -128,42 +131,44 @@ onMount(() => {
}) })
</script> </script>
<svelte:window on:keydown={detect_shift} on:keyup={detect_shift} /> <svelte:window onkeydown={detect_shift} onkeyup={detect_shift} />
<Modal bind:this={modal} width="900px"> <Modal bind:this={modal} width="900px">
<div class="header" slot="title"> {#snippet title()}
<button class="button round" on:click={modal.hide}> <div class="header" >
<i class="icon">close</i> <button class="button round" onclick={modal.hide}>
</button> <i class="icon">close</i>
<button on:click={() => nav.navigate_up()} disabled={$nav.path.length <= 1} title="Up"> </button>
<i class="icon">north</i> <button onclick={() => nav.navigate_up()} disabled={$nav.path.length <= 1} title="Up">
</button> <i class="icon">north</i>
<button on:click={() => nav.reload()} title="Refresh directory listing"> </button>
<i class="icon">refresh</i> <button onclick={() => nav.reload()} title="Refresh directory listing">
</button> <i class="icon">refresh</i>
</button>
<div class="title"> <div class="title">
Selected {selected_files} files Selected {selected_files} files
</div>
<button onclick={() => {show_hidden = !show_hidden}} title="Toggle hidden files">
{#if show_hidden}
<i class="icon">visibility_off</i>
{:else}
<i class="icon">visibility</i>
{/if}
</button>
<button onclick={() => toggle_view()} title="Switch between gallery, list and compact view">
<i class="icon" class:button_highlight={directory_view === "list"}>list</i>
<i class="icon" class:button_highlight={directory_view === "gallery"}>collections</i>
<i class="icon" class:button_highlight={directory_view === "compact"}>view_compact</i>
</button>
<button class="button button_highlight round" onclick={done}>
<i class="icon">done</i> Pick
</button>
</div> </div>
{/snippet}
<button on:click={() => {show_hidden = !show_hidden}} title="Toggle hidden files">
{#if show_hidden}
<i class="icon">visibility_off</i>
{:else}
<i class="icon">visibility</i>
{/if}
</button>
<button on:click={() => toggle_view()} title="Switch between gallery, list and compact view">
<i class="icon" class:button_highlight={directory_view === "list"}>list</i>
<i class="icon" class:button_highlight={directory_view === "gallery"}>collections</i>
<i class="icon" class:button_highlight={directory_view === "compact"}>view_compact</i>
</button>
<button class="button button_highlight round" on:click={done}>
<i class="icon">done</i> Pick
</button>
</div>
<Breadcrumbs nav={nav}/> <Breadcrumbs nav={nav}/>

View File

@@ -5,17 +5,23 @@ import type { FSNavigator } from "filesystem/FSNavigator";
import { FileAction } from "./FileManagerLib"; import { FileAction } from "./FileManagerLib";
let dispatch = createEventDispatcher() let dispatch = createEventDispatcher()
export let nav: FSNavigator let {
export let show_hidden = false nav,
export let large_icons = false show_hidden = false,
large_icons = false
}: {
nav: FSNavigator;
show_hidden?: boolean;
large_icons?: boolean;
} = $props();
</script> </script>
<div class="gallery"> <div class="gallery">
{#each $nav.children as child, index (child.path)} {#each $nav.children as child, index (child.path)}
<a class="file" <a class="file"
href={"/d"+fs_encode_path(child.path)} href={"/d"+fs_encode_path(child.path)}
on:click={e => dispatch("file", {index: index, action: FileAction.Click, original: e})} onclick={e => dispatch("file", {index: index, action: FileAction.Click, original: e})}
on:contextmenu={e => dispatch("file", {index: index, action: FileAction.Context, original: e})} oncontextmenu={e => dispatch("file", {index: index, action: FileAction.Context, original: e})}
class:selected={child.fm_selected} class:selected={child.fm_selected}
class:hidden={child.name.startsWith(".") && !show_hidden} class:hidden={child.name.startsWith(".") && !show_hidden}
class:large_icons class:large_icons

View File

@@ -8,68 +8,81 @@ import { FileAction } from "./FileManagerLib";
let dispatch = createEventDispatcher() let dispatch = createEventDispatcher()
export let nav: FSNavigator let {
export let show_hidden = false nav,
export let large_icons = false show_hidden = false,
export let hide_edit = false large_icons = false,
export let hide_branding = false hide_edit = false,
hide_branding = false
}: {
nav: FSNavigator;
show_hidden?: boolean;
large_icons?: boolean;
hide_edit?: boolean;
hide_branding?: boolean;
} = $props();
</script> </script>
<div class="directory"> <table class="directory">
<tr> <thead>
<td></td> <tr>
<td><SortButton active_field={$nav.sort_last_field} asc={$nav.sort_asc} sort_func={nav.sort_children} field="name">Name</SortButton></td> <td></td>
<td class="hide_small"><SortButton active_field={$nav.sort_last_field} asc={$nav.sort_asc} sort_func={nav.sort_children} field="file_size">Size</SortButton></td> <td><SortButton active_field={$nav.sort_last_field} asc={$nav.sort_asc} sort_func={nav.sort_children} field="name">Name</SortButton></td>
<td></td> <td class="hide_small"><SortButton active_field={$nav.sort_last_field} asc={$nav.sort_asc} sort_func={nav.sort_children} field="file_size">Size</SortButton></td>
</tr> <td></td>
{#each $nav.children as child, index (child.path)} </tr>
<a </thead>
href={"/d"+fs_encode_path(child.path)} <tbody>
on:click={e => dispatch("file", {index: index, action: FileAction.Click, original: e})} {#each $nav.children as child, index (child.path)}
on:contextmenu={e => dispatch("file", {index: index, action: FileAction.Context, original: e})} <tr
class="node" onclick={e => dispatch("file", {index: index, action: FileAction.Click, original: e})}
class:node_selected={child.fm_selected} oncontextmenu={e => dispatch("file", {index: index, action: FileAction.Context, original: e})}
class:hidden={child.name.startsWith(".") && !show_hidden} class="node"
> class:node_selected={child.fm_selected}
<td> class:hidden={child.name.startsWith(".") && !show_hidden}
<img src={fs_node_icon(child, 64, 64)} class="node_icon" class:large_icons alt="icon"/> >
</td> <td>
<td class="node_name"> <img src={fs_node_icon(child, 64, 64)} class="node_icon" class:large_icons alt="icon"/>
{child.name} </td>
</td> <td class="node_name">
<td class="node_size hide_small"> <a href={"/d"+fs_encode_path(child.path)}>
{#if child.type === "file"} {child.name}
{formatDataVolume(child.file_size, 3)} </a>
{/if} </td>
</td> <td class="node_size hide_small">
<td class="node_icons"> {#if child.type === "file"}
<div class="icons_wrap"> {formatDataVolume(child.file_size, 3)}
{#if child.abuse_type !== undefined}
<i class="icon" title="This file / directory has received an abuse report. It cannot be shared">block</i>
{:else if node_is_shared(child)}
<a
href="/d/{child.id}"
on:click={e => dispatch("file", {index: index, action: FileAction.Share, original: e})}
class="button action_button"
>
<i class="icon" title="This file / directory is shared. Click to open public link">share</i>
</a>
{/if} {/if}
{#if child.properties !== undefined && child.properties.branding_enabled === "true" && !hide_branding} </td>
<button class="action_button" on:click={e => dispatch("file", {index: index, action: FileAction.Branding, original: e})}> <td class="node_icons">
<i class="icon">palette</i> <div class="icons_wrap">
</button> {#if child.abuse_type !== undefined}
{/if} <i class="icon" title="This file / directory has received an abuse report. It cannot be shared">block</i>
{#if !hide_edit} {:else if node_is_shared(child)}
<button class="action_button" on:click={e => dispatch("file", {index: index, action: FileAction.Menu, original: e})}> <a
<i class="icon">menu</i> href="/d/{child.id}"
</button> onclick={e => dispatch("file", {index: index, action: FileAction.Share, original: e})}
{/if} class="button action_button"
</div> >
</td> <i class="icon" title="This file / directory is shared. Click to open public link">share</i>
</a> </a>
{/each} {/if}
</div> {#if child.properties !== undefined && child.properties.branding_enabled === "true" && !hide_branding}
<button class="action_button" onclick={e => dispatch("file", {index: index, action: FileAction.Branding, original: e})}>
<i class="icon">palette</i>
</button>
{/if}
{#if !hide_edit}
<button class="action_button" onclick={e => dispatch("file", {index: index, action: FileAction.Menu, original: e})}>
<i class="icon">menu</i>
</button>
{/if}
</div>
</td>
</tr>
{/each}
</tbody>
</table>
<style> <style>
.directory { .directory {

View File

@@ -1,19 +1,23 @@
<script lang="ts"> <script lang="ts">
import { preventDefault } from 'svelte/legacy';
import { onMount } from "svelte"; import { onMount } from "svelte";
import { fs_search, fs_encode_path, fs_thumbnail_url } from "lib/FilesystemAPI"; import { fs_search, fs_encode_path, fs_thumbnail_url } from "lib/FilesystemAPI";
import type { FSNavigator } from "filesystem/FSNavigator"; import type { FSNavigator } from "filesystem/FSNavigator";
import { loading_finish, loading_start } from "lib/Loading"; import { loading_finish, loading_start } from "lib/Loading";
export let nav: FSNavigator let { nav }: {
nav: FSNavigator;
} = $props();
let search_bar: HTMLInputElement let search_bar: HTMLInputElement = $state()
let error = "" let error = $state("")
let search_term = "" let search_term = $state("")
let search_results: string[] = [] let search_results: string[] = $state([])
let selected_result = 0 let selected_result = $state(0)
let searching = false let searching = false
let last_searched_term = "" let last_searched_term = ""
let last_limit = 10 let last_limit = $state(10)
onMount(() => { onMount(() => {
// Clear results when the user moves to a new directory // Clear results when the user moves to a new directory
@@ -144,7 +148,7 @@ const window_keydown = (e: KeyboardEvent) => {
} }
</script> </script>
<svelte:window on:keydown={window_keydown} /> <svelte:window onkeydown={window_keydown} />
{#if error === "path_not_found" || error === "node_is_a_directory"} {#if error === "path_not_found" || error === "node_is_a_directory"}
<div class="highlight_yellow center"> <div class="highlight_yellow center">
@@ -160,7 +164,7 @@ const window_keydown = (e: KeyboardEvent) => {
{/if} {/if}
<div class="center"> <div class="center">
<form class="search_form" on:submit|preventDefault={submit_search}> <form class="search_form" onsubmit={preventDefault(submit_search)}>
<i class="icon">search</i> <i class="icon">search</i>
<input <input
bind:this={search_bar} bind:this={search_bar}
@@ -169,12 +173,12 @@ const window_keydown = (e: KeyboardEvent) => {
placeholder="Press / to search in {$nav.base.name}" placeholder="Press / to search in {$nav.base.name}"
style="width: 100%;" style="width: 100%;"
bind:value={search_term} bind:value={search_term}
on:keydown={input_keydown} onkeydown={input_keydown}
on:keyup={input_keyup} onkeyup={input_keyup}
/> />
{#if search_term !== ""} {#if search_term !== ""}
<!-- Button needs to be of button type in order to not submit the form --> <!-- Button needs to be of button type in order to not submit the form -->
<button on:click={() => clear_search(false)} type="button"> <button onclick={() => clear_search(false)} type="button">
<i class="icon">close</i> <i class="icon">close</i>
</button> </button>
{/if} {/if}
@@ -188,7 +192,7 @@ const window_keydown = (e: KeyboardEvent) => {
{#each search_results as result, index} {#each search_results as result, index}
<a <a
href={"/d"+fs_encode_path(result)} href={"/d"+fs_encode_path(result)}
on:click|preventDefault={() => open_result(index)} onclick={preventDefault(() => open_result(index))}
class="node" class="node"
class:node_selected={selected_result === index} class:node_selected={selected_result === index}
> >
@@ -203,7 +207,7 @@ const window_keydown = (e: KeyboardEvent) => {
{#if search_results.length === last_limit} {#if search_results.length === last_limit}
<div class="node"> <div class="node">
<div class="node_name" style="text-align: center;"> <div class="node_name" style="text-align: center;">
<button on:click={() => {search(last_limit + 100)}}> <button onclick={() => {search(last_limit + 100)}}>
<i class="icon">expand_more</i> <i class="icon">expand_more</i>
More results More results
</button> </button>

View File

@@ -1,4 +1,4 @@
<script lang="ts" context="module"> <script lang="ts" module>
export type UploadJob = { export type UploadJob = {
task_id: number, task_id: number,
file: File, file: File,
@@ -14,9 +14,11 @@ import { tick } from "svelte";
import UploadProgress from "./UploadProgress.svelte"; import UploadProgress from "./UploadProgress.svelte";
import type { FSNavigator } from "filesystem/FSNavigator"; import type { FSNavigator } from "filesystem/FSNavigator";
export let nav: FSNavigator let { nav }: {
nav: FSNavigator;
} = $props();
let file_input_field: HTMLInputElement let file_input_field: HTMLInputElement = $state()
let file_input_change = (e: Event) => { let file_input_change = (e: Event) => {
// Start uploading the files async // Start uploading the files async
upload_files((e.target as HTMLInputElement).files) upload_files((e.target as HTMLInputElement).files)
@@ -28,8 +30,8 @@ export const pick_files = () => {
file_input_field.click() file_input_field.click()
} }
let visible = false let visible = $state(false)
let upload_queue: UploadJob[] = []; let upload_queue: UploadJob[] = $state([]);
let task_id_counter = 0 let task_id_counter = 0
export const upload_files = async (files: File[]|FileList) => { export const upload_files = async (files: File[]|FileList) => {
@@ -76,8 +78,8 @@ export const upload_file = async (file: File) => {
// each upload progress bar will have bound itself to its array item // each upload progress bar will have bound itself to its array item
upload_queue = upload_queue upload_queue = upload_queue
if (active_uploads === 0 && state !== "uploading") { if (active_uploads === 0 && status !== "uploading") {
state = "uploading" status = "uploading"
visible = true visible = true
await tick() await tick()
await start_upload() await start_upload()
@@ -85,7 +87,7 @@ export const upload_file = async (file: File) => {
} }
let active_uploads = 0 let active_uploads = 0
let state = "idle" let status = $state("idle")
const start_upload = async () => { const start_upload = async () => {
active_uploads = 0 active_uploads = 0
@@ -115,7 +117,7 @@ const start_upload = async () => {
} }
if (active_uploads === 0) { if (active_uploads === 0) {
state = "finished" status = "finished"
nav.reload() nav.reload()
// Empty the queue to free any references to lingering components // Empty the queue to free any references to lingering components
@@ -123,12 +125,12 @@ const start_upload = async () => {
// In ten seconds we close the popup // In ten seconds we close the popup
setTimeout(() => { setTimeout(() => {
if (state === "finished") { if (status === "finished") {
visible = false visible = false
} }
}, 10000) }, 10000)
} else { } else {
state = "uploading" status = "uploading"
} }
} }
@@ -139,7 +141,7 @@ const finish_upload = () => {
} }
const leave_confirmation = (e: BeforeUnloadEvent) => { const leave_confirmation = (e: BeforeUnloadEvent) => {
if (state === "uploading") { if (status === "uploading") {
e.preventDefault() e.preventDefault()
return "If you close this page your files will stop uploading. Do you want to continue?" return "If you close this page your files will stop uploading. Do you want to continue?"
} else { } else {
@@ -148,22 +150,22 @@ const leave_confirmation = (e: BeforeUnloadEvent) => {
} }
</script> </script>
<svelte:window on:beforeunload={leave_confirmation} /> <svelte:window onbeforeunload={leave_confirmation} />
<input <input
bind:this={file_input_field} bind:this={file_input_field}
on:change={file_input_change} onchange={file_input_change}
class="upload_input" type="file" name="file" multiple class="upload_input" type="file" name="file" multiple
/> />
{#if visible} {#if visible}
<div class="upload_widget"> <div class="upload_widget">
<div class="header"> <div class="header">
{#if state === "idle"} {#if status === "idle"}
Waiting for files Waiting for files
{:else if state === "uploading"} {:else if status === "uploading"}
Uploading files... Uploading files...
{:else if state === "finished"} {:else if status === "finished"}
Done Done
{/if} {/if}
</div> </div>

View File

@@ -6,11 +6,19 @@ import Button from "layout/Button.svelte"
import type { UploadJob } from "./FSUploadWidget.svelte"; import type { UploadJob } from "./FSUploadWidget.svelte";
let dispatch = createEventDispatcher() let dispatch = createEventDispatcher()
export let job: UploadJob
export let total = 0 let {
export let loaded = 0 job = $bindable(),
let error_code = "" total = $bindable(0),
let error_message = "" loaded = $bindable(0)
}: {
job: UploadJob;
total?: number;
loaded?: number;
} = $props();
let error_code = $state("")
let error_message = $state("")
let xhr: XMLHttpRequest = null let xhr: XMLHttpRequest = null
export const start = () => { export const start = () => {

View File

@@ -1,10 +1,18 @@
<script lang="ts"> <script lang="ts">
import { preventDefault } from 'svelte/legacy';
import type { FSNavigator } from "filesystem/FSNavigator"; import type { FSNavigator } from "filesystem/FSNavigator";
export let nav: FSNavigator let {
export let path = "" nav,
path = "",
children
}: {
nav: FSNavigator;
path?: string;
children?: import('svelte').Snippet;
} = $props();
</script> </script>
<a href={"/d"+path} on:click|preventDefault={() => {nav.navigate(path, true)}}> <a href={"/d"+path} onclick={preventDefault(() => {nav.navigate(path, true)})}>
<slot></slot> {@render children?.()}
</a> </a>

View File

@@ -1,14 +1,19 @@
<script lang="ts"> <script lang="ts">
import { preventDefault } from 'svelte/legacy';
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { fs_path_url, fs_encode_path, fs_node_icon } from "lib/FilesystemAPI" import { fs_path_url, fs_encode_path, fs_node_icon } from "lib/FilesystemAPI"
import TextBlock from "layout/TextBlock.svelte" import TextBlock from "layout/TextBlock.svelte"
import type { FSNavigator } from 'filesystem/FSNavigator'; import type { FSNavigator } from 'filesystem/FSNavigator';
export let nav: FSNavigator let { nav, children }: {
let player: HTMLAudioElement nav: FSNavigator;
let playing = false children?: import('svelte').Snippet;
} = $props();
let player: HTMLAudioElement = $state()
let playing = $state(false)
let media_session = false let media_session = false
let siblings = [] let siblings = $state([])
export const toggle_playback = () => playing ? player.pause() : player.play() export const toggle_playback = () => playing ? player.pause() : player.play()
export const toggle_mute = () => player.muted = !player.muted export const toggle_mute = () => player.muted = !player.muted
@@ -47,7 +52,7 @@ onMount(() => {
}) })
</script> </script>
<slot></slot> {@render children?.()}
<TextBlock width="1000px"> <TextBlock width="1000px">
<audio <audio
@@ -56,30 +61,30 @@ onMount(() => {
src={fs_path_url($nav.base.path)} src={fs_path_url($nav.base.path)}
autoplay autoplay
controls controls
on:pause={() => playing = false } onpause={() => playing = false}
on:play={() => playing = true } onplay={() => playing = true}
on:ended={() => nav.open_sibling(1) }> onended={() => nav.open_sibling(1)}>
<track kind="captions"/> <track kind="captions"/>
</audio> </audio>
<div style="text-align: center;"> <div style="text-align: center;">
<button on:click={() => nav.open_sibling(-1) }><i class="icon">skip_previous</i></button> <button onclick={() => nav.open_sibling(-1)}><i class="icon">skip_previous</i></button>
<button on:click={() => seek(-10) }><i class="icon">replay_10</i></button> <button onclick={() => seek(-10)}><i class="icon">replay_10</i></button>
<button on:click={toggle_playback}> <button onclick={toggle_playback}>
{#if playing} {#if playing}
<i class="icon">pause</i> <i class="icon">pause</i>
{:else} {:else}
<i class="icon">play_arrow</i> <i class="icon">play_arrow</i>
{/if} {/if}
</button> </button>
<button on:click={() => seek(10) }><i class="icon">forward_10</i></button> <button onclick={() => seek(10)}><i class="icon">forward_10</i></button>
<button on:click={() => nav.open_sibling(1) }><i class="icon">skip_next</i></button> <button onclick={() => nav.open_sibling(1)}><i class="icon">skip_next</i></button>
</div> </div>
<h2>Tracklist</h2> <h2>Tracklist</h2>
{#each siblings as sibling (sibling.path)} {#each siblings as sibling (sibling.path)}
<a <a
href={"/d"+fs_encode_path(sibling.path)} href={"/d"+fs_encode_path(sibling.path)}
on:click|preventDefault={() => nav.navigate(sibling.path, true)} onclick={preventDefault(() => nav.navigate(sibling.path, true))}
class="node" class="node"
> >
{#if sibling.path === $nav.base.path} {#if sibling.path === $nav.base.path}

View File

@@ -1,10 +1,12 @@
<script lang="ts"> <script lang="ts">
export let path = [] import type { FSNode } from 'lib/FilesystemAPI';
import { run } from 'svelte/legacy';
let image_uri: string let { path = [] }: {path: FSNode[]} = $props();
let image_link: string
$: update_links(path) let image_uri: string = $state()
const update_links = (path) => { let image_link: string = $state()
const update_links = (path: FSNode[]) => {
image_uri = null image_uri = null
image_link = null image_link = null
for (let node of path) { for (let node of path) {
@@ -18,6 +20,9 @@ const update_links = (path) => {
} }
} }
} }
run(() => {
update_links(path)
});
</script> </script>
{#if image_uri} {#if image_uri}

View File

@@ -8,10 +8,13 @@ import type { FSNavigator } from "filesystem/FSNavigator";
let dispatch = createEventDispatcher() let dispatch = createEventDispatcher()
export let nav: FSNavigator let { nav, children }: {
nav: FSNavigator;
children?: import('svelte').Snippet;
} = $props();
</script> </script>
<slot></slot> {@render children?.()}
<h1>{$nav.base.name}</h1> <h1>{$nav.base.name}</h1>
@@ -20,11 +23,11 @@ export let nav: FSNavigator
Size: {formatDataVolume($nav.base.file_size, 3)}<br/> Size: {formatDataVolume($nav.base.file_size, 3)}<br/>
Upload date: {formatDate($nav.base.created, true, true, false)} Upload date: {formatDate($nav.base.created, true, true, false)}
<hr/> <hr/>
<button class="button_highlight" on:click={() => {dispatch("download")}}> <button class="button_highlight" onclick={() => {dispatch("download")}}>
<i class="icon">download</i> <i class="icon">download</i>
<span>Download</span> <span>Download</span>
</button> </button>
<button on:click={() => {dispatch("details")}}> <button onclick={() => {dispatch("details")}}>
<i class="icon">help</i> <i class="icon">help</i>
<span>Details</span> <span>Details</span>
</button> </button>

View File

@@ -16,12 +16,14 @@ import type { FSNavigator } from "filesystem/FSNavigator";
import FsUploadWidget from "filesystem/upload_widget/FSUploadWidget.svelte"; import FsUploadWidget from "filesystem/upload_widget/FSUploadWidget.svelte";
import EditWindow from "filesystem/edit_window/EditWindow.svelte"; import EditWindow from "filesystem/edit_window/EditWindow.svelte";
export let nav: FSNavigator let { nav, upload_widget, edit_window }: {
export let upload_widget: FsUploadWidget nav: FSNavigator;
export let edit_window: EditWindow upload_widget: FsUploadWidget;
edit_window: EditWindow;
} = $props();
let viewer: any let viewer: any = $state()
let viewer_type = "" let viewer_type = $state("")
let last_path = "" let last_path = ""
onMount(() => nav.subscribe(state_update)) onMount(() => nav.subscribe(state_update))

View File

@@ -6,13 +6,16 @@ import type { FSNavigator } from "filesystem/FSNavigator";
let dispatch = createEventDispatcher(); let dispatch = createEventDispatcher();
export let nav: FSNavigator let { nav }: {
let container: HTMLDivElement nav: FSNavigator;
let zoom = false } = $props();
let container: HTMLDivElement = $state()
let zoom = $state(false)
let x = 0, y = 0 let x = 0, y = 0
let dragging = false let dragging = false
let swipe_prev = true let swipe_prev = $state(true)
let swipe_next = true let swipe_next = $state(true)
export const update = async () => { export const update = async () => {
dispatch("loading", true) dispatch("loading", true)
@@ -66,7 +69,7 @@ const mouseup = (e: MouseEvent) => {
} }
</script> </script>
<svelte:window on:mousemove={mousemove} on:mouseup={mouseup} /> <svelte:window onmousemove={mousemove} onmouseup={mouseup} />
<div <div
bind:this={container} bind:this={container}
@@ -80,12 +83,12 @@ const mouseup = (e: MouseEvent) => {
on_next: () => nav.open_sibling(1), on_next: () => nav.open_sibling(1),
}} }}
> >
<!-- svelte-ignore a11y-no-noninteractive-element-interactions --> <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<img <img
on:dblclick={() => {zoom = !zoom}} ondblclick={() => {zoom = !zoom}}
on:mousedown={mousedown} onmousedown={mousedown}
on:load={on_load} onload={on_load}
on:error={on_load} onerror={on_load}
class="image" class="image"
class:zoom class:zoom
src={fs_path_url($nav.base.path)} src={fs_path_url($nav.base.path)}

View File

@@ -2,7 +2,9 @@
import { fs_path_url } from "lib/FilesystemAPI"; import { fs_path_url } from "lib/FilesystemAPI";
import type { FSNavigator } from "filesystem/FSNavigator"; import type { FSNavigator } from "filesystem/FSNavigator";
export let nav: FSNavigator let { nav }: {
nav: FSNavigator;
} = $props();
</script> </script>
<iframe <iframe

View File

@@ -3,8 +3,12 @@ import { tick } from "svelte";
import { fs_path_url, type FSNode } from "lib/FilesystemAPI"; import { fs_path_url, type FSNode } from "lib/FilesystemAPI";
import type { FSNavigator } from "filesystem/FSNavigator"; import type { FSNavigator } from "filesystem/FSNavigator";
export let nav: FSNavigator let { nav, children }: {
let text_type = "text" nav: FSNavigator;
children?: import('svelte').Snippet;
} = $props();
let text_type = $state("text")
export const update = () => { export const update = () => {
console.debug("Loading text file", nav.base.name) console.debug("Loading text file", nav.base.name)
@@ -25,7 +29,7 @@ export const update = () => {
} }
} }
let text_pre: HTMLPreElement let text_pre: HTMLPreElement = $state()
const text = async (file: FSNode) => { const text = async (file: FSNode) => {
text_type = "text" text_type = "text"
await tick() await tick()
@@ -42,7 +46,7 @@ const text = async (file: FSNode) => {
}) })
} }
let md_container: HTMLElement let md_container: HTMLElement = $state()
const markdown = async (file: FSNode) => { const markdown = async (file: FSNode) => {
text_type = "markdown" text_type = "markdown"
await tick() await tick()
@@ -61,7 +65,7 @@ const markdown = async (file: FSNode) => {
</script> </script>
<div class="container"> <div class="container">
<slot></slot> {@render children?.()}
{#if text_type === "markdown"} {#if text_type === "markdown"}
<section bind:this={md_container} class="md"> <section bind:this={md_container} class="md">

View File

@@ -1,4 +1,4 @@
<script lang="ts" context="module"> <script lang="ts" module>
export type TorrentInfo = { export type TorrentInfo = {
trackers: string[] trackers: string[]
comment: string, comment: string,
@@ -26,9 +26,12 @@ import { loading_finish, loading_start } from "lib/Loading";
let dispatch = createEventDispatcher() let dispatch = createEventDispatcher()
export let nav: FSNavigator let { nav, children }: {
nav: FSNavigator;
children?: import('svelte').Snippet;
} = $props();
let status = "loading" let status = $state("loading")
export const update = async () => { export const update = async () => {
try { try {
@@ -64,11 +67,11 @@ export const update = async () => {
} }
} }
let torrent: TorrentInfo = {} as TorrentInfo let torrent: TorrentInfo = $state({} as TorrentInfo)
let magnet = "" let magnet = $state("")
</script> </script>
<slot></slot> {@render children?.()}
<h1>{$nav.base.name}</h1> <h1>{$nav.base.name}</h1>
@@ -93,7 +96,7 @@ let magnet = ""
Torrent file could not be parsed. It may be corrupted. Torrent file could not be parsed. It may be corrupted.
</p> </p>
{/if} {/if}
<button on:click={() => {dispatch("download")}} class="button"> <button onclick={() => {dispatch("download")}} class="button">
<i class="icon">download</i> <i class="icon">download</i>
<span>Download torrent file</span> <span>Download torrent file</span>
</button> </button>

View File

@@ -1,8 +1,11 @@
<script lang="ts"> <script lang="ts">
import TorrentItem from './TorrentItem.svelte';
import { formatDataVolume } from "util/Formatting"; import { formatDataVolume } from "util/Formatting";
import type { TorrentFile } from "./Torrent.svelte"; import type { TorrentFile } from "./Torrent.svelte";
export let item: TorrentFile = {} as TorrentFile let { item = {} as TorrentFile }: {
item?: TorrentFile;
} = $props();
</script> </script>
<ul class="list_open"> <ul class="list_open">
@@ -10,7 +13,7 @@ export let item: TorrentFile = {} as TorrentFile
<li class:list_closed={!child.children}> <li class:list_closed={!child.children}>
{name} ({formatDataVolume(child.size, 3)})<br/> {name} ({formatDataVolume(child.size, 3)})<br/>
{#if child.children} {#if child.children}
<svelte:self item={child}></svelte:self> <TorrentItem item={child}></TorrentItem>
{/if} {/if}
</li> </li>
{/each} {/each}

View File

@@ -5,16 +5,18 @@ import { fs_path_url } from "lib/FilesystemAPI";
import type { FSNavigator } from "filesystem/FSNavigator"; import type { FSNavigator } from "filesystem/FSNavigator";
let dispatch = createEventDispatcher() let dispatch = createEventDispatcher()
export let nav: FSNavigator let { nav }: {
nav: FSNavigator;
} = $props();
// Used to detect when the file path changes // Used to detect when the file path changes
let last_path = "" let last_path = ""
let loaded = false let loaded = $state(false)
let player: HTMLVideoElement let player: HTMLVideoElement = $state()
let playing = false let playing = $state(false)
let media_session = false let media_session = false
let loop = false let loop = $state(false)
export const update = async () => { export const update = async () => {
if (media_session) { if (media_session) {
@@ -85,8 +87,7 @@ const video_keydown = (e: KeyboardEvent) => {
{#if {#if
$nav.base.file_type === "video/x-matroska" || $nav.base.file_type === "video/x-matroska" ||
$nav.base.file_type === "video/quicktime" || $nav.base.file_type === "video/quicktime" ||
$nav.base.file_type === "video/x-ms-asf" $nav.base.file_type === "video/x-ms-asf"}
}
<div class="compatibility_warning"> <div class="compatibility_warning">
This video file type is not compatible with every web This video file type is not compatible with every web
browser. If the video fails to play you can try downloading browser. If the video fails to play you can try downloading
@@ -96,7 +97,7 @@ const video_keydown = (e: KeyboardEvent) => {
<div class="player_and_controls"> <div class="player_and_controls">
<div class="player"> <div class="player">
{#if loaded} {#if loaded}
<!-- svelte-ignore a11y-media-has-caption --> <!-- svelte-ignore a11y_media_has_caption -->
<video <video
bind:this={player} bind:this={player}
controls controls
@@ -104,9 +105,9 @@ const video_keydown = (e: KeyboardEvent) => {
autoplay autoplay
loop={loop} loop={loop}
class="video" class="video"
on:pause={() => playing = false } onpause={() => playing = false}
on:play={() => playing = true } onplay={() => playing = true}
on:keydown={video_keydown} onkeydown={video_keydown}
use:video_position={() => $nav.base.sha256_sum.substring(0, 8)} use:video_position={() => $nav.base.sha256_sum.substring(0, 8)}
> >
<source src={fs_path_url($nav.base.path)} type={$nav.base.file_type} /> <source src={fs_path_url($nav.base.path)} type={$nav.base.file_type} />
@@ -116,34 +117,34 @@ const video_keydown = (e: KeyboardEvent) => {
<div class="controls"> <div class="controls">
<div class="spacer"></div> <div class="spacer"></div>
<button on:click={() => dispatch("open_sibling", -1) }> <button onclick={() => dispatch("open_sibling", -1)}>
<i class="icon">skip_previous</i> <i class="icon">skip_previous</i>
</button> </button>
<button on:click={() => seek(-10)}> <button onclick={() => seek(-10)}>
<i class="icon">replay_10</i> <i class="icon">replay_10</i>
</button> </button>
<button on:click={toggle_playback} class="button_highlight"> <button onclick={toggle_playback} class="button_highlight">
{#if playing} {#if playing}
<i class="icon">pause</i> <i class="icon">pause</i>
{:else} {:else}
<i class="icon">play_arrow</i> <i class="icon">play_arrow</i>
{/if} {/if}
</button> </button>
<button on:click={() => seek(10)}> <button onclick={() => seek(10)}>
<i class="icon">forward_10</i> <i class="icon">forward_10</i>
</button> </button>
<button on:click={() => dispatch("open_sibling", 1) }> <button onclick={() => dispatch("open_sibling", 1)}>
<i class="icon">skip_next</i> <i class="icon">skip_next</i>
</button> </button>
<div style="width: 16px; height: 8px;"></div> <div style="width: 16px; height: 8px;"></div>
<button on:click={toggle_mute} class:button_red={player && player.muted}> <button onclick={toggle_mute} class:button_red={player && player.muted}>
{#if player && player.muted} {#if player && player.muted}
<i class="icon">volume_off</i> <i class="icon">volume_off</i>
{:else} {:else}
<i class="icon">volume_up</i> <i class="icon">volume_up</i>
{/if} {/if}
</button> </button>
<button on:click={toggle_fullscreen}> <button onclick={toggle_fullscreen}>
<i class="icon">fullscreen</i> <i class="icon">fullscreen</i>
</button> </button>
<div class="spacer"></div> <div class="spacer"></div>

View File

@@ -1,4 +1,4 @@
<script lang="ts" context="module"> <script lang="ts" module>
export type ZipEntry = { export type ZipEntry = {
size: number, size: number,
children?: {[index: string]: ZipEntry}, children?: {[index: string]: ZipEntry},
@@ -19,15 +19,18 @@ import { loading_finish, loading_start } from "lib/Loading";
let dispatch = createEventDispatcher() let dispatch = createEventDispatcher()
export let nav: FSNavigator let { nav, children }: {
nav: FSNavigator;
children?: import('svelte').Snippet;
} = $props();
let status = "loading" let status = $state("loading")
let zip: ZipEntry = {size: 0} as ZipEntry let zip: ZipEntry = $state({size: 0} as ZipEntry)
let uncomp_size = 0 let uncomp_size = 0
let comp_ratio = 0 let comp_ratio = $state(0)
let archive_type = "" let archive_type = $state("")
let truncated = false let truncated = $state(false)
export const update = async () => { export const update = async () => {
if (nav.base.file_type === "application/x-7z-compressed") { if (nav.base.file_type === "application/x-7z-compressed") {
@@ -93,7 +96,7 @@ const recursive_size = (file: ZipEntry) => {
} }
</script> </script>
<slot></slot> {@render children?.()}
<h1>{$nav.base.name}</h1> <h1>{$nav.base.name}</h1>
@@ -110,7 +113,7 @@ const recursive_size = (file: ZipEntry) => {
{/if} {/if}
Uploaded on: {formatDate($nav.base.created, true, true, true)} Uploaded on: {formatDate($nav.base.created, true, true, true)}
<br/> <br/>
<button class="button_highlight" on:click={() => {dispatch("download")}}> <button class="button_highlight" onclick={() => {dispatch("download")}}>
<i class="icon">download</i> <i class="icon">download</i>
<span>Download</span> <span>Download</span>
</button> </button>

View File

@@ -1,8 +1,11 @@
<script lang="ts"> <script lang="ts">
import ZipItem from './ZipItem.svelte';
import type { ZipEntry } from "filesystem/viewers/Zip.svelte"; import type { ZipEntry } from "filesystem/viewers/Zip.svelte";
import { formatDataVolume } from "util/Formatting"; import { formatDataVolume } from "util/Formatting";
export let item: ZipEntry = {} as ZipEntry let { item = {} as ZipEntry }: {
item?: ZipEntry;
} = $props();
</script> </script>
<!-- First get directories and render them as details collapsibles --> <!-- First get directories and render them as details collapsibles -->
@@ -23,7 +26,7 @@ export let item: ZipEntry = {} as ZipEntry
<!-- Performance optimization, only render children if details is expanded --> <!-- Performance optimization, only render children if details is expanded -->
{#if child.details_open} {#if child.details_open}
<svelte:self item={child}></svelte:self> <ZipItem item={child}></ZipItem>
{/if} {/if}
</details> </details>
{/if} {/if}

View File

@@ -3,7 +3,7 @@ import { onMount } from "svelte";
import Expandable from "util/Expandable.svelte"; import Expandable from "util/Expandable.svelte";
import { formatDate } from "util/Formatting"; import { formatDate } from "util/Formatting";
let result = null; let result = $state(null);
onMount(async () => { onMount(async () => {
try { try {
@@ -21,9 +21,11 @@ onMount(async () => {
{#if result !== null && result.user_banned} {#if result !== null && result.user_banned}
<section> <section>
<Expandable click_expand> <Expandable click_expand>
<div slot="header" class="header red"> {#snippet header()}
Your account has been banned, click for details <div class="header red">
</div> Your account has been banned, click for details
</div>
{/snippet}
<p> <p>
Your user account has been banned from uploading to Your user account has been banned from uploading to
pixeldrain due to violation of the pixeldrain due to violation of the
@@ -72,13 +74,16 @@ onMount(async () => {
{:else if result !== null && result.ip_offences.length > 0} {:else if result !== null && result.ip_offences.length > 0}
<section> <section>
<Expandable click_expand> <Expandable click_expand>
<div slot="header" class="header" class:red={result.ip_banned} class:yellow={!result.ip_banned}> {#snippet header()}
{#if result.ip_banned} <div class="header" class:red={result.ip_banned} class:yellow={!result.ip_banned}>
Your IP address has been banned, click for details {#if result.ip_banned}
{:else} Your IP address has been banned, click for details
Your IP address has received a copyright strike, click for details {:else}
{/if} Your IP address has received a copyright strike, click for details
</div> {/if}
</div>
{/snippet}
{#if result.ip_banned} {#if result.ip_banned}
<p> <p>
Your IP address ({result.address}) has been banned from Your IP address ({result.address}) has been banned from

View File

@@ -36,10 +36,12 @@ import OtherPlans from "./OtherPlans.svelte";
</div> </div>
<div class="feature_cell pro_feat"> <div class="feature_cell pro_feat">
<Tooltip> <Tooltip>
<span slot="label"> {#snippet label()}
<span class="bold">€4 / month</span> or <span>
<span class="bold">€40 / year</span> <span class="bold">€4 / month</span> or
</span> <span class="bold">€40 / year</span>
</span>
{/snippet}
The Pro subscription is managed by Patreon. Patreon's own fees The Pro subscription is managed by Patreon. Patreon's own fees
and sales tax will be added to this price. After paying you need and sales tax will be added to this price. After paying you need
to link your pixeldrain account to Patreon to activate the plan. to link your pixeldrain account to Patreon to activate the plan.
@@ -47,7 +49,9 @@ import OtherPlans from "./OtherPlans.svelte";
</div> </div>
<div class="feature_cell prepaid_feat"> <div class="feature_cell prepaid_feat">
<Tooltip> <Tooltip>
<span slot="label" class="bold">€1 / month minimum</span> {#snippet label()}
<span class="bold">€1 / month minimum</span>
{/snippet}
<p> <p>
The minimum fee is only charged when usage is less than €1. The minimum fee is only charged when usage is less than €1.
This calculation is per day, the €1 amount is divided by the This calculation is per day, the €1 amount is divided by the
@@ -62,7 +66,9 @@ import OtherPlans from "./OtherPlans.svelte";
</div> </div>
<div class="feature_cell free_feat"> <div class="feature_cell free_feat">
<Tooltip> <Tooltip>
<span slot="label" class="bold">6 GB per day</span> {#snippet label()}
<span class="bold">6 GB per day</span>
{/snippet}
<p> <p>
Free users are limited to downloading 6 GB per day, this Free users are limited to downloading 6 GB per day, this
limit is linked to your IP address, even if you are logged limit is linked to your IP address, even if you are logged
@@ -77,7 +83,9 @@ import OtherPlans from "./OtherPlans.svelte";
</div> </div>
<div class="feature_cell pro_feat"> <div class="feature_cell pro_feat">
<Tooltip> <Tooltip>
<span slot="label" class="bold">4 TB per 30 days</span> {#snippet label()}
<span class="bold">4 TB per 30 days</span>
{/snippet}
<p> <p>
The transfer limit is used for downloading, sharing and The transfer limit is used for downloading, sharing and
hotlinking files. hotlinking files.
@@ -89,7 +97,9 @@ import OtherPlans from "./OtherPlans.svelte";
</div> </div>
<div class="feature_cell prepaid_feat"> <div class="feature_cell prepaid_feat">
<Tooltip> <Tooltip>
<span slot="label" class="bold">€1 per TB transferred</span> {#snippet label()}
<span class="bold">€1 per TB transferred</span>
{/snippet}
<p> <p>
Prepaid does not have a transfer limit, instead you are Prepaid does not have a transfer limit, instead you are
charged for what you use at a rate of €1 per terabyte charged for what you use at a rate of €1 per terabyte
@@ -132,7 +142,9 @@ import OtherPlans from "./OtherPlans.svelte";
</div> </div>
<div class="feature_cell free_feat"> <div class="feature_cell free_feat">
<Tooltip> <Tooltip>
<span slot="label" class="bold">120 days (4 months)</span> {#snippet label()}
<span class="bold">120 days (4 months)</span>
{/snippet}
<p> <p>
Files expire when they have not been downloaded in the last Files expire when they have not been downloaded in the last
120 days. A download is counted when more than 10% of the 120 days. A download is counted when more than 10% of the
@@ -142,7 +154,9 @@ import OtherPlans from "./OtherPlans.svelte";
</div> </div>
<div class="feature_cell pro_feat"> <div class="feature_cell pro_feat">
<Tooltip> <Tooltip>
<span slot="label" class="bold">240 days (8 months)</span> {#snippet label()}
<span class="bold">240 days (8 months)</span>
{/snippet}
<p> <p>
The Pro plan has 240 day file expiry. The same rules apply The Pro plan has 240 day file expiry. The same rules apply
as the free plan. Higher Patreon subscription plans are as the free plan. Higher Patreon subscription plans are
@@ -152,7 +166,9 @@ import OtherPlans from "./OtherPlans.svelte";
</div> </div>
<div class="feature_cell prepaid_feat"> <div class="feature_cell prepaid_feat">
<Tooltip> <Tooltip>
<span slot="label" class="bold">Files do not expire</span> {#snippet label()}
<span class="bold">Files do not expire</span>
{/snippet}
<p> <p>
Files don't expire while your Prepaid plan is active. If Files don't expire while your Prepaid plan is active. If
your credit runs out you have one week to top up your your credit runs out you have one week to top up your

View File

@@ -1,63 +0,0 @@
<script>
import Expandable from "util/Expandable.svelte";
</script>
<section>
<Expandable click_expand>
<div slot="header" class="header">
Important policy change! Click to expand
</div>
<p>
Sometime this summer pixeldrain will start requiring you to be
logged in to an account in order to upload new files. This is
necessary due to the large amount of regional blockings that have
been implemented against pixeldrain recently.
</p>
<p>
None of the countries that have blocked pixeldrain have specified a
valid reason, nor have I been able to contact them to ask what's
going on. But I can only guess that it has something to do with
abusive content being uploaded. Pixeldrain currently uses an IP
address banning system for restricting uploads from users who have
violated the content policy, but at the scale the site operates at
now that has proven to not be effective anymore. For that reason
pixeldrain will be switching to an account ban system.
</p>
<p>
The account ban system will restrict file uploads to your account
for a certain amount of time which depends on the type of the
offence. Serious violations might get your account banned for up to
a year, minor violations maybe a day or a week per file. It will
depend on the amount of abuse that slips through. Once the account
ban system is activated the IP ban system will be disabled.
</p>
<p>
Account banning is only effective if there is some system in place
to prevent the automated creation of accounts. There is already a
CAPTCHA in place on the registration page. I might start requiring
e-mail addresses for new accounts as well. The lack of an e-mail
address requirement has caused many issues with lost accounts as
well, so it had to happen anyway. I will also be looking into
integrating with OAuth providers (Google, Microsoft, Facebook,
Patreon, etc) to make the login flow simpler for newcomers.
</p>
<p>
After the site has been cleaned up it might take a long time before
all the regional blocking issues are resolved. Because of that I
will be adding alternative domain names for premium subscribers to
use. Most of the blocks are only on DNS level, which means they can
be circumvented by using a different domain name.
</p>
</Expandable>
</section>
<style>
.header {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
border-radius: 6px;
background-color: rgba(0, 0, 255, 0.2);
}
</style>

View File

@@ -1,27 +1,28 @@
<script> <script>
import { run } from 'svelte/legacy';
import { onMount } from "svelte"; import { onMount } from "svelte";
import Euro from "util/Euro.svelte"; import Euro from "util/Euro.svelte";
import ProgressBar from "util/ProgressBar.svelte"; import ProgressBar from "util/ProgressBar.svelte";
let pixeldrain_storage = 0 let pixeldrain_storage = $state(0)
let pixeldrain_egress = 0 let pixeldrain_egress = $state(0)
let pixeldrain_total = 0 let pixeldrain_total = $state(0)
let backblaze_storage = 0 let backblaze_storage = $state(0)
let backblaze_egress = 0 let backblaze_egress = $state(0)
let backblaze_api = 0 let backblaze_api = $state(0)
let backblaze_total = 0 let backblaze_total = $state(0)
let wasabi_storage = 0 let wasabi_storage = $state(0)
let wasabi_total = 0 let wasabi_total = $state(0)
let price_amazon = 0 let price_amazon = 0
let price_azure = 0 let price_azure = 0
let price_google = 0 let price_google = 0
let price_max = 0 let price_max = $state(0)
let storage = 10 // TB let storage = $state(10) // TB
let egress = 10 // TB let egress = $state(10) // TB
let avg_file_size = 1000 // kB let avg_file_size = $state(1000) // kB
$: { run(() => {
pixeldrain_storage = storage * 4 pixeldrain_storage = storage * 4
pixeldrain_egress = egress * 1 pixeldrain_egress = egress * 1
pixeldrain_total = pixeldrain_storage + pixeldrain_egress pixeldrain_total = pixeldrain_storage + pixeldrain_egress
@@ -43,7 +44,7 @@ $: {
// price_google = (storage * 20) + (egress * 20) // price_google = (storage * 20) + (egress * 20)
price_max = Math.max(pixeldrain_total, backblaze_total, wasabi_total, price_amazon, price_azure, price_google) price_max = Math.max(pixeldrain_total, backblaze_total, wasabi_total, price_amazon, price_azure, price_google)
} });
onMount(() => {}) onMount(() => {})
</script> </script>

View File

@@ -1,5 +1,7 @@
<script lang="ts"> <script lang="ts">
export let style: string; let { style }: {
style: string;
} = $props();
</script> </script>
<svg style={style} width="24" height="24" viewBox="0 -28.5 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid"> <svg style={style} width="24" height="24" viewBox="0 -28.5 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">

View File

@@ -1,5 +1,7 @@
<script lang="ts"> <script lang="ts">
export let style: string; let { style = "" }: {
style: string;
} = $props();
</script> </script>
<svg style={style} role="img" viewBox="0 0 24 24" width="24" height="24" xmlns="http://www.w3.org/2000/svg"> <svg style={style} role="img" viewBox="0 0 24 24" width="24" height="24" xmlns="http://www.w3.org/2000/svg">

View File

@@ -1,5 +1,7 @@
<script lang="ts"> <script lang="ts">
export let style: string; let { style = "" }: {
style: string;
} = $props();
</script> </script>
<svg style={style} xmlns="http://www.w3.org/2000/svg" version="1.1" width="24" height="24" viewBox="0 0 987.525 987.525"> <svg style={style} xmlns="http://www.w3.org/2000/svg" version="1.1" width="24" height="24" viewBox="0 0 987.525 987.525">

View File

@@ -1,5 +1,7 @@
<script lang="ts"> <script lang="ts">
export let style: string; let { style = "" }: {
style: string;
} = $props();
</script> </script>
<svg style={style} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 216.4144 232.00976"> <svg style={style} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 216.4144 232.00976">

View File

@@ -1,5 +1,7 @@
<script lang="ts"> <script lang="ts">
export let style: string; let { style = "" }: {
style: string;
} = $props();
</script> </script>
<svg style={style} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 100 100"> <svg style={style} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 100 100">

View File

@@ -1,5 +1,7 @@
<script lang="ts"> <script lang="ts">
export let style: string; let { style = "" }: {
style: string;
} = $props();
</script> </script>
<svg style={style} xmlns="http://www.w3.org/2000/svg" version="1.1" width="24" height="24" viewBox="0 0 24 24"> <svg style={style} xmlns="http://www.w3.org/2000/svg" version="1.1" width="24" height="24" viewBox="0 0 24 24">

View File

@@ -1,20 +1,39 @@
<script lang="ts"> <script lang="ts">
export let highlight = false; let {
export let highlight_on_click = false highlight = $bindable(false),
export let red = false; highlight_on_click = false,
export let round = false; red = $bindable(false),
export let flat = false; round = false,
export let disabled = false; flat = false,
export let icon = "" disabled = false,
export let icon_small = false; icon = "",
export let label = "" icon_small = false,
export let title = null label = "",
export let link_href = "" title = null,
export let link_target = "_self" link_href = "",
export let click: (e?: MouseEvent) => void = null link_target = "_self",
export let style = null click = null,
export let type = null style = null,
export let form = null type = null,
form = null
}: {
highlight?: boolean;
highlight_on_click?: boolean;
red?: boolean;
round?: boolean;
flat?: boolean;
disabled?: boolean;
icon?: string;
icon_small?: boolean;
label?: string;
title?: any;
link_href?: string;
link_target?: string;
click?: (e?: MouseEvent) => void;
style?: any;
type?: any;
form?: any;
} = $props();
let click_int = (e: MouseEvent) => { let click_int = (e: MouseEvent) => {
if (highlight_on_click) { if (highlight_on_click) {
@@ -33,7 +52,7 @@ let click_int = (e: MouseEvent) => {
{#if link_href === ""} {#if link_href === ""}
<button <button
on:click={click_int} onclick={click_int}
class="button" class="button"
class:button_highlight={highlight} class:button_highlight={highlight}
class:button_red={red} class:button_red={red}
@@ -54,7 +73,7 @@ let click_int = (e: MouseEvent) => {
</button> </button>
{:else} {:else}
<a <a
on:click={click_int} onclick={click_int}
href="{link_href}" href="{link_href}"
target={link_target} target={link_target}
class="button" class="button"

View File

@@ -1,12 +1,22 @@
<script lang="ts"> <script lang="ts">
import { copy_text } from "util/Util.svelte"; import { copy_text } from "util/Util";
export let text = "" let {
export let style = "" text = "",
export let large_icon = false style = "",
export let small_icon = false large_icon = false,
let failed = false small_icon = false,
let success = false children
}: {
text?: string;
style?: string;
large_icon?: boolean;
small_icon?: boolean;
children?: import('svelte').Snippet;
} = $props();
let failed = $state(false)
let success = $state(false)
// Exported so it can be called manually // Exported so it can be called manually
export const copy = () => { export const copy = () => {
@@ -26,7 +36,7 @@ export const copy = () => {
</script> </script>
<button <button
on:click={copy} onclick={copy}
style={style} style={style}
class="button" class="button"
class:button_highlight={success} class:button_highlight={success}
@@ -43,7 +53,7 @@ export const copy = () => {
{:else if failed} {:else if failed}
Copy failed Copy failed
{:else} {:else}
<slot></slot> {@render children?.()}
{/if} {/if}
</span> </span>
</button> </button>

View File

@@ -1,5 +1,8 @@
<script lang="ts"> <script lang="ts">
let dialog: HTMLDialogElement let { children }: {
children?: import('svelte').Snippet;
} = $props();
let dialog: HTMLDialogElement = $state()
export const open = (button_rect: DOMRect) => { export const open = (button_rect: DOMRect) => {
// Show the window so we can get the location // Show the window so we can get the location
@@ -37,10 +40,10 @@ const click = (e: MouseEvent) => {
} }
</script> </script>
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y-no-noninteractive-element-interactions --> <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<dialog bind:this={dialog} on:click={click}> <dialog bind:this={dialog} onclick={click}>
<slot></slot> {@render children?.()}
</dialog> </dialog>
<style> <style>

View File

@@ -1,5 +1,7 @@
<script lang="ts"> <script lang="ts">
export let title = "" let { title = "" }: {
title?: string;
} = $props();
</script> </script>
<h1>{title}</h1> <h1>{title}</h1>

View File

@@ -8,10 +8,13 @@ import Reddit from "icons/Reddit.svelte";
import { formatDataVolumeBits } from "util/Formatting"; import { formatDataVolumeBits } from "util/Formatting";
import { get_endpoint, get_hostname } from "lib/PixeldrainAPI"; import { get_endpoint, get_hostname } from "lib/PixeldrainAPI";
export let nobg = false let { nobg = false }: {
let server_tx = 0 nobg?: boolean;
let cache_tx = 0 } = $props();
let storage_tx = 0
let server_tx = $state(0)
let cache_tx = $state(0)
let storage_tx = $state(0)
onMount(async () => { onMount(async () => {
try { try {
const resp = await fetch(get_endpoint()+"/misc/cluster_speed") const resp = await fetch(get_endpoint()+"/misc/cluster_speed")

View File

@@ -1,11 +1,15 @@
<script lang="ts"> <script lang="ts">
export let toggle = false; let {
toggle = $bindable(false)
}: {
toggle?: boolean;
} = $props();
</script> </script>
<button class="button small_button round" <button class="button small_button round"
class:button_highlight={toggle} class:button_highlight={toggle}
style="margin: 0;" style="margin: 0;"
on:click={() => toggle = !toggle} onclick={() => toggle = !toggle}
> >
<i class="icon">help</i> <i class="icon">help</i>
</button> </button>

View File

@@ -1,12 +1,19 @@
<script lang="ts"> <script lang="ts">
export let icon_href = "" let {
export let width = "750px" icon_href = "",
width = "750px",
children
}: {
icon_href?: string;
width?: string;
children?: import('svelte').Snippet;
} = $props();
</script> </script>
<div class="block" style="width: {width}; max-width: 100%"> <div class="block" style="width: {width}; max-width: 100%">
<img src={icon_href} alt="File icon" class="icon"> <img src={icon_href} alt="File icon" class="icon">
<div class="description"> <div class="description">
<slot></slot> {@render children?.()}
</div> </div>
</div> </div>

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { user } from "lib/UserStore"; import { user } from "lib/UserStore";
let nav: HTMLElement; let nav: HTMLElement = $state();
export const toggle = () => { export const toggle = () => {
var body = document.getElementById("page_body"); var body = document.getElementById("page_body");
@@ -21,7 +21,7 @@ export const reset = () => {
} }
</script> </script>
<button id="button_toggle_navigation" class="button_toggle_navigation" on:click={toggle}>☰</button> <button id="button_toggle_navigation" class="button_toggle_navigation" onclick={toggle}>☰</button>
<nav bind:this={nav} id="page_navigation" class="page_navigation"> <nav bind:this={nav} id="page_navigation" class="page_navigation">
<a href="/#">Home</a> <a href="/#">Home</a>
<a href="/#prepaid">For Creators</a> <a href="/#prepaid">For Creators</a>

View File

@@ -1,15 +1,24 @@
<script> <script lang="ts">
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import { formatDataVolume, formatDuration } from "util/Formatting"; import { formatDataVolume, formatDuration } from "util/Formatting";
import { stats } from "lib/StatsSocket" import { stats } from "lib/StatsSocket"
import TextBlock from "./TextBlock.svelte" import TextBlock from "./TextBlock.svelte"
import IconBlock from "./IconBlock.svelte"; import IconBlock from "./IconBlock.svelte";
import { user } from "lib/UserStore";
let dispatch = createEventDispatcher() let dispatch = createEventDispatcher()
export let file_size = 0 let {
export let file_name = "" file_size = 0,
export let file_type = "" file_name = "",
export let icon_href = "" file_type = "",
icon_href = ""
}: {
file_size?: number;
file_name?: string;
file_type?: string;
icon_href?: string;
} = $props();
</script> </script>
<TextBlock> <TextBlock>
@@ -41,7 +50,7 @@ export let icon_href = ""
<i class="icon">bolt</i> Get Premium <i class="icon">bolt</i> Get Premium
</a> </a>
and earn my eternal gratitude and earn my eternal gratitude
{#if !window.user_authenticated} {#if $user.username === undefined || $user.username === ""}
(you will need a <a href="/register">pixeldrain account</a> to (you will need a <a href="/register">pixeldrain account</a> to
receive the benefits) receive the benefits)
{/if} {/if}
@@ -57,7 +66,7 @@ export let icon_href = ""
<tr><td>Size</td><td>{formatDataVolume(file_size, 3)}</td></tr> <tr><td>Size</td><td>{formatDataVolume(file_size, 3)}</td></tr>
</tbody> </tbody>
</table> </table>
<button on:click={() => {dispatch("download")}}> <button onclick={() => {dispatch("download")}}>
<i class="icon">download</i> Download <i class="icon">download</i> Download
</button> </button>
</IconBlock> </IconBlock>

View File

@@ -1,15 +1,24 @@
<script lang="ts"> <script lang="ts">
export let field = "" let {
export let active_field = "" field = "",
export let asc = true active_field = "",
export let sort_func: (field: string) => void asc = true,
sort_func,
children
}: {
field?: string;
active_field?: string;
asc?: boolean;
sort_func: (field: string) => void;
children?: import('svelte').Snippet;
} = $props();
</script> </script>
<button on:click={() => sort_func(field)}> <button onclick={() => sort_func(field)}>
{#if active_field === field} {#if active_field === field}
{#if asc}{:else}{/if} {#if asc}{:else}{/if}
{/if} {/if}
<slot></slot> {@render children?.()}
</button> </button>
<style> <style>

View File

@@ -1,12 +1,13 @@
<script lang="ts" context="module"> <script lang="ts" module>
// svelte-ignore non_reactive_update
export enum FieldType { export enum FieldType {
Text, Text = 1,
Bytes, Bytes = 2,
Bits, Bits = 3,
Number, Number = 4,
Euro, Euro = 5,
HTML, HTML = 6,
Func, Func = 7,
} }
export type SortColumn = { export type SortColumn = {
field: string, field: string,
@@ -20,13 +21,21 @@ import SortButton from "layout/SortButton.svelte";
import Euro from "util/Euro.svelte"; import Euro from "util/Euro.svelte";
import { formatDataVolume, formatDataVolumeBits, formatThousands } from "util/Formatting"; import { formatDataVolume, formatDataVolumeBits, formatThousands } from "util/Formatting";
export let index_field = "" let {
export let rows = []; index_field = "",
export let columns: SortColumn[] = [] rows = $bindable([]),
export let sort_field = index_field columns = [],
export let totals = false sort_field = $bindable(index_field),
totals = false
}: {
index_field?: string;
rows?: any;
columns?: SortColumn[];
sort_field?: any;
totals?: boolean;
} = $props();
let asc = true let asc = $state(true)
const sort = (field: string) => { const sort = (field: string) => {
if (field !== "" && field === sort_field) { if (field !== "" && field === sort_field) {
asc = !asc asc = !asc

View File

@@ -1,10 +1,17 @@
<script> <script lang="ts">
export let width = "750px" let {
export let center = false width = "750px",
center = false,
children
}: {
width?: string;
center?: boolean;
children?: import('svelte').Snippet;
} = $props();
</script> </script>
<div class="block" class:center style="width: {width};"> <div class="block" class:center style="width: {width};">
<slot></slot> {@render children?.()}
</div> </div>
<style> <style>

View File

@@ -1,11 +1,23 @@
<script lang="ts"> <script lang="ts">
export let on = false let {
export let icon_on = "check" on = $bindable(),
export let icon_off = "close" icon_on = "check",
export let group_first = false icon_off = "close",
export let group_middle = false group_first = false,
export let group_last = false group_middle = false,
export let action = (e: MouseEvent) => {} group_last = false,
action,
children,
}: {
on?: boolean;
icon_on?: string;
icon_off?: string;
group_first?: boolean;
group_middle?: boolean;
group_last?: boolean;
action?: (e: MouseEvent) => void;
children?: import('svelte').Snippet;
} = $props();
const click = (e: MouseEvent) => { const click = (e: MouseEvent) => {
on = !on on = !on
@@ -16,7 +28,7 @@ const click = (e: MouseEvent) => {
</script> </script>
<button <button
on:click={click} onclick={click}
type="button" type="button"
class="button" class="button"
class:button_highlight={on} class:button_highlight={on}
@@ -29,7 +41,7 @@ const click = (e: MouseEvent) => {
{:else} {:else}
<i class="icon">{icon_off}</i> <i class="icon">{icon_off}</i>
{/if} {/if}
<slot></slot> {@render children?.()}
</button> </button>
<style> <style>

View File

@@ -1,19 +1,24 @@
<script lang="ts"> <script lang="ts">
import Dialog from "./Dialog.svelte"; import Dialog from "./Dialog.svelte";
let button: HTMLButtonElement let { label, children }: {
let dialog: Dialog label?: import('svelte').Snippet;
children?: import('svelte').Snippet;
} = $props();
let button: HTMLButtonElement = $state()
let dialog: Dialog = $state()
const open = () => dialog.open(button.getBoundingClientRect()) const open = () => dialog.open(button.getBoundingClientRect())
</script> </script>
<button bind:this={button} on:click={open} class="button round"> <button bind:this={button} onclick={open} class="button round">
<i class="icon">info</i><slot name="label"></slot> <i class="icon">info</i>{@render label?.()}
</button> </button>
<Dialog bind:this={dialog}> <Dialog bind:this={dialog}>
<div class="menu"> <div class="menu">
<slot></slot> {@render children?.()}
</div> </div>
</Dialog> </Dialog>

View File

@@ -8,7 +8,7 @@ import { get_misc_vat_rate, get_user } from "lib/PixeldrainAPI";
import { countries } from "country-data-list"; import { countries } from "country-data-list";
import PickProvider from "./PickProvider.svelte"; import PickProvider from "./PickProvider.svelte";
let state: CheckoutState = {country: null, provider: null, amount: 0, vat: 0, name: ""} let status: CheckoutState = $state({country: null, provider: null, amount: 0, vat: 0, name: ""})
onMount(async () => { onMount(async () => {
try { try {
@@ -17,21 +17,21 @@ onMount(async () => {
// Get the country from the user profile // Get the country from the user profile
if (user.checkout_country !== "" && countries[user.checkout_country] !== undefined) { if (user.checkout_country !== "" && countries[user.checkout_country] !== undefined) {
const vat_rate = await get_misc_vat_rate(user.checkout_country) const vat_rate = await get_misc_vat_rate(user.checkout_country)
state.vat = vat_rate.vat*100 status.vat = vat_rate.vat*100
state.country = countries[user.checkout_country] status.country = countries[user.checkout_country]
} }
// Get the provider from the user profile // Get the provider from the user profile
for (const p of payment_providers) { for (const p of payment_providers) {
if (p.name === user.checkout_provider) { if (p.name === user.checkout_provider) {
state.provider = p status.provider = p
break break
} }
} }
// Get the checkout name from the user profile // Get the checkout name from the user profile
if (user.checkout_name !== "") { if (user.checkout_name !== "") {
state.name = user.checkout_name status.name = user.checkout_name
} }
}catch (err) { }catch (err) {
alert("Failed to get user:"+err) alert("Failed to get user:"+err)
@@ -40,45 +40,45 @@ onMount(async () => {
</script> </script>
<div class="highlight_border"> <div class="highlight_border">
{#if state.country !== null} {#if status.country !== null}
<div class="navbar"> <div class="navbar">
{#if state.country !== null} {#if status.country !== null}
<button on:click={() => state.country = null}> <button onclick={() => status.country = null}>
Country: Country:
<span style="font-size: 1.5em; line-height: 1em;">{state.country.emoji}</span> <span style="font-size: 1.5em; line-height: 1em;">{status.country.emoji}</span>
{state.country.name} {status.country.name}
</button> </button>
{/if} {/if}
{#if state.provider !== null} {#if status.provider !== null}
<button on:click={() => state.provider = null}> <button onclick={() => status.provider = null}>
Provider: Provider:
<img <img
class="provider_icon" class="provider_icon"
src="/res/img/payment_providers/{state.provider.icon}.svg" src="/res/img/payment_providers/{status.provider.icon}.svg"
alt={state.provider.label} alt={status.provider.label}
title={state.provider.label}/> title={status.provider.label}/>
{state.provider.label} {status.provider.label}
</button> </button>
{/if} {/if}
{#if state.name !== ""} {#if status.name !== ""}
<button on:click={() => state.name = ""}> <button onclick={() => status.name = ""}>
Name: {state.name} Name: {status.name}
</button> </button>
{/if} {/if}
</div> </div>
<hr/> <hr/>
{/if} {/if}
{#if state.country === null} {#if status.country === null}
<PickCountry bind:state/> <PickCountry bind:status/>
{:else if state.provider === null} {:else if status.provider === null}
<PickProvider bind:state/> <PickProvider bind:status/>
{:else if state.provider.need_name === true && state.name === ""} {:else if status.provider.need_name === true && status.name === ""}
<PickName bind:state/> <PickName bind:status/>
{:else} {:else}
<PickAmount bind:state/> <PickAmount bind:status/>
{/if} {/if}
</div> </div>

View File

@@ -1,27 +1,30 @@
<script lang="ts"> <script lang="ts">
import { preventDefault } from 'svelte/legacy';
import Euro from "util/Euro.svelte"; import Euro from "util/Euro.svelte";
import { checkout, type CheckoutState } from "./CheckoutLib"; import { checkout, type CheckoutState } from "./CheckoutLib";
import { loading_finish, loading_start } from "lib/Loading"; import { loading_finish, loading_start } from "lib/Loading";
export let state: CheckoutState let { status = $bindable() }: {
status: CheckoutState;
} = $props();
const amounts = [10, 20, 50, 100, 200, 500, 1000, 2000, 5000] const amounts = [10, 20, 50, 100, 200, 500, 1000, 2000, 5000]
let amount = 20 let amount = $state(20)
const submit = async (e: SubmitEvent) => { const submit = async (e: SubmitEvent) => {
e.preventDefault() e.preventDefault()
state.amount = amount status.amount = amount
loading_start() loading_start()
try { try {
await checkout(state) await checkout(status)
}finally { }finally {
loading_finish() loading_finish()
} }
} }
</script> </script>
{#if state.provider.crypto === true} {#if status.provider.crypto === true}
<p style="text-align: initial;" class="highlight_blue"> <p style="text-align: initial;" class="highlight_blue">
When paying with cryptocurrencies it is important that you pay When paying with cryptocurrencies it is important that you pay
the <b>exact amount</b> stated on the order. If you pay too the <b>exact amount</b> stated on the order. If you pay too
@@ -33,11 +36,11 @@ const submit = async (e: SubmitEvent) => {
</p> </p>
{/if} {/if}
<form class="amount_grid" on:submit={submit}> <form class="amount_grid" onsubmit={submit}>
<div class="span3">Please choose an amount</div> <div class="span3">Please choose an amount</div>
{#each amounts as a} {#each amounts as a}
<button <button
on:click|preventDefault={() => amount = a} onclick={preventDefault(() => amount = a)}
class="amount_button" class="amount_button"
class:button_highlight={amount === a} class:button_highlight={amount === a}
> >
@@ -52,7 +55,7 @@ const submit = async (e: SubmitEvent) => {
<div class="span2" style="text-align: initial;"> <div class="span2" style="text-align: initial;">
Total including VAT: Total including VAT:
<Euro amount={(amount*1e6) + (amount*1e6)*(state.vat/100)}/> <Euro amount={(amount*1e6) + (amount*1e6)*(status.vat/100)}/>
</div> </div>
<button type="submit" class="button_highlight"> <button type="submit" class="button_highlight">

View File

@@ -1,11 +1,14 @@
<script lang="ts"> <script lang="ts">
import { preventDefault } from 'svelte/legacy';
import { countries } from "country-data-list"; import { countries } from "country-data-list";
import { get_misc_vat_rate, put_user } from "lib/PixeldrainAPI"; import { get_misc_vat_rate, put_user } from "lib/PixeldrainAPI";
import { payment_providers, type CheckoutState } from "./CheckoutLib"; import { payment_providers, type CheckoutState } from "./CheckoutLib";
import { loading_finish, loading_start } from "lib/Loading"; import { loading_finish, loading_start } from "lib/Loading";
export let state: CheckoutState let { status = $bindable() }: {
let country_input = "" status: CheckoutState;
} = $props();
let country_input = $state("")
const set_country = async (e?: Event) => { const set_country = async (e?: Event) => {
if (e !== undefined) { if (e !== undefined) {
@@ -21,7 +24,7 @@ const set_country = async (e?: Event) => {
// We always clear the provider field after picking a country, as providers // We always clear the provider field after picking a country, as providers
// vary per country. If we did not clear the provider then we may end up // vary per country. If we did not clear the provider then we may end up
// with an invalid combination // with an invalid combination
state.provider = null status.provider = null
loading_start() loading_start()
try { try {
@@ -29,8 +32,8 @@ const set_country = async (e?: Event) => {
await put_user({checkout_country: c.alpha3}) await put_user({checkout_country: c.alpha3})
const vat_rate = await get_misc_vat_rate(c.alpha3) const vat_rate = await get_misc_vat_rate(c.alpha3)
state.vat = vat_rate.vat*100 status.vat = vat_rate.vat*100
state.country = c status.country = c
} catch (err) { } catch (err) {
alert(err) alert(err)
} finally { } finally {
@@ -56,7 +59,7 @@ const format_country = (c: typeof countries.all[0]) => {
Please pick your country of residence Please pick your country of residence
</div> </div>
<div> <div>
<form on:submit|preventDefault={set_country} class="country_form"> <form onsubmit={preventDefault(set_country)} class="country_form">
<div class="country_search"> <div class="country_search">
<datalist id="country_picker"> <datalist id="country_picker">
{#each countries.all.filter(c => c.status === "assigned") as c} {#each countries.all.filter(c => c.status === "assigned") as c}
@@ -74,7 +77,7 @@ const format_country = (c: typeof countries.all[0]) => {
Continue Continue
</button> </button>
</div> </div>
<select bind:value={country_input} on:dblclick={set_country} style="padding: 0;" size="10"> <select bind:value={country_input} ondblclick={set_country} style="padding: 0;" size="10">
{#each countries.all.filter(c => c.status === "assigned") as c} {#each countries.all.filter(c => c.status === "assigned") as c}
<option value={c.alpha2}>{format_country(c)}</option> <option value={c.alpha2}>{format_country(c)}</option>
{/each} {/each}

View File

@@ -2,8 +2,11 @@
import { put_user } from "lib/PixeldrainAPI"; import { put_user } from "lib/PixeldrainAPI";
import { type CheckoutState } from "./CheckoutLib"; import { type CheckoutState } from "./CheckoutLib";
export let state: CheckoutState let { status = $bindable() }: {
let name: string status: CheckoutState;
} = $props();
let name: string = $state()
const submit = async (e: SubmitEvent) => { const submit = async (e: SubmitEvent) => {
e.preventDefault() e.preventDefault()
@@ -14,11 +17,11 @@ const submit = async (e: SubmitEvent) => {
alert("Failed to update user:"+ err) alert("Failed to update user:"+ err)
} }
state.name = name status.name = name
} }
</script> </script>
<form class="name_form" on:submit={submit}> <form class="name_form" onsubmit={submit}>
<div> <div>
This payment provider requires a name for checkout. Please enter your This payment provider requires a name for checkout. Please enter your
full name below full name below

View File

@@ -2,7 +2,9 @@
import { put_user } from "lib/PixeldrainAPI"; import { put_user } from "lib/PixeldrainAPI";
import { payment_providers, type CheckoutState, type PaymentProvider } from "./CheckoutLib"; import { payment_providers, type CheckoutState, type PaymentProvider } from "./CheckoutLib";
export let state: CheckoutState let { status = $bindable() }: {
status: CheckoutState;
} = $props();
const set_provider = async (p: PaymentProvider) => { const set_provider = async (p: PaymentProvider) => {
try { try {
@@ -11,7 +13,7 @@ const set_provider = async (p: PaymentProvider) => {
alert("Failed to update user:"+ err) alert("Failed to update user:"+ err)
} }
state.provider = p status.provider = p
} }
</script> </script>
@@ -19,8 +21,8 @@ const set_provider = async (p: PaymentProvider) => {
<div class="providers"> <div class="providers">
{#each payment_providers as p (p.name)} {#each payment_providers as p (p.name)}
{#if p.country_filter === undefined || p.country_filter.includes(state.country.alpha2)} {#if p.country_filter === undefined || p.country_filter.includes(status.country.alpha2)}
<button on:click={() => set_provider(p)}> <button onclick={() => set_provider(p)}>
<img src="/res/img/payment_providers/{p.icon}.svg" alt={p.label} title={p.label}/> <img src="/res/img/payment_providers/{p.icon}.svg" alt={p.label} title={p.label}/>
{p.label} {p.label}
</button> </button>

View File

@@ -45,7 +45,7 @@ const form_otp: FormConfig = {
} }
// The currently rendered form // The currently rendered form
let form: FormConfig = form_login let form: FormConfig = $state(form_login)
let username = "" let username = ""
let password = "" let password = ""

View File

@@ -6,15 +6,15 @@ const finish_login = async () => {
location.reload() location.reload()
} }
let page = "login" let page = $state("login")
</script> </script>
<div class="tab_bar"> <div class="tab_bar">
<button on:click={() => page = "login"} class:button_highlight={page === "login"}> <button onclick={() => page = "login"} class:button_highlight={page === "login"}>
<i class="icon small">login</i> <i class="icon small">login</i>
<span>Login</span> <span>Login</span>
</button> </button>
<button on:click={() => page = "register"} class:button_highlight={page === "register"}> <button onclick={() => page = "register"} class:button_highlight={page === "register"}>
<i class="icon small">how_to_reg</i> <i class="icon small">how_to_reg</i>
<span>Register</span> <span>Register</span>
</button> </button>

View File

@@ -49,7 +49,7 @@ onMount(() => {
}); });
}) })
let stylesheet_2: HTMLLinkElement let stylesheet_2: HTMLLinkElement = $state()
const reload_sheet = () => { const reload_sheet = () => {
let stylesheet1 = document.getElementById("stylesheet_theme") as HTMLLinkElement let stylesheet1 = document.getElementById("stylesheet_theme") as HTMLLinkElement

View File

@@ -5,8 +5,8 @@ import { formatDataVolume, formatDataVolumeBits } from "util/Formatting";
import ProgressBar from "util/ProgressBar.svelte"; import ProgressBar from "util/ProgressBar.svelte";
import CopyButton from "layout/CopyButton.svelte"; import CopyButton from "layout/CopyButton.svelte";
let running = false let running = $state(false)
let data_received = 0 let data_received = $state(0)
const update_interval = 100 const update_interval = 100
const start = async (dur = 10000) => { const start = async (dur = 10000) => {
if (running) { if (running) {
@@ -39,7 +39,7 @@ const start = async (dur = 10000) => {
running = false running = false
} }
let latency = 0 let latency = $state(0)
const measure_latency = async () => { const measure_latency = async () => {
// We request one byte from the server ten times. If the latency of one // We request one byte from the server ten times. If the latency of one
// request is lower than the last one then that latency is saved in the // request is lower than the last one then that latency is saved in the
@@ -68,12 +68,12 @@ const measure_latency = async () => {
} }
// Average speed for the whole test // Average speed for the whole test
let speed = 0 let speed = $state(0)
let result_link = "" let result_link = $state("")
let server = "" let server = $state("")
let progress_duration = 0 let progress_duration = $state(0)
let progress_unchanged = 0 let progress_unchanged = $state(0)
const measure_speed = (stop, test_duration) => { const measure_speed = (stop, test_duration) => {
speed = 0 speed = 0

View File

@@ -1,9 +1,10 @@
<script> <script>
import { preventDefault } from 'svelte/legacy';
import { loading_finish, loading_start } from "lib/Loading"; import { loading_finish, loading_start } from "lib/Loading";
import { formatDate } from "util/Formatting"; import { formatDate } from "util/Formatting";
let loaded = false let loaded = $state(false)
let rows = [] let rows = $state([])
const load_keys = async () => { const load_keys = async () => {
loading_start() loading_start()
@@ -93,14 +94,14 @@ const logout = async (key) => {
full control over your account. Do not show your API keys to full control over your account. Do not show your API keys to
someone or something you don't trust! someone or something you don't trust!
</p> </p>
<button class="button_red" on:click={load_keys}> <button class="button_red" onclick={load_keys}>
<i class="icon">lock_open</i> Show API keys <i class="icon">lock_open</i> Show API keys
</button> </button>
</div> </div>
{:else} {:else}
<div class="toolbar" style="text-align: left;"> <div class="toolbar" style="text-align: left;">
<div class="toolbar_spacer"></div> <div class="toolbar_spacer"></div>
<button on:click={create_key}> <button onclick={create_key}>
<i class="icon">add</i> Create new API key <i class="icon">add</i> Create new API key
</button> </button>
</div> </div>
@@ -134,7 +135,7 @@ const logout = async (key) => {
<td>{formatDate(row.last_used_time, true, true, false)}</td> <td>{formatDate(row.last_used_time, true, true, false)}</td>
<td>{row.creation_ip_address}</td> <td>{row.creation_ip_address}</td>
<td> <td>
<button on:click|preventDefault={() => {logout(row.auth_key)}} class="button button_red round"> <button onclick={preventDefault(() => {logout(row.auth_key)})} class="button button_red round">
<i class="icon">delete</i> <i class="icon">delete</i>
</button> </button>
</td> </td>

View File

@@ -4,10 +4,10 @@ import CopyButton from "layout/CopyButton.svelte";
import Form from "util/Form.svelte"; import Form from "util/Form.svelte";
import Button from "layout/Button.svelte"; import Button from "layout/Button.svelte";
import OtpSetup from "./OTPSetup.svelte"; import OtpSetup from "./OTPSetup.svelte";
import { put_user } from "lib/PixeldrainAPI"; import { put_user } from "lib/PixeldrainAPI";
let affiliate_link = window.location.protocol+"//"+window.location.host + "?ref=" + encodeURIComponent(window.user.username) let affiliate_link = window.location.protocol+"//"+window.location.host + "?ref=" + encodeURIComponent(window.user.username)
let affiliate_deny = false let affiliate_deny = $state(false)
onMount(() => { onMount(() => {
affiliate_deny = localStorage.getItem("affiliate_deny") === "1" affiliate_deny = localStorage.getItem("affiliate_deny") === "1"
}) })

View File

@@ -5,8 +5,8 @@ import { formatDate } from "util/Formatting";
let year = 0 let year = 0
let month = 0 let month = 0
let month_str = "" let month_str = $state("")
let data = [] let data = $state([])
const load_activity = async () => { const load_activity = async () => {
loading_start() loading_start()
@@ -66,12 +66,12 @@ onMount(() => {
<h3>{month_str}</h3> <h3>{month_str}</h3>
<div class="toolbar"> <div class="toolbar">
<button on:click={last_month}> <button onclick={last_month}>
<i class="icon">chevron_left</i> <i class="icon">chevron_left</i>
Previous month Previous month
</button> </button>
<div class="toolbar_spacer"></div> <div class="toolbar_spacer"></div>
<button on:click={next_month}> <button onclick={next_month}>
Next month Next month
<i class="icon">chevron_right</i> <i class="icon">chevron_right</i>
</button> </button>
@@ -118,12 +118,12 @@ onMount(() => {
{#if data.length > 100} {#if data.length > 100}
<div class="toolbar"> <div class="toolbar">
<button on:click={last_month}> <button onclick={last_month}>
<i class="icon">chevron_left</i> <i class="icon">chevron_left</i>
Previous month Previous month
</button> </button>
<div class="toolbar_spacer"></div> <div class="toolbar_spacer"></div>
<button on:click={next_month}> <button onclick={next_month}>
Next month Next month
<i class="icon">chevron_right</i> <i class="icon">chevron_right</i>
</button> </button>

View File

@@ -2,13 +2,16 @@
import { onMount } from "svelte"; import { onMount } from "svelte";
import Modal from "util/Modal.svelte"; import Modal from "util/Modal.svelte";
import { get_user, put_user } from "lib/PixeldrainAPI"; import { get_user, put_user } from "lib/PixeldrainAPI";
import { loading_finish, loading_start } from "lib/Loading"; import { loading_finish, loading_start } from "lib/Loading";
// When the always flag is set then the pop-up will also show if the user let { always = false }: {
// already has an affiliate ID set // When the always flag is set then the pop-up will also show if the user
export let always = false // already has an affiliate ID set
let modal: Modal always?: boolean;
let referral: string } = $props();
let modal: Modal = $state()
let referral: string = $state()
let shown = false let shown = false
export const prompt = async (ref: string) => { export const prompt = async (ref: string) => {
@@ -105,10 +108,10 @@ const deny = () => {
choose 'Deny' then we will never show this pop-up again. choose 'Deny' then we will never show this pop-up again.
</p> </p>
<div class="buttons"> <div class="buttons">
<button class="button button_red" on:click={e => deny()}> <button class="button button_red" onclick={e => deny()}>
Deny Deny
</button> </button>
<button class="button button_highlight" on:click={e => allow()}> <button class="button button_highlight" onclick={e => allow()}>
Accept Accept
</button> </button>
</div> </div>

View File

@@ -1,15 +1,16 @@
<script> <script>
import { preventDefault } from 'svelte/legacy';
import { onMount } from "svelte"; import { onMount } from "svelte";
import Pro from "icons/Pro.svelte"; import Pro from "icons/Pro.svelte";
import { formatDataVolume } from "util/Formatting"; import { formatDataVolume } from "util/Formatting";
import ProgressBar from "util/ProgressBar.svelte"; import ProgressBar from "util/ProgressBar.svelte";
import SuccessMessage from "util/SuccessMessage.svelte"; import SuccessMessage from "util/SuccessMessage.svelte";
import { loading_finish, loading_start } from "lib/Loading"; import { loading_finish, loading_start } from "lib/Loading";
let success_message let success_message = $state()
let hotlinking = window.user.hotlinking_enabled let hotlinking = $state(window.user.hotlinking_enabled)
let transfer_cap = window.user.monthly_transfer_cap / 1e12 let transfer_cap = $state(window.user.monthly_transfer_cap / 1e12)
let skip_viewer = window.user.skip_file_viewer let skip_viewer = $state(window.user.skip_file_viewer)
const update = async () => { const update = async () => {
const form = new FormData() const form = new FormData()
@@ -44,7 +45,7 @@ let toggle_hotlinking = () => {
update() update()
} }
let transfer_used = 0 let transfer_used = $state(0)
let load_transfer_used = () => { let load_transfer_used = () => {
let today = new Date() let today = new Date()
let start = new Date() let start = new Date()
@@ -80,7 +81,7 @@ onMount(() => {
<h2><Pro/>Hotlinking (bandwidth sharing)</h2> <h2><Pro/>Hotlinking (bandwidth sharing)</h2>
<SuccessMessage bind:this={success_message}></SuccessMessage> <SuccessMessage bind:this={success_message}></SuccessMessage>
<button on:click={toggle_hotlinking}> <button onclick={toggle_hotlinking}>
{#if hotlinking} {#if hotlinking}
<i class="icon green">check</i> ON (click to turn off) <i class="icon green">check</i> ON (click to turn off)
{:else} {:else}
@@ -101,7 +102,7 @@ onMount(() => {
Billshock limit in terabytes per month (1 TB = 1000 GB). Set to 0 to Billshock limit in terabytes per month (1 TB = 1000 GB). Set to 0 to
disable. disable.
</p> </p>
<form on:submit|preventDefault={update} class="billshock_container"> <form onsubmit={preventDefault(update)} class="billshock_container">
<input type="number" bind:value={transfer_cap} step="0.1" min="0" style="width: 5em;"/> <input type="number" bind:value={transfer_cap} step="0.1" min="0" style="width: 5em;"/>
<div style="margin: 0.5em;">TB</div> <div style="margin: 0.5em;">TB</div>
<button type="submit"> <button type="submit">
@@ -123,7 +124,7 @@ onMount(() => {
</p> </p>
<h2><Pro/>Skip download page</h2> <h2><Pro/>Skip download page</h2>
<button on:click={toggle_skip_viewer}> <button onclick={toggle_skip_viewer}>
{#if skip_viewer} {#if skip_viewer}
<i class="icon green">check</i> ON (click to turn off) <i class="icon green">check</i> ON (click to turn off)
{:else} {:else}

View File

@@ -3,8 +3,8 @@ import { onMount } from "svelte";
import CopyButton from "layout/CopyButton.svelte"; import CopyButton from "layout/CopyButton.svelte";
import { loading_finish, loading_start } from "lib/Loading"; import { loading_finish, loading_start } from "lib/Loading";
let app_name = "" let app_name = $state("")
let api_key = "" let api_key = $state("")
const create_key = async () => { const create_key = async () => {
loading_start() loading_start()
try { try {
@@ -30,7 +30,7 @@ const create_key = async () => {
} }
} }
let show_key = "" let show_key = $state("")
const toggle_show_key = () => { const toggle_show_key = () => {
if (show_key === "") { if (show_key === "") {
show_key = api_key show_key = api_key
@@ -90,7 +90,7 @@ onMount(() => {
{#if !api_key} {#if !api_key}
<div class="center"> <div class="center">
<button class="button_highlight" on:click={create_key}> <button class="button_highlight" onclick={create_key}>
<i class="icon">add</i> <i class="icon">add</i>
Generate key Generate key
</button> </button>
@@ -100,7 +100,7 @@ onMount(() => {
<div class="copy_container"> <div class="copy_container">
<CopyButton text={api_key}>Copy key to clipboard</CopyButton> <CopyButton text={api_key}>Copy key to clipboard</CopyButton>
<button on:click={toggle_show_key} class="copy_button" class:button_highlight={show_key !== ""}> <button onclick={toggle_show_key} class="copy_button" class:button_highlight={show_key !== ""}>
<i class="icon">visibility</i> <i class="icon">visibility</i>
{#if show_key === ""} {#if show_key === ""}
Show key Show key

View File

@@ -37,8 +37,8 @@ const checkout = async (network = "", amount = 0, country = "") => {
} }
} }
let invoices = [] let invoices = $state([])
let unpaid_invoice = 0 let unpaid_invoice = $state(0)
const load_invoices = async () => { const load_invoices = async () => {
loading_start() loading_start()
try { try {

View File

@@ -1,13 +1,14 @@
<script> <script>
import { preventDefault } from 'svelte/legacy';
import { onMount } from "svelte"; import { onMount } from "svelte";
import Persistence from "icons/Persistence.svelte"; import Persistence from "icons/Persistence.svelte";
import SuccessMessage from "util/SuccessMessage.svelte"; import SuccessMessage from "util/SuccessMessage.svelte";
import { loading_finish, loading_start } from "lib/Loading"; import { loading_finish, loading_start } from "lib/Loading";
let success_message let success_message = $state()
// Embedding settings // Embedding settings
let embed_domains = "" let embed_domains = $state("")
const save_embed = async () => { const save_embed = async () => {
const form = new FormData() const form = new FormData()
@@ -63,7 +64,7 @@ onMount(() => {
space. Like this: 'pixeldrain.com google.com twitter.com' space. Like this: 'pixeldrain.com google.com twitter.com'
</p> </p>
Domain names:<br/> Domain names:<br/>
<form class="form_row" on:submit|preventDefault={save_embed}> <form class="form_row" onsubmit={preventDefault(save_embed)}>
<input class="grow" bind:value={embed_domains} type="text"/> <input class="grow" bind:value={embed_domains} type="text"/>
<button class="shrink" action="submit"><i class="icon">save</i> Save</button> <button class="shrink" action="submit"><i class="icon">save</i> Save</button>
</form> </form>

View File

@@ -1,11 +1,14 @@
<script> <script lang="ts">
import ProgressBar from "util/ProgressBar.svelte"; import ProgressBar from "util/ProgressBar.svelte";
import { formatDataVolume } from "util/Formatting" import { formatDataVolume } from "util/Formatting"
import { user } from "lib/UserStore";
export let total = 0 let { total = 0, used = 0 }: {
export let used = 0 total?: number;
used?: number;
} = $props();
$: frac = used / total let frac = $derived(used / total)
</script> </script>
<ProgressBar total={total} used={used}></ProgressBar> <ProgressBar total={total} used={used}></ProgressBar>
@@ -31,7 +34,7 @@ $: frac = used / total
files have a daily download limit and hotlinking is disabled. files have a daily download limit and hotlinking is disabled.
</p> </p>
{#if window.user.monthly_transfer_cap > 0} {#if $user.monthly_transfer_cap > 0}
<p> <p>
You have a billshock limit configured. <a You have a billshock limit configured. <a
href="/user/sharing/bandwidth">increase or disable the limit</a> to href="/user/sharing/bandwidth">increase or disable the limit</a> to
@@ -53,7 +56,7 @@ $: frac = used / total
and hotlinking is disabled. and hotlinking is disabled.
</p> </p>
{#if window.user.monthly_transfer_cap > 0} {#if $user.monthly_transfer_cap > 0}
<p> <p>
You have a billshock limit configured. <a You have a billshock limit configured. <a
href="/user/sharing/bandwidth">increase or disable the limit</a> to href="/user/sharing/bandwidth">increase or disable the limit</a> to

View File

@@ -5,11 +5,11 @@ import CopyButton from "layout/CopyButton.svelte";
import ToggleButton from "layout/ToggleButton.svelte"; import ToggleButton from "layout/ToggleButton.svelte";
import { check_response, get_endpoint, get_user, type User } from "lib/PixeldrainAPI"; import { check_response, get_endpoint, get_user, type User } from "lib/PixeldrainAPI";
let user: User = null let user: User = $state(null)
let secret = "" let secret = $state("")
let uri = "" let uri = $state("")
let qr = "" let qr = $state("")
let reveal_key = false let reveal_key = $state(false)
const generate_key = async () => { const generate_key = async () => {
try { try {
let form = new FormData() let form = new FormData()
@@ -28,7 +28,7 @@ const generate_key = async () => {
} }
} }
let otp = "" let otp = $state("")
const verify = async (e: SubmitEvent) => { const verify = async (e: SubmitEvent) => {
e.preventDefault() e.preventDefault()
@@ -133,7 +133,7 @@ onMount(async () => {
app is working properly. This step enables two-factor authentication app is working properly. This step enables two-factor authentication
on your account. on your account.
</p> </p>
<form id="otp_verify" on:submit={verify} class="otp_form"> <form id="otp_verify" onsubmit={verify} class="otp_form">
<input bind:value={otp} type="text" autocomplete="one-time-code" pattern={"[0-9]{6}"} required> <input bind:value={otp} type="text" autocomplete="one-time-code" pattern={"[0-9]{6}"} required>
<Button form="otp_verify" label="Verify OTP"/> <Button form="otp_verify" label="Verify OTP"/>
</form> </form>

View File

@@ -1,9 +1,9 @@
<script> <script>
import { onMount } from "svelte"; import { onMount } from "svelte";
let patreon_result = "" let patreon_result = $state("")
let patreon_message = "" let patreon_message = $state("")
let patreon_error = "" let patreon_error = $state("")
onMount(() => { onMount(() => {
if (window.location.hash.startsWith("#")) { if (window.location.hash.startsWith("#")) {
@@ -57,7 +57,7 @@ onMount(() => {
cause issues with the authentication process. Try linking your cause issues with the authentication process. Try linking your
subscription in <a subscription in <a
href="https://www.mozilla.org/firefox/">Firefox</a> for example. href="https://www.mozilla.org/firefox/">Firefox</a> for example.
<p/> </p>
<p> <p>
If the information above does not solve your issue then please If the information above does not solve your issue then please
contact me on <a contact me on <a

Some files were not shown because too many files have changed in this diff Show More