@brftech/filex-core 0.21.5 → 0.22.0

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/dist/index.d.ts CHANGED
@@ -824,6 +824,32 @@ export declare interface ExplorerConfig {
824
824
  driver?: string;
825
825
  readOnly?: boolean;
826
826
  }>;
827
+ /**
828
+ * Desktop-shell hook — selective sync ("keep on this computer").
829
+ *
830
+ * Present only when the explorer runs inside the filex desktop app; the
831
+ * shell passes functions that talk to its sync engine, and the explorer
832
+ * grows "Keep on this computer" / "Online only" entries on folder menus.
833
+ * Absent (web admin, embeds): nothing about it renders.
834
+ *
835
+ * Folders only, by design: the sync engine pairs directories, and a file
836
+ * rides along with the folder that holds it.
837
+ */
838
+ desktopSync?: {
839
+ /** Kept folders for the mounted account, adapter-qualified remotes. */
840
+ kept: () => Promise<Array<{
841
+ remote: string;
842
+ local: string;
843
+ }>>;
844
+ /** Start keeping a folder. Resolves once the pair is registered (or the
845
+ * user cancelled the shell's root-folder prompt — re-read `kept`). */
846
+ keep: (remote: string) => Promise<void>;
847
+ /** Stop keeping. The SHELL owns the "what happens to the local copy"
848
+ * question — it asks natively and may cancel; re-read `kept` after. */
849
+ unkeep: (remote: string) => Promise<void>;
850
+ /** Open the folder's local mirror in the OS file manager. */
851
+ reveal: (remote: string) => Promise<void>;
852
+ };
827
853
  }
828
854
 
