@le-space/orbitdb-storage-bridge 0.10.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/LICENSE +21 -0
- package/README.md +290 -0
- package/dist/components/StorachaAuth.svelte +801 -0
- package/dist/components/StorachaIntegration.svelte +965 -0
- package/dist/components/WebAuthnDIDProvider.js +562 -0
- package/dist/components/storacha-backup.js +325 -0
- package/dist/components/theme.js +79 -0
- package/lib/backends/aleph-pin.js +161 -0
- package/lib/backends/aleph.js +197 -0
- package/lib/backends/lighthouse.js +325 -0
- package/lib/backends/memory.js +140 -0
- package/lib/backends/pinata.js +330 -0
- package/lib/backends/resolve.js +72 -0
- package/lib/backends/storacha.js +178 -0
- package/lib/backends/types.js +187 -0
- package/lib/backup-car.js +1286 -0
- package/lib/backup-helpers.js +87 -0
- package/lib/backup-metadata.js +40 -0
- package/lib/backup.js +249 -0
- package/lib/block-bytes.js +39 -0
- package/lib/car-storage.js +342 -0
- package/lib/courier-sync.js +886 -0
- package/lib/dehydrate.js +146 -0
- package/lib/extract-blocks.js +258 -0
- package/lib/gateway-fetch.js +116 -0
- package/lib/ipns-helpers.js +324 -0
- package/lib/logger.js +128 -0
- package/lib/memory-courier.js +105 -0
- package/lib/orbitdb-storacha-bridge.js +2789 -0
- package/lib/pointer-ipns.js +234 -0
- package/lib/restore-cid.js +302 -0
- package/lib/ucan-bridge.js +921 -0
- package/lib/utils.js +316 -0
- package/package.json +172 -0
|
@@ -0,0 +1,965 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import {
|
|
4
|
+
Cloud,
|
|
5
|
+
Upload,
|
|
6
|
+
Plus,
|
|
7
|
+
List,
|
|
8
|
+
Key,
|
|
9
|
+
Mail,
|
|
10
|
+
LogOut,
|
|
11
|
+
Loader2,
|
|
12
|
+
AlertCircle,
|
|
13
|
+
CheckCircle,
|
|
14
|
+
Download
|
|
15
|
+
} from 'lucide-svelte';
|
|
16
|
+
import {
|
|
17
|
+
initializeStorachaClient,
|
|
18
|
+
createStorachaAccount,
|
|
19
|
+
listSpaces,
|
|
20
|
+
createSpace
|
|
21
|
+
} from './storacha-backup.js';
|
|
22
|
+
import { OrbitDBStorachaBridge, restoreDatabaseFromSpace } from 'orbitdb-storage-bridge';
|
|
23
|
+
import { todosStore } from './db-actions.js';
|
|
24
|
+
import { initializationStore, orbitDBStore, libp2pStore, peerIdStore } from './p2p.js';
|
|
25
|
+
import { initializeDatabase, loadTodos, todoDBStore } from './db-actions.js';
|
|
26
|
+
import { logger } from '../lib/logger.js';
|
|
27
|
+
// Add imports for creating fresh instances
|
|
28
|
+
import { createHelia } from 'helia';
|
|
29
|
+
import { createOrbitDB } from '@orbitdb/core';
|
|
30
|
+
import { createLibp2pConfig } from './libp2p-config.js';
|
|
31
|
+
import { LevelBlockstore } from 'blockstore-level';
|
|
32
|
+
import { LevelDatastore } from 'datastore-level';
|
|
33
|
+
|
|
34
|
+
// Component state
|
|
35
|
+
let showStoracha = false;
|
|
36
|
+
let isLoading = false;
|
|
37
|
+
let status = '';
|
|
38
|
+
let error = null;
|
|
39
|
+
let success = null;
|
|
40
|
+
|
|
41
|
+
// Auth state
|
|
42
|
+
let isLoggedIn = false;
|
|
43
|
+
let client = null;
|
|
44
|
+
let currentSpace = null;
|
|
45
|
+
|
|
46
|
+
// Progress tracking state
|
|
47
|
+
let showProgress = false;
|
|
48
|
+
let progressType = ''; // 'upload' or 'download'
|
|
49
|
+
let progressCurrent = 0;
|
|
50
|
+
let progressTotal = 0;
|
|
51
|
+
let progressPercentage = 0;
|
|
52
|
+
let progressCurrentBlock = null;
|
|
53
|
+
let progressError = null;
|
|
54
|
+
|
|
55
|
+
// Bridge instance
|
|
56
|
+
let bridge = null;
|
|
57
|
+
|
|
58
|
+
// LocalStorage keys
|
|
59
|
+
const STORAGE_KEYS = {
|
|
60
|
+
STORACHA_KEY: 'storacha_key',
|
|
61
|
+
STORACHA_PROOF: 'storacha_proof',
|
|
62
|
+
AUTO_LOGIN: 'storacha_auto_login'
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Form state
|
|
66
|
+
let showCreateForm = false;
|
|
67
|
+
let showCredentialsForm = false;
|
|
68
|
+
let email = '';
|
|
69
|
+
let storachaKey = '';
|
|
70
|
+
let storachaProof = '';
|
|
71
|
+
let newSpaceName = '';
|
|
72
|
+
|
|
73
|
+
// Data state
|
|
74
|
+
let spaces = [];
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
// Progress tracking functions
|
|
79
|
+
function initializeBridge(storachaKey, storachaProof) {
|
|
80
|
+
if (bridge) {
|
|
81
|
+
// Remove existing listeners
|
|
82
|
+
bridge.removeAllListeners();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
bridge = new OrbitDBStorachaBridge({
|
|
86
|
+
storachaKey,
|
|
87
|
+
storachaProof
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Set up progress event listeners
|
|
91
|
+
bridge.on('uploadProgress', (progress) => {
|
|
92
|
+
logger.info({
|
|
93
|
+
current: progress.current,
|
|
94
|
+
total: progress.total,
|
|
95
|
+
percentage: progress.percentage
|
|
96
|
+
}, `Upload Progress: ${progress.current}/${progress.total} (${progress.percentage}%)`);
|
|
97
|
+
|
|
98
|
+
progressType = 'upload';
|
|
99
|
+
progressCurrent = progress.current;
|
|
100
|
+
progressTotal = progress.total;
|
|
101
|
+
progressPercentage = progress.percentage;
|
|
102
|
+
progressCurrentBlock = progress.currentBlock;
|
|
103
|
+
progressError = progress.error;
|
|
104
|
+
showProgress = true;
|
|
105
|
+
|
|
106
|
+
// Update status text
|
|
107
|
+
if (progress.error) {
|
|
108
|
+
status = `Upload error: ${progress.error.message}`;
|
|
109
|
+
} else if (progress.currentBlock) {
|
|
110
|
+
status = `Uploading block ${progress.current} of ${progress.total} (${progress.currentBlock.hash.slice(0, 8)}...)`;
|
|
111
|
+
} else {
|
|
112
|
+
status = `Uploading block ${progress.current} of ${progress.total}`;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
bridge.on('downloadProgress', (progress) => {
|
|
117
|
+
logger.info({
|
|
118
|
+
current: progress.current,
|
|
119
|
+
total: progress.total,
|
|
120
|
+
percentage: progress.percentage
|
|
121
|
+
}, `Download Progress: ${progress.current}/${progress.total} (${progress.percentage}%)`);
|
|
122
|
+
|
|
123
|
+
progressType = 'download';
|
|
124
|
+
progressCurrent = progress.current;
|
|
125
|
+
progressTotal = progress.total;
|
|
126
|
+
progressPercentage = progress.percentage;
|
|
127
|
+
progressCurrentBlock = progress.currentBlock;
|
|
128
|
+
progressError = progress.error;
|
|
129
|
+
showProgress = true;
|
|
130
|
+
|
|
131
|
+
// Update status text
|
|
132
|
+
if (progress.error) {
|
|
133
|
+
status = `Download error: ${progress.error.message}`;
|
|
134
|
+
} else if (progress.currentBlock) {
|
|
135
|
+
status = `Downloading block ${progress.current} of ${progress.total} (${progress.currentBlock.storachaCID.slice(0, 8)}...)`;
|
|
136
|
+
} else {
|
|
137
|
+
status = `Downloading block ${progress.current} of ${progress.total}`;
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return bridge;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function resetProgress() {
|
|
145
|
+
showProgress = false;
|
|
146
|
+
progressType = '';
|
|
147
|
+
progressCurrent = 0;
|
|
148
|
+
progressTotal = 0;
|
|
149
|
+
progressPercentage = 0;
|
|
150
|
+
progressCurrentBlock = null;
|
|
151
|
+
progressError = null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// LocalStorage functions
|
|
155
|
+
function saveCredentials(key, proof) {
|
|
156
|
+
try {
|
|
157
|
+
logger.info('💾 Saving credentials to localStorage...');
|
|
158
|
+
localStorage.setItem(STORAGE_KEYS.STORACHA_KEY, key);
|
|
159
|
+
localStorage.setItem(STORAGE_KEYS.STORACHA_PROOF, proof);
|
|
160
|
+
localStorage.setItem(STORAGE_KEYS.AUTO_LOGIN, 'true');
|
|
161
|
+
logger.info('✅ Credentials saved successfully');
|
|
162
|
+
} catch (err) {
|
|
163
|
+
logger.warn({ error: err.message }, '❌ Failed to save credentials to localStorage');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function loadCredentials() {
|
|
168
|
+
try {
|
|
169
|
+
logger.info('📖 Checking localStorage for credentials...');
|
|
170
|
+
const key = localStorage.getItem(STORAGE_KEYS.STORACHA_KEY);
|
|
171
|
+
const proof = localStorage.getItem(STORAGE_KEYS.STORACHA_PROOF);
|
|
172
|
+
const autoLogin = localStorage.getItem(STORAGE_KEYS.AUTO_LOGIN);
|
|
173
|
+
|
|
174
|
+
if (key && proof && autoLogin === 'true') {
|
|
175
|
+
logger.info('✅ Valid credentials found!');
|
|
176
|
+
return { key, proof };
|
|
177
|
+
} else {
|
|
178
|
+
logger.info('❌ No valid credentials found');
|
|
179
|
+
}
|
|
180
|
+
} catch (err) {
|
|
181
|
+
logger.warn({ error: err.message }, '❌ Failed to load credentials from localStorage');
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function clearStoredCredentials() {
|
|
187
|
+
try {
|
|
188
|
+
localStorage.removeItem(STORAGE_KEYS.STORACHA_KEY);
|
|
189
|
+
localStorage.removeItem(STORAGE_KEYS.STORACHA_PROOF);
|
|
190
|
+
localStorage.removeItem(STORAGE_KEYS.AUTO_LOGIN);
|
|
191
|
+
} catch (err) {
|
|
192
|
+
logger.warn({ error: err.message }, 'Failed to clear credentials from localStorage');
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Auto-hide messages
|
|
197
|
+
function showMessage(message, type = 'info') {
|
|
198
|
+
if (type === 'error') {
|
|
199
|
+
error = message;
|
|
200
|
+
success = null;
|
|
201
|
+
} else {
|
|
202
|
+
success = message;
|
|
203
|
+
error = null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
setTimeout(() => {
|
|
207
|
+
error = null;
|
|
208
|
+
success = null;
|
|
209
|
+
}, 5000);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Clear forms
|
|
213
|
+
function clearForms() {
|
|
214
|
+
showCreateForm = false;
|
|
215
|
+
showCredentialsForm = false;
|
|
216
|
+
email = '';
|
|
217
|
+
storachaKey = '';
|
|
218
|
+
storachaProof = '';
|
|
219
|
+
newSpaceName = '';
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Login with email
|
|
223
|
+
async function handleEmailLogin() {
|
|
224
|
+
if (!email.trim()) {
|
|
225
|
+
showMessage('Please enter your email address', 'error');
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
isLoading = true;
|
|
230
|
+
status = 'Creating account...';
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
const result = await createStorachaAccount(email.trim());
|
|
234
|
+
|
|
235
|
+
if (result.success) {
|
|
236
|
+
showMessage(result.message);
|
|
237
|
+
clearForms();
|
|
238
|
+
} else {
|
|
239
|
+
showMessage(result.error, 'error');
|
|
240
|
+
}
|
|
241
|
+
} catch (err) {
|
|
242
|
+
showMessage(`Failed to create account: ${err.message}`, 'error');
|
|
243
|
+
} finally {
|
|
244
|
+
isLoading = false;
|
|
245
|
+
status = '';
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Login with credentials
|
|
250
|
+
async function handleCredentialsLogin(useStoredCredentials = false) {
|
|
251
|
+
logger.info({ useStoredCredentials }, '🚀 handleCredentialsLogin called with useStoredCredentials =');
|
|
252
|
+
let keyToUse = storachaKey.trim();
|
|
253
|
+
let proofToUse = storachaProof.trim();
|
|
254
|
+
|
|
255
|
+
logger.info({
|
|
256
|
+
storachaKey: storachaKey ? '***' : 'empty',
|
|
257
|
+
storachaProof: storachaProof ? '***' : 'empty',
|
|
258
|
+
selectedSpace: selectedSpace?.did() || 'none'
|
|
259
|
+
}, '🔍 Form values');
|
|
260
|
+
|
|
261
|
+
// If using stored credentials, load them
|
|
262
|
+
if (useStoredCredentials) {
|
|
263
|
+
logger.info('🔄 Loading stored credentials for auto-login...');
|
|
264
|
+
const stored = loadCredentials();
|
|
265
|
+
if (!stored) {
|
|
266
|
+
logger.info('⚠️ Auto-login failed: no stored credentials');
|
|
267
|
+
showMessage('No stored credentials found', 'error');
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
keyToUse = stored.key;
|
|
271
|
+
proofToUse = stored.proof;
|
|
272
|
+
logger.info('✅ Loaded stored credentials successfully');
|
|
273
|
+
} else {
|
|
274
|
+
logger.info('🔐 Manual login with form credentials');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (!keyToUse || !proofToUse) {
|
|
278
|
+
showMessage('Please provide both Storacha key and proof', 'error');
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
isLoading = true;
|
|
283
|
+
status = useStoredCredentials ? 'Auto-logging in...' : 'Logging in...';
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
client = await initializeStorachaClient(keyToUse, proofToUse);
|
|
287
|
+
|
|
288
|
+
// Initialize the bridge with credentials
|
|
289
|
+
initializeBridge(keyToUse, proofToUse);
|
|
290
|
+
|
|
291
|
+
// For credential-based login, check current space instead of accounts
|
|
292
|
+
currentSpace = client.currentSpace();
|
|
293
|
+
if (currentSpace) {
|
|
294
|
+
// Credential-based login successful
|
|
295
|
+
isLoggedIn = true;
|
|
296
|
+
|
|
297
|
+
// Save credentials for future auto-login (only if not already stored)
|
|
298
|
+
if (!useStoredCredentials) {
|
|
299
|
+
saveCredentials(keyToUse, proofToUse);
|
|
300
|
+
showMessage('Successfully logged in to Storacha! Credentials saved for auto-login.');
|
|
301
|
+
} else {
|
|
302
|
+
showMessage('Successfully auto-logged in to Storacha!');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
clearForms();
|
|
306
|
+
|
|
307
|
+
// Load spaces (will handle credential-based client properly)
|
|
308
|
+
await loadSpaces();
|
|
309
|
+
} else {
|
|
310
|
+
// Try traditional account-based approach as fallback
|
|
311
|
+
const accounts = client.accounts();
|
|
312
|
+
if (accounts.length > 0) {
|
|
313
|
+
isLoggedIn = true;
|
|
314
|
+
|
|
315
|
+
if (!useStoredCredentials) {
|
|
316
|
+
saveCredentials(keyToUse, proofToUse);
|
|
317
|
+
showMessage('Successfully logged in to Storacha! Credentials saved for auto-login.');
|
|
318
|
+
} else {
|
|
319
|
+
showMessage('Successfully auto-logged in to Storacha!');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
clearForms();
|
|
323
|
+
await loadSpaces();
|
|
324
|
+
} else {
|
|
325
|
+
throw new Error('Failed to authenticate - no space or account found');
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
} catch (err) {
|
|
329
|
+
showMessage(`Login failed: ${err.message}`, 'error');
|
|
330
|
+
// If auto-login failed, clear stored credentials
|
|
331
|
+
if (useStoredCredentials) {
|
|
332
|
+
clearStoredCredentials();
|
|
333
|
+
}
|
|
334
|
+
} finally {
|
|
335
|
+
isLoading = false;
|
|
336
|
+
status = '';
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Logout
|
|
341
|
+
function handleLogout() {
|
|
342
|
+
isLoggedIn = false;
|
|
343
|
+
client = null;
|
|
344
|
+
currentSpace = null;
|
|
345
|
+
spaces = [];
|
|
346
|
+
clearForms();
|
|
347
|
+
clearStoredCredentials(); // Clear stored credentials on logout
|
|
348
|
+
|
|
349
|
+
// Clean up bridge
|
|
350
|
+
if (bridge) {
|
|
351
|
+
bridge.removeAllListeners();
|
|
352
|
+
bridge = null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
resetProgress();
|
|
356
|
+
showMessage('Logged out successfully');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Load spaces
|
|
360
|
+
async function loadSpaces() {
|
|
361
|
+
if (!client) return;
|
|
362
|
+
|
|
363
|
+
isLoading = true;
|
|
364
|
+
status = 'Loading spaces...';
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
spaces = await listSpaces(client);
|
|
368
|
+
logger.info({ count: spaces.length, spaces: spaces.map(s => s.did()) }, '📋 Available spaces loaded');
|
|
369
|
+
} catch (err) {
|
|
370
|
+
showMessage(`Failed to load spaces: ${err.message}`, 'error');
|
|
371
|
+
} finally {
|
|
372
|
+
isLoading = false;
|
|
373
|
+
status = '';
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Create new space
|
|
378
|
+
async function handleCreateSpace() {
|
|
379
|
+
if (!newSpaceName.trim()) {
|
|
380
|
+
showMessage('Please enter a space name', 'error');
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
isLoading = true;
|
|
385
|
+
status = 'Creating space...';
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
const result = await createSpace(client, newSpaceName.trim());
|
|
389
|
+
|
|
390
|
+
if (result.success) {
|
|
391
|
+
showMessage(`Space "${newSpaceName}" created successfully!`);
|
|
392
|
+
newSpaceName = '';
|
|
393
|
+
await loadSpaces(); // Reload spaces
|
|
394
|
+
} else {
|
|
395
|
+
showMessage(result.error, 'error');
|
|
396
|
+
}
|
|
397
|
+
} catch (err) {
|
|
398
|
+
showMessage(`Failed to create space: ${err.message}`, 'error');
|
|
399
|
+
} finally {
|
|
400
|
+
isLoading = false;
|
|
401
|
+
status = '';
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Set current space
|
|
406
|
+
async function selectSpace(space) {
|
|
407
|
+
isLoading = true;
|
|
408
|
+
status = 'Switching space...';
|
|
409
|
+
|
|
410
|
+
try {
|
|
411
|
+
await client.setCurrentSpace(space.did);
|
|
412
|
+
currentSpace = space;
|
|
413
|
+
showMessage(`Switched to space: ${space.name}`);
|
|
414
|
+
} catch (err) {
|
|
415
|
+
showMessage(`Failed to switch space: ${err.message}`, 'error');
|
|
416
|
+
} finally {
|
|
417
|
+
isLoading = false;
|
|
418
|
+
status = '';
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Backup database with progress tracking
|
|
423
|
+
async function handleBackup() {
|
|
424
|
+
if (!bridge) {
|
|
425
|
+
showMessage('Please log in first', 'error');
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (!$initializationStore.isInitialized) {
|
|
430
|
+
showMessage('OrbitDB is not initialized yet', 'error');
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if ($todosStore.length === 0) {
|
|
435
|
+
showMessage('No todos to backup', 'error');
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
isLoading = true;
|
|
440
|
+
resetProgress();
|
|
441
|
+
status = 'Preparing backup...';
|
|
442
|
+
|
|
443
|
+
try {
|
|
444
|
+
logger.info('🚀 Starting backup with real progress tracking...', $todoDBStore);
|
|
445
|
+
|
|
446
|
+
const result = await bridge.backup($orbitDBStore, $todoDBStore.address);
|
|
447
|
+
|
|
448
|
+
if (result.success) {
|
|
449
|
+
showMessage(
|
|
450
|
+
`Backup completed! ${result.blocksUploaded}/${result.blocksTotal} blocks uploaded`
|
|
451
|
+
);
|
|
452
|
+
} else {
|
|
453
|
+
showMessage(result.error, 'error');
|
|
454
|
+
}
|
|
455
|
+
} catch (err) {
|
|
456
|
+
showMessage(`Backup failed: ${err.message}`, 'error');
|
|
457
|
+
} finally {
|
|
458
|
+
isLoading = false;
|
|
459
|
+
status = '';
|
|
460
|
+
resetProgress();
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
// Format date
|
|
467
|
+
function formatDate(dateString) {
|
|
468
|
+
return new Date(dateString).toLocaleString();
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Format space name
|
|
472
|
+
function formatSpaceName(space) {
|
|
473
|
+
return space.name === 'Unnamed Space' ? `Space ${space.did.slice(-8)}` : space.name;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
// Auto-login on component mount
|
|
479
|
+
onMount(async () => {
|
|
480
|
+
logger.info('🚀 StorachaIntegration component mounted');
|
|
481
|
+
|
|
482
|
+
// Try to auto-login with stored credentials (but don't show error for first-time users)
|
|
483
|
+
const stored = loadCredentials();
|
|
484
|
+
if (stored) {
|
|
485
|
+
logger.info('🔐 Found stored Storacha credentials, attempting auto-login...');
|
|
486
|
+
try {
|
|
487
|
+
await handleCredentialsLogin(true);
|
|
488
|
+
} catch (err) {
|
|
489
|
+
logger.warn({ error: err.message }, '⚠️ Auto-login failed, clearing stored credentials:');
|
|
490
|
+
clearStoredCredentials();
|
|
491
|
+
}
|
|
492
|
+
} else {
|
|
493
|
+
logger.info('🔒 No stored credentials found, user needs to login manually');
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
async function restoreFromSpace() {
|
|
500
|
+
if (!$orbitDBStore) {
|
|
501
|
+
showMessage('OrbitDB not initialized. Please wait for initialization to complete.', 'error');
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
isLoading = true;
|
|
506
|
+
resetProgress();
|
|
507
|
+
status = 'Preparing restore...';
|
|
508
|
+
|
|
509
|
+
try {
|
|
510
|
+
logger.info('🔄 Starting restore from Storacha space with fresh OrbitDB instance...');
|
|
511
|
+
|
|
512
|
+
// Get Storacha credentials
|
|
513
|
+
const storachaKey = localStorage.getItem('storacha_key') || '';
|
|
514
|
+
const storachaProof = localStorage.getItem('storacha_proof') || '';
|
|
515
|
+
|
|
516
|
+
if (!storachaKey || !storachaProof) {
|
|
517
|
+
throw new Error('Storacha credentials not found. Please login to Storacha first.');
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Step 1: Create fresh instances for restore
|
|
521
|
+
status = 'Creating fresh OrbitDB instance for restore...';
|
|
522
|
+
logger.info('🔧 Creating fresh OrbitDB instance for restore...');
|
|
523
|
+
|
|
524
|
+
// Create unique libp2p config
|
|
525
|
+
const config = await createLibp2pConfig({
|
|
526
|
+
enablePeerConnections: false,
|
|
527
|
+
enableNetworkConnection: false,
|
|
528
|
+
enablePersistentStorage: true
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
// Create fresh Helia instance with persistent initializeDatabasestorage
|
|
533
|
+
logger.info('🗄️ Initializing fresh Helia with persistent storage...');
|
|
534
|
+
const blockstore = new LevelBlockstore(`./helia-blocks-restore-${Date.now()}`);
|
|
535
|
+
const datastore = new LevelDatastore(`./helia-data-restore-${Date.now()}`);
|
|
536
|
+
// Helia 7 builds libp2p from options, and fills any of the keys it knows
|
|
537
|
+
// that are left out from its own defaults, which dial the public network.
|
|
538
|
+
const newHelia = await createHelia({
|
|
539
|
+
libp2p: { addresses: { listen: [] }, peerDiscovery: [], services: {}, ...config },
|
|
540
|
+
blockstore,
|
|
541
|
+
datastore
|
|
542
|
+
}).start();
|
|
543
|
+
const newLibp2p = newHelia.libp2p;
|
|
544
|
+
logger.info('✅ Fresh libp2p instance created');
|
|
545
|
+
logger.info('✅ Fresh Helia instance created');
|
|
546
|
+
|
|
547
|
+
// Create fresh OrbitDB instance
|
|
548
|
+
const newOrbitDB = await createOrbitDB({
|
|
549
|
+
ipfs: newHelia,
|
|
550
|
+
id: `restored-instance-${Date.now()}`
|
|
551
|
+
});
|
|
552
|
+
logger.info('✅ Fresh OrbitDB instance created:', newOrbitDB.id);
|
|
553
|
+
|
|
554
|
+
// Step 2: Restore from Storacha backup
|
|
555
|
+
status = 'Restoring database from Storacha backup...';
|
|
556
|
+
logger.info('🔄 Restoring database from Storacha backup...');
|
|
557
|
+
|
|
558
|
+
const result = await restoreDatabaseFromSpace(newOrbitDB, {
|
|
559
|
+
storachaKey,
|
|
560
|
+
storachaProof,
|
|
561
|
+
timeout: 60000
|
|
562
|
+
});
|
|
563
|
+
logger.info('Restore result:', result);
|
|
564
|
+
|
|
565
|
+
if (result.success) {
|
|
566
|
+
// Step 3: Replace the current instances with the restored ones
|
|
567
|
+
status = 'Replacing current database with restored data...';
|
|
568
|
+
logger.info('🔄 Replacing current database with restored data...');
|
|
569
|
+
|
|
570
|
+
// Close existing instances (if any)
|
|
571
|
+
try {
|
|
572
|
+
if ($todoDBStore) {
|
|
573
|
+
await $todoDBStore.close();
|
|
574
|
+
}
|
|
575
|
+
} catch (closeError) {
|
|
576
|
+
logger.warn({ error: closeError.message }, '⚠️ Error closing existing database:');
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Update the stores with new instances
|
|
580
|
+
libp2pStore.set(newLibp2p);
|
|
581
|
+
peerIdStore.set(newLibp2p.peerId.toString());
|
|
582
|
+
orbitDBStore.set(newOrbitDB);
|
|
583
|
+
|
|
584
|
+
// Initialize database with the restored database
|
|
585
|
+
await initializeDatabase(newOrbitDB, result.database, {
|
|
586
|
+
enablePersistentStorage: true,
|
|
587
|
+
enableNetworkConnection: true,
|
|
588
|
+
enablePeerConnections: true
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
// Load todos from the restored database
|
|
592
|
+
await loadTodos();
|
|
593
|
+
|
|
594
|
+
showMessage(
|
|
595
|
+
`Database restored successfully! ${result.entriesRecovered} entries recovered from Storacha space. Fresh OrbitDB instance created.`
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
logger.info('🎉 Restore completed with fresh instance:', {
|
|
599
|
+
newOrbitDBId: newOrbitDB.id,
|
|
600
|
+
newPeerId: newLibp2p.peerId.toString(),
|
|
601
|
+
restoredAddress: result.database.address,
|
|
602
|
+
entriesRecovered: result.entriesRecovered
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
} else {
|
|
606
|
+
// If restore failed, cleanup the new instances
|
|
607
|
+
try {
|
|
608
|
+
await newOrbitDB.stop();
|
|
609
|
+
await newHelia.stop();
|
|
610
|
+
await newLibp2p.stop();
|
|
611
|
+
} catch (cleanupError) {
|
|
612
|
+
logger.warn({ error: cleanupError.message }, '⚠️ Error cleaning up failed restore instances:');
|
|
613
|
+
}
|
|
614
|
+
showMessage(`Restore failed: ${result.error}`, 'error');
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
} catch (error) {
|
|
618
|
+
logger.error({ error }, '❌ Restore failed:');
|
|
619
|
+
showMessage(`Restore failed: ${error.message}`, 'error');
|
|
620
|
+
} finally {
|
|
621
|
+
isLoading = false;
|
|
622
|
+
status = '';
|
|
623
|
+
resetProgress();
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
</script>
|
|
627
|
+
|
|
628
|
+
<div
|
|
629
|
+
class="mt-6 rounded-lg border border-blue-200 bg-gradient-to-r from-blue-50 to-indigo-50 p-4 dark:border-gray-600 dark:from-gray-700 dark:to-gray-600"
|
|
630
|
+
>
|
|
631
|
+
<!-- Header -->
|
|
632
|
+
<div class="mb-4 flex items-center justify-between">
|
|
633
|
+
<div class="flex items-center space-x-2">
|
|
634
|
+
<Cloud class="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
|
635
|
+
<h3 class="text-lg font-semibold text-gray-800 dark:text-white">Storacha Integration</h3>
|
|
636
|
+
</div>
|
|
637
|
+
|
|
638
|
+
<button
|
|
639
|
+
on:click={() => (showStoracha = !showStoracha)}
|
|
640
|
+
class="text-blue-600 transition-colors hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
|
|
641
|
+
>
|
|
642
|
+
{showStoracha ? 'Hide' : 'Show'}
|
|
643
|
+
</button>
|
|
644
|
+
</div>
|
|
645
|
+
|
|
646
|
+
{#if showStoracha}
|
|
647
|
+
<!-- Status Messages -->
|
|
648
|
+
{#if error}
|
|
649
|
+
<div
|
|
650
|
+
class="mb-4 rounded-md border border-red-300 bg-red-100 p-3 dark:border-red-600 dark:bg-red-900/30"
|
|
651
|
+
>
|
|
652
|
+
<div class="flex items-center space-x-2">
|
|
653
|
+
<AlertCircle class="h-4 w-4 text-red-600 dark:text-red-400" />
|
|
654
|
+
<span class="text-sm text-red-700 dark:text-red-300">{error}</span>
|
|
655
|
+
</div>
|
|
656
|
+
</div>
|
|
657
|
+
{/if}
|
|
658
|
+
|
|
659
|
+
{#if success}
|
|
660
|
+
<div
|
|
661
|
+
class="mb-4 rounded-md border border-green-300 bg-green-100 p-3 dark:border-green-600 dark:bg-green-900/30"
|
|
662
|
+
>
|
|
663
|
+
<div class="flex items-center space-x-2">
|
|
664
|
+
<CheckCircle class="h-4 w-4 text-green-600 dark:text-green-400" />
|
|
665
|
+
<span class="text-sm text-green-700 dark:text-green-300">{success}</span>
|
|
666
|
+
</div>
|
|
667
|
+
</div>
|
|
668
|
+
{/if}
|
|
669
|
+
|
|
670
|
+
{#if status}
|
|
671
|
+
<div
|
|
672
|
+
class="mb-4 rounded-md border border-blue-300 bg-blue-100 p-3 dark:border-blue-600 dark:bg-blue-900/30"
|
|
673
|
+
>
|
|
674
|
+
<div class="flex items-center space-x-2">
|
|
675
|
+
<Loader2 class="h-4 w-4 animate-spin text-blue-600 dark:text-blue-400" />
|
|
676
|
+
<span class="text-sm text-blue-700 dark:text-blue-300">{status}</span>
|
|
677
|
+
</div>
|
|
678
|
+
</div>
|
|
679
|
+
{/if}
|
|
680
|
+
|
|
681
|
+
<!-- Progress Bar -->
|
|
682
|
+
{#if showProgress}
|
|
683
|
+
<div
|
|
684
|
+
class="mb-4 rounded-md border border-purple-300 bg-purple-100 p-3 dark:border-purple-600 dark:bg-purple-900/30"
|
|
685
|
+
>
|
|
686
|
+
<div class="mb-2 flex items-center justify-between text-sm">
|
|
687
|
+
<span class="font-medium text-purple-800 dark:text-purple-200">
|
|
688
|
+
{progressType === 'upload' ? 'Uploading' : 'Downloading'} Progress
|
|
689
|
+
</span>
|
|
690
|
+
<span class="text-purple-700 dark:text-purple-300">
|
|
691
|
+
{progressPercentage}% ({progressCurrent}/{progressTotal})
|
|
692
|
+
</span>
|
|
693
|
+
</div>
|
|
694
|
+
<div class="h-2 w-full rounded-full bg-purple-200 dark:bg-purple-700">
|
|
695
|
+
<div
|
|
696
|
+
class="h-2 rounded-full bg-purple-600 transition-all duration-300 ease-out dark:bg-purple-400"
|
|
697
|
+
style="width: {progressPercentage}%"
|
|
698
|
+
></div>
|
|
699
|
+
</div>
|
|
700
|
+
{#if progressCurrentBlock}
|
|
701
|
+
<div class="mt-1 font-mono text-xs text-purple-600 dark:text-purple-400">
|
|
702
|
+
{progressType === 'upload' ? 'Current block hash:' : 'Current CID:'}
|
|
703
|
+
{progressType === 'upload'
|
|
704
|
+
? progressCurrentBlock.hash?.slice(0, 16)
|
|
705
|
+
: progressCurrentBlock.storachaCID?.slice(0, 16)}...
|
|
706
|
+
</div>
|
|
707
|
+
{/if}
|
|
708
|
+
{#if progressError}
|
|
709
|
+
<div class="mt-1 text-xs text-red-600 dark:text-red-400">
|
|
710
|
+
Error: {progressError.message}
|
|
711
|
+
</div>
|
|
712
|
+
{/if}
|
|
713
|
+
</div>
|
|
714
|
+
{/if}
|
|
715
|
+
|
|
716
|
+
{#if !isLoggedIn}
|
|
717
|
+
<!-- Login Section -->
|
|
718
|
+
<div class="space-y-4">
|
|
719
|
+
<div class="text-center text-sm text-gray-600 dark:text-gray-300">
|
|
720
|
+
Connect to Storacha to backup your todos to decentralized storage
|
|
721
|
+
</div>
|
|
722
|
+
|
|
723
|
+
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
|
|
724
|
+
<button
|
|
725
|
+
on:click={() => {
|
|
726
|
+
clearForms();
|
|
727
|
+
showCreateForm = true;
|
|
728
|
+
}}
|
|
729
|
+
disabled={isLoading}
|
|
730
|
+
class="flex items-center justify-center space-x-2 rounded-md bg-green-600 px-4 py-2 text-white transition-colors hover:bg-green-700 disabled:opacity-50"
|
|
731
|
+
>
|
|
732
|
+
<Mail class="h-4 w-4" />
|
|
733
|
+
<span>Create New Account</span>
|
|
734
|
+
</button>
|
|
735
|
+
|
|
736
|
+
<button
|
|
737
|
+
on:click={() => {
|
|
738
|
+
clearForms();
|
|
739
|
+
showCredentialsForm = true;
|
|
740
|
+
}}
|
|
741
|
+
disabled={isLoading}
|
|
742
|
+
class="flex items-center justify-center space-x-2 rounded-md bg-blue-600 px-4 py-2 text-white transition-colors hover:bg-blue-700 disabled:opacity-50"
|
|
743
|
+
>
|
|
744
|
+
<Key class="h-4 w-4" />
|
|
745
|
+
<span>Login with Credentials</span>
|
|
746
|
+
</button>
|
|
747
|
+
</div>
|
|
748
|
+
|
|
749
|
+
<!-- Create Account Form -->
|
|
750
|
+
{#if showCreateForm}
|
|
751
|
+
<div class="rounded-md border bg-white p-4 dark:bg-gray-700">
|
|
752
|
+
<h4 class="text-md mb-3 font-medium text-gray-800 dark:text-white">
|
|
753
|
+
Create New Storacha Account
|
|
754
|
+
</h4>
|
|
755
|
+
<div class="space-y-3">
|
|
756
|
+
<input
|
|
757
|
+
bind:value={email}
|
|
758
|
+
type="email"
|
|
759
|
+
placeholder="Enter your email address"
|
|
760
|
+
class="w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-gray-900 focus:border-transparent focus:ring-2 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-800 dark:text-white"
|
|
761
|
+
/>
|
|
762
|
+
<div class="flex space-x-2">
|
|
763
|
+
<button
|
|
764
|
+
on:click={handleEmailLogin}
|
|
765
|
+
disabled={isLoading || !email.trim()}
|
|
766
|
+
class="flex-1 rounded-md bg-green-600 px-4 py-2 text-white transition-colors hover:bg-green-700 disabled:opacity-50"
|
|
767
|
+
>
|
|
768
|
+
{isLoading ? 'Creating...' : 'Create Account'}
|
|
769
|
+
</button>
|
|
770
|
+
<button
|
|
771
|
+
on:click={clearForms}
|
|
772
|
+
class="rounded-md bg-gray-300 px-4 py-2 text-gray-700 transition-colors hover:bg-gray-400 dark:bg-gray-600 dark:text-gray-200 dark:hover:bg-gray-500"
|
|
773
|
+
>
|
|
774
|
+
Cancel
|
|
775
|
+
</button>
|
|
776
|
+
</div>
|
|
777
|
+
</div>
|
|
778
|
+
</div>
|
|
779
|
+
{/if}
|
|
780
|
+
|
|
781
|
+
<!-- Credentials Form -->
|
|
782
|
+
{#if showCredentialsForm}
|
|
783
|
+
<div class="rounded-md border bg-white p-4 dark:bg-gray-700">
|
|
784
|
+
<h4 class="text-md mb-3 font-medium text-gray-800 dark:text-white">
|
|
785
|
+
Login with Storacha Credentials
|
|
786
|
+
</h4>
|
|
787
|
+
<div class="space-y-3">
|
|
788
|
+
<input
|
|
789
|
+
bind:value={storachaKey}
|
|
790
|
+
type="password"
|
|
791
|
+
placeholder="Storacha Private Key"
|
|
792
|
+
class="w-full rounded-md border border-gray-300 bg-white px-3 py-2 font-mono text-sm text-gray-900 focus:border-transparent focus:ring-2 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-800 dark:text-white"
|
|
793
|
+
/>
|
|
794
|
+
<textarea
|
|
795
|
+
bind:value={storachaProof}
|
|
796
|
+
placeholder="Storacha Proof (delegation)"
|
|
797
|
+
rows="3"
|
|
798
|
+
class="w-full resize-none rounded-md border border-gray-300 bg-white px-3 py-2 font-mono text-xs text-gray-900 focus:border-transparent focus:ring-2 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-800 dark:text-white"
|
|
799
|
+
></textarea>
|
|
800
|
+
<div class="flex space-x-2">
|
|
801
|
+
<button
|
|
802
|
+
on:click={() => handleCredentialsLogin()}
|
|
803
|
+
disabled={isLoading || !storachaKey.trim() || !storachaProof.trim()}
|
|
804
|
+
class="flex-1 rounded-md bg-blue-600 px-4 py-2 text-white transition-colors hover:bg-blue-700 disabled:opacity-50"
|
|
805
|
+
>
|
|
806
|
+
{isLoading ? 'Logging in...' : 'Login'}
|
|
807
|
+
</button>
|
|
808
|
+
<button
|
|
809
|
+
on:click={clearForms}
|
|
810
|
+
class="rounded-md bg-gray-300 px-4 py-2 text-gray-700 transition-colors hover:bg-gray-400 dark:bg-gray-600 dark:text-gray-200 dark:hover:bg-gray-500"
|
|
811
|
+
>
|
|
812
|
+
Cancel
|
|
813
|
+
</button>
|
|
814
|
+
</div>
|
|
815
|
+
</div>
|
|
816
|
+
</div>
|
|
817
|
+
{/if}
|
|
818
|
+
</div>
|
|
819
|
+
{:else}
|
|
820
|
+
<!-- Logged In Section -->
|
|
821
|
+
<div class="space-y-4">
|
|
822
|
+
<!-- Account Info -->
|
|
823
|
+
<div
|
|
824
|
+
class="flex items-center justify-between rounded-md border bg-white p-3 dark:bg-gray-700"
|
|
825
|
+
>
|
|
826
|
+
<div class="flex items-center space-x-3">
|
|
827
|
+
<div
|
|
828
|
+
class="flex h-8 w-8 items-center justify-center rounded-full bg-green-100 dark:bg-green-900/30"
|
|
829
|
+
>
|
|
830
|
+
<CheckCircle class="h-4 w-4 text-green-600 dark:text-green-400" />
|
|
831
|
+
</div>
|
|
832
|
+
<div>
|
|
833
|
+
<div class="text-sm font-medium text-gray-800 dark:text-white">
|
|
834
|
+
Connected to Storacha
|
|
835
|
+
</div>
|
|
836
|
+
{#if currentSpace}
|
|
837
|
+
<div class="text-xs text-gray-500 dark:text-gray-400">
|
|
838
|
+
Current space: {formatSpaceName(currentSpace)}
|
|
839
|
+
</div>
|
|
840
|
+
{/if}
|
|
841
|
+
</div>
|
|
842
|
+
</div>
|
|
843
|
+
|
|
844
|
+
<button
|
|
845
|
+
on:click={handleLogout}
|
|
846
|
+
class="flex items-center space-x-1 px-3 py-1 text-sm text-red-600 transition-colors hover:text-red-800 dark:text-red-400 dark:hover:text-red-300"
|
|
847
|
+
>
|
|
848
|
+
<LogOut class="h-3 w-3" />
|
|
849
|
+
<span>Logout</span>
|
|
850
|
+
</button>
|
|
851
|
+
</div>
|
|
852
|
+
|
|
853
|
+
<!-- Action Buttons -->
|
|
854
|
+
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
|
|
855
|
+
<button
|
|
856
|
+
on:click={handleBackup}
|
|
857
|
+
disabled={isLoading || !$initializationStore.isInitialized || $todosStore.length === 0}
|
|
858
|
+
class="flex items-center justify-center space-x-2 rounded-md bg-green-600 px-4 py-2 text-white transition-colors hover:bg-green-700 disabled:opacity-50"
|
|
859
|
+
>
|
|
860
|
+
<Upload class="h-4 w-4" />
|
|
861
|
+
<span>Backup Database</span>
|
|
862
|
+
</button>
|
|
863
|
+
|
|
864
|
+
<button
|
|
865
|
+
on:click={restoreFromSpace}
|
|
866
|
+
disabled={isLoading || !$initializationStore.isInitialized}
|
|
867
|
+
class="flex items-center justify-center space-x-2 rounded-md bg-orange-600 px-4 py-2 text-white transition-colors hover:bg-orange-700 disabled:opacity-50"
|
|
868
|
+
>
|
|
869
|
+
<Download class="h-4 w-4" />
|
|
870
|
+
<span>Restore Database</span>
|
|
871
|
+
</button>
|
|
872
|
+
</div>
|
|
873
|
+
|
|
874
|
+
<!-- Spaces Management -->
|
|
875
|
+
<div class="rounded-md border bg-white p-4 dark:bg-gray-700">
|
|
876
|
+
<h4
|
|
877
|
+
class="text-md mb-3 flex items-center space-x-2 font-medium text-gray-800 dark:text-white"
|
|
878
|
+
>
|
|
879
|
+
<List class="h-4 w-4" />
|
|
880
|
+
<span>Spaces ({spaces.length})</span>
|
|
881
|
+
</h4>
|
|
882
|
+
|
|
883
|
+
<!-- Create New Space -->
|
|
884
|
+
<div class="mb-4 flex space-x-2">
|
|
885
|
+
<input
|
|
886
|
+
bind:value={newSpaceName}
|
|
887
|
+
placeholder="New space name"
|
|
888
|
+
class="flex-1 rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 focus:border-transparent focus:ring-2 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-800 dark:text-white"
|
|
889
|
+
/>
|
|
890
|
+
<button
|
|
891
|
+
on:click={handleCreateSpace}
|
|
892
|
+
disabled={isLoading || !newSpaceName.trim()}
|
|
893
|
+
class="rounded-md bg-green-600 px-3 py-2 text-white transition-colors hover:bg-green-700 disabled:opacity-50"
|
|
894
|
+
>
|
|
895
|
+
<Plus class="h-4 w-4" />
|
|
896
|
+
</button>
|
|
897
|
+
</div>
|
|
898
|
+
|
|
899
|
+
<!-- Spaces List -->
|
|
900
|
+
{#if spaces.length === 0}
|
|
901
|
+
<div class="py-4 text-center text-sm text-gray-500 dark:text-gray-400">
|
|
902
|
+
No spaces found
|
|
903
|
+
</div>
|
|
904
|
+
{:else}
|
|
905
|
+
<div class="max-h-40 space-y-2 overflow-y-auto">
|
|
906
|
+
{#each spaces as space (space.did)}
|
|
907
|
+
<div
|
|
908
|
+
class="flex items-center justify-between rounded border bg-gray-50 p-2 dark:bg-gray-600"
|
|
909
|
+
>
|
|
910
|
+
<div>
|
|
911
|
+
<div class="text-sm font-medium text-gray-800 dark:text-white">
|
|
912
|
+
{formatSpaceName(space)}
|
|
913
|
+
</div>
|
|
914
|
+
<div class="font-mono text-xs text-gray-500 dark:text-gray-400">
|
|
915
|
+
{space.did.slice(0, 20)}...
|
|
916
|
+
</div>
|
|
917
|
+
</div>
|
|
918
|
+
{#if currentSpace?.did !== space.did}
|
|
919
|
+
<button
|
|
920
|
+
on:click={() => selectSpace(space)}
|
|
921
|
+
class="px-2 py-1 text-sm text-blue-600 transition-colors hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
|
|
922
|
+
>
|
|
923
|
+
Select
|
|
924
|
+
</button>
|
|
925
|
+
{:else}
|
|
926
|
+
<span
|
|
927
|
+
class="px-2 py-1 text-sm font-medium text-green-600 dark:text-green-400"
|
|
928
|
+
>
|
|
929
|
+
Current
|
|
930
|
+
</span>
|
|
931
|
+
{/if}
|
|
932
|
+
</div>
|
|
933
|
+
{/each}
|
|
934
|
+
</div>
|
|
935
|
+
{/if}
|
|
936
|
+
</div>
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
</div>
|
|
940
|
+
{/if}
|
|
941
|
+
{/if}
|
|
942
|
+
</div>
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
<style>
|
|
947
|
+
/* Custom scrollbar for webkit browsers */
|
|
948
|
+
.overflow-y-auto::-webkit-scrollbar {
|
|
949
|
+
width: 4px;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
.overflow-y-auto::-webkit-scrollbar-track {
|
|
953
|
+
background: rgba(0, 0, 0, 0.1);
|
|
954
|
+
border-radius: 2px;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
.overflow-y-auto::-webkit-scrollbar-thumb {
|
|
958
|
+
background: rgba(0, 0, 0, 0.3);
|
|
959
|
+
border-radius: 2px;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
.overflow-y-auto::-webkit-scrollbar-thumb:hover {
|
|
963
|
+
background: rgba(0, 0, 0, 0.4);
|
|
964
|
+
}
|
|
965
|
+
</style>
|