@axium/storage 0.18.8 → 0.18.10

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.
@@ -0,0 +1,8 @@
1
+ import en from '../../locales/en.json';
2
+ import '../common.js';
3
+ type en = typeof en;
4
+ declare module '@axium/client/locales' {
5
+ interface Locale extends en {
6
+ }
7
+ }
8
+ export {};
@@ -0,0 +1,4 @@
1
+ import { extendLocale } from '@axium/client';
2
+ import en from '../../locales/en.json' with { type: 'json' };
3
+ import '../common.js';
4
+ extendLocale('en', en);
@@ -54,7 +54,7 @@ addRoute({
54
54
  .selectFrom('storage')
55
55
  .selectAll()
56
56
  .where('id', 'in', [...deletedIds, ...Object.keys(header.metadata), ...changedIds])
57
- .select(acl.from('storage', { user }))
57
+ .select(acl.from('storage', { filterByUser: user }))
58
58
  .$castTo()
59
59
  .execute()
60
60
  .catch(withError('Item(s) not found', 404));
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" />Upload</span>
38
- {@render _item('inode/directory', 'New Folder')}
39
- {@render _item('text/plain', 'Plain Text')}
40
- {@render _item('text/x-uri', 'URL', true)}
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="Upload"
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="Create"
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">Name</label>
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">Content</label>
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 = 'Folder is empty.',
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>Name</span>
50
- <span>Last Modified</span>
51
- <span>Size</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: 'Rename', action: () => ((activeIndex = i), dialogs.rename.showModal()) },
65
- { i: 'user-group', text: 'Share', action: () => ((activeIndex = i), dialogs['share:' + item.id].showModal()) },
66
- { i: 'download', text: 'Download', action: () => ((activeIndex = i), dialogs.download.showModal()) },
67
- { i: 'link-horizontal', text: 'Copy Link', action: () => ((activeIndex = i), copyShortURL(item)) },
68
- { i: 'trash', text: 'Trash', action: () => ((activeIndex = i), dialogs.trash.showModal()) },
69
- user?.preferences?.debug && { i: 'hashtag', text: 'Copy ID', action: () => copy('text/plain', item.id) }
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="Rename"
127
+ submitText={text('storage.generic.rename')}
122
128
  submit={async (data: { name: string }) => {
123
- if (!activeItem) throw 'No item is selected';
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">Name</label>
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="Trash"
141
+ submitText={text('storage.generic.trash')}
136
142
  submitDanger
137
143
  submit={async () => {
138
- if (!activeItem) throw 'No item is selected';
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>Are you sure you want to trash {@render _itemName()}?</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="Download"
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>Are you sure you want to download {@render _itemName()}?</p>
162
+ <p>{@html text('storage.generic.download_confirm', { $html: true, name: activeItemName })}</p>
157
163
  </FormDialog>
158
164
 
159
165
  <style>
@@ -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>Open with <a href={first.openURL(item)} target="_blank">{first.name}</a></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
- <p>PDF not displayed? <a href={item.dataURL} download={item.name}>Download</a></p>
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>Loading</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>Error loading preview. You might not have permission to view this file.</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 not available</span>
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="Rename"
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">Name</label>
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="Trash"
124
+ submitText={text('storage.generic.trash')}
124
125
  submitDanger
125
126
  submit={async () => {
126
- if (!item) throw 'No item is selected';
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>Are you sure you want to trash this?</p>
132
+ <p>{text('storage.Preview.trash_confirm')}</p>
132
133
  </FormDialog>
133
134
  <FormDialog
134
135
  bind:dialog={dialogs.download}
135
- submitText="Download"
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>Are you sure you want to download this?</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: 1px solid var(--border-accent);
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: 1px solid var(--border-accent);
236
+ border: var(--border-accent);
236
237
  padding: 1em;
237
238
  justify-content: center;
238
239
  display: flex;
@@ -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>Loading...</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>No files yet</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>
@@ -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, text: 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
- {text}
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>Loading...</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', 'Rename')}
105
- {@render action('delete', 'trash', 'Delete')}
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', '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
- Copy ID
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="Rename"
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">Name</label>
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="Delete"
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>Are you sure you want to delete {@render _itemName()}?</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="Download"
141
+ submitText={text('storage.generic.download')}
145
142
  submit={async () => {
146
143
  open(item.dataURL, '_blank');
147
144
  }}
148
145
  >
149
146
  <p>
150
- We are not responsible for the contents of this file. <br />
151
- Are you sure you want to download {@render _itemName()}?
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>Log in to see storage usage.</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>Couldn't load your uploads.</p>
27
+ <p>{text('storage.Usage.error')}</p>
27
28
  <p>{error.message}</p>
28
29
  {/await}
29
30
  {/if}
@@ -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.8",
3
+ "version": "0.18.10",
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,9 +40,9 @@
39
40
  "build": "tsc"
40
41
  },
41
42
  "peerDependencies": {
42
- "@axium/client": ">=0.15.0",
43
+ "@axium/client": ">=0.17.0",
43
44
  "@axium/core": ">=0.19.0",
44
- "@axium/server": ">=0.36.0",
45
+ "@axium/server": ">=0.39.0",
45
46
  "@sveltejs/kit": "^2.27.3",
46
47
  "utilium": "^2.6.3"
47
48
  },
@@ -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/common.js"
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
  ],
@@ -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
- { name: 'files', href: '/files', icon: 'folders', active: route.id.endsWith('/files/[id]') || route.id.endsWith('/files') },
14
- { name: 'trash', href: '/files/trash', icon: 'trash', active: route.id.endsWith('/files/trash') },
15
- { name: 'shared', href: '/files/shared', icon: 'user-group', active: route.id.endsWith('/files/shared') },
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>Files</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>Files {item.name}</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>This item is trashed</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" /> Restore
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', 'Back', () => (location.href = parentHref))}
67
- {@render action('pencil', 'Rename', () => dialogs.rename.showModal())}
68
- {@render action('user-group', 'Share', () => shareDialog.showModal())}
69
- {@render action('download', 'Download', () => dialogs.download.showModal())}
70
- {@render action('link-horizontal', 'Copy Link', () => copyShortURL(item))}
71
- {@render action('trash', 'Trash', () => dialogs.trash.showModal())}
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="Rename"
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">Name</label>
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="Trash"
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>Are you sure you want to trash this folder?</p>
101
+ <p>{text('page.files.trash_folder_confirm')}</p>
100
102
  </FormDialog>
101
103
  <FormDialog
102
104
  bind:dialog={dialogs.download}
103
- submitText="Download"
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>Are you sure you want to download this folder?</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>Files - Shared With You</title>
10
+ <title>{text('page.files.shared.title')}</title>
10
11
  </svelte:head>
11
12
 
12
- <List appMode bind:items emptyText="No items have been shared with you." user={data.session?.user} />
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>Files - Trash</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>Name</span>
35
- <span>Last Modified</span>
36
- <span>Size</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">Trash is empty.</p>
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="Restore"
64
+ submitText={text('page.files.trash_page.restore')}
65
65
  submit={async () => {
66
- if (!activeItem) throw 'No item is selected';
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>Restore {@render _name()}?</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="Delete"
75
+ submitText={text('page.files.trash_page.delete')}
76
76
  submitDanger
77
77
  submit={async () => {
78
- if (!activeItem) throw 'No item is selected';
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
- Are you sure you want to permanently delete {@render _name()}?
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(`Using ${formatBytes(usedBytes)} ${limits.user_size ? 'of ' + formatBytes(limits.user_size * 1_000_000) : ''}`);
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>Your Storage Usage</title>
22
+ <title>{text('page.files.usage.title')}</title>
18
23
  </svelte:head>
19
24
 
20
- <h2>Storage Usage</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="You have not uploaded any files yet." user={data.session?.user} />
29
+ <List bind:items emptyText={text('page.files.usage.empty')} user={data.session?.user} />