@brftech/filex-core 0.31.0 → 0.33.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.
Files changed (62) hide show
  1. package/README.md +26 -2
  2. package/dist/{CsvViewer-DxujFEM3.js → CsvViewer-CqWeV8VO.js} +22 -22
  3. package/dist/CsvViewer-CqWeV8VO.js.map +1 -0
  4. package/dist/{DrawioViewer-DCvWnX2t.js → DrawioViewer-BNALOB04.js} +16 -16
  5. package/dist/DrawioViewer-BNALOB04.js.map +1 -0
  6. package/dist/filex-core.js +9346 -8069
  7. package/dist/filex-core.js.map +1 -1
  8. package/dist/filex-core.umd.cjs +49 -49
  9. package/dist/filex-core.umd.cjs.map +1 -1
  10. package/dist/index.d.ts +235 -16
  11. package/dist/style.css +1 -1
  12. package/package.json +1 -1
  13. package/src/FileExplorer.vue +776 -246
  14. package/src/components/CommandPalette.vue +21 -5
  15. package/src/components/ConnectionsPanel.vue +2 -1
  16. package/src/components/E2eRecoveryUnlockModal.vue +29 -4
  17. package/src/components/FilterBar.vue +244 -0
  18. package/src/components/GalleryView.vue +12 -1
  19. package/src/components/GridView.vue +48 -5
  20. package/src/components/InspectorPanel.vue +231 -8
  21. package/src/components/ListView.vue +12 -3
  22. package/src/components/NFSExportsPanel.vue +2 -1
  23. package/src/components/PendingOpsTray.vue +1 -1
  24. package/src/components/PresenceBar.vue +3 -2
  25. package/src/components/S3KeysPanel.vue +2 -1
  26. package/src/components/SSHKeysPanel.vue +2 -1
  27. package/src/components/SecondaryPane.vue +28 -13
  28. package/src/components/SideNav.vue +161 -1
  29. package/src/components/StarButton.vue +2 -1
  30. package/src/components/TabBar.vue +2 -2
  31. package/src/components/ThemeGallery.vue +1 -1
  32. package/src/components/TokensPanel.vue +2 -1
  33. package/src/components/Toolbar.vue +244 -58
  34. package/src/components/ViewSwitcher.vue +81 -0
  35. package/src/composables/useFileApi.ts +49 -2
  36. package/src/composables/useLocale.ts +1 -1
  37. package/src/composables/useOperations.ts +3 -3
  38. package/src/composables/usePendingOps.ts +1 -1
  39. package/src/composables/useTabs.ts +1 -1
  40. package/src/composables/useUploadChunked.ts +39 -7
  41. package/src/index.ts +8 -2
  42. package/src/lib/e2ecrypto.ts +191 -3
  43. package/src/lib/fileFilters.ts +143 -0
  44. package/src/lib/listing.ts +47 -0
  45. package/src/lib/themes.ts +7 -7
  46. package/src/lib/transfer.ts +6 -6
  47. package/src/locales/en.ts +115 -0
  48. package/src/locales/index.ts +1 -0
  49. package/src/locales/resolve.ts +66 -0
  50. package/src/locales/tr.ts +116 -0
  51. package/src/modals/ConvertModal.vue +14 -12
  52. package/src/modals/NewFolderModal.vue +1 -1
  53. package/src/modals/PermissionsModal.vue +30 -13
  54. package/src/modals/PreviewModal.vue +9 -9
  55. package/src/styles/base.css +520 -26
  56. package/src/styles/variables.css +1 -1
  57. package/src/types/ExplorerConfig.ts +18 -2
  58. package/src/types/FileNode.ts +1 -1
  59. package/src/viewers/CsvViewer.vue +4 -4
  60. package/src/viewers/DrawioViewer.vue +2 -2
  61. package/dist/CsvViewer-DxujFEM3.js.map +0 -1
  62. package/dist/DrawioViewer-DCvWnX2t.js.map +0 -1
@@ -24,7 +24,7 @@
24
24
  * The panel is mounted with v-if by the host — closed state leaves zero DOM.
