Convert multiple pages into SPA

This commit is contained in:
2025-10-09 15:48:23 +02:00
parent c616b2da7f
commit 06d04a1abc
110 changed files with 1245 additions and 1319 deletions

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { fs_encode_path, node_is_shared } from "./FilesystemAPI";
import { fs_encode_path, node_is_shared } from "lib/FilesystemAPI";
import type { FSNavigator } from "./FSNavigator";
export let nav: FSNavigator
@@ -9,8 +9,7 @@ export let nav: FSNavigator
{#each $nav.path as node, i (node.path)}
<a
href={"/d"+fs_encode_path(node.path)}
class="breadcrumb button"
class:button_highlight={$nav.base_index === i}
class="breadcrumb button flat"
on:click|preventDefault={() => {nav.navigate(node.path, true)}}
>
{#if node.abuse_type !== undefined}
@@ -22,19 +21,24 @@ export let nav: FSNavigator
{node.name}
</div>
</a>
{#if $nav.base_index !== i}
<i class="icon">chevron_right</i>
{/if}
{/each}
</div>
<style>
.breadcrumbs {
flex-grow: 1;
flex-shrink: 1;
flex: 0 0 auto;
display: flex;
align-items: center;
justify-content: center;
justify-content: left;
flex-wrap: wrap;
flex-direction: row;
overflow: hidden;
background: var(--shaded_background);
backdrop-filter: blur(4px);
border-bottom: 1px solid var(--separator);
}
.breadcrumb {
min-width: 1em;
@@ -42,6 +46,8 @@ export let nav: FSNavigator
word-break: break-all;
display: inline-flex;
flex-direction: row;
background-color: unset;
box-shadow: none;
}
.node_name {
max-width: 20vw;

View File

@@ -2,7 +2,7 @@
import Chart from "util/Chart.svelte";
import { formatDataVolume, formatDate, formatThousands } from "util/Formatting";
import Modal from "util/Modal.svelte";
import { fs_path_url, fs_share_hotlink_url, fs_share_url, fs_timeseries, type FSNode } from "./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 { tick } from "svelte";
import CopyButton from "layout/CopyButton.svelte";

View File

@@ -1,6 +1,6 @@
import { fs_get_node, fs_encode_path, fs_split_path } from "./FilesystemAPI";
import type { FSNode, FSPath, FSPermissions, FSContext } from "./FilesystemAPI";
import type { Writable } from "svelte/store"
import { loading_finish, loading_start } from "lib/Loading";
import { fs_get_node, fs_encode_path, fs_split_path } from "../lib/FilesystemAPI";
import type { FSNode, FSPath, FSPermissions, FSContext } from "../lib/FilesystemAPI";
export class FSNavigator {
// Parts of the raw API response
@@ -22,27 +22,16 @@ export class FSNavigator {
constructor(history_enabled = true) {
this.history_enabled = history_enabled
// If history logging is enabled we capture the popstate event, which
// fires when the user uses the back and forward buttons in the browser.
// Instead of reloading the page we use the navigator to navigate to the
// new page
if (history_enabled) {
window.addEventListener("popstate", () => {
// Get the part of the URL after the fs root and navigate to it
const path = document.location.pathname.replace("/d/", "")
this.navigate(decodeURIComponent(path), false)
})
}
}
// If you set the loading property to a boolean writable store the navigator
// will use it to publish its loading states
loading: Writable<boolean> | null = null
set_loading = (b: boolean) => {
if (this.loading !== null) {
this.loading.set(b)
}
// The popstate event can be used to listen for navigation events. Register
// this event listener on the <svelte:window> in the parent element. When
// the user presses the back or forward buttons in the browser we'll catch
// the event and navigate to the proper directory
popstate = (e: PopStateEvent) => {
// Get the part of the URL after the fs root and navigate to it
const path = window.location.pathname.replace(/^\/d/, "")
this.navigate(decodeURI(path), false)
}
// The FSNavigator acts as a svelte store. This allows for DOM reactivity.
@@ -72,7 +61,7 @@ export class FSNavigator {
console.debug("Navigating to path", path, push_history)
try {
this.set_loading(true)
loading_start()
const resp = await fs_get_node(path)
this.open_node(resp, push_history)
} catch (err: any) {
@@ -89,7 +78,7 @@ export class FSNavigator {
alert("Error: " + err)
}
} finally {
this.set_loading(false)
loading_finish()
}
}
@@ -108,7 +97,7 @@ export class FSNavigator {
// we still replace the URL with replaceState. This way the user is not
// greeted to a 404 page when refreshing after renaming a file
if (this.history_enabled) {
window.document.title = node.path[node.base_index].name + " ~ pixeldrain"
window.document.title = node.path[node.base_index].name + " / FNX"
const url = "/d" + fs_encode_path(node.path[node.base_index].path) + window.location.hash
if (push_history) {
window.history.pushState({}, window.document.title, url)
@@ -189,14 +178,14 @@ export class FSNavigator {
let siblings: Array<FSNode>
try {
this.set_loading(true)
loading_start()
siblings = await this.get_siblings()
} catch (err) {
console.error(err)
alert(err)
return
} finally {
this.set_loading(false)
loading_finish()
}
let next_sibling: FSNode | null = null

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { onMount } from "svelte";
import { formatDataVolume, formatThousands } from "util/Formatting"
import { fs_path_url } from "./FilesystemAPI";
import { fs_path_url } from "lib/FilesystemAPI";
import type { FSNavigator } from "./FSNavigator";
export let nav: FSNavigator
@@ -104,7 +104,7 @@ const close_socket = () => {
</div>
<div class="group">
<div class="label">Transfer used</div>
<div class="label">Egress</div>
<div class="stat">
{loading ? "Loading..." : formatDataVolume(transfer_used, 3)}
</div>
@@ -140,18 +140,11 @@ const close_socket = () => {
text-align: center;
}
.label {
padding-left: 0.5em;
text-align: left;
text-align: center;
font-size: 0.8em;
line-height: 1em;
}
.stat {
line-height: 1.2em;
}
@media (max-width: 1000px) {
.label {
text-align: center;
padding-left: 0;
}
}
</style>

View File

@@ -1,20 +1,17 @@
<script lang="ts">
import { onMount } from "svelte";
import LoadingIndicator from "util/LoadingIndicator.svelte";
import EditWindow from "./edit_window/EditWindow.svelte";
import Toolbar from "./Toolbar.svelte";
import Breadcrumbs from "./Breadcrumbs.svelte";
import DetailsWindow from "./DetailsWindow.svelte";
import FilePreview from "./viewers/FilePreview.svelte";
import FSUploadWidget from "./upload_widget/FSUploadWidget.svelte";
import { fs_download, type FSPath } from "./FilesystemAPI";
import Menu from "./Menu.svelte";
import { fs_download, type FSPath } from "lib/FilesystemAPI";
import { FSNavigator } from "./FSNavigator"
import { writable } from "svelte/store";
import { css_from_path } from "filesystem/edit_window/Branding";
import AffiliatePrompt from "user_home/AffiliatePrompt.svelte";
import { current_page_store } from "wrap/RouterStore";
let file_viewer: HTMLDivElement
let file_preview: FilePreview
let toolbar: Toolbar
let upload_widget: FSUploadWidget
@@ -22,25 +19,37 @@ let details_visible = false
let edit_window: EditWindow
let edit_visible = false
const loading = writable(true)
const nav = new FSNavigator(true)
onMount(() => {
nav.loading = loading
nav.open_node((window as any).initial_node as FSPath, false)
if ((window as any).intial_node !== undefined) {
console.debug("Loading initial node")
nav.open_node((window as any).initial_node as FSPath, false)
} else {
console.debug("No initial node, fetching path", window.location.pathname)
nav.navigate(decodeURI(window.location.pathname).replace(/^\/d/, ""), false)
}
const page_sub = current_page_store.subscribe(() => {
console.debug("Caught page transition to", window.location.pathname)
nav.navigate(decodeURI(window.location.pathname).replace(/^\/d/, ""), false)
})
// Subscribe to navigation updates. This function returns a deconstructor
// which we can conveniently return from our mount function as well
return nav.subscribe(nav => {
const nav_sub = nav.subscribe(nav => {
if (!nav.initialized) {
return
}
// Custom CSS rules for the whole viewer
document.documentElement.style = css_from_path(nav.path)
loading.set(false)
})
return () => {
page_sub()
nav_sub()
document.documentElement.style = ""
}
})
const keydown = (e: KeyboardEvent) => {
@@ -127,50 +136,41 @@ const keydown = (e: KeyboardEvent) => {
<svelte:window on:keydown={keydown} />
<div bind:this={file_viewer} class="file_viewer">
<div class="headerbar">
<Menu/>
<Breadcrumbs nav={nav}/>
</div>
<div class="filesystem">
<Breadcrumbs nav={nav}/>
<div class="viewer_area">
<Toolbar
bind:this={toolbar}
<div class="file_preview">
<FilePreview
bind:this={file_preview}
nav={nav}
file_viewer={file_viewer}
file_preview={file_preview}
bind:details_visible={details_visible}
upload_widget={upload_widget}
edit_window={edit_window}
bind:edit_visible={edit_visible}
on:open_sibling={e => nav.open_sibling(e.detail)}
on:download={() => fs_download(nav.base)}
on:details={() => details_visible = !details_visible}
/>
<div class="file_preview">
<FilePreview
bind:this={file_preview}
nav={nav}
upload_widget={upload_widget}
edit_window={edit_window}
on:open_sibling={e => nav.open_sibling(e.detail)}
on:download={() => fs_download(nav.base)}
on:details={() => details_visible = !details_visible}
/>
</div>
</div>
<DetailsWindow nav={nav} bind:visible={details_visible} />
<EditWindow nav={nav} bind:this={edit_window} bind:visible={edit_visible} />
<!-- This one is included at the highest level so uploads can keep running
even when the user navigates to a different directory -->
<FSUploadWidget nav={nav} bind:this={upload_widget} />
<AffiliatePrompt/>
<LoadingIndicator loading={$loading}/>
<Toolbar
bind:this={toolbar}
nav={nav}
bind:details_visible={details_visible}
edit_window={edit_window}
bind:edit_visible={edit_visible}
on:download={() => fs_download(nav.base)}
/>
</div>
<DetailsWindow nav={nav} bind:visible={details_visible} />
<EditWindow nav={nav} bind:this={edit_window} bind:visible={edit_visible} />
<!-- This one is included at the highest level so uploads can keep running
even when the user navigates to a different directory -->
<FSUploadWidget nav={nav} bind:this={upload_widget} />
<AffiliatePrompt/>
<style>
:global(*) {
transition: background-color 0.2s,
@@ -183,56 +183,15 @@ const keydown = (e: KeyboardEvent) => {
}
/* Viewer container */
.file_viewer {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
.filesystem {
display: flex;
flex-direction: column;
overflow: hidden;
/* Force some variable usage that is normally out of scope */
color: var(--body_text_color);
background-image: var(--background_image, var(--background_pattern));
background-color: var(--background_pattern_color);
background-size: var(--background_image_size, initial);
background-position: var(--background_image_position, initial);
background-repeat: var(--background_image_repeat, repeat);
}
/* Headerbar (row 1) */
.headerbar {
flex: 0 0 0;
display: flex;
flex-direction: row;
text-align: left;
box-shadow: none;
background-color: var(--shaded_background);
backdrop-filter: blur(4px);
}
/* File preview area (row 2) */
.viewer_area {
flex: 1 1 0;
display: flex;
flex-direction: row;
overflow: hidden;
}
/* This max-width needs to be synced with the .toolbar max-width in
Toolbar.svelte and the .label max-width in FileStats.svelte */
@media (max-width: 1000px) {
.viewer_area {
flex-direction: column-reverse;
}
height: 100vh;
width: 100%;
}
.file_preview {
flex: 1 1 0;
flex: 1 1 auto;
overflow: auto;
border: 1px solid var(--separator);
}
</style>

View File

@@ -1,394 +0,0 @@
// Response types
// ==============
export type GenericResponse = {
value: string,
message: string,
}
export type FSPath = {
path: Array<FSNode>,
base_index: number,
children: Array<FSNode>,
permissions: FSPermissions,
context: FSContext,
}
export type FSNode = {
type: string
path: string
name: string
created: string
modified: string
mode_string: string
mode_octal: string
created_by: string
abuse_type?: string
abuse_report_time?: string
custom_domain_name?: string
file_size: number
file_type: string
sha256_sum: string
id?: string
properties?: FSNodeProperties
link_permissions?: FSPermissions
user_permissions?: { [index: string]: FSPermissions }
password_permissions?: { [index: string]: FSPermissions }
// Added by us
// Indicates whether the file is selected in the file manager
fm_selected?: boolean
}
export const node_is_shared = (node: FSNode): boolean => {
if (node.link_permissions !== undefined && node.link_permissions.read) {
return true
}
return false
}
export type FSNodeProperties = {
branding_enabled?: string,
brand_input_color?: string,
brand_highlight_color?: string,
brand_danger_color?: string,
brand_background_color?: string,
brand_body_color?: string,
brand_card_color?: string,
brand_header_image?: string,
brand_header_link?: string,
brand_footer_image?: string,
brand_footer_link?: string,
brand_background_image?: string,
}
export type FSPermissions = {
owner: boolean,
read: boolean,
write: boolean,
delete: boolean,
}
export type FSContext = {
premium_transfer: boolean,
}
// API parameters
// ==============
// NodeOptions are options which can be applied by sending a PUT request to a
// filesystem node. This includes all values which can be set in
// FSNode.properties
export type NodeOptions = {
mode?: number,
created?: string,
modified?: string,
// Permissions
link_permissions?: FSPermissions,
user_permissions?: { [index: string]: FSPermissions },
password_permissions?: { [index: string]: FSPermissions },
// Custom domain name options
custom_domain_name?: string,
custom_domain_cert?: string,
custom_domain_key?: string,
} & FSNodeProperties
// API methods
// ===========
// mkdir only supports the "mode" option
export const fs_mkdir = async (path: string, opts?: NodeOptions) => {
const form = new FormData()
form.append("action", "mkdir")
if (opts !== undefined && opts.mode !== undefined) {
form.append("mode", opts.mode.toFixed(0))
}
return await fs_check_response(
await fetch(fs_path_url(path), { method: "POST", body: form })
)
}
export const fs_mkdirall = async (path: string, opts: NodeOptions) => {
const form = new FormData()
form.append("action", "mkdirall")
if (opts && opts.mode) {
form.append("mode", opts.mode.toFixed(0))
}
return await fs_check_response(
await fetch(fs_path_url(path), { method: "POST", body: form })
)
}
export const fs_get_node = async (path: string) => {
return await fs_check_response(
await fetch(fs_path_url(path) + "?stat")
) as FSPath
}
// Updates a node's parameters. Available options are:
// - created, Date object
// - modified, Date object
// - mode, file mode formatted as octal string
// - shared, boolean. If true the node will receive a public ID
//
// Returns the modified filesystem node object
export const fs_update = async (path: string, opts: NodeOptions) => {
const form = new FormData()
form.append("action", "update")
for (let key of Object.keys(opts)) {
if (opts[key] === undefined) {
continue
} else if ((key === "created" || key === "modified")) {
form.append(key, new Date(opts[key]).toISOString())
} else if (typeof opts[key] === "object") {
form.append(key, JSON.stringify(opts[key]))
} else {
form.append(key, opts[key])
}
}
return await fs_check_response(
await fetch(fs_path_url(path), { method: "POST", body: form })
) as FSNode
}
export const fs_rename = async (old_path: string, new_path: string) => {
const form = new FormData()
form.append("action", "rename")
form.append("target", new_path)
return await fs_check_response(
await fetch(fs_path_url(old_path), { method: "POST", body: form })
) as FSNode
}
export const fs_delete = async (path: string) => {
return await fs_check_response(
await fetch(fs_path_url(path), { method: "DELETE" })
) as GenericResponse
}
export const fs_delete_all = async (path: string) => {
return await fs_check_response(
await fetch(fs_path_url(path) + "?recursive", { method: "DELETE" })
) as GenericResponse
}
export const fs_search = async (path: string, term: string, limit = 10) => {
return await fs_check_response(
await fetch(
fs_path_url(path) +
"?search=" + encodeURIComponent(term) +
"&limit=" + limit
)
) as string[]
}
export type TimeSeries = {
timestamps: string[],
amounts: number[],
}
export type NodeTimeSeries = {
downloads: TimeSeries,
transfer_free: TimeSeries,
transfer_paid: TimeSeries,
}
export const fs_timeseries = async (path: string, start: Date, end: Date, interval = 60) => {
return await fs_check_response(
await fetch(
fs_path_url(path) +
"?timeseries" +
"&start=" + start.toISOString() +
"&end=" + end.toISOString() +
"&interval=" + interval
)
) as NodeTimeSeries
}
export const fs_import = async (parent_dir_path = "", filelist: Array<string>) => {
const form = new FormData()
form.append("action", "import")
form.append("files", JSON.stringify(filelist))
return await fs_check_response(
await fetch(fs_path_url(parent_dir_path), { method: "POST", body: form })
) as GenericResponse
}
// Utility functions
// =================
export const fs_check_response = async (resp: Response) => {
let text = await resp.text()
if (resp.status >= 400) {
let error: any
try {
error = JSON.parse(text) as GenericResponse
} catch (err) {
error = text
}
throw error
}
return JSON.parse(text)
}
export const fs_path_url = (path: string) => {
if (!path || path.length === 0) {
return ""
}
if (path[0] !== "/") {
path = "/" + path
}
if (window["api_endpoint"] !== undefined) {
return window["api_endpoint"] + "/filesystem" + fs_encode_path(path)
} else {
throw Error("fs_path_url: api_endpoint is undefined")
}
}
export const fs_encode_path = (path: string) => {
// Encode all path elements separately to preserve forward slashes
let split = path.split("/")
for (let i = 0; i < split.length; i++) {
split[i] = encodeURIComponent(split[i])
}
return split.join("/")
}
export const fs_split_path = (path: string) => {
let patharr = path.split("/")
return { base: patharr.pop(), parent: patharr.join("/") }
}
export const fs_node_type = (node: FSNode) => {
if (node.type === "dir") {
return "dir"
} else if (node.file_type === "application/bittorrent" || node.file_type === "application/x-bittorrent") {
return "torrent"
} else if (
node.file_type === "application/zip" ||
node.file_type === "application/x-7z-compressed" ||
node.file_type === "application/x-tar" ||
(node.file_type === "application/gzip" && node.name.endsWith(".tar.gz")) ||
(node.file_type === "application/x-xz" && node.name.endsWith(".tar.xz")) ||
(node.file_type === "application/zstd" && node.name.endsWith(".tar.zst"))
) {
return "zip"
} else if (node.file_type.startsWith("image")) {
return "image"
} else if (
node.file_type.startsWith("video") ||
node.file_type === "application/matroska" ||
node.file_type === "application/x-matroska"
) {
return "video"
} else if (
node.file_type.startsWith("audio") ||
node.file_type === "application/ogg" ||
node.name.endsWith(".mp3")
) {
return "audio"
} else if (
node.file_type === "application/pdf" ||
node.file_type === "application/x-pdf"
) {
return "pdf"
} else if (
node.file_type === "application/json" ||
node.file_type === "application/x-yaml" ||
node.file_type === "application/x-shellscript" ||
node.file_type.startsWith("text")
) {
return "text"
} else {
return "file"
}
}
export const fs_node_icon = (node: FSNode, width = 64, height = 64) => {
if (node.type === "dir") {
// Folders with an ID are publically shared, use the shared folder icon
if (node_is_shared(node)) {
return "/res/img/mime/folder-remote.png"
} else {
return "/res/img/mime/folder.png"
}
}
return fs_thumbnail_url(node.path, width, height) + "&mod=" + new Date(node.modified).getTime()
}
export const fs_thumbnail_url = (path: string, width = 64, height = 64) => {
return fs_path_url(path) + "?thumbnail&width=" + width + "&height=" + height
}
export const fs_share_url = (path: FSNode[]): string => {
let share_path = fs_share_path(path)
if (share_path !== "") {
share_path = window.location.protocol + "//" + window.location.host + "/d/" + fs_encode_path(share_path)
}
return share_path
}
export const fs_share_hotlink_url = (path: FSNode[]): string => {
let share_path = fs_share_path(path)
if (share_path !== "") {
share_path = window.location.protocol + "//" + window.location.host + fs_path_url(share_path)
}
return share_path
}
export const fs_share_path = (path: FSNode[]): string => {
let share_url = ""
let bucket_idx = -1
// Find the last node in the path that has a public ID
for (let i = path.length - 1; i >= 0; i--) {
if (path[i].id !== undefined && path[i].id !== "me") {
bucket_idx = i
break
}
}
if (bucket_idx !== -1) {
share_url = path[bucket_idx].id
// Construct the path starting from the bucket
for (let i = bucket_idx + 1; i < path.length; i++) {
share_url += "/" + path[i].name
}
}
return share_url
}
export const fs_download = (node: FSNode) => {
const a = document.createElement("a")
if (node.type === "file") {
a.href = fs_path_url(node.path) + "?attach"
a.download = node.name
} else if (node.type === "dir") {
a.href = fs_path_url(node.path) + "?bulk_download"
a.download = node.name + ".zip"
}
// You can't call .click() on an element that is not in the DOM. But
// emitting a click event works
a.dispatchEvent(new MouseEvent("click"))
}

View File

@@ -1,6 +1,6 @@
<script lang="ts">
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 "./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 CopyButton from "layout/CopyButton.svelte";
import Dialog from "layout/Dialog.svelte";

View File

@@ -4,8 +4,7 @@ import { copy_text } from "util/Util.svelte";
import FileStats from "./FileStats.svelte";
import type { FSNavigator } from "./FSNavigator";
import EditWindow from "./edit_window/EditWindow.svelte";
import FilePreview from "./viewers/FilePreview.svelte";
import { fs_share_url } from "./FilesystemAPI";
import { fs_share_url } from "lib/FilesystemAPI";
import ShareDialog from "./ShareDialog.svelte";
let dispatch = createEventDispatcher()
@@ -14,8 +13,6 @@ export let nav: FSNavigator
export let details_visible = false
export let edit_window: EditWindow
export let edit_visible = false
export let file_viewer: HTMLDivElement
export let file_preview: FilePreview
let share_dialog: ShareDialog
$: share_url = fs_share_url($nav.path)
@@ -30,46 +27,11 @@ export const copy_link = () => {
link_copied = true
setTimeout(() => {link_copied = false}, 60000)
}
let fullscreen = false
export const toggle_fullscreen = () => {
if (document.fullscreenElement !== null) {
try {
document.exitFullscreen()
} catch (err) {
console.debug("Failed to exit fullscreen", err)
}
fullscreen = false
} else {
if (!file_preview.toggle_fullscreen()) {
file_viewer.requestFullscreen()
}
fullscreen = true
}
}
let expanded = true
let expand = (e: Event) => {
e.preventDefault()
e.stopPropagation()
expanded = !expanded
}
</script>
<div class="toolbar" class:expanded>
<div class="stats_container" on:click={expand} on:keypress={expand} role="button" tabindex="0">
<button class="button_expand hidden_vertical" on:click={expand}>
{#if expanded}
<i class="icon">expand_more</i>
{:else}
<i class="icon">expand_less</i>
{/if}
</button>
<FileStats nav={nav}/>
</div>
<div class="separator"></div>
<div class="toolbar">
<div class="grid">
<FileStats nav={nav}/>
<div class="button_row">
<button on:click={() => {nav.open_sibling(-1)}}>
@@ -83,8 +45,6 @@ let expand = (e: Event) => {
</button>
</div>
<div class="separator hidden_horizontal"></div>
<button on:click={() => dispatch("download")}>
<i class="icon">save</i>
<span>Download</span>
@@ -105,21 +65,6 @@ let expand = (e: Event) => {
</button>
{/if}
<button
class="toolbar_button"
on:click={toggle_fullscreen}
class:button_highlight={fullscreen}
title="Open page in full screen mode">
{#if fullscreen}
<i class="icon">fullscreen_exit</i>
{:else}
<i class="icon">fullscreen</i>
{/if}
<span>Fullscreen</span>
</button>
<div class="separator hidden_horizontal"></div>
<button on:click={() => details_visible = !details_visible} class:button_highlight={details_visible}>
<i class="icon">help</i>
<span>Deta<u>i</u>ls</span>
@@ -140,22 +85,17 @@ let expand = (e: Event) => {
.toolbar {
flex: 0 0 auto;
overflow-x: hidden;
overflow-y: scroll;
overflow-y: hidden;
transition: max-height 0.3s;
background-color: var(--shaded_background);
border-top: 1px solid var(--separator);
background: var(--shaded_background);
backdrop-filter: blur(4px);
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(7.5em, 1fr));
}
.separator {
height: 1px;
margin: 2px 0;
width: 100%;
background-color: var(--separator);
}
.button_row {
display: flex;
flex-direction: row;
@@ -164,46 +104,4 @@ let expand = (e: Event) => {
flex: 1 1 auto;
justify-content: center;
}
.stats_container {
display: flex;
flex-direction: column;
}
.button_expand {
line-height: 1em;
}
.hidden_vertical {
display: none;
}
.hidden_horizontal {
display: block;
}
/* This max-width needs to be synced with the .viewer_area max-width in
Toolbar.svelte and the .label max-width in FileStats.svelte */
@media (max-width: 1000px) {
.toolbar {
overflow-y: hidden;
max-height: 2.1em;
}
.toolbar.expanded {
overflow-y: scroll;
max-height: 25vh;
}
.stats_container {
flex-direction: row;
}
.separator {
margin: 0;
}
.hidden_vertical {
display: block;
}
.hidden_horizontal {
display: none;
}
}
</style>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import Button from "layout/Button.svelte";
import type { FSPermissions, NodeOptions } from "filesystem/FilesystemAPI";
import type { FSPermissions, NodeOptions } from "lib/FilesystemAPI";
import PermissionButton from "./PermissionButton.svelte";
export let options: NodeOptions

View File

@@ -2,10 +2,38 @@ import parse from "pure-color/parse";
import rgb2hsl from "pure-color/convert/rgb2hsl";
import hsl2rgb from "pure-color/convert/hsl2rgb";
import rgb2hex from "pure-color/convert/rgb2hex";
import type { FSNode, FSNodeProperties } from "lib/FilesystemAPI";
type Style = {
input_background: string,
input_hover_background: string,
input_text: string,
highlight_color: string,
highlight_background: string,
highlight_text_color: string,
link_color: string,
danger_color: string,
danger_text_color: string,
background_color: string,
background: string,
background_text_color: string,
background_pattern_color: string,
body_color: string,
body_background: string,
body_text_color: string,
shaded_background: string,
separator: string,
shadow_color: string,
card_color: string,
background_image: string,
background_image_size: string,
background_image_position: string,
background_image_repeat: string,
}
// Generate a branding style from a file's properties map
export const branding_from_path = path => {
let style = {}
export const branding_from_path = (path: Array<FSNode>) => {
let style = <Style>{}
for (let node of path) {
add_styles(style, node.properties)
}
@@ -15,17 +43,17 @@ export const branding_from_path = path => {
// The last style which was generated is cached, when we don't have a complete
// path to generate the style with we will use the cached style as a basis
let last_generated_style = {}
export const branding_from_node = node => {
let last_generated_style = <Style>{}
export const branding_from_node = (node: FSNode) => {
add_styles(last_generated_style, node.properties)
return gen_css(last_generated_style)
}
export const css_from_path = path => {
export const css_from_path = (path: Array<FSNode>) => {
return gen_css(branding_from_path(path))
}
const gen_css = style => {
const gen_css = (style: Style) => {
return Object.entries(style).map(([key, value]) => `--${key}:${value}`).join(';');
}
@@ -33,7 +61,7 @@ const gen_css = style => {
// existing style which is passed as the first argument. When navigating to a
// path this function is executed on every member of the path so all the styles
// get combined
const add_styles = (style, properties) => {
const add_styles = (style: Style, properties: FSNodeProperties) => {
if (!properties || !properties.branding_enabled || properties.branding_enabled !== "true") {
return
}
@@ -83,7 +111,7 @@ const add_styles = (style, properties) => {
}
}
const add_contrast = (color, amt) => {
const add_contrast = (color: string, amt: number) => {
let hsl = rgb2hsl(parse(color)) // Convert hex to hsl
// If the lightness is less than 40 it is considered a dark colour. This
// threshold is 40 instead of 50 because overall dark text is more legible
@@ -96,20 +124,20 @@ const add_contrast = (color, amt) => {
}
// Darken and desaturate. Only used for shadows
const darken = (color, percent) => {
const darken = (color: string, percent: number) => {
let hsl = rgb2hsl(parse(color)) // Convert hex to hsl
hsl[1] = hsl[1] * percent
hsl[2] = hsl[2] * percent
return rgb2hex(hsl2rgb(hsl)) // Convert back to hex
}
const set_alpha = (color, amt) => {
const set_alpha = (color: string, amt: number) => {
let rgb = parse(color)
rgb.push(amt)
return "rgba(" + rgb.join(", ") + ")"
}
const generate_link_color = (link_color, body_color) => {
const generate_link_color = (link_color: string, body_color: string) => {
let link = rgb2hsl(parse(link_color))
let body = rgb2hsl(parse(body_color))

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import ThemePresets from "./ThemePresets.svelte";
import { fs_update, fs_node_type, type FSNode, type NodeOptions, node_is_shared, type FSPermissions } from "filesystem/FilesystemAPI";
import { fs_update, fs_node_type, type FSNode, type NodeOptions, node_is_shared, type FSPermissions } from "lib/FilesystemAPI";
import CustomBanner from "filesystem/viewers/CustomBanner.svelte";
import HelpButton from "layout/HelpButton.svelte";
import FilePicker from "filesystem/filemanager/FilePicker.svelte";

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { fs_rename, fs_update, type FSNode, type NodeOptions } from "filesystem/FilesystemAPI";
import { fs_rename, fs_update, type FSNode, type NodeOptions } from "lib/FilesystemAPI";
import Modal from "util/Modal.svelte";
import BrandingOptions from "./BrandingOptions.svelte";
import { branding_from_node } from "./Branding";
@@ -39,18 +39,9 @@ export const edit = (f: FSNode, oae = false, open_tab = "") => {
}
options.custom_domain_name = file.custom_domain_name
options.shared = !(file.id === undefined || file.id === "")
if (options.shared) {
if (file.link_permissions === undefined) {
// Default to read-only for public links
file.link_permissions = { owner: false, read: true, write: false, delete: false}
} else {
options.link_permissions = file.link_permissions
}
options.user_permissions = file.user_permissions
options.password_permissions = file.password_permissions
}
options.link_permissions = file.link_permissions
options.user_permissions = file.user_permissions
options.password_permissions = file.password_permissions
branding_enabled = options.branding_enabled === "true"
if (branding_enabled) {
@@ -122,7 +113,7 @@ const save = async (keep_editing = false) => {
<i class="icon">share</i>
Sharing
</button>
{#if options.shared && $nav.permissions.owner}
{#if $nav.permissions.owner}
<button class:button_highlight={tab === "access"} on:click={() => tab = "access"}>
<i class="icon">key</i>
Access control

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import Button from "layout/Button.svelte";
import { fs_delete_all, type FSNode } from "filesystem/FilesystemAPI";
import { fs_delete_all, type FSNode } from "lib/FilesystemAPI";
import PathLink from "filesystem/util/PathLink.svelte";
import type { FSNavigator } from "filesystem/FSNavigator";

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import ToggleButton from "layout/ToggleButton.svelte";
import type { FSPermissions } from "filesystem/FilesystemAPI";
import type { FSPermissions } from "lib/FilesystemAPI";
export let permissions = <FSPermissions>{}
</script>

View File

@@ -1,11 +1,10 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { domain_url } from "util/Util.svelte";
import CopyButton from "layout/CopyButton.svelte";
import { formatDate } from "util/Formatting";
import type { FSNode, NodeOptions } from "filesystem/FilesystemAPI";
import { node_is_shared, type FSNode, type NodeOptions } from "lib/FilesystemAPI";
import AccessControl from "./AccessControl.svelte";
let dispatch = createEventDispatcher()
export let file: FSNode = {} as FSNode
export let options: NodeOptions
@@ -14,8 +13,8 @@ let preview_area: HTMLDivElement
$: share_link = window.location.protocol+"//"+window.location.host+"/d/"+file.id
$: embed_iframe(file, options)
let embed_iframe = (file: FSNode, options: NodeOptions) => {
if (!options.shared) {
const embed_iframe = (file: FSNode, options: NodeOptions) => {
if (!node_is_shared(file)) {
example = false
embed_html = "File is not shared, can't generate embed code"
return
@@ -24,14 +23,14 @@ let embed_iframe = (file: FSNode, options: NodeOptions) => {
let url = domain_url()+"/d/"+file.id
embed_html = `<iframe ` +
`src="${url}" ` +
`style="border: none; width: 100%; max-width 90vw; height: 800px; max-height: 75vh; border-radius: 6px; "` +
`style="border: none; width: 100%; max-width 90vw; height: 800px; max-height: 75vh; border-radius: 6px;" ` +
`allowfullscreen` +
`></iframe>`
}
let example = false
const toggle_example = () => {
if (options.shared) {
if (node_is_shared(file)) {
example = !example
if (example) {
preview_area.innerHTML = embed_html
@@ -41,15 +40,6 @@ const toggle_example = () => {
}
}
const update_shared = () => {
// If sharing is enabled we automatically save the file so the user can copy
// the sharing link. But if the user disables sharing we don't automatically
// save so that the user can't accidentally discard a sharing link that's in
// use
if (options.shared && !file.id) {
dispatch("save")
}
}
</script>
<fieldset>
@@ -64,34 +54,14 @@ const update_shared = () => {
</div>
{/if}
<div>
<input
form="edit_form"
bind:checked={options.shared}
on:change={update_shared}
id="shared"
type="checkbox"
class="form_input"
/>
<label for="shared">Share this file or directory</label>
</div>
<div class="link_grid">
{#if options.shared}
<span>Public link: <a href={share_link}>{share_link}</a></span>
<a href={share_link}>{share_link}</a>
<CopyButton text={share_link}>Copy</CopyButton>
{/if}
</div>
<p>
When a file or directory is shared it can be accessed through a
unique link. You can get the URL with the 'Copy link' button on
the toolbar, or share the link with the 'Share' button. If you
share a directory all the files within the directory are also
accessible from the link.
</p>
</fieldset>
<AccessControl options={options}/>
<fieldset>
<legend>Embedding</legend>
<p>
@@ -108,7 +78,7 @@ const update_shared = () => {
<textarea bind:value={embed_html} style="width: 100%; height: 4em;"></textarea>
<br/>
<CopyButton text={embed_html}>Copy HTML</CopyButton>
<button on:click={toggle_example} class:button_highlight={example} disabled={!options.shared}>
<button on:click={toggle_example} class:button_highlight={example} disabled={!node_is_shared(file)}>
<i class="icon">visibility</i> Show example
</button>
</div>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import type { FSNodeProperties } from "filesystem/FilesystemAPI";
import type { FSNodeProperties } from "lib/FilesystemAPI";
export let properties: FSNodeProperties = {} as FSNodeProperties

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { fs_encode_path, fs_node_icon, node_is_shared } from "filesystem/FilesystemAPI"
import { fs_encode_path, fs_node_icon, node_is_shared } from "lib/FilesystemAPI"
import type { FSNavigator } from "filesystem/FSNavigator";
import { FileAction } from "./FileManagerLib";
@@ -36,21 +36,14 @@ export let hide_edit = false
</a>
{/if}
{#if $nav.permissions.write && !hide_edit}
<button
class="action_button flat"
on:click={e => dispatch("file", {index: index, action: FileAction.Edit, original: e})}
>
<i class="icon">edit</i>
</button>
{/if}
{#if !hide_edit}
<button
class="action_button flat"
on:click={e => dispatch("file", {index: index, action: FileAction.Download, original: e})}
on:click={e => dispatch("file", {index: index, action: FileAction.Menu, original: e})}
>
<i class="icon">save</i>
<i class="icon">menu</i>
</button>
{/if}
</a>
{/each}
</div>
@@ -71,9 +64,9 @@ export let hide_edit = false
color: var(--body_text-color);
padding: 2px;
align-items: center;
background: var(--input_background);
background: var(--shaded_background);
backdrop-filter: blur(4px);
border-radius: 4px;
box-shadow: 1px 1px 8px 0px var(--shadow_color);
gap: 6px;
}
.node:hover:not(.node_selected) {
@@ -92,7 +85,6 @@ export let hide_edit = false
height: 2em;
width: 2em;
vertical-align: middle;
border-radius: 4px;
}
.node_name {
flex: 1 1 content;

View File

@@ -1,8 +1,9 @@
<script lang="ts">
import { onMount } from "svelte";
import { fs_mkdir } from "filesystem/FilesystemAPI";
import { fs_mkdir } from "lib/FilesystemAPI";
import Button from "layout/Button.svelte";
import type { FSNavigator } from "filesystem/FSNavigator";
import { loading_finish, loading_start } from "lib/Loading";
export let nav: FSNavigator
@@ -14,7 +15,7 @@ let create_dir = async () => {
form.append("type", "dir")
try {
nav.set_loading(true)
loading_start()
await fs_mkdir(nav.base.path+"/"+new_dir_name)
new_dir_name = "" // Clear input field
error_msg = "" // Clear error msg
@@ -26,7 +27,7 @@ let create_dir = async () => {
error_msg = "Server returned an error: code: '"+err.value+"' message: "+err.message
}
} finally {
nav.set_loading(false)
loading_finish()
}
}

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { fs_delete_all, fs_download, fs_rename, type FSNode } from "filesystem/FilesystemAPI"
import { fs_delete_all, fs_download, fs_rename, type FSNode } from "lib/FilesystemAPI"
import { onMount } from "svelte"
import CreateDirectory from "./CreateDirectory.svelte"
import ListView from "./ListView.svelte"
@@ -13,6 +13,7 @@ import type { FSNavigator } from "filesystem/FSNavigator";
import FsUploadWidget from "filesystem/upload_widget/FSUploadWidget.svelte";
import EditWindow from "filesystem/edit_window/EditWindow.svelte";
import { FileAction, type FileEvent } from "./FileManagerLib";
import FileMenu from "./FileMenu.svelte";
export let nav: FSNavigator
export let upload_widget: FsUploadWidget
@@ -23,6 +24,7 @@ let uploader: FsUploadWidget
let mode = "viewing"
let creating_dir = false
let show_hidden = false
let file_menu: FileMenu
export const upload = (files: File[]) => {
return uploader.upload(files)
@@ -84,6 +86,11 @@ const file_event = (e: CustomEvent<FileEvent>) => {
e.detail.original.stopPropagation()
fs_download(nav.children[index])
break
case FileAction.Menu:
e.detail.original.preventDefault()
e.detail.original.stopPropagation()
file_menu.open(nav.children[index], e.detail.original.target)
break
}
}
@@ -410,10 +417,10 @@ onMount(() => {
{/if}
</div>
<FileMenu bind:this={file_menu} bind:nav bind:edit_window />
<style>
.container {
height: 100%;
width: 100%;
padding: 0;
overflow: auto;
display: block;

View File

@@ -3,4 +3,4 @@ export type FileEvent = {
action: FileAction,
original: MouseEvent,
}
export enum FileAction { Click, Context, Edit, Share, Branding, Select, Download }
export enum FileAction { Click, Context, Edit, Share, Branding, Select, Download, Menu }

View File

@@ -0,0 +1,64 @@
<script lang="ts">
import EditWindow from "filesystem/edit_window/EditWindow.svelte";
import type { FSNavigator } from "filesystem/FSNavigator";
import Button from "layout/Button.svelte";
import Dialog from "layout/Dialog.svelte";
import { bookmark_add, bookmark_del, bookmarks_store } from "lib/Bookmarks";
import { fs_download, type FSNode } from "lib/FilesystemAPI";
import { tick } from "svelte";
export let nav: FSNavigator
export let edit_window: EditWindow
let dialog: Dialog
let node: FSNode
let is_bookmark: boolean = false
export const open = async (n: FSNode, target: EventTarget) => {
node = n
is_bookmark = false
for (const bm of $bookmarks_store) {
console.log(bm)
if (bm.id === n.id) {
is_bookmark = true
break
}
}
// Wait for the view to update, so the dialog gets the proper measurements
await tick()
dialog.open((target as Element).closest("button").getBoundingClientRect())
}
const bookmark = () => {
bookmark_add({
id: node.id,
icon: "folder_shared",
label: node.name,
})
}
</script>
<Dialog bind:this={dialog}>
<div class="menu">
<Button click={() => {dialog.close(); fs_download(node)}} icon="save" label="Download"/>
{#if is_bookmark}
<Button click={() => {dialog.close(); bookmark_del(node.id)}} icon="bookmark_remove" label="Remove bookmark"/>
{:else}
<Button click={() => {dialog.close(); bookmark()}} icon="bookmark_add" label="Add bookmark"/>
{/if}
{#if $nav.permissions.write}
<Button click={() => {dialog.close(); edit_window.edit(node, false, "file")}} icon="edit" label="Edit"/>
<Button click={() => {dialog.close(); edit_window.edit(node, false, "share")}} icon="share" label="Share"/>
<Button click={() => {dialog.close(); edit_window.edit(node, false, "branding")}} icon="palette" label="Branding"/>
{/if}
</div>
</Dialog>
<style>
.menu {
display: flex;
flex-direction: column;
max-width: 20em;
}
</style>

View File

@@ -4,17 +4,15 @@ import ListView from "./ListView.svelte"
import GalleryView from "./GalleryView.svelte"
import CompactView from "./CompactView.svelte"
import Modal from "util/Modal.svelte";
import LoadingIndicator from "util/LoadingIndicator.svelte";
import Breadcrumbs from "filesystem/Breadcrumbs.svelte"
import { FSNavigator } from "filesystem/FSNavigator";
import type { FSNode } from "filesystem/FilesystemAPI";
import type { FSNode } from "lib/FilesystemAPI";
import { FileAction, type FileEvent } from "./FileManagerLib";
let nav = new FSNavigator(false)
let modal: Modal
let dispatch = createEventDispatcher()
let directory_view = ""
let loading = false
let large_icons = false
let show_hidden = false
export let select_multiple = false
@@ -194,8 +192,6 @@ onMount(() => {
on:file={file_event}
/>
{/if}
<LoadingIndicator loading={loading}/>
</Modal>
<style>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { createEventDispatcher } from "svelte"
import { fs_node_icon, fs_node_type, fs_encode_path } from "filesystem/FilesystemAPI";
import { fs_node_icon, fs_node_type, fs_encode_path } from "lib/FilesystemAPI";
import type { FSNavigator } from "filesystem/FSNavigator";
import { FileAction } from "./FileManagerLib";
let dispatch = createEventDispatcher()
@@ -46,15 +46,15 @@ export let large_icons = false
width: 150px;
height: 150px;
overflow: hidden;
border-radius: 8px;
background: var(--input_background);
background: var(--shaded_background);
backdrop-filter: blur(4px);
border-radius: 4px;
color: var(--input_text);
display: flex;
flex-direction: column;
transition: background 0.2s;
text-decoration: none;
padding: 3px;
box-shadow: 1px 1px 0px 0px var(--shadow_color);
}
.file.large_icons {
width: 200px;
@@ -87,7 +87,7 @@ export let large_icons = false
}
.node_icon {
flex: 1 1 0;
border-radius: 6px;
border-radius: 4px;
background-position: center;
background-size: contain;
background-repeat: no-repeat;

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { formatDataVolume } from "util/Formatting";
import { fs_encode_path, fs_node_icon, node_is_shared } from "filesystem/FilesystemAPI"
import { fs_encode_path, fs_node_icon, node_is_shared } from "lib/FilesystemAPI"
import type { FSNavigator } from "filesystem/FSNavigator";
import SortButton from "layout/SortButton.svelte";
import { FileAction } from "./FileManagerLib";
@@ -19,7 +19,7 @@ export let hide_branding = false
<tr>
<td></td>
<td><SortButton active_field={$nav.sort_last_field} asc={$nav.sort_asc} sort_func={nav.sort_children} field="name">Name</SortButton></td>
<td><SortButton active_field={$nav.sort_last_field} asc={$nav.sort_asc} sort_func={nav.sort_children} field="file_size">Size</SortButton></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></td>
</tr>
{#each $nav.children as child, index (child.path)}
@@ -60,14 +60,11 @@ export let hide_branding = false
<i class="icon">palette</i>
</button>
{/if}
{#if $nav.permissions.write && !hide_edit}
<button class="action_button" on:click={e => dispatch("file", {index: index, action: FileAction.Edit, original: e})}>
<i class="icon">edit</i>
{#if !hide_edit}
<button class="action_button" on:click={e => dispatch("file", {index: index, action: FileAction.Menu, original: e})}>
<i class="icon">menu</i>
</button>
{/if}
<button class="action_button" on:click={e => dispatch("file", {index: index, action: FileAction.Download, original: e})}>
<i class="icon">save</i>
</button>
</div>
</td>
</a>
@@ -77,13 +74,11 @@ export let hide_branding = false
<style>
.directory {
display: table;
margin: 8px auto 16px auto;
background: var(--shaded_background);
border-collapse: collapse;
border-radius: 8px;
max-width: 99%;
width: 1200px;
backdrop-filter: blur(4px);
max-width: 1200px;
margin: auto; /* center */
}
.directory > * {
display: table-row;
@@ -116,7 +111,7 @@ td {
height: 32px;
width: 32px;
vertical-align: middle;
border-radius: 4px;
/* border-radius: 4px; */
margin: 2px;
}
.node_name {

View File

@@ -1,7 +1,8 @@
<script lang="ts">
import { onMount } from "svelte";
import { fs_search, fs_encode_path, fs_thumbnail_url } from "filesystem/FilesystemAPI";
import { fs_search, fs_encode_path, fs_thumbnail_url } from "lib/FilesystemAPI";
import type { FSNavigator } from "filesystem/FSNavigator";
import { loading_finish, loading_start } from "lib/Loading";
export let nav: FSNavigator
@@ -43,9 +44,9 @@ const search = async (limit = 10) => {
last_limit = limit
searching = true
nav.set_loading(true)
try {
loading_start()
search_results = await fs_search(nav.base.path, search_term, limit)
} catch (err) {
if (err.value) {
@@ -54,6 +55,8 @@ const search = async (limit = 10) => {
alert(err)
console.error(err)
}
} finally {
loading_finish()
}
if (search_results.length > 0 && selected_result > search_results.length-1) {
@@ -61,7 +64,6 @@ const search = async (limit = 10) => {
}
searching = false
nav.set_loading(false)
// It's possible that the user entered another letter while we were
// performing the search reqeust. If this happens we run the search function
@@ -218,7 +220,6 @@ const window_keydown = (e: KeyboardEvent) => {
max-width: 100%;
padding-top: 2px;
padding-bottom: 2px;
border-bottom: 1px solid var(--separator);
}
.search_form {

View File

@@ -11,14 +11,11 @@ export type UploadJob = {
</script>
<script lang="ts">
import { tick } from "svelte";
import { fade } from "svelte/transition";
import UploadProgress from "./UploadProgress.svelte";
import type { FSNavigator } from "filesystem/FSNavigator";
export let nav: FSNavigator
const max_concurrent_uploads = 5
let file_input_field: HTMLInputElement
let file_input_change = (e: Event) => {
// Start uploading the files async
@@ -91,22 +88,29 @@ let active_uploads = 0
let state = "idle"
const start_upload = async () => {
// Count the number of active uploads so we can know how many new uploads we
// can start
active_uploads = upload_queue.reduce((acc, val) => {
if (val.status === "uploading") {
acc++
}
return acc
}, 0)
for (let i = 0; i < upload_queue.length && active_uploads < max_concurrent_uploads; i++) {
active_uploads = 0
let uploading_size = 0
for (let i = 0; i < upload_queue.length; i++) {
if (upload_queue[i]) {
// If this file is queued, start the upload
if (upload_queue[i].status === "queued") {
active_uploads++
upload_queue[i].component.start()
upload_queue[i].status = "uploading"
}
// If this file is already uploading (or just started), count it
if (upload_queue[i].status === "uploading") {
uploading_size += upload_queue[i].total_size
active_uploads++
}
// If the size threshold or the concurrent upload limit is reached
// we break the loop. The system tries to keep an upload queue of
// 100 MB and a minimum of two concurrent uploads.
if ((uploading_size >= 100e6 && active_uploads >= 2) || active_uploads >= 10) {
console.debug("Current uploads", active_uploads, "uploads size", uploading_size)
break
}
}
}
@@ -153,7 +157,7 @@ const leave_confirmation = (e: BeforeUnloadEvent) => {
/>
{#if visible}
<div class="upload_widget" transition:fade={{duration: 200}}>
<div class="upload_widget">
<div class="header">
{#if state === "idle"}
Waiting for files
@@ -184,7 +188,8 @@ const leave_confirmation = (e: BeforeUnloadEvent) => {
position: fixed;
display: flex;
flex-direction: column;
width: 500px;
width: auto;
min-width: 400px;
max-width: 80%;
height: auto;
max-height: 50%;

View File

@@ -9,7 +9,7 @@
//
// on_error is called when the upload has failed. The parameters are the error
import { fs_path_url, type GenericResponse } from "filesystem/FilesystemAPI"
import { fs_path_url, type GenericResponse } from "lib/FilesystemAPI"
// code and an error message
export const upload_file = (

View File

@@ -1,6 +1,5 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { fade } from "svelte/transition";
import { upload_file } from "./UploadFunc";
import ProgressBar from "util/ProgressBar.svelte";
import Button from "layout/Button.svelte"
@@ -56,14 +55,14 @@ const cancel = () => {
}
</script>
<div class="prog" transition:fade={{duration: 200}} class:error={job.status === "error"}>
<div class="prog" class:error={job.status === "error"}>
<div class="bar">
{job.file.name}<br/>
{#if error_code !== ""}
{error_message}<br/>
{error_code}<br/>
{/if}
<ProgressBar total={total} used={loaded}/>
<ProgressBar total={total} used={loaded} speed={500}/>
</div>
<div class="cancel">
<Button icon="cancel" click={cancel}/>

View File

@@ -1,7 +1,6 @@
<script lang="ts">
import { onMount } from 'svelte'
import { fs_path_url, fs_encode_path, fs_node_icon } from "filesystem/FilesystemAPI"
import FileTitle from "layout/FileTitle.svelte";
import { fs_path_url, fs_encode_path, fs_node_icon } from "lib/FilesystemAPI"
import TextBlock from "layout/TextBlock.svelte"
import type { FSNavigator } from 'filesystem/FSNavigator';
@@ -50,8 +49,6 @@ onMount(() => {
<slot></slot>
<FileTitle title={$nav.base.name}/>
<TextBlock width="1000px">
<audio
bind:this={player}

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import IconBlock from "layout/IconBlock.svelte";
import { fs_thumbnail_url } from "filesystem/FilesystemAPI";
import { fs_thumbnail_url } from "lib/FilesystemAPI";
import TextBlock from "layout/TextBlock.svelte"
import { formatDataVolume, formatDate } from "util/Formatting";
import type { FSNavigator } from "filesystem/FSNavigator";

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { onMount, tick } from "svelte";
import Spinner from "util/Spinner.svelte";
import { fs_node_type } from "filesystem/FilesystemAPI";
import { fs_node_type } from "lib/FilesystemAPI";
import FileManager from "filesystem/filemanager/FileManager.svelte";
import Audio from "./Audio.svelte";
import File from "./File.svelte";
@@ -74,7 +74,7 @@ export const seek = (delta: number) => {
{#if viewer_type === ""}
<div class="center">
<Spinner></Spinner>
<Spinner/>
</div>
{:else if viewer_type === "dir"}
<FileManager nav={nav} upload_widget={upload_widget} edit_window={edit_window}>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { swipe_nav } from "lib/SwipeNavigate";
import { fs_path_url } from "filesystem/FilesystemAPI";
import { fs_path_url } from "lib/FilesystemAPI";
import type { FSNavigator } from "filesystem/FSNavigator";
let dispatch = createEventDispatcher();

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { fs_path_url } from "filesystem/FilesystemAPI";
import { fs_path_url } from "lib/FilesystemAPI";
import type { FSNavigator } from "filesystem/FSNavigator";
export let nav: FSNavigator

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { tick } from "svelte";
import { fs_path_url, type FSNode } from "filesystem/FilesystemAPI";
import { fs_path_url, type FSNode } from "lib/FilesystemAPI";
import type { FSNavigator } from "filesystem/FSNavigator";
export let nav: FSNavigator

View File

@@ -19,9 +19,10 @@ import { formatDate } from "util/Formatting"
import TorrentItem from "./TorrentItem.svelte"
import IconBlock from "layout/IconBlock.svelte";
import TextBlock from "layout/TextBlock.svelte"
import { fs_node_icon, fs_path_url } from "filesystem/FilesystemAPI";
import { fs_node_icon, fs_path_url } from "lib/FilesystemAPI";
import CopyButton from "layout/CopyButton.svelte";
import type { FSNavigator } from "filesystem/FSNavigator";
import { loading_finish, loading_start } from "lib/Loading";
let dispatch = createEventDispatcher()
@@ -31,7 +32,7 @@ let status = "loading"
export const update = async () => {
try {
nav.set_loading(true)
loading_start()
let resp = await fetch(fs_path_url(nav.base.path)+"?torrent_info")
if (resp.status >= 400) {
@@ -58,7 +59,7 @@ export const update = async () => {
} catch (err) {
console.error(err)
} finally {
nav.set_loading(false)
loading_finish()
status = "finished"
}
}

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { onMount, createEventDispatcher, tick } from "svelte";
import { video_position } from "lib/VideoPosition";
import { fs_path_url } from "filesystem/FilesystemAPI";
import { fs_path_url } from "lib/FilesystemAPI";
import type { FSNavigator } from "filesystem/FSNavigator";
let dispatch = createEventDispatcher()

View File

@@ -13,8 +13,9 @@ import { formatDataVolume, formatDate } from "util/Formatting"
import ZipItem from "filesystem/viewers/ZipItem.svelte";
import IconBlock from "layout/IconBlock.svelte";
import TextBlock from "layout/TextBlock.svelte"
import { fs_node_icon, fs_path_url } from "filesystem/FilesystemAPI";
import { fs_node_icon, fs_path_url } from "lib/FilesystemAPI";
import type { FSNavigator } from "filesystem/FSNavigator";
import { loading_finish, loading_start } from "lib/Loading";
let dispatch = createEventDispatcher()
@@ -36,8 +37,8 @@ export const update = async () => {
}
try {
loading_start()
status = "loading"
nav.set_loading(true)
let resp = await fetch(fs_path_url(nav.base.path)+"?zip_info")
if (resp.status >= 400) {
@@ -64,7 +65,7 @@ export const update = async () => {
console.error(err)
status = "parse_failed"
} finally {
nav.set_loading(false)
loading_finish()
}
}