@3sln/trove 0.0.2
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 +1227 -0
- package/package.json +75 -0
- package/packages/core/src/collections/index.js +249 -0
- package/packages/core/src/errors.js +186 -0
- package/packages/core/src/identity/discovery.js +210 -0
- package/packages/core/src/identity/index.js +188 -0
- package/packages/core/src/identity/jwt.js +199 -0
- package/packages/core/src/index.js +104 -0
- package/packages/core/src/indexers/contribution.js +115 -0
- package/packages/core/src/indexers/registry.js +162 -0
- package/packages/core/src/indexing.js +340 -0
- package/packages/core/src/issues.js +150 -0
- package/packages/core/src/kv.js +0 -0
- package/packages/core/src/links.js +141 -0
- package/packages/core/src/metadata/cursor.js +73 -0
- package/packages/core/src/metadata/interface.js +244 -0
- package/packages/core/src/metadata/memory.js +270 -0
- package/packages/core/src/metadata/sqlite.js +412 -0
- package/packages/core/src/notifications/index.js +139 -0
- package/packages/core/src/notifications/webpush.js +217 -0
- package/packages/core/src/plugins/contributions.js +177 -0
- package/packages/core/src/plugins/identity.js +98 -0
- package/packages/core/src/plugins/index.js +225 -0
- package/packages/core/src/plugins/indexers.js +142 -0
- package/packages/core/src/plugins/installStore.js +134 -0
- package/packages/core/src/plugins/package.js +102 -0
- package/packages/core/src/plugins/packageStore.js +61 -0
- package/packages/core/src/plugins/runtime.js +101 -0
- package/packages/core/src/plugins/sql.js +52 -0
- package/packages/core/src/retry.js +74 -0
- package/packages/core/src/scan.js +302 -0
- package/packages/core/src/search/embeddings.js +128 -0
- package/packages/core/src/search/index.js +200 -0
- package/packages/core/src/search/keywordStore.js +107 -0
- package/packages/core/src/search/sqliteStores.js +455 -0
- package/packages/core/src/search/tagMatch.js +59 -0
- package/packages/core/src/search/transformer.js +195 -0
- package/packages/core/src/search/vectorStore.js +274 -0
- package/packages/core/src/search/vectorize.js +249 -0
- package/packages/core/src/sidecar/document.js +213 -0
- package/packages/core/src/sidecar/index.js +174 -0
- package/packages/core/src/sidecar/manager.js +239 -0
- package/packages/core/src/sidecar/store.js +46 -0
- package/packages/core/src/signedUrls.js +170 -0
- package/packages/core/src/sqlite-d1.js +162 -0
- package/packages/core/src/sqlite-driver.js +42 -0
- package/packages/core/src/sqlite.js +162 -0
- package/packages/core/src/storage/filesystem.js +283 -0
- package/packages/core/src/storage/interface.js +222 -0
- package/packages/core/src/storage/memory.js +113 -0
- package/packages/core/src/storage/prefixed.js +75 -0
- package/packages/core/src/storage/s3.js +316 -0
- package/packages/core/src/storage/s3sigv4.js +185 -0
- package/packages/core/src/tasks.js +228 -0
- package/packages/core/src/uploads.js +386 -0
- package/packages/core/src/util.js +125 -0
- package/packages/core/src/vfs.js +666 -0
- package/packages/plugin-sdk/src/browser.js +316 -0
- package/packages/plugin-sdk/src/index.js +32 -0
- package/packages/plugin-sdk/src/protocol.js +59 -0
- package/packages/plugin-sdk/src/rpc.js +95 -0
- package/packages/server/src/adapters/bun.js +78 -0
- package/packages/server/src/adapters/node.js +115 -0
- package/packages/server/src/adapters/staticAssets.js +123 -0
- package/packages/server/src/adapters/webDist.js +70 -0
- package/packages/server/src/adapters/worker-tasks.js +206 -0
- package/packages/server/src/adapters/worker.js +159 -0
- package/packages/server/src/cachePolicy.js +34 -0
- package/packages/server/src/engine/README.md +88 -0
- package/packages/server/src/engine/actions/scanCollection.js +114 -0
- package/packages/server/src/engine/index.js +95 -0
- package/packages/server/src/engine/lazy.js +25 -0
- package/packages/server/src/engine/providers/access.js +363 -0
- package/packages/server/src/engine/providers/core.js +405 -0
- package/packages/server/src/engine/providers/scan.js +67 -0
- package/packages/server/src/index.js +698 -0
- package/packages/server/src/manifest.js +98 -0
- package/packages/server/src/mcp/auth.js +40 -0
- package/packages/server/src/mcp/index.js +213 -0
- package/packages/server/src/mcp/protocol.js +181 -0
- package/packages/server/src/mcp/tools.js +351 -0
- package/packages/server/src/router.js +229 -0
- package/packages/server/src/routes.js +1066 -0
- package/packages/server/src/scope.js +43 -0
- package/packages/web/dist/assets/chunk-4xqbzebh.js +5 -0
- package/packages/web/dist/assets/chunk-4xqbzebh.js.map +9 -0
- package/packages/web/dist/assets/chunk-h05bxfbs.js +5 -0
- package/packages/web/dist/assets/chunk-h05bxfbs.js.map +10 -0
- package/packages/web/dist/assets/main-4cxs7prw.js +356 -0
- package/packages/web/dist/assets/main-4cxs7prw.js.map +103 -0
- package/packages/web/dist/assets/styles-kcx1x337.css +1 -0
- package/packages/web/dist/icon.svg +11 -0
- package/packages/web/dist/index.html +16 -0
- package/packages/web/dist/sql-wasm.wasm +0 -0
- package/packages/web/dist/sw.js +186 -0
- package/packages/web/src/bl/actions.js +410 -0
- package/packages/web/src/bl/activity.js +306 -0
- package/packages/web/src/bl/commands.js +274 -0
- package/packages/web/src/bl/fileType.js +49 -0
- package/packages/web/src/bl/index.js +70 -0
- package/packages/web/src/bl/links.js +54 -0
- package/packages/web/src/bl/offline.js +268 -0
- package/packages/web/src/bl/openers.js +71 -0
- package/packages/web/src/bl/pluginInstall.js +59 -0
- package/packages/web/src/bl/services.js +143 -0
- package/packages/web/src/bl/social.js +234 -0
- package/packages/web/src/bl/tagQuery.js +44 -0
- package/packages/web/src/main.js +10 -0
- package/packages/web/src/platform/api.js +529 -0
- package/packages/web/src/platform/commands.js +89 -0
- package/packages/web/src/platform/context.js +77 -0
- package/packages/web/src/platform/contributions.js +156 -0
- package/packages/web/src/platform/index.js +150 -0
- package/packages/web/src/platform/keybindings.js +199 -0
- package/packages/web/src/platform/mediaUrls.js +137 -0
- package/packages/web/src/platform/navigation.js +131 -0
- package/packages/web/src/platform/notifications.js +50 -0
- package/packages/web/src/platform/overlay.js +81 -0
- package/packages/web/src/platform/pluginClientDb.js +132 -0
- package/packages/web/src/platform/pluginDock.js +141 -0
- package/packages/web/src/platform/pluginFrames.js +194 -0
- package/packages/web/src/platform/pluginHost.js +648 -0
- package/packages/web/src/platform/pluginMedia.js +62 -0
- package/packages/web/src/platform/pluginModules.js +90 -0
- package/packages/web/src/platform/pluginNet.js +71 -0
- package/packages/web/src/platform/pluginPackage.js +247 -0
- package/packages/web/src/platform/pluginRpc.js +377 -0
- package/packages/web/src/platform/pluginSigning.js +168 -0
- package/packages/web/src/platform/pluginStore.js +67 -0
- package/packages/web/src/platform/settings.js +101 -0
- package/packages/web/src/platform/spatialNav.js +286 -0
- package/packages/web/src/platform/viewport.js +123 -0
- package/packages/web/src/platform/voice.js +133 -0
- package/packages/web/src/platform/voiceSearch.js +155 -0
- package/packages/web/src/platform/whenclause.js +162 -0
- package/packages/web/src/platform/workbench.js +156 -0
- package/packages/web/src/runtime.js +73 -0
- package/packages/web/src/styles.css +1382 -0
- package/packages/web/src/ui/components/activityBar.js +35 -0
- package/packages/web/src/ui/components/activityPanel.js +132 -0
- package/packages/web/src/ui/components/commandPalette.js +154 -0
- package/packages/web/src/ui/components/editorArea.js +75 -0
- package/packages/web/src/ui/components/launcher.js +392 -0
- package/packages/web/src/ui/components/openers/index.js +212 -0
- package/packages/web/src/ui/components/openers/markdown.js +222 -0
- package/packages/web/src/ui/components/overlays.js +255 -0
- package/packages/web/src/ui/components/phoneChrome.js +188 -0
- package/packages/web/src/ui/components/pluginReview.js +151 -0
- package/packages/web/src/ui/components/pluginsView.js +120 -0
- package/packages/web/src/ui/components/settingsView.js +258 -0
- package/packages/web/src/ui/components/social.js +290 -0
- package/packages/web/src/ui/components/statusBar.js +198 -0
- package/packages/web/src/ui/components/views/grid.js +115 -0
- package/packages/web/src/ui/components/views/index.js +155 -0
- package/packages/web/src/ui/components/views/list.js +50 -0
- package/packages/web/src/ui/components/views/parts.js +58 -0
- package/packages/web/src/ui/compositions/workbench.js +125 -0
- package/packages/web/src/ui/format.js +33 -0
- package/packages/web/src/ui/icon.js +81 -0
- package/packages/web/src/ui/media.js +114 -0
- package/packages/web/src/ui/sanitize.js +86 -0
- package/packages/web/src/workbench.js +205 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
// ngin Actions — the workbench's business logic as thin, dispatchable verbs
|
|
2
|
+
// (CQRS-style). Each depends on the `app` provider (platform + reactive
|
|
3
|
+
// services) and performs one user intent: navigate, mutate the tree, upload,
|
|
4
|
+
// search, open a file. Errors surface as notifications; the reactive services
|
|
5
|
+
// they update flow straight back into the UI via `watch`.
|
|
6
|
+
|
|
7
|
+
import { Action } from '@3sln/ngin';
|
|
8
|
+
import { newId } from '@3sln/trove/core/util.js';
|
|
9
|
+
import { matchesTagFilters } from './tagQuery.js';
|
|
10
|
+
import { availableOpeners, rememberedOpenerId } from './openers.js';
|
|
11
|
+
|
|
12
|
+
class AppAction extends Action {
|
|
13
|
+
static deps = ['app'];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Load a collection's items. There is nothing to navigate INTO any more — a drive is
|
|
18
|
+
* browsed by search and by following links — so this only ever switches which
|
|
19
|
+
* collection is on screen, or refreshes the current one.
|
|
20
|
+
*/
|
|
21
|
+
export class NavigateAction extends AppAction {
|
|
22
|
+
constructor(collectionId) {
|
|
23
|
+
super();
|
|
24
|
+
this.collectionId = collectionId;
|
|
25
|
+
}
|
|
26
|
+
async execute({ app }) {
|
|
27
|
+
const { explorer, platform } = app;
|
|
28
|
+
const collectionId = this.collectionId || explorer.state.collectionId || 'default';
|
|
29
|
+
const switching = collectionId !== explorer.state.collectionId;
|
|
30
|
+
// A collection's trash belongs to that collection. Carrying it across a switch
|
|
31
|
+
// listed the OLD collection's deleted files under the new one — above an "Empty
|
|
32
|
+
// trash" button that purges the new one, so the rows and the button disagreed
|
|
33
|
+
// about which drive they were talking about.
|
|
34
|
+
explorer.set({ loading: true, error: null, collectionId, ...(switching ? { trash: null } : {}) });
|
|
35
|
+
try {
|
|
36
|
+
const sort = platform.settings.get('explorer.sort');
|
|
37
|
+
const order = platform.settings.get('explorer.sortOrder');
|
|
38
|
+
const res = await platform.api.list({ sort, order, collection: collectionId });
|
|
39
|
+
explorer.set({
|
|
40
|
+
items: res.items, loading: false, selection: [], sort, order,
|
|
41
|
+
collectionId: res.collectionId || collectionId,
|
|
42
|
+
// What the COLLECTION holds, as against the page we were handed. Reporting the
|
|
43
|
+
// page length would tell someone with 3,000 files that they have 500.
|
|
44
|
+
stats: res.stats || null,
|
|
45
|
+
usage: res.usage || null,
|
|
46
|
+
nextCursor: res.nextCursor || null,
|
|
47
|
+
});
|
|
48
|
+
// Remember where they were, so the next visit opens there rather than guessing.
|
|
49
|
+
platform.settings.set?.('explorer.lastCollection', collectionId);
|
|
50
|
+
// Explorer→context projection (collectionId/hasSelection) lives in bl/index.js
|
|
51
|
+
// so it stays in sync with selection too — nothing to mirror here.
|
|
52
|
+
} catch (err) {
|
|
53
|
+
explorer.set({ loading: false, error: err.message });
|
|
54
|
+
platform.notifications.error(`Couldn't load this collection: ${err.message}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export class LoadCollectionsAction extends AppAction {
|
|
60
|
+
async execute({ app }) {
|
|
61
|
+
try {
|
|
62
|
+
const res = await app.platform.api.collections();
|
|
63
|
+
const collections = res.collections || [];
|
|
64
|
+
app.explorer.set({ collections, canCreateCollection: !!res.canCreate });
|
|
65
|
+
return collections;
|
|
66
|
+
} catch { /* collections disabled */ return []; }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Open the drive the user should land on.
|
|
72
|
+
*
|
|
73
|
+
* NOT simply 'default'. On a multi-user deployment plenty of people have no access to
|
|
74
|
+
* the default collection at all, and starting there shows them a permission error
|
|
75
|
+
* instead of their own files — the drive appears broken to the exact users for whom it
|
|
76
|
+
* is working correctly. So: keep the last collection if it is still readable, otherwise
|
|
77
|
+
* take the first one they can actually see.
|
|
78
|
+
*/
|
|
79
|
+
export class OpenInitialCollectionAction extends AppAction {
|
|
80
|
+
async execute({ app }) {
|
|
81
|
+
// Calls the API directly rather than dispatching LoadCollectionsAction: ngin's
|
|
82
|
+
// dispatch() returns an event feed, not the action's value, so awaiting it would
|
|
83
|
+
// hand back an EventTarget and this would fail in a way nothing reports.
|
|
84
|
+
let collections = [];
|
|
85
|
+
try {
|
|
86
|
+
const res = await app.platform.api.collections();
|
|
87
|
+
collections = res.collections || [];
|
|
88
|
+
app.explorer.set({ collections, canCreateCollection: !!res.canCreate });
|
|
89
|
+
} catch (err) {
|
|
90
|
+
app.explorer.set({ loading: false, error: `Couldn't load your collections: ${err.message}` });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const ids = collections.map((c) => c.id);
|
|
94
|
+
const remembered = app.platform.settings.get('explorer.lastCollection');
|
|
95
|
+
if (!ids.length) {
|
|
96
|
+
// A signed-in user with no grants at all. Say so; an empty file list with no
|
|
97
|
+
// explanation reads as "your drive is empty", which is a different and much worse
|
|
98
|
+
// thing to believe.
|
|
99
|
+
app.explorer.set({
|
|
100
|
+
loading: false, items: [], collections: [],
|
|
101
|
+
error: 'You do not have access to any collections yet. Ask an administrator to grant you one.',
|
|
102
|
+
});
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const target = ids.includes(remembered) ? remembered : (ids.includes('default') ? 'default' : ids[0]);
|
|
106
|
+
return app.engine.dispatch(new NavigateAction(target));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export class CreateCollectionAction extends AppAction {
|
|
111
|
+
constructor(record) {
|
|
112
|
+
super();
|
|
113
|
+
this.record = record;
|
|
114
|
+
}
|
|
115
|
+
async execute({ app }) {
|
|
116
|
+
try {
|
|
117
|
+
const res = await app.platform.api.createCollection(this.record);
|
|
118
|
+
app.platform.notifications.success(`Created collection “${res.collection.name}”`);
|
|
119
|
+
await app.engine.dispatch(new LoadCollectionsAction());
|
|
120
|
+
app.engine.dispatch(new NavigateAction(res.collection.id));
|
|
121
|
+
} catch (err) {
|
|
122
|
+
app.platform.notifications.error(`Couldn’t create collection: ${err.message}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Append the next page of a collection to what's already on screen. */
|
|
128
|
+
export class LoadMoreAction extends AppAction {
|
|
129
|
+
async execute({ app }) {
|
|
130
|
+
const { explorer, platform } = app;
|
|
131
|
+
const cursor = explorer.state.nextCursor;
|
|
132
|
+
if (!cursor || explorer.state.loadingMore) return;
|
|
133
|
+
explorer.set({ loadingMore: true });
|
|
134
|
+
try {
|
|
135
|
+
const res = await platform.api.list({
|
|
136
|
+
sort: explorer.state.sort, order: explorer.state.order,
|
|
137
|
+
collection: explorer.state.collectionId, cursor,
|
|
138
|
+
});
|
|
139
|
+
explorer.set({
|
|
140
|
+
items: [...explorer.state.items, ...res.items],
|
|
141
|
+
nextCursor: res.nextCursor || null,
|
|
142
|
+
loadingMore: false,
|
|
143
|
+
});
|
|
144
|
+
} catch (err) {
|
|
145
|
+
explorer.set({ loadingMore: false });
|
|
146
|
+
platform.notifications.error(`Couldn't load more: ${err.message}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export class RefreshAction extends AppAction {
|
|
152
|
+
async execute({ app }) {
|
|
153
|
+
return app.engine.dispatch(new NavigateAction(app.explorer.state.collectionId));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Show what's been deleted but not destroyed, and act on it. */
|
|
158
|
+
export class TrashAction extends AppAction {
|
|
159
|
+
constructor(op = 'list', id = null) {
|
|
160
|
+
super();
|
|
161
|
+
this.op = op;
|
|
162
|
+
this.id = id;
|
|
163
|
+
}
|
|
164
|
+
async execute({ app }) {
|
|
165
|
+
const { explorer, platform } = app;
|
|
166
|
+
const collection = explorer.state.collectionId;
|
|
167
|
+
// Putting it away is one of the things you can do to the trash. Without this the
|
|
168
|
+
// section, once opened, stayed on screen until a page reload — there was no way
|
|
169
|
+
// back to a plain list of files.
|
|
170
|
+
if (this.op === 'hide') {
|
|
171
|
+
explorer.set({ trash: null });
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
if (this.op === 'restore') {
|
|
176
|
+
const { node } = await platform.api.restore(this.id);
|
|
177
|
+
platform.notifications.success(`Restored “${node.name}”`);
|
|
178
|
+
} else if (this.op === 'purge') {
|
|
179
|
+
await platform.api.purgeTrash({ id: this.id });
|
|
180
|
+
} else if (this.op === 'empty') {
|
|
181
|
+
const { purged } = await platform.api.purgeTrash({ collection });
|
|
182
|
+
platform.notifications.success(`Deleted ${purged} item${purged === 1 ? '' : 's'} for good`);
|
|
183
|
+
}
|
|
184
|
+
const { items } = await platform.api.trash(collection);
|
|
185
|
+
explorer.set({ trash: items });
|
|
186
|
+
// Restoring puts something back in the drive, so the list on screen is now stale.
|
|
187
|
+
if (this.op !== 'list') await app.engine.dispatch(new NavigateAction(collection));
|
|
188
|
+
} catch (err) {
|
|
189
|
+
platform.notifications.error(`Trash: ${err.message}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export class DeleteAction extends AppAction {
|
|
195
|
+
constructor(ids) {
|
|
196
|
+
super();
|
|
197
|
+
this.ids = ids;
|
|
198
|
+
}
|
|
199
|
+
async execute({ app }) {
|
|
200
|
+
try {
|
|
201
|
+
for (const id of this.ids) await app.platform.api.remove(id);
|
|
202
|
+
app.platform.notifications.info(`Deleted ${this.ids.length} item${this.ids.length > 1 ? 's' : ''}`);
|
|
203
|
+
for (const id of this.ids) app.platform.workbench.closeTab(id);
|
|
204
|
+
} catch (err) {
|
|
205
|
+
app.platform.notifications.error(`Couldn’t delete: ${err.message}`);
|
|
206
|
+
}
|
|
207
|
+
app.engine.dispatch(new RefreshAction());
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export class RenameAction extends AppAction {
|
|
212
|
+
constructor(id, newName) {
|
|
213
|
+
super();
|
|
214
|
+
this.id = id;
|
|
215
|
+
this.newName = newName;
|
|
216
|
+
}
|
|
217
|
+
async execute({ app }) {
|
|
218
|
+
try {
|
|
219
|
+
const node = await app.platform.api.rename(this.id, this.newName);
|
|
220
|
+
app.platform.workbench.updateTabNode(node.node);
|
|
221
|
+
app.engine.dispatch(new RefreshAction());
|
|
222
|
+
} catch (err) {
|
|
223
|
+
app.platform.notifications.error(`Couldn’t rename: ${err.message}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
export class OpenFileAction extends AppAction {
|
|
230
|
+
/** @param {object} node @param {{reset?:boolean}} [opts] reset → start a fresh stack (modal search) */
|
|
231
|
+
constructor(node, opts = {}) {
|
|
232
|
+
super();
|
|
233
|
+
this.node = node;
|
|
234
|
+
this.opts = opts;
|
|
235
|
+
}
|
|
236
|
+
async execute({ app }) {
|
|
237
|
+
// Guard against being invoked with no target (e.g. a command fired with an empty
|
|
238
|
+
// selection) — dereferencing a null node would throw an unhandled rejection.
|
|
239
|
+
if (!this.node) return;
|
|
240
|
+
const { platform } = app;
|
|
241
|
+
const open = (openerId) => platform.workbench.openFile(this.node, openerId, { reset: !!this.opts.reset });
|
|
242
|
+
|
|
243
|
+
// An explicit opener (e.g. the switch-opener control) wins outright.
|
|
244
|
+
if (this.opts.openerId) return open(this.opts.openerId);
|
|
245
|
+
|
|
246
|
+
// Only openers available right now (a plugin previewer needing the network while
|
|
247
|
+
// offline is skipped, so we fall back to a built-in one).
|
|
248
|
+
const avail = availableOpeners(platform, this.node);
|
|
249
|
+
|
|
250
|
+
// A remembered choice for this file type, if that opener is still available.
|
|
251
|
+
const remembered = rememberedOpenerId(platform, this.node);
|
|
252
|
+
if (remembered && avail.some((o) => o.id === remembered)) return open(remembered);
|
|
253
|
+
|
|
254
|
+
// Several openers and no saved preference → let the user choose (with an option to
|
|
255
|
+
// remember). One or none → just open the best (or the download fallback).
|
|
256
|
+
if (avail.length > 1) {
|
|
257
|
+
return platform.workbench.showDialog({ kind: 'opener-chooser', node: this.node, openers: avail, reset: !!this.opts.reset });
|
|
258
|
+
}
|
|
259
|
+
open(avail[0]?.id || 'core.fallback');
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export class UploadFilesAction extends AppAction {
|
|
264
|
+
constructor(files, collectionId) {
|
|
265
|
+
super();
|
|
266
|
+
this.files = files;
|
|
267
|
+
this.collectionId = collectionId;
|
|
268
|
+
}
|
|
269
|
+
async execute({ app }) {
|
|
270
|
+
const collection = this.collectionId || app.explorer.state.collectionId || 'default';
|
|
271
|
+
const concurrency = app.platform.settings.get('uploads.concurrency');
|
|
272
|
+
const uploads = [...this.files].map((file) => this.#one(app, file, collection, concurrency));
|
|
273
|
+
await Promise.allSettled(uploads);
|
|
274
|
+
app.engine.dispatch(new NavigateAction(collection));
|
|
275
|
+
}
|
|
276
|
+
async #one(app, file, collection, concurrency) {
|
|
277
|
+
const { transfers, platform } = app;
|
|
278
|
+
const tid = newId('xfer');
|
|
279
|
+
const controller = new AbortController();
|
|
280
|
+
transfers.start(tid, file.name, file.size, controller);
|
|
281
|
+
let uploadId = null;
|
|
282
|
+
try {
|
|
283
|
+
const node = await platform.api.upload(file, {
|
|
284
|
+
collection, concurrency, signal: controller.signal,
|
|
285
|
+
onStart: (id) => { uploadId = id; },
|
|
286
|
+
onProgress: (p) => transfers.progress(tid, p),
|
|
287
|
+
});
|
|
288
|
+
transfers.finish(tid, 'done');
|
|
289
|
+
// The server disambiguates a same-name collision rather than overwriting — tell
|
|
290
|
+
// the user when the saved name differs from what they dropped.
|
|
291
|
+
if (node?.name && node.name !== file.name) {
|
|
292
|
+
platform.notifications.info(`"${file.name}" already existed — saved as "${node.name}".`);
|
|
293
|
+
}
|
|
294
|
+
} catch (err) {
|
|
295
|
+
// Release the server-side session so a cancelled/failed multipart upload doesn't
|
|
296
|
+
// leak an open multipart object (best-effort; the server also sweeps stale ones).
|
|
297
|
+
if (uploadId) platform.api.abortUpload(uploadId).catch(() => {});
|
|
298
|
+
if (err.code === 'aborted') transfers.finish(tid, 'cancelled');
|
|
299
|
+
else {
|
|
300
|
+
transfers.finish(tid, 'error', err.message);
|
|
301
|
+
platform.notifications.error(`Upload failed: ${file.name} — ${err.message}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Drive-wide tag/property filter (`#tag`, `#key:op:value`), optionally narrowed
|
|
308
|
+
* by free text. Falls back to filtering the loaded folder when offline. */
|
|
309
|
+
export class FilterAction extends AppAction {
|
|
310
|
+
constructor(filters, text) {
|
|
311
|
+
super();
|
|
312
|
+
this.filters = filters || [];
|
|
313
|
+
this.text = text || '';
|
|
314
|
+
}
|
|
315
|
+
async execute({ app }) {
|
|
316
|
+
const { search, platform } = app;
|
|
317
|
+
if (!this.filters.length) {
|
|
318
|
+
search.set({ results: [], ran: false, filtered: false });
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
search.set({ query: this.text, loading: true, error: null, filtered: true });
|
|
322
|
+
if (app.offline && !app.offline.state.online) {
|
|
323
|
+
const items = (app.explorer.state.items || []).filter((n) => matchesTagFilters(n, this.filters));
|
|
324
|
+
search.set({ results: items.map((node) => ({ node })), loading: false, ran: true, filtered: true, offline: true });
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
try {
|
|
328
|
+
const res = await platform.api.tagSearch(this.filters, this.text.trim() || undefined, { limit: 100 });
|
|
329
|
+
search.set({ results: (res.items || []).map((node) => ({ node })), loading: false, ran: true, filtered: true, offline: false });
|
|
330
|
+
} catch (err) {
|
|
331
|
+
search.set({ loading: false, error: err.message, ran: true, filtered: true });
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Command-palette quick-open: a keyword file search whose results live in the search
|
|
337
|
+
* service (state.se.paletteFiles) instead of ad-hoc state hung off the UI. */
|
|
338
|
+
export class QuickOpenAction extends AppAction {
|
|
339
|
+
constructor(query) {
|
|
340
|
+
super();
|
|
341
|
+
this.query = query;
|
|
342
|
+
}
|
|
343
|
+
async execute({ app }) {
|
|
344
|
+
const { search, platform } = app;
|
|
345
|
+
const q = (this.query || '').trim();
|
|
346
|
+
if (!q) { search.set({ paletteFiles: [], paletteQuery: '', paletteError: null, paletteLoading: false }); return; }
|
|
347
|
+
// Keystrokes outrun the network: a slower request for an earlier query must not
|
|
348
|
+
// land on top of a newer one's results. The query itself is the sequence token —
|
|
349
|
+
// whatever the palette input holds now is the only answer worth showing.
|
|
350
|
+
search.set({ paletteQuery: q, paletteLoading: true, paletteError: null });
|
|
351
|
+
try {
|
|
352
|
+
const res = await platform.api.search(q, { mode: 'keyword', limit: 30 });
|
|
353
|
+
if (search.state.paletteQuery !== q) return; // superseded
|
|
354
|
+
search.set({ paletteFiles: res.results || [], paletteLoading: false });
|
|
355
|
+
} catch (err) {
|
|
356
|
+
if (search.state.paletteQuery !== q) return;
|
|
357
|
+
// A failed search must not look like "no files matched" — that reads as a fact
|
|
358
|
+
// about the drive when it's actually a fact about the request.
|
|
359
|
+
search.set({ paletteFiles: [], paletteLoading: false, paletteError: err?.message || 'Search failed' });
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export class SearchAction extends AppAction {
|
|
365
|
+
constructor(query, mode) {
|
|
366
|
+
super();
|
|
367
|
+
this.query = query;
|
|
368
|
+
this.mode = mode;
|
|
369
|
+
}
|
|
370
|
+
async execute({ app }) {
|
|
371
|
+
const { search, platform } = app;
|
|
372
|
+
const q = this.query.trim();
|
|
373
|
+
if (!q) {
|
|
374
|
+
search.set({ query: '', results: [], ran: false, error: null, resolved: null });
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const mode = this.mode || platform.settings.get('search.mode');
|
|
378
|
+
search.set({ query: this.query, mode, loading: true, error: null, resolved: null });
|
|
379
|
+
const offline = app.offline;
|
|
380
|
+
// Offline (or server unreachable) → search the pinned corpus locally.
|
|
381
|
+
if (offline && !offline.state.online) {
|
|
382
|
+
try {
|
|
383
|
+
const results = await offline.searchOffline(q, { limit: 40 });
|
|
384
|
+
search.set({ results, loading: false, ran: true, offline: true, resolved: null });
|
|
385
|
+
} catch (err) {
|
|
386
|
+
search.set({ results: [], loading: false, ran: true, offline: true, error: err.message });
|
|
387
|
+
}
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
try {
|
|
391
|
+
// The server transforms the raw query (parse/LLM) and tells us what it actually
|
|
392
|
+
// searched — surfaced to the user via `resolved` so search is honest.
|
|
393
|
+
//
|
|
394
|
+
// We send the views we can draw with, because the transformer is the only thing in
|
|
395
|
+
// the stack that read the sentence: "photos from the trip last summer" asks for a
|
|
396
|
+
// gallery, and by the time the results are back all anyone can do is guess from
|
|
397
|
+
// content types. It can only name a view from this list.
|
|
398
|
+
const views = platform.contributions.ofType('view').map((v) => ({ id: v.id, title: v.title || v.id }));
|
|
399
|
+
const res = await platform.api.query(q, { mode, limit: 40, views });
|
|
400
|
+
search.set({ results: res.results, resolved: res.resolved || null, loading: false, ran: true, offline: false });
|
|
401
|
+
} catch (err) {
|
|
402
|
+
if (offline) {
|
|
403
|
+
const results = await offline.searchOffline(q, { limit: 40 });
|
|
404
|
+
search.set({ results, loading: false, ran: true, offline: true, error: results.length ? null : err.message });
|
|
405
|
+
} else {
|
|
406
|
+
search.set({ loading: false, error: err.message, ran: true });
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|