25
25
  */
26
26
  import { computed, ref, watch } from 'vue';
27
- import type { FileApi, NodeVersion } from '../composables/useFileApi';
27
+ import type { FileApi, Grant, NodeVersion } from '../composables/useFileApi';
28
28
  import type { FileNode, ShareInfo } from '../types/FileNode';
29
29
  import type { LocaleCode } from '../types/ExplorerConfig';
30
30
  import { useLocale } from '../composables/useLocale';
@@ -45,10 +45,28 @@ const props = defineProps<{
45
45
  narrow?: boolean;
46
46
  /** Authenticated thumbnail resolver (useThumbs.src). Optional. */
47
47
  thumbSrc?: (n: FileNode) => string | null;
48
+ /* === surucu:d1 — the Drive shell's details panel ===================== */
49
+ /**
50
+ * Split the panel into **Details** and **Activity** tabs, and draw the two
51
+ * sections the mockups add: who has access, and the share link with a
52
+ * "Create link" button. `uiProfile: 'drive'` turns it on; absent/false is
53
+ * the flat scroll of sections this panel has always been, with no extra
54
+ * request made.
55
+ *
56
+ * ⚠ "Activity" is version history + comments, and nothing else, because
57
+ * nothing else exists: there is no per-file activity endpoint. The audit log
58
+ * is admin-only, has no path filter, and stores an EMPTY `target_id` for
59
+ * every file action (auth/audit_middleware.go) — so "Ayşe renamed this on
60
+ * Tuesday" cannot be answered by this server, and a timeline that made it up
61
+ * would be worse than the two real feeds.
62
+ */
63
+ tabs?: boolean;
48
64
  }>();
49
65
 
50
66
  const emit = defineEmits<{
51
67
  (e: 'close'): void;
68
+ /** surucu:d1 — a share link was minted from the panel. */
69
+ (e: 'share-created', payload: { path: string; url: string }): void;
52
70
  (e: 'manage-permissions', node: FileNode): void;
53
71
  (e: 'toast', message: string): void;
54
72
  /** Fired after a successful restore/snapshot so the host can reload. */
@@ -239,6 +257,107 @@ async function takeSnapshot(): Promise<void> {
239
257
  }
240
258
  }
241
259
 