829
855
  /** Component emits — the parent listens for these events. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brftech/filex-core",
3
- "version": "0.21.5",
3
+ "version": "0.22.0",
4
4
  "description": "filex core — Vue 3 source of truth for the filex file manager (FileExplorer + ConnectionsPanel SFCs, composables, types)",
5
5
  "type": "module",
6
6
  "main": "./dist/filex-core.umd.cjs",
@@ -1419,8 +1419,68 @@ async function onToolbarAction(key: string) {
1419
1419
  await dispatchItemAction(key, sel);
1420
1420
  }
1421
1421
 
1422
+ // ─── desktop selective sync — "keep on this computer" ──────────────────
1423
+ // Present only when the desktop shell passes config.desktopSync; the web
1424
+ // admin and the embeds never see these entries. State is PULLED, not pushed:
1425
+ // the kept list is re-read as a menu opens, so the component needs no event
1426
+ // channel back to the shell and cannot go stale in a way that outlives one
1427
+ // right-click.
1428
+ const desktopSync = computed(() => props.config.desktopSync ?? null);
1429
+ const keptPairs = ref<Array<{ remote: string; local: string }>>([]);
1430
+
1431
+ async function refreshKept(): Promise<void> {
1432
+ if (!desktopSync.value) return;
1433
+ try {
1434
+ keptPairs.value = await desktopSync.value.kept();
1435
+ } catch {
1436
+ // Shell went away mid-call; a stale entry only mislabels a menu item.
1437
+ }
1438
+ }
1439
+ onMounted(() => void refreshKept());
1440
+
1441
+ /** Adapter-qualified remote for a row. Virtual storage rows carry a bare
1442
+ * name (`docs`), real rows a wire path (`docs://reports`). */
1443
+ function keepRemoteOf(node: FileNode): string {
1444
+ const p = String(node.path ?? '');
1445
+ return p.includes('://') ? p.replace(/\/+$/, '') : `${p}://`;
1446
+ }
1447
+
1448
+ type KeepState = 'none' | 'kept' | 'inherited' | 'partial';
1449
+
1450
+ /** How `remote` relates to the kept set: exactly a pair, inside one
1451
+ * (inherited), an ancestor of some (partial), or unrelated. */
1452
+ function keepStateOf(remote: string): KeepState {
1453
+ const inside = (child: string, parent: string) =>
1454
+ parent.endsWith('://')
1455
+ ? child.startsWith(parent) && child !== parent
1456
+ : child === parent
1457
+ ? false
1458
+ : child.startsWith(parent + '/');
1459
+ if (keptPairs.value.some((p) => p.remote === remote)) return 'kept';
1460
+ if (keptPairs.value.some((p) => inside(remote, p.remote))) return 'inherited';
1461
+ if (keptPairs.value.some((p) => inside(p.remote, remote))) return 'partial';
1462
+ return 'none';
1463
+ }
1464
+
1465
+ /** Menu entries for one selected folder, by its keep state. Empty for
1466
+ * multi-selections, files, trash, encrypted folders, or a web mount. */
1467
+ function keepActionsFor(sel: FileNode[]): ContextAction[] {
1468
+ const ds = desktopSync.value;
1469
+ const single = sel.length === 1 && sel[0]?.type === 'dir';
1470
+ if (!ds || !single || trashActive.value || e2eActive.value || sel[0]?.e2e === true) return [];
1471
+ const st = keepStateOf(keepRemoteOf(sel[0]!));
1472
+ return [
1473
+ { divider: true, key: 'sep-keep', label: '' },
1474
+ { key: 'keep-local', label: t('ctx.keep_local'), icon: '📌', hidden: st === 'kept' || st === 'inherited' },
1475
+ { key: 'keep-online', label: t('ctx.keep_online'), icon: '☁', hidden: st !== 'kept' },
1476
+ { key: 'keep-inherited', label: t('ctx.keep_inherited'), icon: '📌', disabled: true, hidden: st !== 'inherited' },
1477
+ { key: 'keep-reveal', label: t('ctx.keep_reveal'), icon: '📂', hidden: st !== 'kept' && st !== 'inherited' },
1478
+ ];
1479
+ }
1480
+
1422
1481
  async function onContextTarget(node: FileNode, ev: MouseEvent) {
1423
1482
  ctxMode.value = 'selection';
1483
+ void refreshKept(); // menu labels react if the kept set changed since last look
1424
1484
  if (!selection.has(node.path)) {
1425
1485
  selection.click(node.path);
1426
1486
  await nextTick();
@@ -1447,6 +1507,7 @@ function onCrumbContext(payload: { x: number; y: number; adapterPath: string; la
1447
1507
  * dispatchItemAction'a gider ve ctxMode==='pane' iken pane-route'lanır. */
1448
1508
  function onPaneContext(node: FileNode | null, ev: MouseEvent) {
1449
1509
  activePane.value = 'split';
1510
+ void refreshKept();
1450
1511
  const sel = splitPaneRef.value?.selectedNodes() ?? [];
1451
1512
  // node=null (boş alana sağ-tık): seçimsiz menü (Yeni Klasör + Yapıştır).
1452
1513
  // Aksi halde pane seçimi (yoksa tıklanan node) hedeftir.
@@ -1507,6 +1568,9 @@ const contextActions = computed<ContextAction[]>(() => {
1507
1568
  return [
1508
1569
  { key: 'open', label: t('ctx.open'), icon: '↗' },
1509
1570
  { key: 'open-tab', label: t('ctx.open_new_tab'), icon: '⧉' } /* wiring:d1 */,
1571
+ // A whole storage can be kept too — that IS the "sync everything"
1572
+ // shape, and it is one pair, not one per subfolder.
1573
+ ...keepActionsFor(sel),
1510
1574
  ];
1511
1575
  }
1512
1576
 
@@ -1571,6 +1635,7 @@ function selectionActionList(sel: FileNode[]): ContextAction[] {
1571
1635
  { key: 'paste', label: t('ctx.paste'), icon: '📋', hidden: !w, disabled: !clipboard.value.mode },
1572
1636
  { divider: true, key: 'sep-meta', label: '', hidden: !singleHasId },
1573
1637
  { key: 'tags', label: tagsLabel, icon: '🏷', hidden: !singleHasId, disabled: !singleHasId },
1638
+ ...keepActionsFor(sel),
1574
1639
  { divider: true, key: 'sep2', label: '', hidden: !w },
1575
1640
  { key: 'delete', label: t('ctx.delete'), icon: '🗑', danger: true, hidden: !any || !w, disabled: !any },
1576
1641
  ];
@@ -1631,6 +1696,38 @@ async function dispatchItemAction(key: string, targets: FileNode[]) {
1631
1696
  case 'download':
1632
1697
  if (targets[0]) downloadFile(targets[0]);
1633
1698
  break;
1699
+ case 'keep-local': {
1700
+ const ds = desktopSync.value;
1701
+ if (!ds || !targets[0]) break;
1702
+ const remote = keepRemoteOf(targets[0]);
1703
+ try {
1704
+ await ds.keep(remote);
1705
+ await refreshKept();
1706
+ // The shell may have shown its root-folder prompt and been cancelled —
1707
+ // only claim success when the pair is really there now.
1708
+ if (keepStateOf(remote) !== 'none') flashToast(t('keep.started'));
1709
+ } catch (e) {
1710
+ await refreshKept();
1711
+ flashToast(`${t('keep.failed')}: ${String((e as Error)?.message ?? e)}`);
1712
+ }
1713
+ break;
1714
+ }
1715
+ case 'keep-online': {
1716
+ const ds = desktopSync.value;
1717
+ if (!ds || !targets[0]) break;
1718
+ try {
1719
+ await ds.unkeep(keepRemoteOf(targets[0]));
1720
+ } catch {
1721
+ // The shell owns the confirm dialog and reports its own failures.
1722
+ }
1723
+ await refreshKept();
1724
+ break;
1725
+ }
1726
+ case 'keep-reveal': {
1727
+ const ds = desktopSync.value;
1728
+ if (ds && targets[0]) void ds.reveal(keepRemoteOf(targets[0]));
1729
+ break;
1730
+ }
1634
1731
  case 'convert':
1635
1732
  if (targets[0]) openConvert(targets[0]);
1636
1733
  break;
package/src/locales/en.ts CHANGED
@@ -23,6 +23,12 @@ export const en: Record<string, string> = {
23
23
  'ctx.cut': 'Cut',
24
24
  'ctx.show_hidden': 'Show hidden files',
25
25
  'ctx.hide_hidden': 'Hide hidden files',
26
+ 'ctx.keep_local': 'Keep on this computer',
27
+ 'ctx.keep_online': 'Keep online only',
28
+ 'ctx.keep_reveal': 'Open local folder',
29
+ 'ctx.keep_inherited': 'Kept on this computer with its parent',
30
+ 'keep.started': 'This folder will be kept on this computer — sync started',
31
+ 'keep.failed': 'Could not keep on this computer',
26
32
  'ctx.paste': 'Paste',
27
33
  'ctx.info': 'Info',
28
34
  'ctx.duplicate': 'Duplicate',
package/src/locales/tr.ts CHANGED
@@ -23,6 +23,12 @@ export const tr: Record<string, string> = {
23
23
  'ctx.cut': 'Kes',
24
24
  'ctx.show_hidden': 'Gizli dosyaları göster',
25
25
  'ctx.hide_hidden': 'Gizli dosyaları gizle',
26
+ 'ctx.keep_local': 'Bilgisayarda tut',
27
+ 'ctx.keep_online': 'Yalnızca çevrimiçi tut',
28
+ 'ctx.keep_reveal': 'Yerel klasörü aç',
29
+ 'ctx.keep_inherited': 'Üst klasörle bilgisayarda tutuluyor',
30
+ 'keep.started': 'Klasör bilgisayarda tutulacak — eşitleme başladı',
31
+ 'keep.failed': 'Bilgisayarda tutulamadı',
26
32
  'ctx.paste': 'Yapıştır',
27
33
  'ctx.info': 'Bilgi',
28
34
  'ctx.duplicate': 'Kopyasını Oluştur',
@@ -308,6 +308,30 @@ export interface ExplorerConfig {
308
308
  driver?: string;
309
309
  readOnly?: boolean;
310
310
  }>;
311
+
312
+ /**
313
+ * Desktop-shell hook — selective sync ("keep on this computer").
314
+ *
315
+ * Present only when the explorer runs inside the filex desktop app; the
316
+ * shell passes functions that talk to its sync engine, and the explorer
317
+ * grows "Keep on this computer" / "Online only" entries on folder menus.
318
+ * Absent (web admin, embeds): nothing about it renders.
319
+ *
320
+ * Folders only, by design: the sync engine pairs directories, and a file
321
+ * rides along with the folder that holds it.
322
+ */
323
+ desktopSync?: {
324
+ /** Kept folders for the mounted account, adapter-qualified remotes. */
325
+ kept: () => Promise<Array<{ remote: string; local: string }>>;
326
+ /** Start keeping a folder. Resolves once the pair is registered (or the
327
+ * user cancelled the shell's root-folder prompt — re-read `kept`). */
328
+ keep: (remote: string) => Promise<void>;
329
+ /** Stop keeping. The SHELL owns the "what happens to the local copy"
330
+ * question — it asks natively and may cancel; re-read `kept` after. */
331
+ unkeep: (remote: string) => Promise<void>;
332
+ /** Open the folder's local mirror in the OS file manager. */
333
+ reveal: (remote: string) => Promise<void>;
334
+ };
311
335
  }
312
336
 
313
337
  /** Component emits — the parent listens for these events. */