@brftech/filex-core 0.5.0 → 0.6.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 (45) hide show
  1. package/dist/{ArchiveViewer-DKW-HyXT.js → ArchiveViewer-EGgNqLPE.js} +2 -2
  2. package/dist/{ArchiveViewer-DKW-HyXT.js.map → ArchiveViewer-EGgNqLPE.js.map} +1 -1
  3. package/dist/{CsvViewer-D2HmP2yr.js → CsvViewer-BvriJ42M.js} +2 -2
  4. package/dist/{CsvViewer-D2HmP2yr.js.map → CsvViewer-BvriJ42M.js.map} +1 -1
  5. package/dist/{DrawioViewer-DPNvOFCs.js → DrawioViewer-B_bSPsWX.js} +2 -2
  6. package/dist/{DrawioViewer-DPNvOFCs.js.map → DrawioViewer-B_bSPsWX.js.map} +1 -1
  7. package/dist/{EpubViewer-ByqjNuNL.js → EpubViewer-hl68h4gG.js} +2 -2
  8. package/dist/{EpubViewer-ByqjNuNL.js.map → EpubViewer-hl68h4gG.js.map} +1 -1
  9. package/dist/{IpynbViewer-2iONhbuJ.js → IpynbViewer-lhwOjXK7.js} +2 -2
  10. package/dist/{IpynbViewer-2iONhbuJ.js.map → IpynbViewer-lhwOjXK7.js.map} +1 -1
  11. package/dist/{MermaidViewer-CmiU7hIT.js → MermaidViewer-H2Z6AYXg.js} +2 -2
  12. package/dist/{MermaidViewer-CmiU7hIT.js.map → MermaidViewer-H2Z6AYXg.js.map} +1 -1
  13. package/dist/{PsdViewer-Inhl2yN9.js → PsdViewer-Bsk-11yH.js} +2 -2
  14. package/dist/{PsdViewer-Inhl2yN9.js.map → PsdViewer-Bsk-11yH.js.map} +1 -1
  15. package/dist/{TiffViewer-C2AFFEze.js → TiffViewer-D-j9vycG.js} +2 -2
  16. package/dist/{TiffViewer-C2AFFEze.js.map → TiffViewer-D-j9vycG.js.map} +1 -1
  17. package/dist/{Viewer3D-YdW14q_n.js → Viewer3D-Bcr70rTe.js} +2 -2
  18. package/dist/{Viewer3D-YdW14q_n.js.map → Viewer3D-Bcr70rTe.js.map} +1 -1
  19. package/dist/filex-core.js +42 -39
  20. package/dist/filex-core.umd.cjs +59 -58
  21. package/dist/filex-core.umd.cjs.map +1 -1
  22. package/dist/index-C_ZSnk_I.js +11109 -0
  23. package/dist/index-C_ZSnk_I.js.map +1 -0
  24. package/dist/index.d.ts +234 -1
  25. package/dist/style.css +1 -1
  26. package/package.json +1 -1
  27. package/src/FileExplorer.vue +367 -14
  28. package/src/components/CommandPalette.vue +12 -1
  29. package/src/components/GalleryView.vue +214 -0
  30. package/src/components/GridView.vue +1 -0
  31. package/src/components/InspectorPanel.vue +170 -0
  32. package/src/components/ListView.vue +1 -0
  33. package/src/components/SecondaryPane.vue +411 -0
  34. package/src/components/TabBar.vue +174 -0
  35. package/src/components/Toolbar.vue +23 -5
  36. package/src/composables/useFileApi.ts +53 -0
  37. package/src/composables/useKeyboardShortcuts.ts +19 -0
  38. package/src/composables/useTabs.ts +222 -0
  39. package/src/index.ts +6 -0
  40. package/src/locales/en.ts +33 -0
  41. package/src/locales/tr.ts +33 -0
  42. package/src/styles/base.css +538 -0
  43. package/src/types/FileNode.ts +1 -1
  44. package/dist/index-eARevpz0.js +0 -9952
  45. package/dist/index-eARevpz0.js.map +0 -1
