Lashtw commited on
Commit
984507d
·
verified ·
1 Parent(s): 68d4579

Update index.html

Browse files
Files changed (1) hide show
  1. 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-2">
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="mt-4 flex bg-slate-200 p-1 rounded-lg">
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
- // 1. Auto-save restoration check
1485
- const savedData = localStorage.getItem(AUTOSAVE_KEY);
1486
- if (savedData) {
1487
- try {
1488
- const parsed = JSON.parse(savedData);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- const dataToSave = {
 
 
 
1507
  grid: currentGrid.value,
1508
  pages: pages.value,
1509
  customImages: customImages.value
1510
- };
1511
- localStorage.setItem(AUTOSAVE_KEY, JSON.stringify(dataToSave));
 
 
 
 
 
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');