@brftech/filex-core 0.41.0 → 0.41.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.
Files changed (40) hide show
  1. package/dist/{ArchiveViewer-BNqukFg8.js → ArchiveViewer-DxxjhYZD.js} +2 -2
  2. package/dist/{ArchiveViewer-BNqukFg8.js.map → ArchiveViewer-DxxjhYZD.js.map} +1 -1
  3. package/dist/{CsvViewer-ii_-MgmG.js → CsvViewer-CyuwENI_.js} +2 -2
  4. package/dist/{CsvViewer-ii_-MgmG.js.map → CsvViewer-CyuwENI_.js.map} +1 -1
  5. package/dist/{DrawioViewer-B2tuu4rX.js → DrawioViewer-BIYsTX97.js} +2 -2
  6. package/dist/{DrawioViewer-B2tuu4rX.js.map → DrawioViewer-BIYsTX97.js.map} +1 -1
  7. package/dist/{EpubViewer-p4B8iiWb.js → EpubViewer-K9PjCMmS.js} +2 -2
  8. package/dist/{EpubViewer-p4B8iiWb.js.map → EpubViewer-K9PjCMmS.js.map} +1 -1
  9. package/dist/{IpynbViewer-D_qYRJhu.js → IpynbViewer-DYU6Zgz2.js} +2 -2
  10. package/dist/{IpynbViewer-D_qYRJhu.js.map → IpynbViewer-DYU6Zgz2.js.map} +1 -1
  11. package/dist/{MermaidViewer-3ZkfNM8v.js → MermaidViewer-Bp73aId8.js} +2 -2
  12. package/dist/{MermaidViewer-3ZkfNM8v.js.map → MermaidViewer-Bp73aId8.js.map} +1 -1
  13. package/dist/{PsdViewer-CuV-OPSd.js → PsdViewer-C2eLivw1.js} +2 -2
  14. package/dist/{PsdViewer-CuV-OPSd.js.map → PsdViewer-C2eLivw1.js.map} +1 -1
  15. package/dist/{TiffViewer-BLeoF4Be.js → TiffViewer-DsHg5gcz.js} +2 -2
  16. package/dist/{TiffViewer-BLeoF4Be.js.map → TiffViewer-DsHg5gcz.js.map} +1 -1
  17. package/dist/{Viewer3D-DHhhU76E.js → Viewer3D-C_dc2HN9.js} +2 -2
  18. package/dist/{Viewer3D-DHhhU76E.js.map → Viewer3D-C_dc2HN9.js.map} +1 -1
  19. package/dist/filex-core.js +1 -1
  20. package/dist/filex-core.umd.cjs +45 -45
  21. package/dist/filex-core.umd.cjs.map +1 -1
  22. package/dist/{index-BraG7Cz4.js → index-vxMZpYb-.js} +10094 -10058
  23. package/dist/index-vxMZpYb-.js.map +1 -0
  24. package/dist/index.d.ts +3 -38
  25. package/package.json +1 -1
  26. package/src/components/FilePane.vue +14 -2
  27. package/src/components/GalleryView.vue +10 -26
  28. package/src/components/GridView.vue +10 -25
  29. package/src/components/ListView.vue +17 -29
  30. package/src/components/TimeZoneDialog.vue +18 -10
  31. package/src/components/TokensPanel.vue +20 -11
  32. package/src/composables/useExplorerTimeZone.ts +7 -3
  33. package/src/composables/useLocale.ts +31 -15
  34. package/src/composables/useRowTouch.ts +95 -0
  35. package/src/lib/shareTtl.ts +21 -2
  36. package/src/lib/timezone.ts +46 -1
  37. package/src/locales/en.ts +2 -0
  38. package/src/locales/tr.ts +2 -0
  39. package/src/modals/PermissionsModal.vue +16 -5
  40. package/dist/index-BraG7Cz4.js.map +0 -1
