@brftech/filex-core 0.31.0 → 0.32.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/README.md +26 -2
- package/dist/filex-core.js +9289 -8103
- package/dist/filex-core.js.map +1 -1
- package/dist/filex-core.umd.cjs +49 -49
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/index.d.ts +186 -8
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +510 -12
- package/src/components/CommandPalette.vue +18 -2
- package/src/components/E2eRecoveryUnlockModal.vue +29 -4
- package/src/components/FilterBar.vue +244 -0
- package/src/components/GalleryView.vue +11 -0
- package/src/components/GridView.vue +46 -3
- package/src/components/InspectorPanel.vue +231 -8
- package/src/components/ListView.vue +10 -1
- package/src/components/SecondaryPane.vue +15 -1
- package/src/components/SideNav.vue +161 -1
- package/src/components/Toolbar.vue +235 -49
- package/src/components/ViewSwitcher.vue +81 -0
- package/src/composables/useFileApi.ts +45 -0
- package/src/index.ts +6 -0
- package/src/lib/e2ecrypto.ts +191 -3
- package/src/lib/fileFilters.ts +143 -0
- package/src/lib/listing.ts +47 -0
- package/src/locales/en.ts +83 -0
- package/src/locales/tr.ts +83 -0
- package/src/modals/PermissionsModal.vue +18 -2
- package/src/styles/base.css +491 -0
- package/src/types/ExplorerConfig.ts +17 -1
package/src/FileExplorer.vue
CHANGED
|
@@ -46,6 +46,14 @@ import RecentlyOpened from './components/RecentlyOpened.vue';
|
|
|
46
46
|
import Breadcrumb from './components/Breadcrumb.vue';
|
|
47
47
|
import ListView from './components/ListView.vue';
|
|
48
48
|
import GridView from './components/GridView.vue';
|
|
49
|
+
import FilterBar from './components/FilterBar.vue' /* surucu:d1 */;
|
|
50
|
+
import ViewSwitcher from './components/ViewSwitcher.vue' /* surucu:d1 */;
|
|
51
|
+
import {
|
|
52
|
+
EMPTY_FILTERS,
|
|
53
|
+
applyFilters,
|
|
54
|
+
filtersActive,
|
|
55
|
+
type DriveFilters,
|
|
56
|
+
} from './lib/fileFilters' /* surucu:d1 */;
|
|
49
57
|
import GalleryView from './components/GalleryView.vue'; /* wiring:d2 */
|
|
50
58
|
import ContextMenu, { type ContextAction } from './components/ContextMenu.vue';
|
|
51
59
|
import UploadProgress from './components/UploadProgress.vue';
|
|
@@ -92,13 +100,16 @@ import {
|
|
|
92
100
|
createKeyRing,
|
|
93
101
|
createEncryptedFolder,
|
|
94
102
|
upgradeMarkerV1,
|
|
103
|
+
addEscrowSlot,
|
|
104
|
+
declineEscrowSlot,
|
|
105
|
+
escrowOfferState,
|
|
95
106
|
parseMarker,
|
|
96
107
|
unlockWithPassword,
|
|
97
108
|
unlockWithRecoveryKey,
|
|
98
109
|
unlockWithEscrowKey,
|
|
99
110
|
importEscrowPrivateKey,
|
|
100
111
|
markerHasRecovery,
|
|
101
|
-
|
|
112
|
+
escrowAvailability,
|
|
102
113
|
bytesToB64,
|
|
103
114
|
b64ToBytes,
|
|
104
115
|
encryptFile,
|
|
@@ -496,8 +507,16 @@ function onStarChange(n: FileNode, value: boolean) {
|
|
|
496
507
|
* this calls it. A menu cannot render a component, but it must not grow its
|
|
497
508
|
* own fetch either — that is the second path that drifts.
|
|
498
509
|
*/
|
|
499
|
-
/** Which of `targets` can carry a star: files the server knows by id.
|
|
510
|
+
/** Which of `targets` can carry a star: files the server knows by id.
|
|
511
|
+
*
|
|
512
|
+
* Empty when the identity surfaces are suppressed, which is the one
|
|
513
|
+
* chokepoint the context menu, the toolbar and the keyboard action all go
|
|
514
|
+
* through — so the affordance disappears everywhere at once rather than in
|
|
515
|
+
* the two places somebody remembered. Offering to star a file while the
|
|
516
|
+
* Starred view is hidden would write into a list the person cannot open,
|
|
517
|
+
* and under a shared app token that list belongs to everyone at once. */
|
|
500
518
|
function starableNodes(targets: FileNode[]): FileNode[] {
|
|
519
|
+
if (!identitySurfaces.value) return [];
|
|
501
520
|
return targets.filter((n) => typeof n.id === 'number' && n.type === 'file');
|
|
502
521
|
}
|
|
503
522
|
|
|
@@ -700,7 +719,17 @@ function closeInspector() {
|
|
|
700
719
|
* the "one behaviour on one surface" split this shared package exists to
|
|
701
720
|
* prevent. */
|
|
702
721
|
const uiProfile = computed(() => props.config.uiProfile ?? 'standard');
|
|
703
|
-
|
|
722
|
+
/**
|
|
723
|
+
* ⚠ `drive` answers TRUE here. It is a superset of `simple`, so every question
|
|
724
|
+
* `simple` already answers ("one pane?", "no tab strip?", "list and grid
|
|
725
|
+
* only?") must keep the same answer under it — asking `=== 'simple'` in those
|
|
726
|
+
* places is how the drive profile would silently grow a split pane the day
|
|
727
|
+
* somebody adds a fourth condition. `driveShell` is only for what `drive` adds
|
|
728
|
+
* on TOP.
|
|
729
|
+
*/
|
|
730
|
+
const simpleUi = computed(() => uiProfile.value === 'simple' || uiProfile.value === 'drive');
|
|
731
|
+
/* === surucu:d1 — the Drive shell (GitHub #14, the reporter's mockups) ===== */
|
|
732
|
+
const driveShell = computed(() => uiProfile.value === 'drive');
|
|
704
733
|
|
|
705
734
|
const SIDENAV_LS_KEY = 'filex.sidenav';
|
|
706
735
|
const sideNavExpanded = ref<boolean>(
|
|
@@ -740,6 +769,31 @@ const sideNavEnabled = computed(() => props.config.sideNav ?? !rootPathProp);
|
|
|
740
769
|
const navVisible = computed(
|
|
741
770
|
() => sideNavEnabled.value && (isNarrow.value ? navDrawerOpen.value : true),
|
|
742
771
|
);
|
|
772
|
+
/**
|
|
773
|
+
* Is the navigation panel already offering Trash as a destination?
|
|
774
|
+
*
|
|
775
|
+
* ⚠ Keyed to `sideNavEnabled`, NOT to `navVisible`. Three cases decided here,
|
|
776
|
+
* and each one is a judgement about whether the person has a way to the bin
|
|
777
|
+
* that is not this row:
|
|
778
|
+
*
|
|
779
|
+
* - collapsed to the icon rail → OFFERING. The entry is still rendered, still
|
|
780
|
+
* one click, still labelled for a screen reader; only its text is gone.
|
|
781
|
+
* - the drawer under 560px, closed → OFFERING. The toolbar's panel toggle is
|
|
782
|
+
* on screen at every width, so the destination is one tap away. Keying this
|
|
783
|
+
* to `navVisible` instead would add and remove a row from the LISTING every
|
|
784
|
+
* time somebody opened or closed the drawer — the folder changing under
|
|
785
|
+
* them for a reason that has nothing to do with the folder.
|
|
786
|
+
* - no panel at all (`sideNav: false`, or the default under `rootPath`) → NOT
|
|
787
|
+
* offering, and the row stays. That is the case it was invented for: a
|
|
788
|
+
* listing with no other door to the bin.
|
|
789
|
+
*
|
|
790
|
+
* `trashVisible: false` is handled inside the helper and outranks all of it —
|
|
791
|
+
* it means no Trash anywhere, panel entry included.
|
|
792
|
+
*/
|
|
793
|
+
const navOffersTrash = computed(
|
|
794
|
+
() => sideNavEnabled.value && props.config.trashVisible !== false,
|
|
795
|
+
);
|
|
796
|
+
|
|
743
797
|
/** What the toolbar toggle reports as pressed. */
|
|
744
798
|
const navToggleOn = computed(() =>
|
|
745
799
|
isNarrow.value ? navDrawerOpen.value : sideNavExpanded.value,
|
|
@@ -840,6 +894,130 @@ watchEffect(() => {
|
|
|
840
894
|
if (simpleUi.value && viewMode.value === 'gallery') viewMode.value = 'grid';
|
|
841
895
|
});
|
|
842
896
|
|
|
897
|
+
/* === surucu:d1 — the filter row =========================================
|
|
898
|
+
* Three chips over the listing in hand: Type · Modified · Size.
|
|
899
|
+
*
|
|
900
|
+
* ⚠ CLIENT-SIDE, and that is the honest place for them, not a shortcut. The
|
|
901
|
+
* listing endpoint reads exactly six parameters (`action`, `path`, `filter`,
|
|
902
|
+
* `storage`, `parent`, `cache`) and has no `limit`/`offset` either — so the
|
|
903
|
+
* rows in hand ARE the folder, and filtering them here answers the whole
|
|
904
|
+
* question rather than "the first page of it". Wiring a chip to a `min_size`
|
|
905
|
+
* the server never reads would look identical and change nothing.
|
|
906
|
+
*
|
|
907
|
+
* ⚠ Reset on navigation. A filter that survives a folder change makes the next
|
|
908
|
+
* folder look empty, and the reason is off-screen the moment you scroll.
|
|
909
|
+
*/
|
|
910
|
+
const driveFilters = ref<DriveFilters>({ ...EMPTY_FILTERS });
|
|
911
|
+
const filtersOn = computed(() => driveShell.value && filtersActive(driveFilters.value));
|
|
912
|
+
/** What the views render. Identical reference to `files` when nothing is set. */
|
|
913
|
+
const displayFiles = computed<FileNode[]>(() =>
|
|
914
|
+
driveShell.value ? applyFilters(files.value, driveFilters.value) : files.value,
|
|
915
|
+
);
|
|
916
|
+
function clearDriveFilters() {
|
|
917
|
+
setDriveFilters({ ...EMPTY_FILTERS });
|
|
918
|
+
}
|
|
919
|
+
function setDriveFilters(v: DriveFilters) {
|
|
920
|
+
driveFilters.value = v;
|
|
921
|
+
// A selection the filter just hid would still be what Delete acts on, with
|
|
922
|
+
// nothing on screen to say so.
|
|
923
|
+
selection.clear();
|
|
924
|
+
}
|
|
925
|
+
watch(
|
|
926
|
+
() => `${currentPath.value}|${navView.value}`,
|
|
927
|
+
() => {
|
|
928
|
+
if (filtersActive(driveFilters.value)) driveFilters.value = { ...EMPTY_FILTERS };
|
|
929
|
+
},
|
|
930
|
+
);
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* surucu:d1 — what the header field says it will search: the folder you are
|
|
934
|
+
* standing in, by name. At a storage root that is the storage; in a panel view
|
|
935
|
+
* ("Recent", a tag) it is that view's own name, because searching from there
|
|
936
|
+
* searches what is on screen.
|
|
937
|
+
*/
|
|
938
|
+
const driveScopeLabel = computed(() => {
|
|
939
|
+
if (navView.value === 'tag') return navTag.value;
|
|
940
|
+
if (navView.value) return t(`sidenav.${navView.value}`);
|
|
941
|
+
const rel = currentPath.value.replace(/\/+$/, '');
|
|
942
|
+
const last = rel.split('/').filter(Boolean).pop();
|
|
943
|
+
return last || adapter.value || '';
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* surucu:d1 — the ⌘K escalation. The header field searches THIS folder (it
|
|
948
|
+
* sets `searchQuery`, which the loader answers with `action=search`); this
|
|
949
|
+
* hands the same words to the command palette, which is where "everywhere",
|
|
950
|
+
* the saved searches and the commands live. One box, one shortcut, and the
|
|
951
|
+
* hint printed on the box does what it says.
|
|
952
|
+
*/
|
|
953
|
+
const paletteSeed = ref('');
|
|
954
|
+
function openPaletteWith(q: string) {
|
|
955
|
+
paletteSeed.value = q;
|
|
956
|
+
showPalette.value = true;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/**
|
|
960
|
+
* surucu:d1 — "Request files" from the New menu: the access modal on the
|
|
961
|
+
* CURRENT folder, opened on its file-drop tab. The modal already owns that
|
|
962
|
+
* surface; this only gives it a target, since the folder you are standing in
|
|
963
|
+
* is not a selected row and has no FileNode of its own.
|
|
964
|
+
*/
|
|
965
|
+
const permInitialTab = ref<'perms' | 'share' | 'drop' | undefined>(undefined);
|
|
966
|
+
function openFileRequest() {
|
|
967
|
+
const path = qualify(currentPath.value);
|
|
968
|
+
if (!path) return;
|
|
969
|
+
const segs = currentPath.value.split('/').filter(Boolean);
|
|
970
|
+
permTarget.value = {
|
|
971
|
+
path,
|
|
972
|
+
basename: segs.length ? segs[segs.length - 1] : adapter.value,
|
|
973
|
+
type: 'dir',
|
|
974
|
+
};
|
|
975
|
+
permInitialTab.value = 'drop';
|
|
976
|
+
showPerm.value = true;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/** surucu:d1 — a link minted from the details panel; same event the share
|
|
980
|
+
* dialog emits, so a host listening for `share-created` hears both. */
|
|
981
|
+
function onInspectorShareCreated(payload: { path: string; url: string }) {
|
|
982
|
+
emit('share-created', { path: payload.path, url: payload.url, pin: null });
|
|
983
|
+
flashToast(t('inspector.copied'));
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/* === surucu:d1 — the storage line under the navigation ==================
|
|
987
|
+
* Fetched once per mount, and ONLY in the drive shell: no other profile draws
|
|
988
|
+
* it, and an explorer that has drawn this panel for a year should not start
|
|
989
|
+
* making a request it has no use for. `quotaMe()` answers null for a server
|
|
990
|
+
* without the route or a caller without a person behind it, and null renders
|
|
991
|
+
* nothing at all.
|
|
992
|
+
*/
|
|
993
|
+
const quotaSnapshot = ref<{ used: number; total: number; unlimited: boolean } | null>(null);
|
|
994
|
+
async function loadQuota() {
|
|
995
|
+
if (!driveShell.value || !identitySurfaces.value) {
|
|
996
|
+
quotaSnapshot.value = null;
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
const q = await api.quotaMe();
|
|
1000
|
+
if (!q) {
|
|
1001
|
+
quotaSnapshot.value = null;
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
const unlimited = !!q.unlimited || q.quota_bytes <= 0;
|
|
1005
|
+
// ⚠ No ceiling AND nothing counted = nothing to say, so say nothing.
|
|
1006
|
+
//
|
|
1007
|
+
// Measured, not guessed: `used_bytes` is `SUM(nodes.size) WHERE owner_id = me`
|
|
1008
|
+
// and `nodes.owner_id` is set by the UPLOAD path only — it is nil for every
|
|
1009
|
+
// file a storage sync discovered (handlers/shared.go). On an install whose
|
|
1010
|
+
// drives were mounted rather than uploaded into, the honest figure is
|
|
1011
|
+
// therefore "0 B", and a line reading "0 B used" under a drive visibly full
|
|
1012
|
+
// of files reads as a broken widget, not as a fact about quota. With a quota
|
|
1013
|
+
// set the line still earns its place — the ceiling is real and uploads count
|
|
1014
|
+
// against it — so only the no-quota-no-usage case is dropped.
|
|
1015
|
+
quotaSnapshot.value =
|
|
1016
|
+
unlimited && q.used_bytes <= 0
|
|
1017
|
+
? null
|
|
1018
|
+
: { used: q.used_bytes, total: q.quota_bytes, unlimited };
|
|
1019
|
+
}
|
|
1020
|
+
|
|
843
1021
|
/**
|
|
844
1022
|
* nodeRowToFileNode — the starred / recently-opened endpoints answer with raw
|
|
845
1023
|
* node rows (relative `path`, numeric `storage_id`), not the listing shape.
|
|
@@ -1350,7 +1528,16 @@ async function load(path?: string) {
|
|
|
1350
1528
|
files.value = filterListing(resp.files);
|
|
1351
1529
|
// Inject virtual `.trash` entry at root only — shared helper so the
|
|
1352
1530
|
// split-view secondary pane shows the exact same row (no row-offset).
|
|
1353
|
-
|
|
1531
|
+
// ⚠ …and NOT into a search result. The search response's `dirname` is the
|
|
1532
|
+
// scope it searched, so it is the storage root exactly as an `index` of
|
|
1533
|
+
// that folder would be; only this call site knows which of the two it just
|
|
1534
|
+
// asked for.
|
|
1535
|
+
if (
|
|
1536
|
+
injectTrashRow(files.value, resp.adapter, resp.dirname, props.config.trashVisible !== false, {
|
|
1537
|
+
isSearchResult: !!searchQuery.value,
|
|
1538
|
+
navOffersTrash: navOffersTrash.value,
|
|
1539
|
+
})
|
|
1540
|
+
) {
|
|
1354
1541
|
void hydrateTrashRowShared(files.value, resp.adapter, api);
|
|
1355
1542
|
}
|
|
1356
1543
|
// currentPath is the user-facing form: `s3-test/example` in
|
|
@@ -1655,6 +1842,9 @@ onMounted(async () => {
|
|
|
1655
1842
|
module-level cache in lib/tags.ts means several explorers on one page
|
|
1656
1843
|
still cost a single query. */
|
|
1657
1844
|
void loadNavTags();
|
|
1845
|
+
/* surucu:d1 — the storage line. After the listing, like the tag list: it is
|
|
1846
|
+
a status, and the folder somebody asked for is the critical path. */
|
|
1847
|
+
void loadQuota();
|
|
1658
1848
|
/* ⚠ The panel is not always on screen at mount. Below 560px it is a DRAWER
|
|
1659
1849
|
that starts closed, so `navVisible` is false and the call above returns
|
|
1660
1850
|
without asking for anything — measured at 390px: the drawer opened with
|
|
@@ -1806,6 +1996,11 @@ useKeyboardShortcuts(rootEl, {
|
|
|
1806
1996
|
onGoUp: () => (paneIsActive.value ? splitPaneRef.value?.goUp() : goUp()) /* wiring:d1 pane-route */,
|
|
1807
1997
|
/* cila:c wiring */
|
|
1808
1998
|
onPathJump: () => {
|
|
1999
|
+
/* surucu:d1 — the shortcut from anywhere else opens a BLANK palette. The
|
|
2000
|
+
seed belongs to the drive header's field; leaving the last one in place
|
|
2001
|
+
would reopen the palette pre-filled with a query the user has since
|
|
2002
|
+
moved on from. */
|
|
2003
|
+
paletteSeed.value = '';
|
|
1809
2004
|
showPalette.value = true;
|
|
1810
2005
|
},
|
|
1811
2006
|
onShowHelp: () => {
|
|
@@ -3906,11 +4101,42 @@ const e2eUpgradeOffer = ref(false);
|
|
|
3906
4101
|
const e2eUpgradePw = ref('');
|
|
3907
4102
|
const e2eUpgradeBusy = ref(false);
|
|
3908
4103
|
|
|
4104
|
+
/* wiring:e2 escrow-offer — offering an EXISTING folder an escrow slot.
|
|
4105
|
+
*
|
|
4106
|
+
* Adoption covers folders created after it, which on an installation that
|
|
4107
|
+
* has been running for a while is nobody's folders: the ones that matter
|
|
4108
|
+
* already exist. The server cannot reach them — adding a slot needs the
|
|
4109
|
+
* folder master key. Its owner can, from inside, with the password, and the
|
|
4110
|
+
* one moment that password exists in the browser is an unlock. So this
|
|
4111
|
+
* lives exactly where the v1 recovery offer lives, in the same strip, with
|
|
4112
|
+
* the same shape.
|
|
4113
|
+
*
|
|
4114
|
+
* mode distinguishes the two ways in:
|
|
4115
|
+
* 'unlock' right after a successful password unlock — we still hold the
|
|
4116
|
+
* password, so Accept needs no typing, and Not now RECORDS a
|
|
4117
|
+
* refusal so the question is not asked again.
|
|
4118
|
+
* 'manual' the way back, from the unlocked strip, for somebody who said
|
|
4119
|
+
* no earlier. The password is gone by then, so it is typed, and
|
|
4120
|
+
* Cancel records nothing — they already answered once. */
|
|
4121
|
+
const e2eEscrowOffer = ref(false);
|
|
4122
|
+
const e2eEscrowOfferMode = ref<'unlock' | 'manual'>('unlock');
|
|
4123
|
+
const e2eEscrowPw = ref('');
|
|
4124
|
+
const e2eEscrowBusy = ref(false);
|
|
4125
|
+
const e2eEscrowErr = ref('');
|
|
4126
|
+
|
|
3909
4127
|
/** The installation's escrow public key, or null when escrow is off.
|
|
3910
4128
|
* Published in /api/capabilities on purpose — see docs/E2E-ENCRYPTION.md. */
|
|
3911
4129
|
const e2eEscrowPub = computed<string | null>(
|
|
3912
4130
|
() => capabilitiesData.value?.e2e_escrow?.public_key || null,
|
|
3913
4131
|
);
|
|
4132
|
+
/** Where the honest version of "what escrow can and cannot do" lives.
|
|
4133
|
+
* ⚠ Linked from the offer on purpose: the notice says what accepting means,
|
|
4134
|
+
* and the page says what the use-notification can and cannot promise — that
|
|
4135
|
+
* an operator holding the private key can decrypt offline with no request,
|
|
4136
|
+
* no notification and no audit row. Somebody being asked to hand over a key
|
|
4137
|
+
* is entitled to read that before answering, not after. */
|
|
4138
|
+
const e2eEscrowDocsUrl = 'https://docs.filex.sh/E2E-ENCRYPTION#what-escrow-can-and-cannot-do';
|
|
4139
|
+
|
|
3914
4140
|
const e2eEscrowKid = computed<string | null>(
|
|
3915
4141
|
() => capabilitiesData.value?.e2e_escrow?.kid || null,
|
|
3916
4142
|
);
|
|
@@ -3933,7 +4159,7 @@ async function e2eUnlock() {
|
|
|
3933
4159
|
try {
|
|
3934
4160
|
let markerText = '';
|
|
3935
4161
|
try {
|
|
3936
|
-
const { blob, url } = await api.fetchBlob(wireJoin(e2eRoot.value, E2E_MARKER_NAME));
|
|
4162
|
+
const { blob, url } = await api.fetchBlob(wireJoin(e2eRoot.value, E2E_MARKER_NAME), { fresh: true });
|
|
3937
4163
|
URL.revokeObjectURL(url);
|
|
3938
4164
|
markerText = await blob.text();
|
|
3939
4165
|
} catch {
|
|
@@ -3962,6 +4188,18 @@ async function e2eUnlock() {
|
|
|
3962
4188
|
if (marker.v === 1) {
|
|
3963
4189
|
e2eUpgradePw.value = e2ePw.value;
|
|
3964
4190
|
e2eUpgradeOffer.value = true;
|
|
4191
|
+
} else if (escrowOfferState(marker, e2eEscrowKid.value) === 'offer') {
|
|
4192
|
+
/* wiring:e2 escrow-offer — a v2 folder that predates escrow here.
|
|
4193
|
+
* ⚠ Only after the unlock SUCCEEDED, and only by password: accepting
|
|
4194
|
+
* gives the operator a key to this folder, so the person asked has to
|
|
4195
|
+
* be the person who can already open it. A v1 folder is handled by the
|
|
4196
|
+
* branch above, which seals an escrow slot as part of the upgrade and
|
|
4197
|
+
* already discloses that — two offers on one unlock would be two
|
|
4198
|
+
* chances to get the disclosure wrong. */
|
|
4199
|
+
e2eEscrowPw.value = e2ePw.value;
|
|
4200
|
+
e2eEscrowOfferMode.value = 'unlock';
|
|
4201
|
+
e2eEscrowErr.value = '';
|
|
4202
|
+
e2eEscrowOffer.value = true;
|
|
3965
4203
|
}
|
|
3966
4204
|
e2ePw.value = '';
|
|
3967
4205
|
} finally {
|
|
@@ -4204,7 +4442,7 @@ async function e2eRecoverUnlock(payload: { mode: 'recovery' | 'escrow'; value: s
|
|
|
4204
4442
|
async function openRecoveryUnlock() {
|
|
4205
4443
|
if (!e2eMarker.value && e2eRoot.value) {
|
|
4206
4444
|
try {
|
|
4207
|
-
const { blob, url } = await api.fetchBlob(wireJoin(e2eRoot.value, E2E_MARKER_NAME));
|
|
4445
|
+
const { blob, url } = await api.fetchBlob(wireJoin(e2eRoot.value, E2E_MARKER_NAME), { fresh: true });
|
|
4208
4446
|
URL.revokeObjectURL(url);
|
|
4209
4447
|
e2eMarker.value = parseMarker(await blob.text());
|
|
4210
4448
|
} catch {
|
|
@@ -4243,6 +4481,101 @@ async function e2eDoUpgrade() {
|
|
|
4243
4481
|
}
|
|
4244
4482
|
}
|
|
4245
4483
|
|
|
4484
|
+
/* wiring:e2 escrow-offer ------------------------------------------- */
|
|
4485
|
+
|
|
4486
|
+
/** Whether this folder could be given an escrow slot, and whether its owner
|
|
4487
|
+
* has already answered. Drives the way-back button in the unlocked strip. */
|
|
4488
|
+
const e2eEscrowOfferState = computed(() => escrowOfferState(e2eMarker.value, e2eEscrowKid.value));
|
|
4489
|
+
|
|
4490
|
+
/** The way back. Somebody who said no last month can say yes today without
|
|
4491
|
+
* deleting and re-creating the folder — but the password is long gone from
|
|
4492
|
+
* memory, so they type it. That is not friction to be smoothed away: it is
|
|
4493
|
+
* the same proof of ownership the offer at unlock had, and handing the
|
|
4494
|
+
* operator a key deserves it. */
|
|
4495
|
+
function e2eOpenEscrowOffer() {
|
|
4496
|
+
e2eEscrowOfferMode.value = 'manual';
|
|
4497
|
+
e2eEscrowPw.value = '';
|
|
4498
|
+
e2eEscrowErr.value = '';
|
|
4499
|
+
e2eEscrowOffer.value = true;
|
|
4500
|
+
}
|
|
4501
|
+
|
|
4502
|
+
/** Write a marker back to the folder and adopt it as the one we are holding.
|
|
4503
|
+
* Both escrow-offer answers change only `.filex-e2e.json`. */
|
|
4504
|
+
async function e2eWriteMarker(next: E2eMarker) {
|
|
4505
|
+
const file = new File([JSON.stringify(next)], E2E_MARKER_NAME, { type: 'application/json' });
|
|
4506
|
+
await api.uploadMultipart(e2eRoot.value, [file]);
|
|
4507
|
+
e2eMarker.value = next;
|
|
4508
|
+
}
|
|
4509
|
+
|
|
4510
|
+
/** Accept: seal this folder's master key to the installation's escrow key.
|
|
4511
|
+
* No file is re-encrypted or moved — only the marker gains a slot. */
|
|
4512
|
+
async function e2eAcceptEscrow() {
|
|
4513
|
+
const marker = e2eMarker.value;
|
|
4514
|
+
const pub = e2eEscrowPub.value;
|
|
4515
|
+
if (!marker || !pub || !e2eRoot.value || e2eEscrowBusy.value) return;
|
|
4516
|
+
if (!e2eEscrowPw.value) {
|
|
4517
|
+
e2eEscrowErr.value = t('e2e.escrowoffer.password_required');
|
|
4518
|
+
return;
|
|
4519
|
+
}
|
|
4520
|
+
e2eEscrowBusy.value = true;
|
|
4521
|
+
e2eEscrowErr.value = '';
|
|
4522
|
+
try {
|
|
4523
|
+
// ⚠ The kid comes from the installation's CURRENT key, via the marker
|
|
4524
|
+
// addEscrowSlot writes — never from anything the UI is displaying. The
|
|
4525
|
+
// sibling bug went the other way (the dialog labelled a folder's slot
|
|
4526
|
+
// with the installation's kid); writing the installation's kid into a
|
|
4527
|
+
// slot sealed to some other key would be the same lie in reverse.
|
|
4528
|
+
const next = await addEscrowSlot(marker, e2eEscrowPw.value, pub);
|
|
4529
|
+
await e2eWriteMarker(next);
|
|
4530
|
+
e2eEscrowOffer.value = false;
|
|
4531
|
+
e2eEscrowPw.value = '';
|
|
4532
|
+
flashToast(t('e2e.escrowoffer.done'));
|
|
4533
|
+
} catch (err) {
|
|
4534
|
+
// A wrong password is the ordinary case here and reads as a typo, not
|
|
4535
|
+
// as a broken folder; anything else is worth reporting.
|
|
4536
|
+
if ((err as Error)?.name === 'E2eDecryptError') {
|
|
4537
|
+
e2eEscrowErr.value = t('e2e.escrowoffer.wrong_password');
|
|
4538
|
+
} else {
|
|
4539
|
+
e2eEscrowErr.value = t('e2e.escrowoffer.failed');
|
|
4540
|
+
emit('error', { message: (err as Error).message, context: { op: 'e2e-escrow-offer' } });
|
|
4541
|
+
}
|
|
4542
|
+
} finally {
|
|
4543
|
+
e2eEscrowBusy.value = false;
|
|
4544
|
+
}
|
|
4545
|
+
}
|
|
4546
|
+
|
|
4547
|
+
/** Not now. Recorded IN THE FOLDER, so the same person on another device is
|
|
4548
|
+
* not asked again and the answer survives a cleared browser. A question
|
|
4549
|
+
* that returns every single unlock is how people learn to click past
|
|
4550
|
+
* security dialogs without reading them. */
|
|
4551
|
+
async function e2eDeclineEscrow() {
|
|
4552
|
+
const marker = e2eMarker.value;
|
|
4553
|
+
if (!marker || !e2eRoot.value || e2eEscrowBusy.value) return;
|
|
4554
|
+
e2eEscrowBusy.value = true;
|
|
4555
|
+
try {
|
|
4556
|
+
await e2eWriteMarker(declineEscrowSlot(marker, new Date().toISOString()));
|
|
4557
|
+
e2eEscrowOffer.value = false;
|
|
4558
|
+
e2eEscrowPw.value = '';
|
|
4559
|
+
flashToast(t('e2e.escrowoffer.declined_toast'));
|
|
4560
|
+
} catch (err) {
|
|
4561
|
+
// ⚠ Fail towards asking again rather than towards silence: if the
|
|
4562
|
+
// refusal could not be written, the honest state is "not answered yet".
|
|
4563
|
+
e2eEscrowErr.value = t('e2e.escrowoffer.decline_failed');
|
|
4564
|
+
emit('error', { message: (err as Error).message, context: { op: 'e2e-escrow-decline' } });
|
|
4565
|
+
} finally {
|
|
4566
|
+
e2eEscrowBusy.value = false;
|
|
4567
|
+
}
|
|
4568
|
+
}
|
|
4569
|
+
|
|
4570
|
+
/** Close the way-back panel without answering anything. Distinct from
|
|
4571
|
+
* declining: they answered once already, and re-recording the same refusal
|
|
4572
|
+
* would overwrite the date it was actually made. */
|
|
4573
|
+
function e2eCloseEscrowOffer() {
|
|
4574
|
+
e2eEscrowOffer.value = false;
|
|
4575
|
+
e2eEscrowPw.value = '';
|
|
4576
|
+
e2eEscrowErr.value = '';
|
|
4577
|
+
}
|
|
4578
|
+
|
|
4246
4579
|
/** Decline the offer. The folder keeps working exactly as it did, and the
|
|
4247
4580
|
* prompt returns on the next unlock because the risk has not changed. */
|
|
4248
4581
|
function e2eDeclineUpgrade() {
|
|
@@ -4311,6 +4644,9 @@ function closeRecoveryKey() {
|
|
|
4311
4644
|
:nav-open="navToggleOn /* gezinti:g1 */"
|
|
4312
4645
|
:nav-enabled="sideNavEnabled /* gezinti:g1 */"
|
|
4313
4646
|
:view-modes="allowedViewModes /* gezinti:g1 */"
|
|
4647
|
+
:shell="driveShell ? 'drive' : 'classic' /* surucu:d1 */"
|
|
4648
|
+
:scope-label="driveScopeLabel /* surucu:d1 */"
|
|
4649
|
+
@open-palette="openPaletteWith /* surucu:d1 */"
|
|
4314
4650
|
@toggle-inspector="toggleInspector /* koru:k1 */"
|
|
4315
4651
|
@toggle-nav="toggleSideNav /* gezinti:g1 */"
|
|
4316
4652
|
@open-theme="showThemeGallery = true /* wiring:c1 */"
|
|
@@ -4352,6 +4688,11 @@ function closeRecoveryKey() {
|
|
|
4352
4688
|
:show-identity-surfaces="identitySurfaces"
|
|
4353
4689
|
:can-write="canWriteHere && !atVirtualRoot && !trashActive"
|
|
4354
4690
|
:locale="locale"
|
|
4691
|
+
:new-menu="driveShell /* surucu:d1 */"
|
|
4692
|
+
:can-request-files="canWriteHere && !atVirtualRoot && !trashActive && !navView /* surucu:d1 */"
|
|
4693
|
+
:quota="quotaSnapshot /* surucu:d1 */"
|
|
4694
|
+
:theme="themeMode /* surucu:d1 — the teleported New menu leaves .fe */"
|
|
4695
|
+
@request-files="openFileRequest /* surucu:d1 */"
|
|
4355
4696
|
@toggle="toggleSideNav"
|
|
4356
4697
|
@close="closeNavDrawer"
|
|
4357
4698
|
@open-view="loadNavView"
|
|
@@ -4380,6 +4721,14 @@ function closeRecoveryKey() {
|
|
|
4380
4721
|
class="fe__primary"
|
|
4381
4722
|
:class="{ 'fe-pane--focus': mainPaneFocus } /* wiring:d1 — aktif panel vurgusu */"
|
|
4382
4723
|
>
|
|
4724
|
+
<!-- surucu:d1 — the breadcrumb row. In the drive shell it also carries the
|
|
4725
|
+
two controls that belong to a LISTING rather than to the app (the view
|
|
4726
|
+
switcher and the details toggle), which is where the mockups put them
|
|
4727
|
+
and where the header then has room for one wide search field.
|
|
4728
|
+
Everywhere else the wrapper is `display: contents`, so the breadcrumb
|
|
4729
|
+
is the same flex child of `.fe__primary` it has always been — one
|
|
4730
|
+
Breadcrumb, not a second copy that can drift. -->
|
|
4731
|
+
<div :class="driveShell ? 'fe-subhead' : 'fe-subhead--plain'">
|
|
4383
4732
|
<Breadcrumb
|
|
4384
4733
|
:dirname="dirname"
|
|
4385
4734
|
:adapter="adapter"
|
|
@@ -4392,6 +4741,49 @@ function closeRecoveryKey() {
|
|
|
4392
4741
|
@crumb-context="onCrumbContext"
|
|
4393
4742
|
@crumb-drop="onCrumbDropInto"
|
|
4394
4743
|
/>
|
|
4744
|
+
<div v-if="driveShell" class="fe-subhead__actions">
|
|
4745
|
+
<ViewSwitcher
|
|
4746
|
+
:view-mode="displayedViewMode"
|
|
4747
|
+
:locale="locale"
|
|
4748
|
+
:modes="allowedViewModes"
|
|
4749
|
+
@update:view-mode="setDisplayedViewMode($event)"
|
|
4750
|
+
/>
|
|
4751
|
+
<button
|
|
4752
|
+
type="button"
|
|
4753
|
+
class="fe-btn fe-btn--icon-only fe-toolbar__inspector"
|
|
4754
|
+
:class="{ 'is-active': showInspector }"
|
|
4755
|
+
:aria-pressed="showInspector"
|
|
4756
|
+
:title="t('toolbar.inspector')"
|
|
4757
|
+
:aria-label="t('toolbar.inspector')"
|
|
4758
|
+
data-testid="subhead-inspector"
|
|
4759
|
+
@click="toggleInspector"
|
|
4760
|
+
>
|
|
4761
|
+
<svg
|
|
4762
|
+
class="fe-ficon"
|
|
4763
|
+
viewBox="0 0 24 24"
|
|
4764
|
+
fill="none"
|
|
4765
|
+
stroke="currentColor"
|
|
4766
|
+
stroke-width="1.8"
|
|
4767
|
+
stroke-linecap="round"
|
|
4768
|
+
aria-hidden="true"
|
|
4769
|
+
focusable="false"
|
|
4770
|
+
>
|
|
4771
|
+
<circle cx="12" cy="12" r="9" />
|
|
4772
|
+
<path d="M12 11v5" />
|
|
4773
|
+
<circle cx="12" cy="7.6" r="1" fill="currentColor" stroke="none" />
|
|
4774
|
+
</svg>
|
|
4775
|
+
</button>
|
|
4776
|
+
</div>
|
|
4777
|
+
</div>
|
|
4778
|
+
<FilterBar
|
|
4779
|
+
v-if="driveShell && !atVirtualRoot"
|
|
4780
|
+
:value="driveFilters"
|
|
4781
|
+
:locale="locale"
|
|
4782
|
+
:theme="themeMode"
|
|
4783
|
+
:shown="displayFiles.length"
|
|
4784
|
+
:total="files.length"
|
|
4785
|
+
@update:value="setDriveFilters"
|
|
4786
|
+
/>
|
|
4395
4787
|
|
|
4396
4788
|
<!-- Live presence: who else is viewing this folder (empty → nothing shown).
|
|
4397
4789
|
When the live socket is unavailable the same strip carries a small
|
|
@@ -4438,9 +4830,76 @@ function closeRecoveryKey() {
|
|
|
4438
4830
|
</button>
|
|
4439
4831
|
</div>
|
|
4440
4832
|
</div>
|
|
4833
|
+
<!-- wiring:e2 escrow-offer — an existing folder that predates escrow on
|
|
4834
|
+
this installation. Shown only after an unlock SUCCEEDED, because
|
|
4835
|
+
accepting hands the operator a permanent second key to this folder
|
|
4836
|
+
and the only person entitled to do that is the one who can already
|
|
4837
|
+
open it. Doing nothing leaves the folder exactly as it is. -->
|
|
4838
|
+
<div v-if="e2eEscrowOffer" class="fe-e2e-upgrade fe-e2e-upgrade--escrow" role="alert">
|
|
4839
|
+
<div class="fe-e2e-upgrade__text">
|
|
4840
|
+
<strong>{{ t('e2e.escrowoffer.title') }}</strong>
|
|
4841
|
+
<p>{{ t('e2e.escrowoffer.body') }}</p>
|
|
4842
|
+
<p class="fe-e2e-upgrade__escrow">
|
|
4843
|
+
{{ t('e2e.escrowoffer.consequence') }}
|
|
4844
|
+
<a :href="e2eEscrowDocsUrl" target="_blank" rel="noopener noreferrer">
|
|
4845
|
+
{{ t('e2e.escrowoffer.learn_more') }}
|
|
4846
|
+
</a>
|
|
4847
|
+
</p>
|
|
4848
|
+
<p v-if="e2eEscrowKid" class="fe-e2e-upgrade__escrow">
|
|
4849
|
+
{{ t('e2e.recover.escrow_kid') }}: <code>{{ e2eEscrowKid }}</code>
|
|
4850
|
+
</p>
|
|
4851
|
+
<!-- The way-back path has no password in memory any more, so it is
|
|
4852
|
+
typed. The same proof of ownership the unlock path already had. -->
|
|
4853
|
+
<label v-if="e2eEscrowOfferMode === 'manual'" class="fe-e2e-upgrade__pw">
|
|
4854
|
+
<span>{{ t('e2e.escrowoffer.password_label') }}</span>
|
|
4855
|
+
<input
|
|
4856
|
+
v-model="e2eEscrowPw"
|
|
4857
|
+
type="password"
|
|
4858
|
+
class="fe-input"
|
|
4859
|
+
autocomplete="current-password"
|
|
4860
|
+
:disabled="e2eEscrowBusy"
|
|
4861
|
+
@keyup.enter="e2eAcceptEscrow"
|
|
4862
|
+
/>
|
|
4863
|
+
</label>
|
|
4864
|
+
<p v-if="e2eEscrowErr" class="fe-form__error" role="alert">{{ e2eEscrowErr }}</p>
|
|
4865
|
+
</div>
|
|
4866
|
+
<div class="fe-e2e-upgrade__actions">
|
|
4867
|
+
<button
|
|
4868
|
+
type="button"
|
|
4869
|
+
class="fe-btn"
|
|
4870
|
+
:disabled="e2eEscrowBusy"
|
|
4871
|
+
@click="e2eEscrowOfferMode === 'manual' ? e2eCloseEscrowOffer() : e2eDeclineEscrow()"
|
|
4872
|
+
>
|
|
4873
|
+
{{
|
|
4874
|
+
e2eEscrowOfferMode === 'manual'
|
|
4875
|
+
? t('e2e.escrowoffer.cancel')
|
|
4876
|
+
: t('e2e.escrowoffer.decline')
|
|
4877
|
+
}}
|
|
4878
|
+
</button>
|
|
4879
|
+
<button
|
|
4880
|
+
type="button"
|
|
4881
|
+
class="fe-btn fe-btn--primary"
|
|
4882
|
+
:disabled="e2eEscrowBusy"
|
|
4883
|
+
@click="e2eAcceptEscrow"
|
|
4884
|
+
>
|
|
4885
|
+
{{ e2eEscrowBusy ? t('e2e.escrowoffer.busy') : t('e2e.escrowoffer.accept') }}
|
|
4886
|
+
</button>
|
|
4887
|
+
</div>
|
|
4888
|
+
</div>
|
|
4441
4889
|
<div v-if="e2eUnlocked" class="fe-e2e-strip" role="status">
|
|
4442
4890
|
<span class="fe-e2e-strip__icon" aria-hidden="true">🔒</span>
|
|
4443
4891
|
<span class="fe-e2e-strip__label">{{ t('e2e.strip.label') }}</span>
|
|
4892
|
+
<!-- The way back for somebody who declined. Quiet, but present: a
|
|
4893
|
+
refusal that could not be reversed without deleting the folder
|
|
4894
|
+
would not be a decision, it would be a trap. -->
|
|
4895
|
+
<button
|
|
4896
|
+
v-if="e2eEscrowOfferState !== 'n/a' && !e2eEscrowOffer"
|
|
4897
|
+
type="button"
|
|
4898
|
+
class="fe-btn fe-e2e-strip__btn"
|
|
4899
|
+
@click="e2eOpenEscrowOffer"
|
|
4900
|
+
>
|
|
4901
|
+
{{ t('e2e.escrowoffer.strip_action') }}
|
|
4902
|
+
</button>
|
|
4444
4903
|
<button type="button" class="fe-btn fe-e2e-strip__btn" @click="e2eLock">
|
|
4445
4904
|
{{ t('e2e.strip.lock') }}
|
|
4446
4905
|
</button>
|
|
@@ -4675,6 +5134,36 @@ function closeRecoveryKey() {
|
|
|
4675
5134
|
</svg>
|
|
4676
5135
|
<p class="fe-state__title">{{ t('empty.trash.title') }}</p>
|
|
4677
5136
|
</div>
|
|
5137
|
+
<!-- surucu:d1 — the folder HAS rows and the filters hid all of them.
|
|
5138
|
+
"This folder is empty" would be false, and the way back is the chip
|
|
5139
|
+
row just above, so the message names it and offers the button. -->
|
|
5140
|
+
<div
|
|
5141
|
+
v-else-if="!loading && filtersOn && displayFiles.length === 0 && files.length > 0"
|
|
5142
|
+
class="fe-state"
|
|
5143
|
+
data-testid="empty-filtered"
|
|
5144
|
+
>
|
|
5145
|
+
<svg
|
|
5146
|
+
class="fe-state__art"
|
|
5147
|
+
viewBox="0 0 120 100"
|
|
5148
|
+
width="110"
|
|
5149
|
+
height="92"
|
|
5150
|
+
fill="none"
|
|
5151
|
+
stroke="currentColor"
|
|
5152
|
+
stroke-width="2"
|
|
5153
|
+
stroke-linecap="round"
|
|
5154
|
+
stroke-linejoin="round"
|
|
5155
|
+
aria-hidden="true"
|
|
5156
|
+
>
|
|
5157
|
+
<path d="M26 28h68L68 58v24l-16 8V58z" />
|
|
5158
|
+
</svg>
|
|
5159
|
+
<p class="fe-state__title">{{ t('filter.empty.title') }}</p>
|
|
5160
|
+
<p class="fe-state__hint">{{ t('filter.empty.hint') }}</p>
|
|
5161
|
+
<div class="fe-state__actions">
|
|
5162
|
+
<button type="button" class="fe-btn" @click="clearDriveFilters">
|
|
5163
|
+
{{ t('filter.clear') }}
|
|
5164
|
+
</button>
|
|
5165
|
+
</div>
|
|
5166
|
+
</div>
|
|
4678
5167
|
<!-- Loaded, zero files, no search: the real empty-folder state. The
|
|
4679
5168
|
upload affordances follow write permission (RBAC viewers only get
|
|
4680
5169
|
the title). -->
|
|
@@ -4707,7 +5196,7 @@ function closeRecoveryKey() {
|
|
|
4707
5196
|
</div>
|
|
4708
5197
|
<ListView
|
|
4709
5198
|
v-else-if="viewMode === 'list'"
|
|
4710
|
-
:files="
|
|
5199
|
+
:files="displayFiles /* surucu:d1 */"
|
|
4711
5200
|
:selected="selection.selected.value"
|
|
4712
5201
|
:clipped="clippedPaths"
|
|
4713
5202
|
:show-parent-path="!!searchQuery"
|
|
@@ -4715,6 +5204,7 @@ function closeRecoveryKey() {
|
|
|
4715
5204
|
:loading="loading"
|
|
4716
5205
|
:keep-badge-for="desktopSync ? keepBadgeFor : undefined"
|
|
4717
5206
|
:starred-ids="starredIds"
|
|
5207
|
+
:star-enabled="identitySurfaces"
|
|
4718
5208
|
:api-base="props.config.apiBase ?? ''"
|
|
4719
5209
|
:auth-headers="() => buildAuthHeaders()"
|
|
4720
5210
|
:auth-credentials="api.credentialsMode()"
|
|
@@ -4727,7 +5217,8 @@ function closeRecoveryKey() {
|
|
|
4727
5217
|
/>
|
|
4728
5218
|
<GridView
|
|
4729
5219
|
v-else-if="viewMode === 'grid' /* wiring:d2 — v-else → v-else-if (3. mod eklendi) */"
|
|
4730
|
-
:files="
|
|
5220
|
+
:files="displayFiles /* surucu:d1 */"
|
|
5221
|
+
:sections="driveShell /* surucu:d1 */"
|
|
4731
5222
|
:selected="selection.selected.value"
|
|
4732
5223
|
:clipped="clippedPaths"
|
|
4733
5224
|
:show-parent-path="!!searchQuery"
|
|
@@ -4736,6 +5227,7 @@ function closeRecoveryKey() {
|
|
|
4736
5227
|
:keep-badge-for="desktopSync ? keepBadgeFor : undefined"
|
|
4737
5228
|
:thumb-src="thumbs.src"
|
|
4738
5229
|
:starred-ids="starredIds"
|
|
5230
|
+
:star-enabled="identitySurfaces"
|
|
4739
5231
|
:api-base="props.config.apiBase ?? ''"
|
|
4740
5232
|
:auth-headers="() => buildAuthHeaders()"
|
|
4741
5233
|
:auth-credentials="api.credentialsMode()"
|
|
@@ -4749,7 +5241,7 @@ function closeRecoveryKey() {
|
|
|
4749
5241
|
<!-- wiring:d2 — galeri görünümü (GridView ile aynı event sözleşmesi) -->
|
|
4750
5242
|
<GalleryView
|
|
4751
5243
|
v-else
|
|
4752
|
-
:files="
|
|
5244
|
+
:files="displayFiles /* surucu:d1 */"
|
|
4753
5245
|
:selected="selection.selected.value"
|
|
4754
5246
|
:clipped="clippedPaths"
|
|
4755
5247
|
:show-parent-path="!!searchQuery"
|
|
@@ -4757,6 +5249,7 @@ function closeRecoveryKey() {
|
|
|
4757
5249
|
:loading="loading"
|
|
4758
5250
|
:thumb-src="thumbs.src"
|
|
4759
5251
|
:starred-ids="starredIds"
|
|
5252
|
+
:star-enabled="identitySurfaces"
|
|
4760
5253
|
:api-base="props.config.apiBase ?? ''"
|
|
4761
5254
|
:auth-headers="() => buildAuthHeaders()"
|
|
4762
5255
|
:auth-credentials="api.credentialsMode()"
|
|
@@ -4793,6 +5286,7 @@ function closeRecoveryKey() {
|
|
|
4793
5286
|
:view-mode="paneViewMode /* ui-fix */"
|
|
4794
5287
|
:thumb-src="thumbs.src /* ui-fix */"
|
|
4795
5288
|
:trash-visible="config.trashVisible !== false /* ui-fix — çöp satırı simetrisi */"
|
|
5289
|
+
:nav-offers-trash="navOffersTrash /* surucu:d1 — aynı kapı iki kez açılmaz */"
|
|
4796
5290
|
@navigate="onPaneNavigate"
|
|
4797
5291
|
@activate="activePane = 'split'"
|
|
4798
5292
|
@close="closeSplit"
|
|
@@ -4815,7 +5309,9 @@ function closeRecoveryKey() {
|
|
|
4815
5309
|
:locale="locale"
|
|
4816
5310
|
:narrow="isNarrow"
|
|
4817
5311
|
:thumb-src="thumbs.src"
|
|
5312
|
+
:tabs="driveShell /* surucu:d1 */"
|
|
4818
5313
|
@close="closeInspector"
|
|
5314
|
+
@share-created="onInspectorShareCreated /* surucu:d1 */"
|
|
4819
5315
|
@manage-permissions="onInspectorManage"
|
|
4820
5316
|
@toast="flashToast"
|
|
4821
5317
|
@changed="() => load()"
|
|
@@ -4974,8 +5470,8 @@ function closeRecoveryKey() {
|
|
|
4974
5470
|
:open="showRecoveryUnlock"
|
|
4975
5471
|
:locale="locale"
|
|
4976
5472
|
:has-recovery="markerHasRecovery(e2eMarker)"
|
|
4977
|
-
:
|
|
4978
|
-
:escrow-kid="e2eEscrowKid"
|
|
5473
|
+
:escrow-state="escrowAvailability(e2eMarker, e2eEscrowKid)"
|
|
5474
|
+
:escrow-kid="e2eMarker?.esc?.kid || e2eEscrowKid"
|
|
4979
5475
|
:busy="e2eRecoverBusy"
|
|
4980
5476
|
:error="e2eRecoverErr"
|
|
4981
5477
|
@close="showRecoveryUnlock = false"
|
|
@@ -5042,7 +5538,8 @@ function closeRecoveryKey() {
|
|
|
5042
5538
|
:size="typeof permTarget.size === 'number' ? permTarget.size : undefined"
|
|
5043
5539
|
:locale="locale === 'en' ? 'en' : 'tr'"
|
|
5044
5540
|
:share-max-ttl-days="shareMaxTtlDays"
|
|
5045
|
-
|
|
5541
|
+
:initial-tab="permInitialTab /* surucu:d1 */"
|
|
5542
|
+
@close="showPerm = false; permInitialTab = undefined"
|
|
5046
5543
|
/>
|
|
5047
5544
|
|
|
5048
5545
|
<!-- Recently-opened tray. Anchored to the toolbar trigger via fixed
|
|
@@ -5108,6 +5605,7 @@ function closeRecoveryKey() {
|
|
|
5108
5605
|
|
|
5109
5606
|
<!-- cila:c wiring — command palette (Ctrl/Cmd+K) + shortcuts help (?) -->
|
|
5110
5607
|
<CommandPalette
|
|
5608
|
+
:initial-query="paletteSeed /* surucu:d1 */"
|
|
5111
5609
|
:open="showPalette"
|
|
5112
5610
|
:locale="locale"
|
|
5113
5611
|
:files="files"
|