260
+ /* === surucu:d1 — tabs, people with access, the share-link row ==========
261
+ *
262
+ * The tab strip is presentation only: every section below is the same section,
263
+ * rendered under Details or under Activity. Nothing is fetched twice, and
264
+ * nothing new is fetched at all unless `tabs` is on.
265
+ */
266
+ type InspectorTab = 'details' | 'activity';
267
+ const tab = ref<InspectorTab>('details');
268
+
269
+ /** People with access — `GET /api/files/permissions?path=…`.
270
+ *
271
+ * ⚠ Owner-gated on the server (handlers/grants.go `requireOwner`), so an
272
+ * editor or a viewer gets a 403 for a file they can perfectly well see. That
273
+ * is the server's call, not something to route around: the section hides,
274
+ * exactly the way the shares list already hides for the same class of caller.
275
+ * What it must NOT do is render an empty "People with access" and let the
276
+ * reader conclude that nobody has any.
277
+ */
278
+ const peopleState = ref<SectionState>('hidden');
279
+ const people = ref<Grant[]>([]);
280
+ let peopleSeq = 0;
281
+
282
+ async function loadPeople(): Promise<void> {
283
+ const seq = ++peopleSeq;
284
+ const node = single.value;
285
+ if (!props.tabs || !node) {
286
+ peopleState.value = 'hidden';
287
+ people.value = [];
288
+ return;
289
+ }
290
+ peopleState.value = 'loading';
291
+ try {
292
+ const r = await props.api.listPermissions(node.path);
293
+ if (seq !== peopleSeq) return;
294
+ // RBAC off on this storage → there are no grants, and a section reading
295
+ // "nobody" would misdescribe a drive where everyone can already read it.
296
+ if (!r.storage_rbac) {
297
+ peopleState.value = 'hidden';
298
+ people.value = [];
299
+ return;
300
+ }
301
+ people.value = [...(r.direct ?? []), ...(r.inherited ?? [])];
302
+ peopleState.value = 'ok';
303
+ } catch {
304
+ if (seq !== peopleSeq) return;
305
+ people.value = [];
306
+ peopleState.value = 'hidden';
307
+ }
308
+ }
309
+
310
+ function personName(g: Grant): string {
311
+ return g.user_display_name || g.user_email || `#${g.user_id}`;
312
+ }
313
+
314
+ function personInitial(g: Grant): string {
315
+ const n = personName(g).trim();
316
+ return n ? n[0].toUpperCase() : '?';
317
+ }
318
+
319
+ /** The share-link row: the first live link, or the Create button. */
320
+ const primaryShare = computed(() => (shares.value.length ? shares.value[0] : null));
321
+ const shareBusy = ref(false);
322
+
323
+ async function createLink(): Promise<void> {
324
+ const node = single.value;
325
+ if (!node || shareBusy.value) return;
326
+ shareBusy.value = true;
327
+ try {
328
+ const r = await props.api.createShare({ path: node.path });
329
+ const url = r.share?.url ?? '';
330
+ // Re-read the list rather than push the response into it: the LIST
331
+ // endpoint is what a later copy or revoke acts on, and its `uuid` is the
332
+ // numeric id the DELETE route wants — not the token the create response
333
+ // carries under that name.
334
+ const { shares: list } = await props.api.listShares(node.path);
335
+ shares.value = Array.isArray(list) ? list : [];
336
+ sharesState.value = 'ok';
337
+ if (url) {
338
+ emit('share-created', { path: node.path, url });
339
+ await copyText(url);
340
+ }
341
+ } catch (err) {
342
+ emit('toast', (err as Error).message);
343
+ } finally {
344
+ shareBusy.value = false;
345
+ }
346
+ }
347
+
348
+ watch(
349
+ () => [props.nodes.map((n) => n.path).join('|'), props.tabs] as const,
350
+ () => void loadPeople(),
351
+ { immediate: true },
352
+ );
353
+
354
+ /* Versions and comments are the two real feeds; when a selection has neither,
355
+ * the Activity tab says so instead of showing two empty headings. */
356
+ const activityUsable = computed(
357
+ () => versionsState.value !== 'hidden' || commentsState.value !== 'hidden',
358
+ );
359
+ /* === /surucu:d1 === */
360
+
242
361
  watch(
243
362
  () => props.nodes.map((n) => n.path).join(''),
244
363
  () => void refresh(),
@@ -372,9 +491,32 @@ watch(
372
491
  >×</button>
373
492
  </header>
374
493
 
494
+ <!-- surucu:d1 — Details / Activity. Rendered only in the drive shell; the
495
+ classic panel keeps its single flat scroll of sections. -->
496
+ <div v-if="tabs" class="fe-inspector__tabs" role="tablist" :aria-label="t('inspector.title')">
497
+ <button
498
+ type="button"
499
+ class="fe-inspector__tab"
500
+ :class="{ 'is-active': tab === 'details' }"
501
+ role="tab"
502
+ :aria-selected="tab === 'details'"
503
+ data-testid="inspector-tab-details"
504
+ @click="tab = 'details'"
505
+ >{{ t('inspector.tab.details') }}</button>
506
+ <button
507
+ type="button"
508
+ class="fe-inspector__tab"
509
+ :class="{ 'is-active': tab === 'activity' }"
510
+ role="tab"
511
+ :aria-selected="tab === 'activity'"
512
+ data-testid="inspector-tab-activity"
513
+ @click="tab = 'activity'"
514
+ >{{ t('inspector.tab.activity') }}</button>
515
+ </div>
516
+
375
517
  <div class="fe-inspector__scroll">
376
518
  <!-- ══ Genel ══ -->
377
- <section class="fe-inspector__section">
519
+ <section v-if="!tabs || tab === 'details'" class="fe-inspector__section">
378
520
  <h3 class="fe-inspector__heading">{{ t('inspector.section.general') }}</h3>
379
521
 
380
522
  <!-- Multi selection → summary -->
@@ -457,7 +599,7 @@ watch(
457
599
 
458
600
  <!-- ══ Sürümler ══ -->
459
601
  <section
460
- v-if="versionsState === 'ok' || versionsState === 'error'"
602
+ v-if="(versionsState === 'ok' || versionsState === 'error') && (!tabs || tab === 'activity')"
461
603
  class="fe-inspector__section"
462
604
  >
463
605
  <h3 class="fe-inspector__heading">{{ t('inspector.section.versions') }}</h3>
@@ -519,7 +661,21 @@ watch(
519
661
  </section>
520
662
 
521
663
  <!-- ══ İzinler ══ -->
522
- <section v-if="single && effectivePerm" class="fe-inspector__section">
664
+ <!-- ⚠ surucu:d1 — stands down when "People with access" is on screen. The
665
+ two say the same thing there (your own row is in the roster, with the
666
+ same level) and each carries its own button into the SAME modal —
667
+ two doors into one room, six lines apart. It stays for every other
668
+ case, which is exactly the case where People is hidden: RBAC off on
669
+ the storage, or the server refusing the grant list to a non-owner. -->
670
+ <section
671
+ v-if="
672
+ single &&
673
+ effectivePerm &&
674
+ (!tabs || tab === 'details') &&
675
+ !(tabs && peopleState === 'ok')
676
+ "
677
+ class="fe-inspector__section"
678
+ >
523
679
  <h3 class="fe-inspector__heading">{{ t('inspector.section.permissions') }}</h3>
524
680
  <div class="fe-inspector__permrow">
525
681
  <span
@@ -535,10 +691,62 @@ watch(
535
691
  </div>
536
692
  </section>
537
693
 
694
+ <!-- ══ surucu:d1 — People with access ══
695
+ Real grants from `GET /api/files/permissions`; hidden entirely when
696
+ the caller is not an owner (403) or the storage has RBAC off, rather
697
+ than drawn empty. -->
698
+ <section
699
+ v-if="tabs && tab === 'details' && peopleState === 'ok'"
700
+ class="fe-inspector__section"
701
+ data-testid="inspector-people"
702
+ >
703
+ <h3 class="fe-inspector__heading">{{ t('inspector.people') }}</h3>
704
+ <p v-if="people.length === 0" class="fe-inspector__empty">
705
+ {{ t('inspector.people.empty') }}
706
+ </p>
707
+ <ul v-else class="fe-inspector__people">
708
+ <li v-for="g in people" :key="`${g.id}-${g.user_id}`" class="fe-inspector__person">
709
+ <span class="fe-inspector__avatar" aria-hidden="true">{{ personInitial(g) }}</span>
710
+ <span class="fe-inspector__person-main">
711
+ <span class="fe-inspector__person-name" :title="g.user_email">{{ personName(g) }}</span>
712
+ <span class="fe-inspector__person-sub">
713
+ {{ permLabel(g.level) }}<template v-if="g.inherited"> · {{ t('inspector.people.inherited') }}</template>
714
+ </span>
715
+ </span>
716
+ </li>
717
+ </ul>
718
+ <button
719
+ v-if="canManagePerms && single"
720
+ type="button"
721
+ class="fe-btn fe-btn--sm"
722
+ @click="emit('manage-permissions', single)"
723
+ >{{ t('inspector.people.manage') }}</button>
724
+ </section>
725
+
538
726
  <!-- ══ Paylaşımlar ══ -->
539
- <section v-if="sharesState === 'ok'" class="fe-inspector__section">
540
- <h3 class="fe-inspector__heading">{{ t('inspector.section.shares') }}</h3>
541
- <p v-if="shares.length === 0" class="fe-inspector__empty">
727
+ <section
728
+ v-if="sharesState === 'ok' && (!tabs || tab === 'details')"
729
+ class="fe-inspector__section"
730
+ data-testid="inspector-shares"
731
+ >
732
+ <h3 class="fe-inspector__heading">
733
+ {{ tabs ? t('inspector.link') : t('inspector.section.shares') }}
734
+ </h3>
735
+ <!-- surucu:d1 — the mockup's link row: what the state IS, and the one
736
+ button that changes it. Only when there is no link yet; the list
737
+ below is what an item that HAS links has always shown. -->
738
+ <div v-if="tabs && shares.length === 0" class="fe-inspector__linkrow">
739
+ <span class="fe-inspector__linkicon" aria-hidden="true">🔗</span>
740
+ <span class="fe-inspector__linknone">{{ t('inspector.link.none') }}</span>
741
+ <button
742
+ type="button"
743
+ class="fe-btn fe-btn--sm"
744
+ :disabled="shareBusy || !single"
745
+ data-testid="inspector-create-link"
746
+ @click="createLink"
747
+ >{{ t('inspector.link.create') }}</button>
748
+ </div>
749
+ <p v-else-if="shares.length === 0" class="fe-inspector__empty">
542
750
  {{ t('inspector.shares.empty') }}
543
751
  </p>
544
752
  <ul v-else class="fe-inspector__shares">
@@ -561,7 +769,7 @@ watch(
561
769
 
562
770
  <!-- ══ calisma:d3 — Yorumlar ══ -->
563
771
  <section
564
- v-if="commentsState === 'ok' || commentsState === 'error'"
772
+ v-if="(commentsState === 'ok' || commentsState === 'error') && (!tabs || tab === 'activity')"
565
773
  class="fe-inspector__section"
566
774
  >
567
775
  <h3 class="fe-inspector__heading">
@@ -620,6 +828,21 @@ watch(
620
828
  </template>
621
829
  </section>
622
830
  <!-- ══ /calisma:d3 ══ -->
831
+
832
+ <!-- surucu:d1 — the Activity tab with nothing behind it. It says which
833
+ two feeds fill it, because there is no third one to wait for: this
834
+ server keeps no per-file audit trail (target_id is empty for every
835
+ file action, and the log is admin-only). -->
836
+ <section
837
+ v-if="tabs && tab === 'activity' && !activityUsable"
838
+ class="fe-inspector__section"
839
+ data-testid="inspector-activity-empty"
840
+ >
841
+ <p class="fe-inspector__empty">
842
+ {{ single ? t('inspector.activity.empty') : t('inspector.activity.select') }}
843
+ </p>
844
+ <p v-if="single" class="fe-inspector__hint">{{ t('inspector.activity.hint') }}</p>
845
+ </section>
623
846
  </div>
624
847
  </aside>
625
848
  </template>
@@ -33,6 +33,15 @@ const props = defineProps<{
33
33
  /** Set of node IDs flagged starred by the user — render an inline
34
34
  * filled star indicator and let the user toggle it from the row. */
35
35
  starredIds?: Set<number>;
36
+ /**
37
+ * Offer the star affordance at all. Follows the Starred view: when the panel
38
+ * leaves that view out — a shared app token has no single person behind it,
39
+ * so "your starred files" is one list shown to strangers — offering to star
40
+ * something is offering to write into that same shared list. Owner's call,
41
+ * 2026-09-05, verbatim (translated from Turkish): "if the starred place is
42
+ * not visible, then star/unstar should not be visible either".
43
+ */
44
+ starEnabled?: boolean;
36
45
  /** Backend base URL + auth header builder forwarded to StarButton
37
46
  * so it can POST /api/files/manager/star on click. Optional —
38
47
  * embedders without auth wire-up pass nothing and the star column
@@ -145,7 +154,7 @@ function cancelPress() {
145
154
  function specialEmojiFor(n: FileNode): string | null {
146
155
  if (n.basename === '.trash') return '🗑';
147
156
  if (n.mime_type === 'inode/storage') return '💾';
148
- if (n.type === 'dir' && n.e2e === true) return '🔒'; /* wiring:e2 — şifreli klasör rozeti */
157
+ if (n.type === 'dir' && n.e2e === true) return '🔒'; /* wiring:e2 — encrypted-folder badge */
149
158
  return null;
150
159
  }
151
160
 
@@ -375,7 +384,7 @@ const segments = computed<Segment[]>(() => {
375
384
  tabindex="0"
376
385
  :aria-selected="isSelected(n) ? 'true' : 'false'"
377
386
  :aria-label="nodeDisplayName(n) /* wiring:c4 */"
378
- :data-fe-path="n.path /* wiring:d1 — orta-tık yeni sekme delegasyonu */"
387
+ :data-fe-path="n.path /* wiring:d1 — middle-click open-in-new-tab delegation */"
379
388
  draggable="true"
380
389
  @click="onRowClick(n, $event)"
381
390
  @dblclick="onRowDbl(n)"
@@ -390,7 +399,7 @@ const segments = computed<Segment[]>(() => {
390
399
  >
391
400
  <div class="fe-list__col fe-list__col--star" role="gridcell" @click.stop>
392
401
  <StarButton
393
- v-if="typeof n.id === 'number' && n.type === 'file'"
402
+ v-if="starEnabled !== false && typeof n.id === 'number' && n.type === 'file'"
394
403
  :starred="!!starredIds?.has(n.id)"
395
404
  :node-id="n.id"
396
405
  :api-base="apiBase"
@@ -17,6 +17,7 @@ import type { ExplorerConfig, LocaleCode } from '../types/ExplorerConfig';
17
17
  import type { NFSExport } from '../types/NFSExports';
18
18
  import { useLocale } from '../composables/useLocale';
19
19
  import { useNFSExports } from '../composables/useNFSExports';
20
+ import { resolveLocale } from '../locales/resolve';
20
21
 
21
22
  const props = defineProps<{
22
23
  config: ExplorerConfig;
@@ -28,7 +29,7 @@ const emit = defineEmits<{
28
29
  (e: 'active', v: { host: string; port: number; enabled: boolean; path?: string; readOnly: boolean }): void;
29
30
  }>();
30
31
 
31
- const locale = computed<LocaleCode>(() => props.config.locale ?? 'tr');
32
+ const locale = computed<LocaleCode>(() => resolveLocale(props.config.locale));
32
33
  const { t } = useLocale(locale);
33
34
 
34
35
  const {
@@ -5,7 +5,7 @@
5
5
  * Historically a bottom-left toast list for queued copy/move/delete. Since
6
6
  * the unified operations center it renders NOTHING: it reconciles the
7
7
  * usePendingOps rows into the shared useOperations store; OperationsCenter
8
- * draws them. Terminal-state lingering ("bitti" flash → history) and sticky
8
+ * draws them. Terminal-state lingering ("done" flash → history) and sticky
9
9
  * errors are handled by the store — note usePendingOps sweeps terminal rows
10
10
  * after its RETAIN window, which the store treats as "move to history"
11
11
  * (errors stay pinned until the user dismisses them).
@@ -11,6 +11,7 @@ import type { PresenceUser } from '../lib/realtime';
11
11
  import type { LocaleCode } from '../types/ExplorerConfig';
12
12
  import { useLocale } from '../composables/useLocale';
13
13
  import PresenceAvatar from './PresenceAvatar.vue';
14
+ import { resolveLocale } from '../locales/resolve';
14
15
 
15
16
  const props = defineProps<{
16
17
  users: PresenceUser[];
@@ -18,7 +19,7 @@ const props = defineProps<{
18
19
  locale?: LocaleCode;
19
20
  }>();
20
21
 
21
- const { t } = useLocale(() => props.locale ?? 'tr');
22
+ const { t } = useLocale(() => resolveLocale(props.locale));
22
23
 
23
24
  const others = computed(() =>
24
25
  (props.users ?? []).filter((u) => props.selfId == null || u.id !== props.selfId),
@@ -87,7 +88,7 @@ function label(u: PresenceUser): string {
87
88
  <span v-if="others.length > 5" class="fx-presence-more">+{{ others.length - 5 }}</span>
88
89
  </div>
89
90
  <span class="fx-presence-text" :title="others.map(label).join(', ')">
90
- {{ others.length === 1 ? others[0].name : `${others.length} kişi` }}
91
+ {{ others.length === 1 ? others[0].name : t('presence.others', { n: others.length }) }}
91
92
  <template v-if="others.length === 1 && others[0].file">
92
93
  · <span class="fx-presence-file">{{ others[0].file }}</span>
93
94
  </template>
@@ -22,6 +22,7 @@ import type { ExplorerConfig, LocaleCode } from '../types/ExplorerConfig';
22
22
  import type { S3AccessKey } from '../types/S3Keys';
23
23
  import { useLocale } from '../composables/useLocale';
24
24
  import { useS3Keys } from '../composables/useS3Keys';
25
+ import { resolveLocale } from '../locales/resolve';
25
26
 
26
27
  const props = defineProps<{
27
28
  config: ExplorerConfig;
@@ -34,7 +35,7 @@ const emit = defineEmits<{
34
35
  (e: 'active', v: { accessKeyID: string; secret?: string; endpoint: string; pathStyle: boolean }): void;
35
36
  }>();
36
37
 
37
- const locale = computed<LocaleCode>(() => props.config.locale ?? 'tr');
38
+ const locale = computed<LocaleCode>(() => resolveLocale(props.config.locale));
38
39
  const { t } = useLocale(locale);
39
40
 
40
41
  const {
@@ -18,6 +18,7 @@ import type { ExplorerConfig, LocaleCode } from '../types/ExplorerConfig';
18
18
  import type { FTPSFacts, SSHPublicKey } from '../types/SSHKeys';
19
19
  import { useLocale } from '../composables/useLocale';
20
20
  import { useSSHKeys } from '../composables/useSSHKeys';
21
+ import { resolveLocale } from '../locales/resolve';
21
22
 
22
23
  const props = defineProps<{
23
24
  config: ExplorerConfig;
@@ -47,7 +48,7 @@ const emit = defineEmits<{
47
48
  }): void;
48
49
  }>();
49
50
 
50
- const locale = computed<LocaleCode>(() => props.config.locale ?? 'tr');
51
+ const locale = computed<LocaleCode>(() => resolveLocale(props.config.locale));
51
52
  const { t } = useLocale(locale);
52
53
 
53
54
  const { keys, connection, loading, error, canAdd, hasUsableKey, load, add, setDisabled, remove } =
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  /**
3
- * SecondaryPane — wiring:d1 split görünümün sağ (ikincil) paneli.
3
+ * SecondaryPane — wiring:d1 the right-hand (secondary) pane of the split view.
4
4
  *
5
5
  * Deliberately LIGHT: its own path + its own listing — fetched through
6
6
  * the SAME FileApi instance the host already constructed (`api.index`,
@@ -64,6 +64,13 @@ const props = defineProps<{
64
64
  /** ui-fix — mirror the main panel's virtual `.trash` row at storage root
65
65
  * so both split panes list identical rows (no row-offset). Defaults on. */
66
66
  trashVisible?: boolean;
67
+ /**
68
+ * The navigation panel is already offering Trash, so neither pane draws the
69
+ * virtual row. Passed down rather than worked out here: the panel belongs to
70
+ * the host explorer, and a pane that guessed at it is how the two halves of a
71
+ * split end up listing different rows.
72
+ */
73
+ navOffersTrash?: boolean;
67
74
  }>();
68
75
 
69
76
  const emit = defineEmits<{
@@ -72,11 +79,12 @@ const emit = defineEmits<{
72
79
  (e: 'close'): void;
73
80
  (e: 'open-tab', path: string): void;
74
81
  (e: 'transfer', p: { sources: string[]; targetWire: string; originWire?: string }): void;
75
- /* ui-fix — yan panelde sağ-tık: menü ana bileşende (FileExplorer) açılır.
76
- * node=null → boş alana sağ-tık (seçimsiz, yalnız Yapıştır). */
82
+ /* ui-fix — right-click inside the side pane: the menu is opened by the main
83
+ * component (FileExplorer). node=null → right-click on empty space (nothing
84
+ * selected, so "Paste" only). */
77
85
  (e: 'context', node: FileNode | null, ev: MouseEvent): void;
78
- /* ui-fix — çöp kutusu satırı açıldığında: trash görünümü ana panele aittir
79
- * (geri yükleme aksiyonlarıyla), host loadTrash() ile açar. */
86
+ /* ui-fix — when the trash row is opened: the trash view belongs to the main
87
+ * panel (it carries the restore actions), so the host opens it via loadTrash(). */
80
88
  (e: 'open-trash'): void;
81
89
  }>();
82
90
 
@@ -114,7 +122,14 @@ async function loadPane(target?: string): Promise<void> {
114
122
  // Same internal-entry filter + virtual `.trash` row as the main panel
115
123
  // (shared helpers → both split panes list identical rows, no offset).
116
124
  files.value = filterListing(resp.files);
117
- if (injectTrashRow(files.value, resp.adapter, resp.dirname, props.trashVisible !== false)) {
125
+ if (
126
+ injectTrashRow(files.value, resp.adapter, resp.dirname, props.trashVisible !== false, {
127
+ // ⚠ The panel is a sibling of BOTH panes. If it is offering Trash, this
128
+ // pane must drop the row too — otherwise the duplication is not removed,
129
+ // it just moves to the right-hand half of a split.
130
+ navOffersTrash: props.navOffersTrash,
131
+ })
132
+ ) {
118
133
  void hydrateTrashRow(files.value, resp.adapter, props.api);
119
134
  }
120
135
  path.value = props.toUser(resp.dirname);
@@ -130,7 +145,7 @@ async function loadPane(target?: string): Promise<void> {
130
145
  void loadPane(props.initialPath ?? '');
131
146
 
132
147
  // ------------------------------------------------------------------
133
- // Breadcrumb (kırıntı)
148
+ // Breadcrumb
134
149
  // ------------------------------------------------------------------
135
150
 
136
151
  interface Crumb {
@@ -190,12 +205,12 @@ function onViewContext(n: FileNode, ev: MouseEvent) {
190
205
  emit('context', n, ev);
191
206
  }
192
207
 
193
- /* ui-fix — boş alana (arka plan) sağ-tık: seçimsiz menü (yalnız Yapıştır)
194
- * → boş klasöre / mevcut klasöre dosya yapıştırma. Satır menüsü stopProp
195
- * ettiğinden bu yalnız gerçek boş alanda tetiklenir. */
208
+ /* ui-fix — right-click on empty space (the background): a selection-less menu
209
+ * ("Paste" only) pasting files into an empty / into the current folder. The
210
+ * row menu calls stopProp, so this only fires on genuinely empty space. */
196
211
  function onBgContext(ev: MouseEvent) {
197
212
  ev.preventDefault();
198
- ev.stopPropagation(); // FileExplorer kökündeki canvas menüsüne bubble etmesin
213
+ ev.stopPropagation(); // don't bubble into the canvas menu on the FileExplorer root
199
214
  emit('activate');
200
215
  selected.value = new Set();
201
216
  emit('context', null, ev);
@@ -385,8 +400,8 @@ defineExpose({ reload, goUp, selectAll, openSelected, selectedNodes, getPath });
385
400
  <div v-else-if="files.length === 0" class="fe-split__state">
386
401
  {{ t('empty.folder') }}
387
402
  </div>
388
- <!-- ui-fix — aynı görünüm bileşenleri (list/grid/gallery), pane'in
389
- kendi viewMode'uyla; eski düz fe-split__list kalktı. -->
403
+ <!-- ui-fix — the same view components (list/grid/gallery), driven by the
404
+ pane's own viewMode; the old flat fe-split__list is gone. -->
390
405
  <ListView
391
406
  v-else-if="(viewMode ?? 'list') === 'list'"
392
407
  :files="files"