@@ -0,0 +1,95 @@
1
+ /**
2
+ * useRowTouch — the one touch grammar every file view speaks (list, grid,
3
+ * gallery).
4
+ *
5
+ * A mouse selects with a click and opens with a double-click. A finger has no
6
+ * double-click: the browser swallows the second tap into a zoom or never sends
7
+ * `dblclick` at all, so on a phone the desktop grammar left nothing openable
8
+ * (issue #26). What a finger does instead is what every mobile file manager
9
+ * does:
10
+ *
11
+ * - a long press opens the item's menu (and, through it, selects the item);
12
+ * - a tap is reported AS a tap, so the host can open instead of select.
13
+ *
14
+ * The views only report. What a tap means is decided once, in FilePane.
15
+ *
16
+ * ⚠ A tap is judged from the gesture that produced the click, never from the
17
+ * screen: `pointerType` where the browser sets it on click (Chromium, Firefox),
18
+ * and the touchend that just preceded the click where it does not (older
19
+ * WebKit). A touch laptop's trackpad therefore keeps the desktop grammar while
20
+ * its screen gets the phone one. A `(pointer: coarse)` media query cannot tell
21
+ * those two apart.
22
+ *
23
+ * ⚠ It used to live as three identical copies of the long-press timer, one per
24
+ * view; the tap rule is exactly the kind of addition a copy misses.
25
+ */
26
+ import { onBeforeUnmount } from 'vue';
27
+
28
+ /** How long a finger rests before the press becomes a long press. */
29
+ export const LONG_PRESS_MS = 500;
30
+ /** A click this soon after a touchend is the tap that touchend ended. */
31
+ const TAP_WINDOW_MS = 800;
32
+ /** A finger that travels further than this is scrolling, not pressing. */
33
+ const MOVE_TOLERANCE_PX = 10;
34
+
35
+ export interface TouchPoint {
36
+ clientX: number;
37
+ clientY: number;
38
+ }
39
+
40
+ export function useRowTouch<T>(onLongPress: (item: T, at: TouchPoint) => void) {
41
+ let timer: ReturnType<typeof setTimeout> | undefined;
42
+ let target: T | null = null;
43
+ let origin: TouchPoint = { clientX: 0, clientY: 0 };
44
+ let longPressed = false;
45
+ let tapEndedAt = 0;
46
+
47
+ function stopTimer() {
48
+ if (timer) clearTimeout(timer);
49
+ timer = undefined;
50
+ }
51
+
52
+ function onTouchStart(item: T, ev: TouchEvent) {
53
+ const t0 = ev.touches[0];
54
+ if (!t0) return;
55
+ stopTimer();
56
+ target = item;
57
+ longPressed = false;
58
+ origin = { clientX: t0.clientX, clientY: t0.clientY };
59
+ timer = setTimeout(() => {
60
+ timer = undefined;
61
+ if (target === null) return;
62
+ longPressed = true;
63
+ onLongPress(target, origin);
64
+ }, LONG_PRESS_MS);
65
+ }
66
+
67
+ function onTouchMove(ev: TouchEvent) {
68
+ const t0 = ev.touches[0];
69
+ if (
70
+ !t0 ||
71
+ Math.abs(t0.clientX - origin.clientX) > MOVE_TOLERANCE_PX ||
72
+ Math.abs(t0.clientY - origin.clientY) > MOVE_TOLERANCE_PX
73
+ ) {
74
+ stopTimer();
75
+ target = null;
76
+ }
77
+ }
78
+
79
+ function onTouchEnd() {
80
+ stopTimer();
81
+ if (target !== null && !longPressed) tapEndedAt = Date.now();
82
+ target = null;
83
+ }
84
+
85
+ /** Whether this click is a finger's tap. */
86
+ function isTap(ev: MouseEvent): boolean {
87
+ const kind = (ev as PointerEvent).pointerType;
88
+ if (kind) return kind === 'touch';
89
+ return Date.now() - tapEndedAt < TAP_WINDOW_MS;
90
+ }
91
+
92
+ onBeforeUnmount(stopTimer);
93
+
94
+ return { onTouchStart, onTouchMove, onTouchEnd, isTap };
95
+ }
@@ -74,7 +74,12 @@ export function expiryInputMax(maxDays: number | undefined, now: Date = new Date
74
74
  }
75
75
 
76
76
  /** "Valid until 30 Aug 2026, 14:05" / "Does not expire" from the server's `expires_at`. */
