@walkthru-earth/objex 0.1.0 → 1.1.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 +9 -2
- package/dist/components/browser/FileBrowser.svelte +53 -41
- package/dist/components/browser/FileRow.svelte +8 -3
- package/dist/components/browser/FileTreeSidebar.svelte +2 -4
- package/dist/components/layout/AboutSheet.svelte +126 -0
- package/dist/components/layout/AboutSheet.svelte.d.ts +6 -0
- package/dist/components/layout/ConnectionDialog.svelte +186 -138
- package/dist/components/layout/ConnectionDialog.svelte.d.ts +1 -0
- package/dist/components/layout/Sidebar.svelte +19 -3
- package/dist/components/layout/TabBar.svelte +4 -7
- package/dist/components/viewers/CodeViewer.svelte +17 -9
- package/dist/components/viewers/ImageViewer.svelte +6 -16
- package/dist/components/viewers/MarkdownViewer.svelte +8 -16
- package/dist/components/viewers/MediaViewer.svelte +6 -17
- package/dist/components/viewers/ModelViewer.svelte +4 -2
- package/dist/components/viewers/NotebookViewer.svelte +90 -40
- package/dist/components/viewers/PdfViewer.svelte +5 -3
- package/dist/components/viewers/RawViewer.svelte +4 -2
- package/dist/components/viewers/TableGrid.svelte +3 -2
- package/dist/components/viewers/ZarrMapViewer.svelte +334 -40
- package/dist/components/viewers/ZarrMapViewer.svelte.d.ts +3 -8
- package/dist/components/viewers/ZarrViewer.svelte +459 -178
- package/dist/components/viewers/map/AttributeTable.svelte +1 -6
- package/dist/components/viewers/pmtiles/PmtilesArchiveView.svelte +2 -6
- package/dist/components/viewers/pmtiles/PmtilesTileInspector.svelte +96 -22
- package/dist/constants.d.ts +28 -0
- package/dist/constants.js +34 -0
- package/dist/file-icons/index.js +6 -0
- package/dist/i18n/ar.js +34 -0
- package/dist/i18n/en.js +34 -0
- package/dist/index.d.ts +13 -1
- package/dist/index.js +16 -1
- package/dist/query/wasm.js +5 -4
- package/dist/storage/browser-cloud.d.ts +7 -0
- package/dist/storage/browser-cloud.js +74 -7
- package/dist/storage/providers.d.ts +53 -0
- package/dist/storage/providers.js +318 -0
- package/dist/stores/connections.svelte.js +8 -34
- package/dist/stores/files.svelte.d.ts +1 -6
- package/dist/stores/files.svelte.js +4 -36
- package/dist/stores/query-history.svelte.js +5 -28
- package/dist/stores/settings.svelte.d.ts +1 -0
- package/dist/stores/settings.svelte.js +11 -31
- package/dist/types.d.ts +2 -2
- package/dist/utils/clipboard.d.ts +13 -0
- package/dist/utils/clipboard.js +38 -0
- package/dist/utils/cloud-url.d.ts +27 -0
- package/dist/utils/cloud-url.js +61 -0
- package/dist/utils/error.d.ts +8 -0
- package/dist/utils/error.js +12 -0
- package/dist/utils/export.d.ts +22 -2
- package/dist/utils/export.js +35 -10
- package/dist/utils/file-sort.d.ts +20 -0
- package/dist/utils/file-sort.js +41 -0
- package/dist/utils/format.d.ts +10 -0
- package/dist/utils/format.js +22 -0
- package/dist/utils/host-detection.js +78 -18
- package/dist/utils/local-storage.d.ts +16 -0
- package/dist/utils/local-storage.js +37 -0
- package/dist/utils/notebook.d.ts +59 -0
- package/dist/utils/notebook.js +211 -0
- package/dist/utils/parquet-metadata.js +1 -1
- package/dist/utils/pmtiles-tile.js +2 -1
- package/dist/utils/pmtiles.js +2 -1
- package/dist/utils/storage-url.d.ts +1 -1
- package/dist/utils/storage-url.js +82 -24
- package/dist/utils/url-state.js +2 -7
- package/dist/utils/url.d.ts +0 -2
- package/dist/utils/url.js +3 -29
- package/dist/utils/zarr.d.ts +60 -20
- package/dist/utils/zarr.js +450 -103
- package/package.json +66 -54
- package/dist/assets/favicon.svg +0 -17
- package/dist/components/CLAUDE.md +0 -44
- package/dist/components/viewers/CLAUDE.md +0 -60
- package/dist/file-icons/CLAUDE.md +0 -21
- package/dist/i18n/CLAUDE.md +0 -19
- package/dist/query/CLAUDE.md +0 -22
- package/dist/storage/CLAUDE.md +0 -23
- package/dist/stores/CLAUDE.md +0 -29
- package/dist/types/notebookjs.d.ts +0 -14
- package/dist/utils/CLAUDE.md +0 -54
- package/dist/utils/analytics.d.ts +0 -10
- package/dist/utils/analytics.js +0 -38
|
@@ -18,6 +18,13 @@ import {
|
|
|
18
18
|
} from '../ui/sheet/index.js';
|
|
19
19
|
import { Switch } from '../ui/switch/index.js';
|
|
20
20
|
import { t } from '../../i18n/index.svelte.js';
|
|
21
|
+
import {
|
|
22
|
+
buildEndpointFromTemplate,
|
|
23
|
+
getProvider,
|
|
24
|
+
PROVIDER_IDS,
|
|
25
|
+
PROVIDERS,
|
|
26
|
+
type ProviderId
|
|
27
|
+
} from '../../storage/providers.js';
|
|
21
28
|
import { connections } from '../../stores/connections.svelte.js';
|
|
22
29
|
import type { Connection, ConnectionConfig } from '../../types.js';
|
|
23
30
|
import { describeParseResult, looksLikeUrl, parseStorageUrl } from '../../utils/storage-url.js';
|
|
@@ -26,24 +33,24 @@ interface Props {
|
|
|
26
33
|
open: boolean;
|
|
27
34
|
editConnection?: Connection | null;
|
|
28
35
|
onSaved?: () => void;
|
|
36
|
+
onClose?: () => void;
|
|
29
37
|
}
|
|
30
38
|
|
|
31
|
-
let {
|
|
39
|
+
let {
|
|
40
|
+
open = $bindable(false),
|
|
41
|
+
editConnection = null,
|
|
42
|
+
onSaved = () => {},
|
|
43
|
+
onClose = () => {}
|
|
44
|
+
}: Props = $props();
|
|
32
45
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
{ value: 'r2', label: 'R2' },
|
|
37
|
-
{ value: 'minio', label: 'MinIO' },
|
|
38
|
-
{ value: 'azure', label: 'Azure' },
|
|
39
|
-
{ value: 'storj', label: 'Storj' }
|
|
40
|
-
];
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Form state
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
41
49
|
|
|
42
|
-
// Form state — initialized with defaults, then reset via $effect when editConnection changes
|
|
43
50
|
let name = $state('');
|
|
44
|
-
let provider = $state<
|
|
51
|
+
let provider = $state<ProviderId>('s3');
|
|
45
52
|
let bucket = $state('');
|
|
46
|
-
let region = $state('us-
|
|
53
|
+
let region = $state('us-east-1');
|
|
47
54
|
let endpoint = $state('');
|
|
48
55
|
let anonymous = $state(true);
|
|
49
56
|
let accessKey = $state('');
|
|
@@ -53,11 +60,79 @@ let saving = $state(false);
|
|
|
53
60
|
let testing = $state(false);
|
|
54
61
|
let testResult = $state<'success' | 'error' | null>(null);
|
|
55
62
|
let parsedHint = $state<string | null>(null);
|
|
63
|
+
let endpointAutoFilled = $state(false);
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Derived state from provider registry
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
56
68
|
|
|
69
|
+
let providerDef = $derived(getProvider(provider));
|
|
57
70
|
let isAzure = $derived(provider === 'azure');
|
|
58
|
-
let
|
|
59
|
-
let needsRegion = $derived(
|
|
60
|
-
let bucketLabel = $derived(
|
|
71
|
+
let hasRegions = $derived(providerDef.regions.length > 0);
|
|
72
|
+
let needsRegion = $derived(providerDef.needsRegion);
|
|
73
|
+
let bucketLabel = $derived(providerDef.bucketLabel ?? t('connection.bucket'));
|
|
74
|
+
|
|
75
|
+
let isEditMode = $derived(editConnection !== null && editConnection !== undefined);
|
|
76
|
+
let title = $derived(isEditMode ? t('connection.editTitle') : t('connection.newTitle'));
|
|
77
|
+
let canSave = $derived(
|
|
78
|
+
name.trim() !== '' &&
|
|
79
|
+
bucket.trim() !== '' &&
|
|
80
|
+
(!needsRegion || region.trim() !== '') &&
|
|
81
|
+
(!providerDef.needsEndpoint || endpoint.trim() !== '')
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Form helpers
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
function resetForm(conn: Connection | null | undefined) {
|
|
89
|
+
const def = conn ? getProvider(conn.provider) : PROVIDERS.s3;
|
|
90
|
+
name = conn?.name ?? '';
|
|
91
|
+
provider = (conn?.provider as ProviderId) ?? 's3';
|
|
92
|
+
bucket = conn?.bucket ?? '';
|
|
93
|
+
region = conn?.region ?? def.defaultRegion;
|
|
94
|
+
endpoint = conn?.endpoint ?? '';
|
|
95
|
+
anonymous = conn?.anonymous ?? true;
|
|
96
|
+
accessKey = '';
|
|
97
|
+
secretKey = '';
|
|
98
|
+
sasToken = '';
|
|
99
|
+
saving = false;
|
|
100
|
+
testing = false;
|
|
101
|
+
testResult = null;
|
|
102
|
+
parsedHint = null;
|
|
103
|
+
endpointAutoFilled = false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function selectProvider(id: ProviderId) {
|
|
107
|
+
const prev = provider;
|
|
108
|
+
if (id === prev) return;
|
|
109
|
+
|
|
110
|
+
provider = id;
|
|
111
|
+
const def = getProvider(id);
|
|
112
|
+
|
|
113
|
+
// Clear auto-filled endpoint from previous provider
|
|
114
|
+
if (endpointAutoFilled) {
|
|
115
|
+
endpoint = '';
|
|
116
|
+
endpointAutoFilled = false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Set default region
|
|
120
|
+
region = def.defaultRegion;
|
|
121
|
+
|
|
122
|
+
// Auto-fill endpoint from template if available and not user-typed
|
|
123
|
+
if (!endpoint && def.endpointTemplate) {
|
|
124
|
+
endpoint = buildEndpointFromTemplate(id, def.defaultRegion);
|
|
125
|
+
endpointAutoFilled = true;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function selectRegion(regionCode: string) {
|
|
130
|
+
region = regionCode;
|
|
131
|
+
// Update endpoint if it was auto-filled from template
|
|
132
|
+
if (endpointAutoFilled && providerDef.endpointTemplate) {
|
|
133
|
+
endpoint = buildEndpointFromTemplate(provider, regionCode);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
61
136
|
|
|
62
137
|
function handleBucketInput(value: string) {
|
|
63
138
|
bucket = value;
|
|
@@ -80,50 +155,13 @@ function applyParsedUrl() {
|
|
|
80
155
|
bucket = parsed.bucket;
|
|
81
156
|
if (parsed.endpoint) endpoint = parsed.endpoint;
|
|
82
157
|
if (parsed.region) region = parsed.region;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
s3: 's3',
|
|
86
|
-
gcs: 'gcs',
|
|
87
|
-
r2: 'r2',
|
|
88
|
-
minio: 'minio',
|
|
89
|
-
azure: 'azure',
|
|
90
|
-
storj: 'storj'
|
|
91
|
-
};
|
|
92
|
-
if (parsed.provider in providerMap) {
|
|
93
|
-
provider = providerMap[parsed.provider];
|
|
158
|
+
if (parsed.provider && parsed.provider !== 'unknown' && parsed.provider in PROVIDERS) {
|
|
159
|
+
provider = parsed.provider as ProviderId;
|
|
94
160
|
}
|
|
95
161
|
parsedHint = null;
|
|
96
162
|
}
|
|
97
163
|
|
|
98
|
-
|
|
99
|
-
let title = $derived(isEditMode ? t('connection.editTitle') : t('connection.newTitle'));
|
|
100
|
-
let canSave = $derived(
|
|
101
|
-
name.trim() !== '' && bucket.trim() !== '' && (!needsRegion || region.trim() !== '')
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
// Reset form fields when editConnection changes
|
|
105
|
-
$effect(() => {
|
|
106
|
-
const conn = editConnection;
|
|
107
|
-
name = conn?.name ?? '';
|
|
108
|
-
provider = conn?.provider ?? 's3';
|
|
109
|
-
bucket = conn?.bucket ?? '';
|
|
110
|
-
region = conn?.region ?? 'us-west-2';
|
|
111
|
-
endpoint = conn?.endpoint ?? '';
|
|
112
|
-
anonymous = conn?.anonymous ?? true;
|
|
113
|
-
accessKey = '';
|
|
114
|
-
secretKey = '';
|
|
115
|
-
sasToken = '';
|
|
116
|
-
saving = false;
|
|
117
|
-
testing = false;
|
|
118
|
-
testResult = null;
|
|
119
|
-
parsedHint = null;
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
async function handleSave() {
|
|
123
|
-
if (!canSave) return;
|
|
124
|
-
saving = true;
|
|
125
|
-
|
|
126
|
-
// Auto-parse URL in bucket field before saving
|
|
164
|
+
function buildConfig(fallbackName?: string): ConnectionConfig {
|
|
127
165
|
let finalBucket = bucket.trim();
|
|
128
166
|
let finalRegion = region.trim();
|
|
129
167
|
let finalEndpoint = endpoint.trim();
|
|
@@ -136,29 +174,56 @@ async function handleSave() {
|
|
|
136
174
|
if (parsed.endpoint) finalEndpoint = parsed.endpoint;
|
|
137
175
|
if (parsed.region) finalRegion = parsed.region;
|
|
138
176
|
}
|
|
177
|
+
const def = getProvider(provider);
|
|
178
|
+
return {
|
|
179
|
+
name: name.trim() || fallbackName || '',
|
|
180
|
+
provider,
|
|
181
|
+
bucket: finalBucket,
|
|
182
|
+
region: finalRegion,
|
|
183
|
+
endpoint: finalEndpoint,
|
|
184
|
+
anonymous,
|
|
185
|
+
authMethod:
|
|
186
|
+
def.authMethod === 'sas-token' && !anonymous ? 'sas-token' : !anonymous ? 'sigv4' : undefined,
|
|
187
|
+
...(anonymous
|
|
188
|
+
? {}
|
|
189
|
+
: def.authMethod === 'sas-token'
|
|
190
|
+
? { sas_token: sasToken }
|
|
191
|
+
: { access_key: accessKey, secret_key: secretKey })
|
|
192
|
+
};
|
|
193
|
+
}
|
|
139
194
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
provider,
|
|
144
|
-
bucket: finalBucket,
|
|
145
|
-
region: finalRegion,
|
|
146
|
-
endpoint: finalEndpoint,
|
|
147
|
-
anonymous,
|
|
148
|
-
authMethod: isAzure && !anonymous ? 'sas-token' : !anonymous ? 'sigv4' : undefined,
|
|
149
|
-
...(anonymous
|
|
150
|
-
? {}
|
|
151
|
-
: isAzure
|
|
152
|
-
? { sas_token: sasToken }
|
|
153
|
-
: { access_key: accessKey, secret_key: secretKey })
|
|
154
|
-
};
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
// Effects
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
155
198
|
|
|
199
|
+
// Reset form when dialog opens
|
|
200
|
+
$effect(() => {
|
|
201
|
+
if (open) {
|
|
202
|
+
resetForm(editConnection);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Notify parent when dialog closes
|
|
207
|
+
$effect(() => {
|
|
208
|
+
if (!open) {
|
|
209
|
+
onClose();
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// Actions
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
async function handleSave() {
|
|
218
|
+
if (!canSave) return;
|
|
219
|
+
saving = true;
|
|
220
|
+
try {
|
|
221
|
+
const config = buildConfig();
|
|
156
222
|
if (isEditMode && editConnection) {
|
|
157
223
|
await connections.update(editConnection.id, config);
|
|
158
224
|
} else {
|
|
159
225
|
await connections.save(config);
|
|
160
226
|
}
|
|
161
|
-
|
|
162
227
|
onSaved();
|
|
163
228
|
open = false;
|
|
164
229
|
} catch (err) {
|
|
@@ -171,37 +236,8 @@ async function handleSave() {
|
|
|
171
236
|
async function handleTestConnection() {
|
|
172
237
|
testing = true;
|
|
173
238
|
testResult = null;
|
|
174
|
-
|
|
175
239
|
try {
|
|
176
|
-
|
|
177
|
-
let finalBucket = bucket.trim();
|
|
178
|
-
let finalRegion = region.trim();
|
|
179
|
-
let finalEndpoint = endpoint.trim();
|
|
180
|
-
if (looksLikeUrl(finalBucket)) {
|
|
181
|
-
const parsed = parseStorageUrl(finalBucket, {
|
|
182
|
-
region: finalRegion || undefined,
|
|
183
|
-
endpoint: finalEndpoint || undefined
|
|
184
|
-
});
|
|
185
|
-
finalBucket = parsed.bucket;
|
|
186
|
-
if (parsed.endpoint) finalEndpoint = parsed.endpoint;
|
|
187
|
-
if (parsed.region) finalRegion = parsed.region;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const config: ConnectionConfig = {
|
|
191
|
-
name: name.trim() || 'test',
|
|
192
|
-
provider,
|
|
193
|
-
bucket: finalBucket,
|
|
194
|
-
region: finalRegion,
|
|
195
|
-
endpoint: finalEndpoint,
|
|
196
|
-
anonymous,
|
|
197
|
-
authMethod: isAzure && !anonymous ? 'sas-token' : !anonymous ? 'sigv4' : undefined,
|
|
198
|
-
...(anonymous
|
|
199
|
-
? {}
|
|
200
|
-
: isAzure
|
|
201
|
-
? { sas_token: sasToken }
|
|
202
|
-
: { access_key: accessKey, secret_key: secretKey })
|
|
203
|
-
};
|
|
204
|
-
|
|
240
|
+
const config = buildConfig('test');
|
|
205
241
|
const ok = await connections.testWithConfig(config, editConnection?.id);
|
|
206
242
|
testResult = ok ? 'success' : 'error';
|
|
207
243
|
} catch {
|
|
@@ -210,10 +246,6 @@ async function handleTestConnection() {
|
|
|
210
246
|
testing = false;
|
|
211
247
|
}
|
|
212
248
|
}
|
|
213
|
-
|
|
214
|
-
function handleCancel() {
|
|
215
|
-
open = false;
|
|
216
|
-
}
|
|
217
249
|
</script>
|
|
218
250
|
|
|
219
251
|
<Sheet bind:open>
|
|
@@ -248,25 +280,20 @@ function handleCancel() {
|
|
|
248
280
|
<!-- Provider -->
|
|
249
281
|
<fieldset class="flex flex-col gap-1.5">
|
|
250
282
|
<legend class="text-sm font-medium">{t('connection.provider')}</legend>
|
|
251
|
-
<div class="flex flex-wrap gap-
|
|
252
|
-
{#each
|
|
283
|
+
<div class="flex flex-wrap gap-1.5" role="radiogroup" aria-label="Cloud storage provider">
|
|
284
|
+
{#each PROVIDER_IDS as id (id)}
|
|
253
285
|
<Button
|
|
254
|
-
variant={provider ===
|
|
286
|
+
variant={provider === id ? 'default' : 'outline'}
|
|
255
287
|
size="sm"
|
|
256
|
-
class="h-
|
|
257
|
-
aria-pressed={provider ===
|
|
258
|
-
onclick={() =>
|
|
259
|
-
provider = p.value;
|
|
260
|
-
if (p.value === 'storj' && !endpoint) {
|
|
261
|
-
endpoint = 'https://gateway.storjshare.io';
|
|
262
|
-
region = 'us1';
|
|
263
|
-
}
|
|
264
|
-
}}
|
|
288
|
+
class="h-7 px-2.5 text-xs"
|
|
289
|
+
aria-pressed={provider === id}
|
|
290
|
+
onclick={() => selectProvider(id)}
|
|
265
291
|
>
|
|
266
|
-
{
|
|
292
|
+
{PROVIDERS[id].label}
|
|
267
293
|
</Button>
|
|
268
294
|
{/each}
|
|
269
295
|
</div>
|
|
296
|
+
<p class="text-xs text-muted-foreground">{providerDef.description}</p>
|
|
270
297
|
</fieldset>
|
|
271
298
|
|
|
272
299
|
<!-- Bucket / Container -->
|
|
@@ -295,39 +322,60 @@ function handleCancel() {
|
|
|
295
322
|
</p>
|
|
296
323
|
</div>
|
|
297
324
|
|
|
298
|
-
<!-- Region
|
|
325
|
+
<!-- Region -->
|
|
299
326
|
{#if needsRegion}
|
|
300
327
|
<div class="flex flex-col gap-1.5">
|
|
301
328
|
<label for="conn-region" class="text-sm font-medium">
|
|
302
329
|
{t('connection.region')} <span class="text-destructive">*</span>
|
|
303
330
|
</label>
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
331
|
+
{#if hasRegions}
|
|
332
|
+
<!-- Dropdown for providers with known regions -->
|
|
333
|
+
<select
|
|
334
|
+
id="conn-region"
|
|
335
|
+
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
336
|
+
value={region}
|
|
337
|
+
onchange={(e) => selectRegion((e.target as HTMLSelectElement).value)}
|
|
338
|
+
>
|
|
339
|
+
{#each providerDef.regions as r (r.code)}
|
|
340
|
+
<option value={r.code}>{r.label} ({r.code})</option>
|
|
341
|
+
{/each}
|
|
342
|
+
</select>
|
|
343
|
+
{:else}
|
|
344
|
+
<Input
|
|
345
|
+
id="conn-region"
|
|
346
|
+
placeholder={providerDef.defaultRegion}
|
|
347
|
+
bind:value={region}
|
|
348
|
+
/>
|
|
349
|
+
{/if}
|
|
309
350
|
</div>
|
|
310
351
|
{/if}
|
|
311
352
|
|
|
312
353
|
<!-- Endpoint -->
|
|
313
354
|
<div class="flex flex-col gap-1.5">
|
|
314
|
-
<label for="conn-endpoint" class="text-sm font-medium">
|
|
355
|
+
<label for="conn-endpoint" class="text-sm font-medium">
|
|
356
|
+
{t('connection.endpoint')}{providerDef.needsEndpoint ? ' *' : ''}
|
|
357
|
+
</label>
|
|
315
358
|
<Input
|
|
316
359
|
id="conn-endpoint"
|
|
317
|
-
placeholder={
|
|
318
|
-
? 'https://myaccount.blob.core.windows.net'
|
|
319
|
-
: isStorj
|
|
320
|
-
? 'https://gateway.storjshare.io'
|
|
321
|
-
: 'https://custom-endpoint.example.com'}
|
|
360
|
+
placeholder={providerDef.endpointPlaceholder}
|
|
322
361
|
bind:value={endpoint}
|
|
362
|
+
oninput={() => {
|
|
363
|
+
endpointAutoFilled = false;
|
|
364
|
+
}}
|
|
323
365
|
/>
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
366
|
+
{#if providerDef.endpointTemplate && !providerDef.needsEndpoint}
|
|
367
|
+
<p class="text-xs text-muted-foreground">
|
|
368
|
+
{t('connection.endpointHelper')}
|
|
369
|
+
</p>
|
|
370
|
+
{:else if isAzure}
|
|
371
|
+
<p class="text-xs text-muted-foreground">
|
|
372
|
+
{t('connection.azureEndpointHelper')}
|
|
373
|
+
</p>
|
|
374
|
+
{:else}
|
|
375
|
+
<p class="text-xs text-muted-foreground">
|
|
376
|
+
{t('connection.endpointHelper')}
|
|
377
|
+
</p>
|
|
378
|
+
{/if}
|
|
331
379
|
</div>
|
|
332
380
|
|
|
333
381
|
<!-- Anonymous Access -->
|
|
@@ -417,7 +465,7 @@ function handleCancel() {
|
|
|
417
465
|
</Button>
|
|
418
466
|
|
|
419
467
|
<div class="flex min-w-0 flex-1 justify-end gap-2">
|
|
420
|
-
<Button variant="ghost" size="sm" onclick={
|
|
468
|
+
<Button variant="ghost" size="sm" onclick={() => (open = false)} disabled={saving}>
|
|
421
469
|
{t('connection.cancel')}
|
|
422
470
|
</Button>
|
|
423
471
|
|
|
@@ -28,10 +28,12 @@ import type { Connection } from '../../types.js';
|
|
|
28
28
|
import { type DetectedHost, detectHostBucket } from '../../utils/host-detection.js';
|
|
29
29
|
import { parseStorageUrl } from '../../utils/storage-url.js';
|
|
30
30
|
import { clearUrlState, syncUrlParam } from '../../utils/url-state.js';
|
|
31
|
+
import AboutSheet from './AboutSheet.svelte';
|
|
31
32
|
import ConnectionDialog from './ConnectionDialog.svelte';
|
|
32
33
|
import LocaleToggle from './LocaleToggle.svelte';
|
|
33
34
|
import ThemeToggle from './ThemeToggle.svelte';
|
|
34
35
|
|
|
36
|
+
let aboutOpen = $state(false);
|
|
35
37
|
let dialogOpen = $state(false);
|
|
36
38
|
let editingConnection = $state<Connection | null>(null);
|
|
37
39
|
let detectedHost = $state<DetectedHost | null>(null);
|
|
@@ -209,9 +211,17 @@ async function handleBrowseConnection(connection: Connection) {
|
|
|
209
211
|
<TooltipProvider>
|
|
210
212
|
<div class="flex h-full w-12 flex-col items-center bg-sidebar py-2">
|
|
211
213
|
<!-- App icon -->
|
|
212
|
-
<
|
|
213
|
-
<
|
|
214
|
-
|
|
214
|
+
<Tooltip>
|
|
215
|
+
<TooltipTrigger>
|
|
216
|
+
<button
|
|
217
|
+
class="mb-2 flex size-8 items-center justify-center rounded-lg transition-colors hover:bg-accent/50"
|
|
218
|
+
onclick={() => { aboutOpen = true; }}
|
|
219
|
+
>
|
|
220
|
+
<DatabaseIcon class="size-5 text-sidebar-primary" />
|
|
221
|
+
</button>
|
|
222
|
+
</TooltipTrigger>
|
|
223
|
+
<TooltipContent side="right">{t('about.title')}</TooltipContent>
|
|
224
|
+
</Tooltip>
|
|
215
225
|
|
|
216
226
|
<Separator class="mx-2 mb-2" />
|
|
217
227
|
|
|
@@ -296,6 +306,8 @@ async function handleBrowseConnection(connection: Connection) {
|
|
|
296
306
|
</div>
|
|
297
307
|
</TooltipProvider>
|
|
298
308
|
|
|
309
|
+
<AboutSheet bind:open={aboutOpen} />
|
|
310
|
+
|
|
299
311
|
<ConnectionDialog
|
|
300
312
|
bind:open={dialogOpen}
|
|
301
313
|
editConnection={editingConnection}
|
|
@@ -309,6 +321,10 @@ async function handleBrowseConnection(connection: Connection) {
|
|
|
309
321
|
syncUrlParam(conn);
|
|
310
322
|
}
|
|
311
323
|
}
|
|
324
|
+
editingConnection = null;
|
|
312
325
|
handleAutoDetection();
|
|
313
326
|
}}
|
|
327
|
+
onClose={() => {
|
|
328
|
+
editingConnection = null;
|
|
329
|
+
}}
|
|
314
330
|
/>
|
|
@@ -8,6 +8,7 @@ import * as ContextMenu from '../ui/context-menu/index.js';
|
|
|
8
8
|
import { ScrollArea } from '../ui/scroll-area/index.js';
|
|
9
9
|
import { t } from '../../i18n/index.svelte.js';
|
|
10
10
|
import { tabs } from '../../stores/tabs.svelte.js';
|
|
11
|
+
import { copyToClipboard } from '../../utils/clipboard.js';
|
|
11
12
|
import { buildHttpsUrl, buildStorageUrl } from '../../utils/url.js';
|
|
12
13
|
|
|
13
14
|
let { leading }: { leading?: Snippet } = $props();
|
|
@@ -27,13 +28,9 @@ function handleClose(event: MouseEvent, id: string) {
|
|
|
27
28
|
async function handleCopy(type: 'https' | 's3', tab: (typeof tabs.items)[0]) {
|
|
28
29
|
const url = type === 'https' ? buildHttpsUrl(tab) : buildStorageUrl(tab);
|
|
29
30
|
if (!url) return;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
setTimeout(() => (copiedType = null), 2000);
|
|
34
|
-
} catch {
|
|
35
|
-
// clipboard API may fail in some contexts
|
|
36
|
-
}
|
|
31
|
+
await copyToClipboard(url, (copied) => {
|
|
32
|
+
copiedType = copied ? `${type}-${tab.id}` : null;
|
|
33
|
+
});
|
|
37
34
|
}
|
|
38
35
|
</script>
|
|
39
36
|
|
|
@@ -8,6 +8,8 @@ import { t } from '../../i18n/index.svelte.js';
|
|
|
8
8
|
import { getAdapter } from '../../storage/index.js';
|
|
9
9
|
import { tabResources } from '../../stores/tab-resources.svelte.js';
|
|
10
10
|
import type { Tab } from '../../types';
|
|
11
|
+
import { copyToClipboard } from '../../utils/clipboard.js';
|
|
12
|
+
import { handleLoadError } from '../../utils/error.js';
|
|
11
13
|
import { extensionToShikiLang, highlightCode } from '../../utils/shiki';
|
|
12
14
|
import { buildHttpsUrl } from '../../utils/url.js';
|
|
13
15
|
import { getUrlView, updateUrlView } from '../../utils/url-state.js';
|
|
@@ -153,12 +155,23 @@ const language = $derived(languageMap[ext] ?? 'Plain Text');
|
|
|
153
155
|
/** File types that support native formatting */
|
|
154
156
|
const canFormat = $derived(['.json', '.sql', '.css', '.html', '.xml'].includes(ext));
|
|
155
157
|
|
|
158
|
+
// Auto-switch to STAC Browser when STAC JSON is detected (unless URL explicitly set #code)
|
|
159
|
+
let stacAutoSwitched = false;
|
|
160
|
+
$effect(() => {
|
|
161
|
+
if (isStacJson && !stacAutoSwitched && viewMode === 'code' && urlView !== 'code') {
|
|
162
|
+
stacAutoSwitched = true;
|
|
163
|
+
viewMode = 'stac-browser';
|
|
164
|
+
updateUrlView('stac-browser');
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
156
168
|
// Reset iframe view mode when tab changes (component reuse across code-type tabs)
|
|
157
169
|
let prevTabId = '';
|
|
158
170
|
$effect(() => {
|
|
159
171
|
const id = tab.id;
|
|
160
172
|
if (prevTabId && prevTabId !== id) {
|
|
161
173
|
viewMode = isHtml ? 'render' : 'code';
|
|
174
|
+
stacAutoSwitched = false;
|
|
162
175
|
updateUrlView('');
|
|
163
176
|
}
|
|
164
177
|
prevTabId = id;
|
|
@@ -195,8 +208,9 @@ async function loadCode() {
|
|
|
195
208
|
rawCode = new TextDecoder().decode(data);
|
|
196
209
|
html = await highlightCode(rawCode, lang);
|
|
197
210
|
} catch (err) {
|
|
198
|
-
|
|
199
|
-
|
|
211
|
+
const msg = handleLoadError(err);
|
|
212
|
+
if (msg === null) return;
|
|
213
|
+
error = msg;
|
|
200
214
|
} finally {
|
|
201
215
|
loading = false;
|
|
202
216
|
}
|
|
@@ -287,13 +301,7 @@ function setViewMode(mode: 'code' | 'render' | 'stac-browser' | 'kepler' | 'mapu
|
|
|
287
301
|
}
|
|
288
302
|
|
|
289
303
|
async function copyCode() {
|
|
290
|
-
|
|
291
|
-
await navigator.clipboard.writeText(rawCode);
|
|
292
|
-
copied = true;
|
|
293
|
-
setTimeout(() => (copied = false), 2000);
|
|
294
|
-
} catch {
|
|
295
|
-
// clipboard not available
|
|
296
|
-
}
|
|
304
|
+
await copyToClipboard(rawCode, (v) => (copied = v));
|
|
297
305
|
}
|
|
298
306
|
</script>
|
|
299
307
|
|
|
@@ -9,24 +9,14 @@ import { onDestroy } from 'svelte';
|
|
|
9
9
|
import { Badge } from '../ui/badge/index.js';
|
|
10
10
|
import { Button } from '../ui/button/index.js';
|
|
11
11
|
import * as DropdownMenu from '../ui/dropdown-menu/index.js';
|
|
12
|
+
import { getMimeType } from '../../file-icons/index.js';
|
|
12
13
|
import { t } from '../../i18n/index.svelte.js';
|
|
13
14
|
import { getAdapter } from '../../storage/index.js';
|
|
14
15
|
import { tabResources } from '../../stores/tab-resources.svelte.js';
|
|
15
16
|
import type { Tab } from '../../types';
|
|
17
|
+
import { handleLoadError } from '../../utils/error.js';
|
|
16
18
|
import { buildHttpsUrl, canStreamDirectly } from '../../utils/url.js';
|
|
17
19
|
|
|
18
|
-
const mimeMap: Record<string, string> = {
|
|
19
|
-
png: 'image/png',
|
|
20
|
-
jpg: 'image/jpeg',
|
|
21
|
-
jpeg: 'image/jpeg',
|
|
22
|
-
gif: 'image/gif',
|
|
23
|
-
webp: 'image/webp',
|
|
24
|
-
avif: 'image/avif',
|
|
25
|
-
svg: 'image/svg+xml',
|
|
26
|
-
bmp: 'image/bmp',
|
|
27
|
-
ico: 'image/x-icon'
|
|
28
|
-
};
|
|
29
|
-
|
|
30
20
|
let { tab }: { tab: Tab } = $props();
|
|
31
21
|
|
|
32
22
|
let abortController: AbortController | null = null;
|
|
@@ -64,16 +54,16 @@ async function loadImage() {
|
|
|
64
54
|
// Authenticated S3 — download via storage adapter
|
|
65
55
|
const adapter = getAdapter(tab.source, tab.connectionId);
|
|
66
56
|
const data = await adapter.read(tab.path, undefined, undefined, signal);
|
|
67
|
-
const ext = tab.extension.toLowerCase();
|
|
68
57
|
const blob = new Blob([data as unknown as BlobPart], {
|
|
69
|
-
type:
|
|
58
|
+
type: getMimeType(tab.extension)
|
|
70
59
|
});
|
|
71
60
|
blobUrl = URL.createObjectURL(blob);
|
|
72
61
|
imgSrc = blobUrl;
|
|
73
62
|
}
|
|
74
63
|
} catch (err) {
|
|
75
|
-
|
|
76
|
-
|
|
64
|
+
const msg = handleLoadError(err);
|
|
65
|
+
if (msg === null) return;
|
|
66
|
+
error = msg;
|
|
77
67
|
} finally {
|
|
78
68
|
loading = false;
|
|
79
69
|
}
|
|
@@ -7,6 +7,8 @@ import { t } from '../../i18n/index.svelte.js';
|
|
|
7
7
|
import { getAdapter } from '../../storage/index.js';
|
|
8
8
|
import { tabResources } from '../../stores/tab-resources.svelte.js';
|
|
9
9
|
import type { Tab } from '../../types';
|
|
10
|
+
import { wireCodeCopyButtons } from '../../utils/clipboard.js';
|
|
11
|
+
import { handleLoadError } from '../../utils/error.js';
|
|
10
12
|
import { EvidenceContext } from '../../utils/evidence-context';
|
|
11
13
|
import { detectRTL, processDirection, renderMarkdown } from '../../utils/markdown';
|
|
12
14
|
import {
|
|
@@ -114,8 +116,9 @@ async function loadMarkdown() {
|
|
|
114
116
|
html = processDirection(rendered, isRTL);
|
|
115
117
|
}
|
|
116
118
|
} catch (err) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
+
const msg = handleLoadError(err);
|
|
120
|
+
if (msg === null) return;
|
|
121
|
+
error = msg;
|
|
119
122
|
} finally {
|
|
120
123
|
loading = false;
|
|
121
124
|
}
|
|
@@ -124,24 +127,13 @@ async function loadMarkdown() {
|
|
|
124
127
|
if (!error) {
|
|
125
128
|
await tick();
|
|
126
129
|
await renderMermaidDiagrams();
|
|
127
|
-
|
|
130
|
+
wireCopyButtons();
|
|
128
131
|
}
|
|
129
132
|
}
|
|
130
133
|
|
|
131
|
-
function
|
|
134
|
+
function wireCopyButtons() {
|
|
132
135
|
if (!contentEl) return;
|
|
133
|
-
|
|
134
|
-
btn.addEventListener('click', async () => {
|
|
135
|
-
const code = decodeURIComponent((btn as HTMLElement).dataset.code ?? '');
|
|
136
|
-
try {
|
|
137
|
-
await navigator.clipboard.writeText(code);
|
|
138
|
-
btn.classList.add('copied');
|
|
139
|
-
setTimeout(() => btn.classList.remove('copied'), 2000);
|
|
140
|
-
} catch {
|
|
141
|
-
// clipboard not available
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
}
|
|
136
|
+
wireCodeCopyButtons(contentEl, '.code-copy-btn');
|
|
145
137
|
}
|
|
146
138
|
|
|
147
139
|
async function renderMermaidDiagrams() {
|