Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files- Qwen.md +56 -0
- 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 = () => {
|
|
|
|
|
|
|
| 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 |
-
|
| 144 |
-
|
| 145 |
-
|
| 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 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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", {
|
| 447 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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) {
|
|
|
|
|
|
|
| 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 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 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) {
|
|
|
|
|
|
|
| 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 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 702 |
} else {
|
| 703 |
-
const pageObj = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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: {
|
|
|
|
|
|
|
| 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 |
+
};
|