hyperbook 0.84.3 → 0.84.5
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/dist/assets/bootstrap.js +248 -0
- package/dist/assets/cloud.js +217 -169
- package/dist/assets/directive-abc-music/client.js +11 -3
- package/dist/assets/directive-archive/client.js +17 -4
- package/dist/assets/directive-audio/client.js +67 -28
- package/dist/assets/directive-bookmarks/client.js +9 -1
- package/dist/assets/directive-download/client.js +10 -2
- package/dist/assets/directive-excalidraw/client.js +9 -0
- package/dist/assets/directive-excalidraw/hyperbook-excalidraw.umd.js +1 -1
- package/dist/assets/directive-geogebra/client.js +16 -3
- package/dist/assets/directive-h5p/client.js +32 -3
- package/dist/assets/directive-learningmap/client.js +11 -3
- package/dist/assets/directive-mermaid/client.js +11 -1
- package/dist/assets/directive-multievent/multievent.js +2 -2
- package/dist/assets/directive-onlineide/client.js +7 -0
- package/dist/assets/directive-onlineide/include/online-ide-embedded.js +43 -43
- package/dist/assets/directive-p5/client.js +39 -7
- package/dist/assets/directive-protect/client.js +11 -3
- package/dist/assets/directive-pyide/client.js +20 -9
- package/dist/assets/directive-scratchblock/client.js +9 -0
- package/dist/assets/directive-slideshow/client.js +12 -4
- package/dist/assets/directive-sqlide/client.js +7 -0
- package/dist/assets/directive-sqlide/include/includeIDE.js +22 -11
- package/dist/assets/directive-sqlide/include/sql-ide-embedded.js +30 -30
- package/dist/assets/directive-tabs/client.js +14 -3
- package/dist/assets/directive-textinput/client.js +14 -3
- package/dist/assets/directive-webide/client.js +45 -10
- package/dist/assets/hyperbook.types.js +209 -0
- package/dist/assets/i18n.js +15 -1
- package/dist/assets/store.js +174 -266
- package/dist/assets/ui.js +279 -0
- package/dist/index.js +53 -29
- package/package.json +4 -4
- package/dist/assets/client.js +0 -506
package/dist/assets/cloud.js
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
|
+
/// <reference path="./hyperbook.types.js" />
|
|
1
2
|
window.hyperbook = window.hyperbook || {};
|
|
2
|
-
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Cloud sync integration for hyperbook store data.
|
|
6
|
+
* Handles authentication, event-based sync, and snapshot sync.
|
|
7
|
+
* @type {HyperbookCloud}
|
|
8
|
+
* @memberof hyperbook
|
|
9
|
+
* @see hyperbook.store
|
|
10
|
+
* @see hyperbook.i18n
|
|
11
|
+
*/
|
|
12
|
+
hyperbook.cloud = (function () {
|
|
3
13
|
// ===== Cloud Integration =====
|
|
4
14
|
const AUTH_TOKEN_KEY = "hyperbook_auth_token";
|
|
5
15
|
const AUTH_USER_KEY = "hyperbook_auth_user";
|
|
16
|
+
const LAST_EVENT_ID_KEY = "hyperbook_last_event_id";
|
|
17
|
+
const EVENT_BATCH_MAX_SIZE = 512 * 1024; // 512KB
|
|
6
18
|
let isLoadingFromCloud = false;
|
|
7
19
|
let syncManager = null;
|
|
8
20
|
|
|
@@ -41,7 +53,6 @@ window.hyperbook.cloud = (function () {
|
|
|
41
53
|
this.maxWaitTime = options.maxWaitTime || 10000;
|
|
42
54
|
this.minSaveInterval = options.minSaveInterval || 1000;
|
|
43
55
|
|
|
44
|
-
this.isDirty = false;
|
|
45
56
|
this.lastSaveTime = 0;
|
|
46
57
|
this.lastChangeTime = 0;
|
|
47
58
|
this.debounceTimer = null;
|
|
@@ -50,110 +61,25 @@ window.hyperbook.cloud = (function () {
|
|
|
50
61
|
this.saveMutex = new Mutex();
|
|
51
62
|
this.retryCount = 0;
|
|
52
63
|
|
|
53
|
-
this.
|
|
64
|
+
this.pendingEvents = [];
|
|
65
|
+
this.lastEventId = parseInt(
|
|
66
|
+
localStorage.getItem(LAST_EVENT_ID_KEY) || "0",
|
|
67
|
+
10,
|
|
68
|
+
);
|
|
54
69
|
|
|
55
70
|
this.offlineQueue = [];
|
|
56
71
|
this.isOnline = navigator.onLine;
|
|
57
72
|
|
|
58
|
-
// Snapshots for external DB polling
|
|
59
|
-
this.externalDBs = options.externalDBs || [];
|
|
60
|
-
this.externalSnapshots = {};
|
|
61
|
-
this.pollInterval = options.pollInterval || 5000;
|
|
62
|
-
this.pollTimer = null;
|
|
63
|
-
|
|
64
73
|
this.setupEventListeners();
|
|
65
74
|
}
|
|
66
75
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
*/
|
|
70
|
-
_snapshotExternalDB(dbName) {
|
|
71
|
-
return new Promise((resolve) => {
|
|
72
|
-
const request = indexedDB.open(dbName);
|
|
73
|
-
request.onerror = () => resolve(null);
|
|
74
|
-
request.onsuccess = (event) => {
|
|
75
|
-
const db = event.target.result;
|
|
76
|
-
const storeNames = Array.from(db.objectStoreNames);
|
|
77
|
-
if (storeNames.length === 0) {
|
|
78
|
-
db.close();
|
|
79
|
-
resolve(null);
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const parts = [];
|
|
84
|
-
const tx = db.transaction(storeNames, "readonly");
|
|
85
|
-
let pending = storeNames.length;
|
|
86
|
-
storeNames.forEach((name) => {
|
|
87
|
-
const rows = [];
|
|
88
|
-
const cursorReq = tx.objectStore(name).openCursor();
|
|
89
|
-
cursorReq.onsuccess = (e) => {
|
|
90
|
-
const cursor = e.target.result;
|
|
91
|
-
if (cursor) {
|
|
92
|
-
rows.push(JSON.stringify(cursor.value));
|
|
93
|
-
cursor.continue();
|
|
94
|
-
} else {
|
|
95
|
-
parts.push(name + ":" + rows.join(","));
|
|
96
|
-
pending--;
|
|
97
|
-
if (pending === 0) {
|
|
98
|
-
db.close();
|
|
99
|
-
resolve(parts.join("|"));
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
cursorReq.onerror = () => {
|
|
104
|
-
pending--;
|
|
105
|
-
if (pending === 0) {
|
|
106
|
-
db.close();
|
|
107
|
-
resolve(parts.join("|"));
|
|
108
|
-
}
|
|
109
|
-
};
|
|
110
|
-
});
|
|
111
|
-
};
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Take initial snapshots and start polling external DBs for changes.
|
|
117
|
-
*/
|
|
118
|
-
async startPolling() {
|
|
119
|
-
for (const dbName of this.externalDBs) {
|
|
120
|
-
this.externalSnapshots[dbName] = await this._snapshotExternalDB(dbName);
|
|
121
|
-
}
|
|
122
|
-
this.pollTimer = setInterval(
|
|
123
|
-
() => this._pollExternalDBs(),
|
|
124
|
-
this.pollInterval,
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Stop polling external DBs.
|
|
130
|
-
*/
|
|
131
|
-
stopPolling() {
|
|
132
|
-
if (this.pollTimer) {
|
|
133
|
-
clearInterval(this.pollTimer);
|
|
134
|
-
this.pollTimer = null;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
async _pollExternalDBs() {
|
|
139
|
-
if (isLoadingFromCloud || this.saveInProgress) return;
|
|
140
|
-
for (const dbName of this.externalDBs) {
|
|
141
|
-
const current = await this._snapshotExternalDB(dbName);
|
|
142
|
-
const prev = this.externalSnapshots[dbName];
|
|
143
|
-
|
|
144
|
-
if (current !== prev) {
|
|
145
|
-
this.externalSnapshots[dbName] = current;
|
|
146
|
-
this.markDirty(dbName);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
76
|
+
get isDirty() {
|
|
77
|
+
return this.pendingEvents.length > 0;
|
|
149
78
|
}
|
|
150
79
|
|
|
151
|
-
|
|
80
|
+
addEvent(event) {
|
|
152
81
|
if (isLoadingFromCloud || isReadOnlyMode()) return;
|
|
153
|
-
|
|
154
|
-
this.dirtyStores.add(storeName);
|
|
155
|
-
}
|
|
156
|
-
this.isDirty = true;
|
|
82
|
+
this.pendingEvents.push(event);
|
|
157
83
|
this.lastChangeTime = Date.now();
|
|
158
84
|
this.updateUI("unsaved");
|
|
159
85
|
this.scheduleSave();
|
|
@@ -196,24 +122,46 @@ window.hyperbook.cloud = (function () {
|
|
|
196
122
|
this.updateUI("saving");
|
|
197
123
|
|
|
198
124
|
try {
|
|
199
|
-
|
|
125
|
+
// Take a snapshot of pending events
|
|
126
|
+
const eventsToSend = this.pendingEvents.slice();
|
|
127
|
+
const serialized = JSON.stringify(eventsToSend);
|
|
200
128
|
|
|
201
129
|
if (!this.isOnline) {
|
|
202
130
|
this.offlineQueue.push({
|
|
203
|
-
|
|
131
|
+
events: eventsToSend,
|
|
132
|
+
afterEventId: this.lastEventId,
|
|
204
133
|
timestamp: Date.now(),
|
|
205
134
|
});
|
|
135
|
+
this.pendingEvents = [];
|
|
206
136
|
this.updateUI("offline-queued");
|
|
207
137
|
return;
|
|
208
138
|
}
|
|
209
139
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
140
|
+
let result;
|
|
141
|
+
|
|
142
|
+
if (serialized.length > EVENT_BATCH_MAX_SIZE) {
|
|
143
|
+
// Large batch — fall back to full snapshot
|
|
144
|
+
result = await this.sendSnapshot();
|
|
145
|
+
} else {
|
|
146
|
+
// Normal path — send events
|
|
147
|
+
result = await this.sendEvents(eventsToSend);
|
|
148
|
+
}
|
|
214
149
|
|
|
215
|
-
|
|
216
|
-
|
|
150
|
+
if (result.conflict) {
|
|
151
|
+
// 409 — stale state, re-fetch
|
|
152
|
+
console.log("⚠ Stale state detected, re-fetching from cloud...");
|
|
153
|
+
await loadFromCloud();
|
|
154
|
+
this.pendingEvents = [];
|
|
155
|
+
window.location.reload();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
this.pendingEvents = [];
|
|
160
|
+
this.lastEventId = result.lastEventId;
|
|
161
|
+
localStorage.setItem(
|
|
162
|
+
LAST_EVENT_ID_KEY,
|
|
163
|
+
String(this.lastEventId),
|
|
164
|
+
);
|
|
217
165
|
this.lastSaveTime = Date.now();
|
|
218
166
|
this.retryCount = 0;
|
|
219
167
|
this.updateUI("saved");
|
|
@@ -228,19 +176,46 @@ window.hyperbook.cloud = (function () {
|
|
|
228
176
|
});
|
|
229
177
|
}
|
|
230
178
|
|
|
231
|
-
async
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
179
|
+
async sendEvents(events, afterEventId) {
|
|
180
|
+
var effectiveAfterId = afterEventId !== undefined ? afterEventId : this.lastEventId;
|
|
181
|
+
try {
|
|
182
|
+
const data = await apiRequest(
|
|
183
|
+
`/api/store/${HYPERBOOK_CLOUD.id}/events`,
|
|
184
|
+
{
|
|
185
|
+
method: "POST",
|
|
186
|
+
body: JSON.stringify({
|
|
187
|
+
events: events,
|
|
188
|
+
afterEventId: effectiveAfterId,
|
|
189
|
+
}),
|
|
190
|
+
},
|
|
191
|
+
);
|
|
192
|
+
return { lastEventId: data.lastEventId, conflict: false };
|
|
193
|
+
} catch (error) {
|
|
194
|
+
if (error.status === 409) {
|
|
195
|
+
return { conflict: true };
|
|
196
|
+
}
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async sendSnapshot() {
|
|
202
|
+
const storeExport = await hyperbook.store.db.export({ prettyJson: false });
|
|
203
|
+
const exportData = JSON.parse(await storeExport.text());
|
|
204
|
+
|
|
205
|
+
const data = await apiRequest(
|
|
206
|
+
`/api/store/${HYPERBOOK_CLOUD.id}/snapshot`,
|
|
207
|
+
{
|
|
208
|
+
method: "POST",
|
|
209
|
+
body: JSON.stringify({
|
|
210
|
+
data: {
|
|
211
|
+
version: 1,
|
|
212
|
+
origin: window.location.origin,
|
|
213
|
+
data: { hyperbook: exportData },
|
|
214
|
+
},
|
|
215
|
+
}),
|
|
242
216
|
},
|
|
243
|
-
|
|
217
|
+
);
|
|
218
|
+
return { lastEventId: data.lastEventId, conflict: false };
|
|
244
219
|
}
|
|
245
220
|
|
|
246
221
|
scheduleRetry() {
|
|
@@ -272,7 +247,6 @@ window.hyperbook.cloud = (function () {
|
|
|
272
247
|
|
|
273
248
|
window.addEventListener("beforeunload", (e) => {
|
|
274
249
|
if (this.isDirty) {
|
|
275
|
-
this.performSave("unload");
|
|
276
250
|
e.preventDefault();
|
|
277
251
|
e.returnValue = "";
|
|
278
252
|
}
|
|
@@ -289,21 +263,38 @@ window.hyperbook.cloud = (function () {
|
|
|
289
263
|
if (this.offlineQueue.length === 0) return;
|
|
290
264
|
|
|
291
265
|
console.log(`Processing ${this.offlineQueue.length} queued saves...`);
|
|
292
|
-
this.offlineQueue.sort((a, b) => a.timestamp - b.timestamp);
|
|
293
266
|
|
|
294
|
-
//
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
267
|
+
// Send queued events in order
|
|
268
|
+
for (let i = 0; i < this.offlineQueue.length; i++) {
|
|
269
|
+
const queued = this.offlineQueue[i];
|
|
270
|
+
try {
|
|
271
|
+
const result = await this.sendEvents(queued.events, queued.afterEventId);
|
|
272
|
+
|
|
273
|
+
if (result.conflict) {
|
|
274
|
+
// Conflict — discard remaining queue, re-fetch
|
|
275
|
+
console.log("⚠ Offline queue conflict, re-fetching...");
|
|
276
|
+
this.offlineQueue = [];
|
|
277
|
+
await loadFromCloud();
|
|
278
|
+
window.location.reload();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
this.lastEventId = result.lastEventId;
|
|
283
|
+
localStorage.setItem(
|
|
284
|
+
LAST_EVENT_ID_KEY,
|
|
285
|
+
String(this.lastEventId),
|
|
286
|
+
);
|
|
287
|
+
} catch (error) {
|
|
288
|
+
console.error("Failed to process offline queue:", error);
|
|
289
|
+
// Keep remaining items in queue
|
|
290
|
+
this.offlineQueue = this.offlineQueue.slice(i);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
306
293
|
}
|
|
294
|
+
|
|
295
|
+
this.offlineQueue = [];
|
|
296
|
+
this.lastSaveTime = Date.now();
|
|
297
|
+
console.log("✓ Offline queue processed");
|
|
307
298
|
}
|
|
308
299
|
|
|
309
300
|
clearTimers() {
|
|
@@ -327,16 +318,31 @@ window.hyperbook.cloud = (function () {
|
|
|
327
318
|
}
|
|
328
319
|
|
|
329
320
|
async manualSave() {
|
|
330
|
-
this.
|
|
321
|
+
if (this.pendingEvents.length === 0) {
|
|
322
|
+
// No pending events — send full snapshot
|
|
323
|
+
this.clearTimers();
|
|
324
|
+
try {
|
|
325
|
+
this.updateUI("saving");
|
|
326
|
+
const result = await this.sendSnapshot();
|
|
327
|
+
this.lastEventId = result.lastEventId;
|
|
328
|
+
localStorage.setItem(
|
|
329
|
+
LAST_EVENT_ID_KEY,
|
|
330
|
+
String(this.lastEventId),
|
|
331
|
+
);
|
|
332
|
+
this.updateUI("saved");
|
|
333
|
+
} catch (error) {
|
|
334
|
+
console.error("Manual save failed:", error);
|
|
335
|
+
this.updateUI("error");
|
|
336
|
+
}
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
331
339
|
this.clearTimers();
|
|
332
340
|
await this.performSave("manual");
|
|
333
341
|
}
|
|
334
342
|
|
|
335
343
|
reset() {
|
|
336
|
-
this.
|
|
337
|
-
this.dirtyStores.clear();
|
|
344
|
+
this.pendingEvents = [];
|
|
338
345
|
this.clearTimers();
|
|
339
|
-
this.stopPolling();
|
|
340
346
|
this.offlineQueue = [];
|
|
341
347
|
this.retryCount = 0;
|
|
342
348
|
}
|
|
@@ -400,6 +406,7 @@ window.hyperbook.cloud = (function () {
|
|
|
400
406
|
function clearAuthToken() {
|
|
401
407
|
localStorage.removeItem(AUTH_TOKEN_KEY);
|
|
402
408
|
localStorage.removeItem(AUTH_USER_KEY);
|
|
409
|
+
localStorage.removeItem(LAST_EVENT_ID_KEY);
|
|
403
410
|
}
|
|
404
411
|
|
|
405
412
|
/**
|
|
@@ -467,12 +474,16 @@ window.hyperbook.cloud = (function () {
|
|
|
467
474
|
const data = await response.json();
|
|
468
475
|
|
|
469
476
|
if (!response.ok) {
|
|
470
|
-
|
|
477
|
+
const err = new Error(data.error || "Request failed");
|
|
478
|
+
err.status = response.status;
|
|
479
|
+
throw err;
|
|
471
480
|
}
|
|
472
481
|
|
|
473
482
|
return data;
|
|
474
483
|
} catch (error) {
|
|
475
|
-
|
|
484
|
+
if (!error.status) {
|
|
485
|
+
console.error("Cloud API error:", error);
|
|
486
|
+
}
|
|
476
487
|
throw error;
|
|
477
488
|
}
|
|
478
489
|
}
|
|
@@ -524,24 +535,26 @@ window.hyperbook.cloud = (function () {
|
|
|
524
535
|
try {
|
|
525
536
|
const data = await apiRequest(`/api/store/${HYPERBOOK_CLOUD.id}`);
|
|
526
537
|
|
|
527
|
-
if (data && data.
|
|
528
|
-
|
|
529
|
-
const
|
|
530
|
-
const { hyperbook, sqlIde, learnJ } = storeData;
|
|
538
|
+
if (data && data.snapshot) {
|
|
539
|
+
const storeData = data.snapshot.data || data.snapshot;
|
|
540
|
+
const { hyperbook } = storeData;
|
|
531
541
|
|
|
532
542
|
if (hyperbook) {
|
|
533
543
|
const blob = new Blob([JSON.stringify(hyperbook)], {
|
|
534
544
|
type: "application/json",
|
|
535
545
|
});
|
|
536
|
-
await store.import(blob, { clearTablesBeforeImport: true });
|
|
546
|
+
await hyperbook.store.db.import(blob, { clearTablesBeforeImport: true });
|
|
537
547
|
}
|
|
538
548
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
549
|
+
// Track the server's lastEventId
|
|
550
|
+
if (data.lastEventId !== undefined) {
|
|
551
|
+
localStorage.setItem(
|
|
552
|
+
LAST_EVENT_ID_KEY,
|
|
553
|
+
String(data.lastEventId),
|
|
554
|
+
);
|
|
555
|
+
if (syncManager) {
|
|
556
|
+
syncManager.lastEventId = data.lastEventId;
|
|
557
|
+
}
|
|
545
558
|
}
|
|
546
559
|
|
|
547
560
|
console.log("✓ Store loaded from cloud");
|
|
@@ -584,20 +597,39 @@ window.hyperbook.cloud = (function () {
|
|
|
584
597
|
debounceDelay: 2000,
|
|
585
598
|
maxWaitTime: 10000,
|
|
586
599
|
minSaveInterval: 1000,
|
|
587
|
-
externalDBs: ["SQL-IDE", "LearnJ"],
|
|
588
|
-
pollInterval: 5000,
|
|
589
600
|
});
|
|
590
601
|
|
|
591
|
-
// Hook Dexie tables to
|
|
592
|
-
store.tables.forEach((table) => {
|
|
602
|
+
// Hook Dexie tables to capture granular events (skip currentState — ephemeral UI data)
|
|
603
|
+
hyperbook.store.tables.forEach((table) => {
|
|
593
604
|
if (table.name === "currentState") return;
|
|
594
|
-
table.hook("creating", () => syncManager.markDirty(table.name));
|
|
595
|
-
table.hook("updating", () => syncManager.markDirty(table.name));
|
|
596
|
-
table.hook("deleting", () => syncManager.markDirty(table.name));
|
|
597
|
-
});
|
|
598
605
|
|
|
599
|
-
|
|
600
|
-
|
|
606
|
+
table.hook("creating", function (primKey, obj) {
|
|
607
|
+
syncManager.addEvent({
|
|
608
|
+
table: table.name,
|
|
609
|
+
op: "create",
|
|
610
|
+
primKey: primKey,
|
|
611
|
+
data: obj,
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
table.hook("updating", function (modifications, primKey) {
|
|
616
|
+
syncManager.addEvent({
|
|
617
|
+
table: table.name,
|
|
618
|
+
op: "update",
|
|
619
|
+
primKey: primKey,
|
|
620
|
+
data: modifications,
|
|
621
|
+
});
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
table.hook("deleting", function (primKey) {
|
|
625
|
+
syncManager.addEvent({
|
|
626
|
+
table: table.name,
|
|
627
|
+
op: "delete",
|
|
628
|
+
primKey: primKey,
|
|
629
|
+
data: null,
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
});
|
|
601
633
|
}
|
|
602
634
|
}
|
|
603
635
|
|
|
@@ -635,29 +667,29 @@ window.hyperbook.cloud = (function () {
|
|
|
635
667
|
statusEl.className = status;
|
|
636
668
|
|
|
637
669
|
if (status === "unsaved") {
|
|
638
|
-
statusEl.textContent = i18n.get("user-unsaved", {}, "Unsaved changes");
|
|
670
|
+
statusEl.textContent = hyperbook.i18n.get("user-unsaved", {}, "Unsaved changes");
|
|
639
671
|
updateUserIconState("logged-in");
|
|
640
672
|
} else if (status === "saving") {
|
|
641
|
-
statusEl.textContent = i18n.get("user-saving", {}, "Saving...");
|
|
673
|
+
statusEl.textContent = hyperbook.i18n.get("user-saving", {}, "Saving...");
|
|
642
674
|
updateUserIconState("syncing");
|
|
643
675
|
} else if (status === "saved") {
|
|
644
|
-
statusEl.textContent = i18n.get("user-saved", {}, "Saved");
|
|
676
|
+
statusEl.textContent = hyperbook.i18n.get("user-saved", {}, "Saved");
|
|
645
677
|
updateUserIconState("synced");
|
|
646
678
|
} else if (status === "error") {
|
|
647
|
-
statusEl.textContent = i18n.get("user-save-error", {}, "Save Error");
|
|
679
|
+
statusEl.textContent = hyperbook.i18n.get("user-save-error", {}, "Save Error");
|
|
648
680
|
updateUserIconState("unsynced");
|
|
649
681
|
} else if (status === "offline") {
|
|
650
|
-
statusEl.textContent = i18n.get("user-offline", {}, "Offline");
|
|
682
|
+
statusEl.textContent = hyperbook.i18n.get("user-offline", {}, "Offline");
|
|
651
683
|
updateUserIconState("unsynced");
|
|
652
684
|
} else if (status === "offline-queued") {
|
|
653
|
-
statusEl.textContent = i18n.get(
|
|
685
|
+
statusEl.textContent = hyperbook.i18n.get(
|
|
654
686
|
"user-offline-queued",
|
|
655
687
|
{},
|
|
656
688
|
"Saved locally",
|
|
657
689
|
);
|
|
658
690
|
updateUserIconState("logged-in");
|
|
659
691
|
} else if (status === "readonly") {
|
|
660
|
-
statusEl.textContent = i18n.get("user-readonly", {}, "Read-Only Mode");
|
|
692
|
+
statusEl.textContent = hyperbook.i18n.get("user-readonly", {}, "Read-Only Mode");
|
|
661
693
|
statusEl.className = "readonly";
|
|
662
694
|
updateUserIconState("synced");
|
|
663
695
|
}
|
|
@@ -686,7 +718,7 @@ window.hyperbook.cloud = (function () {
|
|
|
686
718
|
const errorEl = document.getElementById("user-login-error");
|
|
687
719
|
|
|
688
720
|
if (!username || !password) {
|
|
689
|
-
errorEl.textContent = i18n.get(
|
|
721
|
+
errorEl.textContent = hyperbook.i18n.get(
|
|
690
722
|
"user-login-required",
|
|
691
723
|
{},
|
|
692
724
|
"Username and password required",
|
|
@@ -700,14 +732,14 @@ window.hyperbook.cloud = (function () {
|
|
|
700
732
|
errorEl.textContent = "";
|
|
701
733
|
} catch (error) {
|
|
702
734
|
errorEl.textContent =
|
|
703
|
-
error.message || i18n.get("user-login-failed", {}, "Login failed");
|
|
735
|
+
error.message || hyperbook.i18n.get("user-login-failed", {}, "Login failed");
|
|
704
736
|
}
|
|
705
737
|
};
|
|
706
738
|
|
|
707
739
|
const logout = () => {
|
|
708
740
|
if (
|
|
709
741
|
confirm(
|
|
710
|
-
i18n.get("user-logout-confirm", {}, "Are you sure you want to logout?"),
|
|
742
|
+
hyperbook.i18n.get("user-logout-confirm", {}, "Are you sure you want to logout?"),
|
|
711
743
|
)
|
|
712
744
|
) {
|
|
713
745
|
hyperbookLogout();
|
|
@@ -727,13 +759,21 @@ window.hyperbook.cloud = (function () {
|
|
|
727
759
|
updateUserUI(user);
|
|
728
760
|
}
|
|
729
761
|
|
|
762
|
+
// Hide local export/import/reset when logged into cloud
|
|
763
|
+
if (HYPERBOOK_CLOUD && getAuthToken()) {
|
|
764
|
+
document.querySelectorAll(".export-icon, .import-icon, .reset-icon").forEach((el) => {
|
|
765
|
+
const link = el.closest("a");
|
|
766
|
+
if (link) link.style.display = "none";
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
|
|
730
770
|
// Show impersonation banner if in readonly mode
|
|
731
771
|
if (isReadOnlyMode()) {
|
|
732
772
|
const banner = document.createElement("div");
|
|
733
773
|
banner.id = "impersonation-banner";
|
|
734
774
|
banner.innerHTML = `
|
|
735
|
-
<span>${i18n.get("user-impersonating", {}, "Impersonating")}: <strong>${user ? user.username : ""}</strong> — ${i18n.get("user-readonly", {}, "Read-Only Mode")}</span>
|
|
736
|
-
<a href="#" id="exit-impersonation">${i18n.get("user-exit-impersonation", {}, "Exit Impersonation")}</a>
|
|
775
|
+
<span>${hyperbook.i18n.get("user-impersonating", {}, "Impersonating")}: <strong>${user ? user.username : ""}</strong> — ${hyperbook.i18n.get("user-readonly", {}, "Read-Only Mode")}</span>
|
|
776
|
+
<a href="#" id="exit-impersonation">${hyperbook.i18n.get("user-exit-impersonation", {}, "Exit Impersonation")}</a>
|
|
737
777
|
`;
|
|
738
778
|
document.body.prepend(banner);
|
|
739
779
|
|
|
@@ -752,6 +792,14 @@ window.hyperbook.cloud = (function () {
|
|
|
752
792
|
|
|
753
793
|
return {
|
|
754
794
|
save: () => syncManager?.manualSave(),
|
|
795
|
+
sendSnapshot: async () => {
|
|
796
|
+
if (!syncManager || !HYPERBOOK_CLOUD || !getAuthToken() || isReadOnlyMode()) return;
|
|
797
|
+
syncManager.pendingEvents = [];
|
|
798
|
+
syncManager.clearTimers();
|
|
799
|
+
const result = await syncManager.sendSnapshot();
|
|
800
|
+
syncManager.lastEventId = result.lastEventId;
|
|
801
|
+
localStorage.setItem(LAST_EVENT_ID_KEY, String(result.lastEventId));
|
|
802
|
+
},
|
|
755
803
|
userToggle,
|
|
756
804
|
login,
|
|
757
805
|
logout,
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/// <reference path="../hyperbook.types.js" />
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ABC music notation rendering and editing.
|
|
5
|
+
* @type {HyperbookAbc}
|
|
6
|
+
* @memberof hyperbook
|
|
7
|
+
* @see hyperbook.store
|
|
8
|
+
*/
|
|
1
9
|
hyperbook.abc = (function () {
|
|
2
10
|
window.codeInput?.registerTemplate(
|
|
3
11
|
"abc-highlighted",
|
|
@@ -33,7 +41,7 @@ hyperbook.abc = (function () {
|
|
|
33
41
|
});
|
|
34
42
|
|
|
35
43
|
resetEl?.addEventListener("click", () => {
|
|
36
|
-
store.abcMusic.delete(id);
|
|
44
|
+
hyperbook.store.abcMusic.delete(id);
|
|
37
45
|
window.location.reload();
|
|
38
46
|
});
|
|
39
47
|
|
|
@@ -46,7 +54,7 @@ hyperbook.abc = (function () {
|
|
|
46
54
|
});
|
|
47
55
|
|
|
48
56
|
editorEl.addEventListener("code-input_load", async () => {
|
|
49
|
-
const storeResult = await store.abcMusic
|
|
57
|
+
const storeResult = await hyperbook.store.abcMusic
|
|
50
58
|
.where("id")
|
|
51
59
|
.equals(editorEl.id)
|
|
52
60
|
.first();
|
|
@@ -70,7 +78,7 @@ hyperbook.abc = (function () {
|
|
|
70
78
|
});
|
|
71
79
|
|
|
72
80
|
editorEl.addEventListener("change", () => {
|
|
73
|
-
store.abcMusic.put({
|
|
81
|
+
hyperbook.store.abcMusic.put({
|
|
74
82
|
id: editorEl.id,
|
|
75
83
|
tune: editorEl.value,
|
|
76
84
|
});
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
hyperbook.
|
|
1
|
+
/// <reference path="../hyperbook.types.js" />
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Archive download management.
|
|
5
|
+
* @type {HyperbookArchive}
|
|
6
|
+
* @memberof hyperbook
|
|
7
|
+
* @see hyperbook.i18n
|
|
8
|
+
*/
|
|
9
|
+
hyperbook.archive = (function () {
|
|
2
10
|
function initElement(el) {
|
|
3
11
|
const labelEl = el.getElementsByClassName("label")[0];
|
|
4
12
|
const src = el.href;
|
|
@@ -7,10 +15,10 @@ hyperbook.download = (function () {
|
|
|
7
15
|
const isOnline = r.ok;
|
|
8
16
|
if (isOnline) {
|
|
9
17
|
labelEl.classList.remove("offline");
|
|
10
|
-
labelEl.innerHTML = labelEl.innerHTML.replace(`(${i18n.get("archive-offline")})`, "");
|
|
18
|
+
labelEl.innerHTML = labelEl.innerHTML.replace(`(${hyperbook.i18n.get("archive-offline")})`, "");
|
|
11
19
|
} else {
|
|
12
20
|
labelEl.classList.add("offline");
|
|
13
|
-
labelEl.innerHTML += ` (${i18n.get("archive-offline")})`;
|
|
21
|
+
labelEl.innerHTML += ` (${hyperbook.i18n.get("archive-offline")})`;
|
|
14
22
|
}
|
|
15
23
|
});
|
|
16
24
|
}
|
|
@@ -36,5 +44,10 @@ hyperbook.download = (function () {
|
|
|
36
44
|
|
|
37
45
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
38
46
|
|
|
39
|
-
|
|
47
|
+
// Initialize existing elements on document load
|
|
48
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
49
|
+
init();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return { init };
|
|
40
53
|
})();
|