@axium/storage 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/List.svelte +13 -11
- package/lib/Usage.svelte +10 -8
- package/package.json +2 -2
- package/routes/files/+layout.svelte +1 -4
- package/routes/files/[id]/+page.svelte +2 -1
- package/routes/files/trash/+page.svelte +40 -20
- package/routes/files/usage/+page.svelte +9 -68
- package/styles/list.css +10 -10
package/lib/List.svelte
CHANGED
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
import type { StorageItemMetadata } from '@axium/storage/common';
|
|
7
7
|
import '../styles/list.css';
|
|
8
8
|
|
|
9
|
-
let {
|
|
9
|
+
let {
|
|
10
|
+
items = $bindable([]),
|
|
11
|
+
appMode,
|
|
12
|
+
emptyText = 'Folder is empty.',
|
|
13
|
+
}: { appMode?: boolean; items: StorageItemMetadata[]; emptyText?: string } = $props();
|
|
10
14
|
|
|
11
15
|
let activeIndex = $state<number>(-1);
|
|
12
16
|
let activeItem = $derived(activeIndex == -1 ? null : items[activeIndex]);
|
|
@@ -40,7 +44,7 @@
|
|
|
40
44
|
<dfn title={item.type}><Icon i={iconForMime(item.type)} /></dfn>
|
|
41
45
|
<span class="name">{item.name}</span>
|
|
42
46
|
<span>{item.modifiedAt.toLocaleString()}</span>
|
|
43
|
-
<span>{formatBytes(item.size)}</span>
|
|
47
|
+
<span>{item.type == 'inode/directory' ? '—' : formatBytes(item.size)}</span>
|
|
44
48
|
{@render action('rename', 'pencil', i)}
|
|
45
49
|
{@render action('download', 'download', i)}
|
|
46
50
|
{@render action('trash', 'trash', i)}
|
|
@@ -61,7 +65,7 @@
|
|
|
61
65
|
{@render _item(item, i)}
|
|
62
66
|
{/if}
|
|
63
67
|
{:else}
|
|
64
|
-
<p class="list-empty">
|
|
68
|
+
<p class="list-empty">{emptyText}</p>
|
|
65
69
|
{/each}
|
|
66
70
|
</div>
|
|
67
71
|
|
|
@@ -69,8 +73,9 @@
|
|
|
69
73
|
bind:dialog={dialogs.rename}
|
|
70
74
|
submitText="Rename"
|
|
71
75
|
submit={async (data: { name: string }) => {
|
|
72
|
-
|
|
73
|
-
activeItem
|
|
76
|
+
if (!activeItem) throw 'No item is selected';
|
|
77
|
+
await updateItemMetadata(activeItem.id, data);
|
|
78
|
+
activeItem.name = data.name;
|
|
74
79
|
}}
|
|
75
80
|
>
|
|
76
81
|
<div>
|
|
@@ -83,8 +88,9 @@
|
|
|
83
88
|
submitText="Trash"
|
|
84
89
|
submitDanger
|
|
85
90
|
submit={async () => {
|
|
86
|
-
|
|
87
|
-
|
|
91
|
+
if (!activeItem) throw 'No item is selected';
|
|
92
|
+
await updateItemMetadata(activeItem.id, { trash: true });
|
|
93
|
+
items.splice(activeIndex, 1);
|
|
88
94
|
}}
|
|
89
95
|
>
|
|
90
96
|
<p>Are you sure you want to trash {@render _itemName()}?</p>
|
|
@@ -107,10 +113,6 @@
|
|
|
107
113
|
|
|
108
114
|
<style>
|
|
109
115
|
.list-item {
|
|
110
|
-
display: grid;
|
|
111
116
|
grid-template-columns: 1em 4fr 15em 5em repeat(3, 1em);
|
|
112
|
-
align-items: center;
|
|
113
|
-
gap: 0.5em;
|
|
114
|
-
padding: 0.5em 0;
|
|
115
117
|
}
|
|
116
118
|
</style>
|
package/lib/Usage.svelte
CHANGED
|
@@ -8,14 +8,16 @@
|
|
|
8
8
|
</script>
|
|
9
9
|
|
|
10
10
|
{#await info || getUserStorageInfo(userId) then info}
|
|
11
|
-
<
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
<p>
|
|
12
|
+
<a href="/files/usage">
|
|
13
|
+
<NumberBar
|
|
14
|
+
max={info.limits.user_size * 1_000_000}
|
|
15
|
+
value={info.usage.bytes}
|
|
16
|
+
text="Using {formatBytes(info.usage.bytes)} of {formatBytes(info.limits.user_size * 1_000_000)}"
|
|
17
|
+
--fill="#345"
|
|
18
|
+
/>
|
|
19
|
+
</a>
|
|
20
|
+
</p>
|
|
19
21
|
{:catch error}
|
|
20
22
|
<p>Couldn't load your uploads.</p>
|
|
21
23
|
<p>{error.message}</p>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axium/storage",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"author": "James Prevett <axium@jamespre.dev> (https://jamespre.dev)",
|
|
5
5
|
"description": "User file storage for Axium",
|
|
6
6
|
"funding": {
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"@axium/client": ">=0.1.0",
|
|
42
42
|
"@axium/core": ">=0.5.0",
|
|
43
|
-
"@axium/server": ">=0.
|
|
43
|
+
"@axium/server": ">=0.20.2",
|
|
44
44
|
"@sveltejs/kit": "^2.27.3",
|
|
45
45
|
"utilium": "^2.3.8"
|
|
46
46
|
},
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<div class="app">
|
|
11
11
|
<div class="sidebar">
|
|
12
12
|
{#each data.tabs as { href, name, icon: i, active }}
|
|
13
|
-
<a {href} class={['item', active && 'active']}><Icon {i} /> {capitalize(name)}</a>
|
|
13
|
+
<a {href} class={['item', 'icon-text', active && 'active']}><Icon {i} /> {capitalize(name)}</a>
|
|
14
14
|
{/each}
|
|
15
15
|
|
|
16
16
|
<div class="usage">
|
|
@@ -42,9 +42,6 @@
|
|
|
42
42
|
.item {
|
|
43
43
|
padding: 0.3em 0.5em;
|
|
44
44
|
border-radius: 0.25em 1em 1em 0.25em;
|
|
45
|
-
display: inline-flex;
|
|
46
|
-
align-items: center;
|
|
47
|
-
gap: 1em;
|
|
48
45
|
}
|
|
49
46
|
|
|
50
47
|
.item:hover {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
const { data }: PageProps = $props();
|
|
8
8
|
|
|
9
|
+
let items = $state(data.items!);
|
|
9
10
|
const item = $state(data.item);
|
|
10
11
|
</script>
|
|
11
12
|
|
|
@@ -24,7 +25,7 @@
|
|
|
24
25
|
<Icon i="trash-can-undo" /> Restore
|
|
25
26
|
</button>
|
|
26
27
|
{:else if item.type == 'inode/directory'}
|
|
27
|
-
<StorageList appMode bind:items
|
|
28
|
+
<StorageList appMode bind:items />
|
|
28
29
|
{:else}
|
|
29
30
|
<p>No preview available.</p>
|
|
30
31
|
{/if}
|
|
@@ -2,16 +2,26 @@
|
|
|
2
2
|
import { formatBytes } from '@axium/core/format';
|
|
3
3
|
import { forMime as iconForMime } from '@axium/core/icons';
|
|
4
4
|
import { FormDialog, Icon } from '@axium/server/components';
|
|
5
|
-
import { deleteItem } from '@axium/storage/client';
|
|
5
|
+
import { deleteItem, updateItemMetadata } from '@axium/storage/client';
|
|
6
6
|
import '@axium/storage/styles/list';
|
|
7
7
|
import type { PageProps } from './$types';
|
|
8
8
|
|
|
9
9
|
const { data }: PageProps = $props();
|
|
10
10
|
let items = $state(data.items);
|
|
11
|
-
let
|
|
11
|
+
let restoreDialog = $state<HTMLDialogElement>()!;
|
|
12
|
+
let deleteDialog = $state<HTMLDialogElement>()!;
|
|
12
13
|
|
|
13
14
|
let activeIndex = $state<number>(-1);
|
|
14
|
-
const activeItem = $derived(items[activeIndex]);
|
|
15
|
+
const activeItem = $derived(activeIndex == -1 ? null : items[activeIndex]);
|
|
16
|
+
|
|
17
|
+
function action(index: number, dialog: () => HTMLDialogElement) {
|
|
18
|
+
return (e: Event) => {
|
|
19
|
+
e.stopPropagation();
|
|
20
|
+
e.preventDefault();
|
|
21
|
+
activeIndex = index;
|
|
22
|
+
dialog().showModal();
|
|
23
|
+
};
|
|
24
|
+
}
|
|
15
25
|
</script>
|
|
16
26
|
|
|
17
27
|
<svelte:head>
|
|
@@ -31,16 +41,11 @@
|
|
|
31
41
|
<span class="name">{item.name}</span>
|
|
32
42
|
<span>{item.modifiedAt.toLocaleString()}</span>
|
|
33
43
|
<span>{formatBytes(item.size)}</span>
|
|
34
|
-
<span
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
activeIndex = i;
|
|
40
|
-
dialog?.showModal();
|
|
41
|
-
}}
|
|
42
|
-
>
|
|
43
|
-
<Icon i="trash" --size="14px" --fill="#c44" />
|
|
44
|
+
<span class="action" onclick={action(i, () => restoreDialog)}>
|
|
45
|
+
<Icon i="rotate-left" --size="14px" />
|
|
46
|
+
</span>
|
|
47
|
+
<span class="action" onclick={action(i, () => deleteDialog)}>
|
|
48
|
+
<Icon i="trash-can-xmark" --size="14px" --fill="#c44" />
|
|
44
49
|
</span>
|
|
45
50
|
</div>
|
|
46
51
|
{:else}
|
|
@@ -48,25 +53,40 @@
|
|
|
48
53
|
{/each}
|
|
49
54
|
</div>
|
|
50
55
|
|
|
56
|
+
{#snippet _name()}
|
|
57
|
+
{#if activeItem?.name}<strong>{activeItem.name.length > 23 ? activeItem.name.slice(0, 20) + '...' : activeItem.name}</strong>
|
|
58
|
+
{:else}this
|
|
59
|
+
{/if}
|
|
60
|
+
{/snippet}
|
|
61
|
+
|
|
62
|
+
<FormDialog
|
|
63
|
+
bind:dialog={restoreDialog}
|
|
64
|
+
submitText="Restore"
|
|
65
|
+
submit={async () => {
|
|
66
|
+
if (!activeItem) throw 'No item is selected';
|
|
67
|
+
await updateItemMetadata(activeItem.id, { trash: false });
|
|
68
|
+
items.splice(activeIndex, 1);
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
<p>Restore {@render _name()}?</p>
|
|
72
|
+
</FormDialog>
|
|
51
73
|
<FormDialog
|
|
52
|
-
bind:dialog
|
|
74
|
+
bind:dialog={deleteDialog}
|
|
53
75
|
submitText="Delete"
|
|
54
76
|
submitDanger
|
|
55
77
|
submit={async () => {
|
|
78
|
+
if (!activeItem) throw 'No item is selected';
|
|
56
79
|
await deleteItem(activeItem.id);
|
|
57
|
-
|
|
80
|
+
items.splice(activeIndex, 1);
|
|
58
81
|
}}
|
|
59
82
|
>
|
|
60
83
|
<p>
|
|
61
|
-
Are you sure you want to permanently delete
|
|
62
|
-
{#if activeItem?.name}<strong>{activeItem.name.length > 23 ? activeItem.name.slice(0, 20) + '...' : activeItem.name}</strong>
|
|
63
|
-
{:else}this
|
|
64
|
-
{/if}?
|
|
84
|
+
Are you sure you want to permanently delete {@render _name()}?
|
|
65
85
|
</p>
|
|
66
86
|
</FormDialog>
|
|
67
87
|
|
|
68
88
|
<style>
|
|
69
89
|
.list-item {
|
|
70
|
-
grid-template-columns: 1em 4fr 15em 5em 1em
|
|
90
|
+
grid-template-columns: 1em 4fr 15em 5em 1em 1em;
|
|
71
91
|
}
|
|
72
92
|
</style>
|
|
@@ -4,14 +4,13 @@
|
|
|
4
4
|
import { FormDialog, Icon, NumberBar } from '@axium/server/components';
|
|
5
5
|
import { deleteItem, updateItemMetadata } from '@axium/storage/client';
|
|
6
6
|
import type { StorageItemUpdate } from '@axium/storage/common';
|
|
7
|
+
import { StorageList } from '@axium/storage/components';
|
|
8
|
+
import '@axium/storage/styles/list';
|
|
7
9
|
|
|
8
10
|
const { data } = $props();
|
|
9
|
-
const {
|
|
10
|
-
info: { limits },
|
|
11
|
-
session,
|
|
12
|
-
} = data;
|
|
11
|
+
const { limits } = data.info;
|
|
13
12
|
|
|
14
|
-
|
|
13
|
+
let items = $state(data.info.items.filter(i => i.type != 'inode/directory').sort((a, b) => Math.sign(b.size - a.size)));
|
|
15
14
|
const usage = $state(data.info.usage);
|
|
16
15
|
|
|
17
16
|
let dialogs = $state<Record<string, HTMLDialogElement>>({});
|
|
@@ -23,71 +22,13 @@
|
|
|
23
22
|
</svelte:head>
|
|
24
23
|
|
|
25
24
|
{#snippet action(name: string, i: string = 'pen')}
|
|
26
|
-
<
|
|
25
|
+
<span class="action" onclick={() => dialogs[name].showModal()}>
|
|
27
26
|
<Icon {i} --size="16px" />
|
|
28
|
-
</
|
|
27
|
+
</span>
|
|
29
28
|
{/snippet}
|
|
30
29
|
|
|
31
|
-
<
|
|
32
|
-
<div class="list main">
|
|
33
|
-
<h2>Storage Usage</h2>
|
|
30
|
+
<h2>Storage Usage</h2>
|
|
34
31
|
|
|
35
|
-
|
|
32
|
+
<p><NumberBar max={limits.user_size * 1_000_000} value={usage?.bytes} text={barText} --fill="#345" /></p>
|
|
36
33
|
|
|
37
|
-
|
|
38
|
-
<div class="item">
|
|
39
|
-
<Icon i={forMime(item.type)} />
|
|
40
|
-
<p>{item.name}</p>
|
|
41
|
-
<p>{item.type}</p>
|
|
42
|
-
<p>Owned by {item.userId === session?.userId ? 'You' : item.userId}</p>
|
|
43
|
-
<p>{formatBytes(item.size)}</p>
|
|
44
|
-
<p>Uploaded {item.modifiedAt.toLocaleString()}</p>
|
|
45
|
-
<span>{@render action('rename#' + item.id)}</span>
|
|
46
|
-
<span>{@render action('delete#' + item.id, 'trash')}</span>
|
|
47
|
-
</div>
|
|
48
|
-
<FormDialog
|
|
49
|
-
bind:dialog={dialogs['rename#' + item.id]}
|
|
50
|
-
submit={(data: StorageItemUpdate) => updateItemMetadata(item.id, data).then(n => (item.name = n.name))}
|
|
51
|
-
submitText="Update"
|
|
52
|
-
>
|
|
53
|
-
<div>
|
|
54
|
-
<label for="name">Name</label>
|
|
55
|
-
<input name="name" type="text" value={item.name || ''} required />
|
|
56
|
-
</div>
|
|
57
|
-
</FormDialog>
|
|
58
|
-
<FormDialog
|
|
59
|
-
bind:dialog={dialogs['delete#' + item.id]}
|
|
60
|
-
submit={async (data: StorageItemUpdate) => {
|
|
61
|
-
await deleteItem(item.id);
|
|
62
|
-
dialogs['delete#' + item.id].close();
|
|
63
|
-
items.splice(items.indexOf(item), 1);
|
|
64
|
-
}}
|
|
65
|
-
submitText="Delete"
|
|
66
|
-
submitDanger
|
|
67
|
-
>
|
|
68
|
-
<p>
|
|
69
|
-
Are you sure you want to delete this file?<br />
|
|
70
|
-
This action can't be undone.
|
|
71
|
-
</p>
|
|
72
|
-
</FormDialog>
|
|
73
|
-
{/each}
|
|
74
|
-
</div>
|
|
75
|
-
</div>
|
|
76
|
-
|
|
77
|
-
<style>
|
|
78
|
-
.list {
|
|
79
|
-
width: 80%;
|
|
80
|
-
padding-top: 4em;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
.item {
|
|
84
|
-
display: grid;
|
|
85
|
-
align-items: center;
|
|
86
|
-
width: 100%;
|
|
87
|
-
gap: 1em;
|
|
88
|
-
text-wrap: nowrap;
|
|
89
|
-
border-top: 1px solid #8888;
|
|
90
|
-
padding-bottom: 1em;
|
|
91
|
-
grid-template-columns: 2em 1.5fr 1fr 1fr 5em 1fr 2em 2em;
|
|
92
|
-
}
|
|
93
|
-
</style>
|
|
34
|
+
<StorageList bind:items emptyText="You have not uploaded any files yet." />
|
package/styles/list.css
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
padding: 0.5em;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
.list-
|
|
7
|
+
.list-header {
|
|
8
8
|
font-weight: bold;
|
|
9
|
-
border-bottom:
|
|
9
|
+
border-bottom: 1.5px solid #bbc;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
.list-item-container {
|
|
@@ -18,17 +18,20 @@
|
|
|
18
18
|
display: grid;
|
|
19
19
|
grid-template-columns: 1em 4fr 15em 5em repeat(3, 1em);
|
|
20
20
|
align-items: center;
|
|
21
|
-
gap:
|
|
22
|
-
padding: 0.5em
|
|
21
|
+
gap: 1em;
|
|
22
|
+
padding: 0.5em;
|
|
23
|
+
overflow: hidden;
|
|
24
|
+
text-wrap: nowrap;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
.list-item:not(:
|
|
26
|
-
border-
|
|
27
|
+
.list-item:not(.list-header, :first-child) {
|
|
28
|
+
border-top: 1px solid #bbc;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
.list-item:not(.list-header):hover {
|
|
30
32
|
background-color: #7777;
|
|
31
33
|
}
|
|
34
|
+
|
|
32
35
|
p.list-empty {
|
|
33
36
|
text-align: center;
|
|
34
37
|
color: #888;
|
|
@@ -40,10 +43,7 @@ p.list-empty {
|
|
|
40
43
|
visibility: hidden;
|
|
41
44
|
}
|
|
42
45
|
|
|
43
|
-
.item:hover .action {
|
|
46
|
+
.list-item:hover .action {
|
|
44
47
|
visibility: visible;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
.action:hover {
|
|
48
48
|
cursor: pointer;
|
|
49
49
|
}
|