@@ -0,0 +1,214 @@
1
+ <script setup lang="ts">
2
+ // (unscoped styles by design — see base.css wiring:d2 block; SFC scoped
3
+ // styles are banned for webcomponent data-v hash mismatch reasons.)
4
+ /**
5
+ * GalleryView — wiring:d2. Third view mode: large-thumbnail gallery for
6
+ * visual browsing (photos/videos). Derived from GridView (same props/emits,
7
+ * same listbox a11y, same selection/drag/touch contract) but renders big
8
+ * square tiles: thumbnail (or SVG file-type icon fallback) with the name
9
+ * below and size+date revealed on hover/focus. GridView itself is untouched.
10
+ */
11
+ import { ref } from 'vue';
12
+ import type { FileNode } from '../types/FileNode';
13
+ import type { LocaleCode } from '../types/ExplorerConfig';
14
+ import { useLocale } from '../composables/useLocale';
15
+ import { fileIconSvg } from '../lib/fileIcons';
16
+ import { applyDragGhost } from '../lib/dragGhost';
17
+
18
+ const props = defineProps<{
19
+ files: FileNode[];
20
+ selected: Set<string>;
21
+ clipped?: Set<string>;
22
+ showParentPath?: boolean;
23
+ locale: LocaleCode;
24
+ loading?: boolean;
25
+ /** Authenticated thumb resolver (useThumbs.src) — same contract as
26
+ * GridView: raw `thumb_url` is root-relative and unauthenticated, so
27
+ * embedded hosts NEED this. null = icon fallback. */
28
+ thumbSrc?: (n: FileNode) => string | null;
29
+ }>();
30
+
31
+ const emit = defineEmits<{
32
+ (e: 'click-card', node: FileNode, mod: { ctrl: boolean; shift: boolean }): void;
33
+ (e: 'dbl-card', node: FileNode): void;
34
+ (e: 'context-card', node: FileNode, ev: MouseEvent): void;
35
+ (e: 'item-drag-start', node: FileNode, ev: DragEvent): void;
36
+ (e: 'item-drop-into', target: FileNode, ev: DragEvent): void;
37
+ }>();
38
+
39
+ const { t, formatSize, nodeDisplayName } = useLocale(() => props.locale);
40
+
41
+ function thumbOf(n: FileNode): string | null {
42
+ return props.thumbSrc ? props.thumbSrc(n) : (n.thumb_url ?? null);
43
+ }
44
+
45
+ function isSelected(n: FileNode): boolean {
46
+ return props.selected.has(n.path);
47
+ }
48
+
49
+ function onClick(n: FileNode, ev: MouseEvent) {
50
+ emit('click-card', n, { ctrl: ev.ctrlKey || ev.metaKey, shift: ev.shiftKey });
51
+ }
52
+
53
+ function onDbl(n: FileNode) {
54
+ emit('dbl-card', n);
55
+ }
56
+
57
+ function onCtx(n: FileNode, ev: MouseEvent) {
58
+ ev.preventDefault();
59
+ ev.stopPropagation();
60
+ emit('context-card', n, ev);
61
+ }
62
+
63
+ function onItemDragStart(n: FileNode, ev: DragEvent) {
64
+ applyDragGhost(
65
+ ev,
66
+ nodeDisplayName(n),
67
+ props.selected.has(n.path) ? props.selected.size : 1,
68
+ );
69
+ emit('item-drag-start', n, ev);
70
+ }
71
+
72
+ // Drop-target highlight — mirrors GridView (visual layer only).
73
+ const dropTargetPath = ref<string | null>(null);
74
+
75
+ function onItemDragOver(n: FileNode, ev: DragEvent) {
76
+ if (n.type !== 'dir') return;
77
+ if (!ev.dataTransfer?.types.includes('application/x-brf-files')) return;
78
+ ev.preventDefault();
79
+ ev.stopPropagation();
80
+ if (ev.dataTransfer) ev.dataTransfer.dropEffect = 'move';
81
+ dropTargetPath.value = n.path;
82
+ }
83
+
84
+ function onItemDragLeave(n: FileNode) {
85
+ if (dropTargetPath.value === n.path) dropTargetPath.value = null;
86
+ }
87
+
88
+ function onItemDrop(n: FileNode, ev: DragEvent) {
89
+ dropTargetPath.value = null;
90
+ if (n.type !== 'dir') return;
91
+ if (!ev.dataTransfer?.types.includes('application/x-brf-files')) return;
92
+ ev.preventDefault();
93
+ ev.stopPropagation();
94
+ emit('item-drop-into', n, ev);
95
+ }
96
+
97
+ // Long-press → context menu, same as GridView (touch parity).
98
+ let pressTimer: ReturnType<typeof setTimeout> | undefined;
99
+ let pressTarget: FileNode | null = null;
100
+ function onTouchStart(n: FileNode, ev: TouchEvent) {
101
+ pressTarget = n;
102
+ if (pressTimer) clearTimeout(pressTimer);
103
+ pressTimer = setTimeout(() => {
104
+ if (pressTarget) {
105
+ const t0 = ev.touches[0];
106
+ emit('context-card', pressTarget, {
107
+ clientX: t0.clientX,
108
+ clientY: t0.clientY,
109
+ preventDefault: () => {},
110
+ } as unknown as MouseEvent);
111
+ }
112
+ }, 500);
113
+ }
114
+ function cancelPress() {
115
+ if (pressTimer) clearTimeout(pressTimer);
116
+ pressTarget = null;
117
+ }
118
+
119
+ function parentDir(path: string): string {
120
+ const stripped = path.replace(/^[a-z][a-z0-9+.-]*:\/\//i, '');
121
+ const idx = stripped.lastIndexOf('/');
122
+ if (idx === -1) return '';
123
+ return stripped.slice(0, idx);
124
+ }
125
+
126
+ // Special rows keep their emoji (trash/storage are not file-TYPE icons).
127
+ function specialEmojiFor(n: FileNode): string | null {
128
+ if (n.basename === '.trash') return '🗑';
129
+ if (n.mime_type === 'inode/storage') return '💾';
130
+ return null;
131
+ }
132
+
133
+ function displayDate(ms: number | undefined): string {
134
+ if (!ms) return '';
135
+ const d = new Date(ms * (ms < 1e12 ? 1000 : 1));
136
+ return d.toLocaleString();
137
+ }
138
+
139
+ // Hover meta line: size for files, entry count (when known) for dirs.
140
+ function metaFor(n: FileNode): string {
141
+ const date = displayDate(n.last_modified);
142
+ const size = n.type === 'dir' ? '' : formatSize(n.size);
143
+ return [size, date].filter((s) => !!s).join(' · ');
144
+ }
145
+ </script>
146
+
147
+ <template>
148
+ <div
149
+ class="fe-gal"
150
+ :class="{ 'is-loading': loading }"
151
+ role="listbox"
152
+ aria-multiselectable="true"
153
+ :aria-label="t('gallery.aria')"
154
+ :aria-busy="loading ? 'true' : undefined"
155
+ >
156
+ <div
157
+ v-for="n in files"
158
+ :key="n.path"
159
+ class="fe-gal__card"
160
+ :class="{
161
+ 'is-selected': isSelected(n),
162
+ 'is-dir': n.type === 'dir',
163
+ 'is-trash': n.trashed,
164
+ 'is-clipped': clipped?.has(n.path),
165
+ 'is-droptarget': dropTargetPath === n.path,
166
+ }"
167
+ tabindex="0"
168
+ role="option"
169
+ :aria-selected="isSelected(n) ? 'true' : 'false'"
170
+ :aria-label="nodeDisplayName(n)"
171
+ draggable="true"
172
+ @click="onClick(n, $event)"
173
+ @dblclick="onDbl(n)"
174
+ @contextmenu="onCtx(n, $event)"
175
+ @dragstart="onItemDragStart(n, $event)"
176
+ @dragover="onItemDragOver(n, $event)"
177
+ @dragleave="onItemDragLeave(n)"
178
+ @drop="onItemDrop(n, $event)"
179
+ @touchstart.passive="onTouchStart(n, $event)"
180
+ @touchend="cancelPress"
181
+ @touchmove="cancelPress"
182
+ >
183
+ <div class="fe-gal__thumb">
184
+ <!-- draggable="false" for the same reason as GridView: HTML5 image
185
+ drag adds a 'Files' MIME to dataTransfer, which would trip the
186
+ parent's upload handler on internal drags. -->
187
+ <img
188
+ v-if="thumbOf(n)"
189
+ :src="thumbOf(n)!"
190
+ :alt="n.basename"
191
+ loading="lazy"
192
+ draggable="false"
193
+ />
194
+ <span v-else-if="specialEmojiFor(n)" class="fe-gal__icon">{{ specialEmojiFor(n) }}</span>
195
+ <!-- eslint-disable-next-line vue/no-v-html — static markup from lib/fileIcons -->
196
+ <span v-else class="fe-gal__icon fe-gal__icon--svg" v-html="fileIconSvg(n)"></span>
197
+ <div class="fe-gal__meta" aria-hidden="true">
198
+ <span v-if="metaFor(n)" class="fe-gal__meta-line">{{ metaFor(n) }}</span>
199
+ <span
200
+ v-if="showParentPath && parentDir(n.path)"
201
+ class="fe-gal__meta-line fe-gal__meta-line--path"
202
+ :title="parentDir(n.path)"
203
+ >{{ parentDir(n.path) }}</span>
204
+ </div>
205
+ </div>
206
+ <div class="fe-gal__label" :title="n.basename">
207
+ {{ nodeDisplayName(n) }}
208
+ </div>
209
+ </div>
210
+ <div v-if="!loading && files.length === 0" class="fe-gal__empty">
211
+ {{ t('empty.folder') }}
212
+ </div>
213
+ </div>
214
+ </template>
@@ -171,6 +171,7 @@ function snippetTitle(snippet: string): string {
171
171
  role="option"
172
172
  :aria-selected="isSelected(n) ? 'true' : 'false'"
173
173
  :aria-label="nodeDisplayName(n) /* wiring:c4 */"
174
+ :data-fe-path="n.path /* wiring:d1 — orta-tık yeni sekme delegasyonu */"
174
175
  draggable="true"
175
176
  @click="onClick(n, $event)"
176
177
  @dblclick="onDbl(n)"
@@ -244,6 +244,114 @@ watch(
244
244
  () => void refresh(),
245
245
  { immediate: true },
246
246
  );
247
+
248
+ /* === calisma:d3 — Yorumlar (node comments) ===
249
+ * Flat chronological thread per node (files AND folders — every backend
250
+ * row carries an id). Loaded through its own watcher + seq guard so the
251
+ * existing refresh() stays untouched. Add/delete are NOT optimistic: the
252
+ * list re-fetches after each successful API round-trip. The section hides
253
+ * silently when the backend gates (401/403) or lacks (404) the endpoint.
254
+ */
255
+ import type { NodeComment } from '../composables/useFileApi';
256
+
257
+ const commentsState = ref<SectionState>('hidden');
258
+ const comments = ref<NodeComment[]>([]);
259
+ const commentDraft = ref('');
260
+ const commentBusy = ref(false);
261
+ const commentNodeId = computed<number | null>(() =>
262
+ typeof single.value?.id === 'number' ? (single.value.id as number) : null,
263
+ );
264
+ let commentSeq = 0;
265
+
266
+ async function loadComments(): Promise<void> {
267
+ const seq = ++commentSeq;
268
+ const id = commentNodeId.value;
269
+ if (id == null) {
270
+ commentsState.value = 'hidden';
271
+ comments.value = [];
272
+ return;
273
+ }
274
+ commentsState.value = comments.value.length > 0 ? 'ok' : 'loading';
275
+ try {
276
+ const list = await props.api.listComments(id);
277
+ if (seq !== commentSeq) return;
278
+ comments.value = list;
279
+ commentsState.value = 'ok';
280
+ } catch (err) {
281
+ if (seq !== commentSeq) return;
282
+ comments.value = [];
283
+ const status = (err as { status?: number }).status;
284
+ commentsState.value =
285
+ status === 401 || status === 403 || status === 404 ? 'hidden' : 'error';
286
+ }
287
+ }
288
+
289
+ async function sendComment(): Promise<void> {
290
+ const id = commentNodeId.value;
291
+ const body = commentDraft.value.trim();
292
+ if (id == null || body === '' || commentBusy.value) return;
293
+ commentBusy.value = true;
294
+ try {
295
+ await props.api.addComment(id, body);
296
+ commentDraft.value = '';
297
+ await loadComments();
298
+ } catch (err) {
299
+ emit('toast', (err as Error).message);
300
+ } finally {
301
+ commentBusy.value = false;
302
+ }
303
+ }
304
+
305
+ async function removeComment(c: NodeComment): Promise<void> {
306
+ if (commentBusy.value) return;
307
+ commentBusy.value = true;
308
+ try {
309
+ await props.api.deleteComment(c.id);
310
+ await loadComments();
311
+ } catch (err) {
312
+ emit('toast', (err as Error).message);
313
+ } finally {
314
+ commentBusy.value = false;
315
+ }
316
+ }
317
+
318
+ /** Relative "3 minutes ago" stamp via Intl (locale-aware, no i18n keys). */
319
+ function relativeTime(iso: string): string {
320
+ const ms = Date.parse(iso);
321
+ if (Number.isNaN(ms)) return iso;
322
+ const diffS = Math.round((ms - Date.now()) / 1000);
323
+ const abs = Math.abs(diffS);
324
+ const units: Array<[Intl.RelativeTimeFormatUnit, number]> = [
325
+ ['year', 31536000],
326
+ ['month', 2592000],
327
+ ['day', 86400],
328
+ ['hour', 3600],
329
+ ['minute', 60],
330
+ ];
331
+ try {
332
+ const rtf = new Intl.RelativeTimeFormat(
333
+ props.locale === 'en' ? 'en' : 'tr',
334
+ { numeric: 'auto' },
335
+ );
336
+ for (const [unit, secs] of units) {
337
+ if (abs >= secs) return rtf.format(Math.trunc(diffS / secs), unit);
338
+ }
339
+ return rtf.format(diffS, 'second');
340
+ } catch {
341
+ return formatDateStr(iso);
342
+ }
343
+ }
344
+
345
+ watch(
346
+ () => commentNodeId.value,
347
+ () => {
348
+ comments.value = [];
349
+ commentDraft.value = '';
350
+ void loadComments();
351
+ },
352
+ { immediate: true },
353
+ );
354
+ /* === /calisma:d3 === */
247
355
  </script>
248
356
 
249
357
  <template>
@@ -450,6 +558,68 @@ watch(
450
558
  </li>
451
559
  </ul>
452
560
  </section>
561
+
562
+ <!-- ══ calisma:d3 — Yorumlar ══ -->
563
+ <section
564
+ v-if="commentsState === 'ok' || commentsState === 'error'"
565
+ class="fe-inspector__section"
566
+ >
567
+ <h3 class="fe-inspector__heading">
568
+ {{ t('inspector.section.comments') }}
569
+ <span
570
+ v-if="comments.length > 0"
571
+ class="fe-inspector__countbadge"
572
+ >{{ comments.length }}</span>
573
+ </h3>
574
+
575
+ <p v-if="commentsState === 'error'" class="fe-inspector__empty">
576
+ {{ t('inspector.error') }}
577
+ </p>
578
+ <template v-else>
579
+ <p v-if="comments.length === 0" class="fe-inspector__empty">
580
+ {{ t('inspector.comments.empty') }}
581
+ </p>
582
+ <ul v-else class="fe-inspector__comments">
583
+ <li v-for="c in comments" :key="c.id" class="fe-inspector__comment">
584
+ <div class="fe-inspector__comment-top">
585
+ <span class="fe-inspector__comment-author" :title="c.author_name">
586
+ {{ c.author_name || '—' }}
587
+ </span>
588
+ <span class="fe-inspector__comment-time" :title="formatDateStr(c.created_at)">
589
+ {{ relativeTime(c.created_at) }}
590
+ </span>
591
+ <button
592
+ v-if="c.can_delete"
593
+ type="button"
594
+ class="fe-inspector__comment-del"
595
+ :disabled="commentBusy"
596
+ :title="t('inspector.comments.delete')"
597
+ :aria-label="t('inspector.comments.delete')"
598
+ @click="removeComment(c)"
599
+ >×</button>
600
+ </div>
601
+ <p class="fe-inspector__comment-body">{{ c.body }}</p>
602
+ </li>
603
+ </ul>
604
+ <form class="fe-inspector__comment-form" @submit.prevent="sendComment">
605
+ <input
606
+ v-model="commentDraft"
607
+ type="text"
608
+ class="fe-inspector__comment-input"
609
+ maxlength="5000"
610
+ :placeholder="t('inspector.comments.placeholder')"
611
+ :aria-label="t('inspector.comments.placeholder')"
612
+ :disabled="commentBusy"
613
+ />
614
+ <button
615
+ type="submit"
616
+ class="fe-btn fe-btn--primary fe-btn--sm"
617
+ :disabled="commentBusy || commentDraft.trim() === ''"
618
+ >{{ t('inspector.comments.send') }}</button>
619
+ </form>
620
+ </template>
621
+ </section>
622
+ <!-- ══ /calisma:d3 ══ -->
453
623
  </div>
454
624
  </aside>
455
625
  </template>
@@ -361,6 +361,7 @@ const segments = computed<Segment[]>(() => {
361
361
  tabindex="0"
362
362
  :aria-selected="isSelected(n) ? 'true' : 'false'"
363
363
  :aria-label="nodeDisplayName(n) /* wiring:c4 */"
364
+ :data-fe-path="n.path /* wiring:d1 — orta-tık yeni sekme delegasyonu */"
364
365
  draggable="true"
365
366
  @click="onRowClick(n, $event)"
366
367
  @dblclick="onRowDbl(n)"