Convert multiple pages into SPA
This commit is contained in:
@@ -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;
|
||||
|
@@ -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";
|
||||
|
@@ -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
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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"))
|
||||
}
|
@@ -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";
|
||||
|
@@ -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>
|
||||
|
@@ -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
|
||||
|
@@ -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))
|
||||
|
||||
|
@@ -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";
|
||||
|
@@ -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
|
||||
|
@@ -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";
|
||||
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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;
|
||||
|
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
|
@@ -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 }
|
||||
|
64
svelte/src/filesystem/filemanager/FileMenu.svelte
Normal file
64
svelte/src/filesystem/filemanager/FileMenu.svelte
Normal 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>
|
@@ -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>
|
||||
|
@@ -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;
|
||||
|
@@ -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 {
|
||||
|
@@ -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 {
|
||||
|
@@ -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%;
|
||||
|
@@ -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 = (
|
||||
|
@@ -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}/>
|
||||
|
@@ -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}
|
||||
|
@@ -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";
|
||||
|
@@ -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}>
|
||||
|
@@ -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();
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
||||
|
@@ -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()
|
||||
|
||||
|
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user