@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/utils.js CHANGED
@@ -2,7 +2,13 @@
2
2
  import { filesize } from 'filesize';
3
3
  import { customRef } from 'vue';
4
4
 
5
- // https://vuejs.org/api/reactivity-advanced.html#customref
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(input).hostname;
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
- // this currently does not match: "john.doe"@example.com, user@[192.168.1.1], john.doe(comment)@example.com or 用户@例子.世界
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
- // https://en.wikipedia.org/wiki/Binary_prefix
53
- // binary units (IEC) 1024 based
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
- // decimal units (SI) 1000 based
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
- // this will print a human friendly datetime offset from now
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
- // simple extension detection, does not work with double extension like .tar.gz
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
- // those paths contain the internal type and path reference eg. shares/:shareId/folder/filename or files/folder/filename
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
- // this is from the Cloudron dashboard translation project en.json
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
- export default {
33
- name: 'GenericViewer',
34
- components: {
35
- Button,
36
- TopBar,
37
- MainLayout
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
- this.entry = entry;
56
- },
57
- onClose() {
58
- this.$emit('close');
59
- }
60
- },
61
- mounted() {
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