@axium/storage 0.14.2 → 0.15.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/README.md +2 -15
- package/dist/client/3rd-party.d.ts +11 -0
- package/dist/client/3rd-party.js +2 -0
- package/dist/client/api.d.ts +1 -1
- package/dist/client/api.js +8 -1
- package/dist/server/raw.js +100 -13
- package/lib/Add.svelte +3 -3
- package/lib/List.svelte +170 -11
- package/lib/Usage.svelte +2 -2
- package/package.json +1 -1
- package/routes/files/+page.svelte +1 -1
- package/routes/files/[id]/+page.svelte +1 -1
package/README.md
CHANGED
|
@@ -1,17 +1,4 @@
|
|
|
1
1
|
# Axium Storage
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## Usage
|
|
6
|
-
|
|
7
|
-
Update the configuration to include the files data directory:
|
|
8
|
-
|
|
9
|
-
```json
|
|
10
|
-
{
|
|
11
|
-
"storage": {
|
|
12
|
-
"data": "/path/to/storage/data"
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
Also, make sure to run `axium plugin init @axium/storage` to add the necessary database tables for the plugin.
|
|
3
|
+
File storage and management for Axium.
|
|
4
|
+
This plugin provides a competitor to big tech's cloud storage.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { StorageItemMetadata } from '../common.js';
|
|
3
|
+
export declare const previews: Map<string, Snippet<[item: StorageItemMetadata<Record<string, unknown>>]>>;
|
|
4
|
+
export interface Opener {
|
|
5
|
+
/** Mime types supported by this opener */
|
|
6
|
+
types: string[];
|
|
7
|
+
name: string;
|
|
8
|
+
/** Get a URL to open the item with another plugin */
|
|
9
|
+
openURL(item: StorageItemMetadata): string;
|
|
10
|
+
}
|
|
11
|
+
export declare const openers: Opener[];
|
package/dist/client/api.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { StorageItemMetadata, type StorageItemUpdate, type UserStorage, type UserStorageInfo } from '../common.js';
|
|
2
2
|
export interface UploadOptions {
|
|
3
3
|
parentId?: string;
|
|
4
4
|
name?: string;
|
package/dist/client/api.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { fetchAPI, prefix, token } from '@axium/client/requests';
|
|
2
|
+
import { StorageItemMetadata } from '../common.js';
|
|
3
|
+
import { prettifyError } from 'zod';
|
|
2
4
|
async function _upload(method, url, data, extraHeaders = {}) {
|
|
3
5
|
const init = {
|
|
4
6
|
method,
|
|
@@ -20,7 +22,12 @@ async function _upload(method, url, data, extraHeaders = {}) {
|
|
|
20
22
|
const json = await response.json().catch(() => ({ message: 'Unknown server error (invalid JSON response)' }));
|
|
21
23
|
if (!response.ok)
|
|
22
24
|
throw new Error(json.message);
|
|
23
|
-
|
|
25
|
+
try {
|
|
26
|
+
return StorageItemMetadata.parse(json);
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
throw prettifyError(e);
|
|
30
|
+
}
|
|
24
31
|
}
|
|
25
32
|
function rawStorage(fileId) {
|
|
26
33
|
const raw = '/raw/storage' + (fileId ? '/' + fileId : '');
|
package/dist/server/raw.js
CHANGED
|
@@ -1,3 +1,55 @@
|
|
|
1
|
+
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
|
|
2
|
+
if (value !== null && value !== void 0) {
|
|
3
|
+
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
4
|
+
var dispose, inner;
|
|
5
|
+
if (async) {
|
|
6
|
+
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
7
|
+
dispose = value[Symbol.asyncDispose];
|
|
8
|
+
}
|
|
9
|
+
if (dispose === void 0) {
|
|
10
|
+
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
11
|
+
dispose = value[Symbol.dispose];
|
|
12
|
+
if (async) inner = dispose;
|
|
13
|
+
}
|
|
14
|
+
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
15
|
+
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
16
|
+
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
17
|
+
}
|
|
18
|
+
else if (async) {
|
|
19
|
+
env.stack.push({ async: true });
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
};
|
|
23
|
+
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
24
|
+
return function (env) {
|
|
25
|
+
function fail(e) {
|
|
26
|
+
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
27
|
+
env.hasError = true;
|
|
28
|
+
}
|
|
29
|
+
var r, s = 0;
|
|
30
|
+
function next() {
|
|
31
|
+
while (r = env.stack.pop()) {
|
|
32
|
+
try {
|
|
33
|
+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
34
|
+
if (r.dispose) {
|
|
35
|
+
var result = r.dispose.call(r.value);
|
|
36
|
+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
37
|
+
}
|
|
38
|
+
else s |= 1;
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
fail(e);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
45
|
+
if (env.hasError) throw env.error;
|
|
46
|
+
}
|
|
47
|
+
return next();
|
|
48
|
+
};
|
|
49
|
+
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
50
|
+
var e = new Error(message);
|
|
51
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
|
+
});
|
|
1
53
|
import { getConfig } from '@axium/core';
|
|
2
54
|
import { audit } from '@axium/server/audit';
|
|
3
55
|
import { checkAuthForItem, requireSession } from '@axium/server/auth';
|
|
@@ -5,7 +57,7 @@ import { database } from '@axium/server/database';
|
|
|
5
57
|
import { error, withError } from '@axium/server/requests';
|
|
6
58
|
import { addRoute } from '@axium/server/routes';
|
|
7
59
|
import { createHash } from 'node:crypto';
|
|
8
|
-
import { linkSync,
|
|
60
|
+
import { closeSync, linkSync, openSync, readSync, writeFileSync } from 'node:fs';
|
|
9
61
|
import { join } from 'node:path/posix';
|
|
10
62
|
import * as z from 'zod';
|
|
11
63
|
import '../polyfills.js';
|
|
@@ -95,18 +147,53 @@ addRoute({
|
|
|
95
147
|
path: '/raw/storage/:id',
|
|
96
148
|
params: { id: z.uuid() },
|
|
97
149
|
async GET(request, { id: itemId }) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
},
|
|
109
|
-
|
|
150
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
151
|
+
try {
|
|
152
|
+
if (!getConfig('@axium/storage').enabled)
|
|
153
|
+
error(503, 'User storage is disabled');
|
|
154
|
+
const { item } = await checkAuthForItem(request, 'storage', itemId, { read: true });
|
|
155
|
+
if (item.trashedAt)
|
|
156
|
+
error(410, 'Trashed items can not be downloaded');
|
|
157
|
+
const path = join(getConfig('@axium/storage').data, item.id);
|
|
158
|
+
const range = request.headers.get('range');
|
|
159
|
+
const fd = openSync(path, 'r');
|
|
160
|
+
const _ = __addDisposableResource(env_1, { [Symbol.dispose]: () => closeSync(fd) }, false);
|
|
161
|
+
let start = 0, end = item.size - 1, length = item.size;
|
|
162
|
+
if (range) {
|
|
163
|
+
const [_start, _end = item.size - 1] = range
|
|
164
|
+
.replace(/bytes=/, '')
|
|
165
|
+
.split('-')
|
|
166
|
+
.map(val => (val && Number.isSafeInteger(parseInt(val)) ? parseInt(val) : undefined));
|
|
167
|
+
start = typeof _start == 'number' ? _start : item.size - _end;
|
|
168
|
+
end = typeof _start == 'number' ? _end : item.size - 1;
|
|
169
|
+
length = end - start + 1;
|
|
170
|
+
}
|
|
171
|
+
if (start >= item.size || end >= item.size || start > end || start < 0) {
|
|
172
|
+
return new Response(null, {
|
|
173
|
+
status: 416,
|
|
174
|
+
headers: { 'Content-Range': `bytes */${item.size}` },
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
const content = new Uint8Array(length);
|
|
178
|
+
readSync(fd, content, 0, length, start);
|
|
179
|
+
return new Response(content, {
|
|
180
|
+
status: length == item.size ? 200 : 206,
|
|
181
|
+
headers: {
|
|
182
|
+
'Content-Range': `bytes ${start}-${end}/${item.size}`,
|
|
183
|
+
'Accept-Ranges': 'bytes',
|
|
184
|
+
'Content-Length': String(length),
|
|
185
|
+
'Content-Type': item.type,
|
|
186
|
+
'Content-Disposition': `attachment; filename="${item.name}"`,
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
catch (e_1) {
|
|
191
|
+
env_1.error = e_1;
|
|
192
|
+
env_1.hasError = true;
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
__disposeResources(env_1);
|
|
196
|
+
}
|
|
110
197
|
},
|
|
111
198
|
async POST(request, { id: itemId }) {
|
|
112
199
|
if (!getConfig('@axium/storage').enabled)
|
package/lib/Add.svelte
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { uploadItem } from '@axium/storage/client';
|
|
5
5
|
import type { StorageItemMetadata } from '@axium/storage/common';
|
|
6
6
|
|
|
7
|
-
const { parentId,
|
|
7
|
+
const { parentId, onAdd }: { parentId?: string; onAdd?(item: StorageItemMetadata): void } = $props();
|
|
8
8
|
|
|
9
9
|
let uploadDialog = $state<HTMLDialogElement>()!;
|
|
10
10
|
let input = $state<HTMLInputElement>();
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
submit={async () => {
|
|
46
46
|
for (const file of input?.files!) {
|
|
47
47
|
const item = await uploadItem(file, { parentId });
|
|
48
|
-
|
|
48
|
+
onAdd?.(item);
|
|
49
49
|
}
|
|
50
50
|
}}
|
|
51
51
|
>
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
submit={async (data: { name: string; content?: string }) => {
|
|
59
59
|
const file = new File(createIncludesContent ? [data.content!] : [], data.name, { type: createType });
|
|
60
60
|
const item = await uploadItem(file, { parentId });
|
|
61
|
-
|
|
61
|
+
onAdd?.(item);
|
|
62
62
|
}}
|
|
63
63
|
>
|
|
64
64
|
<div>
|
package/lib/List.svelte
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { AccessControlDialog, FormDialog, Icon } from '@axium/client/components';
|
|
2
|
+
import { AccessControlDialog, FormDialog, Icon, Popover } from '@axium/client/components';
|
|
3
3
|
import '@axium/client/styles/list';
|
|
4
4
|
import type { AccessControllable, UserPublic } from '@axium/core';
|
|
5
5
|
import { formatBytes } from '@axium/core/format';
|
|
6
6
|
import { forMime as iconForMime } from '@axium/core/icons';
|
|
7
|
-
import { getDirectoryMetadata, updateItemMetadata } from '@axium/storage/client';
|
|
7
|
+
import { downloadItem, getDirectoryMetadata, updateItemMetadata } from '@axium/storage/client';
|
|
8
|
+
import { openers, previews } from '@axium/storage/client/3rd-party';
|
|
8
9
|
import type { StorageItemMetadata } from '@axium/storage/common';
|
|
9
10
|
|
|
10
11
|
let {
|
|
@@ -19,15 +20,15 @@
|
|
|
19
20
|
const dialogs = $state<Record<string, HTMLDialogElement>>({});
|
|
20
21
|
</script>
|
|
21
22
|
|
|
22
|
-
{#snippet action(name: string, icon: string, i: number)}
|
|
23
|
+
{#snippet action(name: string, icon: string, i: number, preview: boolean = false)}
|
|
23
24
|
<span
|
|
24
|
-
class=
|
|
25
|
+
class={['icon-text', !preview ? 'action' : 'preview-action']}
|
|
25
26
|
onclick={() => {
|
|
26
27
|
activeIndex = i;
|
|
27
28
|
dialogs[name].showModal();
|
|
28
29
|
}}
|
|
29
30
|
>
|
|
30
|
-
<Icon i={icon} --size=
|
|
31
|
+
<Icon i={icon} --size={preview ? '18px' : '14px'} />
|
|
31
32
|
</span>
|
|
32
33
|
{/snippet}
|
|
33
34
|
|
|
@@ -47,11 +48,12 @@
|
|
|
47
48
|
<span>Size</span>
|
|
48
49
|
</div>
|
|
49
50
|
{#each items as item, i (item.id)}
|
|
51
|
+
{@const itemOpeners = openers.filter(opener => opener.types.includes(item.type))}
|
|
50
52
|
<div
|
|
51
53
|
class="list-item"
|
|
52
54
|
onclick={async () => {
|
|
53
55
|
if (item.type != 'inode/directory') {
|
|
54
|
-
|
|
56
|
+
dialogs['preview:' + item.id].showModal();
|
|
55
57
|
} else if (appMode) location.href = '/files/' + item.id;
|
|
56
58
|
else items = await getDirectoryMetadata(item.id);
|
|
57
59
|
}}
|
|
@@ -70,7 +72,7 @@
|
|
|
70
72
|
{@render action('rename', 'pencil', i)}
|
|
71
73
|
{@render action('share' + item.id, 'user-group', i)}
|
|
72
74
|
<AccessControlDialog
|
|
73
|
-
bind:dialog={dialogs['share' + item.id]}
|
|
75
|
+
bind:dialog={dialogs['share:' + item.id]}
|
|
74
76
|
{item}
|
|
75
77
|
itemType="storage"
|
|
76
78
|
editable={(item.acl?.find(
|
|
@@ -83,6 +85,62 @@
|
|
|
83
85
|
/>
|
|
84
86
|
{@render action('download', 'download', i)}
|
|
85
87
|
{@render action('trash', 'trash', i)}
|
|
88
|
+
<dialog bind:this={dialogs['preview:' + item.id]} class="preview">
|
|
89
|
+
<div class="preview-top-bar">
|
|
90
|
+
<div class="title">{item.name}</div>
|
|
91
|
+
{#if itemOpeners.length}
|
|
92
|
+
{@const [first, ...others] = itemOpeners}
|
|
93
|
+
<div class="openers">
|
|
94
|
+
<span>Open with <a href={first.openURL(item)} target="_blank">{first.name}</a></span>
|
|
95
|
+
{#if others.length}
|
|
96
|
+
<Popover>
|
|
97
|
+
{#snippet toggle()}
|
|
98
|
+
<span class="popover-toggle"><Icon i="caret-down" /></span>
|
|
99
|
+
{/snippet}
|
|
100
|
+
{#each others as opener}
|
|
101
|
+
<a href={opener.openURL(item)} target="_blank">{opener.name}</a>
|
|
102
|
+
{/each}
|
|
103
|
+
</Popover>
|
|
104
|
+
{/if}
|
|
105
|
+
</div>
|
|
106
|
+
{/if}
|
|
107
|
+
<div class="actions">
|
|
108
|
+
{@render action('rename', 'pencil', i, true)}
|
|
109
|
+
{@render action('share:' + item.id, 'user-group', i, true)}
|
|
110
|
+
{@render action('download', 'download', i, true)}
|
|
111
|
+
{@render action('trash', 'trash', i, true)}
|
|
112
|
+
<span class="mobile-hide" onclick={() => dialogs['preview:' + item.id].close()}>
|
|
113
|
+
<Icon i="xmark" --size="20px" />
|
|
114
|
+
</span>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
<div class="content">
|
|
118
|
+
{#if item.type.startsWith('image/')}
|
|
119
|
+
<img src={item.dataURL} alt={item.name} width="100%" />
|
|
120
|
+
{:else if item.type.startsWith('audio/')}
|
|
121
|
+
<audio src={item.dataURL} controls></audio>
|
|
122
|
+
{:else if item.type.startsWith('video/')}
|
|
123
|
+
<video src={item.dataURL} controls width="100%">
|
|
124
|
+
<track kind="captions" />
|
|
125
|
+
</video>
|
|
126
|
+
{:else if item.type == 'application/pdf'}
|
|
127
|
+
<object data={item.dataURL} type="application/pdf" width="100%" height="100%">
|
|
128
|
+
<embed src={item.dataURL} type="application/pdf" width="100%" height="100%" />
|
|
129
|
+
<p>PDF not displayed? <a href={item.dataURL} download={item.name}>Download</a></p>
|
|
130
|
+
</object>
|
|
131
|
+
{:else if item.type.startsWith('text/')}
|
|
132
|
+
<pre
|
|
133
|
+
class="full-fill preview-text">{#await downloadItem(item.id).then( b => b.text() ) then content}{content}{/await}</pre>
|
|
134
|
+
{:else if previews.has(item.type)}
|
|
135
|
+
{@render previews.get(item.type)!(item)}
|
|
136
|
+
{:else}
|
|
137
|
+
<div class="full-fill no-preview">
|
|
138
|
+
<Icon i="eye-slash" --size="50px" />
|
|
139
|
+
<span>Preview not available</span>
|
|
140
|
+
</div>
|
|
141
|
+
{/if}
|
|
142
|
+
</div>
|
|
143
|
+
</dialog>
|
|
86
144
|
</div>
|
|
87
145
|
</div>
|
|
88
146
|
{:else}
|
|
@@ -121,19 +179,120 @@
|
|
|
121
179
|
submitText="Download"
|
|
122
180
|
submit={async () => {
|
|
123
181
|
if (activeItem!.type == 'inode/directory') {
|
|
182
|
+
/** @todo ZIP support */
|
|
124
183
|
const children = await getDirectoryMetadata(activeItem!.id);
|
|
125
184
|
for (const child of children) open(child.dataURL, '_blank');
|
|
126
185
|
} else open(activeItem!.dataURL, '_blank');
|
|
127
186
|
}}
|
|
128
187
|
>
|
|
129
|
-
<p>
|
|
130
|
-
We are not responsible for the contents of this {activeItem?.type == 'inode/directory' ? 'folder' : 'file'}. <br />
|
|
131
|
-
Are you sure you want to download {@render _itemName()}?
|
|
132
|
-
</p>
|
|
188
|
+
<p>Are you sure you want to download {@render _itemName()}?</p>
|
|
133
189
|
</FormDialog>
|
|
134
190
|
|
|
135
191
|
<style>
|
|
136
192
|
.list-item {
|
|
137
193
|
grid-template-columns: 1em 4fr 15em 5em repeat(4, 1em);
|
|
138
194
|
}
|
|
195
|
+
|
|
196
|
+
dialog.preview {
|
|
197
|
+
inset: 0;
|
|
198
|
+
width: 100%;
|
|
199
|
+
height: 100%;
|
|
200
|
+
background-color: #000a;
|
|
201
|
+
border: none;
|
|
202
|
+
padding: 1em;
|
|
203
|
+
word-wrap: normal;
|
|
204
|
+
anchor-scope: --preview-openers;
|
|
205
|
+
|
|
206
|
+
.preview-action:hover {
|
|
207
|
+
cursor: pointer;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.preview-top-bar {
|
|
211
|
+
display: flex;
|
|
212
|
+
align-items: center;
|
|
213
|
+
gap: 1em;
|
|
214
|
+
justify-content: space-between;
|
|
215
|
+
padding: 0;
|
|
216
|
+
position: absolute;
|
|
217
|
+
inset: 0.5em 1em 0;
|
|
218
|
+
height: fit-content;
|
|
219
|
+
|
|
220
|
+
> div {
|
|
221
|
+
display: flex;
|
|
222
|
+
gap: 1em;
|
|
223
|
+
align-items: center;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.openers {
|
|
228
|
+
padding: 1em;
|
|
229
|
+
border: 1px solid var(--border-accent);
|
|
230
|
+
border-radius: 1em;
|
|
231
|
+
height: 2em;
|
|
232
|
+
anchor-name: --preview-openers;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.openers :global([popover]) {
|
|
236
|
+
inset: anchor(bottom) anchor(right) auto anchor(left);
|
|
237
|
+
position-anchor: --preview-openers;
|
|
238
|
+
width: anchor-size(width);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.actions {
|
|
242
|
+
right: 0;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.content {
|
|
246
|
+
position: absolute;
|
|
247
|
+
inset: 3em 10em 0;
|
|
248
|
+
|
|
249
|
+
.full-fill {
|
|
250
|
+
position: absolute;
|
|
251
|
+
inset: 0;
|
|
252
|
+
width: 100%;
|
|
253
|
+
height: 100%;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.preview-text {
|
|
257
|
+
white-space: pre-wrap;
|
|
258
|
+
overflow-y: scroll;
|
|
259
|
+
line-height: 1.6;
|
|
260
|
+
background-color: var(--bg-menu);
|
|
261
|
+
font-family: monospace;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.no-preview {
|
|
266
|
+
display: flex;
|
|
267
|
+
flex-direction: column;
|
|
268
|
+
gap: 1em;
|
|
269
|
+
align-items: center;
|
|
270
|
+
justify-content: center;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
@media (width < 700px) {
|
|
274
|
+
.preview-top-bar {
|
|
275
|
+
flex-direction: column;
|
|
276
|
+
|
|
277
|
+
.actions {
|
|
278
|
+
justify-content: space-around;
|
|
279
|
+
width: 100%;
|
|
280
|
+
|
|
281
|
+
.preview-action {
|
|
282
|
+
padding: 1em;
|
|
283
|
+
flex: 1 1 0;
|
|
284
|
+
border-radius: 1em;
|
|
285
|
+
border: 1px solid var(--border-accent);
|
|
286
|
+
padding: 1em;
|
|
287
|
+
justify-content: center;
|
|
288
|
+
display: flex;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.content {
|
|
294
|
+
inset: 10em 1em 0;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
139
298
|
</style>
|
package/lib/Usage.svelte
CHANGED
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
<NumberBar
|
|
17
17
|
max={info.limits.user_size && info.limits.user_size * 1_000_000}
|
|
18
18
|
value={info.usedBytes}
|
|
19
|
-
text="
|
|
19
|
+
text="{formatBytes(info.usedBytes)} {!info.limits.user_size
|
|
20
20
|
? ''
|
|
21
|
-
: '
|
|
21
|
+
: '/ ' + formatBytes(info.limits.user_size * 1_000_000)}"
|
|
22
22
|
/>
|
|
23
23
|
</a>
|
|
24
24
|
</p>
|
package/package.json
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
<Icon i="folder-arrow-up" /> Back
|
|
36
36
|
</button>
|
|
37
37
|
<StorageList appMode bind:items user={data.session?.user} />
|
|
38
|
-
<StorageAdd parentId={item.id}
|
|
38
|
+
<StorageAdd parentId={item.id} onAdd={item => items.push(item)} />
|
|
39
39
|
{:else}
|
|
40
40
|
<p>No preview available.</p>
|
|
41
41
|
{/if}
|