@brftech/filex-core 0.39.1 → 0.41.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 +32 -25
- package/dist/ArchiveViewer-BNqukFg8.js +124 -0
- package/dist/ArchiveViewer-BNqukFg8.js.map +1 -0
- package/dist/CsvViewer-ii_-MgmG.js +141 -0
- package/dist/CsvViewer-ii_-MgmG.js.map +1 -0
- package/dist/{DrawioViewer-BNALOB04.js → DrawioViewer-B2tuu4rX.js} +48 -42
- package/dist/DrawioViewer-B2tuu4rX.js.map +1 -0
- package/dist/EpubViewer-p4B8iiWb.js +163 -0
- package/dist/EpubViewer-p4B8iiWb.js.map +1 -0
- package/dist/IpynbViewer-D_qYRJhu.js +184 -0
- package/dist/IpynbViewer-D_qYRJhu.js.map +1 -0
- package/dist/MermaidViewer-3ZkfNM8v.js +137 -0
- package/dist/MermaidViewer-3ZkfNM8v.js.map +1 -0
- package/dist/PsdViewer-CuV-OPSd.js +122 -0
- package/dist/PsdViewer-CuV-OPSd.js.map +1 -0
- package/dist/TiffViewer-BLeoF4Be.js +142 -0
- package/dist/TiffViewer-BLeoF4Be.js.map +1 -0
- package/dist/Viewer3D-DHhhU76E.js +75 -0
- package/dist/Viewer3D-DHhhU76E.js.map +1 -0
- package/dist/filex-core.js +217 -20077
- package/dist/filex-core.js.map +1 -1
- package/dist/filex-core.umd.cjs +61 -62
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/index-BraG7Cz4.js +26664 -0
- package/dist/index-BraG7Cz4.js.map +1 -0
- package/dist/index.d.ts +4276 -851
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +2170 -492
- package/src/components/AdvancedSearch.vue +870 -0
- package/src/components/Breadcrumb.vue +191 -19
- package/src/components/CommandPalette.vue +66 -22
- package/src/components/ConnectionsPanel.vue +6 -2
- package/src/components/ContextMenu.vue +57 -2
- package/src/components/EncryptedFolderModal.vue +31 -25
- package/src/components/FilePane.vue +939 -0
- package/src/components/FilterBar.vue +568 -72
- package/src/components/GalleryView.vue +162 -30
- package/src/components/GridView.vue +322 -58
- package/src/components/HomeView.vue +331 -0
- package/src/components/InspectorPanel.vue +296 -53
- package/src/components/ListView.vue +1158 -167
- package/src/components/NFSExportsPanel.vue +7 -4
- package/src/components/OnboardingTour.vue +156 -33
- package/src/components/QuickLook.vue +34 -5
- package/src/components/RecentlyOpened.vue +17 -2
- package/src/components/S3KeysPanel.vue +7 -4
- package/src/components/SSHKeysPanel.vue +7 -4
- package/src/components/ShortcutSettings.vue +43 -2
- package/src/components/ShortcutsHelp.vue +4 -1
- package/src/components/SideNav.vue +206 -140
- package/src/components/StorageFields.vue +19 -1
- package/src/components/TabBar.vue +87 -1
- package/src/components/TagPicker.vue +77 -6
- package/src/components/ThemeGallery.vue +13 -198
- package/src/components/ThemePalette.vue +244 -0
- package/src/components/TimeZoneDialog.vue +127 -0
- package/src/components/TimeZonePicker.vue +696 -0
- package/src/components/TokensPanel.vue +16 -4
- package/src/components/Toolbar.vue +965 -469
- package/src/components/ViewSwitcher.vue +9 -4
- package/src/composables/useConnections.ts +35 -4
- package/src/composables/useExplorerTimeZone.ts +85 -0
- package/src/composables/useFileApi.ts +53 -4
- package/src/composables/useKeyboardShortcuts.ts +122 -0
- package/src/composables/useLocale.ts +365 -25
- package/src/index.ts +182 -3
- package/src/lib/actionIcons.ts +330 -0
- package/src/lib/advSearch.ts +162 -0
- package/src/lib/dateGroups.ts +197 -0
- package/src/lib/destinationTree.ts +235 -0
- package/src/lib/downloadSelection.ts +154 -0
- package/src/lib/fileFilters.ts +319 -12
- package/src/lib/fileIcons.ts +299 -5
- package/src/lib/filePreview.ts +605 -0
- package/src/lib/listing.ts +72 -0
- package/src/lib/shareTtl.ts +11 -1
- package/src/lib/sortOrder.ts +459 -0
- package/src/lib/themes.ts +152 -23
- package/src/lib/timezone.ts +456 -0
- package/src/lib/uiProfile.ts +82 -0
- package/src/lib/viewPrefs.ts +1036 -0
- package/src/locales/en.ts +432 -25
- package/src/locales/tr.ts +425 -25
- package/src/modals/ConvertModal.vue +23 -3
- package/src/modals/DestinationPickerModal.vue +341 -0
- package/src/modals/Modal.vue +18 -4
- package/src/modals/NewDocumentModal.vue +525 -0
- package/src/modals/NewFolderModal.vue +15 -9
- package/src/modals/PermissionsModal.vue +543 -473
- package/src/modals/PreviewModal.vue +516 -156
- package/src/styles/base.css +5435 -8
- package/src/styles/variables.css +140 -60
- package/src/types/Connections.ts +9 -0
- package/src/types/ExplorerConfig.ts +144 -39
- package/src/types/FileNode.ts +28 -0
- package/src/viewers/ArchiveViewer.vue +46 -8
- package/src/viewers/CsvViewer.vue +19 -2
- package/src/viewers/DrawioViewer.vue +12 -1
- package/src/viewers/EpubViewer.vue +50 -6
- package/src/viewers/IpynbViewer.vue +19 -2
- package/src/viewers/MermaidViewer.vue +20 -3
- package/src/viewers/PdfViewer.vue +24 -4
- package/src/viewers/PsdViewer.vue +28 -4
- package/src/viewers/TiffViewer.vue +19 -2
- package/src/viewers/Viewer3D.vue +36 -2
- package/dist/ArchiveViewer-Cdfv9LFC.js +0 -99
- package/dist/ArchiveViewer-Cdfv9LFC.js.map +0 -1
- package/dist/CsvViewer-CqWeV8VO.js +0 -131
- package/dist/CsvViewer-CqWeV8VO.js.map +0 -1
- package/dist/DrawioViewer-BNALOB04.js.map +0 -1
- package/dist/EpubViewer-BPmlImG7.js +0 -145
- package/dist/EpubViewer-BPmlImG7.js.map +0 -1
- package/dist/IpynbViewer-CDlQmuCV.js +0 -175
- package/dist/IpynbViewer-CDlQmuCV.js.map +0 -1
- package/dist/MermaidViewer-sfZWdR-d.js +0 -127
- package/dist/MermaidViewer-sfZWdR-d.js.map +0 -1
- package/dist/PsdViewer-DwuySZsb.js +0 -112
- package/dist/PsdViewer-DwuySZsb.js.map +0 -1
- package/dist/TiffViewer-BDQ3d3CQ.js +0 -132
- package/dist/TiffViewer-BDQ3d3CQ.js.map +0 -1
- package/dist/Viewer3D-CJJJgkuv.js +0 -60
- package/dist/Viewer3D-CJJJgkuv.js.map +0 -1
- package/src/components/SecondaryPane.vue +0 -447
- package/src/modals/ShareModal.vue +0 -139
package/src/lib/shareTtl.ts
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
// from the same rule. A surface that clamps differently would mean the same
|
|
10
10
|
// product behaves two ways.
|
|
11
11
|
|
|
12
|
+
import { formatInstant } from '../composables/useLocale'; /* zaman:z1 */
|
|
13
|
+
|
|
12
14
|
export interface ExpiryOption {
|
|
13
15
|
/** Days; 0 = never. */
|
|
14
16
|
v: number;
|
|
@@ -75,9 +77,17 @@ export function expiryInputMax(maxDays: number | undefined, now: Date = new Date
|
|
|
75
77
|
export function validUntilLine(expiresAt: string | null | undefined, locale: 'tr' | 'en'): string {
|
|
76
78
|
if (!expiresAt) return locale === 'tr' ? 'Bu bağlantının süresi yoktur.' : 'This link does not expire.';
|
|
77
79
|
const d = new Date(expiresAt);
|
|
80
|
+
// zaman:z1 — the viewer's chosen clock, not the browser's. "Valid until
|
|
81
|
+
// 14:05" is a deadline, and a deadline printed in somebody else's zone is
|
|
82
|
+
// the worst kind of wrong: it looks actionable. Both halves of that — the
|
|
83
|
+
// zone AND the locale tag — come from `formatInstant`, which is the one
|
|
84
|
+
// place either is decided. This file used to pass the zone correctly and
|
|
85
|
+
// then map the tag itself, to 'en-GB', while useLocale mapped it to
|
|
86
|
+
// 'en-US': the same deadline was spelled "20 Sept 2026, 13:53" in the share
|
|
87
|
+
// dialog and "Sep 20, 2026, 1:53 PM" in the listing behind it.
|
|
78
88
|
const when = Number.isNaN(d.getTime())
|
|
79
89
|
? expiresAt
|
|
80
|
-
: d
|
|
90
|
+
: formatInstant(d, locale, { dateStyle: 'medium', timeStyle: 'short' });
|
|
81
91
|
return locale === 'tr' ? `Bu bağlantı ${when} tarihine kadar geçerli.` : `This link is valid until ${when}.`;
|
|
82
92
|
}
|
|
83
93
|
|
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sortOrder — THE listing sort: one key, one direction, one comparator, for
|
|
3
|
+
* every surface that draws a folder's contents.
|
|
4
|
+
*
|
|
5
|
+
* ⚠⚠ Why this file exists at all. Before it, the sort lived as two private
|
|
6
|
+
* refs inside `ListView.vue` and the grid had none, so "sorted by size" was a
|
|
7
|
+
* fact about one component rather than about the listing: switching grid ⇄
|
|
8
|
+
* list re-ordered the rows under the user, and any second control that wanted
|
|
9
|
+
* to sort would have had to keep its own copy of the answer. That is the exact
|
|
10
|
+
* shape of filex lesson #67 — the grid and the list each holding a private
|
|
11
|
+
* copy of "folders first" and drifting apart — one level up.
|
|
12
|
+
*
|
|
13
|
+
* ⚠⚠ …and why it is no longer a module-level SINGLETON. A singleton answers
|
|
14
|
+
* "what is the sort" for the whole bundle, which is the right answer only
|
|
15
|
+
* while the bundle draws one listing. The split view draws two, side by side,
|
|
16
|
+
* and the owner's ruling (2026-09-13) is that the second pane is the first
|
|
17
|
+
* pane: it gets the same crumbs, the same filter row, the same view switcher
|
|
18
|
+
* and the same sort control. Two sort CONTROLS over one piece of state is a
|
|
19
|
+
* control that moves the listing the person is not looking at — measured the
|
|
20
|
+
* same day: with the singleton in place, choosing "Size ↓" in the right pane
|
|
21
|
+
* re-ordered the left one too.
|
|
22
|
+
*
|
|
23
|
+
* So the state is a STORE (`createSortStore`) and there is one per pane, while
|
|
24
|
+
* everything that is genuinely one answer stays module-level:
|
|
25
|
+
*
|
|
26
|
+
* · the RULES (`byActiveKey`, `compareNodes`, folders-first, the two click
|
|
27
|
+
* vocabularies) — written once, closed over each store;
|
|
28
|
+
* · the LOCALE the `type` key sorts in — one alphabet for the whole bundle;
|
|
29
|
+
* · the persisted GLOBAL DEFAULT (`filex.list-sort`) — "the last sort a
|
|
30
|
+
* person chose", which is one fact no matter which pane they chose it in.
|
|
31
|
+
*
|
|
32
|
+
* ⚠ Panes find their store by INJECTION (`provideSortStore` / `useSortStore`),
|
|
33
|
+
* not by a prop threaded through every view: `ListView` and `FilterBar` are
|
|
34
|
+
* two levels down and an embedder may mount either on its own. With no
|
|
35
|
+
* provider — a bare `<ListView>` in a test, a host that renders one listing —
|
|
36
|
+
* `useSortStore()` hands back the default store, which IS the old singleton,
|
|
37
|
+
* so nothing that worked before changes.
|
|
38
|
+
*
|
|
39
|
+
* ⚠ Two RULES this file owns, so no caller has to remember them:
|
|
40
|
+
*
|
|
41
|
+
* 1. Folders before files, in every key and BOTH directions. `compareNodes`
|
|
42
|
+
* applies `byFoldersFirst` as the primary comparator and never multiplies
|
|
43
|
+
* it by the direction — multiplying would make "Name ↓" mean "files
|
|
44
|
+
* first", i.e. the arrow would silently regroup the listing instead of
|
|
45
|
+
* reversing it. The key only decides the order WITHIN each group.
|
|
46
|
+
* 2. One vocabulary. A column header cycles (`chooseSortKey`: a new key
|
|
47
|
+
* arrives in its default direction, the active one flips) and a menu item
|
|
48
|
+
* jumps (`setSort`: a named destination never reverses under you). Both
|
|
49
|
+
* live here, so the two surfaces reach for the same words instead of each
|
|
50
|
+
* re-deriving what a click means.
|
|
51
|
+
*
|
|
52
|
+
* ⚠ The persisted shape is the one `ListView` already wrote (`filex.list-sort`,
|
|
53
|
+
* `{key, dir}`), so a user who had sorted a column keeps that sort. The old
|
|
54
|
+
* `key: null` — "leave the backend's order alone" — is NOT a key here; see
|
|
55
|
+
* `ListingOrder` below for where that state went and why it could not be one.
|
|
56
|
+
*/
|
|
57
|
+
import { inject, provide, ref, type InjectionKey, type Ref } from 'vue';
|
|
58
|
+
|
|
59
|
+
import type { FileNode } from '../types/FileNode';
|
|
60
|
+
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
61
|
+
import { messages } from '../locales';
|
|
62
|
+
import { typeLabelKey } from './fileIcons';
|
|
63
|
+
import { byFoldersFirst } from './listing';
|
|
64
|
+
|
|
65
|
+
export type SortKey = 'name' | 'type' | 'modified' | 'size';
|
|
66
|
+
export type SortDir = 'asc' | 'desc';
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* WHERE A LISTING'S ORDER CAME FROM — the third state the old `key: null`
|
|
70
|
+
* used to express, back as a mode rather than as a fourth key.
|
|
71
|
+
*
|
|
72
|
+
* `sort` — a folder's contents. The key and the direction below decide
|
|
73
|
+
* the order, which is what every control in the UI drives.
|
|
74
|
+
* `relevance` — a RANKED answer. The server decided the order and it is the
|
|
75
|
+
* whole value of the reply; re-applying a key here buries the
|
|
76
|
+
* row the person came for.
|
|
77
|
+
*
|
|
78
|
+
* ⚠⚠ Why a mode and not a key. A key is a preference: it is persisted, it is
|
|
79
|
+
* named by a button, and the user picks it. "Relevance" is none of those — it
|
|
80
|
+
* is a FACT about the rows in hand, it must not outlive them, and there is no
|
|
81
|
+
* meaningful "relevance descending". Making it a fifth member of `SortKey`
|
|
82
|
+
* would have persisted it into the next folder (where nothing is ranked) and
|
|
83
|
+
* put a word in the Sort-by menu that does nothing in nine listings out of
|
|
84
|
+
* ten.
|
|
85
|
+
*
|
|
86
|
+
* ⚠⚠ …and why it is an ARGUMENT and not a field on the store. The store is a
|
|
87
|
+
* pane's preference; this is a property of the ROWS a pane happens to be
|
|
88
|
+
* holding, and one pane can be showing a search while the other shows a
|
|
89
|
+
* folder. Same pane, two navigations, two answers — the caller holding the
|
|
90
|
+
* rows is the only one that knows which it has.
|
|
91
|
+
*
|
|
92
|
+
* Measured, 2026-09-13 (qldemo, query `s`, Name ↑): the server ranked
|
|
93
|
+
* `Documents/server.ts` first and the list drew it fifteenth of seventeen,
|
|
94
|
+
* behind every alphabetically earlier row. The owner's ruling that day:
|
|
95
|
+
* ordinary search and ⌘K stay in relevance order.
|
|
96
|
+
*/
|
|
97
|
+
export type ListingOrder = 'sort' | 'relevance';
|
|
98
|
+
|
|
99
|
+
/** The four, in the order the menu draws them. One array, so the menu and
|
|
100
|
+
* `isSortKey` cannot disagree about how many there are. */
|
|
101
|
+
export const SORT_KEYS: readonly SortKey[] = ['name', 'type', 'modified', 'size'] as const;
|
|
102
|
+
|
|
103
|
+
const SORT_LS_KEY = 'filex.list-sort';
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The order the listing arrives in when nobody has chosen one.
|
|
107
|
+
*
|
|
108
|
+
* ⚠ `name` / ascending, and NOT the reference build's `modified` / descending,
|
|
109
|
+
* deliberately: the backend answers a listing name-ascending already, so this
|
|
110
|
+
* default moves nothing on screen for an existing user — and our list view
|
|
111
|
+
* draws date group headings whenever the key is `modified`, which the
|
|
112
|
+
* reference has no equivalent of. Making `modified` the default would put a
|
|
113
|
+
* heading on the first paint of every folder that nobody asked for. Changing
|
|
114
|
+
* it is this one constant; the consequence is that heading.
|
|
115
|
+
*/
|
|
116
|
+
const DEFAULT_KEY: SortKey = 'name';
|
|
117
|
+
|
|
118
|
+
/** Dates read newest-first; everything else reads A→Z / smallest-first. */
|
|
119
|
+
export function defaultSortDir(key: SortKey): SortDir {
|
|
120
|
+
return key === 'modified' ? 'desc' : 'asc';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isSortKey(v: unknown): v is SortKey {
|
|
124
|
+
return typeof v === 'string' && (SORT_KEYS as readonly string[]).includes(v);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function readStored(): { key: SortKey; dir: SortDir } {
|
|
128
|
+
const fallback = { key: DEFAULT_KEY, dir: defaultSortDir(DEFAULT_KEY) };
|
|
129
|
+
if (typeof localStorage === 'undefined') return fallback;
|
|
130
|
+
try {
|
|
131
|
+
const raw = localStorage.getItem(SORT_LS_KEY);
|
|
132
|
+
if (!raw) return fallback;
|
|
133
|
+
const p = JSON.parse(raw) as { key?: unknown; dir?: unknown };
|
|
134
|
+
const key = isSortKey(p.key) ? p.key : DEFAULT_KEY;
|
|
135
|
+
const dir = p.dir === 'asc' || p.dir === 'desc' ? p.dir : defaultSortDir(key);
|
|
136
|
+
return { key, dir };
|
|
137
|
+
} catch {
|
|
138
|
+
return fallback; // private mode / bad JSON
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The active locale, for the one comparison that needs words: `type` sorts by
|
|
144
|
+
* the label the Type column PRINTS, so "Image" and "Görsel" have to fall in
|
|
145
|
+
* their own alphabet's order.
|
|
146
|
+
*
|
|
147
|
+
* ⚠ Module-level and NOT per store, and that is deliberate even now that the
|
|
148
|
+
* key and the direction are per pane: two panes of one window are read by one
|
|
149
|
+
* person in one language. If each store carried its own resolver, the pane
|
|
150
|
+
* whose host forgot to set it would sort by a different alphabet than the pane
|
|
151
|
+
* beside it — the drift this file exists to prevent, in the one field where it
|
|
152
|
+
* would be hardest to see.
|
|
153
|
+
*/
|
|
154
|
+
const localeRef = ref<LocaleCode>('en');
|
|
155
|
+
|
|
156
|
+
export function setSortLocale(code: LocaleCode): void {
|
|
157
|
+
localeRef.value = code;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ── the comparator ────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
function modifiedMs(n: FileNode): number | null {
|
|
163
|
+
const v = n.last_modified;
|
|
164
|
+
if (!v) return null;
|
|
165
|
+
return v * (v < 1e12 ? 1000 : 1);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function nameCompare(a: FileNode, b: FileNode): number {
|
|
169
|
+
return (a.basename || '').localeCompare(b.basename || '', undefined, {
|
|
170
|
+
numeric: true,
|
|
171
|
+
sensitivity: 'base',
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** The word the Type column prints for this row — the thing a user sorting by
|
|
176
|
+
* "Type" is looking at. Unmapped kinds fall back to the uppercased extension,
|
|
177
|
+
* exactly as `typeLabelFor` does, so no row sorts as an empty string. */
|
|
178
|
+
function typeLabel(n: FileNode): string {
|
|
179
|
+
const key = typeLabelKey(n);
|
|
180
|
+
if (key) {
|
|
181
|
+
const cat = messages[localeRef.value] ?? messages.en;
|
|
182
|
+
return cat[key] ?? key;
|
|
183
|
+
}
|
|
184
|
+
const ext = (n.extension || '').trim();
|
|
185
|
+
return ext ? ext.toUpperCase() : '';
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** The active key's own order, direction applied. Folders-first is NOT here —
|
|
189
|
+
* it is the primary comparator in `compareNodes` and must not be reversed.
|
|
190
|
+
*
|
|
191
|
+
* ⚠ Written ONCE and closed over each store's refs, rather than copied into
|
|
192
|
+
* a store factory: the rule is the same in every pane and a per-pane copy of
|
|
193
|
+
* it is how the grid and the list drifted in the first place. */
|
|
194
|
+
function byActiveKey(key: SortKey, dirWord: SortDir, a: FileNode, b: FileNode): number {
|
|
195
|
+
const dir = dirWord === 'asc' ? 1 : -1;
|
|
196
|
+
switch (key) {
|
|
197
|
+
case 'name':
|
|
198
|
+
return dir * nameCompare(a, b);
|
|
199
|
+
case 'size':
|
|
200
|
+
return dir * ((a.size ?? -1) - (b.size ?? -1));
|
|
201
|
+
case 'type': {
|
|
202
|
+
const c = typeLabel(a).localeCompare(typeLabel(b), undefined, { sensitivity: 'base' });
|
|
203
|
+
// ⚠ The tiebreak is name-ASCENDING in both directions, on purpose: every
|
|
204
|
+
// image in a folder shares one label, so without it a Type sort leaves
|
|
205
|
+
// whole runs in whatever order the backend happened to answer, and
|
|
206
|
+
// "Type ↓" would scramble them again rather than reverse the groups.
|
|
207
|
+
return c !== 0 ? dir * c : nameCompare(a, b);
|
|
208
|
+
}
|
|
209
|
+
default: {
|
|
210
|
+
const am = modifiedMs(a);
|
|
211
|
+
const bm = modifiedMs(b);
|
|
212
|
+
// Undated rows go last in BOTH directions, so date groups stay clean.
|
|
213
|
+
if (am == null && bm == null) return 0;
|
|
214
|
+
if (am == null) return 1;
|
|
215
|
+
if (bm == null) return -1;
|
|
216
|
+
return dir * (am - bm);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ── the store ─────────────────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* One pane's sort. Everything a surface needs to READ the order, to CHANGE it
|
|
225
|
+
* from either of the two control vocabularies, and to sort rows by it.
|
|
226
|
+
*
|
|
227
|
+
* ⚠ `key` and `dir` are exposed as refs so a `computed` that reads them
|
|
228
|
+
* subscribes to the next change — the property the module-level singleton had
|
|
229
|
+
* and the reason every listing re-sorted itself for free.
|
|
230
|
+
*/
|
|
231
|
+
export interface SortStore {
|
|
232
|
+
readonly key: Ref<SortKey>;
|
|
233
|
+
readonly dir: Ref<SortDir>;
|
|
234
|
+
/** Set both halves outright. `dir` omitted → the key's own default. */
|
|
235
|
+
setSort(key: SortKey, dir?: SortDir): void;
|
|
236
|
+
/** Restore a remembered sort WITHOUT recording it as a fresh choice. */
|
|
237
|
+
applySort(key: SortKey, dir: SortDir): void;
|
|
238
|
+
/** Same order, other way round. The direction button's whole job. */
|
|
239
|
+
toggleSortDir(): void;
|
|
240
|
+
/** A column header click — see `chooseSortKey` below. */
|
|
241
|
+
chooseSortKey(key: SortKey): void;
|
|
242
|
+
/** Folders first, then the active key. Subscribes the reader. */
|
|
243
|
+
compareNodes(a: FileNode, b: FileNode): number;
|
|
244
|
+
/** THE comparator for a listing, given where its order came from. */
|
|
245
|
+
compareInOrder(order: ListingOrder): (a: FileNode, b: FileNode) => number;
|
|
246
|
+
/** The listing, in its order. A COPY. */
|
|
247
|
+
sortListing(files: FileNode[], order?: ListingOrder): FileNode[];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function persist(key: SortKey, dir: SortDir): void {
|
|
251
|
+
try {
|
|
252
|
+
localStorage.setItem(SORT_LS_KEY, JSON.stringify({ key, dir }));
|
|
253
|
+
} catch {
|
|
254
|
+
/* quota / private mode — the in-memory refs still moved */
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* A fresh sort store, seeded from the global default.
|
|
260
|
+
*
|
|
261
|
+
* ⚠⚠ Every store WRITES the global default when a person chooses in it, and
|
|
262
|
+
* that is the point rather than an oversight: `filex.list-sort` is defined as
|
|
263
|
+
* "the last sort a person chose", and a choice made in the right-hand pane is
|
|
264
|
+
* still a choice. What must not happen — and does not, because the refs are
|
|
265
|
+
* per store — is the OTHER pane moving when it does. Measured 2026-09-13:
|
|
266
|
+
* sorting the split pane by Size left the main pane on Name ↑ and made Size
|
|
267
|
+
* the arrangement the next unconfigured folder opens in.
|
|
268
|
+
*/
|
|
269
|
+
export function createSortStore(): SortStore {
|
|
270
|
+
const stored = readStored();
|
|
271
|
+
const key = ref<SortKey>(stored.key);
|
|
272
|
+
const dir = ref<SortDir>(stored.dir);
|
|
273
|
+
|
|
274
|
+
function setSort(k: SortKey, d?: SortDir): void {
|
|
275
|
+
key.value = k;
|
|
276
|
+
dir.value = d ?? defaultSortDir(k);
|
|
277
|
+
persist(key.value, dir.value);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* tablo:t1 — RESTORE a sort that was already the person's, without recording
|
|
282
|
+
* it as a fresh choice.
|
|
283
|
+
*
|
|
284
|
+
* ⚠⚠ The difference from `setSort` is the whole point and it is not
|
|
285
|
+
* cosmetic. `filex.list-sort` is the GLOBAL default — "what a folder I have
|
|
286
|
+
* never set up opens as" — and per-folder memory (`lib/viewPrefs`) is
|
|
287
|
+
* layered on top of it. If applying a folder's remembered sort went through
|
|
288
|
+
* `setSort`, walking into one folder that is sorted by size would quietly
|
|
289
|
+
* make Size the default for every folder in the product, and the person
|
|
290
|
+
* would have no idea which folder did it. Restoring is not choosing; only a
|
|
291
|
+
* choice writes the default.
|
|
292
|
+
*/
|
|
293
|
+
function applySort(k: SortKey, d: SortDir): void {
|
|
294
|
+
key.value = k;
|
|
295
|
+
dir.value = d;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function toggleSortDir(): void {
|
|
299
|
+
dir.value = dir.value === 'asc' ? 'desc' : 'asc';
|
|
300
|
+
persist(key.value, dir.value);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* A COLUMN HEADER click: a new key arrives in its own default direction, the
|
|
305
|
+
* key already active flips.
|
|
306
|
+
*
|
|
307
|
+
* ⚠ Not what the Sort-by menu calls — see `setSort`. A header is a toggle
|
|
308
|
+
* (click Name twice and you expect Z→A); a menu item is a destination (pick
|
|
309
|
+
* Name twice and you expect Name, still). Both meanings are written here so
|
|
310
|
+
* neither surface has to invent one.
|
|
311
|
+
*/
|
|
312
|
+
function chooseSortKey(k: SortKey): void {
|
|
313
|
+
if (key.value === k) toggleSortDir();
|
|
314
|
+
else setSort(k);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function compareNodesFn(a: FileNode, b: FileNode): number {
|
|
318
|
+
return byFoldersFirst(a, b) || byActiveKey(key.value, dir.value, a, b);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function compareInOrderFn(order: ListingOrder): (a: FileNode, b: FileNode) => number {
|
|
322
|
+
return order === 'relevance' ? byFoldersFirst : compareNodesFn;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
key,
|
|
327
|
+
dir,
|
|
328
|
+
setSort,
|
|
329
|
+
applySort,
|
|
330
|
+
toggleSortDir,
|
|
331
|
+
chooseSortKey,
|
|
332
|
+
compareNodes: compareNodesFn,
|
|
333
|
+
compareInOrder: compareInOrderFn,
|
|
334
|
+
sortListing: (files: FileNode[], order: ListingOrder = 'sort') =>
|
|
335
|
+
[...(files || [])].sort(compareInOrderFn(order)),
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* The store a surface gets when nobody provided one: a bare `<ListView>` in a
|
|
341
|
+
* test, an embedder mounting one listing, and the explorer's own MAIN pane —
|
|
342
|
+
* which keeps using it so that everything already reading the module-level
|
|
343
|
+
* functions below (per-folder view memory, `displayFiles`) is talking about
|
|
344
|
+
* the same state the main pane's controls drive.
|
|
345
|
+
*/
|
|
346
|
+
const defaultStore = createSortStore();
|
|
347
|
+
|
|
348
|
+
export const SORT_STORE_KEY: InjectionKey<SortStore> = Symbol('fe-sort-store');
|
|
349
|
+
|
|
350
|
+
/** Hand this subtree its own sort. Called by the pane component, once. */
|
|
351
|
+
export function provideSortStore(store: SortStore): void {
|
|
352
|
+
provide(SORT_STORE_KEY, store);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* THE store for the surface calling it.
|
|
357
|
+
*
|
|
358
|
+
* ⚠ Must be called during `setup()` (it injects). Surfaces hold the result and
|
|
359
|
+
* read `.key` / `.dir` inside their computeds, which is what subscribes them.
|
|
360
|
+
*/
|
|
361
|
+
export function useSortStore(): SortStore {
|
|
362
|
+
return inject(SORT_STORE_KEY, defaultStore);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ── the default store's API, as free functions ────────────────────────
|
|
366
|
+
//
|
|
367
|
+
// Everything below is the module-level API this file shipped with, now a thin
|
|
368
|
+
// pass-through to the default store. Kept — not deprecated — because the sort
|
|
369
|
+
// is genuinely one answer for a host that draws ONE listing, which is every
|
|
370
|
+
// embed except the split view, and because the explorer's per-folder view
|
|
371
|
+
// memory reads and writes exactly this state for its main pane.
|
|
372
|
+
|
|
373
|
+
export function activeSortKey(): SortKey {
|
|
374
|
+
return defaultStore.key.value;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function activeSortDir(): SortDir {
|
|
378
|
+
return defaultStore.dir.value;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function setSort(key: SortKey, dir?: SortDir): void {
|
|
382
|
+
defaultStore.setSort(key, dir);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export function applySort(key: SortKey, dir: SortDir): void {
|
|
386
|
+
defaultStore.applySort(key, dir);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* tablo:t1 — THE GLOBAL DEFAULT: the sort a folder nobody has configured opens
|
|
391
|
+
* in. Re-read from storage rather than mirrored in a second ref, because
|
|
392
|
+
* `setSort` writes there and `applySort` deliberately does not — so the stored
|
|
393
|
+
* value IS "the last sort a person chose", which is exactly the definition of
|
|
394
|
+
* the default, and a mirror could only drift from it.
|
|
395
|
+
*/
|
|
396
|
+
export function globalSort(): { key: SortKey; dir: SortDir } {
|
|
397
|
+
return readStored();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export function toggleSortDir(): void {
|
|
401
|
+
defaultStore.toggleSortDir();
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export function chooseSortKey(key: SortKey): void {
|
|
405
|
+
defaultStore.chooseSortKey(key);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* The whole rule, in order: folders before files, then the active key.
|
|
410
|
+
*
|
|
411
|
+
* Reading this subscribes the caller to the sort state, so a `computed` built
|
|
412
|
+
* on it re-runs when the key or the direction changes.
|
|
413
|
+
*/
|
|
414
|
+
export function compareNodes(a: FileNode, b: FileNode): number {
|
|
415
|
+
return defaultStore.compareNodes(a, b);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* THE comparator for a listing, given where its order came from. Every
|
|
420
|
+
* surface that draws rows asks this one question; none of them re-derives the
|
|
421
|
+
* answer, which is the whole reason this module exists.
|
|
422
|
+
*
|
|
423
|
+
* ⚠⚠ `relevance` is `byFoldersFirst` and NOTHING ELSE — deliberately the
|
|
424
|
+
* exact function `GridView` already sorts by, so the grid needs no knowledge
|
|
425
|
+
* of relevance at all and cannot drift from the list. `web/tests/lib/
|
|
426
|
+
* searchRelevanceOrder.test.ts` asserts that identity; if this ever returns
|
|
427
|
+
* something else, that test goes red and names GridView as the file that has
|
|
428
|
+
* to change in the same commit. Teaching one view about relevance and leaving
|
|
429
|
+
* the other sorting is filex lesson #67 one level up.
|
|
430
|
+
*
|
|
431
|
+
* ⚠ Folders still come FIRST in a ranked set, and that is a decision, not an
|
|
432
|
+
* oversight. Folders-first is not a sort here, it is a GROUPING: the grid
|
|
433
|
+
* draws it as two labelled sections ("Folders" / "Files"), so a result set
|
|
434
|
+
* that interleaved them would make the grid's own headings false. The ranking
|
|
435
|
+
* is kept in full INSIDE each group — `Array#sort` is stable and this
|
|
436
|
+
* comparator returns 0 for any two rows of the same kind — which is as much
|
|
437
|
+
* relevance as a surface that groups folders can honestly offer. The cost is
|
|
438
|
+
* visible and bounded: a folder that ranked seventh is drawn above the file
|
|
439
|
+
* that ranked first. The alternative costs the grid its sections.
|
|
440
|
+
*/
|
|
441
|
+
export function compareInOrder(order: ListingOrder): (a: FileNode, b: FileNode) => number {
|
|
442
|
+
return defaultStore.compareInOrder(order);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* The listing, in its order. A COPY — the caller's array is usually a prop or
|
|
447
|
+
* a computed's source and sorting it in place mutates somebody else's state.
|
|
448
|
+
*
|
|
449
|
+
* `Array#sort` is stable, so rows the comparator calls equal keep the order
|
|
450
|
+
* they arrived in — which is precisely what carries the server's ranking
|
|
451
|
+
* through `relevance` mode.
|
|
452
|
+
*
|
|
453
|
+
* ⚠ `order` defaults to `'sort'`: an omitted argument is how every caller
|
|
454
|
+
* holding an ordinary folder listing asks for the active key, and that is the
|
|
455
|
+
* overwhelming majority of them.
|
|
456
|
+
*/
|
|
457
|
+
export function sortListing(files: FileNode[], order: ListingOrder = 'sort'): FileNode[] {
|
|
458
|
+
return defaultStore.sortListing(files, order);
|
|
459
|
+
}
|