Jaimodiji commited on
Commit
aeafb00
·
verified ·
1 Parent(s): 596bc67

Upload folder using huggingface_hub

Browse files
Files changed (2) hide show
  1. Qwen.md +56 -0
  2. public/scripts/modules/ColorRmSession.js +144 -142
Qwen.md ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # My Multiplayer App - Development Guidelines
2
+
3
+ ## Overview
4
+ This is a collaborative drawing application built on top of ColorRM technology, featuring real-time multiplayer capabilities, advanced drawing tools, and document management features.
5
+
6
+ ## Key Features
7
+ - Real-time collaborative drawing with Liveblocks integration
8
+ - Advanced drawing tools (pen, eraser, shapes, text, lasso)
9
+ - Document management (PDF import, page management)
10
+ - Template support (graph paper, lined paper, etc.)
11
+ - High-quality rendering with 2K default page sizes
12
+ - Touch-friendly interface with responsive design
13
+
14
+ ## Development Guidelines
15
+
16
+ ### Code Modification Best Practices
17
+ 1. **Use small, specific replacements**: When modifying code, use the smallest possible string fragments to minimize unintended changes
18
+ 2. **Always verify context**: Include sufficient context in `old_string` to ensure unique matching
19
+ 3. **Test syntax**: After making changes, verify that the code remains syntactically correct
20
+ 4. **Check for duplicates**: Avoid creating duplicate function definitions
21
+
22
+ ### File Structure
23
+ - `public/scripts/` - Main application scripts
24
+ - `public/scripts/modules/` - Modular components (ColorRmSession, ColorRmInput, etc.)
25
+ - `public/color_rm.html` - Main application UI
26
+
27
+ ### Common Issues to Avoid
28
+ - Missing closing braces for functions
29
+ - Duplicate function definitions
30
+ - Improper function separation
31
+ - Syntax errors in JavaScript modules
32
+
33
+ ### Working with the ColorRmSession Module
34
+ This module handles:
35
+ - Page management (creation, deletion, resizing)
36
+ - Document import/export
37
+ - Collaboration features
38
+ - State management
39
+
40
+ When modifying this module:
41
+ - Ensure all functions are properly closed with braces
42
+ - Maintain consistent indentation
43
+ - Preserve existing functionality while adding new features
44
+ - Test thoroughly after changes
45
+
46
+ ### Quality Standards
47
+ - Default to 2000x1500 page sizes for high quality output
48
+ - Use 0.95 JPEG quality for better visual fidelity
49
+ - Maintain responsive touch interactions
50
+ - Ensure proper error handling and user feedback
51
+
52
+ ### Testing Protocol
53
+ After making changes:
54
+ 1. Verify functionality in browser
55
+ 2. Test collaborative features if applicable
56
+ 3. Check mobile/touch interactions
public/scripts/modules/ColorRmSession.js CHANGED
@@ -9,7 +9,7 @@ export const ColorRmSession = {
9
  await this.importBaseFile(blob);
10
  console.log("Liveblocks: Base file fetch successful.");
11
  }
