@cloudron/pankow 4.2.3 → 4.3.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/components/ListItem.vue +188 -0
- package/components/Notification.vue +9 -61
- package/components/TabView.vue +26 -31
- package/fallbackImage.js +21 -17
- package/fetcher.js +47 -5
- package/gestures.js +15 -11
- package/index.js +7 -0
- package/notifyState.js +48 -0
- package/package.json +20 -2
- package/plugin.js +39 -0
- package/tooltip.js +34 -15
- package/types/fallbackImage.d.ts +5 -4
- package/types/fetcher.d.ts +60 -10
- package/types/gestures.d.ts +16 -1
- package/types/index.d.ts +4 -1
- package/types/notifyState.d.ts +8 -0
- package/types/plugin.d.ts +4 -0
- package/types/tooltip.d.ts +5 -8
- package/types/utils.d.ts +185 -25
- package/utils.js +154 -14
- package/viewers/GenericViewer.vue +20 -32
- package/viewers/ImageViewer.vue +149 -140
- package/viewers/PdfViewer.vue +27 -35
- package/viewers/TextViewer.vue +134 -136
- package/vite-plugin.js +16 -0
package/utils.js
CHANGED
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
import { filesize } from 'filesize';
|
|
3
3
|
import { customRef } from 'vue';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Creates a debounced reactive ref that delays value updates.
|
|
7
|
+
* @see https://vuejs.org/api/reactivity-advanced.html#customref
|
|
8
|
+
* @param {any} value
|
|
9
|
+
* @param {number} [delay=300] - Debounce delay in milliseconds
|
|
10
|
+
* @returns {import('vue').Ref}
|
|
11
|
+
*/
|
|
6
12
|
function useDebouncedRef(value, delay = 300) {
|
|
7
13
|
let timeout;
|
|
8
14
|
return customRef((track, trigger) => {
|
|
@@ -22,25 +28,46 @@ function useDebouncedRef(value, delay = 300) {
|
|
|
22
28
|
})
|
|
23
29
|
}
|
|
24
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Extracts the MIME type group from an item (e.g. "image" from "image/png").
|
|
33
|
+
* @param {{ mimeType: string }} item
|
|
34
|
+
* @returns {string}
|
|
35
|
+
* @throws {string} If item has no mimeType string property
|
|
36
|
+
*/
|
|
25
37
|
function getFileTypeGroup(item) {
|
|
26
38
|
if (typeof item.mimeType !== 'string') throw 'item must have mimeType string property';
|
|
27
39
|
return item.mimeType.split('/')[0];
|
|
28
40
|
}
|
|
29
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Validates whether a string is a valid domain or URL (extracts hostname from URLs first).
|
|
44
|
+
* @param {string} domain
|
|
45
|
+
* @returns {boolean}
|
|
46
|
+
*/
|
|
30
47
|
function isValidDomainOrURL(domain) {
|
|
31
48
|
try {
|
|
32
|
-
domain = new URL(
|
|
49
|
+
domain = new URL(domain).hostname;
|
|
33
50
|
} catch (e) {}
|
|
34
51
|
|
|
35
52
|
return isValidDomain(domain);
|
|
36
53
|
}
|
|
37
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Validates whether a string is a valid domain name.
|
|
57
|
+
* @param {string} domain
|
|
58
|
+
* @returns {boolean}
|
|
59
|
+
*/
|
|
38
60
|
function isValidDomain(domain) {
|
|
39
61
|
const domainRegex = /^([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/;
|
|
40
62
|
return domainRegex.test(domain);
|
|
41
63
|
}
|
|
42
64
|
|
|
43
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Validates whether a string is a valid email address.
|
|
67
|
+
* Does not match: quoted local-parts, IP-literal domains, comments, or internationalized addresses.
|
|
68
|
+
* @param {string} email
|
|
69
|
+
* @returns {boolean}
|
|
70
|
+
*/
|
|
44
71
|
function isValidEmail(email) {
|
|
45
72
|
if (!email || typeof email !== 'string') return false;
|
|
46
73
|
|
|
@@ -49,8 +76,13 @@ function isValidEmail(email) {
|
|
|
49
76
|
}
|
|
50
77
|
|
|
51
78
|
|
|
52
|
-
|
|
53
|
-
|
|
79
|
+
/**
|
|
80
|
+
* Formats a byte count into human-readable IEC binary units (KiB, MiB, GiB, TiB).
|
|
81
|
+
* @see https://en.wikipedia.org/wiki/Binary_prefix
|
|
82
|
+
* @param {number} size - Byte count; -1 returns "Unlimited"
|
|
83
|
+
* @param {string|number} [fallback] - Value returned when size is falsy (0, null, undefined)
|
|
84
|
+
* @returns {string|number}
|
|
85
|
+
*/
|
|
54
86
|
function prettyBinarySize(size, fallback) {
|
|
55
87
|
if (!size) return fallback || 0;
|
|
56
88
|
if (size === -1) return 'Unlimited';
|
|
@@ -60,7 +92,12 @@ function prettyBinarySize(size, fallback) {
|
|
|
60
92
|
return (size / Math.pow(1024, i)).toFixed(3) * 1 + ' ' + ['B', 'KiB', 'MiB', 'GiB', 'TiB'][i];
|
|
61
93
|
}
|
|
62
94
|
|
|
63
|
-
|
|
95
|
+
/**
|
|
96
|
+
* Formats a byte count into human-readable SI decimal units (kB, MB, GB, TB).
|
|
97
|
+
* @param {number} size - Byte count
|
|
98
|
+
* @param {string|number} [fallback] - Value returned when size is falsy
|
|
99
|
+
* @returns {string|number}
|
|
100
|
+
*/
|
|
64
101
|
function prettyDecimalSize(size, fallback) {
|
|
65
102
|
if (!size) return fallback || 0;
|
|
66
103
|
|
|
@@ -68,6 +105,11 @@ function prettyDecimalSize(size, fallback) {
|
|
|
68
105
|
return (size / Math.pow(1000, i)).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i];
|
|
69
106
|
}
|
|
70
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Formats a date as a relative time string using Intl.RelativeTimeFormat.
|
|
110
|
+
* @param {Date} date
|
|
111
|
+
* @returns {string}
|
|
112
|
+
*/
|
|
71
113
|
function fromNow(date) {
|
|
72
114
|
const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' });
|
|
73
115
|
|
|
@@ -93,7 +135,11 @@ function fromNow(date) {
|
|
|
93
135
|
return rtf.format(years, 'year');
|
|
94
136
|
}
|
|
95
137
|
|
|
96
|
-
|
|
138
|
+
/**
|
|
139
|
+
* Formats a value as a human-friendly relative time offset from now (e.g. "2 days ago").
|
|
140
|
+
* @param {string|number} value - Date string or timestamp; falsy returns "never"
|
|
141
|
+
* @returns {string}
|
|
142
|
+
*/
|
|
97
143
|
function prettyDate(value) {
|
|
98
144
|
if (!value) return 'never';
|
|
99
145
|
|
|
@@ -103,6 +149,12 @@ function prettyDate(value) {
|
|
|
103
149
|
return fromNow(date);
|
|
104
150
|
}
|
|
105
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Formats a date using a named format preset.
|
|
154
|
+
* @param {'hh:mm A'|'hh:mm:ss A'|'DD MMM'|'DD MMM hh:mm A'} format - Preset format key
|
|
155
|
+
* @param {string|number} value - Date string or timestamp
|
|
156
|
+
* @returns {string}
|
|
157
|
+
*/
|
|
106
158
|
function formatDate(format, value) {
|
|
107
159
|
const formatters = {
|
|
108
160
|
'hh:mm A': new Intl.DateTimeFormat(undefined, {
|
|
@@ -126,16 +178,17 @@ function formatDate(format, value) {
|
|
|
126
178
|
hour: '2-digit',
|
|
127
179
|
minute: '2-digit',
|
|
128
180
|
hour12: true
|
|
129
|
-
}),
|
|
130
|
-
'DD MMM': new Intl.DateTimeFormat(undefined, { // force this to get month first
|
|
131
|
-
day: '2-digit',
|
|
132
|
-
month: 'short'
|
|
133
181
|
})
|
|
134
182
|
};
|
|
135
183
|
|
|
136
184
|
return formatters[format].format(new Date(value));
|
|
137
185
|
}
|
|
138
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Formats a value as a short time string (medium time style via Intl.DateTimeFormat).
|
|
189
|
+
* @param {string|number} value - Date string or timestamp; falsy returns "unknown"
|
|
190
|
+
* @returns {string}
|
|
191
|
+
*/
|
|
139
192
|
function prettyShortDate(value) {
|
|
140
193
|
if (!value) return 'unknown';
|
|
141
194
|
|
|
@@ -151,6 +204,11 @@ function prettyShortDate(value) {
|
|
|
151
204
|
return formattedDate;
|
|
152
205
|
}
|
|
153
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Formats a value as a long date string (day, month, year, time).
|
|
209
|
+
* @param {string|number} value - Date string or timestamp; falsy returns "unknown"
|
|
210
|
+
* @returns {string}
|
|
211
|
+
*/
|
|
154
212
|
function prettyLongDate(value) {
|
|
155
213
|
if (!value) return 'unknown';
|
|
156
214
|
|
|
@@ -170,12 +228,22 @@ function prettyLongDate(value) {
|
|
|
170
228
|
return formattedDate;
|
|
171
229
|
}
|
|
172
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Formats a numeric value as a human-readable file size string (uses the `filesize` library).
|
|
233
|
+
* @param {number} value - Byte count
|
|
234
|
+
* @returns {string}
|
|
235
|
+
*/
|
|
173
236
|
function prettyFileSize(value) {
|
|
174
237
|
if (typeof value !== 'number') return 'unknown';
|
|
175
238
|
|
|
176
239
|
return filesize(value);
|
|
177
240
|
}
|
|
178
241
|
|
|
242
|
+
/**
|
|
243
|
+
* Strips angle brackets from email address strings or arrays.
|
|
244
|
+
* @param {string|string[]} addresses - Format: "<name@example.com>"; returns empty string if falsy
|
|
245
|
+
* @returns {string}
|
|
246
|
+
*/
|
|
179
247
|
function prettyEmailAddresses(addresses) {
|
|
180
248
|
if (!addresses) return '';
|
|
181
249
|
if (addresses === '<>') return '<>';
|
|
@@ -183,6 +251,11 @@ function prettyEmailAddresses(addresses) {
|
|
|
183
251
|
return addresses.slice(1, -1);
|
|
184
252
|
}
|
|
185
253
|
|
|
254
|
+
/**
|
|
255
|
+
* Formats milliseconds as a human-readable duration string using Intl.DurationFormat.
|
|
256
|
+
* @param {number} ms - Duration in milliseconds
|
|
257
|
+
* @returns {string}
|
|
258
|
+
*/
|
|
186
259
|
function prettyDuration(ms) {
|
|
187
260
|
const totalSeconds = Math.floor(ms / 1000);
|
|
188
261
|
const days = Math.floor(totalSeconds / 86400);
|
|
@@ -194,15 +267,32 @@ function prettyDuration(ms) {
|
|
|
194
267
|
return df.format({ days, hours, minutes, seconds });
|
|
195
268
|
}
|
|
196
269
|
|
|
270
|
+
/**
|
|
271
|
+
* Normalizes a file path: ensures leading slash and collapses consecutive slashes.
|
|
272
|
+
* @param {string} path
|
|
273
|
+
* @returns {string}
|
|
274
|
+
*/
|
|
197
275
|
function sanitize(path) {
|
|
198
276
|
path = '/' + path;
|
|
199
277
|
return path.replace(/\/+/g, '/');
|
|
200
278
|
}
|
|
201
279
|
|
|
280
|
+
/**
|
|
281
|
+
* Joins path segments and normalizes the result (via sanitize).
|
|
282
|
+
* @param {...string} args - Path segments to join
|
|
283
|
+
* @returns {string}
|
|
284
|
+
*/
|
|
202
285
|
function pathJoin() {
|
|
203
286
|
return sanitize(Array.from(arguments).join('/'));
|
|
204
287
|
}
|
|
205
288
|
|
|
289
|
+
/**
|
|
290
|
+
* Triggers a file download via the browser. Single entry: direct download.
|
|
291
|
+
* Multiple entries: zipped archive download via /api/v1/download.
|
|
292
|
+
* @param {Array<{ filePath: string, share?: { id: string } }>} entries - File entries to download
|
|
293
|
+
* @param {string} [name] - Archive name for multi-file downloads
|
|
294
|
+
* @returns {void}
|
|
295
|
+
*/
|
|
206
296
|
function download(entries, name) {
|
|
207
297
|
if (!entries.length) return;
|
|
208
298
|
|
|
@@ -231,12 +321,22 @@ function download(entries, name) {
|
|
|
231
321
|
window.location.href = '/api/v1/download?' + params.toString();
|
|
232
322
|
}
|
|
233
323
|
|
|
234
|
-
|
|
324
|
+
/**
|
|
325
|
+
* Extracts the file extension from a file entry. Does not detect double extensions (e.g. .tar.gz).
|
|
326
|
+
* @param {{ isFile: boolean, fileName: string }} entry
|
|
327
|
+
* @returns {string} Extension without leading dot (empty string for directories or files without extension)
|
|
328
|
+
*/
|
|
235
329
|
function getExtension(entry) {
|
|
236
330
|
if (entry.isFile) return entry.fileName.slice(entry.fileName.lastIndexOf('.') + 1);
|
|
237
331
|
return '';
|
|
238
332
|
}
|
|
239
333
|
|
|
334
|
+
/**
|
|
335
|
+
* Copies a string to the system clipboard using a temporary input element.
|
|
336
|
+
* @param {string} value
|
|
337
|
+
* @returns {void}
|
|
338
|
+
* @deprecated Prefer the native `navigator.clipboard.writeText()` API.
|
|
339
|
+
*/
|
|
240
340
|
function copyToClipboard(value) {
|
|
241
341
|
var elem = document.createElement('input');
|
|
242
342
|
elem.value = value;
|
|
@@ -246,11 +346,20 @@ function copyToClipboard(value) {
|
|
|
246
346
|
elem.remove();
|
|
247
347
|
}
|
|
248
348
|
|
|
349
|
+
/**
|
|
350
|
+
* Parses the current page's URL query string into a key/value object.
|
|
351
|
+
* @returns {Object<string, string>}
|
|
352
|
+
*/
|
|
249
353
|
function urlSearchQuery() {
|
|
250
354
|
return decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
|
|
251
355
|
}
|
|
252
356
|
|
|
253
|
-
|
|
357
|
+
/**
|
|
358
|
+
* Parses a Cloudron resource path into its components (type, path, shareId, apiPath).
|
|
359
|
+
* Handles paths like "files/folder/filename" or "shares/:shareId/folder/filename".
|
|
360
|
+
* @param {string} resourcePath
|
|
361
|
+
* @returns {{ type: string, path: string, shareId: string, apiPath: string, resourcePath: string }}
|
|
362
|
+
*/
|
|
254
363
|
function parseResourcePath(resourcePath) {
|
|
255
364
|
var result = {
|
|
256
365
|
type: '',
|
|
@@ -279,10 +388,22 @@ function parseResourcePath(resourcePath) {
|
|
|
279
388
|
return result;
|
|
280
389
|
}
|
|
281
390
|
|
|
391
|
+
/**
|
|
392
|
+
* Generates a unique identifier for a file entry, including its share ID if present.
|
|
393
|
+
* @param {{ share?: { id: string }, filePath: string }} entry
|
|
394
|
+
* @returns {string}
|
|
395
|
+
*/
|
|
282
396
|
function getEntryIdentifier(entry) {
|
|
283
397
|
return (entry.share ? (entry.share.id + '/') : '') + entry.filePath;
|
|
284
398
|
}
|
|
285
399
|
|
|
400
|
+
/**
|
|
401
|
+
* Sorts an array of objects by a given property, case-insensitive for strings.
|
|
402
|
+
* @param {Array<Object>} list - Array of objects to sort
|
|
403
|
+
* @param {string} prop - Property name to sort by
|
|
404
|
+
* @param {boolean} desc - If falsey, reverses the sort (descending order)
|
|
405
|
+
* @returns {Array<Object>}
|
|
406
|
+
*/
|
|
286
407
|
function entryListSort(list, prop, desc) {
|
|
287
408
|
var tmp = list.sort(function (a, b) {
|
|
288
409
|
var av = a[prop];
|
|
@@ -296,11 +417,20 @@ function entryListSort(list, prop, desc) {
|
|
|
296
417
|
return tmp.reverse();
|
|
297
418
|
}
|
|
298
419
|
|
|
420
|
+
/**
|
|
421
|
+
* Returns a promise that resolves after the specified number of milliseconds.
|
|
422
|
+
* @param {number} ms
|
|
423
|
+
* @returns {Promise<void>}
|
|
424
|
+
*/
|
|
299
425
|
function sleep(ms) {
|
|
300
426
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
301
427
|
}
|
|
302
428
|
|
|
303
|
-
|
|
429
|
+
/**
|
|
430
|
+
* Fallback English translations from the Cloudron dashboard translation project.
|
|
431
|
+
* Used when the app does not provide its own i18n strings.
|
|
432
|
+
* @type {Object<string, Object<string, string>>}
|
|
433
|
+
*/
|
|
304
434
|
const fallbackTranslations = {
|
|
305
435
|
"main": {
|
|
306
436
|
"dialog": {
|
|
@@ -410,6 +540,12 @@ const fallbackTranslations = {
|
|
|
410
540
|
}
|
|
411
541
|
};
|
|
412
542
|
|
|
543
|
+
/**
|
|
544
|
+
* Looks up a translation string by dot-separated key (e.g. "filemanager.title").
|
|
545
|
+
* Falls back to the key itself if the translation is not found.
|
|
546
|
+
* @param {string} id - Dot-separated translation key
|
|
547
|
+
* @returns {string}
|
|
548
|
+
*/
|
|
413
549
|
function translation(id) {
|
|
414
550
|
let value;
|
|
415
551
|
try {
|
|
@@ -422,6 +558,10 @@ function translation(id) {
|
|
|
422
558
|
return value;
|
|
423
559
|
}
|
|
424
560
|
|
|
561
|
+
/**
|
|
562
|
+
* Generates a random UUID v4 string using crypto.getRandomValues.
|
|
563
|
+
* @returns {string}
|
|
564
|
+
*/
|
|
425
565
|
function uuidv4() {
|
|
426
566
|
return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, c =>
|
|
427
567
|
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
|
|
@@ -22,45 +22,33 @@
|
|
|
22
22
|
</MainLayout>
|
|
23
23
|
</template>
|
|
24
24
|
|
|
25
|
-
<script>
|
|
25
|
+
<script setup>
|
|
26
26
|
|
|
27
|
+
import { reactive } from 'vue';
|
|
27
28
|
import Button from '../components/Button.vue';
|
|
28
29
|
import MainLayout from '../components/MainLayout.vue';
|
|
29
30
|
import TopBar from '../components/TopBar.vue';
|
|
30
31
|
import utils from '../utils.js';
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
},
|
|
39
|
-
props: {
|
|
40
|
-
tr: {
|
|
41
|
-
type: Function,
|
|
42
|
-
default(id) { console.warn('Missing tr for GenericViewer'); return utils.translation(id); }
|
|
43
|
-
}
|
|
44
|
-
},
|
|
45
|
-
emits: [ 'close' ],
|
|
46
|
-
data() {
|
|
47
|
-
return {
|
|
48
|
-
entry: {}
|
|
49
|
-
};
|
|
50
|
-
},
|
|
51
|
-
methods: {
|
|
52
|
-
async open(entry) {
|
|
53
|
-
if (!entry) return;
|
|
33
|
+
const props = defineProps({
|
|
34
|
+
tr: {
|
|
35
|
+
type: Function,
|
|
36
|
+
default(id) { console.warn('Missing tr for GenericViewer'); return utils.translation(id); }
|
|
37
|
+
}
|
|
38
|
+
});
|
|
54
39
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
40
|
+
const emit = defineEmits([ 'close' ]);
|
|
41
|
+
|
|
42
|
+
const entry = reactive({});
|
|
43
|
+
|
|
44
|
+
async function open(e) {
|
|
45
|
+
if (!e) return;
|
|
46
|
+
Object.assign(entry, e);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function onClose() {
|
|
50
|
+
emit('close');
|
|
51
|
+
}
|
|
64
52
|
|
|
65
53
|
</script>
|
|
66
54
|
|