Update index.html
Browse files- index.html +180 -18
index.html
CHANGED
|
@@ -21,6 +21,9 @@
|
|
| 21 |
<!-- Cropper.js for Image Cropping -->
|
| 22 |
<link href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.css" rel="stylesheet">
|
| 23 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.js"></script>
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
<!-- FontAwesome 6 (For Consistent Icons) -->
|
| 26 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
@@ -115,16 +118,36 @@
|
|
| 115 |
<div class="w-full md:w-1/3 lg:w-1/4 bg-white border-r border-slate-200 flex flex-col h-full shadow-lg z-20 relative">
|
| 116 |
|
| 117 |
<!-- Header -->
|
| 118 |
-
<div class="p-6 border-b border-slate-100 bg-slate-50">
|
| 119 |
-
<div class="flex justify-between items-center mb-
|
| 120 |
<h1 class="text-2xl font-bold text-indigo-600 flex items-center gap-2">
|
| 121 |
<span class="text-3xl">🎩</span>
|
| 122 |
摺紙魔術設計師
|
| 123 |
</h1>
|
| 124 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
|
| 126 |
<!-- Page Navigator -->
|
| 127 |
-
<div class="
|
| 128 |
<button
|
| 129 |
@click="viewMode = 'overview'"
|
| 130 |
class="flex-1 py-1 text-sm font-bold rounded-md transition-all"
|
|
@@ -351,6 +374,7 @@
|
|
| 351 |
type="text"
|
| 352 |
v-model="inputBuffer"
|
| 353 |
@input="updateSelectedCellText"
|
|
|
|
| 354 |
placeholder="先點選右側格子..."
|
| 355 |
maxlength="3"
|
| 356 |
:disabled="selectedCellIndices.size === 0"
|
|
@@ -712,8 +736,15 @@
|
|
| 712 |
</div>
|
| 713 |
|
| 714 |
<script>
|
| 715 |
-
const { createApp, ref, computed, nextTick, onMounted, watch } = Vue;
|
| 716 |
const { jsPDF } = window.jspdf;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 717 |
|
| 718 |
createApp({
|
| 719 |
setup() {
|
|
@@ -825,6 +856,80 @@
|
|
| 825 |
return activePageCells.value[idx].imageScale || 'fit';
|
| 826 |
});
|
| 827 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 828 |
// --- Auto-Save Key ---
|
| 829 |
const AUTOSAVE_KEY = 'magic_origami_autosave_v1';
|
| 830 |
|
|
@@ -886,6 +991,8 @@
|
|
| 886 |
clearSelection();
|
| 887 |
activePageId.value = 1;
|
| 888 |
viewMode.value = 'overview';
|
|
|
|
|
|
|
| 889 |
};
|
| 890 |
|
| 891 |
// Custom Grid Logic
|
|
@@ -922,6 +1029,8 @@
|
|
| 922 |
activePageId.value = 1;
|
| 923 |
viewMode.value = 'overview';
|
| 924 |
showGridModal.value = false;
|
|
|
|
|
|
|
| 925 |
};
|
| 926 |
|
| 927 |
const clearAllContent = () => {
|
|
@@ -942,6 +1051,7 @@
|
|
| 942 |
];
|
| 943 |
|
| 944 |
clearSelection();
|
|
|
|
| 945 |
alert("內容已清空!");
|
| 946 |
}
|
| 947 |
};
|
|
@@ -965,6 +1075,7 @@
|
|
| 965 |
selectedCellIndices.value.forEach(index => {
|
| 966 |
activePageCells.value[index].color = color;
|
| 967 |
});
|
|
|
|
| 968 |
};
|
| 969 |
|
| 970 |
const applyBgColor = (bgColor) => {
|
|
@@ -972,6 +1083,7 @@
|
|
| 972 |
selectedCellIndices.value.forEach(index => {
|
| 973 |
activePageCells.value[index].bgColor = bgColor;
|
| 974 |
});
|
|
|
|
| 975 |
};
|
| 976 |
|
| 977 |
const toggleImageScale = () => {
|
|
@@ -979,6 +1091,7 @@
|
|
| 979 |
selectedCellIndices.value.forEach(index => {
|
| 980 |
activePageCells.value[index].imageScale = newScale;
|
| 981 |
});
|
|
|
|
| 982 |
};
|
| 983 |
|
| 984 |
// --- Image Upload Logic ---
|
|
@@ -1114,6 +1227,7 @@
|
|
| 1114 |
applyCustomImageToCell(base64);
|
| 1115 |
}
|
| 1116 |
|
|
|
|
| 1117 |
cancelCrop();
|
| 1118 |
};
|
| 1119 |
|
|
@@ -1129,11 +1243,13 @@
|
|
| 1129 |
});
|
| 1130 |
inputBuffer.value = '';
|
| 1131 |
batchInputBuffer.value = '';
|
|
|
|
| 1132 |
};
|
| 1133 |
|
| 1134 |
const removeCustomImage = (idx) => {
|
| 1135 |
if(confirm('確定要移除這張自定義圖片嗎?')) {
|
| 1136 |
customImages.value.splice(idx, 1);
|
|
|
|
| 1137 |
}
|
| 1138 |
};
|
| 1139 |
|
|
@@ -1185,6 +1301,7 @@
|
|
| 1185 |
activePageId.value = 1;
|
| 1186 |
viewMode.value = 'overview';
|
| 1187 |
|
|
|
|
| 1188 |
alert("專案匯入成功!");
|
| 1189 |
} catch (err) {
|
| 1190 |
console.error(err);
|
|
@@ -1329,6 +1446,7 @@
|
|
| 1329 |
activePageId.value = 1;
|
| 1330 |
viewMode.value = 'overview';
|
| 1331 |
|
|
|
|
| 1332 |
alert("模板套用成功!");
|
| 1333 |
|
| 1334 |
} catch (e) {
|
|
@@ -1412,6 +1530,7 @@
|
|
| 1412 |
const cell = activePageCells.value[index];
|
| 1413 |
cell.rotation = (cell.rotation + 90) % 360;
|
| 1414 |
});
|
|
|
|
| 1415 |
};
|
| 1416 |
|
| 1417 |
// Single Cell Text Update
|
|
@@ -1448,6 +1567,8 @@
|
|
| 1448 |
// Clear buffers after apply
|
| 1449 |
batchInputBuffer.value = '';
|
| 1450 |
inputBuffer.value = '';
|
|
|
|
|
|
|
| 1451 |
};
|
| 1452 |
|
| 1453 |
const applyIconToCell = (iconName) => {
|
|
@@ -1460,6 +1581,8 @@
|
|
| 1460 |
});
|
| 1461 |
inputBuffer.value = '';
|
| 1462 |
batchInputBuffer.value = '';
|
|
|
|
|
|
|
| 1463 |
};
|
| 1464 |
|
| 1465 |
const clearCurrentCell = () => {
|
|
@@ -1476,16 +1599,33 @@
|
|
| 1476 |
batchInputBuffer.value = '';
|
| 1477 |
selectedColor.value = '#1e293b';
|
| 1478 |
selectedBgColor.value = '#ffffff';
|
|
|
|
|
|
|
| 1479 |
};
|
| 1480 |
|
| 1481 |
// --- Auto Save & Init ---
|
| 1482 |
|
| 1483 |
-
onMounted(() => {
|
| 1484 |
-
//
|
| 1485 |
-
|
| 1486 |
-
|
| 1487 |
-
|
| 1488 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1489 |
const hasContent = parsed.pages.some(p => p.cells.some(c => c.content !== ''));
|
| 1490 |
|
| 1491 |
if (hasContent) {
|
|
@@ -1496,21 +1636,37 @@
|
|
| 1496 |
customImages.value = parsed.customImages || [];
|
| 1497 |
}
|
| 1498 |
}
|
| 1499 |
-
} catch (e) {
|
| 1500 |
-
console.error("Auto-save parse error", e);
|
| 1501 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1502 |
}
|
| 1503 |
|
| 1504 |
-
// 2. Auto-save Watcher
|
| 1505 |
-
watch([currentGrid, pages, customImages], () => {
|
| 1506 |
-
|
|
|
|
|
|
|
|
|
|
| 1507 |
grid: currentGrid.value,
|
| 1508 |
pages: pages.value,
|
| 1509 |
customImages: customImages.value
|
| 1510 |
-
};
|
| 1511 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1512 |
}, { deep: true });
|
| 1513 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1514 |
|
| 1515 |
// Helper to replace text element with SVG for perfect centering
|
| 1516 |
const replaceWithSvgText = (el, content, color, fontFamily, fontWeight, fontSize) => {
|
|
@@ -1917,7 +2073,13 @@
|
|
| 1917 |
selectedIndicesArray,
|
| 1918 |
// Puzzle
|
| 1919 |
toggleImageScale,
|
| 1920 |
-
currentImageScale
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1921 |
};
|
| 1922 |
}
|
| 1923 |
}).mount('#app');
|
|
|
|
| 21 |
<!-- Cropper.js for Image Cropping -->
|
| 22 |
<link href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.css" rel="stylesheet">
|
| 23 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.js"></script>
|
| 24 |
+
|
| 25 |
+
<!-- IndexedDB Wrapper (idb-keyval) -->
|
| 26 |
+
<script src="https://cdn.jsdelivr.net/npm/idb-keyval@6/dist/iife/index.js"></script>
|
| 27 |
|
| 28 |
<!-- FontAwesome 6 (For Consistent Icons) -->
|
| 29 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
|
|
| 118 |
<div class="w-full md:w-1/3 lg:w-1/4 bg-white border-r border-slate-200 flex flex-col h-full shadow-lg z-20 relative">
|
| 119 |
|
| 120 |
<!-- Header -->
|
| 121 |
+
<div class="p-6 border-b border-slate-100 bg-slate-50 relative">
|
| 122 |
+
<div class="flex justify-between items-center mb-4">
|
| 123 |
<h1 class="text-2xl font-bold text-indigo-600 flex items-center gap-2">
|
| 124 |
<span class="text-3xl">🎩</span>
|
| 125 |
摺紙魔術設計師
|
| 126 |
</h1>
|
| 127 |
</div>
|
| 128 |
+
|
| 129 |
+
<!-- History Controls (Undo/Redo) -->
|
| 130 |
+
<div class="flex gap-2 mb-4">
|
| 131 |
+
<button
|
| 132 |
+
@click="undo"
|
| 133 |
+
:disabled="historyIndex <= 0"
|
| 134 |
+
class="flex-1 py-2 bg-white border border-slate-300 rounded-lg text-slate-700 font-bold text-sm hover:bg-slate-50 disabled:opacity-40 disabled:cursor-not-allowed transition-all flex items-center justify-center gap-2 shadow-sm"
|
| 135 |
+
title="復原 (Ctrl+Z)"
|
| 136 |
+
>
|
| 137 |
+
<i class="fa-solid fa-rotate-left"></i> 復原
|
| 138 |
+
</button>
|
| 139 |
+
<button
|
| 140 |
+
@click="redo"
|
| 141 |
+
:disabled="historyIndex >= history.length - 1"
|
| 142 |
+
class="flex-1 py-2 bg-white border border-slate-300 rounded-lg text-slate-700 font-bold text-sm hover:bg-slate-50 disabled:opacity-40 disabled:cursor-not-allowed transition-all flex items-center justify-center gap-2 shadow-sm"
|
| 143 |
+
title="重做 (Ctrl+Y)"
|
| 144 |
+
>
|
| 145 |
+
<i class="fa-solid fa-rotate-right"></i> 重做
|
| 146 |
+
</button>
|
| 147 |
+
</div>
|
| 148 |
|
| 149 |
<!-- Page Navigator -->
|
| 150 |
+
<div class="flex bg-slate-200 p-1 rounded-lg">
|
| 151 |
<button
|
| 152 |
@click="viewMode = 'overview'"
|
| 153 |
class="flex-1 py-1 text-sm font-bold rounded-md transition-all"
|
|
|
|
| 374 |
type="text"
|
| 375 |
v-model="inputBuffer"
|
| 376 |
@input="updateSelectedCellText"
|
| 377 |
+
@blur="saveHistory"
|
| 378 |
placeholder="先點選右側格子..."
|
| 379 |
maxlength="3"
|
| 380 |
:disabled="selectedCellIndices.size === 0"
|
|
|
|
| 736 |
</div>
|
| 737 |
|
| 738 |
<script>
|
| 739 |
+
const { createApp, ref, computed, nextTick, onMounted, onUnmounted, watch } = Vue;
|
| 740 |
const { jsPDF } = window.jspdf;
|
| 741 |
+
|
| 742 |
+
// Check if idbKeyval is loaded
|
| 743 |
+
const db = window.idbKeyval || {
|
| 744 |
+
get: async () => null,
|
| 745 |
+
set: async () => {},
|
| 746 |
+
del: async () => {}
|
| 747 |
+
};
|
| 748 |
|
| 749 |
createApp({
|
| 750 |
setup() {
|
|
|
|
| 856 |
return activePageCells.value[idx].imageScale || 'fit';
|
| 857 |
});
|
| 858 |
|
| 859 |
+
// --- History System (Undo/Redo) ---
|
| 860 |
+
const history = ref([]);
|
| 861 |
+
const historyIndex = ref(-1);
|
| 862 |
+
const MAX_HISTORY = 30;
|
| 863 |
+
const isUndoing = ref(false); // Flag to prevent auto-save logic during undo
|
| 864 |
+
|
| 865 |
+
const saveHistory = () => {
|
| 866 |
+
if (isUndoing.value) return;
|
| 867 |
+
|
| 868 |
+
// Remove any future states (redo branch)
|
| 869 |
+
if (historyIndex.value < history.value.length - 1) {
|
| 870 |
+
history.value = history.value.slice(0, historyIndex.value + 1);
|
| 871 |
+
}
|
| 872 |
+
|
| 873 |
+
// Create deep copy snapshot
|
| 874 |
+
const snapshot = JSON.parse(JSON.stringify({
|
| 875 |
+
grid: currentGrid.value,
|
| 876 |
+
pages: pages.value,
|
| 877 |
+
customImages: customImages.value
|
| 878 |
+
}));
|
| 879 |
+
|
| 880 |
+
history.value.push(snapshot);
|
| 881 |
+
historyIndex.value++;
|
| 882 |
+
|
| 883 |
+
// Limit history size
|
| 884 |
+
if (history.value.length > MAX_HISTORY) {
|
| 885 |
+
history.value.shift();
|
| 886 |
+
historyIndex.value--;
|
| 887 |
+
}
|
| 888 |
+
};
|
| 889 |
+
|
| 890 |
+
const undo = () => {
|
| 891 |
+
if (historyIndex.value > 0) {
|
| 892 |
+
isUndoing.value = true;
|
| 893 |
+
historyIndex.value--;
|
| 894 |
+
loadSnapshot(history.value[historyIndex.value]);
|
| 895 |
+
// Wait for Vue reactivity to update before allowing saves
|
| 896 |
+
nextTick(() => { isUndoing.value = false; });
|
| 897 |
+
}
|
| 898 |
+
};
|
| 899 |
+
|
| 900 |
+
const redo = () => {
|
| 901 |
+
if (historyIndex.value < history.value.length - 1) {
|
| 902 |
+
isUndoing.value = true;
|
| 903 |
+
historyIndex.value++;
|
| 904 |
+
loadSnapshot(history.value[historyIndex.value]);
|
| 905 |
+
nextTick(() => { isUndoing.value = false; });
|
| 906 |
+
}
|
| 907 |
+
};
|
| 908 |
+
|
| 909 |
+
const loadSnapshot = (snapshot) => {
|
| 910 |
+
currentGrid.value = snapshot.grid;
|
| 911 |
+
pages.value = snapshot.pages;
|
| 912 |
+
customImages.value = snapshot.customImages;
|
| 913 |
+
// We keep selection if possible, or clear it.
|
| 914 |
+
// Clearing is safer to avoid selecting invalid indices if grid changed.
|
| 915 |
+
// But if grid is same, maybe keep? Let's simply clear for robustness.
|
| 916 |
+
clearSelection();
|
| 917 |
+
};
|
| 918 |
+
|
| 919 |
+
const handleKeyboardShortcuts = (e) => {
|
| 920 |
+
if ((e.ctrlKey || e.metaKey) && e.key === 'z') {
|
| 921 |
+
e.preventDefault();
|
| 922 |
+
if (e.shiftKey) {
|
| 923 |
+
redo();
|
| 924 |
+
} else {
|
| 925 |
+
undo();
|
| 926 |
+
}
|
| 927 |
+
} else if ((e.ctrlKey || e.metaKey) && e.key === 'y') {
|
| 928 |
+
e.preventDefault();
|
| 929 |
+
redo();
|
| 930 |
+
}
|
| 931 |
+
};
|
| 932 |
+
|
| 933 |
// --- Auto-Save Key ---
|
| 934 |
const AUTOSAVE_KEY = 'magic_origami_autosave_v1';
|
| 935 |
|
|
|
|
| 991 |
clearSelection();
|
| 992 |
activePageId.value = 1;
|
| 993 |
viewMode.value = 'overview';
|
| 994 |
+
|
| 995 |
+
saveHistory(); // HISTORY
|
| 996 |
};
|
| 997 |
|
| 998 |
// Custom Grid Logic
|
|
|
|
| 1029 |
activePageId.value = 1;
|
| 1030 |
viewMode.value = 'overview';
|
| 1031 |
showGridModal.value = false;
|
| 1032 |
+
|
| 1033 |
+
saveHistory(); // HISTORY
|
| 1034 |
};
|
| 1035 |
|
| 1036 |
const clearAllContent = () => {
|
|
|
|
| 1051 |
];
|
| 1052 |
|
| 1053 |
clearSelection();
|
| 1054 |
+
saveHistory(); // HISTORY
|
| 1055 |
alert("內容已清空!");
|
| 1056 |
}
|
| 1057 |
};
|
|
|
|
| 1075 |
selectedCellIndices.value.forEach(index => {
|
| 1076 |
activePageCells.value[index].color = color;
|
| 1077 |
});
|
| 1078 |
+
saveHistory(); // HISTORY
|
| 1079 |
};
|
| 1080 |
|
| 1081 |
const applyBgColor = (bgColor) => {
|
|
|
|
| 1083 |
selectedCellIndices.value.forEach(index => {
|
| 1084 |
activePageCells.value[index].bgColor = bgColor;
|
| 1085 |
});
|
| 1086 |
+
saveHistory(); // HISTORY
|
| 1087 |
};
|
| 1088 |
|
| 1089 |
const toggleImageScale = () => {
|
|
|
|
| 1091 |
selectedCellIndices.value.forEach(index => {
|
| 1092 |
activePageCells.value[index].imageScale = newScale;
|
| 1093 |
});
|
| 1094 |
+
saveHistory(); // HISTORY
|
| 1095 |
};
|
| 1096 |
|
| 1097 |
// --- Image Upload Logic ---
|
|
|
|
| 1227 |
applyCustomImageToCell(base64);
|
| 1228 |
}
|
| 1229 |
|
| 1230 |
+
saveHistory(); // HISTORY (New image added or applied)
|
| 1231 |
cancelCrop();
|
| 1232 |
};
|
| 1233 |
|
|
|
|
| 1243 |
});
|
| 1244 |
inputBuffer.value = '';
|
| 1245 |
batchInputBuffer.value = '';
|
| 1246 |
+
saveHistory(); // HISTORY
|
| 1247 |
};
|
| 1248 |
|
| 1249 |
const removeCustomImage = (idx) => {
|
| 1250 |
if(confirm('確定要移除這張自定義圖片嗎?')) {
|
| 1251 |
customImages.value.splice(idx, 1);
|
| 1252 |
+
saveHistory(); // HISTORY
|
| 1253 |
}
|
| 1254 |
};
|
| 1255 |
|
|
|
|
| 1301 |
activePageId.value = 1;
|
| 1302 |
viewMode.value = 'overview';
|
| 1303 |
|
| 1304 |
+
saveHistory(); // HISTORY
|
| 1305 |
alert("專案匯入成功!");
|
| 1306 |
} catch (err) {
|
| 1307 |
console.error(err);
|
|
|
|
| 1446 |
activePageId.value = 1;
|
| 1447 |
viewMode.value = 'overview';
|
| 1448 |
|
| 1449 |
+
saveHistory(); // HISTORY
|
| 1450 |
alert("模板套用成功!");
|
| 1451 |
|
| 1452 |
} catch (e) {
|
|
|
|
| 1530 |
const cell = activePageCells.value[index];
|
| 1531 |
cell.rotation = (cell.rotation + 90) % 360;
|
| 1532 |
});
|
| 1533 |
+
saveHistory(); // HISTORY
|
| 1534 |
};
|
| 1535 |
|
| 1536 |
// Single Cell Text Update
|
|
|
|
| 1567 |
// Clear buffers after apply
|
| 1568 |
batchInputBuffer.value = '';
|
| 1569 |
inputBuffer.value = '';
|
| 1570 |
+
|
| 1571 |
+
saveHistory(); // HISTORY
|
| 1572 |
};
|
| 1573 |
|
| 1574 |
const applyIconToCell = (iconName) => {
|
|
|
|
| 1581 |
});
|
| 1582 |
inputBuffer.value = '';
|
| 1583 |
batchInputBuffer.value = '';
|
| 1584 |
+
|
| 1585 |
+
saveHistory(); // HISTORY
|
| 1586 |
};
|
| 1587 |
|
| 1588 |
const clearCurrentCell = () => {
|
|
|
|
| 1599 |
batchInputBuffer.value = '';
|
| 1600 |
selectedColor.value = '#1e293b';
|
| 1601 |
selectedBgColor.value = '#ffffff';
|
| 1602 |
+
|
| 1603 |
+
saveHistory(); // HISTORY
|
| 1604 |
};
|
| 1605 |
|
| 1606 |
// --- Auto Save & Init ---
|
| 1607 |
|
| 1608 |
+
onMounted(async () => {
|
| 1609 |
+
// Register Keyboard Listeners
|
| 1610 |
+
window.addEventListener('keydown', handleKeyboardShortcuts);
|
| 1611 |
+
|
| 1612 |
+
// 1. Auto-save restoration check (IndexedDB)
|
| 1613 |
+
try {
|
| 1614 |
+
const savedData = await db.get(AUTOSAVE_KEY);
|
| 1615 |
+
|
| 1616 |
+
// If no IndexedDB data, fallback check localStorage (Migration)
|
| 1617 |
+
let parsed = savedData;
|
| 1618 |
+
if (!parsed) {
|
| 1619 |
+
const lsData = localStorage.getItem(AUTOSAVE_KEY);
|
| 1620 |
+
if (lsData) {
|
| 1621 |
+
try {
|
| 1622 |
+
parsed = JSON.parse(lsData);
|
| 1623 |
+
console.log("Migrating from LocalStorage to IndexedDB...");
|
| 1624 |
+
} catch(e) {}
|
| 1625 |
+
}
|
| 1626 |
+
}
|
| 1627 |
+
|
| 1628 |
+
if (parsed) {
|
| 1629 |
const hasContent = parsed.pages.some(p => p.cells.some(c => c.content !== ''));
|
| 1630 |
|
| 1631 |
if (hasContent) {
|
|
|
|
| 1636 |
customImages.value = parsed.customImages || [];
|
| 1637 |
}
|
| 1638 |
}
|
|
|
|
|
|
|
| 1639 |
}
|
| 1640 |
+
|
| 1641 |
+
// Initialize History after load
|
| 1642 |
+
saveHistory();
|
| 1643 |
+
|
| 1644 |
+
} catch (e) {
|
| 1645 |
+
console.error("Auto-save load error", e);
|
| 1646 |
}
|
| 1647 |
|
| 1648 |
+
// 2. Auto-save Watcher (IndexedDB)
|
| 1649 |
+
watch([currentGrid, pages, customImages], async () => {
|
| 1650 |
+
// Do not save if we are currently undoing/redoing to prevent race conditions or unnecessary writes
|
| 1651 |
+
if (isUndoing.value) return;
|
| 1652 |
+
|
| 1653 |
+
const dataToSave = JSON.parse(JSON.stringify({
|
| 1654 |
grid: currentGrid.value,
|
| 1655 |
pages: pages.value,
|
| 1656 |
customImages: customImages.value
|
| 1657 |
+
}));
|
| 1658 |
+
|
| 1659 |
+
try {
|
| 1660 |
+
await db.set(AUTOSAVE_KEY, dataToSave);
|
| 1661 |
+
} catch (e) {
|
| 1662 |
+
console.error("IndexedDB Save Error", e);
|
| 1663 |
+
}
|
| 1664 |
}, { deep: true });
|
| 1665 |
});
|
| 1666 |
+
|
| 1667 |
+
onUnmounted(() => {
|
| 1668 |
+
window.removeEventListener('keydown', handleKeyboardShortcuts);
|
| 1669 |
+
});
|
| 1670 |
|
| 1671 |
// Helper to replace text element with SVG for perfect centering
|
| 1672 |
const replaceWithSvgText = (el, content, color, fontFamily, fontWeight, fontSize) => {
|
|
|
|
| 2073 |
selectedIndicesArray,
|
| 2074 |
// Puzzle
|
| 2075 |
toggleImageScale,
|
| 2076 |
+
currentImageScale,
|
| 2077 |
+
// History
|
| 2078 |
+
undo,
|
| 2079 |
+
redo,
|
| 2080 |
+
saveHistory,
|
| 2081 |
+
historyIndex,
|
| 2082 |
+
history
|
| 2083 |
};
|
| 2084 |
}
|
| 2085 |
}).mount('#app');
|