@brftech/filex-core 0.30.1 → 0.31.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 +11 -0
- package/dist/filex-core.js +9718 -8611
- package/dist/filex-core.js.map +1 -1
- package/dist/filex-core.umd.cjs +95 -87
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/index.d.ts +368 -64
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +599 -19
- package/src/components/Breadcrumb.vue +4 -2
- package/src/components/E2eRecoveryUnlockModal.vue +168 -0
- package/src/components/EncryptedFolderModal.vue +10 -0
- package/src/components/GalleryView.vue +37 -0
- package/src/components/GridView.vue +39 -0
- package/src/components/ListView.vue +1 -0
- package/src/components/RecoveryKeyModal.vue +133 -0
- package/src/components/SideNav.vue +168 -5
- package/src/components/StarButton.vue +27 -15
- package/src/composables/useFileApi.ts +38 -0
- package/src/composables/useKeyboardShortcuts.ts +7 -0
- package/src/index.ts +27 -1
- package/src/lib/e2ecrypto.ts +528 -70
- package/src/lib/listing.ts +56 -1
- package/src/lib/star.ts +42 -0
- package/src/lib/tags.ts +105 -0
- package/src/locales/en.ts +71 -2
- package/src/locales/tr.ts +71 -2
- package/src/styles/base.css +252 -0
- package/src/types/ExplorerConfig.ts +36 -0
- package/src/types/FileNode.ts +23 -0
|
@@ -28,12 +28,12 @@
|
|
|
28
28
|
* hash does not match in the web-component build, so the rules silently stop
|
|
29
29
|
* applying in every embed (measured on the share dialog: raw unstyled HTML).
|
|
30
30
|
*/
|
|
31
|
-
import { computed } from 'vue';
|
|
31
|
+
import { computed, ref } from 'vue';
|
|
32
32
|
import { useLocale } from '../composables/useLocale';
|
|
33
33
|
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
34
34
|
|
|
35
35
|
/** The virtual listings the panel can open. '' = an ordinary folder. */
|
|
36
|
-
export type NavView = '' | 'recent' | 'starred' | 'shared' | 'trash';
|
|
36
|
+
export type NavView = '' | 'recent' | 'starred' | 'shared' | 'trash' | 'tag';
|
|
37
37
|
|
|
38
38
|
export interface NavStorage {
|
|
39
39
|
name: string;
|
|
@@ -61,6 +61,20 @@ const props = defineProps<{
|
|
|
61
61
|
sharedStorages?: string[];
|
|
62
62
|
/** Show the Trash entry (mirrors ExplorerConfig.trashVisible). */
|
|
63
63
|
trashVisible?: boolean;
|
|
64
|
+
/* === etiket:t1 — the Tags section ==================================
|
|
65
|
+
* "Tagged files should show up inside the tag." Tags are the one
|
|
66
|
+
* navigation family whose entries are USER data: unbounded in number and
|
|
67
|
+
* dynamic in name. Still presentational here — the host fetches
|
|
68
|
+
* `tags/all` (once, cached) and hands the list over, exactly as it does
|
|
69
|
+
* for storages. */
|
|
70
|
+
/** Every tag that exists, alphabetical. Empty → the section shows its own
|
|
71
|
+
* "no tags yet" line rather than disappearing. */
|
|
72
|
+
tags?: string[];
|
|
73
|
+
/** False until the first answer arrives, so "no tags yet" is never shown
|
|
74
|
+
* to somebody who is simply still waiting. */
|
|
75
|
+
tagsLoaded?: boolean;
|
|
76
|
+
/** The tag currently on screen (activeView === 'tag'). */
|
|
77
|
+
activeTag?: string;
|
|
64
78
|
/**
|
|
65
79
|
* Show the Connections entries — "How to connect" and "API keys".
|
|
66
80
|
* ⚠ Never derived from a role here or anywhere: the backend decides what a
|
|
@@ -68,6 +82,19 @@ const props = defineProps<{
|
|
|
68
82
|
* accounts that need it (see ExplorerConfig.connections).
|
|
69
83
|
*/
|
|
70
84
|
showConnections?: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Draw the surfaces that only mean something for ONE person: API keys,
|
|
87
|
+
* Recent, Starred, Shared with me. False when the caller is an app token —
|
|
88
|
+
* a host proxy's shared credential, where "your keys" would be the proxy's
|
|
89
|
+
* own and "your Recent" would be the token owner's history shown to a
|
|
90
|
+
* stranger (see ExplorerConfig.callerKind).
|
|
91
|
+
*
|
|
92
|
+
* ⚠ This is a KIND check, not the role check the comment above forbids, and
|
|
93
|
+
* it is not the whole panel: Upload, the storages, Trash and "How to
|
|
94
|
+
* connect" stay — an embed's users still upload and still mount. An embedder
|
|
95
|
+
* who wants no panel at all already has `sideNav: false`.
|
|
96
|
+
*/
|
|
97
|
+
showIdentitySurfaces?: boolean;
|
|
71
98
|
/** RBAC/root state — false hides the write affordances. */
|
|
72
99
|
canWrite?: boolean;
|
|
73
100
|
locale: LocaleCode;
|
|
@@ -75,7 +102,8 @@ const props = defineProps<{
|
|
|
75
102
|
|
|
76
103
|
const emit = defineEmits<{
|
|
77
104
|
(e: 'toggle'): void;
|
|
78
|
-
(e: 'open-view', view: Exclude<NavView, ''>): void;
|
|
105
|
+
(e: 'open-view', view: Exclude<NavView, '' | 'tag'>): void;
|
|
106
|
+
(e: 'open-tag', tag: string): void;
|
|
79
107
|
(e: 'open-storage', name: string): void;
|
|
80
108
|
(e: 'open-root'): void;
|
|
81
109
|
(e: 'upload'): void;
|
|
@@ -95,18 +123,51 @@ const showLabels = computed(() => props.expanded || !!props.narrow);
|
|
|
95
123
|
|
|
96
124
|
const sharedSet = computed(() => new Set(props.sharedStorages ?? []));
|
|
97
125
|
|
|
126
|
+
/** The views that answer "what did *I* do" — dropped for an app token. */
|
|
127
|
+
const IDENTITY_VIEWS = new Set<string>(['recent', 'starred', 'shared']);
|
|
128
|
+
|
|
98
129
|
const views = computed(() => {
|
|
99
|
-
const list: Array<{ key: Exclude<NavView, ''>; label: string }> = [
|
|
130
|
+
const list: Array<{ key: Exclude<NavView, '' | 'tag'>; label: string }> = [
|
|
100
131
|
{ key: 'recent', label: t('sidenav.recent') },
|
|
101
132
|
{ key: 'starred', label: t('sidenav.starred') },
|
|
102
133
|
{ key: 'shared', label: t('sidenav.shared') },
|
|
103
134
|
];
|
|
104
135
|
if (props.trashVisible !== false) list.push({ key: 'trash', label: t('sidenav.trash') });
|
|
136
|
+
// Filtered at the end rather than built conditionally: Trash is shared by
|
|
137
|
+
// everyone and the list keeps growing, so one rule at the bottom beats a
|
|
138
|
+
// condition wrapped around each entry.
|
|
139
|
+
if (props.showIdentitySurfaces === false) return list.filter((v) => !IDENTITY_VIEWS.has(v.key));
|
|
105
140
|
return list;
|
|
106
141
|
});
|
|
107
142
|
|
|
108
143
|
const writable = computed(() => props.canWrite !== false);
|
|
109
144
|
|
|
145
|
+
/* === etiket:t1 — an unbounded list in a fixed panel ====================
|
|
146
|
+
* A user with sixty tags must not push Storages and Connections off the
|
|
147
|
+
* bottom of the panel, and must not be handed sixty identical glyphs on a
|
|
148
|
+
* 56px rail either.
|
|
149
|
+
*
|
|
150
|
+
* expanded / drawer → the first TAG_PEEK, then "Show all (N)". Both states
|
|
151
|
+
* scroll (.fe-sidenav__scroll), so this is about the
|
|
152
|
+
* sections BELOW staying reachable, not about overflow.
|
|
153
|
+
* rail → ONE "Tags" button that opens the panel. Sixty rail
|
|
154
|
+
* icons would be sixty copies of the same glyph with no
|
|
155
|
+
* label — the rail's contract is "every destination one
|
|
156
|
+
* click away", and this keeps it with one click more.
|
|
157
|
+
*/
|
|
158
|
+
const TAG_PEEK = 8;
|
|
159
|
+
const tagsExpanded = ref(false);
|
|
160
|
+
const allTags = computed(() => props.tags ?? []);
|
|
161
|
+
const visibleTags = computed(() =>
|
|
162
|
+
tagsExpanded.value ? allTags.value : allTags.value.slice(0, TAG_PEEK),
|
|
163
|
+
);
|
|
164
|
+
const hiddenTagCount = computed(() => Math.max(0, allTags.value.length - visibleTags.value.length));
|
|
165
|
+
/* The section is rendered as soon as the panel knows there ARE tags, and also
|
|
166
|
+
* once the answer came back empty — an empty section that says why is how a
|
|
167
|
+
* user learns the feature exists at all. It stays hidden only while the first
|
|
168
|
+
* answer is still in flight. */
|
|
169
|
+
const showTags = computed(() => !!props.tagsLoaded || allTags.value.length > 0);
|
|
170
|
+
|
|
110
171
|
const toggleLabel = computed(() =>
|
|
111
172
|
props.narrow
|
|
112
173
|
? t('sidenav.close')
|
|
@@ -278,6 +339,105 @@ const toggleLabel = computed(() =>
|
|
|
278
339
|
</li>
|
|
279
340
|
</ul>
|
|
280
341
|
|
|
342
|
+
<!-- etiket:t1 — Tags. Between the views and the storages because a tag
|
|
343
|
+
IS a view (a listing with no folder behind it), not a place files
|
|
344
|
+
live. On the rail it collapses to one button that opens the panel:
|
|
345
|
+
sixty tags would otherwise be sixty copies of one glyph with no
|
|
346
|
+
label, and the rail's promise is that every destination stays one
|
|
347
|
+
click away. -->
|
|
348
|
+
<div v-if="showTags" class="fe-sidenav__section">
|
|
349
|
+
<template v-if="showLabels">
|
|
350
|
+
<p class="fe-sidenav__heading">{{ t('sidenav.tags') }}</p>
|
|
351
|
+
<ul class="fe-sidenav__group" :aria-label="t('sidenav.tags')">
|
|
352
|
+
<li v-for="tag in visibleTags" :key="tag">
|
|
353
|
+
<button
|
|
354
|
+
type="button"
|
|
355
|
+
class="fe-sidenav__item fe-sidenav__item--tag"
|
|
356
|
+
:class="{ 'is-active': activeView === 'tag' && activeTag === tag }"
|
|
357
|
+
:aria-current="activeView === 'tag' && activeTag === tag ? 'page' : undefined"
|
|
358
|
+
:title="tag"
|
|
359
|
+
:aria-label="tag"
|
|
360
|
+
:data-testid="`sidenav-tag-${tag}`"
|
|
361
|
+
@click="emit('open-tag', tag)"
|
|
362
|
+
>
|
|
363
|
+
<svg
|
|
364
|
+
class="fe-ficon"
|
|
365
|
+
viewBox="0 0 24 24"
|
|
366
|
+
fill="none"
|
|
367
|
+
stroke="currentColor"
|
|
368
|
+
stroke-width="1.8"
|
|
369
|
+
stroke-linecap="round"
|
|
370
|
+
stroke-linejoin="round"
|
|
371
|
+
aria-hidden="true"
|
|
372
|
+
focusable="false"
|
|
373
|
+
>
|
|
374
|
+
<path d="M4 4.5h7l9 9-6.5 6.5-9-9z" />
|
|
375
|
+
<circle cx="8" cy="8.5" r="1.4" />
|
|
376
|
+
</svg>
|
|
377
|
+
<span class="fe-sidenav__text">{{ tag }}</span>
|
|
378
|
+
</button>
|
|
379
|
+
</li>
|
|
380
|
+
<!-- Nothing tagged yet: the section stays, and says how tags get
|
|
381
|
+
made. A section that only exists once you already know the
|
|
382
|
+
feature teaches nobody. -->
|
|
383
|
+
<li v-if="allTags.length === 0">
|
|
384
|
+
<p class="fe-sidenav__hint">{{ t('sidenav.tags.empty') }}</p>
|
|
385
|
+
</li>
|
|
386
|
+
<li v-if="hiddenTagCount > 0">
|
|
387
|
+
<button
|
|
388
|
+
type="button"
|
|
389
|
+
class="fe-sidenav__more"
|
|
390
|
+
data-testid="sidenav-tags-more"
|
|
391
|
+
@click="tagsExpanded = true"
|
|
392
|
+
>
|
|
393
|
+
{{ t('sidenav.tags.more', { count: hiddenTagCount }) }}
|
|
394
|
+
</button>
|
|
395
|
+
</li>
|
|
396
|
+
<li v-else-if="tagsExpanded && allTags.length > 8">
|
|
397
|
+
<button
|
|
398
|
+
type="button"
|
|
399
|
+
class="fe-sidenav__more"
|
|
400
|
+
data-testid="sidenav-tags-less"
|
|
401
|
+
@click="tagsExpanded = false"
|
|
402
|
+
>
|
|
403
|
+
{{ t('sidenav.tags.less') }}
|
|
404
|
+
</button>
|
|
405
|
+
</li>
|
|
406
|
+
</ul>
|
|
407
|
+
</template>
|
|
408
|
+
<template v-else>
|
|
409
|
+
<hr class="fe-sidenav__rule" aria-hidden="true" />
|
|
410
|
+
<ul class="fe-sidenav__group" :aria-label="t('sidenav.tags')">
|
|
411
|
+
<li>
|
|
412
|
+
<button
|
|
413
|
+
type="button"
|
|
414
|
+
class="fe-sidenav__item"
|
|
415
|
+
:class="{ 'is-active': activeView === 'tag' }"
|
|
416
|
+
:title="t('sidenav.tags')"
|
|
417
|
+
:aria-label="t('sidenav.tags')"
|
|
418
|
+
data-testid="sidenav-tags-rail"
|
|
419
|
+
@click="emit('toggle')"
|
|
420
|
+
>
|
|
421
|
+
<svg
|
|
422
|
+
class="fe-ficon"
|
|
423
|
+
viewBox="0 0 24 24"
|
|
424
|
+
fill="none"
|
|
425
|
+
stroke="currentColor"
|
|
426
|
+
stroke-width="1.8"
|
|
427
|
+
stroke-linecap="round"
|
|
428
|
+
stroke-linejoin="round"
|
|
429
|
+
aria-hidden="true"
|
|
430
|
+
focusable="false"
|
|
431
|
+
>
|
|
432
|
+
<path d="M4 4.5h7l9 9-6.5 6.5-9-9z" />
|
|
433
|
+
<circle cx="8" cy="8.5" r="1.4" />
|
|
434
|
+
</svg>
|
|
435
|
+
</button>
|
|
436
|
+
</li>
|
|
437
|
+
</ul>
|
|
438
|
+
</template>
|
|
439
|
+
</div>
|
|
440
|
+
|
|
281
441
|
<div v-if="storages.length" class="fe-sidenav__section">
|
|
282
442
|
<p v-if="showLabels" class="fe-sidenav__heading">{{ t('sidenav.storages') }}</p>
|
|
283
443
|
<hr v-else class="fe-sidenav__rule" aria-hidden="true" />
|
|
@@ -375,7 +535,10 @@ const toggleLabel = computed(() =>
|
|
|
375
535
|
<span v-if="showLabels" class="fe-sidenav__text">{{ t('sidenav.connect') }}</span>
|
|
376
536
|
</button>
|
|
377
537
|
</li>
|
|
378
|
-
|
|
538
|
+
<!-- API keys is the person half of this section: "How to connect"
|
|
539
|
+
stays for an app token (mount instructions are not identity),
|
|
540
|
+
the keys go. -->
|
|
541
|
+
<li v-if="showIdentitySurfaces !== false">
|
|
379
542
|
<button
|
|
380
543
|
type="button"
|
|
381
544
|
class="fe-sidenav__item"
|
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
* `POST /api/files/manager/star`). Optimistic update — flips the local
|
|
7
7
|
* state immediately and rolls back on API error.
|
|
8
8
|
*/
|
|
9
|
-
import { ref, watch } from 'vue';
|
|
9
|
+
import { ref, watch, computed } from 'vue';
|
|
10
|
+
import { setNodeStarred } from '../lib/star';
|
|
11
|
+
import { useLocale } from '../composables/useLocale';
|
|
12
|
+
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
10
13
|
|
|
11
14
|
const props = defineProps<{
|
|
12
15
|
starred: boolean;
|
|
@@ -21,6 +24,17 @@ const props = defineProps<{
|
|
|
21
24
|
authCredentials?: RequestCredentials;
|
|
22
25
|
/** Compact mode for grid view (no label, just the icon). */
|
|
23
26
|
compact?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Card mode — the same button sitting ON a grid/gallery tile instead of in
|
|
29
|
+
* a list cell: a round translucent chip in the tile's corner. It is the SAME
|
|
30
|
+
* component, deliberately: a card star written separately is a second
|
|
31
|
+
* starring path, and the two drift the first time one of them is fixed.
|
|
32
|
+
*/
|
|
33
|
+
card?: boolean;
|
|
34
|
+
/** Locale for the title/label. ⚠ The strings used to be hardcoded English
|
|
35
|
+
* ("Star"/"Unstar"), which is a Turkish user's only untranslated control in
|
|
36
|
+
* the row. */
|
|
37
|
+
locale?: LocaleCode;
|
|
24
38
|
}>();
|
|
25
39
|
|
|
26
40
|
const emit = defineEmits<{
|
|
@@ -31,22 +45,18 @@ const emit = defineEmits<{
|
|
|
31
45
|
const local = ref(props.starred);
|
|
32
46
|
watch(() => props.starred, (v) => { local.value = v; });
|
|
33
47
|
|
|
48
|
+
const { t } = useLocale(() => props.locale ?? 'tr');
|
|
49
|
+
const label = computed(() => t(local.value ? 'ctx.unstar' : 'ctx.star'));
|
|
50
|
+
|
|
34
51
|
async function toggle() {
|
|
35
52
|
const next = !local.value;
|
|
36
53
|
local.value = next; // optimistic
|
|
37
54
|
try {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const base = props.apiBase ?? '';
|
|
43
|
-
const res = await fetch(`${base}/api/files/manager/star`, {
|
|
44
|
-
method: 'POST',
|
|
45
|
-
headers,
|
|
46
|
-
credentials: props.authCredentials ?? 'same-origin',
|
|
47
|
-
body: JSON.stringify({ node_id: props.nodeId, starred: next }),
|
|
55
|
+
await setNodeStarred(props.nodeId, next, {
|
|
56
|
+
apiBase: props.apiBase,
|
|
57
|
+
authHeaders: props.authHeaders,
|
|
58
|
+
authCredentials: props.authCredentials,
|
|
48
59
|
});
|
|
49
|
-
if (!res.ok) throw new Error(`star toggle failed: ${res.status}`);
|
|
50
60
|
emit('change', next);
|
|
51
61
|
} catch (err) {
|
|
52
62
|
local.value = !next; // rollback
|
|
@@ -59,9 +69,11 @@ async function toggle() {
|
|
|
59
69
|
<button
|
|
60
70
|
type="button"
|
|
61
71
|
class="filex-star-btn"
|
|
62
|
-
:class="{ 'is-starred': local, 'is-compact': compact }"
|
|
72
|
+
:class="{ 'is-starred': local, 'is-compact': compact, 'is-card': card }"
|
|
63
73
|
:aria-pressed="local"
|
|
64
|
-
:title="
|
|
74
|
+
:title="label"
|
|
75
|
+
:aria-label="label"
|
|
76
|
+
data-testid="star-toggle"
|
|
65
77
|
@click.stop="toggle"
|
|
66
78
|
>
|
|
67
79
|
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
|
|
@@ -73,7 +85,7 @@ async function toggle() {
|
|
|
73
85
|
d="M12 2.5l3.09 6.26 6.91 1-5 4.87 1.18 6.87L12 18.27l-6.18 3.23L7 14.63 2 9.76l6.91-1z"
|
|
74
86
|
/>
|
|
75
87
|
</svg>
|
|
76
|
-
<span v-if="!compact" class="filex-star-label">{{
|
|
88
|
+
<span v-if="!compact" class="filex-star-label">{{ label }}</span>
|
|
77
89
|
</button>
|
|
78
90
|
</template>
|
|
79
91
|
|
|
@@ -226,6 +226,9 @@ export function resolveEndpoints(config: ExplorerConfig): EndpointMap {
|
|
|
226
226
|
// filex trash: list soft-deleted nodes + restore one by node id.
|
|
227
227
|
trashList: derive(config.trashList, '/api/files/manager/trash'),
|
|
228
228
|
trashRestore: derive(config.trashRestore, '/api/files/manager/restore'),
|
|
229
|
+
/* wiring:e2 — escrow proof-of-possession, then the owner is told. */
|
|
230
|
+
e2eEscrowChallenge: derive(config.e2eEscrowChallenge, '/api/files/e2e/escrow/challenge'),
|
|
231
|
+
e2eEscrowUsed: derive(config.e2eEscrowUsed, '/api/files/e2e/escrow/used'),
|
|
229
232
|
};
|
|
230
233
|
}
|
|
231
234
|
|
|
@@ -687,6 +690,38 @@ export function useFileApi(config: ExplorerConfig) {
|
|
|
687
690
|
return jsonFetch<Capabilities>(endpoints.capabilities);
|
|
688
691
|
}
|
|
689
692
|
|
|
693
|
+
/* wiring:e2 — escrow use is announced, not merely performed.
|
|
694
|
+
*
|
|
695
|
+
* Ask the server for a nonce sealed to the escrow public key; only the
|
|
696
|
+
* holder of the private half can read it back. Returning it is what earns
|
|
697
|
+
* the notification to the folder's owner — a bare "I used escrow" POST
|
|
698
|
+
* would be a string anyone could send.
|
|
699
|
+
*
|
|
700
|
+
* ⚠ This is an announcement, not a gate. An operator holding the private
|
|
701
|
+
* key can decrypt the folder offline with a script and never come here.
|
|
702
|
+
* docs/E2E-ENCRYPTION.md says so plainly and must keep saying so. */
|
|
703
|
+
async function e2eEscrowChallenge(
|
|
704
|
+
path: string,
|
|
705
|
+
): Promise<{ id: string; challenge: string; kid: string }> {
|
|
706
|
+
if (!endpoints.e2eEscrowChallenge) throw new Error('e2e escrow endpoint not configured');
|
|
707
|
+
return jsonFetch(endpoints.e2eEscrowChallenge, {
|
|
708
|
+
method: 'POST',
|
|
709
|
+
body: JSON.stringify({ path }),
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
async function e2eEscrowUsed(payload: {
|
|
714
|
+
path: string;
|
|
715
|
+
id: string;
|
|
716
|
+
nonce: string;
|
|
717
|
+
}): Promise<{ ok: boolean; notified: boolean }> {
|
|
718
|
+
if (!endpoints.e2eEscrowUsed) throw new Error('e2e escrow endpoint not configured');
|
|
719
|
+
return jsonFetch(endpoints.e2eEscrowUsed, {
|
|
720
|
+
method: 'POST',
|
|
721
|
+
body: JSON.stringify(payload),
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
|
|
690
725
|
async function createShare(payload: {
|
|
691
726
|
path: string;
|
|
692
727
|
password?: boolean;
|
|
@@ -869,6 +904,9 @@ export function useFileApi(config: ExplorerConfig) {
|
|
|
869
904
|
// Peripheral
|
|
870
905
|
limits,
|
|
871
906
|
capabilities,
|
|
907
|
+
/* wiring:e2 */
|
|
908
|
+
e2eEscrowChallenge,
|
|
909
|
+
e2eEscrowUsed,
|
|
872
910
|
createShare,
|
|
873
911
|
listShares,
|
|
874
912
|
revokeShare,
|
|
@@ -38,6 +38,8 @@ export interface ShortcutHandlers {
|
|
|
38
38
|
onShowHelp?: () => void; // ? (Shift+/ on most layouts)
|
|
39
39
|
onToggleInspector?: () => void; // i (koru:k1 details panel)
|
|
40
40
|
onToggleHidden?: () => void; // Ctrl+Shift+. — dot-file visibility
|
|
41
|
+
/* yildiz:s1 */
|
|
42
|
+
onStar?: () => void; // S — star / unstar the selection
|
|
41
43
|
onQuickLook?: () => void; // Space (wiring:c2 quick-look overlay)
|
|
42
44
|
/* wiring:d1 — tab strip actions */
|
|
43
45
|
onTabNew?: () => void; // Ctrl+T
|
|
@@ -87,6 +89,10 @@ export const SHORTCUT_ACTIONS: ShortcutActionDef[] = [
|
|
|
87
89
|
{ id: 'inspector', defaultCombo: 'I', labelKey: 'shortcuts.inspector', groupKey: 'shortcuts.group.nav' } /* koru:k1 */,
|
|
88
90
|
{ id: 'help', defaultCombo: '?', labelKey: 'shortcuts.help', groupKey: 'shortcuts.group.nav' },
|
|
89
91
|
{ id: 'toggle-hidden', defaultCombo: 'Ctrl+Shift+.', labelKey: 'shortcuts.toggle_hidden', groupKey: 'shortcuts.group.nav' },
|
|
92
|
+
/* yildiz:s1 — starring is an action, so it belongs where the other verbs
|
|
93
|
+
* are. A bare letter like the inspector's `I`; the handler ignores events
|
|
94
|
+
* from form controls, so it never eats a keystroke meant for a filename. */
|
|
95
|
+
{ id: 'star', defaultCombo: 'S', labelKey: 'shortcuts.star', groupKey: 'shortcuts.group.file' },
|
|
90
96
|
// Selection
|
|
91
97
|
{ id: 'select-all', defaultCombo: 'Ctrl+A', labelKey: 'shortcuts.select_all', groupKey: 'shortcuts.group.selection' },
|
|
92
98
|
// File operations
|
|
@@ -115,6 +121,7 @@ const HANDLER_KEY: Record<string, keyof ShortcutHandlers> = {
|
|
|
115
121
|
close: 'onClose',
|
|
116
122
|
inspector: 'onToggleInspector',
|
|
117
123
|
'toggle-hidden': 'onToggleHidden',
|
|
124
|
+
star: 'onStar' /* yildiz:s1 */,
|
|
118
125
|
help: 'onShowHelp',
|
|
119
126
|
'select-all': 'onSelectAll',
|
|
120
127
|
rename: 'onRename',
|
package/src/index.ts
CHANGED
|
@@ -155,21 +155,47 @@ export {
|
|
|
155
155
|
E2E_MARKER_NAME,
|
|
156
156
|
E2E_MAGIC,
|
|
157
157
|
E2E_VERSION,
|
|
158
|
+
E2E_MARKER_VERSION,
|
|
158
159
|
E2E_DEFAULT_ITERATIONS,
|
|
159
160
|
E2E_MAX_FILE_BYTES,
|
|
160
161
|
E2E_MIN_PASSWORD_LEN,
|
|
162
|
+
E2E_RECOVERY_KEY_BYTES,
|
|
163
|
+
E2E_ESCROW_ALG,
|
|
161
164
|
E2eDecryptError,
|
|
162
165
|
deriveKek,
|
|
163
166
|
createMarker,
|
|
167
|
+
createEncryptedFolder,
|
|
168
|
+
upgradeMarkerV1,
|
|
164
169
|
parseMarker,
|
|
165
170
|
verifyPassword,
|
|
171
|
+
unlockWithPassword,
|
|
172
|
+
unlockWithRecoveryKey,
|
|
173
|
+
unlockWithEscrowKey,
|
|
174
|
+
markerHasRecovery,
|
|
175
|
+
markerHasEscrow,
|
|
176
|
+
generateRecoveryKey,
|
|
177
|
+
formatRecoveryKey,
|
|
178
|
+
parseRecoveryKey,
|
|
179
|
+
importEscrowPublicKey,
|
|
180
|
+
importEscrowPrivateKey,
|
|
181
|
+
escrowKeyId,
|
|
166
182
|
hasMagic,
|
|
167
183
|
encryptFile,
|
|
168
184
|
decryptFile,
|
|
169
185
|
createKeyRing,
|
|
170
186
|
} from './lib/e2ecrypto';
|
|
171
|
-
export type {
|
|
187
|
+
export type {
|
|
188
|
+
E2eMarker,
|
|
189
|
+
E2eKeyRing,
|
|
190
|
+
E2eFmkMode,
|
|
191
|
+
E2eRecoverySlot,
|
|
192
|
+
E2eEscrowSlot,
|
|
193
|
+
CreateFolderOptions,
|
|
194
|
+
CreatedFolder,
|
|
195
|
+
} from './lib/e2ecrypto';
|
|
172
196
|
export { default as EncryptedFolderModal } from './components/EncryptedFolderModal.vue';
|
|
197
|
+
export { default as RecoveryKeyModal } from './components/RecoveryKeyModal.vue';
|
|
198
|
+
export { default as E2eRecoveryUnlockModal } from './components/E2eRecoveryUnlockModal.vue';
|
|
173
199
|
/* /wiring:e2 */
|
|
174
200
|
|
|
175
201
|
/* ── connections ────────────────────────────────────────────────────
|