@axium/storage 0.18.8 → 0.18.9
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/dist/client/web_hook.d.ts +8 -0
- package/dist/client/web_hook.js +4 -0
- package/lib/Add.svelte +10 -9
- package/lib/List.svelte +32 -26
- package/lib/Preview.svelte +15 -14
- package/lib/Sidebar.svelte +3 -2
- package/lib/SidebarItem.svelte +19 -22
- package/lib/Usage.svelte +3 -2
- package/locales/en.json +96 -0
- package/package.json +4 -4
- package/routes/files/+layout.ts +9 -3
- package/routes/files/+page.svelte +2 -1
- package/routes/files/[id]/+page.svelte +17 -15
- package/routes/files/shared/+page.svelte +3 -2
- package/routes/files/trash/+page.svelte +17 -17
- package/routes/files/usage/+page.svelte +9 -4
package/lib/Add.svelte
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { FormDialog, Icon, Popover, Upload } from '@axium/client/components';
|
|
4
4
|
import { uploadItem } from '@axium/storage/client';
|
|
5
5
|
import type { StorageItemMetadata } from '@axium/storage/common';
|
|
6
|
+
import { text } from '@axium/client';
|
|
6
7
|
|
|
7
8
|
const { parentId, onAdd }: { parentId?: string; onAdd?(item: StorageItemMetadata): void } = $props();
|
|
8
9
|
|
|
@@ -31,18 +32,18 @@
|
|
|
31
32
|
|
|
32
33
|
<Popover>
|
|
33
34
|
{#snippet toggle()}
|
|
34
|
-
<button class="icon-text"><Icon i="plus" />Add</button>
|
|
35
|
+
<button class="icon-text"><Icon i="plus" />{text('storage.Add.text')}</button>
|
|
35
36
|
{/snippet}
|
|
36
37
|
|
|
37
|
-
<span class="menu-item" onclick={() => uploadDialog.showModal()}><Icon i="upload" />
|
|
38
|
-
{@render _item('inode/directory', '
|
|
39
|
-
{@render _item('text/plain', '
|
|
40
|
-
{@render _item('text/x-uri', '
|
|
38
|
+
<span class="menu-item" onclick={() => uploadDialog.showModal()}><Icon i="upload" />{text('storage.Add.upload')}</span>
|
|
39
|
+
{@render _item('inode/directory', text('storage.Add.new_folder'))}
|
|
40
|
+
{@render _item('text/plain', text('storage.Add.plain_text'))}
|
|
41
|
+
{@render _item('text/x-uri', text('storage.Add.url'), true)}
|
|
41
42
|
</Popover>
|
|
42
43
|
|
|
43
44
|
<FormDialog
|
|
44
45
|
bind:dialog={uploadDialog}
|
|
45
|
-
submitText=
|
|
46
|
+
submitText={text('storage.Add.upload')}
|
|
46
47
|
cancel={() => (files = new DataTransfer().files)}
|
|
47
48
|
submit={async () => {
|
|
48
49
|
for (const [i, file] of Array.from(files!).entries()) {
|
|
@@ -61,7 +62,7 @@
|
|
|
61
62
|
|
|
62
63
|
<FormDialog
|
|
63
64
|
bind:dialog={createDialog}
|
|
64
|
-
submitText=
|
|
65
|
+
submitText={text('storage.Add.create')}
|
|
65
66
|
submit={async (data: { name: string; content?: string }) => {
|
|
66
67
|
const file = new File(createIncludesContent ? [data.content!] : [], data.name, { type: createType });
|
|
67
68
|
const item = await uploadItem(file, { parentId });
|
|
@@ -69,12 +70,12 @@
|
|
|
69
70
|
}}
|
|
70
71
|
>
|
|
71
72
|
<div>
|
|
72
|
-
<label for="name">
|
|
73
|
+
<label for="name">{text('storage.generic.name')}</label>
|
|
73
74
|
<input name="name" type="text" required />
|
|
74
75
|
</div>
|
|
75
76
|
{#if createIncludesContent}
|
|
76
77
|
<div>
|
|
77
|
-
<label for="content">
|
|
78
|
+
<label for="content">{text('storage.Add.content')}</label>
|
|
78
79
|
<input name="content" type="text" size="40" required />
|
|
79
80
|
</div>
|
|
80
81
|
{/if}
|
package/lib/List.svelte
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { text } from '@axium/client';
|
|
2
3
|
import { contextMenu } from '@axium/client/attachments';
|
|
3
4
|
import { AccessControlDialog, FormDialog, Icon } from '@axium/client/components';
|
|
4
5
|
import '@axium/client/styles/list';
|
|
@@ -14,12 +15,17 @@
|
|
|
14
15
|
let {
|
|
15
16
|
items = $bindable(),
|
|
16
17
|
appMode,
|
|
17
|
-
emptyText = '
|
|
18
|
+
emptyText = text('storage.List.empty'),
|
|
18
19
|
user,
|
|
19
20
|
}: { appMode?: boolean; items: (StorageItemMetadata & AccessControllable)[]; emptyText?: string; user?: UserPublic } = $props();
|
|
20
21
|
|
|
21
22
|
let activeIndex = $state<number>(0);
|
|
22
23
|
const activeItem = $derived(items[activeIndex]);
|
|
24
|
+
const activeItemName = $derived(
|
|
25
|
+
activeItem?.name
|
|
26
|
+
? `<strong>${activeItem.name.length > 23 ? activeItem.name.slice(0, 20) + '...' : activeItem.name}</strong>`
|
|
27
|
+
: 'this'
|
|
28
|
+
);
|
|
23
29
|
const dialogs = $state<Record<string, HTMLDialogElement>>({});
|
|
24
30
|
</script>
|
|
25
31
|
|
|
@@ -35,20 +41,12 @@
|
|
|
35
41
|
</span>
|
|
36
42
|
{/snippet}
|
|
37
43
|
|
|
38
|
-
{#snippet _itemName()}
|
|
39
|
-
{#if activeItem?.name}
|
|
40
|
-
<strong>{activeItem.name.length > 23 ? activeItem.name.slice(0, 20) + '...' : activeItem.name}</strong>
|
|
41
|
-
{:else}
|
|
42
|
-
this
|
|
43
|
-
{/if}
|
|
44
|
-
{/snippet}
|
|
45
|
-
|
|
46
44
|
<div class="list">
|
|
47
45
|
<div class="list-item list-header">
|
|
48
46
|
<span></span>
|
|
49
|
-
<span>
|
|
50
|
-
<span>
|
|
51
|
-
<span>
|
|
47
|
+
<span>{text('storage.generic.name')}</span>
|
|
48
|
+
<span>{text('storage.List.last_modified')}</span>
|
|
49
|
+
<span>{text('storage.List.size')}</span>
|
|
52
50
|
</div>
|
|
53
51
|
{#each items as item, i (item.id)}
|
|
54
52
|
<div
|
|
@@ -61,12 +59,20 @@
|
|
|
61
59
|
else items = await getDirectoryMetadata(item.id);
|
|
62
60
|
}}
|
|
63
61
|
{@attach contextMenu(
|
|
64
|
-
{ i: 'pencil', text: '
|
|
65
|
-
{
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
62
|
+
{ i: 'pencil', text: text('storage.generic.rename'), action: () => ((activeIndex = i), dialogs.rename.showModal()) },
|
|
63
|
+
{
|
|
64
|
+
i: 'user-group',
|
|
65
|
+
text: text('storage.List.share'),
|
|
66
|
+
action: () => ((activeIndex = i), dialogs['share:' + item.id].showModal()),
|
|
67
|
+
},
|
|
68
|
+
{ i: 'download', text: text('storage.generic.download'), action: () => ((activeIndex = i), dialogs.download.showModal()) },
|
|
69
|
+
{ i: 'link-horizontal', text: text('storage.List.copy_link'), action: () => ((activeIndex = i), copyShortURL(item)) },
|
|
70
|
+
{ i: 'trash', text: text('storage.generic.trash'), action: () => ((activeIndex = i), dialogs.trash.showModal()) },
|
|
71
|
+
user?.preferences?.debug && {
|
|
72
|
+
i: 'hashtag',
|
|
73
|
+
text: text('storage.generic.copy_id'),
|
|
74
|
+
action: () => copy('text/plain', item.id),
|
|
75
|
+
}
|
|
70
76
|
)}
|
|
71
77
|
>
|
|
72
78
|
<dfn class="type" title={item.type}><Icon i={iconForMime(item.type)} /></dfn>
|
|
@@ -118,33 +124,33 @@
|
|
|
118
124
|
|
|
119
125
|
<FormDialog
|
|
120
126
|
bind:dialog={dialogs.rename}
|
|
121
|
-
submitText=
|
|
127
|
+
submitText={text('storage.generic.rename')}
|
|
122
128
|
submit={async (data: { name: string }) => {
|
|
123
|
-
if (!activeItem) throw '
|
|
129
|
+
if (!activeItem) throw text('storage.generic.no_item');
|
|
124
130
|
await updateItemMetadata(activeItem.id, data);
|
|
125
131
|
activeItem.name = data.name;
|
|
126
132
|
}}
|
|
127
133
|
>
|
|
128
134
|
<div>
|
|
129
|
-
<label for="name">
|
|
135
|
+
<label for="name">{text('storage.generic.name')}</label>
|
|
130
136
|
<input name="name" type="text" required value={activeItem?.name} />
|
|
131
137
|
</div>
|
|
132
138
|
</FormDialog>
|
|
133
139
|
<FormDialog
|
|
134
140
|
bind:dialog={dialogs.trash}
|
|
135
|
-
submitText=
|
|
141
|
+
submitText={text('storage.generic.trash')}
|
|
136
142
|
submitDanger
|
|
137
143
|
submit={async () => {
|
|
138
|
-
if (!activeItem) throw '
|
|
144
|
+
if (!activeItem) throw text('storage.generic.no_item');
|
|
139
145
|
await updateItemMetadata(activeItem.id, { trash: true });
|
|
140
146
|
items.splice(activeIndex, 1);
|
|
141
147
|
}}
|
|
142
148
|
>
|
|
143
|
-
<p>
|
|
149
|
+
<p>{@html text('storage.List.trash_confirm', { $html: true, name: activeItemName })}</p>
|
|
144
150
|
</FormDialog>
|
|
145
151
|
<FormDialog
|
|
146
152
|
bind:dialog={dialogs.download}
|
|
147
|
-
submitText=
|
|
153
|
+
submitText={text('storage.generic.download')}
|
|
148
154
|
submit={async () => {
|
|
149
155
|
if (activeItem!.type == 'inode/directory') {
|
|
150
156
|
/** @todo ZIP support */
|
|
@@ -153,7 +159,7 @@
|
|
|
153
159
|
} else open(activeItem!.dataURL, '_blank');
|
|
154
160
|
}}
|
|
155
161
|
>
|
|
156
|
-
<p>
|
|
162
|
+
<p>{@html text('storage.generic.download_confirm', { $html: true, name: activeItemName })}</p>
|
|
157
163
|
</FormDialog>
|
|
158
164
|
|
|
159
165
|
<style>
|
package/lib/Preview.svelte
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { text } from '@axium/client';
|
|
2
3
|
import { FormDialog, Icon, Popover } from '@axium/client/components';
|
|
3
4
|
import type { AccessControllable } from '@axium/core';
|
|
4
5
|
import { downloadItem, getDirectoryMetadata, updateItemMetadata } from '@axium/storage/client';
|
|
@@ -35,7 +36,7 @@
|
|
|
35
36
|
{#if itemOpeners.length}
|
|
36
37
|
{@const [first, ...others] = itemOpeners}
|
|
37
38
|
<div class="openers">
|
|
38
|
-
<span>
|
|
39
|
+
<span>{text('storage.Preview.open_with')} <a href={first.openURL(item)} target="_blank">{first.name}</a></span>
|
|
39
40
|
{#if others.length}
|
|
40
41
|
<Popover>
|
|
41
42
|
{#snippet toggle()}
|
|
@@ -79,20 +80,20 @@
|
|
|
79
80
|
{:else if item.type == 'application/pdf'}
|
|
80
81
|
<object data={item.dataURL} type="application/pdf" width="100%" height="100%">
|
|
81
82
|
<embed src={item.dataURL} type="application/pdf" width="100%" height="100%" />
|
|
82
|
-
<
|
|
83
|
+
<a href={item.dataURL} download={item.name}>{text('storage.Preview.pdf_fallback_download')}</a>
|
|
83
84
|
</object>
|
|
84
85
|
{:else if item.type.startsWith('text/')}
|
|
85
86
|
{#await downloadItem(item.id).then(b => b.text())}
|
|
86
87
|
<div class="full-fill no-preview">
|
|
87
88
|
<Icon i="cloud-arrow-down" --size="50px" />
|
|
88
|
-
<span>
|
|
89
|
+
<span>{text('storage.Preview.loading')}</span>
|
|
89
90
|
</div>
|
|
90
91
|
{:then content}
|
|
91
92
|
<pre class="full-fill preview-text">{content}</pre>
|
|
92
93
|
{:catch}
|
|
93
94
|
<div class="full-fill no-preview">
|
|
94
95
|
<Icon i="cloud-exclamation" --size="50px" />
|
|
95
|
-
<span>
|
|
96
|
+
<span>{text('storage.Preview.error_loading')}</span>
|
|
96
97
|
</div>
|
|
97
98
|
{/await}
|
|
98
99
|
{:else if previews.has(item.type)}
|
|
@@ -100,39 +101,39 @@
|
|
|
100
101
|
{:else}
|
|
101
102
|
<div class="full-fill no-preview">
|
|
102
103
|
<Icon i="eye-slash" --size="50px" />
|
|
103
|
-
<span>Preview
|
|
104
|
+
<span>{text('storage.Preview.preview_unavailable')}</span>
|
|
104
105
|
</div>
|
|
105
106
|
{/if}
|
|
106
107
|
</div>
|
|
107
108
|
|
|
108
109
|
<FormDialog
|
|
109
110
|
bind:dialog={dialogs.rename}
|
|
110
|
-
submitText=
|
|
111
|
+
submitText={text('storage.generic.rename')}
|
|
111
112
|
submit={async (data: { name: string }) => {
|
|
112
113
|
await updateItemMetadata(item.id, data);
|
|
113
114
|
item.name = data.name;
|
|
114
115
|
}}
|
|
115
116
|
>
|
|
116
117
|
<div>
|
|
117
|
-
<label for="name">
|
|
118
|
+
<label for="name">{text('storage.generic.name')}</label>
|
|
118
119
|
<input name="name" type="text" required value={item.name} />
|
|
119
120
|
</div>
|
|
120
121
|
</FormDialog>
|
|
121
122
|
<FormDialog
|
|
122
123
|
bind:dialog={dialogs.trash}
|
|
123
|
-
submitText=
|
|
124
|
+
submitText={text('storage.generic.trash')}
|
|
124
125
|
submitDanger
|
|
125
126
|
submit={async () => {
|
|
126
|
-
if (!item) throw '
|
|
127
|
+
if (!item) throw text('storage.generic.no_item');
|
|
127
128
|
await updateItemMetadata(item.id, { trash: true });
|
|
128
129
|
onDelete();
|
|
129
130
|
}}
|
|
130
131
|
>
|
|
131
|
-
<p>
|
|
132
|
+
<p>{text('storage.Preview.trash_confirm')}</p>
|
|
132
133
|
</FormDialog>
|
|
133
134
|
<FormDialog
|
|
134
135
|
bind:dialog={dialogs.download}
|
|
135
|
-
submitText=
|
|
136
|
+
submitText={text('storage.generic.download')}
|
|
136
137
|
submit={async () => {
|
|
137
138
|
if (item!.type == 'inode/directory') {
|
|
138
139
|
/** @todo ZIP support */
|
|
@@ -141,7 +142,7 @@
|
|
|
141
142
|
} else open(item!.dataURL, '_blank');
|
|
142
143
|
}}
|
|
143
144
|
>
|
|
144
|
-
<p>
|
|
145
|
+
<p>{text('storage.Preview.download_confirm')}</p>
|
|
145
146
|
</FormDialog>
|
|
146
147
|
|
|
147
148
|
<style>
|
|
@@ -176,7 +177,7 @@
|
|
|
176
177
|
|
|
177
178
|
.openers {
|
|
178
179
|
padding: 1em;
|
|
179
|
-
border:
|
|
180
|
+
border: var(--border-accent);
|
|
180
181
|
border-radius: 1em;
|
|
181
182
|
height: 2em;
|
|
182
183
|
anchor-name: --preview-openers;
|
|
@@ -232,7 +233,7 @@
|
|
|
232
233
|
padding: 1em;
|
|
233
234
|
flex: 1 1 0;
|
|
234
235
|
border-radius: 1em;
|
|
235
|
-
border:
|
|
236
|
+
border: var(--border-accent);
|
|
236
237
|
padding: 1em;
|
|
237
238
|
justify-content: center;
|
|
238
239
|
display: flex;
|
package/lib/Sidebar.svelte
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { text } from '@axium/client';
|
|
2
3
|
import type { StorageItemMetadata } from '@axium/storage/common';
|
|
3
4
|
import SidebarItem from './SidebarItem.svelte';
|
|
4
5
|
import { items as sb_items, getDirectory } from '@axium/storage/sidebar';
|
|
@@ -15,12 +16,12 @@
|
|
|
15
16
|
|
|
16
17
|
<div id="StorageSidebar">
|
|
17
18
|
{#await typeof root == 'string' ? getDirectory(root, items) : root}
|
|
18
|
-
<i>
|
|
19
|
+
<i>{text('generic.loading')}</i>
|
|
19
20
|
{:then}
|
|
20
21
|
{#each items as _, i (_.id)}
|
|
21
22
|
<SidebarItem bind:item={items[i]} bind:items />
|
|
22
23
|
{:else}
|
|
23
|
-
<i>
|
|
24
|
+
<i>{text('storage.Sidebar.no_files')}</i>
|
|
24
25
|
{/each}
|
|
25
26
|
{:catch error}
|
|
26
27
|
<i class="error-text">{error.message}</i>
|
package/lib/SidebarItem.svelte
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { text } from '@axium/client';
|
|
2
3
|
import { page } from '$app/state';
|
|
3
4
|
import { copy } from '@axium/client/clipboard';
|
|
4
5
|
import { FormDialog, Icon } from '@axium/client/components';
|
|
@@ -51,9 +52,13 @@
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
let children = $state<StorageItemMetadata[]>([]);
|
|
55
|
+
|
|
56
|
+
const itemName = $derived(
|
|
57
|
+
item.name ? `<strong>${item.name.length > 23 ? item.name.slice(0, 20) + '...' : item.name}</strong>` : 'this'
|
|
58
|
+
);
|
|
54
59
|
</script>
|
|
55
60
|
|
|
56
|
-
{#snippet action(name: string, i: string,
|
|
61
|
+
{#snippet action(name: string, i: string, label: string)}
|
|
57
62
|
<div
|
|
58
63
|
onclick={e => {
|
|
59
64
|
e.stopPropagation();
|
|
@@ -63,18 +68,10 @@
|
|
|
63
68
|
class="action icon-text"
|
|
64
69
|
>
|
|
65
70
|
<Icon {i} --size="14px" />
|
|
66
|
-
{
|
|
71
|
+
{label}
|
|
67
72
|
</div>
|
|
68
73
|
{/snippet}
|
|
69
74
|
|
|
70
|
-
{#snippet _itemName()}
|
|
71
|
-
{#if item.name}
|
|
72
|
-
<strong>{item.name.length > 23 ? item.name.slice(0, 20) + '...' : item.name}</strong>
|
|
73
|
-
{:else}
|
|
74
|
-
this
|
|
75
|
-
{/if}
|
|
76
|
-
{/snippet}
|
|
77
|
-
|
|
78
75
|
{#if item.type == 'inode/directory'}
|
|
79
76
|
<details>
|
|
80
77
|
<summary class={['StorageSidebarItem', selection.has(item.id) && 'selected']} {onclick} {oncontextmenu} {onpointerup}>
|
|
@@ -83,7 +80,7 @@
|
|
|
83
80
|
</summary>
|
|
84
81
|
<div>
|
|
85
82
|
{#await getDirectory(item.id, children)}
|
|
86
|
-
<i>
|
|
83
|
+
<i>{text('generic.loading')}</i>
|
|
87
84
|
{:then}
|
|
88
85
|
{#each children as _, i (_.id)}
|
|
89
86
|
<SidebarItem bind:item={children[i]} bind:items={children} />
|
|
@@ -101,35 +98,35 @@
|
|
|
101
98
|
{/if}
|
|
102
99
|
|
|
103
100
|
<div popover bind:this={popover}>
|
|
104
|
-
{@render action('rename', 'pen', '
|
|
105
|
-
{@render action('delete', 'trash', '
|
|
101
|
+
{@render action('rename', 'pen', text('storage.generic.rename'))}
|
|
102
|
+
{@render action('delete', 'trash', text('storage.SidebarItem.delete'))}
|
|
106
103
|
{#if item.type == 'cas_item'}
|
|
107
|
-
{@render action('download', 'download', '
|
|
104
|
+
{@render action('download', 'download', text('storage.generic.download'))}
|
|
108
105
|
{/if}
|
|
109
106
|
{#if page.data.session?.user.preferences.debug}
|
|
110
107
|
<div class="action icon-text" onclick={() => copy('text/plain', item.id)}>
|
|
111
108
|
<Icon i="copy" --size="14px" />
|
|
112
|
-
|
|
109
|
+
{text('storage.generic.copy_id')}
|
|
113
110
|
</div>
|
|
114
111
|
{/if}
|
|
115
112
|
</div>
|
|
116
113
|
|
|
117
114
|
<FormDialog
|
|
118
115
|
bind:dialog={dialogs.rename}
|
|
119
|
-
submitText=
|
|
116
|
+
submitText={text('storage.generic.rename')}
|
|
120
117
|
submit={async (data: { name: string }) => {
|
|
121
118
|
await updateItemMetadata(item.id, data);
|
|
122
119
|
item.name = data.name;
|
|
123
120
|
}}
|
|
124
121
|
>
|
|
125
122
|
<div>
|
|
126
|
-
<label for="name">
|
|
123
|
+
<label for="name">{text('storage.generic.name')}</label>
|
|
127
124
|
<input name="name" type="text" required value={item.name} />
|
|
128
125
|
</div>
|
|
129
126
|
</FormDialog>
|
|
130
127
|
<FormDialog
|
|
131
128
|
bind:dialog={dialogs.delete}
|
|
132
|
-
submitText=
|
|
129
|
+
submitText={text('storage.SidebarItem.delete')}
|
|
133
130
|
submitDanger
|
|
134
131
|
submit={async () => {
|
|
135
132
|
await deleteItem(item.id);
|
|
@@ -137,18 +134,18 @@
|
|
|
137
134
|
if (index !== -1) items.splice(index, 1);
|
|
138
135
|
}}
|
|
139
136
|
>
|
|
140
|
-
<p>
|
|
137
|
+
<p>{@html text('storage.SidebarItem.delete_confirm', { $html: true, name: itemName })}</p>
|
|
141
138
|
</FormDialog>
|
|
142
139
|
<FormDialog
|
|
143
140
|
bind:dialog={dialogs.download}
|
|
144
|
-
submitText=
|
|
141
|
+
submitText={text('storage.generic.download')}
|
|
145
142
|
submit={async () => {
|
|
146
143
|
open(item.dataURL, '_blank');
|
|
147
144
|
}}
|
|
148
145
|
>
|
|
149
146
|
<p>
|
|
150
|
-
|
|
151
|
-
|
|
147
|
+
{text('storage.SidebarItem.download_disclaimer')} <br />
|
|
148
|
+
{@html text('storage.generic.download_confirm', { $html: true, name: itemName })}
|
|
152
149
|
</p>
|
|
153
150
|
</FormDialog>
|
|
154
151
|
|
package/lib/Usage.svelte
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { text } from '@axium/client';
|
|
2
3
|
import { formatBytes } from '@axium/core/format';
|
|
3
4
|
import { NumberBar } from '@axium/client/components';
|
|
4
5
|
import { getUserStats } from '@axium/storage/client';
|
|
@@ -8,7 +9,7 @@
|
|
|
8
9
|
</script>
|
|
9
10
|
|
|
10
11
|
{#if !info && !userId}
|
|
11
|
-
<p>
|
|
12
|
+
<p>{text('storage.Usage.login_prompt')}</p>
|
|
12
13
|
{:else}
|
|
13
14
|
{#await info || getUserStats(userId!) then info}
|
|
14
15
|
<p>
|
|
@@ -23,7 +24,7 @@
|
|
|
23
24
|
</a>
|
|
24
25
|
</p>
|
|
25
26
|
{:catch error}
|
|
26
|
-
<p>
|
|
27
|
+
<p>{text('storage.Usage.error')}</p>
|
|
27
28
|
<p>{error.message}</p>
|
|
28
29
|
{/await}
|
|
29
30
|
{/if}
|
package/locales/en.json
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
{
|
|
2
|
+
"app_name": {
|
|
3
|
+
"files": "Files"
|
|
4
|
+
},
|
|
5
|
+
"page": {
|
|
6
|
+
"files": {
|
|
7
|
+
"title": "Files",
|
|
8
|
+
"detail_title": "Files — {name}",
|
|
9
|
+
"trashed": "This item is trashed",
|
|
10
|
+
"restore": "Restore",
|
|
11
|
+
"back": "Back",
|
|
12
|
+
"rename": "Rename",
|
|
13
|
+
"share": "Share",
|
|
14
|
+
"download": "Download",
|
|
15
|
+
"copy_link": "Copy Link",
|
|
16
|
+
"trash": "Trash",
|
|
17
|
+
"rename_submit": "Rename",
|
|
18
|
+
"trash_folder_confirm": "Are you sure you want to trash this folder?",
|
|
19
|
+
"download_folder_confirm": "Are you sure you want to download this folder?",
|
|
20
|
+
"shared": {
|
|
21
|
+
"title": "Files - Shared With You",
|
|
22
|
+
"empty": "No items have been shared with you."
|
|
23
|
+
},
|
|
24
|
+
"tab": {
|
|
25
|
+
"files": "Files",
|
|
26
|
+
"trash": "Trash",
|
|
27
|
+
"shared": "Shared"
|
|
28
|
+
},
|
|
29
|
+
"trash_page": {
|
|
30
|
+
"title": "Files — Trash",
|
|
31
|
+
"empty": "Trash is empty.",
|
|
32
|
+
"last_modified": "Last Modified",
|
|
33
|
+
"restore": "Restore",
|
|
34
|
+
"restore_confirm": "Restore {name}?",
|
|
35
|
+
"delete": "Delete",
|
|
36
|
+
"delete_confirm": "Are you sure you want to permanently delete {name}?"
|
|
37
|
+
},
|
|
38
|
+
"usage": {
|
|
39
|
+
"title": "Your Storage Usage",
|
|
40
|
+
"heading": "Storage Usage",
|
|
41
|
+
"bar_text": "Using {used} of {total}",
|
|
42
|
+
"bar_text_unlimited": "Using {used}",
|
|
43
|
+
"empty": "You have not uploaded any files yet."
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"storage": {
|
|
48
|
+
"generic": {
|
|
49
|
+
"copy_id": "Copy ID",
|
|
50
|
+
"download": "Download",
|
|
51
|
+
"download_confirm": "Are you sure you want to download {name}?",
|
|
52
|
+
"name": "Name",
|
|
53
|
+
"no_item": "No item is selected",
|
|
54
|
+
"rename": "Rename",
|
|
55
|
+
"trash": "Trash"
|
|
56
|
+
},
|
|
57
|
+
"Add": {
|
|
58
|
+
"content": "Content",
|
|
59
|
+
"create": "Create",
|
|
60
|
+
"new_folder": "New Folder",
|
|
61
|
+
"plain_text": "Plain Text",
|
|
62
|
+
"text": "Add",
|
|
63
|
+
"upload": "Upload",
|
|
64
|
+
"url": "URL"
|
|
65
|
+
},
|
|
66
|
+
"List": {
|
|
67
|
+
"copy_link": "Copy Link",
|
|
68
|
+
"empty": "Folder is empty.",
|
|
69
|
+
"last_modified": "Last Modified",
|
|
70
|
+
"share": "Share",
|
|
71
|
+
"size": "Size",
|
|
72
|
+
"trash_confirm": "Are you sure you want to trash {name}?"
|
|
73
|
+
},
|
|
74
|
+
"Preview": {
|
|
75
|
+
"download_confirm": "Are you sure you want to download this?",
|
|
76
|
+
"error_loading": "Error loading preview. You might not have permission to view this file.",
|
|
77
|
+
"loading": "Loading",
|
|
78
|
+
"open_with": "Open with",
|
|
79
|
+
"pdf_fallback_download": "PDF couldn't be previewed. Click here to download.",
|
|
80
|
+
"preview_unavailable": "Preview not available",
|
|
81
|
+
"trash_confirm": "Are you sure you want to trash this?"
|
|
82
|
+
},
|
|
83
|
+
"Sidebar": {
|
|
84
|
+
"no_files": "No files yet"
|
|
85
|
+
},
|
|
86
|
+
"SidebarItem": {
|
|
87
|
+
"delete": "Delete",
|
|
88
|
+
"delete_confirm": "Are you sure you want to delete {name}?",
|
|
89
|
+
"download_disclaimer": "We are not responsible for the contents of this file."
|
|
90
|
+
},
|
|
91
|
+
"Usage": {
|
|
92
|
+
"error": "Couldn't load your uploads.",
|
|
93
|
+
"login_prompt": "Log in to see storage usage."
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axium/storage",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.9",
|
|
4
4
|
"author": "James Prevett <axium@jamespre.dev>",
|
|
5
5
|
"description": "User file storage for Axium",
|
|
6
6
|
"funding": {
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"files": [
|
|
32
32
|
"dist",
|
|
33
33
|
"lib",
|
|
34
|
+
"locales",
|
|
34
35
|
"build",
|
|
35
36
|
"routes",
|
|
36
37
|
"db.json"
|
|
@@ -39,7 +40,7 @@
|
|
|
39
40
|
"build": "tsc"
|
|
40
41
|
},
|
|
41
42
|
"peerDependencies": {
|
|
42
|
-
"@axium/client": ">=0.
|
|
43
|
+
"@axium/client": ">=0.17.0",
|
|
43
44
|
"@axium/core": ">=0.19.0",
|
|
44
45
|
"@axium/server": ">=0.36.0",
|
|
45
46
|
"@sveltejs/kit": "^2.27.3",
|
|
@@ -56,7 +57,7 @@
|
|
|
56
57
|
"routes": "./routes",
|
|
57
58
|
"cli": "./dist/server/cli.js",
|
|
58
59
|
"db": "./db.json",
|
|
59
|
-
"web_client_hooks": "./dist/
|
|
60
|
+
"web_client_hooks": "./dist/client/web_hook.js"
|
|
60
61
|
},
|
|
61
62
|
"client": {
|
|
62
63
|
"cli": "./dist/client/cli.js",
|
|
@@ -65,7 +66,6 @@
|
|
|
65
66
|
"apps": [
|
|
66
67
|
{
|
|
67
68
|
"id": "files",
|
|
68
|
-
"name": "Files",
|
|
69
69
|
"icon": "folders"
|
|
70
70
|
}
|
|
71
71
|
],
|
package/routes/files/+layout.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { text } from '@axium/client';
|
|
1
2
|
import { getCurrentSession } from '@axium/client/user';
|
|
2
3
|
import type { Session, User } from '@axium/core';
|
|
3
4
|
import type { LayoutRouteId } from './$types';
|
|
@@ -10,9 +11,14 @@ export async function load({ url, route, parent }) {
|
|
|
10
11
|
session ||= await getCurrentSession().catch(() => null);
|
|
11
12
|
|
|
12
13
|
const tabs = [
|
|
13
|
-
{
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
{
|
|
15
|
+
name: text('page.files.tab.files'),
|
|
16
|
+
href: '/files',
|
|
17
|
+
icon: 'folders',
|
|
18
|
+
active: route.id.endsWith('/files/[id]') || route.id.endsWith('/files'),
|
|
19
|
+
},
|
|
20
|
+
{ name: text('page.files.tab.trash'), href: '/files/trash', icon: 'trash', active: route.id.endsWith('/files/trash') },
|
|
21
|
+
{ name: text('page.files.tab.shared'), href: '/files/shared', icon: 'user-group', active: route.id.endsWith('/files/shared') },
|
|
16
22
|
] satisfies { name: string; href: LayoutRouteId; icon: string; active: boolean }[];
|
|
17
23
|
|
|
18
24
|
return { session, tabs };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { text } from '@axium/client';
|
|
2
3
|
import { Add, List } from '@axium/storage/components';
|
|
3
4
|
|
|
4
5
|
const { data } = $props();
|
|
@@ -6,7 +7,7 @@
|
|
|
6
7
|
</script>
|
|
7
8
|
|
|
8
9
|
<svelte:head>
|
|
9
|
-
<title>
|
|
10
|
+
<title>{text('page.files.title')}</title>
|
|
10
11
|
</svelte:head>
|
|
11
12
|
|
|
12
13
|
<List appMode bind:items user={data.session?.user} />
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { text } from '@axium/client';
|
|
2
3
|
import { AccessControlDialog, FormDialog, Icon } from '@axium/client/components';
|
|
3
4
|
import { Add, List, Preview } from '@axium/storage/components';
|
|
4
5
|
import type { PageProps } from './$types';
|
|
@@ -17,18 +18,19 @@
|
|
|
17
18
|
</script>
|
|
18
19
|
|
|
19
20
|
<svelte:head>
|
|
20
|
-
<title>
|
|
21
|
+
<title>{text('page.files.detail_title', { name: item.name })}</title>
|
|
21
22
|
</svelte:head>
|
|
22
23
|
|
|
23
24
|
{#if item.trashedAt}
|
|
24
|
-
<p>
|
|
25
|
+
<p>{text('page.files.trashed')}</p>
|
|
25
26
|
<button
|
|
26
27
|
onclick={async e => {
|
|
27
28
|
e.preventDefault();
|
|
28
29
|
await updateItemMetadata(item.id, { trash: false });
|
|
29
30
|
}}
|
|
30
31
|
>
|
|
31
|
-
<Icon i="trash-can-undo" />
|
|
32
|
+
<Icon i="trash-can-undo" />
|
|
33
|
+
{text('page.files.restore')}
|
|
32
34
|
</button>
|
|
33
35
|
{:else}
|
|
34
36
|
<AccessControlDialog
|
|
@@ -63,12 +65,12 @@
|
|
|
63
65
|
{/snippet}
|
|
64
66
|
|
|
65
67
|
<div class="folder-actions">
|
|
66
|
-
{@render action('folder-arrow-up', '
|
|
67
|
-
{@render action('pencil', '
|
|
68
|
-
{@render action('user-group', '
|
|
69
|
-
{@render action('download', '
|
|
70
|
-
{@render action('link-horizontal', '
|
|
71
|
-
{@render action('trash', '
|
|
68
|
+
{@render action('folder-arrow-up', text('page.files.back'), () => (location.href = parentHref))}
|
|
69
|
+
{@render action('pencil', text('page.files.rename'), () => dialogs.rename.showModal())}
|
|
70
|
+
{@render action('user-group', text('page.files.share'), () => shareDialog.showModal())}
|
|
71
|
+
{@render action('download', text('page.files.download'), () => dialogs.download.showModal())}
|
|
72
|
+
{@render action('link-horizontal', text('page.files.copy_link'), () => copyShortURL(item))}
|
|
73
|
+
{@render action('trash', text('page.files.trash'), () => dialogs.trash.showModal())}
|
|
72
74
|
</div>
|
|
73
75
|
|
|
74
76
|
<List appMode bind:items user={data.session?.user} />
|
|
@@ -76,38 +78,38 @@
|
|
|
76
78
|
|
|
77
79
|
<FormDialog
|
|
78
80
|
bind:dialog={dialogs.rename}
|
|
79
|
-
submitText=
|
|
81
|
+
submitText={text('page.files.rename_submit')}
|
|
80
82
|
submit={async (data: { name: string }) => {
|
|
81
83
|
await updateItemMetadata(item.id, data);
|
|
82
84
|
item.name = data.name;
|
|
83
85
|
}}
|
|
84
86
|
>
|
|
85
87
|
<div>
|
|
86
|
-
<label for="name">
|
|
88
|
+
<label for="name">{text('storage.generic.name')}</label>
|
|
87
89
|
<input name="name" type="text" required value={item.name} />
|
|
88
90
|
</div>
|
|
89
91
|
</FormDialog>
|
|
90
92
|
<FormDialog
|
|
91
93
|
bind:dialog={dialogs.trash}
|
|
92
|
-
submitText=
|
|
94
|
+
submitText={text('page.files.trash')}
|
|
93
95
|
submitDanger
|
|
94
96
|
submit={async () => {
|
|
95
97
|
await updateItemMetadata(item.id, { trash: true });
|
|
96
98
|
location.href = parentHref;
|
|
97
99
|
}}
|
|
98
100
|
>
|
|
99
|
-
<p>
|
|
101
|
+
<p>{text('page.files.trash_folder_confirm')}</p>
|
|
100
102
|
</FormDialog>
|
|
101
103
|
<FormDialog
|
|
102
104
|
bind:dialog={dialogs.download}
|
|
103
|
-
submitText=
|
|
105
|
+
submitText={text('page.files.download')}
|
|
104
106
|
submit={async () => {
|
|
105
107
|
/** @todo ZIP support */
|
|
106
108
|
const children = await getDirectoryMetadata(item.id);
|
|
107
109
|
for (const child of children) open(child.dataURL, '_blank');
|
|
108
110
|
}}
|
|
109
111
|
>
|
|
110
|
-
<p>
|
|
112
|
+
<p>{text('page.files.download_folder_confirm')}</p>
|
|
111
113
|
</FormDialog>
|
|
112
114
|
{:else}
|
|
113
115
|
<div class="preview-container">
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { text } from '@axium/client';
|
|
2
3
|
import { List } from '@axium/storage/components';
|
|
3
4
|
|
|
4
5
|
const { data } = $props();
|
|
@@ -6,7 +7,7 @@
|
|
|
6
7
|
</script>
|
|
7
8
|
|
|
8
9
|
<svelte:head>
|
|
9
|
-
<title>
|
|
10
|
+
<title>{text('page.files.shared.title')}</title>
|
|
10
11
|
</svelte:head>
|
|
11
12
|
|
|
12
|
-
<List appMode bind:items emptyText=
|
|
13
|
+
<List appMode bind:items emptyText={text('page.files.shared.empty')} user={data.session?.user} />
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { text } from '@axium/client';
|
|
2
3
|
import { FormDialog, Icon } from '@axium/client/components';
|
|
3
4
|
import '@axium/client/styles/list';
|
|
4
5
|
import { formatBytes } from '@axium/core/format';
|
|
@@ -13,6 +14,11 @@
|
|
|
13
14
|
|
|
14
15
|
let activeIndex = $state<number>(-1);
|
|
15
16
|
const activeItem = $derived(activeIndex == -1 ? null : items[activeIndex]);
|
|
17
|
+
const activeItemName = $derived(
|
|
18
|
+
activeItem?.name
|
|
19
|
+
? `<strong>${activeItem.name.length > 23 ? activeItem.name.slice(0, 20) + '...' : activeItem.name}</strong>`
|
|
20
|
+
: 'this'
|
|
21
|
+
);
|
|
16
22
|
|
|
17
23
|
function action(index: number, dialog: () => HTMLDialogElement) {
|
|
18
24
|
return (e: Event) => {
|
|
@@ -25,15 +31,15 @@
|
|
|
25
31
|
</script>
|
|
26
32
|
|
|
27
33
|
<svelte:head>
|
|
28
|
-
<title>
|
|
34
|
+
<title>{text('page.files.trash_page.title')}</title>
|
|
29
35
|
</svelte:head>
|
|
30
36
|
|
|
31
37
|
<div class="list">
|
|
32
38
|
<div class="list-item list-header">
|
|
33
39
|
<span></span>
|
|
34
|
-
<span>
|
|
35
|
-
<span>
|
|
36
|
-
<span>
|
|
40
|
+
<span>{text('storage.generic.name')}</span>
|
|
41
|
+
<span>{text('page.files.trash_page.last_modified')}</span>
|
|
42
|
+
<span>{text('storage.List.size')}</span>
|
|
37
43
|
</div>
|
|
38
44
|
{#each items as item, i (item.id)}
|
|
39
45
|
<div class="list-item">
|
|
@@ -49,39 +55,33 @@
|
|
|
49
55
|
</span>
|
|
50
56
|
</div>
|
|
51
57
|
{:else}
|
|
52
|
-
<p class="list-empty">
|
|
58
|
+
<p class="list-empty">{text('page.files.trash_page.empty')}</p>
|
|
53
59
|
{/each}
|
|
54
60
|
</div>
|
|
55
61
|
|
|
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
62
|
<FormDialog
|
|
63
63
|
bind:dialog={restoreDialog}
|
|
64
|
-
submitText=
|
|
64
|
+
submitText={text('page.files.trash_page.restore')}
|
|
65
65
|
submit={async () => {
|
|
66
|
-
if (!activeItem) throw '
|
|
66
|
+
if (!activeItem) throw text('storage.generic.no_item');
|
|
67
67
|
await updateItemMetadata(activeItem.id, { trash: false });
|
|
68
68
|
items.splice(activeIndex, 1);
|
|
69
69
|
}}
|
|
70
70
|
>
|
|
71
|
-
<p>
|
|
71
|
+
<p>{@html text('page.files.trash_page.restore_confirm', { $html: true, name: activeItemName })}</p>
|
|
72
72
|
</FormDialog>
|
|
73
73
|
<FormDialog
|
|
74
74
|
bind:dialog={deleteDialog}
|
|
75
|
-
submitText=
|
|
75
|
+
submitText={text('page.files.trash_page.delete')}
|
|
76
76
|
submitDanger
|
|
77
77
|
submit={async () => {
|
|
78
|
-
if (!activeItem) throw '
|
|
78
|
+
if (!activeItem) throw text('storage.generic.no_item');
|
|
79
79
|
await deleteItem(activeItem.id);
|
|
80
80
|
items.splice(activeIndex, 1);
|
|
81
81
|
}}
|
|
82
82
|
>
|
|
83
83
|
<p>
|
|
84
|
-
|
|
84
|
+
{@html text('page.files.trash_page.delete_confirm', { $html: true, name: activeItemName })}
|
|
85
85
|
</p>
|
|
86
86
|
</FormDialog>
|
|
87
87
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { text } from '@axium/client';
|
|
2
3
|
import { NumberBar } from '@axium/client/components';
|
|
3
4
|
import '@axium/client/styles/list';
|
|
4
5
|
import { formatBytes } from '@axium/core/format';
|
|
@@ -10,15 +11,19 @@
|
|
|
10
11
|
let items = $state(data.info.items.filter(i => i.type != 'inode/directory').sort((a, b) => Math.sign(b.size - a.size)));
|
|
11
12
|
const usedBytes = $state(data.info.usedBytes);
|
|
12
13
|
|
|
13
|
-
let barText = $derived(
|
|
14
|
+
let barText = $derived(
|
|
15
|
+
limits.user_size
|
|
16
|
+
? text('page.files.usage.bar_text', { used: formatBytes(usedBytes), total: formatBytes(limits.user_size * 1_000_000) })
|
|
17
|
+
: text('page.files.usage.bar_text_unlimited', { used: formatBytes(usedBytes) })
|
|
18
|
+
);
|
|
14
19
|
</script>
|
|
15
20
|
|
|
16
21
|
<svelte:head>
|
|
17
|
-
<title>
|
|
22
|
+
<title>{text('page.files.usage.title')}</title>
|
|
18
23
|
</svelte:head>
|
|
19
24
|
|
|
20
|
-
<h2>
|
|
25
|
+
<h2>{text('page.files.usage.heading')}</h2>
|
|
21
26
|
|
|
22
27
|
<p><NumberBar max={limits.user_size * 1_000_000} value={usedBytes} text={barText} /></p>
|
|
23
28
|
|
|
24
|
-
<List bind:items emptyText=
|
|
29
|
+
<List bind:items emptyText={text('page.files.usage.empty')} user={data.session?.user} />
|