77
- export function validUntilLine(expiresAt: string | null | undefined, locale: 'tr' | 'en'): string {
77
+ export function validUntilLine(
78
+ expiresAt: string | null | undefined,
79
+ locale: 'tr' | 'en',
80
+ /** The explorer whose clock applies (lib/timezone EXPLORER_CLOCK). */
81
+ clock?: symbol,
82
+ ): string {
78
83
  if (!expiresAt) return locale === 'tr' ? 'Bu bağlantının süresi yoktur.' : 'This link does not expire.';
79
84
  const d = new Date(expiresAt);
80
85
  // zaman:z1 — the viewer's chosen clock, not the browser's. "Valid until
@@ -87,7 +92,7 @@ export function validUntilLine(expiresAt: string | null | undefined, locale: 'tr
87
92
  // dialog and "Sep 20, 2026, 1:53 PM" in the listing behind it.
88
93
  const when = Number.isNaN(d.getTime())
89
94
  ? expiresAt
90
- : formatInstant(d, locale, { dateStyle: 'medium', timeStyle: 'short' });
95
+ : formatInstant(d, locale, { dateStyle: 'medium', timeStyle: 'short' }, clock);
91
96
  return locale === 'tr' ? `Bu bağlantı ${when} tarihine kadar geçerli.` : `This link is valid until ${when}.`;
92
97
  }
93
98
 
@@ -99,3 +104,17 @@ export function ttlCeilingHint(maxDays: number | undefined, locale: 'tr' | 'en')
99
104
  ? `Bağlantılar en fazla ${max} gün geçerli olabilir (sunucu ayarı).`
100
105
  : `Links can be valid for at most ${max} day${max === 1 ? '' : 's'} (server setting).`;
101
106
  }
107
+
108
+ /**
109
+ * The share dialog's muted detail line: its facts joined with " · ".
110
+ *
111
+ * ⚠ The first fact is a whole sentence ("This link is valid until …, 10:00
112
+ * AM.") and the ones after it are fragments ("3 downloads"), so a plain join
113
+ * printed "10:00 AM. · 3 downloads" — a full stop in the middle of a line. A
114
+ * fact that is followed by another loses its closing full stop; the last one
115
+ * keeps whatever it has.
116
+ */
117
+ export function shareDetailLine(facts: Array<string | null | undefined>): string {
118
+ const kept = facts.filter((f): f is string => !!f);
119
+ return kept.map((f, i) => (i < kept.length - 1 ? f.replace(/\.\s*$/, '') : f)).join(' · ');
120
+ }
@@ -36,7 +36,7 @@
36
36
  * that is shared state by nature — the browser's.
37
37
  */
38
38
 
39
- import { computed, ref, type Ref } from 'vue';
39
+ import { computed, ref, type InjectionKey, type Ref } from 'vue';
40
40
 
41
41
  /* ── the order ────────────────────────────────────────────────────────── */
42
42
 
@@ -312,6 +312,11 @@ function ownedTier() {
312
312
  const list = entries.value;
313
313
  return list.length ? list[list.length - 1].zone : '';
314
314
  },
315
+ /** The newest answer among the owners `pick` accepts. */
316
+ currentWhere(pick: (owner: symbol) => boolean): string {
317
+ const list = entries.value.filter((e) => pick(e.owner));
318
+ return list.length ? list[list.length - 1].zone : '';
319
+ },
315
320
  };
316
321
  }
317
322
 
@@ -345,6 +350,46 @@ export function resolvedTimeZone(): ResolvedTimeZone {
345
350
  return resolved.value;
346
351
  }
347
352
 
353
+ /**
354
+ * The owner key an explorer PROVIDES to its components, so the dates they
355
+ * print resolve against that explorer's own tiers (useLocale injects it).
356
+ */
357
+ export const EXPLORER_CLOCK: InjectionKey<symbol> = Symbol('filex-explorer-clock');
358
+
359
+ /**
360
+ * What the tiers hold for ONE explorer.
361
+ *
362
+ * ⚠⚠ The page-wide tiers answer with the NEWEST write, which is right for a
363
+ * surface outside every explorer and wrong inside one: two explorers on one
364
+ * page with different `config.timeZone` both printed the zone of whichever
365
+ * mounted last. The host tier is by definition one explorer's setting, so an
366
+ * explorer reads only its own. Its account tier is its own too; the only other
367
+ * account it may inherit is the one a HOST APP remembered for the person
368
+ * signed in (the admin app's session, `remember: true`) — never a sibling
369
+ * explorer's, which may belong to a different key altogether. The viewer tier
370
+ * is this browser's, shared by everything on the page.
371
+ *
372
+ * `owner` undefined is the page-wide answer, unchanged.
373
+ */
374
+ export function timeZoneSourcesFor(owner: symbol | undefined): Required<TimeZoneSources> {
375
+ if (!owner) return timeZoneSources();
376
+ return {
377
+ viewer: viewerZone.value,
378
+ host: hostTier.of(owner) ?? '',
379
+ account: accountTier.of(owner) ?? accountTier.currentWhere((o) => rememberingOwners.has(o)),
380
+ };
381
+ }
382
+
383
+ /** resolvedTimeZone for ONE explorer (see timeZoneSourcesFor). Reactive. */
384
+ export function resolvedTimeZoneFor(owner: symbol | undefined): ResolvedTimeZone {
385
+ return owner ? resolveTimeZone(timeZoneSourcesFor(owner)) : resolved.value;
386
+ }
387
+
388
+ /** activeTimeZone for ONE explorer (see timeZoneSourcesFor). Reactive. */
389
+ export function activeTimeZoneFor(owner: symbol | undefined): string | undefined {
390
+ return resolvedTimeZoneFor(owner).zone;
391
+ }
392
+
348
393
  /** What each tier currently holds (`''` = nothing). Reactive. */
