@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,86 @@
|
|
|
1
|
+
// A small, allowlist-only HTML sanitizer for content a PLUGIN produces (status bar
|
|
2
|
+
// items today). Plugin code runs on an opaque origin with no reach into the host DOM;
|
|
3
|
+
// anything it hands us to render is untrusted string data and must not be able to
|
|
4
|
+
// become script, load a resource, or navigate.
|
|
5
|
+
//
|
|
6
|
+
// Allowlist, not denylist: parse into an inert document, then rebuild only the nodes
|
|
7
|
+
// and attributes named below. Anything unrecognised — an element, an attribute, a
|
|
8
|
+
// URL scheme — is dropped rather than "cleaned", so a novel injection has nothing to
|
|
9
|
+
// slip through. `template` parsing is inert: no scripts execute and no images load
|
|
10
|
+
// while we walk it.
|
|
11
|
+
|
|
12
|
+
const ALLOWED_TAGS = new Set(['SPAN', 'B', 'STRONG', 'I', 'EM', 'CODE', 'SMALL', 'BR', 'S', 'U']);
|
|
13
|
+
const ALLOWED_ATTRS = new Set(['class', 'title']);
|
|
14
|
+
// Cap the output so a runaway plugin can't wedge the shell with a megabyte of markup.
|
|
15
|
+
const MAX_LENGTH = 4096;
|
|
16
|
+
const MAX_NODES = 200;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Sanitize `html` into a DocumentFragment safe to insert into the host DOM.
|
|
20
|
+
* @param {string} html
|
|
21
|
+
* @returns {DocumentFragment}
|
|
22
|
+
*/
|
|
23
|
+
export function sanitizeFragment(html) {
|
|
24
|
+
const out = document.createDocumentFragment();
|
|
25
|
+
if (typeof html !== 'string' || !html) return out;
|
|
26
|
+
const tpl = document.createElement('template');
|
|
27
|
+
tpl.innerHTML = html.slice(0, MAX_LENGTH);
|
|
28
|
+
const budget = { nodes: MAX_NODES };
|
|
29
|
+
for (const child of tpl.content.childNodes) {
|
|
30
|
+
const clean = clone(child, budget);
|
|
31
|
+
if (clean) out.appendChild(clean);
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Sanitize `html` and set it as the only content of `el`. */
|
|
37
|
+
export function setSanitizedHtml(el, html) {
|
|
38
|
+
el.replaceChildren(sanitizeFragment(html));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The plain-text content of `html`, for tooltips and other attribute contexts. */
|
|
42
|
+
export function htmlToText(html) {
|
|
43
|
+
const frag = sanitizeFragment(html);
|
|
44
|
+
return (frag.textContent || '').trim();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Sanitize `html` into VDOM nodes, so plugin content reconciles like everything else
|
|
49
|
+
* instead of being an opaque innerHTML island the renderer can't see into.
|
|
50
|
+
* @param {string} html
|
|
51
|
+
* @param {Record<string, Function>} dd the element factories (dodo's `dd`)
|
|
52
|
+
*/
|
|
53
|
+
export function sanitizedVNodes(html, dd) {
|
|
54
|
+
return [...sanitizeFragment(html).childNodes].map((n) => toVNode(n, dd)).filter((n) => n != null);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function toVNode(node, dd) {
|
|
58
|
+
if (node.nodeType === Node.TEXT_NODE) return node.nodeValue;
|
|
59
|
+
const factory = dd[node.tagName.toLowerCase()];
|
|
60
|
+
if (!factory) return node.textContent; // allowed tag the renderer doesn't model
|
|
61
|
+
const props = {};
|
|
62
|
+
if (node.className) props.className = node.className;
|
|
63
|
+
if (node.title) props.title = node.title;
|
|
64
|
+
return factory(props, ...[...node.childNodes].map((c) => toVNode(c, dd)).filter((n) => n != null));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function clone(node, budget) {
|
|
68
|
+
if (budget.nodes-- <= 0) return null;
|
|
69
|
+
if (node.nodeType === Node.TEXT_NODE) return document.createTextNode(node.nodeValue);
|
|
70
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return null; // comments, CDATA, …
|
|
71
|
+
if (!ALLOWED_TAGS.has(node.tagName)) return null;
|
|
72
|
+
|
|
73
|
+
const el = document.createElement(node.tagName.toLowerCase());
|
|
74
|
+
for (const attr of node.attributes) {
|
|
75
|
+
const name = attr.name.toLowerCase();
|
|
76
|
+
// `on*` handlers never reach here (not in the allowlist), but be explicit: no
|
|
77
|
+
// event handler, no `style` (CSS can exfiltrate via url()), no URL attributes.
|
|
78
|
+
if (!ALLOWED_ATTRS.has(name)) continue;
|
|
79
|
+
el.setAttribute(name, attr.value.slice(0, 200));
|
|
80
|
+
}
|
|
81
|
+
for (const child of node.childNodes) {
|
|
82
|
+
const clean = clone(child, budget);
|
|
83
|
+
if (clean) el.appendChild(clean);
|
|
84
|
+
}
|
|
85
|
+
return el;
|
|
86
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// Booting a workbench — the whole client, as a function you call with what you want
|
|
2
|
+
// added to it.
|
|
3
|
+
//
|
|
4
|
+
// This used to be `main.js`: a fixed sequence that registered a fixed set of openers
|
|
5
|
+
// and mounted. Which is fine until someone wants to SHIP a different drive — a hosted
|
|
6
|
+
// build with a rich media player, an image-caption indexer and a gallery view — and
|
|
7
|
+
// finds the only way in is to fork the package. Every contribution the built-ins use
|
|
8
|
+
// goes through the same registry a plugin's does, so there was never a technical reason
|
|
9
|
+
// for that; there was only a hard-coded list.
|
|
10
|
+
//
|
|
11
|
+
// import { createWorkbench } from '@3sln/trove/web/workbench.js';
|
|
12
|
+
//
|
|
13
|
+
// createWorkbench({
|
|
14
|
+
// openers: [{ id: 'acme.player', title: 'Player', priority: 60,
|
|
15
|
+
// match: { mime: ['video/*'] }, component: player }],
|
|
16
|
+
// views: [{ id: 'acme.gallery', title: 'Gallery', icon: 'grid', priority: 70,
|
|
17
|
+
// match: { mime: ['image/*'] }, render: gallery }],
|
|
18
|
+
// });
|
|
19
|
+
//
|
|
20
|
+
// Two ways to displace a built-in, both falling out of how the registry already works:
|
|
21
|
+
// register a HIGHER PRIORITY (selection sorts by it, and the user can still pick the
|
|
22
|
+
// plain one from the chooser), or register the SAME ID (the registry is a map, so it is
|
|
23
|
+
// replaced outright). Prefer the first — it leaves the original reachable.
|
|
24
|
+
//
|
|
25
|
+
// These openers and views run IN PROCESS. That is the difference between a build of the
|
|
26
|
+
// app and a plugin, and it cuts both ways: full access to the platform, and no sandbox
|
|
27
|
+
// to catch a mistake. Anything a user installs is a plugin and stays in its frame.
|
|
28
|
+
|
|
29
|
+
import { dd, effect } from './runtime.js';
|
|
30
|
+
import { createPlatform } from './platform/index.js';
|
|
31
|
+
import { createApp } from './bl/index.js';
|
|
32
|
+
import { NavigateAction, UploadFilesAction, OpenInitialCollectionAction } from './bl/actions.js';
|
|
33
|
+
import { parsePackage } from './platform/pluginPackage.js';
|
|
34
|
+
import workbenchComposition from './ui/compositions/workbench.js';
|
|
35
|
+
import { registerBuiltinOpeners } from './ui/components/openers/index.js';
|
|
36
|
+
import { registerBuiltinViews } from './ui/components/views/index.js';
|
|
37
|
+
import { attachMedia } from './ui/media.js';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Build the platform, mount the workbench, and wire the cross-cutting behaviours that
|
|
41
|
+
* belong to no single component — theme, global keybindings, OS drag-and-drop upload,
|
|
42
|
+
* capability probing, the plugin catalog.
|
|
43
|
+
*
|
|
44
|
+
* @param {object} [options]
|
|
45
|
+
* @param {Element} [options.root] where to mount (default `.workbench`)
|
|
46
|
+
* @param {string} [options.baseUrl] API origin (default same-origin)
|
|
47
|
+
* @param {Array} [options.openers] extra openers: `{ id, title, match, priority?, component }`
|
|
48
|
+
* @param {Array} [options.views] extra views: `{ id, title, icon?, match?, priority?, render, move? }`
|
|
49
|
+
* @param {Array} [options.settings] extra setting schemas, for whatever those contribute
|
|
50
|
+
* @param {boolean} [options.serviceWorker] register /sw.js (default true)
|
|
51
|
+
* @returns {{platform: object, engine: object, app: object}}
|
|
52
|
+
*/
|
|
53
|
+
export function createWorkbench({
|
|
54
|
+
root = document.querySelector('.workbench'),
|
|
55
|
+
baseUrl = '',
|
|
56
|
+
openers = [],
|
|
57
|
+
views = [],
|
|
58
|
+
settings = [],
|
|
59
|
+
serviceWorker = true,
|
|
60
|
+
} = {}) {
|
|
61
|
+
const platform = createPlatform({ baseUrl });
|
|
62
|
+
const { engine, app } = createApp(platform);
|
|
63
|
+
|
|
64
|
+
// Built-in openers and views are UI, so they register from the composition layer
|
|
65
|
+
// (which is allowed to know both platform and ui) rather than from bl/.
|
|
66
|
+
registerBuiltinOpeners(platform);
|
|
67
|
+
registerBuiltinViews(platform);
|
|
68
|
+
// Then whatever this build adds, so a same-id registration replaces the built-in
|
|
69
|
+
// rather than being replaced by it.
|
|
70
|
+
register(platform, 'opener', openers);
|
|
71
|
+
register(platform, 'view', views);
|
|
72
|
+
if (settings.length) platform.settings.register(settings);
|
|
73
|
+
|
|
74
|
+
// Plugins are installed by the user (from a .zip or a URL) and persisted locally;
|
|
75
|
+
// restore any that were installed on this device, then run.
|
|
76
|
+
platform.plugins.restore();
|
|
77
|
+
|
|
78
|
+
const plugins = { uninstall: (id) => platform.plugins.uninstall(id) };
|
|
79
|
+
|
|
80
|
+
// --- mount ----------------------------------------------------------------
|
|
81
|
+
const App = workbenchComposition({ engine, app, platform, plugins });
|
|
82
|
+
dd.reconcile(root, [App()]);
|
|
83
|
+
|
|
84
|
+
// --- theme ----------------------------------------------------------------
|
|
85
|
+
const applyTheme = () => {
|
|
86
|
+
document.documentElement.dataset.theme = platform.settings.get('workbench.theme') || 'dark';
|
|
87
|
+
};
|
|
88
|
+
applyTheme();
|
|
89
|
+
effect(platform.settings.observe(), () => applyTheme());
|
|
90
|
+
|
|
91
|
+
// --- form factor ----------------------------------------------------------
|
|
92
|
+
// Watch the window so rotating a phone, or dragging a desktop window narrow, re-picks
|
|
93
|
+
// the shell. Also stamps data-layout on <html> for the CSS half of the same decision.
|
|
94
|
+
platform.viewport.install();
|
|
95
|
+
// Ask once whether this browser can transcribe on-device, so the launcher knows
|
|
96
|
+
// whether to offer a microphone. Nothing is downloaded and no permission is requested
|
|
97
|
+
// — see platform/voice.js.
|
|
98
|
+
platform.voice.refresh().catch(() => {});
|
|
99
|
+
// Remote-control navigation, which switches itself on and off with the TV layout.
|
|
100
|
+
platform.spatialNav.install();
|
|
101
|
+
|
|
102
|
+
// --- keybindings ----------------------------------------------------------
|
|
103
|
+
platform.keybindings.install(window);
|
|
104
|
+
|
|
105
|
+
// --- viewer stack ↔ browser history ---------------------------------------
|
|
106
|
+
window.addEventListener('popstate', (e) => platform.workbench.onPopState(e));
|
|
107
|
+
installDoubleShift(platform);
|
|
108
|
+
|
|
109
|
+
// --- service worker (offline shell + pinned files + push) -----------------
|
|
110
|
+
if (serviceWorker && 'serviceWorker' in navigator) {
|
|
111
|
+
window.addEventListener('load', () => navigator.serviceWorker.register('/sw.js').catch(() => {}));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
installDragAndDrop(engine, app);
|
|
115
|
+
|
|
116
|
+
// --- initial load ---------------------------------------------------------
|
|
117
|
+
(async () => {
|
|
118
|
+
try {
|
|
119
|
+
platform.capabilities = await platform.api.capabilities();
|
|
120
|
+
// What the server can do is read straight off `platform`, not held in a store, so
|
|
121
|
+
// nothing invalidated when it arrived. Re-pushing the same state object will not
|
|
122
|
+
// do it either: a cell compares with Object.is and drops a write of what it
|
|
123
|
+
// already holds. Saying "this changed under you" is the store's job.
|
|
124
|
+
platform.workbench.touch(); // the status bar reads capabilities
|
|
125
|
+
} catch (err) {
|
|
126
|
+
platform.notifications.error(`Cannot reach the Trove server: ${err.message}`);
|
|
127
|
+
}
|
|
128
|
+
// Land on a collection the user can actually read — see OpenInitialCollectionAction.
|
|
129
|
+
engine.dispatch(new OpenInitialCollectionAction());
|
|
130
|
+
})();
|
|
131
|
+
|
|
132
|
+
// Expose for debugging / e2e.
|
|
133
|
+
window.__trove = {
|
|
134
|
+
platform, engine, app,
|
|
135
|
+
// Test/automation hook: install a package from raw zip bytes.
|
|
136
|
+
test: {
|
|
137
|
+
parsePackage,
|
|
138
|
+
assessTrust: (pkg) => platform.plugins.assessTrust(pkg),
|
|
139
|
+
install: (pkg, opts) => platform.plugins.install(pkg, opts),
|
|
140
|
+
NavigateAction,
|
|
141
|
+
// Keeping a media element pointed at a URL that still works is the half of signed
|
|
142
|
+
// URLs that only shows up over time, so it is drivable from a probe.
|
|
143
|
+
attachMedia,
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
return { platform, engine, app };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Register a build's own contributions.
|
|
152
|
+
*
|
|
153
|
+
* `id` is required and it is not a formality: an unaddressed contribution cannot be
|
|
154
|
+
* remembered as someone's default opener, cannot be pinned as their view, and cannot be
|
|
155
|
+
* named in a keymap. Failing here is far better than shipping a build whose media
|
|
156
|
+
* player silently forgets which one the user chose.
|
|
157
|
+
*/
|
|
158
|
+
function register(platform, type, list) {
|
|
159
|
+
for (const c of list) {
|
|
160
|
+
if (!c?.id) throw new Error(`A ${type} passed to createWorkbench needs an "id"`);
|
|
161
|
+
const { id, ...spec } = c;
|
|
162
|
+
platform.contributions.register(id, { type, ...spec });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Double-shift → modal search overlay (Raycast-style). Two Shift presses within 400ms,
|
|
167
|
+
// with no other key between, open it.
|
|
168
|
+
function installDoubleShift(platform) {
|
|
169
|
+
let lastShift = 0;
|
|
170
|
+
window.addEventListener('keydown', (e) => {
|
|
171
|
+
if (e.key === 'Shift' && !e.repeat) {
|
|
172
|
+
const now = Date.now();
|
|
173
|
+
if (now - lastShift < 400) { platform.workbench.openSearchModal(); lastShift = 0; }
|
|
174
|
+
else lastShift = now;
|
|
175
|
+
} else if (e.key !== 'Shift') {
|
|
176
|
+
lastShift = 0;
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Dropping files anywhere on the window uploads them to the open collection.
|
|
182
|
+
function installDragAndDrop(engine, app) {
|
|
183
|
+
let dragDepth = 0;
|
|
184
|
+
window.addEventListener('dragenter', (e) => {
|
|
185
|
+
if (!e.dataTransfer?.types.includes('Files')) return;
|
|
186
|
+
dragDepth++;
|
|
187
|
+
document.body.classList.add('dragging-file');
|
|
188
|
+
});
|
|
189
|
+
window.addEventListener('dragover', (e) => {
|
|
190
|
+
if (e.dataTransfer?.types.includes('Files')) e.preventDefault();
|
|
191
|
+
});
|
|
192
|
+
window.addEventListener('dragleave', () => {
|
|
193
|
+
if (--dragDepth <= 0) {
|
|
194
|
+
dragDepth = 0;
|
|
195
|
+
document.body.classList.remove('dragging-file');
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
window.addEventListener('drop', (e) => {
|
|
199
|
+
if (!e.dataTransfer?.files?.length) return;
|
|
200
|
+
e.preventDefault();
|
|
201
|
+
dragDepth = 0;
|
|
202
|
+
document.body.classList.remove('dragging-file');
|
|
203
|
+
engine.dispatch(new UploadFilesAction(e.dataTransfer.files, app.explorer.state.collectionId));
|
|
204
|
+
});
|
|
205
|
+
}
|