Make filesystem list view columns sortable
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { formatDate } from "util/Formatting";
|
||||
import Modal from "util/Modal.svelte"
|
||||
import SortButton from "./SortButton.svelte";
|
||||
import SortButton from "layout/SortButton.svelte";
|
||||
import { flip } from "svelte/animate";
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import { flip } from "svelte/animate";
|
||||
import { formatDataVolume } from "util/Formatting";
|
||||
import SortButton from "./SortButton.svelte";
|
||||
import SortButton from "layout/SortButton.svelte";
|
||||
|
||||
export let peers = [];
|
||||
$: update_peers(peers)
|
||||
|
@@ -2,7 +2,7 @@
|
||||
import { onMount } from "svelte";
|
||||
import LoadingIndicator from "util/LoadingIndicator.svelte";
|
||||
import { formatDataVolume, formatDate } from "util/Formatting";
|
||||
import SortButton from "admin_panel/SortButton.svelte";
|
||||
import SortButton from "layout/SortButton.svelte";
|
||||
|
||||
export let user_id = ""
|
||||
let files = []
|
||||
|
@@ -39,7 +39,7 @@ export class FSNavigator {
|
||||
// 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) {
|
||||
set_loading = (b: boolean) => {
|
||||
if (this.loading !== null) {
|
||||
this.loading.set(b)
|
||||
}
|
||||
@@ -49,7 +49,7 @@ export class FSNavigator {
|
||||
// This works by implementing the store contract:
|
||||
// https://svelte.dev/docs/svelte-components#script-4-prefix-stores-with-$-to-access-their-values
|
||||
subscribers: Array<(nav: FSNavigator) => void> = []
|
||||
subscribe(sub_func: (nav: FSNavigator) => void) {
|
||||
subscribe = (sub_func: (nav: FSNavigator) => void) => {
|
||||
// Immediately return the current value
|
||||
sub_func(this)
|
||||
|
||||
@@ -58,8 +58,13 @@ export class FSNavigator {
|
||||
// Return the unsubscribe function
|
||||
return () => this.subscribers.splice(this.subscribers.indexOf(sub_func), 1)
|
||||
}
|
||||
notify_subscribers = () => {
|
||||
for (let i = 0; i < this.subscribers.length; i++) {
|
||||
this.subscribers[i](this)
|
||||
}
|
||||
}
|
||||
|
||||
async navigate(path: string, push_history: boolean) {
|
||||
navigate = async (path: string, push_history: boolean) => {
|
||||
if (path[0] !== "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
@@ -88,17 +93,17 @@ export class FSNavigator {
|
||||
}
|
||||
}
|
||||
|
||||
async navigate_up() {
|
||||
navigate_up = async () => {
|
||||
if (this.path.length > 1) {
|
||||
await this.navigate(this.path[this.path.length - 2].path, false)
|
||||
}
|
||||
}
|
||||
|
||||
async reload() {
|
||||
reload = async () => {
|
||||
await this.navigate(this.base.path, false)
|
||||
}
|
||||
|
||||
open_node(node: FSPath, push_history: boolean) {
|
||||
open_node = (node: FSPath, push_history: boolean) => {
|
||||
// Update window title and navigation history. If push_history is false
|
||||
// we still replace the URL with replaceState. This way the user is not
|
||||
// greeted to a 404 page when refreshing after renaming a file
|
||||
@@ -122,7 +127,7 @@ export class FSNavigator {
|
||||
}
|
||||
|
||||
// Sort directory children
|
||||
sort_children(node.children)
|
||||
sort_children(node.children, this.sort_last_field, this.sort_asc)
|
||||
|
||||
// Update shared state
|
||||
this.path = node.path
|
||||
@@ -137,9 +142,7 @@ export class FSNavigator {
|
||||
|
||||
// Signal to our subscribers that the new node is loaded. This triggers
|
||||
// the reactivity
|
||||
for (let i = 0; i < this.subscribers.length; i++) {
|
||||
this.subscribers[i](this)
|
||||
}
|
||||
this.notify_subscribers()
|
||||
}
|
||||
|
||||
// These are used to navigate forward and backward within a directory (using
|
||||
@@ -151,7 +154,7 @@ export class FSNavigator {
|
||||
cached_siblings_path = ""
|
||||
cached_siblings: Array<FSNode> | null = null
|
||||
|
||||
async get_siblings() {
|
||||
get_siblings = async () => {
|
||||
// If this node is a filesystem root then there are no siblings
|
||||
if (this.path.length < 2) {
|
||||
return []
|
||||
@@ -166,7 +169,7 @@ export class FSNavigator {
|
||||
const resp = await fs_get_node(this.path[this.path.length - 2].path)
|
||||
|
||||
// Sort directory children to make sure the order is consistent
|
||||
sort_children(resp.children)
|
||||
sort_children(resp.children, this.sort_last_field, this.sort_asc)
|
||||
|
||||
// Save new siblings in navigator state
|
||||
this.cached_siblings_path = this.path[this.path.length - 2].path
|
||||
@@ -179,7 +182,7 @@ export class FSNavigator {
|
||||
// Opens a sibling of the currently open file. The offset is relative to the
|
||||
// file which is currently open. Give a positive number to move forward and
|
||||
// a negative number to move backward
|
||||
async open_sibling(offset: number) {
|
||||
open_sibling = async (offset: number) => {
|
||||
if (this.path.length <= 1) {
|
||||
return
|
||||
}
|
||||
@@ -233,14 +236,51 @@ export class FSNavigator {
|
||||
console.debug("No siblings found")
|
||||
}
|
||||
}
|
||||
|
||||
sort_last_field: string = "name"
|
||||
sort_asc: boolean = true
|
||||
sort_children = (field: string) => {
|
||||
// If the field is the same as last time we invert the direction
|
||||
if (field !== "" && field === this.sort_last_field) {
|
||||
this.sort_asc = !this.sort_asc
|
||||
}
|
||||
// If the field is empty we reuse the last field
|
||||
if (field === "") {
|
||||
field = this.sort_last_field
|
||||
}
|
||||
this.sort_last_field = field
|
||||
|
||||
sort_children(this.children, field, this.sort_asc)
|
||||
|
||||
// Signal to our subscribers that the order has changed. This triggers
|
||||
// the reactivity
|
||||
this.notify_subscribers()
|
||||
}
|
||||
}
|
||||
|
||||
const sort_children = (children: Array<FSNode>) => {
|
||||
const sort_children = (children: FSNode[], field: string, asc: boolean) => {
|
||||
console.log("Sorting directory children by", field, "asc", asc)
|
||||
children.sort((a, b) => {
|
||||
// Sort directories before files
|
||||
if (a.type !== b.type) {
|
||||
return a.type === "dir" ? -1 : 1
|
||||
}
|
||||
return a.name.localeCompare(b.name, undefined, { numeric: true })
|
||||
|
||||
// If sort is descending we swap the arguments
|
||||
if (asc === false) {
|
||||
[a, b] = [b, a]
|
||||
}
|
||||
|
||||
// If the two values are equal then we force sort by name, since names
|
||||
// are always unique
|
||||
if (a[field] === b[field]) {
|
||||
return a.name.localeCompare(b.name, undefined, { numeric: true })
|
||||
} else if (typeof (a[field]) === "number") {
|
||||
// Sort ints from high to low
|
||||
return a[field] - b[field]
|
||||
} else {
|
||||
// Sort strings alphabetically
|
||||
return a[field].localeCompare(b[field], undefined, { numeric: true })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@@ -65,13 +65,6 @@ const update_shared = () => {
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<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>
|
||||
<div>
|
||||
<input
|
||||
form="edit_form"
|
||||
@@ -83,13 +76,21 @@ const update_shared = () => {
|
||||
/>
|
||||
<label for="shared">Share this file or directory</label>
|
||||
</div>
|
||||
|
||||
<div class="form_grid">
|
||||
{#if is_shared}
|
||||
<span>Your sharing link: <a href={share_link}>{share_link}</a></span>
|
||||
<span>Public link: <a href={share_link}>{share_link}</a></span>
|
||||
<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>
|
||||
|
||||
<fieldset>
|
||||
|
@@ -8,6 +8,7 @@ let dispatch = createEventDispatcher()
|
||||
export let nav: FSNavigator
|
||||
export let show_hidden = false
|
||||
export let large_icons = false
|
||||
export let hide_edit = false
|
||||
</script>
|
||||
|
||||
<div class="directory">
|
||||
@@ -28,11 +29,17 @@ export let large_icons = false
|
||||
<a
|
||||
href="/d/{child.id}"
|
||||
on:click|preventDefault|stopPropagation={e => {dispatch("node_share_click", {index: index, original: e})}}
|
||||
class="button action_button"
|
||||
class="button flat action_button"
|
||||
>
|
||||
<i class="icon" title="This file / directory is shared. Click to open public link">share</i>
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
{#if $nav.permissions.write && !hide_edit}
|
||||
<button class="action_button flat" on:click|preventDefault|stopPropagation={e => dispatch("node_settings", {index: index, original: e})}>
|
||||
<i class="icon">edit</i>
|
||||
</button>
|
||||
{/if}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -42,7 +49,7 @@ export let large_icons = false
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(20em, 1fr));
|
||||
gap: 8px;
|
||||
margin: 8px;
|
||||
padding: 8px;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -55,6 +62,7 @@ export let large_icons = false
|
||||
align-items: center;
|
||||
background: var(--input_background);
|
||||
border-radius: 4px;
|
||||
box-shadow: 1px 1px 8px 0px var(--shadow_color);
|
||||
gap: 6px;
|
||||
}
|
||||
.node:hover:not(.node_selected) {
|
||||
@@ -83,6 +91,12 @@ export let large_icons = false
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
.flat {
|
||||
background: none;
|
||||
color: var(--body_text_color);
|
||||
box-shadow: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Large icon mode only supported on wide screens */
|
||||
@media (min-width: 500px) {
|
||||
|
@@ -66,7 +66,7 @@ let node_context = (e: CustomEvent<FMNodeEvent>) => {
|
||||
}
|
||||
const node_share_click = (e: CustomEvent<FMNodeEvent>) => {
|
||||
creating_dir = false
|
||||
nav.navigate(nav.children[e.detail.index].id, true)
|
||||
edit_window.edit(nav.children[e.detail.index], false, "share")
|
||||
}
|
||||
const node_select = (e: CustomEvent<FMNodeEvent>) => {
|
||||
const index = e.detail.index
|
||||
|
@@ -189,6 +189,7 @@ onMount(() => {
|
||||
nav={nav}
|
||||
show_hidden={show_hidden}
|
||||
large_icons={large_icons}
|
||||
hide_edit
|
||||
on:node_click={node_click}
|
||||
on:node_context={node_context}
|
||||
on:node_select={node_select}
|
||||
|
@@ -3,6 +3,8 @@ import { createEventDispatcher } from "svelte";
|
||||
import { formatDataVolume } from "util/Formatting";
|
||||
import { fs_encode_path, fs_node_icon } from "filesystem/FilesystemAPI"
|
||||
import type { FSNavigator } from "filesystem/FSNavigator";
|
||||
import SortButton from "layout/SortButton.svelte";
|
||||
import { flip } from "svelte/animate";
|
||||
|
||||
let dispatch = createEventDispatcher()
|
||||
|
||||
@@ -16,8 +18,8 @@ export let hide_branding = false
|
||||
<div class="directory">
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>Name</td>
|
||||
<td class="hide_small">Size</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></td>
|
||||
</tr>
|
||||
{#each $nav.children as child, index (child.path)}
|
||||
@@ -25,6 +27,7 @@ export let hide_branding = false
|
||||
href={"/d"+fs_encode_path(child.path)}
|
||||
on:click|preventDefault={e => dispatch("node_click", {index: index, original: e})}
|
||||
on:contextmenu={e => dispatch("node_context", {index: index, original: e})}
|
||||
animate:flip={{duration: 500}}
|
||||
class="node"
|
||||
class:node_selected={child.fm_selected}
|
||||
class:hidden={child.name.startsWith(".") && !show_hidden}
|
||||
@@ -53,7 +56,7 @@ export let hide_branding = false
|
||||
<i class="icon" title="This file / directory is shared. Click to open public link">share</i>
|
||||
</a>
|
||||
{/if}
|
||||
{#if child.properties !== undefined && child.properties.branding_enabled !== undefined && !hide_branding}
|
||||
{#if child.properties !== undefined && child.properties.branding_enabled === "true" && !hide_branding}
|
||||
<button class="action_button" on:click|preventDefault|stopPropagation={e => dispatch("node_branding", {index: index, original: e})}>
|
||||
<i class="icon">palette</i>
|
||||
</button>
|
||||
@@ -78,7 +81,7 @@ export let hide_branding = false
|
||||
border-radius: 8px;
|
||||
|
||||
max-width: 99%;
|
||||
width: 1000px;
|
||||
width: 1200px;
|
||||
}
|
||||
.directory > * {
|
||||
display: table-row;
|
||||
@@ -120,7 +123,7 @@ td {
|
||||
word-break: break-all;
|
||||
}
|
||||
.node_size {
|
||||
min-width: 50px;
|
||||
min-width: 5em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.icons_wrap {
|
||||
|
@@ -1,17 +1,13 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
export let field = ""
|
||||
export let active_field = ""
|
||||
export let asc = true
|
||||
export let sort_func
|
||||
export let sort_func: (field: string) => void
|
||||
</script>
|
||||
|
||||
<button class:button_highlight={active_field === field} on:click={() => sort_func(field)}>
|
||||
{#if active_field === field}
|
||||
{#if asc}
|
||||
↓
|
||||
{:else}
|
||||
↑
|
||||
{/if}
|
||||
{#if asc}↓{:else}↑{/if}
|
||||
{/if}
|
||||
<slot></slot>
|
||||
</button>
|
@@ -9,7 +9,7 @@ import SuccessMessage from "util/SuccessMessage.svelte";
|
||||
let loading = false
|
||||
let success_message
|
||||
let hotlinking = window.user.hotlinking_enabled
|
||||
let transfer_cap = window.user.monthly_transfer_cap / 1e9
|
||||
let transfer_cap = window.user.monthly_transfer_cap / 1e12
|
||||
let skip_viewer = window.user.skip_file_viewer
|
||||
|
||||
const update = async () => {
|
||||
@@ -17,7 +17,7 @@ const update = async () => {
|
||||
|
||||
const form = new FormData()
|
||||
form.append("hotlinking_enabled", hotlinking)
|
||||
form.append("transfer_cap", transfer_cap*1e9)
|
||||
form.append("transfer_cap", transfer_cap*1e12)
|
||||
form.append("skip_file_viewer", skip_viewer)
|
||||
|
||||
try {
|
||||
@@ -31,7 +31,7 @@ const update = async () => {
|
||||
}
|
||||
|
||||
window.user.hotlinking_enabled = hotlinking
|
||||
window.user.monthly_transfer_cap = transfer_cap*1e9
|
||||
window.user.monthly_transfer_cap = transfer_cap*1e12
|
||||
|
||||
success_message.set(true, "Sharing settings updated")
|
||||
} catch (err) {
|
||||
@@ -102,11 +102,12 @@ onMount(() => {
|
||||
|
||||
<h2><Pro/>Bill shock limit</h2>
|
||||
<p>
|
||||
Billshock limit in gigabytes per month (1 TB = 1000 GB). Set to 0 to disable.
|
||||
Billshock limit in terabytes per month (1 TB = 1000 GB). Set to 0 to
|
||||
disable.
|
||||
</p>
|
||||
<form on:submit|preventDefault={update} class="billshock_container">
|
||||
<input type="number" bind:value={transfer_cap} step="100" min="0"/>
|
||||
<div style="margin: 0.5em;">GB</div>
|
||||
<input type="number" bind:value={transfer_cap} step="0.1" min="0" style="width: 5em;"/>
|
||||
<div style="margin: 0.5em;">TB</div>
|
||||
<button type="submit">
|
||||
<i class="icon">save</i> Save
|
||||
</button>
|
||||
@@ -114,9 +115,9 @@ onMount(() => {
|
||||
|
||||
<p>
|
||||
Bandwidth used in the last 30 days: {formatDataVolume(transfer_used, 3)},
|
||||
new limit: {formatDataVolume(transfer_cap*1e9, 3)}
|
||||
new limit: {formatDataVolume(transfer_cap*1e12, 3)}
|
||||
</p>
|
||||
<ProgressBar used={transfer_used} total={transfer_cap*1e9}></ProgressBar>
|
||||
<ProgressBar used={transfer_used} total={transfer_cap*1e12}></ProgressBar>
|
||||
<p>
|
||||
The billshock limit limits how much bandwidth your account can use in a
|
||||
30 day window. When this limit is reached hotlinking will be disabled
|
||||
|
Reference in New Issue
Block a user