12
- } catch(e) {
13
  console.error("Liveblocks: Base file fetch failed:", e);
14
  } finally {
15
  this.isFetchingBase = false;
@@ -21,7 +21,7 @@ export const ColorRmSession = {
21
  this.ui.showInput("New Folder", "Folder Name", async (name) => {
22
  if (!name) return;
23
  const folder = {
24
- id: `folder_${Date.now()}_${Math.random().toString(36).substring(2,5)}`,
25
  name: name,
26
  parentId: this.state.currentFolderId || null,
27
  ownerId: this.state.ownerId || this.liveSync?.userId || 'local'
@@ -66,10 +66,10 @@ export const ColorRmSession = {
66
 
67
  navigateUp() {
68
  // Simple 1-level for now, or fetch parent from DB if nested
69
- this.state.currentFolderId = null;
70
  this.loadSessionList();
71
  },
72
-
73
  async moveSelectedToFolder(folderId) {
74
  if (!this.state.selectedSessions || this.state.selectedSessions.size === 0) return;
75
 
@@ -111,11 +111,13 @@ export const ColorRmSession = {
111
  const tx = this.db.transaction(['sessions', 'folders'], 'readonly');
112
  const sessionsReq = tx.objectStore('sessions').getAll();
113
  const foldersReq = tx.objectStore('folders').getAll();
114
-
115
  // Wait for both
116
  await new Promise(resolve => {
117
  let completed = 0;
118
- const check = () => { if (++completed === 2) resolve(); };
 
 
119
  sessionsReq.onsuccess = check;
120
  foldersReq.onsuccess = check;
121
  });
@@ -123,7 +125,7 @@ export const ColorRmSession = {
123
  const l = this.getElement('sessionList');
124
  if (!l) return;
125
  l.innerHTML = '';
126
-
127
  // Render Navigation Header
128
  if (this.state.currentFolderId) {
129
  const backBtn = document.createElement('div');
@@ -138,32 +140,32 @@ export const ColorRmSession = {
138
  // Only show folders in current path
139
  return (f.parentId || null) === (this.state.currentFolderId || null);
140
  });
141
-
142
  folders.forEach(f => {
143
- const item = document.createElement('div');
144
- item.className = 'session-item folder-item';
145
- item.innerHTML = `
146
  <div style="display:flex; gap:10px; align-items:center;">
147
  <i class="bi bi-folder-fill" style="color:#fbbf24; font-size:1.2rem;"></i>
148
  <span style="font-weight:600; color:white;">${f.name}</span>
149
  </div>
150
  <button class="btn btn-sm" style="background:none; border:none; color:#666;"><i class="bi bi-trash"></i></button>
151
  `;
152
-
153
- // Click to open
154
- item.onclick = (e) => {
155
- if (e.target.closest('button')) {
156
- this.deleteFolder(f.id);
157
- } else {
158
- this.openFolder(f.id);
159
- }
160
- };
161
- l.appendChild(item);
162
  });
163
 
164
 
165
  const sessions = sessionsReq.result || [];
166
- if(sessions.length === 0 && folders.length === 0) {
167
  l.innerHTML += '<div style="color:#666;text-align:center;padding:10px">No projects found.</div>';
168
  const editBtn = this.getElement('dashEditBtn');
169
  if (editBtn) editBtn.style.display = 'none';
@@ -177,8 +179,8 @@ export const ColorRmSession = {
177
 
178
  // Filter Sessions by Folder
179
  sessions.filter(s => (s.folderId || null) === (this.state.currentFolderId || null))
180
- .sort((a,b) => b.lastMod - a.lastMod).forEach(s => {
181
-
182
  const isMine = s.ownerId === userId;
183
  const badge = isMine ? '<span class="owner-badge">Owner</span>' : `<span class="other-badge">Shared</span>`;
184
  const cloudIcon = s.isCloudBackedUp ? '<i class="bi bi-cloud-check-fill" style="color:var(--success); margin-left:6px;" title="Backed up to Cloud"></i>' : '';
@@ -210,7 +212,7 @@ export const ColorRmSession = {
210
 
211
  l.appendChild(item);
212
  });
213
- this.updateMultiSelectUI();
214
  } catch (e) {
215
  console.error("Dashboard render error:", e);
216
  }
@@ -254,13 +256,13 @@ export const ColorRmSession = {
254
 
255
  // Update Checkboxes and classes
256
  const list = this.getElement('sessionList');
257
- if(!list) return;
258
  const items = list.querySelectorAll('.session-item');
259
  items.forEach(el => {
260
  const idStr = el.id.replace('sess_', '');
261
  // Support both string and numeric IDs in the set
262
- const isSelected = this.state.selectedSessions.has(idStr) ||
263
- (!isNaN(Number(idStr)) && this.state.selectedSessions.has(Number(idStr)));
264
 
265
  el.classList.toggle('selected', isSelected);
266
  const cb = el.querySelector('.session-checkbox');
@@ -341,7 +343,7 @@ export const ColorRmSession = {
341
  return new Promise(async (resolve, reject) => {
342
  const q = this.db.transaction('pages').objectStore('pages').index('sessionId').getAll(id);
343
  q.onsuccess = async () => {
344
- this.state.images = q.result.sort((a,b)=>a.pageIndex-b.pageIndex);
345
 
346
  // Retroactively assign IDs to legacy items
347
  this.state.images.forEach(img => {
@@ -412,21 +414,21 @@ export const ColorRmSession = {
412
  }
413
 
414
  // Sort pages by index again after adding any missing pages
415
- this.state.images.sort((a,b) => a.pageIndex - b.pageIndex);
416
 
417
  console.log(`Loaded ${this.state.images.length} pages from DB.`);
418
  const pageTotal = this.getElement('pageTotal');
419
  if (pageTotal) pageTotal.innerText = '/ ' + this.state.images.length;
420
 
421
- if(this.state.images.length === 0) {
422
  // Try to fetch base from server if pages are missing
423
  this.retryBaseFetch();
424
  }
425
 
426
- if(this.state.activeSideTab === 'pages') this.renderPageSidebar();
427
 
428
  // Only load page 0 if we don't have a current page loaded or if the current page is out of bounds
429
- if(this.state.images.length > 0) {
430
  if (!this.cache.currentImg || this.state.idx >= this.state.images.length) {
431
  // Adjust current page index if it's out of bounds
432
  if (this.state.idx >= this.state.images.length) {
@@ -443,8 +445,14 @@ export const ColorRmSession = {
443
 
444
  async importBaseFile(blob) {
445
  // Simulates a file input event to reuse existing handleImport logic
446
- const file = new File([blob], "base_document_blob", { type: blob.type });
447
- await this.handleImport({ target: { files: [file] } }, true); // Pass true to skip upload
 
 
 
 
 
 
448
  },
449
 
450
  async computeFileHash(file) {
@@ -462,7 +470,7 @@ export const ColorRmSession = {
462
 
463
  async handleImport(e, skipUpload = false, lazy = false) {
464
  const files = e.target.files;
465
- if(!files || !files.length) return;
466
 
467
  // Deduplication Check
468
  let fileHash = null;
@@ -488,7 +496,9 @@ export const ColorRmSession = {
488
  }
489
  }
490
  }
491
- } catch (err) { console.error("Hash check error:", err); }
 
 
492
  }
493
 
494
  this.isUploading = true; // Set flag
@@ -505,18 +515,18 @@ export const ColorRmSession = {
505
  pName = files[0].name.replace(/\.[^/.]+$/, "");
506
  console.log(`[Import] Derived pName from file: "${pName}"`);
507
  if (pName.includes("base_document_blob")) {
508
- console.log(`[Import] Detected base blob. Current project name: "${this.state.projectName}"`);
509
- // Only overwrite if we don't have a valid project name already
510
- if (this.state.projectName && this.state.projectName !== "Untitled" && this.state.projectName !== "Untitled Project") {
511
- pName = this.state.projectName;
512
- console.log(`[Import] Preserving existing name: "${pName}"`);
513
- } else {
514
- pName = "Untitled Project";
515
- console.log(`[Import] Fallback to Untitled Project (no valid existing name)`);
516
- }
517
  }
518
  }
519
- if(!pName || pName === "Untitled") {
520
  pName = "Untitled Project";
521
  console.log(`[Import] Final fallback to Untitled Project`);
522
  }
@@ -574,7 +584,7 @@ export const ColorRmSession = {
574
  if (!this.state.ownerId) this.state.ownerId = this.liveSync?.userId || 'local';
575
 
576
  const session = await this.dbGet('sessions', this.state.sessionId);
577
- if(session) {
578
  session.name = pName;
579
  session.baseFileName = this.state.baseFileName;
580
  session.ownerId = this.state.ownerId;
@@ -592,7 +602,7 @@ export const ColorRmSession = {
592
  baseFileName: this.state.baseFileName,
593
  pageCount: 0,
594
  lastMod: Date.now(),
595
- idx:0,
596
  bookmarks: [],
597
  clipboardBox: [],
598
  ownerId: this.state.ownerId,
@@ -609,9 +619,11 @@ export const ColorRmSession = {
609
  const d = await files[0].arrayBuffer();
610
  const pdf = await pdfjsLib.getDocument(d).promise;
611
  pageCount = pdf.numPages;
612
- } catch(e) { console.error("PDF metadata failed", e); }
 
 
613
  }
614
-
615
  const session = await this.dbGet('sessions', this.state.sessionId);
616
  if (session) {
617
  session.pageCount = pageCount;
@@ -634,7 +646,7 @@ export const ColorRmSession = {
634
  // Wrap processing in a promise to await completion
635
  await new Promise((resolve) => {
636
  const processNext = async () => {
637
- if(processQueue.length === 0) {
638
  // Update storage with final page count
639
  const session = await this.dbGet('sessions', this.state.sessionId);
640
  if (session) {
@@ -662,49 +674,71 @@ export const ColorRmSession = {
662
  }
663
 
664
  const f = processQueue.shift();
665
- if(f.type.includes('pdf')) {
666
- try {
667
- const d = await f.arrayBuffer();
668
- const pdf = await pdfjsLib.getDocument(d).promise;
669
- for(let i=1; i<=pdf.numPages; i+=BATCH_SIZE) {
670
- const batch = [];
671
- for(let j=0; j<BATCH_SIZE && (i+j)<=pdf.numPages; j++) {
672
- const pNum = i+j;
673
- batch.push(pdf.getPage(pNum).then(async page => {
674
- const v = page.getViewport({scale:1.5}); // Increased scale for higher quality
675
- const cvs = document.createElement('canvas'); cvs.width=v.width; cvs.height=v.height;
676
- await page.render({canvasContext:cvs.getContext('2d'), viewport:v}).promise;
677
- const b = await new Promise(r=>cvs.toBlob(r, 'image/jpeg', 0.9)); // Higher quality JPEG
678
- const pageObj = { id:`${this.state.sessionId}_${idx+j}`, sessionId:this.state.sessionId, pageIndex:idx+j, blob:b, history:[] };
679
- await this.dbPut('pages', pageObj);
680
- return pageObj;
681
- }));
682
- }
683
- const results = await Promise.all(batch);
684
-
685
- // INCREMENTAL UPDATE: Add results to state and update UI
686
- this.state.images.push(...results);
687
- this.state.images.sort((a,b) => a.pageIndex - b.pageIndex);
688
-
689
- if (this.state.images.length > 0 && !this.cache.currentImg) {
690
- await this.loadPage(0, false); // Load first page as soon as it's ready
691
- }
692
-
693
- if(this.state.activeSideTab === 'pages') this.renderPageSidebar();
694
- const pt = this.getElement('pageTotal');
695
- if (pt) pt.innerText = '/ ' + this.state.images.length;
696
-
697
- idx += results.length;
698
- this.ui.updateProgress(((i/pdf.numPages)*100), `Processing Page ${i}/${pdf.numPages}`);
699
- await new Promise(r => setTimeout(r, 0));
700
- }
701
- } catch(e) { console.error(e); this.ui.showToast("Failed to load PDF"); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
702
  } else {
703
- const pageObj = { id:`${this.state.sessionId}_${idx}`, sessionId:this.state.sessionId, pageIndex:idx, blob:f, history:[] };
 
 
 
 
 
 
704
  await this.dbPut('pages', pageObj);
705
  this.state.images.push(pageObj);
706
  if (this.state.images.length === 1) await this.loadPage(0, false);
707
- if(this.state.activeSideTab === 'pages') this.renderPageSidebar();
708
  idx++;
709
  }
710
  processNext();
@@ -742,15 +776,15 @@ export const ColorRmSession = {
742
  this.state.clipboardBox = [];
743
  const pt = this.getElement('pageTotal');
744
  if (pt) pt.innerText = '/ 0';
745
- if(this.state.activeSideTab === 'pages') this.renderPageSidebar();
746
 
747
  const c = this.getElement('canvas');
748
- if(c) c.getContext('2d').clearRect(0,0,c.width,c.height);
749
  // ----------------------------
750
 
751
  this.ui.setSyncStatus('new');
752
 
753
- if(openPicker) {
754
  const fileIn = this.getElement('fileIn');
755
  if (fileIn) fileIn.click();
756
  }
@@ -768,10 +802,12 @@ export const ColorRmSession = {
768
  await fetch(window.Config?.apiUrl(`/api/color_rm/upload/${this.state.sessionId}`) || `/api/color_rm/upload/${this.state.sessionId}`, {
769
  method: 'POST',
770
  body: this.state.images[0].blob,
771
- headers: { 'Content-Type': this.state.images[0].blob.type }
 
 
772
  });
773
  this.ui.showToast("Base file restored!");
774
- } catch(e) {
775
  this.ui.showToast("Restore failed");
776
  }
777
  } else {
@@ -864,7 +900,7 @@ export const ColorRmSession = {
864
  await this.dbPut('pages', pageObj);
865
 
866
  // Update UI
867
- if(this.state.activeSideTab === 'pages') this.renderPageSidebar();
868
  const pt = this.getElement('pageTotal');
869
  if (pt) pt.innerText = '/ ' + this.state.images.length;
870
 
@@ -981,7 +1017,7 @@ export const ColorRmSession = {
981
  await this.dbPut('pages', pageObj);
982
 
983
  // Update UI
984
- if(this.state.activeSideTab === 'pages') this.renderPageSidebar();
985
  const pt = this.getElement('pageTotal');
986
  if (pt) pt.innerText = '/ ' + this.state.images.length;
987
 
@@ -1256,7 +1292,7 @@ export const ColorRmSession = {
1256
  if (item.shapeType === 'rectangle') {
1257
  ctx.rect(x, y, w, h);
1258
  } else if (item.shapeType === 'circle') {
1259
- ctx.ellipse(x + w/2, y + h/2, Math.abs(w/2), Math.abs(h/2), 0, 0, 2 * Math.PI);
1260
  } else if (item.shapeType === 'line') {
1261
  ctx.moveTo(x, y);
1262
  ctx.lineTo(x + w, y + h);
@@ -1267,13 +1303,13 @@ export const ColorRmSession = {
1267
  ctx.moveTo(x, y);
1268
  ctx.lineTo(x + w, y + h);
1269
  ctx.lineTo(
1270
- x + w - headLength * Math.cos(angle - Math.PI/6),
1271
- y + h - headLength * Math.sin(angle - Math.PI/6)
1272
  );
1273
  ctx.moveTo(x + w, y + h);
1274
  ctx.lineTo(
1275
- x + w - headLength * Math.cos(angle + Math.PI/6),
1276
- y + h - headLength * Math.sin(angle + Math.PI/6)
1277
  );
1278
  }
1279
 
@@ -1364,40 +1400,6 @@ export const ColorRmSession = {
1364
  await this.loadPage(this.state.idx, false);
1365
 
1366
  this.ui.showToast(`All ${this.state.images.length} pages resized to ${width}×${height}`);
1367
- }
1368
-
1369
- reorderPages(fromIndex, toIndex) {
1370
- if (fromIndex < 0 || toIndex < 0 ||
1371
- fromIndex >= this.state.images.length ||
1372
- toIndex >= this.state.images.length) {
1373
- return;
1374
- }
1375
-
1376
- // Reorder in state
1377
- const page = this.state.images.splice(fromIndex, 1)[0];
1378
- this.state.images.splice(toIndex, 0, page);
1379
-
1380
- // Update page indices
1381
- this.state.images.forEach((img, idx) => {
1382
- img.pageIndex = idx;
1383
- // Update the ID to reflect new index
1384
- img.id = `${this.state.sessionId}_${idx}`;
1385
- });
1386
-
1387
- // Update database
1388
- const promises = this.state.images.map(img => this.dbPut('pages', img));
1389
- Promise.all(promises).then(() => {
1390
- this.ui.showToast("Pages reordered");
1391
- if(this.state.activeSideTab === 'pages') this.renderPageSidebar();
1392
-
1393
- // Synchronize with Liveblocks
1394
- if (this.liveSync) {
1395
- // Update the page count in metadata
1396
- this.liveSync.updatePageCount(this.state.images.length);
1397
- // Update presence to notify other users about the page structure change
1398
- this.liveSync.notifyPageStructureChange();
1399
- }
1400
- });
1401
  },
1402
 
1403
  async addTemplatePage(templateType) {
@@ -1490,7 +1492,7 @@ export const ColorRmSession = {
1490
  await this.dbPut('pages', pageObj);
1491
 
1492
  // Update UI
1493
- if(this.state.activeSideTab === 'pages') this.renderPageSidebar();
1494
  const pt = this.getElement('pageTotal');
1495
  if (pt) pt.innerText = '/ ' + this.state.images.length;
1496
 
@@ -1540,7 +1542,7 @@ export const ColorRmSession = {
1540
  }
1541
 
1542
  this.ui.showToast(`Added ${templateType} template page ${newPageIndex + 1}`);
1543
- }
1544
 
1545
  reorderPages(fromIndex, toIndex) {
1546
  if (fromIndex < 0 || toIndex < 0 ||
@@ -1564,7 +1566,7 @@ export const ColorRmSession = {
1564
  const promises = this.state.images.map(img => this.dbPut('pages', img));
1565
  Promise.all(promises).then(() => {
1566
  this.ui.showToast("Pages reordered");
1567
- if(this.state.activeSideTab === 'pages') this.renderPageSidebar();
1568
 
1569
  // Synchronize with Liveblocks
1570
  if (this.liveSync) {
@@ -1574,5 +1576,5 @@ export const ColorRmSession = {
1574
  this.liveSync.notifyPageStructureChange();
1575
  }
1576
  });
1577
- },
1578
- };
 
9
  await this.importBaseFile(blob);
10
  console.log("Liveblocks: Base file fetch successful.");
11
  }
12
+ } catch (e) {
13
  console.error("Liveblocks: Base file fetch failed:", e);
14
  } finally {
15
  this.isFetchingBase = false;
 
21
  this.ui.showInput("New Folder", "Folder Name", async (name) => {
22
  if (!name) return;
23
  const folder = {
24
+ id: `folder_${Date.now()}_${Math.random().toString(36).substring(2, 5)}`,
25
  name: name,
26
  parentId: this.state.currentFolderId || null,
27
  ownerId: this.state.ownerId || this.liveSync?.userId || 'local'
 
66
 
67
  navigateUp() {
68
  // Simple 1-level for now, or fetch parent from DB if nested
69
+ this.state.currentFolderId = null;
70
  this.loadSessionList();
71
  },
72
+
73
  async moveSelectedToFolder(folderId) {
74
  if (!this.state.selectedSessions || this.state.selectedSessions.size === 0) return;
75
 
 
111
  const tx = this.db.transaction(['sessions', 'folders'], 'readonly');
112
  const sessionsReq = tx.objectStore('sessions').getAll();
113
  const foldersReq = tx.objectStore('folders').getAll();
114
+
115
  // Wait for both
116
  await new Promise(resolve => {
117
  let completed = 0;
118
+ const check = () => {
119
+ if (++completed === 2) resolve();
120
+ };
121
  sessionsReq.onsuccess = check;
122
  foldersReq.onsuccess = check;
123
  });
 
125
  const l = this.getElement('sessionList');
126
  if (!l) return;
127
  l.innerHTML = '';
128
+
129
  // Render Navigation Header
130
  if (this.state.currentFolderId) {
131
  const backBtn = document.createElement('div');
 
140
  // Only show folders in current path
141
  return (f.parentId || null) === (this.state.currentFolderId || null);
142
  });
143
+
144
  folders.forEach(f => {
145
+ const item = document.createElement('div');
146
+ item.className = 'session-item folder-item';
147
+ item.innerHTML = `
148
  <div style="display:flex; gap:10px; align-items:center;">
149
  <i class="bi bi-folder-fill" style="color:#fbbf24; font-size:1.2rem;"></i>
150
  <span style="font-weight:600; color:white;">${f.name}</span>
151
  </div>
152
  <button class="btn btn-sm" style="background:none; border:none; color:#666;"><i class="bi bi-trash"></i></button>
153
  `;
154
+
155
+ // Click to open
156
+ item.onclick = (e) => {
157
+ if (e.target.closest('button')) {
158
+ this.deleteFolder(f.id);
159
+ } else {
160
+ this.openFolder(f.id);
161
+ }
162
+ };
163
+ l.appendChild(item);
164
  });
165
 
166
 
167
  const sessions = sessionsReq.result || [];
168
+ if (sessions.length === 0 && folders.length === 0) {
169
  l.innerHTML += '<div style="color:#666;text-align:center;padding:10px">No projects found.</div>';
170
  const editBtn = this.getElement('dashEditBtn');
171
  if (editBtn) editBtn.style.display = 'none';
 
179
 
180
  // Filter Sessions by Folder
181
  sessions.filter(s => (s.folderId || null) === (this.state.currentFolderId || null))
182
+ .sort((a, b) => b.lastMod - a.lastMod).forEach(s => {
183
+
184
  const isMine = s.ownerId === userId;
185
  const badge = isMine ? '<span class="owner-badge">Owner</span>' : `<span class="other-badge">Shared</span>`;
186
  const cloudIcon = s.isCloudBackedUp ? '<i class="bi bi-cloud-check-fill" style="color:var(--success); margin-left:6px;" title="Backed up to Cloud"></i>' : '';
 
212
 
213
  l.appendChild(item);
214
  });
215
+ this.updateMultiSelectUI();
216
  } catch (e) {
217
  console.error("Dashboard render error:", e);
218
  }
 
256
 
257
  // Update Checkboxes and classes
258
  const list = this.getElement('sessionList');
259
+ if (!list) return;
260
  const items = list.querySelectorAll('.session-item');
261
  items.forEach(el => {
262
  const idStr = el.id.replace('sess_', '');
263
  // Support both string and numeric IDs in the set
264
+ const isSelected = this.state.selectedSessions.has(idStr) ||
265
+ (!isNaN(Number(idStr)) && this.state.selectedSessions.has(Number(idStr)));
266
 
267
  el.classList.toggle('selected', isSelected);
268
  const cb = el.querySelector('.session-checkbox');
 
343
  return new Promise(async (resolve, reject) => {
344
  const q = this.db.transaction('pages').objectStore('pages').index('sessionId').getAll(id);
345
  q.onsuccess = async () => {
346
+ this.state.images = q.result.sort((a, b) => a.pageIndex - b.pageIndex);
347
 
348
  // Retroactively assign IDs to legacy items
349
  this.state.images.forEach(img => {
 
414
  }
415
 
416
  // Sort pages by index again after adding any missing pages
417
+ this.state.images.sort((a, b) => a.pageIndex - b.pageIndex);
418
 
419
  console.log(`Loaded ${this.state.images.length} pages from DB.`);
420
  const pageTotal = this.getElement('pageTotal');
421
  if (pageTotal) pageTotal.innerText = '/ ' + this.state.images.length;
422
 
423
+ if (this.state.images.length === 0) {
424
  // Try to fetch base from server if pages are missing
425
  this.retryBaseFetch();
426
  }
427
 
428
+ if (this.state.activeSideTab === 'pages') this.renderPageSidebar();
429
 
430
  // Only load page 0 if we don't have a current page loaded or if the current page is out of bounds
431
+ if (this.state.images.length > 0) {
432
  if (!this.cache.currentImg || this.state.idx >= this.state.images.length) {
433
  // Adjust current page index if it's out of bounds
434
  if (this.state.idx >= this.state.images.length) {
 
445
 
446
  async importBaseFile(blob) {
447
  // Simulates a file input event to reuse existing handleImport logic
448
+ const file = new File([blob], "base_document_blob", {
449
+ type: blob.type
450
+ });
451
+ await this.handleImport({
452
+ target: {
453
+ files: [file]
454
+ }
455
+ }, true); // Pass true to skip upload
456
  },
457
 
458
  async computeFileHash(file) {
 
470
 
471
  async handleImport(e, skipUpload = false, lazy = false) {
472
  const files = e.target.files;
473
+ if (!files || !files.length) return;
474
 
475
  // Deduplication Check
476
  let fileHash = null;
 
496
  }
497
  }
498
  }
499
+ } catch (err) {
500
+ console.error("Hash check error:", err);
501
+ }
502
  }
503
 
504
  this.isUploading = true; // Set flag
 
515
  pName = files[0].name.replace(/\.[^/.]+$/, "");
516
  console.log(`[Import] Derived pName from file: "${pName}"`);
517
  if (pName.includes("base_document_blob")) {
518
+ console.log(`[Import] Detected base blob. Current project name: "${this.state.projectName}"`);
519
+ // Only overwrite if we don't have a valid project name already
520
+ if (this.state.projectName && this.state.projectName !== "Untitled" && this.state.projectName !== "Untitled Project") {
521
+ pName = this.state.projectName;
522
+ console.log(`[Import] Preserving existing name: "${pName}"`);
523
+ } else {
524
+ pName = "Untitled Project";
525
+ console.log(`[Import] Fallback to Untitled Project (no valid existing name)`);
526
+ }
527
  }
528
  }
529
+ if (!pName || pName === "Untitled") {
530
  pName = "Untitled Project";
531
  console.log(`[Import] Final fallback to Untitled Project`);
532
  }
 
584
  if (!this.state.ownerId) this.state.ownerId = this.liveSync?.userId || 'local';
585
 
586
  const session = await this.dbGet('sessions', this.state.sessionId);
587
+ if (session) {
588
  session.name = pName;
589
  session.baseFileName = this.state.baseFileName;
590
  session.ownerId = this.state.ownerId;
 
602
  baseFileName: this.state.baseFileName,
603
  pageCount: 0,
604
  lastMod: Date.now(),
605
+ idx: 0,
606
  bookmarks: [],
607
  clipboardBox: [],
608
  ownerId: this.state.ownerId,
 
619
  const d = await files[0].arrayBuffer();
620
  const pdf = await pdfjsLib.getDocument(d).promise;
621
  pageCount = pdf.numPages;
622
+ } catch (e) {
623
+ console.error("PDF metadata failed", e);
624
+ }
625
  }
626
+
627
  const session = await this.dbGet('sessions', this.state.sessionId);
628
  if (session) {
629
  session.pageCount = pageCount;
 
646
  // Wrap processing in a promise to await completion
647
  await new Promise((resolve) => {
648
  const processNext = async () => {
649
+ if (processQueue.length === 0) {
650
  // Update storage with final page count
651
  const session = await this.dbGet('sessions', this.state.sessionId);
652
  if (session) {
 
674
  }
675
 
676
  const f = processQueue.shift();
677
+ if (f.type.includes('pdf')) {
678
+ try {
679
+ const d = await f.arrayBuffer();
680
+ const pdf = await pdfjsLib.getDocument(d).promise;
681
+ for (let i = 1; i <= pdf.numPages; i += BATCH_SIZE) {
682
+ const batch = [];
683
+ for (let j = 0; j < BATCH_SIZE && (i + j) <= pdf.numPages; j++) {
684
+ const pNum = i + j;
685
+ batch.push(pdf.getPage(pNum).then(async page => {
686
+ const v = page.getViewport({
687
+ scale: 1.5
688
+ }); // Increased scale for higher quality
689
+ const cvs = document.createElement('canvas');
690
+ cvs.width = v.width;
691
+ cvs.height = v.height;
692
+ await page.render({
693
+ canvasContext: cvs.getContext('2d'),
694
+ viewport: v
695
+ }).promise;
696
+ const b = await new Promise(r => cvs.toBlob(r, 'image/jpeg', 0.9)); // Higher quality JPEG
697
+ const pageObj = {
698
+ id: `${this.state.sessionId}_${idx+j}`,
699
+ sessionId: this.state.sessionId,
700
+ pageIndex: idx + j,
701
+ blob: b,
702
+ history: []
703
+ };
704
+ await this.dbPut('pages', pageObj);
705
+ return pageObj;
706
+ }));
707
+ }
708
+ const results = await Promise.all(batch);
709
+
710
+ // INCREMENTAL UPDATE: Add results to state and update UI
711
+ this.state.images.push(...results);
712
+ this.state.images.sort((a, b) => a.pageIndex - b.pageIndex);
713
+
714
+ if (this.state.images.length > 0 && !this.cache.currentImg) {
715
+ await this.loadPage(0, false); // Load first page as soon as it's ready
716
+ }
717
+
718
+ if (this.state.activeSideTab === 'pages') this.renderPageSidebar();
719
+ const pt = this.getElement('pageTotal');
720
+ if (pt) pt.innerText = '/ ' + this.state.images.length;
721
+
722
+ idx += results.length;
723
+ this.ui.updateProgress(((i / pdf.numPages) * 100), `Processing Page ${i}/${pdf.numPages}`);
724
+ await new Promise(r => setTimeout(r, 0));
725
+ }
726
+ } catch (e) {
727
+ console.error(e);
728
+ this.ui.showToast("Failed to load PDF");
729
+ }
730
  } else {
731
+ const pageObj = {
732
+ id: `${this.state.sessionId}_${idx}`,
733
+ sessionId: this.state.sessionId,
734
+ pageIndex: idx,
735
+ blob: f,
736
+ history: []
737
+ };
738
  await this.dbPut('pages', pageObj);
739
  this.state.images.push(pageObj);
740
  if (this.state.images.length === 1) await this.loadPage(0, false);
741
+ if (this.state.activeSideTab === 'pages') this.renderPageSidebar();
742
  idx++;
743
  }
744
  processNext();
 
776
  this.state.clipboardBox = [];
777
  const pt = this.getElement('pageTotal');
778
  if (pt) pt.innerText = '/ 0';
779
+ if (this.state.activeSideTab === 'pages') this.renderPageSidebar();
780
 
781
  const c = this.getElement('canvas');
782
+ if (c) c.getContext('2d').clearRect(0, 0, c.width, c.height);
783
  // ----------------------------
784
 
785
  this.ui.setSyncStatus('new');
786
 
787
+ if (openPicker) {
788
  const fileIn = this.getElement('fileIn');
789
  if (fileIn) fileIn.click();
790
  }
 
802
  await fetch(window.Config?.apiUrl(`/api/color_rm/upload/${this.state.sessionId}`) || `/api/color_rm/upload/${this.state.sessionId}`, {
803
  method: 'POST',
804
  body: this.state.images[0].blob,
805
+ headers: {
806
+ 'Content-Type': this.state.images[0].blob.type
807
+ }
808
  });
809
  this.ui.showToast("Base file restored!");
810
+ } catch (e) {
811
  this.ui.showToast("Restore failed");
812
  }
813
  } else {
 
900
  await this.dbPut('pages', pageObj);
901
 
902
  // Update UI
903
+ if (this.state.activeSideTab === 'pages') this.renderPageSidebar();
904
  const pt = this.getElement('pageTotal');
905
  if (pt) pt.innerText = '/ ' + this.state.images.length;
906
 
 
1017
  await this.dbPut('pages', pageObj);
1018
 
1019
  // Update UI
1020
+ if (this.state.activeSideTab === 'pages') this.renderPageSidebar();
1021
  const pt = this.getElement('pageTotal');
1022
  if (pt) pt.innerText = '/ ' + this.state.images.length;
1023
 
 
1292
  if (item.shapeType === 'rectangle') {
1293
  ctx.rect(x, y, w, h);
1294
  } else if (item.shapeType === 'circle') {
1295
+ ctx.ellipse(x + w / 2, y + h / 2, Math.abs(w / 2), Math.abs(h / 2), 0, 0, 2 * Math.PI);
1296
  } else if (item.shapeType === 'line') {
1297
  ctx.moveTo(x, y);
1298
  ctx.lineTo(x + w, y + h);
 
1303
  ctx.moveTo(x, y);
1304
  ctx.lineTo(x + w, y + h);
1305
  ctx.lineTo(
1306
+ x + w - headLength * Math.cos(angle - Math.PI / 6),
1307
+ y + h - headLength * Math.sin(angle - Math.PI / 6)
1308
  );
1309
  ctx.moveTo(x + w, y + h);
1310
  ctx.lineTo(
1311
+ x + w - headLength * Math.cos(angle + Math.PI / 6),
1312
+ y + h - headLength * Math.sin(angle + Math.PI / 6)
1313
  );
1314
  }
1315
 
 
1400
  await this.loadPage(this.state.idx, false);
1401
 
1402
  this.ui.showToast(`All ${this.state.images.length} pages resized to ${width}×${height}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1403
  },
1404
 
1405
  async addTemplatePage(templateType) {
 
1492
  await this.dbPut('pages', pageObj);
1493
 
1494
  // Update UI
1495
+ if (this.state.activeSideTab === 'pages') this.renderPageSidebar();
1496
  const pt = this.getElement('pageTotal');
1497
  if (pt) pt.innerText = '/ ' + this.state.images.length;
1498
 
 
1542
  }
1543
 
1544
  this.ui.showToast(`Added ${templateType} template page ${newPageIndex + 1}`);
1545
+ },
1546
 
1547
  reorderPages(fromIndex, toIndex) {
1548
  if (fromIndex < 0 || toIndex < 0 ||
 
1566
  const promises = this.state.images.map(img => this.dbPut('pages', img));
1567
  Promise.all(promises).then(() => {
1568
  this.ui.showToast("Pages reordered");
1569
+ if (this.state.activeSideTab === 'pages') this.renderPageSidebar();
1570
 
1571
  // Synchronize with Liveblocks
1572
  if (this.liveSync) {
 
1576
  this.liveSync.notifyPageStructureChange();
1577
  }
1578
  });
1579
+ }
1580
+ };