@walkthru-earth/objex 1.0.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 +8 -1
- package/dist/components/browser/FileBrowser.svelte +16 -40
- package/dist/components/browser/FileTreeSidebar.svelte +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/stores/connections.svelte.js +5 -31
- package/dist/stores/files.svelte.d.ts +1 -6
- package/dist/stores/files.svelte.js +4 -36
- package/dist/stores/query-history.svelte.js +3 -25
- package/dist/stores/settings.svelte.d.ts +1 -0
- package/dist/stores/settings.svelte.js +10 -30
- package/dist/utils/cloud-url.d.ts +27 -0
- package/dist/utils/cloud-url.js +61 -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/local-storage.d.ts +16 -0
- package/dist/utils/local-storage.js +37 -0
- package/dist/utils/url.d.ts +0 -16
- package/dist/utils/url.js +2 -54
- package/package.json +14 -14
package/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# objex
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@walkthru-earth/objex)
|
|
4
|
+
[](https://www.npmjs.com/package/@walkthru-earth/objex-utils)
|
|
5
|
+
[](https://github.com/walkthru-earth/objex/actions/workflows/ci.yml)
|
|
6
|
+
[](https://creativecommons.org/licenses/by/4.0/)
|
|
7
|
+
|
|
3
8
|
Cloud storage explorer that runs entirely in the browser. Connect to S3, Azure, GCS, R2, MinIO -- browse files, query data with SQL, and visualize geospatial formats on interactive maps. No backend required.
|
|
4
9
|
|
|
5
10
|
```mermaid
|
|
@@ -92,12 +97,14 @@ import {
|
|
|
92
97
|
| `./utils/geoarrow` | `buildGeoArrowTables`, `normalizeGeomType` |
|
|
93
98
|
| `./utils/storage-url` | `parseStorageUrl`, `looksLikeUrl` |
|
|
94
99
|
| `./utils/parquet-metadata` | `readParquetMetadata`, `extractEpsgFromGeoMeta` |
|
|
95
|
-
| `./utils/format` | `formatFileSize`, `formatDate`, `getFileExtension` |
|
|
100
|
+
| `./utils/format` | `formatFileSize`, `formatDate`, `formatValue`, `getFileExtension`, `jsonReplacerBigInt` |
|
|
96
101
|
| `./utils/hex` | `generateHexDump` |
|
|
97
102
|
| `./utils/column-types` | `classifyType`, `typeColor`, `typeBadgeClass` |
|
|
98
103
|
| `./file-icons` | `getFileTypeInfo`, `getDuckDbReadFn`, `getViewerKind` |
|
|
99
104
|
| `./types` | `FileEntry`, `Connection`, `Tab`, `WriteResult`, `Theme` |
|
|
100
105
|
|
|
106
|
+
The main export also includes `copyToClipboard`, `handleLoadError`, and shared constants (`WGS84_CODES`, `STORAGE_KEYS`, `DEFAULT_TARGET_CRS`, etc.).
|
|
107
|
+
|
|
101
108
|
## Quick Start (Development)
|
|
102
109
|
|
|
103
110
|
```bash
|
|
@@ -8,6 +8,12 @@ import { browser } from '../../stores/browser.svelte.js';
|
|
|
8
8
|
import { safeLock } from '../../stores/safelock.svelte.js';
|
|
9
9
|
import { tabs } from '../../stores/tabs.svelte.js';
|
|
10
10
|
import type { FileEntry } from '../../types.js';
|
|
11
|
+
import {
|
|
12
|
+
type SortConfig,
|
|
13
|
+
type SortField,
|
|
14
|
+
sortFileEntries,
|
|
15
|
+
toggleSortField
|
|
16
|
+
} from '../../utils/file-sort.js';
|
|
11
17
|
import { detectZarrMarkers } from '../../utils/zarr.js';
|
|
12
18
|
import Breadcrumb from './Breadcrumb.svelte';
|
|
13
19
|
import CreateFolderDialog from './CreateFolderDialog.svelte';
|
|
@@ -18,12 +24,8 @@ import RenameDialog from './RenameDialog.svelte';
|
|
|
18
24
|
import SearchBar from './SearchBar.svelte';
|
|
19
25
|
import UploadButton from './UploadButton.svelte';
|
|
20
26
|
|
|
21
|
-
type SortField = 'name' | 'size' | 'modified' | 'extension';
|
|
22
|
-
type SortDirection = 'asc' | 'desc';
|
|
23
|
-
|
|
24
27
|
let filterQuery = $state('');
|
|
25
|
-
let
|
|
26
|
-
let sortDirection = $state<SortDirection>('asc');
|
|
28
|
+
let sortConfig = $state<SortConfig>({ field: 'name', direction: 'asc' });
|
|
27
29
|
|
|
28
30
|
let deleteDialogOpen = $state(false);
|
|
29
31
|
let deleteTarget = $state<FileEntry | null>(null);
|
|
@@ -50,7 +52,7 @@ function openAsZarr() {
|
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
const sortedAndFilteredEntries = $derived.by(() => {
|
|
53
|
-
let result =
|
|
55
|
+
let result = browser.entries;
|
|
54
56
|
|
|
55
57
|
// Filter
|
|
56
58
|
if (filterQuery) {
|
|
@@ -58,28 +60,7 @@ const sortedAndFilteredEntries = $derived.by(() => {
|
|
|
58
60
|
result = result.filter((entry: FileEntry) => entry.name.toLowerCase().includes(q));
|
|
59
61
|
}
|
|
60
62
|
|
|
61
|
-
|
|
62
|
-
const dir = sortDirection === 'asc' ? 1 : -1;
|
|
63
|
-
result.sort((a, b) => {
|
|
64
|
-
// Directories always come first
|
|
65
|
-
if (a.is_dir && !b.is_dir) return -1;
|
|
66
|
-
if (!a.is_dir && b.is_dir) return 1;
|
|
67
|
-
|
|
68
|
-
switch (sortField) {
|
|
69
|
-
case 'name':
|
|
70
|
-
return dir * a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
|
|
71
|
-
case 'size':
|
|
72
|
-
return dir * (a.size - b.size);
|
|
73
|
-
case 'modified':
|
|
74
|
-
return dir * (a.modified - b.modified);
|
|
75
|
-
case 'extension':
|
|
76
|
-
return dir * a.extension.localeCompare(b.extension, undefined, { sensitivity: 'base' });
|
|
77
|
-
default:
|
|
78
|
-
return 0;
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
return result;
|
|
63
|
+
return sortFileEntries(result, sortConfig);
|
|
83
64
|
});
|
|
84
65
|
|
|
85
66
|
function handleFilter(query: string) {
|
|
@@ -91,12 +72,7 @@ function handleNavigate(path: string) {
|
|
|
91
72
|
}
|
|
92
73
|
|
|
93
74
|
function handleSort(field: SortField) {
|
|
94
|
-
|
|
95
|
-
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
|
|
96
|
-
} else {
|
|
97
|
-
sortField = field;
|
|
98
|
-
sortDirection = 'asc';
|
|
99
|
-
}
|
|
75
|
+
sortConfig = toggleSortField(sortConfig, field);
|
|
100
76
|
}
|
|
101
77
|
|
|
102
78
|
function handleDelete(entry: FileEntry) {
|
|
@@ -152,8 +128,8 @@ function handleRename(entry: FileEntry) {
|
|
|
152
128
|
onclick={() => handleSort('name')}
|
|
153
129
|
>
|
|
154
130
|
{t('fileBrowser.name')}
|
|
155
|
-
{#if
|
|
156
|
-
{#if
|
|
131
|
+
{#if sortConfig.field === 'name'}
|
|
132
|
+
{#if sortConfig.direction === 'asc'}
|
|
157
133
|
<ArrowUp class="size-3" />
|
|
158
134
|
{:else}
|
|
159
135
|
<ArrowDown class="size-3" />
|
|
@@ -166,8 +142,8 @@ function handleRename(entry: FileEntry) {
|
|
|
166
142
|
class="text-muted-foreground hover:text-foreground flex w-20 shrink-0 items-center justify-end gap-1 transition-colors"
|
|
167
143
|
onclick={() => handleSort('size')}
|
|
168
144
|
>
|
|
169
|
-
{#if
|
|
170
|
-
{#if
|
|
145
|
+
{#if sortConfig.field === 'size'}
|
|
146
|
+
{#if sortConfig.direction === 'asc'}
|
|
171
147
|
<ArrowUp class="size-3" />
|
|
172
148
|
{:else}
|
|
173
149
|
<ArrowDown class="size-3" />
|
|
@@ -179,8 +155,8 @@ function handleRename(entry: FileEntry) {
|
|
|
179
155
|
class="text-muted-foreground hover:text-foreground flex w-24 shrink-0 items-center justify-end gap-1 transition-colors"
|
|
180
156
|
onclick={() => handleSort('modified')}
|
|
181
157
|
>
|
|
182
|
-
{#if
|
|
183
|
-
{#if
|
|
158
|
+
{#if sortConfig.field === 'modified'}
|
|
159
|
+
{#if sortConfig.direction === 'asc'}
|
|
184
160
|
<ArrowUp class="size-3" />
|
|
185
161
|
{:else}
|
|
186
162
|
<ArrowDown class="size-3" />
|
|
@@ -17,7 +17,7 @@ import { getAdapter } from '../../storage/index.js';
|
|
|
17
17
|
import { browser } from '../../stores/browser.svelte.js';
|
|
18
18
|
import { tabs } from '../../stores/tabs.svelte.js';
|
|
19
19
|
import type { Connection, FileEntry } from '../../types.js';
|
|
20
|
-
import { getNativeScheme } from '../../utils/url.js';
|
|
20
|
+
import { getNativeScheme } from '../../utils/cloud-url.js';
|
|
21
21
|
import { syncUrlParam } from '../../utils/url-state.js';
|
|
22
22
|
|
|
23
23
|
let {
|
package/dist/index.d.ts
CHANGED
|
@@ -4,17 +4,26 @@ export { buildDuckDbSource, getDuckDbReadFn, getFileTypeInfo, getMimeType, getVi
|
|
|
4
4
|
export type { MapQueryHandle, MapQueryResult, QueryEngine, QueryHandle, QueryResult, SchemaField } from './query/engine.js';
|
|
5
5
|
export { QueryCancelledError } from './query/engine.js';
|
|
6
6
|
export type { ListPage, StorageAdapter } from './storage/adapter.js';
|
|
7
|
+
export type { ProviderDef, ProviderId, ProviderRegion } from './storage/providers.js';
|
|
8
|
+
export { buildEndpointFromTemplate, buildProviderBaseUrl, getProvider, isGcsProvider, PROVIDER_IDS, PROVIDERS } from './storage/providers.js';
|
|
7
9
|
export { UrlAdapter } from './storage/url-adapter.js';
|
|
8
10
|
export type { Connection, ConnectionConfig, FileEntry, Tab, Theme, WriteResult } from './types.js';
|
|
9
11
|
export { copyToClipboard, wireCodeCopyButtons } from './utils/clipboard.js';
|
|
12
|
+
export { getNativeScheme, resolveCloudUrl, safeDecodeURIComponent } from './utils/cloud-url.js';
|
|
10
13
|
export type { TypeCategory } from './utils/column-types.js';
|
|
11
14
|
export { classifyType, typeBadgeClass, typeColor, typeLabel } from './utils/column-types.js';
|
|
12
15
|
export { handleLoadError } from './utils/error.js';
|
|
16
|
+
export { escapeCsvField, serializeToCsv, serializeToJson } from './utils/export.js';
|
|
17
|
+
export type { SortConfig, SortDirection, SortField } from './utils/file-sort.js';
|
|
18
|
+
export { sortFileEntries, toggleSortField } from './utils/file-sort.js';
|
|
13
19
|
export { formatDate, formatFileSize, formatValue, getFileExtension, jsonReplacerBigInt } from './utils/format.js';
|
|
14
20
|
export type { GeoArrowGeomType, GeoArrowResult } from './utils/geoarrow.js';
|
|
15
21
|
export { buildGeoArrowTables, normalizeGeomType } from './utils/geoarrow.js';
|
|
16
22
|
export type { HexRow } from './utils/hex.js';
|
|
17
23
|
export { generateHexDump } from './utils/hex.js';
|
|
24
|
+
export { loadFromStorage, persistToStorage } from './utils/local-storage.js';
|
|
25
|
+
export type { ParsedMarkdownDocument, SqlBlock } from './utils/markdown-sql.js';
|
|
26
|
+
export { interpolateTemplates, markSqlBlocks, parseMarkdownDocument } from './utils/markdown-sql.js';
|
|
18
27
|
export type { GeoColumnMeta, GeoParquetMeta, ParquetFileMetadata } from './utils/parquet-metadata.js';
|
|
19
28
|
export { extractBounds, extractEpsgFromGeoMeta, extractGeometryTypes, readParquetMetadata } from './utils/parquet-metadata.js';
|
|
20
29
|
export type { Defaults, ParsedStorageUrl, StorageProvider } from './utils/storage-url.js';
|
package/dist/index.js
CHANGED
|
@@ -4,15 +4,24 @@ export { COPY_FEEDBACK_MS, DEFAULT_TARGET_CRS, DUCKDB_INIT_TIMEOUT_MS, LAYER_HUE
|
|
|
4
4
|
// File icons registry
|
|
5
5
|
export { buildDuckDbSource, getDuckDbReadFn, getFileTypeInfo, getMimeType, getViewerKind, isCloudNativeFormat, isQueryable } from './file-icons/index.js';
|
|
6
6
|
export { QueryCancelledError } from './query/engine.js';
|
|
7
|
+
export { buildEndpointFromTemplate, buildProviderBaseUrl, getProvider, isGcsProvider, PROVIDER_IDS, PROVIDERS } from './storage/providers.js';
|
|
7
8
|
export { UrlAdapter } from './storage/url-adapter.js';
|
|
8
9
|
// Clipboard
|
|
9
10
|
export { copyToClipboard, wireCodeCopyButtons } from './utils/clipboard.js';
|
|
11
|
+
// Cloud URL resolution
|
|
12
|
+
export { getNativeScheme, resolveCloudUrl, safeDecodeURIComponent } from './utils/cloud-url.js';
|
|
10
13
|
export { classifyType, typeBadgeClass, typeColor, typeLabel } from './utils/column-types.js';
|
|
11
14
|
// Error handling
|
|
12
15
|
export { handleLoadError } from './utils/error.js';
|
|
16
|
+
// Data export / serialization
|
|
17
|
+
export { escapeCsvField, serializeToCsv, serializeToJson } from './utils/export.js';
|
|
18
|
+
export { sortFileEntries, toggleSortField } from './utils/file-sort.js';
|
|
13
19
|
export { formatDate, formatFileSize, formatValue, getFileExtension, jsonReplacerBigInt } from './utils/format.js';
|
|
14
20
|
export { buildGeoArrowTables, normalizeGeomType } from './utils/geoarrow.js';
|
|
15
21
|
export { generateHexDump } from './utils/hex.js';
|
|
22
|
+
// localStorage helpers
|
|
23
|
+
export { loadFromStorage, persistToStorage } from './utils/local-storage.js';
|
|
24
|
+
export { interpolateTemplates, markSqlBlocks, parseMarkdownDocument } from './utils/markdown-sql.js';
|
|
16
25
|
export { extractBounds, extractEpsgFromGeoMeta, extractGeometryTypes, readParquetMetadata } from './utils/parquet-metadata.js';
|
|
17
26
|
export { describeParseResult, looksLikeUrl, parseStorageUrl } from './utils/storage-url.js';
|
|
18
27
|
// Utilities
|
|
@@ -1,33 +1,7 @@
|
|
|
1
1
|
import { STORAGE_KEYS } from '../constants.js';
|
|
2
|
+
import { loadFromStorage, persistToStorage } from '../utils/local-storage.js';
|
|
2
3
|
import { credentialStore, storeToNative } from './credentials.svelte.js';
|
|
3
4
|
// ---------------------------------------------------------------------------
|
|
4
|
-
// localStorage helpers
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
function loadFromLocalStorage() {
|
|
7
|
-
if (typeof window === 'undefined')
|
|
8
|
-
return [];
|
|
9
|
-
try {
|
|
10
|
-
const raw = localStorage.getItem(STORAGE_KEYS.CONNECTIONS);
|
|
11
|
-
if (raw) {
|
|
12
|
-
return JSON.parse(raw);
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
catch {
|
|
16
|
-
// ignore parse errors
|
|
17
|
-
}
|
|
18
|
-
return [];
|
|
19
|
-
}
|
|
20
|
-
function persistToLocalStorage(connections) {
|
|
21
|
-
if (typeof window === 'undefined')
|
|
22
|
-
return;
|
|
23
|
-
try {
|
|
24
|
-
localStorage.setItem(STORAGE_KEYS.CONNECTIONS, JSON.stringify(connections));
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
// ignore storage errors
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
// ---------------------------------------------------------------------------
|
|
31
5
|
// Store
|
|
32
6
|
// ---------------------------------------------------------------------------
|
|
33
7
|
function createConnectionsStore() {
|
|
@@ -48,7 +22,7 @@ function createConnectionsStore() {
|
|
|
48
22
|
async load() {
|
|
49
23
|
if (loaded)
|
|
50
24
|
return;
|
|
51
|
-
connections =
|
|
25
|
+
connections = loadFromStorage(STORAGE_KEYS.CONNECTIONS, []);
|
|
52
26
|
loaded = true;
|
|
53
27
|
},
|
|
54
28
|
/**
|
|
@@ -75,7 +49,7 @@ function createConnectionsStore() {
|
|
|
75
49
|
rootPrefix: config.rootPrefix
|
|
76
50
|
};
|
|
77
51
|
connections = [...connections, conn];
|
|
78
|
-
|
|
52
|
+
persistToStorage(STORAGE_KEYS.CONNECTIONS, connections);
|
|
79
53
|
// Store credentials in memory (never persisted to localStorage).
|
|
80
54
|
if (!config.anonymous) {
|
|
81
55
|
if (config.sas_token) {
|
|
@@ -114,7 +88,7 @@ function createConnectionsStore() {
|
|
|
114
88
|
rootPrefix: config.rootPrefix
|
|
115
89
|
};
|
|
116
90
|
connections = [...connections];
|
|
117
|
-
|
|
91
|
+
persistToStorage(STORAGE_KEYS.CONNECTIONS, connections);
|
|
118
92
|
// Invalidate cached adapter for this connection
|
|
119
93
|
import('../storage/index.js').then(({ clearAdapterCache }) => clearAdapterCache(id));
|
|
120
94
|
// Update in-memory credentials.
|
|
@@ -148,7 +122,7 @@ function createConnectionsStore() {
|
|
|
148
122
|
async remove(id) {
|
|
149
123
|
const before = connections.length;
|
|
150
124
|
connections = connections.filter((c) => c.id !== id);
|
|
151
|
-
|
|
125
|
+
persistToStorage(STORAGE_KEYS.CONNECTIONS, connections);
|
|
152
126
|
credentialStore.remove(id);
|
|
153
127
|
// Invalidate cached adapter for this connection
|
|
154
128
|
import('../storage/index.js').then(({ clearAdapterCache }) => clearAdapterCache(id));
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import type { FileEntry } from '../types.js';
|
|
2
|
-
|
|
3
|
-
export type SortDirection = 'asc' | 'desc';
|
|
4
|
-
export interface SortConfig {
|
|
5
|
-
field: SortField;
|
|
6
|
-
direction: SortDirection;
|
|
7
|
-
}
|
|
2
|
+
import { type SortConfig, type SortField } from '../utils/file-sort.js';
|
|
8
3
|
export declare const fileStore: {
|
|
9
4
|
readonly entries: FileEntry[];
|
|
10
5
|
readonly currentPath: string;
|
|
@@ -1,27 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
const sorted = [...entries];
|
|
3
|
-
const dir = config.direction === 'asc' ? 1 : -1;
|
|
4
|
-
sorted.sort((a, b) => {
|
|
5
|
-
// Directories always come first
|
|
6
|
-
if (a.is_dir && !b.is_dir)
|
|
7
|
-
return -1;
|
|
8
|
-
if (!a.is_dir && b.is_dir)
|
|
9
|
-
return 1;
|
|
10
|
-
switch (config.field) {
|
|
11
|
-
case 'name':
|
|
12
|
-
return dir * a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
|
|
13
|
-
case 'size':
|
|
14
|
-
return dir * (a.size - b.size);
|
|
15
|
-
case 'modified':
|
|
16
|
-
return dir * (a.modified - b.modified);
|
|
17
|
-
case 'extension':
|
|
18
|
-
return dir * a.extension.localeCompare(b.extension, undefined, { sensitivity: 'base' });
|
|
19
|
-
default:
|
|
20
|
-
return 0;
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
return sorted;
|
|
24
|
-
}
|
|
1
|
+
import { sortFileEntries, toggleSortField } from '../utils/file-sort.js';
|
|
25
2
|
function createFilesStore() {
|
|
26
3
|
let files = $state([]);
|
|
27
4
|
let currentPath = $state('');
|
|
@@ -45,7 +22,7 @@ function createFilesStore() {
|
|
|
45
22
|
return sortConfig;
|
|
46
23
|
},
|
|
47
24
|
setFiles(entries) {
|
|
48
|
-
files =
|
|
25
|
+
files = sortFileEntries(entries, sortConfig);
|
|
49
26
|
error = null;
|
|
50
27
|
},
|
|
51
28
|
setPath(path) {
|
|
@@ -58,17 +35,8 @@ function createFilesStore() {
|
|
|
58
35
|
error = message;
|
|
59
36
|
},
|
|
60
37
|
sort(field) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
sortConfig = {
|
|
64
|
-
field,
|
|
65
|
-
direction: sortConfig.direction === 'asc' ? 'desc' : 'asc'
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
else {
|
|
69
|
-
sortConfig = { field, direction: 'asc' };
|
|
70
|
-
}
|
|
71
|
-
files = sortEntries(files, sortConfig);
|
|
38
|
+
sortConfig = toggleSortField(sortConfig, field);
|
|
39
|
+
files = sortFileEntries(files, sortConfig);
|
|
72
40
|
}
|
|
73
41
|
};
|
|
74
42
|
}
|
|
@@ -1,31 +1,9 @@
|
|
|
1
1
|
import { MAX_QUERY_HISTORY_ENTRIES, STORAGE_KEYS } from '../constants.js';
|
|
2
|
-
|
|
3
|
-
if (typeof window === 'undefined')
|
|
4
|
-
return [];
|
|
5
|
-
try {
|
|
6
|
-
const raw = localStorage.getItem(STORAGE_KEYS.QUERY_HISTORY);
|
|
7
|
-
if (raw)
|
|
8
|
-
return JSON.parse(raw);
|
|
9
|
-
}
|
|
10
|
-
catch {
|
|
11
|
-
// ignore parse errors
|
|
12
|
-
}
|
|
13
|
-
return [];
|
|
14
|
-
}
|
|
15
|
-
function persistEntries(entries) {
|
|
16
|
-
if (typeof window === 'undefined')
|
|
17
|
-
return;
|
|
18
|
-
try {
|
|
19
|
-
localStorage.setItem(STORAGE_KEYS.QUERY_HISTORY, JSON.stringify(entries));
|
|
20
|
-
}
|
|
21
|
-
catch {
|
|
22
|
-
// ignore storage errors
|
|
23
|
-
}
|
|
24
|
-
}
|
|
2
|
+
import { loadFromStorage, persistToStorage } from '../utils/local-storage.js';
|
|
25
3
|
function createQueryHistoryStore() {
|
|
26
|
-
let entries = $state(
|
|
4
|
+
let entries = $state(loadFromStorage(STORAGE_KEYS.QUERY_HISTORY, []));
|
|
27
5
|
function save() {
|
|
28
|
-
|
|
6
|
+
persistToStorage(STORAGE_KEYS.QUERY_HISTORY, entries);
|
|
29
7
|
}
|
|
30
8
|
return {
|
|
31
9
|
get entries() {
|
|
@@ -1,36 +1,16 @@
|
|
|
1
1
|
import { STORAGE_KEYS } from '../constants.js';
|
|
2
2
|
import { setLocale } from '../i18n/index.svelte.js';
|
|
3
|
+
import { loadFromStorage, persistToStorage } from '../utils/local-storage.js';
|
|
4
|
+
const SETTINGS_DEFAULTS = { theme: 'system', locale: 'en', featureLimit: 1000 };
|
|
3
5
|
function loadSettings() {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const parsed = JSON.parse(raw);
|
|
11
|
-
return {
|
|
12
|
-
theme: parsed.theme ?? 'system',
|
|
13
|
-
locale: parsed.locale ?? 'en',
|
|
14
|
-
featureLimit: parsed.featureLimit ?? 1000
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
catch {
|
|
19
|
-
// ignore parse errors
|
|
20
|
-
}
|
|
21
|
-
return { theme: 'system', locale: 'en', featureLimit: 1000 };
|
|
22
|
-
}
|
|
23
|
-
function persistSettings(settings) {
|
|
24
|
-
if (typeof window === 'undefined')
|
|
25
|
-
return;
|
|
26
|
-
try {
|
|
27
|
-
localStorage.setItem(STORAGE_KEYS.SETTINGS, JSON.stringify(settings));
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
// ignore storage errors
|
|
31
|
-
}
|
|
6
|
+
const stored = loadFromStorage(STORAGE_KEYS.SETTINGS, {});
|
|
7
|
+
return {
|
|
8
|
+
theme: stored.theme ?? SETTINGS_DEFAULTS.theme,
|
|
9
|
+
locale: stored.locale ?? SETTINGS_DEFAULTS.locale,
|
|
10
|
+
featureLimit: stored.featureLimit ?? SETTINGS_DEFAULTS.featureLimit
|
|
11
|
+
};
|
|
32
12
|
}
|
|
33
|
-
function resolveTheme(theme) {
|
|
13
|
+
export function resolveTheme(theme) {
|
|
34
14
|
if (theme !== 'system')
|
|
35
15
|
return theme;
|
|
36
16
|
if (typeof window === 'undefined')
|
|
@@ -51,7 +31,7 @@ function createSettingsStore() {
|
|
|
51
31
|
document.documentElement.lang = initial.locale;
|
|
52
32
|
}
|
|
53
33
|
function persist() {
|
|
54
|
-
|
|
34
|
+
persistToStorage(STORAGE_KEYS.SETTINGS, { theme, locale, featureLimit });
|
|
55
35
|
}
|
|
56
36
|
function applyTheme(t) {
|
|
57
37
|
theme = t;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud storage protocol URL utilities — pure TS, no Svelte dependency.
|
|
3
|
+
*
|
|
4
|
+
* Converts cloud protocol URLs (s3://, gs://) to HTTPS URLs for browser access.
|
|
5
|
+
* Provider-aware native scheme lookup.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Map provider to its native URI scheme prefix.
|
|
9
|
+
* Derived from the registry's `schemes` array (first entry is the primary scheme).
|
|
10
|
+
* Falls back to 's3' for providers without a scheme (S3-compatible).
|
|
11
|
+
*/
|
|
12
|
+
export declare function getNativeScheme(provider: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Safely decode a percent-encoded URI component.
|
|
15
|
+
* Returns the original string if decoding fails (malformed sequences).
|
|
16
|
+
*/
|
|
17
|
+
export declare function safeDecodeURIComponent(s: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Convert a cloud storage protocol URL (s3://, gs://) to an HTTPS URL
|
|
20
|
+
* for browser access. Returns the original URL if already HTTP(S) or unknown.
|
|
21
|
+
*
|
|
22
|
+
* Supported:
|
|
23
|
+
* - `s3://bucket/key` → `https://s3.{region}.amazonaws.com/{bucket}/{key}`
|
|
24
|
+
* (region auto-detected from bucket name when possible)
|
|
25
|
+
* - `gs://bucket/key` → `https://storage.googleapis.com/{bucket}/{key}`
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveCloudUrl(url: string): string;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud storage protocol URL utilities — pure TS, no Svelte dependency.
|
|
3
|
+
*
|
|
4
|
+
* Converts cloud protocol URLs (s3://, gs://) to HTTPS URLs for browser access.
|
|
5
|
+
* Provider-aware native scheme lookup.
|
|
6
|
+
*/
|
|
7
|
+
import { buildProviderBaseUrl, PROVIDERS } from '../storage/providers.js';
|
|
8
|
+
/** AWS region pattern — matches prefixes like "us-west-2", "eu-central-1", etc. */
|
|
9
|
+
const AWS_REGION_RE = /^(us|eu|ap|sa|ca|me|af|il)-(north|south|east|west|central|northeast|southeast|northwest|southwest)-\d+/;
|
|
10
|
+
/**
|
|
11
|
+
* Map provider to its native URI scheme prefix.
|
|
12
|
+
* Derived from the registry's `schemes` array (first entry is the primary scheme).
|
|
13
|
+
* Falls back to 's3' for providers without a scheme (S3-compatible).
|
|
14
|
+
*/
|
|
15
|
+
export function getNativeScheme(provider) {
|
|
16
|
+
const def = PROVIDERS[provider];
|
|
17
|
+
if (def?.schemes.length)
|
|
18
|
+
return def.schemes[0];
|
|
19
|
+
return 's3';
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Safely decode a percent-encoded URI component.
|
|
23
|
+
* Returns the original string if decoding fails (malformed sequences).
|
|
24
|
+
*/
|
|
25
|
+
export function safeDecodeURIComponent(s) {
|
|
26
|
+
try {
|
|
27
|
+
return decodeURIComponent(s);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return s;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Convert a cloud storage protocol URL (s3://, gs://) to an HTTPS URL
|
|
35
|
+
* for browser access. Returns the original URL if already HTTP(S) or unknown.
|
|
36
|
+
*
|
|
37
|
+
* Supported:
|
|
38
|
+
* - `s3://bucket/key` → `https://s3.{region}.amazonaws.com/{bucket}/{key}`
|
|
39
|
+
* (region auto-detected from bucket name when possible)
|
|
40
|
+
* - `gs://bucket/key` → `https://storage.googleapis.com/{bucket}/{key}`
|
|
41
|
+
*/
|
|
42
|
+
export function resolveCloudUrl(url) {
|
|
43
|
+
// S3 / S3-compatible: s3://, s3a://, s3n://
|
|
44
|
+
const s3Match = url.match(/^s3[an]?:\/\/([^/]+)\/?(.*)$/);
|
|
45
|
+
if (s3Match) {
|
|
46
|
+
const [, bucket, key] = s3Match;
|
|
47
|
+
// Detect region from bucket name (e.g. "us-west-2.opendata.source.coop")
|
|
48
|
+
const regionMatch = bucket.match(AWS_REGION_RE);
|
|
49
|
+
const region = regionMatch ? regionMatch[0] : 'us-east-1';
|
|
50
|
+
const base = buildProviderBaseUrl('s3', '', bucket, region);
|
|
51
|
+
return key ? `${base}/${key}` : base;
|
|
52
|
+
}
|
|
53
|
+
// Google Cloud Storage: gs://, gcs://
|
|
54
|
+
const gcsMatch = url.match(/^gcs?:\/\/([^/]+)\/?(.*)$/);
|
|
55
|
+
if (gcsMatch) {
|
|
56
|
+
const [, bucket, key] = gcsMatch;
|
|
57
|
+
const base = buildProviderBaseUrl('gcs', '', bucket, '');
|
|
58
|
+
return key ? `${base}/${key}` : base;
|
|
59
|
+
}
|
|
60
|
+
return url;
|
|
61
|
+
}
|
package/dist/utils/export.d.ts
CHANGED
|
@@ -1,2 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Escape a CSV field value per RFC 4180.
|
|
3
|
+
*/
|
|
4
|
+
export declare function escapeCsvField(value: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* Serialize column/row data to a CSV string.
|
|
7
|
+
* Pure function — no browser APIs, works in Node.js.
|
|
8
|
+
*/
|
|
9
|
+
export declare function serializeToCsv(columns: string[], rows: Record<string, unknown>[]): string;
|
|
10
|
+
/**
|
|
11
|
+
* Serialize column/row data to a formatted JSON string.
|
|
12
|
+
* Pure function — no browser APIs, works in Node.js.
|
|
13
|
+
*/
|
|
14
|
+
export declare function serializeToJson(columns: string[], rows: Record<string, unknown>[]): string;
|
|
15
|
+
/**
|
|
16
|
+
* Export data as CSV file (triggers browser download).
|
|
17
|
+
*/
|
|
18
|
+
export declare function exportToCsv(columns: string[], rows: Record<string, unknown>[], filename: string): void;
|
|
19
|
+
/**
|
|
20
|
+
* Export data as JSON file (triggers browser download).
|
|
21
|
+
*/
|
|
22
|
+
export declare function exportToJson(columns: string[], rows: Record<string, unknown>[], filename: string): void;
|
package/dist/utils/export.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { jsonReplacerBigInt } from './format.js';
|
|
1
2
|
function triggerDownload(content, filename, mimeType) {
|
|
2
3
|
const blob = new Blob([content], { type: mimeType });
|
|
3
4
|
const url = URL.createObjectURL(blob);
|
|
@@ -9,30 +10,43 @@ function triggerDownload(content, filename, mimeType) {
|
|
|
9
10
|
document.body.removeChild(a);
|
|
10
11
|
URL.revokeObjectURL(url);
|
|
11
12
|
}
|
|
12
|
-
|
|
13
|
+
/** Format a cell value for export (empty string for null/undefined). */
|
|
14
|
+
function formatCellValue(value) {
|
|
13
15
|
if (value === null || value === undefined)
|
|
14
16
|
return '';
|
|
15
17
|
if (value instanceof Date)
|
|
16
18
|
return value.toISOString();
|
|
19
|
+
if (typeof value === 'bigint')
|
|
20
|
+
return value.toString();
|
|
17
21
|
if (typeof value === 'object')
|
|
18
|
-
return JSON.stringify(value);
|
|
22
|
+
return JSON.stringify(value, jsonReplacerBigInt);
|
|
19
23
|
return String(value);
|
|
20
24
|
}
|
|
21
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Escape a CSV field value per RFC 4180.
|
|
27
|
+
*/
|
|
28
|
+
export function escapeCsvField(value) {
|
|
22
29
|
if (value.includes(',') || value.includes('"') || value.includes('\n') || value.includes('\r')) {
|
|
23
30
|
return `"${value.replace(/"/g, '""')}"`;
|
|
24
31
|
}
|
|
25
32
|
return value;
|
|
26
33
|
}
|
|
27
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Serialize column/row data to a CSV string.
|
|
36
|
+
* Pure function — no browser APIs, works in Node.js.
|
|
37
|
+
*/
|
|
38
|
+
export function serializeToCsv(columns, rows) {
|
|
28
39
|
const header = columns.map(escapeCsvField).join(',');
|
|
29
40
|
const body = rows
|
|
30
|
-
.map((row) => columns.map((col) => escapeCsvField(
|
|
41
|
+
.map((row) => columns.map((col) => escapeCsvField(formatCellValue(row[col]))).join(','))
|
|
31
42
|
.join('\n');
|
|
32
|
-
|
|
33
|
-
triggerDownload(csv, filename.endsWith('.csv') ? filename : `${filename}.csv`, 'text/csv;charset=utf-8;');
|
|
43
|
+
return `${header}\n${body}`;
|
|
34
44
|
}
|
|
35
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Serialize column/row data to a formatted JSON string.
|
|
47
|
+
* Pure function — no browser APIs, works in Node.js.
|
|
48
|
+
*/
|
|
49
|
+
export function serializeToJson(columns, rows) {
|
|
36
50
|
const data = rows.map((row) => {
|
|
37
51
|
const obj = {};
|
|
38
52
|
for (const col of columns) {
|
|
@@ -46,6 +60,17 @@ export function exportToJson(columns, rows, filename) {
|
|
|
46
60
|
}
|
|
47
61
|
return obj;
|
|
48
62
|
});
|
|
49
|
-
|
|
50
|
-
|
|
63
|
+
return JSON.stringify(data, jsonReplacerBigInt, 2);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Export data as CSV file (triggers browser download).
|
|
67
|
+
*/
|
|
68
|
+
export function exportToCsv(columns, rows, filename) {
|
|
69
|
+
triggerDownload(serializeToCsv(columns, rows), filename.endsWith('.csv') ? filename : `${filename}.csv`, 'text/csv;charset=utf-8;');
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Export data as JSON file (triggers browser download).
|
|
73
|
+
*/
|
|
74
|
+
export function exportToJson(columns, rows, filename) {
|
|
75
|
+
triggerDownload(serializeToJson(columns, rows), filename.endsWith('.json') ? filename : `${filename}.json`, 'application/json');
|
|
51
76
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure file entry sorting — framework-agnostic, works in Node.js.
|
|
3
|
+
*/
|
|
4
|
+
import type { FileEntry } from '../types.js';
|
|
5
|
+
export type SortField = 'name' | 'size' | 'modified' | 'extension';
|
|
6
|
+
export type SortDirection = 'asc' | 'desc';
|
|
7
|
+
export interface SortConfig {
|
|
8
|
+
field: SortField;
|
|
9
|
+
direction: SortDirection;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Sort file entries by the given config.
|
|
13
|
+
* Directories always sort before files regardless of sort field.
|
|
14
|
+
* Returns a new array (does not mutate the input).
|
|
15
|
+
*/
|
|
16
|
+
export declare function sortFileEntries(entries: FileEntry[], config: SortConfig): FileEntry[];
|
|
17
|
+
/**
|
|
18
|
+
* Toggle sort config: same field flips direction, new field starts ascending.
|
|
19
|
+
*/
|
|
20
|
+
export declare function toggleSortField(current: SortConfig, field: SortField): SortConfig;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure file entry sorting — framework-agnostic, works in Node.js.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Sort file entries by the given config.
|
|
6
|
+
* Directories always sort before files regardless of sort field.
|
|
7
|
+
* Returns a new array (does not mutate the input).
|
|
8
|
+
*/
|
|
9
|
+
export function sortFileEntries(entries, config) {
|
|
10
|
+
const sorted = [...entries];
|
|
11
|
+
const dir = config.direction === 'asc' ? 1 : -1;
|
|
12
|
+
sorted.sort((a, b) => {
|
|
13
|
+
// Directories always come first
|
|
14
|
+
if (a.is_dir && !b.is_dir)
|
|
15
|
+
return -1;
|
|
16
|
+
if (!a.is_dir && b.is_dir)
|
|
17
|
+
return 1;
|
|
18
|
+
switch (config.field) {
|
|
19
|
+
case 'name':
|
|
20
|
+
return dir * a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
|
|
21
|
+
case 'size':
|
|
22
|
+
return dir * (a.size - b.size);
|
|
23
|
+
case 'modified':
|
|
24
|
+
return dir * (a.modified - b.modified);
|
|
25
|
+
case 'extension':
|
|
26
|
+
return dir * a.extension.localeCompare(b.extension, undefined, { sensitivity: 'base' });
|
|
27
|
+
default:
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
return sorted;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Toggle sort config: same field flips direction, new field starts ascending.
|
|
35
|
+
*/
|
|
36
|
+
export function toggleSortField(current, field) {
|
|
37
|
+
if (current.field === field) {
|
|
38
|
+
return { field, direction: current.direction === 'asc' ? 'desc' : 'asc' };
|
|
39
|
+
}
|
|
40
|
+
return { field, direction: 'asc' };
|
|
41
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic localStorage helpers with SSR safety.
|
|
3
|
+
*
|
|
4
|
+
* Used by connection, settings, and query-history stores to avoid
|
|
5
|
+
* repeating the same load/persist/try-catch/SSR-guard pattern.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Load a JSON value from localStorage.
|
|
9
|
+
* Returns `defaultValue` on SSR, missing key, or parse error.
|
|
10
|
+
*/
|
|
11
|
+
export declare function loadFromStorage<T>(key: string, defaultValue: T): T;
|
|
12
|
+
/**
|
|
13
|
+
* Persist a JSON-serializable value to localStorage.
|
|
14
|
+
* Silently no-ops on SSR or storage errors (quota, private browsing).
|
|
15
|
+
*/
|
|
16
|
+
export declare function persistToStorage(key: string, value: unknown): void;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic localStorage helpers with SSR safety.
|
|
3
|
+
*
|
|
4
|
+
* Used by connection, settings, and query-history stores to avoid
|
|
5
|
+
* repeating the same load/persist/try-catch/SSR-guard pattern.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Load a JSON value from localStorage.
|
|
9
|
+
* Returns `defaultValue` on SSR, missing key, or parse error.
|
|
10
|
+
*/
|
|
11
|
+
export function loadFromStorage(key, defaultValue) {
|
|
12
|
+
if (typeof window === 'undefined')
|
|
13
|
+
return defaultValue;
|
|
14
|
+
try {
|
|
15
|
+
const raw = localStorage.getItem(key);
|
|
16
|
+
if (raw)
|
|
17
|
+
return JSON.parse(raw);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// ignore parse errors
|
|
21
|
+
}
|
|
22
|
+
return defaultValue;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Persist a JSON-serializable value to localStorage.
|
|
26
|
+
* Silently no-ops on SSR or storage errors (quota, private browsing).
|
|
27
|
+
*/
|
|
28
|
+
export function persistToStorage(key, value) {
|
|
29
|
+
if (typeof window === 'undefined')
|
|
30
|
+
return;
|
|
31
|
+
try {
|
|
32
|
+
localStorage.setItem(key, JSON.stringify(value));
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// ignore storage errors
|
|
36
|
+
}
|
|
37
|
+
}
|
package/dist/utils/url.d.ts
CHANGED
|
@@ -4,12 +4,6 @@ import type { Tab } from '../types.js';
|
|
|
4
4
|
* Works for any viewer that needs an HTTP-accessible URL (COG, PMTiles, Zarr, etc.)
|
|
5
5
|
*/
|
|
6
6
|
export declare function buildHttpsUrl(tab: Tab): string;
|
|
7
|
-
/**
|
|
8
|
-
* Map provider to its native URI scheme prefix.
|
|
9
|
-
* Derived from the registry's `schemes` array (first entry is the primary scheme).
|
|
10
|
-
* Falls back to 's3' for providers without a scheme (S3-compatible).
|
|
11
|
-
*/
|
|
12
|
-
export declare function getNativeScheme(provider: string): string;
|
|
13
7
|
/**
|
|
14
8
|
* Build a provider-native protocol URL (s3://bucket/path, sj://bucket/path, etc.).
|
|
15
9
|
*/
|
|
@@ -29,13 +23,3 @@ export declare function buildDuckDbUrl(tab: Tab): string;
|
|
|
29
23
|
* False for authenticated S3 (needs signed URLs or blob download via adapter).
|
|
30
24
|
*/
|
|
31
25
|
export declare function canStreamDirectly(tab: Tab): boolean;
|
|
32
|
-
/**
|
|
33
|
-
* Convert a cloud storage protocol URL (s3://, gs://) to an HTTPS URL
|
|
34
|
-
* for browser access. Returns the original URL if already HTTP(S) or unknown.
|
|
35
|
-
*
|
|
36
|
-
* Supported:
|
|
37
|
-
* - `s3://bucket/key` → `https://s3.{region}.amazonaws.com/{bucket}/{key}`
|
|
38
|
-
* (region auto-detected from bucket name when possible, e.g. "us-west-2.opendata.source.coop")
|
|
39
|
-
* - `gs://bucket/key` → `https://storage.googleapis.com/{bucket}/{key}`
|
|
40
|
-
*/
|
|
41
|
-
export declare function resolveCloudUrl(url: string): string;
|
package/dist/utils/url.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { buildProviderBaseUrl
|
|
1
|
+
import { buildProviderBaseUrl } from '../storage/providers.js';
|
|
2
2
|
import { connections } from '../stores/connections.svelte.js';
|
|
3
3
|
import { credentialStore } from '../stores/credentials.svelte.js';
|
|
4
|
+
import { getNativeScheme, safeDecodeURIComponent } from './cloud-url.js';
|
|
4
5
|
/**
|
|
5
6
|
* Build an HTTPS URL for a tab's file.
|
|
6
7
|
* Works for any viewer that needs an HTTP-accessible URL (COG, PMTiles, Zarr, etc.)
|
|
@@ -19,17 +20,6 @@ export function buildHttpsUrl(tab) {
|
|
|
19
20
|
}
|
|
20
21
|
return `${buildProviderBaseUrl(conn.provider, conn.endpoint, conn.bucket, conn.region)}/${cleanPath}`;
|
|
21
22
|
}
|
|
22
|
-
/**
|
|
23
|
-
* Map provider to its native URI scheme prefix.
|
|
24
|
-
* Derived from the registry's `schemes` array (first entry is the primary scheme).
|
|
25
|
-
* Falls back to 's3' for providers without a scheme (S3-compatible).
|
|
26
|
-
*/
|
|
27
|
-
export function getNativeScheme(provider) {
|
|
28
|
-
const def = PROVIDERS[provider];
|
|
29
|
-
if (def?.schemes.length)
|
|
30
|
-
return def.schemes[0];
|
|
31
|
-
return 's3';
|
|
32
|
-
}
|
|
33
23
|
/**
|
|
34
24
|
* Build a provider-native protocol URL (s3://bucket/path, sj://bucket/path, etc.).
|
|
35
25
|
*/
|
|
@@ -68,14 +58,6 @@ export function buildDuckDbUrl(tab) {
|
|
|
68
58
|
const rawPath = safeDecodeURIComponent(tab.path.replace(/^\//, ''));
|
|
69
59
|
return `s3://${conn.bucket}/${rawPath}`;
|
|
70
60
|
}
|
|
71
|
-
function safeDecodeURIComponent(s) {
|
|
72
|
-
try {
|
|
73
|
-
return decodeURIComponent(s);
|
|
74
|
-
}
|
|
75
|
-
catch {
|
|
76
|
-
return s;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
61
|
/**
|
|
80
62
|
* Check if a tab's file can be loaded directly via HTTPS URL (streaming).
|
|
81
63
|
* True for URL-sourced tabs, anonymous buckets, and Azure (SAS token in URL).
|
|
@@ -105,37 +87,3 @@ function appendAzureSas(url, connectionId) {
|
|
|
105
87
|
const sep = url.includes('?') ? '&' : '?';
|
|
106
88
|
return `${url}${sep}${cleanToken}`;
|
|
107
89
|
}
|
|
108
|
-
// ---------------------------------------------------------------------------
|
|
109
|
-
// Cloud protocol URL → HTTPS conversion
|
|
110
|
-
// ---------------------------------------------------------------------------
|
|
111
|
-
/** AWS region pattern — matches prefixes like "us-west-2", "eu-central-1", etc. */
|
|
112
|
-
const AWS_REGION_RE = /^(us|eu|ap|sa|ca|me|af|il)-(north|south|east|west|central|northeast|southeast|northwest|southwest)-\d+/;
|
|
113
|
-
/**
|
|
114
|
-
* Convert a cloud storage protocol URL (s3://, gs://) to an HTTPS URL
|
|
115
|
-
* for browser access. Returns the original URL if already HTTP(S) or unknown.
|
|
116
|
-
*
|
|
117
|
-
* Supported:
|
|
118
|
-
* - `s3://bucket/key` → `https://s3.{region}.amazonaws.com/{bucket}/{key}`
|
|
119
|
-
* (region auto-detected from bucket name when possible, e.g. "us-west-2.opendata.source.coop")
|
|
120
|
-
* - `gs://bucket/key` → `https://storage.googleapis.com/{bucket}/{key}`
|
|
121
|
-
*/
|
|
122
|
-
export function resolveCloudUrl(url) {
|
|
123
|
-
// S3 / S3-compatible: s3://, s3a://, s3n://
|
|
124
|
-
const s3Match = url.match(/^s3[an]?:\/\/([^/]+)\/?(.*)$/);
|
|
125
|
-
if (s3Match) {
|
|
126
|
-
const [, bucket, key] = s3Match;
|
|
127
|
-
// Detect region from bucket name (e.g. "us-west-2.opendata.source.coop")
|
|
128
|
-
const regionMatch = bucket.match(AWS_REGION_RE);
|
|
129
|
-
const region = regionMatch ? regionMatch[0] : 'us-east-1';
|
|
130
|
-
const base = buildProviderBaseUrl('s3', '', bucket, region);
|
|
131
|
-
return key ? `${base}/${key}` : base;
|
|
132
|
-
}
|
|
133
|
-
// Google Cloud Storage: gs://, gcs://
|
|
134
|
-
const gcsMatch = url.match(/^gcs?:\/\/([^/]+)\/?(.*)$/);
|
|
135
|
-
if (gcsMatch) {
|
|
136
|
-
const [, bucket, key] = gcsMatch;
|
|
137
|
-
const base = buildProviderBaseUrl('gcs', '', bucket, '');
|
|
138
|
-
return key ? `${base}/${key}` : base;
|
|
139
|
-
}
|
|
140
|
-
return url;
|
|
141
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@walkthru-earth/objex",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Svelte 5 components and utilities for exploring geospatial object storage — S3, GCS, Azure, R2",
|
|
5
5
|
"author": "Youssef Harby <yharby@walkthru.earth>",
|
|
6
6
|
"license": "CC-BY-4.0",
|
|
@@ -123,13 +123,14 @@
|
|
|
123
123
|
}
|
|
124
124
|
},
|
|
125
125
|
"devDependencies": {
|
|
126
|
-
"@biomejs/biome": "^2.4.
|
|
126
|
+
"@biomejs/biome": "^2.4.7",
|
|
127
127
|
"@changesets/changelog-github": "^0.5.2",
|
|
128
128
|
"@changesets/cli": "^2.30.0",
|
|
129
|
+
"@fontsource/cairo": "^5.2.7",
|
|
129
130
|
"@internationalized/date": "^3.12.0",
|
|
130
131
|
"@lucide/svelte": "^0.561.0",
|
|
131
132
|
"@sveltejs/adapter-static": "^3.0.10",
|
|
132
|
-
"@sveltejs/kit": "^2.
|
|
133
|
+
"@sveltejs/kit": "^2.55.0",
|
|
133
134
|
"@sveltejs/package": "^2.5.7",
|
|
134
135
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
|
135
136
|
"@tailwindcss/forms": "^0.5.11",
|
|
@@ -137,12 +138,11 @@
|
|
|
137
138
|
"@tailwindcss/vite": "^4.2.1",
|
|
138
139
|
"bits-ui": "^2.16.3",
|
|
139
140
|
"clsx": "^2.1.1",
|
|
140
|
-
"
|
|
141
|
-
"lefthook": "^2.1.3",
|
|
141
|
+
"lefthook": "^2.1.4",
|
|
142
142
|
"paneforge": "^1.0.2",
|
|
143
|
-
"posthog-js": "^1.360.
|
|
143
|
+
"posthog-js": "^1.360.2",
|
|
144
144
|
"publint": "^0.3.18",
|
|
145
|
-
"svelte": "^5.53.
|
|
145
|
+
"svelte": "^5.53.12",
|
|
146
146
|
"svelte-check": "^4.4.5",
|
|
147
147
|
"tailwind-merge": "^3.5.0",
|
|
148
148
|
"tailwind-variants": "^3.2.2",
|
|
@@ -152,16 +152,16 @@
|
|
|
152
152
|
"vite": "^7.3.1"
|
|
153
153
|
},
|
|
154
154
|
"dependencies": {
|
|
155
|
-
"@babylonjs/core": "^8.
|
|
156
|
-
"@babylonjs/loaders": "^8.
|
|
155
|
+
"@babylonjs/core": "^8.55.3",
|
|
156
|
+
"@babylonjs/loaders": "^8.55.3",
|
|
157
157
|
"@carbonplan/zarr-layer": "^0.3.1",
|
|
158
158
|
"@codemirror/autocomplete": "^6.20.1",
|
|
159
|
-
"@codemirror/commands": "^6.10.
|
|
159
|
+
"@codemirror/commands": "^6.10.3",
|
|
160
160
|
"@codemirror/lang-sql": "^6.10.0",
|
|
161
161
|
"@codemirror/language": "^6.12.2",
|
|
162
|
-
"@codemirror/state": "^6.
|
|
162
|
+
"@codemirror/state": "^6.6.0",
|
|
163
163
|
"@codemirror/theme-one-dark": "^6.1.3",
|
|
164
|
-
"@codemirror/view": "^6.
|
|
164
|
+
"@codemirror/view": "^6.40.0",
|
|
165
165
|
"@deck.gl/core": "^9.2.11",
|
|
166
166
|
"@deck.gl/geo-layers": "^9.2.11",
|
|
167
167
|
"@deck.gl/layers": "^9.2.11",
|
|
@@ -182,12 +182,12 @@
|
|
|
182
182
|
"chart.js": "^4.5.1",
|
|
183
183
|
"deck.gl": "^9.2.11",
|
|
184
184
|
"flatgeobuf": "^4.4.0",
|
|
185
|
-
"geotiff": "^3.0.
|
|
185
|
+
"geotiff": "^3.0.5",
|
|
186
186
|
"geotiff-geokeys-to-proj4": "^2024.4.13",
|
|
187
187
|
"hyparquet": "^1.25.1",
|
|
188
188
|
"hyparquet-compressors": "^1.1.1",
|
|
189
189
|
"lz-string": "^1.5.0",
|
|
190
|
-
"maplibre-gl": "^5.
|
|
190
|
+
"maplibre-gl": "^5.20.1",
|
|
191
191
|
"marked": "^17.0.4",
|
|
192
192
|
"mermaid": "^11.13.0",
|
|
193
193
|
"pbf": "^4.0.1",
|