@brftech/filex-core 0.22.0 → 0.23.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/filex-core.js +6562 -6451
- package/dist/filex-core.js.map +1 -1
- package/dist/filex-core.umd.cjs +44 -44
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/index.d.ts +49 -27
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +125 -13
- package/src/components/GridView.vue +17 -0
- package/src/components/ListView.vue +18 -0
- package/src/components/SecondaryPane.vue +4 -0
- package/src/locales/en.ts +8 -1
- package/src/locales/tr.ts +8 -1
- package/src/styles/base.css +80 -0
- package/src/styles/variables.css +3 -0
- package/src/types/ExplorerConfig.ts +21 -3
package/src/FileExplorer.vue
CHANGED
|
@@ -1436,7 +1436,57 @@ async function refreshKept(): Promise<void> {
|
|
|
1436
1436
|
// Shell went away mid-call; a stale entry only mislabels a menu item.
|
|
1437
1437
|
}
|
|
1438
1438
|
}
|
|
1439
|
-
|
|
1439
|
+
|
|
1440
|
+
/** The folder the engine is working on RIGHT NOW (null between runs). Drives
|
|
1441
|
+
* the ⟳ row badges and the bottom progress strip. */
|
|
1442
|
+
const keepActive = ref<{
|
|
1443
|
+
remote: string;
|
|
1444
|
+
phase: 'inventory' | 'plan' | 'transfer' | 'settling';
|
|
1445
|
+
done: number;
|
|
1446
|
+
total: number;
|
|
1447
|
+
} | null>(null);
|
|
1448
|
+
|
|
1449
|
+
async function refreshKeepStatus(): Promise<void> {
|
|
1450
|
+
const ds = desktopSync.value;
|
|
1451
|
+
if (!ds?.status) return;
|
|
1452
|
+
try {
|
|
1453
|
+
keepActive.value = (await ds.status()).active ?? null;
|
|
1454
|
+
} catch {
|
|
1455
|
+
keepActive.value = null;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
// The shell pokes on every engine output line — during a transfer that can be
|
|
1460
|
+
// several a second, and each refresh is an IPC round-trip. Trailing-edge
|
|
1461
|
+
// throttle: at most one refresh per 300ms, and the FINAL poke always lands,
|
|
1462
|
+
// so the strip cannot get stuck showing a finished transfer.
|
|
1463
|
+
let keepPokeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
1464
|
+
// The shell holds the callback and only drops it on the NEXT mount, so a poke
|
|
1465
|
+
// can arrive after this instance is gone — pokes then set refs nothing reads.
|
|
1466
|
+
// Cheap to make explicit rather than rely on that being harmless.
|
|
1467
|
+
let keepAlive = true;
|
|
1468
|
+
function onKeepPoke(): void {
|
|
1469
|
+
if (keepPokeTimer || !keepAlive) return;
|
|
1470
|
+
keepPokeTimer = setTimeout(() => {
|
|
1471
|
+
keepPokeTimer = null;
|
|
1472
|
+
if (!keepAlive) return;
|
|
1473
|
+
void refreshKeepStatus();
|
|
1474
|
+
void refreshKept();
|
|
1475
|
+
}, 300);
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
onMounted(() => {
|
|
1479
|
+
void refreshKept();
|
|
1480
|
+
void refreshKeepStatus();
|
|
1481
|
+
desktopSync.value?.onChange?.(onKeepPoke);
|
|
1482
|
+
});
|
|
1483
|
+
onBeforeUnmount(() => {
|
|
1484
|
+
keepAlive = false;
|
|
1485
|
+
if (keepPokeTimer) {
|
|
1486
|
+
clearTimeout(keepPokeTimer);
|
|
1487
|
+
keepPokeTimer = null;
|
|
1488
|
+
}
|
|
1489
|
+
});
|
|
1440
1490
|
|
|
1441
1491
|
/** Adapter-qualified remote for a row. Virtual storage rows carry a bare
|
|
1442
1492
|
* name (`docs`), real rows a wire path (`docs://reports`). */
|
|
@@ -1447,26 +1497,75 @@ function keepRemoteOf(node: FileNode): string {
|
|
|
1447
1497
|
|
|
1448
1498
|
type KeepState = 'none' | 'kept' | 'inherited' | 'partial';
|
|
1449
1499
|
|
|
1500
|
+
/** True when `child` lives strictly inside `parent` (both wire-form). */
|
|
1501
|
+
function remoteInside(child: string, parent: string): boolean {
|
|
1502
|
+
if (parent.endsWith('://')) return child.startsWith(parent) && child !== parent;
|
|
1503
|
+
return child === parent ? false : child.startsWith(parent + '/');
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1450
1506
|
/** How `remote` relates to the kept set: exactly a pair, inside one
|
|
1451
1507
|
* (inherited), an ancestor of some (partial), or unrelated. */
|
|
1452
1508
|
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
1509
|
if (keptPairs.value.some((p) => p.remote === remote)) return 'kept';
|
|
1460
|
-
if (keptPairs.value.some((p) =>
|
|
1461
|
-
if (keptPairs.value.some((p) =>
|
|
1510
|
+
if (keptPairs.value.some((p) => remoteInside(remote, p.remote))) return 'inherited';
|
|
1511
|
+
if (keptPairs.value.some((p) => remoteInside(p.remote, remote))) return 'partial';
|
|
1462
1512
|
return 'none';
|
|
1463
1513
|
}
|
|
1464
1514
|
|
|
1465
|
-
|
|
1466
|
-
|
|
1515
|
+
type KeepBadge = 'kept' | 'syncing' | 'cloud' | 'partial';
|
|
1516
|
+
|
|
1517
|
+
/**
|
|
1518
|
+
* The availability badge for one row: on this computer, being synced right
|
|
1519
|
+
* now, holding kept items somewhere below (partial), or online-only. Every
|
|
1520
|
+
* row gets one — that is the OneDrive/Drive grammar people already read —
|
|
1521
|
+
* except the rows where it would be a lie or noise: trash, and the `.trash`
|
|
1522
|
+
* row itself. `partial` is what saves the user from drilling into every
|
|
1523
|
+
* folder to find out whether anything inside is on this computer.
|
|
1524
|
+
*/
|
|
1525
|
+
function keepBadgeFor(n: FileNode): KeepBadge | null {
|
|
1526
|
+
if (!desktopSync.value || trashActive.value) return null;
|
|
1527
|
+
if (n.basename === '.trash') return null;
|
|
1528
|
+
const r = keepRemoteOf(n);
|
|
1529
|
+
const act = keepActive.value;
|
|
1530
|
+
if (act && (r === act.remote || remoteInside(r, act.remote) || remoteInside(act.remote, r))) {
|
|
1531
|
+
return 'syncing';
|
|
1532
|
+
}
|
|
1533
|
+
const st = keepStateOf(r);
|
|
1534
|
+
if (st === 'kept' || st === 'inherited') return 'kept';
|
|
1535
|
+
if (st === 'partial') return 'partial';
|
|
1536
|
+
return 'cloud';
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
/** Bottom strip: what to say while the engine works. */
|
|
1540
|
+
const keepStripLabel = computed<string>(() => {
|
|
1541
|
+
const act = keepActive.value;
|
|
1542
|
+
if (!act) return '';
|
|
1543
|
+
const name = act.remote.endsWith('://')
|
|
1544
|
+
? act.remote.slice(0, -'://'.length)
|
|
1545
|
+
: act.remote.slice(act.remote.lastIndexOf('/') + 1);
|
|
1546
|
+
if (act.phase === 'transfer' && act.total > 0) {
|
|
1547
|
+
return t('keep.strip_transfer', {
|
|
1548
|
+
name,
|
|
1549
|
+
done: String(act.done),
|
|
1550
|
+
total: String(act.total),
|
|
1551
|
+
pct: String(Math.min(100, Math.round((act.done * 100) / act.total))),
|
|
1552
|
+
});
|
|
1553
|
+
}
|
|
1554
|
+
if (act.phase === 'settling') return t('keep.strip_settling', { name });
|
|
1555
|
+
return t('keep.strip_inventory', { name });
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1558
|
+
const keepStripPercent = computed<number | null>(() => {
|
|
1559
|
+
const act = keepActive.value;
|
|
1560
|
+
if (!act || act.phase !== 'transfer' || act.total <= 0) return null;
|
|
1561
|
+
return Math.min(100, Math.round((act.done * 100) / act.total));
|
|
1562
|
+
});
|
|
1563
|
+
|
|
1564
|
+
/** Menu entries for one selected folder OR file, by its keep state. Empty
|
|
1565
|
+
* for multi-selections, trash, encrypted folders, or a web mount. */
|
|
1467
1566
|
function keepActionsFor(sel: FileNode[]): ContextAction[] {
|
|
1468
1567
|
const ds = desktopSync.value;
|
|
1469
|
-
const single = sel.length === 1 && sel[0]?.type === 'dir';
|
|
1568
|
+
const single = sel.length === 1 && (sel[0]?.type === 'dir' || sel[0]?.type === 'file');
|
|
1470
1569
|
if (!ds || !single || trashActive.value || e2eActive.value || sel[0]?.e2e === true) return [];
|
|
1471
1570
|
const st = keepStateOf(keepRemoteOf(sel[0]!));
|
|
1472
1571
|
return [
|
|
@@ -1701,7 +1800,7 @@ async function dispatchItemAction(key: string, targets: FileNode[]) {
|
|
|
1701
1800
|
if (!ds || !targets[0]) break;
|
|
1702
1801
|
const remote = keepRemoteOf(targets[0]);
|
|
1703
1802
|
try {
|
|
1704
|
-
await ds.keep(remote);
|
|
1803
|
+
await ds.keep(remote, targets[0].type === 'file' ? 'file' : 'dir');
|
|
1705
1804
|
await refreshKept();
|
|
1706
1805
|
// The shell may have shown its root-folder prompt and been cancelled —
|
|
1707
1806
|
// only claim success when the pair is really there now.
|
|
@@ -3514,6 +3613,7 @@ async function submitEncryptedFolder(payload: { name: string; password: string }
|
|
|
3514
3613
|
:show-parent-path="!!searchQuery"
|
|
3515
3614
|
:locale="locale"
|
|
3516
3615
|
:loading="loading"
|
|
3616
|
+
:keep-badge-for="desktopSync ? keepBadgeFor : undefined"
|
|
3517
3617
|
:starred-ids="starredIds"
|
|
3518
3618
|
:api-base="props.config.apiBase ?? ''"
|
|
3519
3619
|
:auth-headers="() => buildAuthHeaders()"
|
|
@@ -3533,6 +3633,7 @@ async function submitEncryptedFolder(payload: { name: string; password: string }
|
|
|
3533
3633
|
:show-parent-path="!!searchQuery"
|
|
3534
3634
|
:locale="locale"
|
|
3535
3635
|
:loading="loading"
|
|
3636
|
+
:keep-badge-for="desktopSync ? keepBadgeFor : undefined"
|
|
3536
3637
|
:thumb-src="thumbs.src"
|
|
3537
3638
|
@click-card="(n, m) => selection.click(n.path, m)"
|
|
3538
3639
|
@dbl-card="openNode"
|
|
@@ -3564,6 +3665,7 @@ async function submitEncryptedFolder(payload: { name: string; password: string }
|
|
|
3564
3665
|
:key tab kimliğine bağlı — tab geçişinde pane kendi konumuyla temiz
|
|
3565
3666
|
remount olur. -->
|
|
3566
3667
|
<SecondaryPane
|
|
3668
|
+
:keep-badge-for="desktopSync ? keepBadgeFor : undefined"
|
|
3567
3669
|
v-if="splitVisible && activeSplit"
|
|
3568
3670
|
ref="splitPaneRef"
|
|
3569
3671
|
:key="'split-' + tabsActiveId"
|
|
@@ -3669,6 +3771,16 @@ async function submitEncryptedFolder(payload: { name: string; password: string }
|
|
|
3669
3771
|
</svg>
|
|
3670
3772
|
</button>
|
|
3671
3773
|
|
|
3774
|
+
<!-- selective-sync progress: the folder the engine is moving RIGHT NOW.
|
|
3775
|
+
Overlay, pointer-events none — status is never in the way of work. -->
|
|
3776
|
+
<div v-if="keepActive" class="fe-keep-strip" role="status" aria-live="polite">
|
|
3777
|
+
<span class="fe-keep-strip__icon" aria-hidden="true">⟳</span>
|
|
3778
|
+
<span class="fe-keep-strip__label">{{ keepStripLabel }}</span>
|
|
3779
|
+
<div v-if="keepStripPercent !== null" class="fe-keep-strip__bar" aria-hidden="true">
|
|
3780
|
+
<div class="fe-keep-strip__fill" :style="{ width: keepStripPercent + '%' }"></div>
|
|
3781
|
+
</div>
|
|
3782
|
+
</div>
|
|
3783
|
+
|
|
3672
3784
|
<ContextMenu
|
|
3673
3785
|
ref="ctxRef"
|
|
3674
3786
|
:locale="locale"
|
|
@@ -21,6 +21,9 @@ const props = defineProps<{
|
|
|
21
21
|
* root-relative and unauthenticated — a bare <img src> only works for the
|
|
22
22
|
* native same-origin SPA, so embedded hosts NEED this. null = icon. */
|
|
23
23
|
thumbSrc?: (n: FileNode) => string | null;
|
|
24
|
+
/** Desktop selective sync: availability badge per tile. Absent on the
|
|
25
|
+
* web — no badge renders at all. */
|
|
26
|
+
keepBadgeFor?: (n: FileNode) => 'kept' | 'syncing' | 'cloud' | 'partial' | null;
|
|
24
27
|
}>();
|
|
25
28
|
|
|
26
29
|
const emit = defineEmits<{
|
|
@@ -123,6 +126,13 @@ function parentDir(path: string): string {
|
|
|
123
126
|
|
|
124
127
|
// Special rows keep their emoji (trash/storage are not file-TYPE icons);
|
|
125
128
|
// everything else renders the SVG icon set from lib/fileIcons.
|
|
129
|
+
function keepGlyph(b: 'kept' | 'syncing' | 'cloud' | 'partial'): string {
|
|
130
|
+
if (b === 'kept') return '\u2713';
|
|
131
|
+
if (b === 'syncing') return '\u27f3';
|
|
132
|
+
if (b === 'partial') return '\u25d0';
|
|
133
|
+
return '\u2601';
|
|
134
|
+
}
|
|
135
|
+
|
|
126
136
|
function specialEmojiFor(n: FileNode): string | null {
|
|
127
137
|
if (n.basename === '.trash') return '🗑';
|
|
128
138
|
if (n.mime_type === 'inode/storage') return '💾';
|
|
@@ -205,6 +215,13 @@ function snippetTitle(snippet: string): string {
|
|
|
205
215
|
</div>
|
|
206
216
|
<div class="fe-grid__label" :title="n.basename">
|
|
207
217
|
{{ nodeDisplayName(n) }}
|
|
218
|
+
<span
|
|
219
|
+
v-if="keepBadgeFor && keepBadgeFor(n)"
|
|
220
|
+
:class="['fe-keepbadge', 'fe-keepbadge--' + keepBadgeFor(n)]"
|
|
221
|
+
:title="t('keep.badge_' + keepBadgeFor(n))"
|
|
222
|
+
role="img"
|
|
223
|
+
:aria-label="t('keep.badge_' + keepBadgeFor(n))"
|
|
224
|
+
>{{ keepGlyph(keepBadgeFor(n)!) }}</span>
|
|
208
225
|
</div>
|
|
209
226
|
<div
|
|
210
227
|
v-if="showParentPath"
|
|
@@ -39,6 +39,10 @@ const props = defineProps<{
|
|
|
39
39
|
apiBase?: string;
|
|
40
40
|
authHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
|
|
41
41
|
authCredentials?: RequestCredentials;
|
|
42
|
+
/** Desktop selective sync: availability badge per row (kept on this
|
|
43
|
+
* computer / syncing right now / online only). Absent on the web —
|
|
44
|
+
* no badge renders at all. */
|
|
45
|
+
keepBadgeFor?: (n: FileNode) => 'kept' | 'syncing' | 'cloud' | 'partial' | null;
|
|
42
46
|
}>();
|
|
43
47
|
|
|
44
48
|
const emit = defineEmits<{
|
|
@@ -144,6 +148,13 @@ function specialEmojiFor(n: FileNode): string | null {
|
|
|
144
148
|
return null;
|
|
145
149
|
}
|
|
146
150
|
|
|
151
|
+
function keepGlyph(b: 'kept' | 'syncing' | 'cloud' | 'partial'): string {
|
|
152
|
+
if (b === 'kept') return '\u2713';
|
|
153
|
+
if (b === 'syncing') return '\u27f3';
|
|
154
|
+
if (b === 'partial') return '\u25d0';
|
|
155
|
+
return '\u2601';
|
|
156
|
+
}
|
|
157
|
+
|
|
147
158
|
function isPinnedSpecial(n: FileNode): boolean {
|
|
148
159
|
return n.basename === '.trash' || n.mime_type === 'inode/storage';
|
|
149
160
|
}
|
|
@@ -397,6 +408,13 @@ const segments = computed<Segment[]>(() => {
|
|
|
397
408
|
{{ nodeDisplayName(n) }}
|
|
398
409
|
<!-- bul:s3 — content-match badge -->
|
|
399
410
|
<span v-if="rowInContent(n)" class="fe-list__badge">{{ t('search.in_content') }}</span>
|
|
411
|
+
<span
|
|
412
|
+
v-if="keepBadgeFor && keepBadgeFor(n)"
|
|
413
|
+
:class="['fe-keepbadge', 'fe-keepbadge--' + keepBadgeFor(n)]"
|
|
414
|
+
:title="t('keep.badge_' + keepBadgeFor(n))"
|
|
415
|
+
role="img"
|
|
416
|
+
:aria-label="t('keep.badge_' + keepBadgeFor(n))"
|
|
417
|
+
>{{ keepGlyph(keepBadgeFor(n)!) }}</span>
|
|
400
418
|
</span>
|
|
401
419
|
<span
|
|
402
420
|
v-if="showParentPath"
|
|
@@ -58,6 +58,8 @@ const props = defineProps<{
|
|
|
58
58
|
viewMode?: ViewMode;
|
|
59
59
|
/** ui-fix — authenticated thumb resolver, forwarded to grid/gallery. */
|
|
60
60
|
thumbSrc?: (n: FileNode) => string | null;
|
|
61
|
+
/** Desktop selective sync badge resolver, forwarded to the views. */
|
|
62
|
+
keepBadgeFor?: (n: FileNode) => 'kept' | 'syncing' | 'cloud' | 'partial' | null;
|
|
61
63
|
/** ui-fix — mirror the main panel's virtual `.trash` row at storage root
|
|
62
64
|
* so both split panes list identical rows (no row-offset). Defaults on. */
|
|
63
65
|
trashVisible?: boolean;
|
|
@@ -396,6 +398,7 @@ defineExpose({ reload, goUp, selectAll, openSelected, selectedNodes, getPath });
|
|
|
396
398
|
:selected="selected"
|
|
397
399
|
:locale="locale"
|
|
398
400
|
:loading="loading"
|
|
401
|
+
:keep-badge-for="keepBadgeFor"
|
|
399
402
|
@click-row="onViewClick"
|
|
400
403
|
@dbl-row="onRowDbl"
|
|
401
404
|
@context-row="onViewContext"
|
|
@@ -409,6 +412,7 @@ defineExpose({ reload, goUp, selectAll, openSelected, selectedNodes, getPath });
|
|
|
409
412
|
:locale="locale"
|
|
410
413
|
:loading="loading"
|
|
411
414
|
:thumb-src="thumbSrc"
|
|
415
|
+
:keep-badge-for="keepBadgeFor"
|
|
412
416
|
@click-card="onViewClick"
|
|
413
417
|
@dbl-card="onRowDbl"
|
|
414
418
|
@context-card="onViewContext"
|
package/src/locales/en.ts
CHANGED
|
@@ -27,8 +27,15 @@ export const en: Record<string, string> = {
|
|
|
27
27
|
'ctx.keep_online': 'Keep online only',
|
|
28
28
|
'ctx.keep_reveal': 'Open local folder',
|
|
29
29
|
'ctx.keep_inherited': 'Kept on this computer with its parent',
|
|
30
|
-
'keep.started': '
|
|
30
|
+
'keep.started': 'Will be kept on this computer — sync started',
|
|
31
31
|
'keep.failed': 'Could not keep on this computer',
|
|
32
|
+
'keep.badge_kept': 'On this computer',
|
|
33
|
+
'keep.badge_syncing': 'Syncing…',
|
|
34
|
+
'keep.badge_cloud': 'Online only',
|
|
35
|
+
'keep.badge_partial': 'Contains items kept on this computer',
|
|
36
|
+
'keep.strip_transfer': 'Syncing {name} — {done}/{total} ({pct}%)',
|
|
37
|
+
'keep.strip_inventory': 'Syncing {name} — scanning the server…',
|
|
38
|
+
'keep.strip_settling': 'Syncing {name} — settling…',
|
|
32
39
|
'ctx.paste': 'Paste',
|
|
33
40
|
'ctx.info': 'Info',
|
|
34
41
|
'ctx.duplicate': 'Duplicate',
|
package/src/locales/tr.ts
CHANGED
|
@@ -27,8 +27,15 @@ export const tr: Record<string, string> = {
|
|
|
27
27
|
'ctx.keep_online': 'Yalnızca çevrimiçi tut',
|
|
28
28
|
'ctx.keep_reveal': 'Yerel klasörü aç',
|
|
29
29
|
'ctx.keep_inherited': 'Üst klasörle bilgisayarda tutuluyor',
|
|
30
|
-
'keep.started': '
|
|
30
|
+
'keep.started': 'Bilgisayarda tutulacak — eşitleme başladı',
|
|
31
31
|
'keep.failed': 'Bilgisayarda tutulamadı',
|
|
32
|
+
'keep.badge_kept': 'Bilgisayarda',
|
|
33
|
+
'keep.badge_syncing': 'Eşitleniyor…',
|
|
34
|
+
'keep.badge_cloud': 'Yalnızca çevrimiçi',
|
|
35
|
+
'keep.badge_partial': 'İçinde bilgisayarda tutulanlar var',
|
|
36
|
+
'keep.strip_transfer': '{name} eşitleniyor — {done}/{total} (%{pct})',
|
|
37
|
+
'keep.strip_inventory': '{name} eşitleniyor — sunucu taranıyor…',
|
|
38
|
+
'keep.strip_settling': '{name} eşitleniyor — son durum kaydediliyor…',
|
|
32
39
|
'ctx.paste': 'Yapıştır',
|
|
33
40
|
'ctx.info': 'Bilgi',
|
|
34
41
|
'ctx.duplicate': 'Kopyasını Oluştur',
|
package/src/styles/base.css
CHANGED
|
@@ -3928,3 +3928,83 @@ filex-explorer {
|
|
|
3928
3928
|
}
|
|
3929
3929
|
}
|
|
3930
3930
|
/* === /ui-fix ========================================================= */
|
|
3931
|
+
|
|
3932
|
+
/* ── selective sync (desktop shell only): availability badges + strip ──
|
|
3933
|
+
The glyphs read in the OneDrive/Drive grammar: ✓ on this computer,
|
|
3934
|
+
⟳ being synced right now, ☁ online-only. Rendered only when the host
|
|
3935
|
+
passes keepBadgeFor — the web admin and the embeds never show them. */
|
|
3936
|
+
.fe-keepbadge {
|
|
3937
|
+
flex: 0 0 auto;
|
|
3938
|
+
display: inline-block;
|
|
3939
|
+
margin-left: 4px;
|
|
3940
|
+
font-size: 11px;
|
|
3941
|
+
line-height: 1;
|
|
3942
|
+
vertical-align: middle;
|
|
3943
|
+
}
|
|
3944
|
+
.fe-keepbadge--kept {
|
|
3945
|
+
color: var(--fe-keep-ok, #16a34a);
|
|
3946
|
+
}
|
|
3947
|
+
.fe-keepbadge--cloud {
|
|
3948
|
+
color: var(--fe-text-muted);
|
|
3949
|
+
opacity: 0.7;
|
|
3950
|
+
}
|
|
3951
|
+
.fe-keepbadge--partial {
|
|
3952
|
+
color: var(--fe-keep-ok, #16a34a);
|
|
3953
|
+
opacity: 0.85;
|
|
3954
|
+
}
|
|
3955
|
+
.fe-keepbadge--syncing {
|
|
3956
|
+
color: var(--fe-primary);
|
|
3957
|
+
animation: fe-keep-spin 1.2s linear infinite;
|
|
3958
|
+
}
|
|
3959
|
+
@keyframes fe-keep-spin {
|
|
3960
|
+
to {
|
|
3961
|
+
transform: rotate(360deg);
|
|
3962
|
+
}
|
|
3963
|
+
}
|
|
3964
|
+
|
|
3965
|
+
/* Bottom overlay strip: progress of the folder being synced right now.
|
|
3966
|
+
pointer-events none — it reports, it never blocks a click. */
|
|
3967
|
+
.fe-keep-strip {
|
|
3968
|
+
position: absolute;
|
|
3969
|
+
left: 12px;
|
|
3970
|
+
right: 12px;
|
|
3971
|
+
bottom: 10px;
|
|
3972
|
+
z-index: 30;
|
|
3973
|
+
display: flex;
|
|
3974
|
+
align-items: center;
|
|
3975
|
+
gap: 8px;
|
|
3976
|
+
padding: 6px 12px;
|
|
3977
|
+
border: 1px solid var(--fe-border);
|
|
3978
|
+
border-radius: 10px;
|
|
3979
|
+
background: var(--fe-bg-elev);
|
|
3980
|
+
box-shadow: var(--fe-shadow-sm, 0 2px 6px rgba(0, 0, 0, 0.18));
|
|
3981
|
+
font-size: 12.5px;
|
|
3982
|
+
color: var(--fe-text-muted);
|
|
3983
|
+
pointer-events: none;
|
|
3984
|
+
}
|
|
3985
|
+
.fe-keep-strip__icon {
|
|
3986
|
+
font-size: 13px;
|
|
3987
|
+
line-height: 1;
|
|
3988
|
+
color: var(--fe-primary);
|
|
3989
|
+
animation: fe-keep-spin 1.2s linear infinite;
|
|
3990
|
+
}
|
|
3991
|
+
.fe-keep-strip__label {
|
|
3992
|
+
flex: 0 1 auto;
|
|
3993
|
+
overflow: hidden;
|
|
3994
|
+
text-overflow: ellipsis;
|
|
3995
|
+
white-space: nowrap;
|
|
3996
|
+
}
|
|
3997
|
+
.fe-keep-strip__bar {
|
|
3998
|
+
flex: 1 1 80px;
|
|
3999
|
+
min-width: 60px;
|
|
4000
|
+
height: 4px;
|
|
4001
|
+
border-radius: 999px;
|
|
4002
|
+
background: color-mix(in srgb, var(--fe-primary) 18%, transparent);
|
|
4003
|
+
overflow: hidden;
|
|
4004
|
+
}
|
|
4005
|
+
.fe-keep-strip__fill {
|
|
4006
|
+
height: 100%;
|
|
4007
|
+
border-radius: 999px;
|
|
4008
|
+
background: var(--fe-primary);
|
|
4009
|
+
transition: width 0.3s ease;
|
|
4010
|
+
}
|
package/src/styles/variables.css
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
--fe-primary-hover: #2563eb;
|
|
18
18
|
--fe-danger: #dc2626;
|
|
19
19
|
--fe-danger-hover: #b91c1c;
|
|
20
|
+
--fe-keep-ok: #16a34a; /* selective-sync "on this computer" check */
|
|
20
21
|
--fe-shadow: 0 10px 32px rgba(15, 23, 42, 0.14);
|
|
21
22
|
--fe-shadow-sm: 0 2px 6px rgba(15, 23, 42, 0.08);
|
|
22
23
|
--fe-radius: 8px;
|
|
@@ -65,6 +66,7 @@
|
|
|
65
66
|
--fe-primary-hover: #3b82f6;
|
|
66
67
|
--fe-danger: #f87171;
|
|
67
68
|
--fe-danger-hover: #ef4444;
|
|
69
|
+
--fe-keep-ok: #4ade80;
|
|
68
70
|
--fe-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
|
|
69
71
|
--fe-shadow-sm: 0 2px 6px rgba(0, 0, 0, 0.25);
|
|
70
72
|
}
|
|
@@ -85,6 +87,7 @@
|
|
|
85
87
|
--fe-primary-hover: #3b82f6;
|
|
86
88
|
--fe-danger: #f87171;
|
|
87
89
|
--fe-danger-hover: #ef4444;
|
|
90
|
+
--fe-keep-ok: #4ade80;
|
|
88
91
|
--fe-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
|
|
89
92
|
--fe-shadow-sm: 0 2px 6px rgba(0, 0, 0, 0.25);
|
|
90
93
|
}
|
|
@@ -323,14 +323,32 @@ export interface ExplorerConfig {
|
|
|
323
323
|
desktopSync?: {
|
|
324
324
|
/** Kept folders for the mounted account, adapter-qualified remotes. */
|
|
325
325
|
kept: () => Promise<Array<{ remote: string; local: string }>>;
|
|
326
|
-
/** Start keeping a folder
|
|
327
|
-
*
|
|
328
|
-
|
|
326
|
+
/** Start keeping a folder — or, with kind 'file', a single file.
|
|
327
|
+
* Resolves once the pair is registered (or the user cancelled the
|
|
328
|
+
* shell's root-folder prompt — re-read `kept`). */
|
|
329
|
+
keep: (remote: string, kind: 'dir' | 'file') => Promise<void>;
|
|
329
330
|
/** Stop keeping. The SHELL owns the "what happens to the local copy"
|
|
330
331
|
* question — it asks natively and may cancel; re-read `kept` after. */
|
|
331
332
|
unkeep: (remote: string) => Promise<void>;
|
|
332
333
|
/** Open the folder's local mirror in the OS file manager. */
|
|
333
334
|
reveal: (remote: string) => Promise<void>;
|
|
335
|
+
/** Live engine state, for the row badges and the bottom progress strip.
|
|
336
|
+
* `active` is the folder being worked on right now — the engine walks
|
|
337
|
+
* its pairs one at a time — or null between runs. */
|
|
338
|
+
status?: () => Promise<{
|
|
339
|
+
running: boolean;
|
|
340
|
+
lastError?: string | null;
|
|
341
|
+
active: {
|
|
342
|
+
remote: string;
|
|
343
|
+
phase: 'inventory' | 'plan' | 'transfer' | 'settling';
|
|
344
|
+
done: number;
|
|
345
|
+
total: number;
|
|
346
|
+
} | null;
|
|
347
|
+
}>;
|
|
348
|
+
/** Subscribe to "something about sync changed" pokes from the shell —
|
|
349
|
+
* the explorer re-reads `kept` and `status` when poked. The shell keeps
|
|
350
|
+
* ONE subscriber (the mounted explorer) and overwrites it on remount. */
|
|
351
|
+
onChange?: (cb: () => void) => void;
|
|
334
352
|
};
|
|
335
353
|
}
|
|
336
354
|
|