349
394
  export function timeZoneSources(): Required<TimeZoneSources> {
350
395
  return {
package/src/locales/en.ts CHANGED
@@ -1081,6 +1081,8 @@ export const en: Record<string, string> = {
1081
1081
  'conn.tokens.rootPlaceholder': 'storage://folder',
1082
1082
  'conn.tokens.expiry': 'Expires in (days)',
1083
1083
  'conn.tokens.expiryNever': 'never',
1084
+ 'conn.tokens.defaultName': 'API key {date}',
1085
+ 'conn.tokens.namePlaceholder': 'Name — e.g. backup script',
1084
1086
  'conn.tokens.capNote': 'Your account and its permissions are the ceiling — asking for more than you have is refused, not granted.',
1085
1087
  /* === /gezinti:g1 === */
1086
1088
 
package/src/locales/tr.ts CHANGED
@@ -1079,6 +1079,8 @@ export const tr: Record<string, string> = {
1079
1079
  'conn.tokens.rootPlaceholder': 'depo://klasör',
1080
1080
  'conn.tokens.expiry': 'Geçerlilik (gün)',
1081
1081
  'conn.tokens.expiryNever': 'süresiz',
1082
+ 'conn.tokens.defaultName': 'API anahtarı {date}',
1083
+ 'conn.tokens.namePlaceholder': 'Ad — ör. yedekleme betiği',
1082
1084
  'conn.tokens.capNote': 'Tavan hesabın ve yetkilerin — sahip olduğundan fazlasını istemek reddedilir, verilmez.',
1083
1085
  /* === /gezinti:g1 === */
1084
1086
 
@@ -27,11 +27,19 @@
27
27
  * (`gorunum:v2-share`). A scoped style block here is silently dropped in the
28
28
  * web-component build — see web/tests/api/scopedStyles.test.ts.
29
29
  */
30
- import { ref, onMounted, onBeforeUnmount, computed, watch } from 'vue';
30
+ import { ref, onMounted, onBeforeUnmount, computed, inject, watch } from 'vue';
31
31
  import type { FileApi, Grant, UserSuggestion } from '../composables/useFileApi';
32
32
  import type { ShareInfo } from '../types/FileNode';
33
33
  import { shareCliCommand } from '../lib/shareCli';
34
- import { STOCK_EXPIRY_DAYS, clampExpiryOptions, defaultExpiryDays, ttlCeilingHint, validUntilLine } from '../lib/shareTtl';
34
+ import {
35
+ STOCK_EXPIRY_DAYS,
36
+ clampExpiryOptions,
37
+ defaultExpiryDays,
38
+ shareDetailLine,
39
+ ttlCeilingHint,
40
+ validUntilLine,
41
+ } from '../lib/shareTtl';
42
+ import { EXPLORER_CLOCK } from '../lib/timezone';
35
43
  import { resolveLocale } from '../locales/resolve';
36
44
  import { formatByteSize, useLocale } from '../composables/useLocale';
37
45
  import { actionIconSvg } from '../lib/actionIcons';
@@ -636,8 +644,11 @@ function expiryLine(days: number): string {
636
644
  }
637
645
  // What the server actually stored — shown under a fresh link so the real
638
646
  // expiry is visible even when the server shortened the request.
647
+ // The explorer this dialog belongs to reads deadlines on its own clock.
648
+ const clock = inject(EXPLORER_CLOCK, undefined);
649
+
639
650
  function validUntil(r: { expiresAt?: string | null } | null): string {
640
- return validUntilLine(r?.expiresAt ?? null, tr.value ? 'tr' : 'en');
651
+ return validUntilLine(r?.expiresAt ?? null, tr.value ? 'tr' : 'en', clock);
641
652
  }
642
653
 
643
654
  /* ── the top tier: one switch, one sentence, one link ──────────────────── */
@@ -671,10 +682,10 @@ const linkDetail = computed(() => {
671
682
  if (shareMaxDl.value) bits.push(maxDlOptions.find((o) => o.v === shareMaxDl.value)?.l ?? '');
672
683
  } else if (downloadShares.value[0]) {
673
684
  const s = downloadShares.value[0];
674
- bits.push(validUntilLine(s.expires_at ?? null, tr.value ? 'tr' : 'en'));
685
+ bits.push(validUntil({ expiresAt: s.expires_at ?? null }));
675
686
  if (s.max_downloads) bits.push(maxDlOptions.find((o) => o.v === s.max_downloads)?.l ?? String(s.max_downloads));
676
687
  }
677
- return bits.filter(Boolean).join(' · ');
688
+ return shareDetailLine(bits);
678
689
  });
679
690
 
680
691
  /* Section summaries — a named section still has to say what is inside it, or