Skip to content

Commit 4f895f1

Browse files
authored
Add files via upload
1 parent ac58c41 commit 4f895f1

3 files changed

Lines changed: 186 additions & 1 deletion

File tree

app.js

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,34 @@ const CLOUDINARY_CONFIG = {
1313
uploadPreset: "gogo_app_upload",
1414
folder: "gogo_app"
1515
};
16+
const FIREBASE_CONFIG = {
17+
apiKey: "AIzaSyCkRt_XnSiNTfraQ2OgYInwQq9H65jFdeE",
18+
authDomain: "gogo-app-e5311.firebaseapp.com",
19+
projectId: "gogo-app-e5311",
20+
storageBucket: "gogo-app-e5311.firebasestorage.app",
21+
messagingSenderId: "1044106911706",
22+
appId: "1:1044106911706:web:27a7c5fc0beadd530aeae3",
23+
measurementId: "G-TJN4QMWX77"
24+
};
1625
const LONG_PRESS_MS = 750;
1726
const LONG_PRESS_MOVE_TOLERANCE = 12;
1827
const IMAGE_MAX_SIZE = 900;
1928
const IMAGE_QUALITY = 0.72;
29+
const CLOUD_BACKUP_URL_KEY = `${STORAGE_KEY}-cloud-backup-url`;
30+
const CLOUD_BACKUP_TIME_KEY = `${STORAGE_KEY}-cloud-backup-time`;
31+
const CLOUD_BACKUP_HASH_KEY = `${STORAGE_KEY}-cloud-backup-hash`;
32+
const CLOUD_BACKUP_DELAY_MS = 30000;
33+
const CLOUD_BACKUP_MIN_INTERVAL_MS = 10 * 60 * 1000;
2034
let storageWarningShown = false;
35+
let cloudBackupTimer = null;
36+
let cloudBackupInProgress = false;
37+
let firebaseApp = null;
38+
let firebaseAuth = null;
39+
let firebaseDb = null;
40+
let firebaseUser = null;
41+
let cloudSyncTimer = null;
42+
let cloudSyncInProgress = false;
43+
let applyingRemoteState = false;
2144

2245
const defaultData = {
2346
"settings": {
@@ -1185,6 +1208,10 @@ const els = {
11851208
restoreDataInput: document.querySelector("#restoreDataInput"),
11861209
updateAppBtn: document.querySelector("#updateAppBtn"),
11871210
resetDataBtn: document.querySelector("#resetDataBtn"),
1211+
cloudBackupStatus: document.querySelector("#cloudBackupStatus"),
1212+
copyCloudBackupLinkBtn: document.querySelector("#copyCloudBackupLinkBtn"),
1213+
cloudBackupNowBtn: document.querySelector("#cloudBackupNowBtn"),
1214+
restoreCloudBackupBtn: document.querySelector("#restoreCloudBackupBtn"),
11881215
toolEditModal: document.querySelector("#toolEditModal"),
11891216
closeToolEditModal: document.querySelector("#closeToolEditModal"),
11901217
editToolName: document.querySelector("#editToolName"),
@@ -1671,6 +1698,7 @@ function normalizeGroupItems(group, fallbackId = "sc") {
16711698
function saveState() {
16721699
try {
16731700
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
1701+
scheduleCloudSync();
16741702
} catch (error) {
16751703
console.error("Save failed", error);
16761704
if (!storageWarningShown) {
@@ -2602,6 +2630,146 @@ function restoreFullBackup(payload) {
26022630
switchView("projects");
26032631
}
26042632

2633+
function cloudDocRef() {
2634+
if (!firebaseDb || !firebaseUser) return null;
2635+
return firebaseDb.collection("users").doc(firebaseUser.uid).collection("workbench").doc("state");
2636+
}
2637+
2638+
function updateCloudSyncStatus(text) {
2639+
if (els.cloudBackupStatus) els.cloudBackupStatus.textContent = text;
2640+
}
2641+
2642+
function cloudStatePayload() {
2643+
return {
2644+
type: "gogo-full-backup",
2645+
version: 1,
2646+
clientUpdatedAt: new Date().toISOString(),
2647+
state
2648+
};
2649+
}
2650+
2651+
function updateCloudAuthButtons() {
2652+
els.copyCloudBackupLinkBtn?.classList.toggle("hidden", Boolean(firebaseUser));
2653+
els.restoreCloudBackupBtn?.classList.toggle("hidden", !firebaseUser);
2654+
els.cloudBackupNowBtn?.classList.toggle("hidden", !firebaseUser);
2655+
}
2656+
2657+
function scheduleCloudSync(delay = CLOUD_BACKUP_DELAY_MS) {
2658+
if (applyingRemoteState || !firebaseUser || !firebaseDb) return;
2659+
window.clearTimeout(cloudSyncTimer);
2660+
cloudSyncTimer = window.setTimeout(() => syncStateToCloud(false), delay);
2661+
}
2662+
2663+
async function syncStateToCloud(showAlert = true) {
2664+
if (!firebaseUser || !firebaseDb) {
2665+
if (showAlert) alert("請先用 Google 登入。");
2666+
return;
2667+
}
2668+
if (cloudSyncInProgress) return;
2669+
cloudSyncInProgress = true;
2670+
updateCloudSyncStatus("同步中...");
2671+
try {
2672+
await cloudDocRef().set({
2673+
...cloudStatePayload(),
2674+
updatedAt: firebase.firestore.FieldValue.serverTimestamp()
2675+
}, { merge: true });
2676+
const syncedAt = new Date().toLocaleString("zh-TW", { hour12: false });
2677+
updateCloudSyncStatus(`已登入:${firebaseUser.email || "Google 帳號"},上次同步 ${syncedAt}`);
2678+
localStorage.setItem(CLOUD_BACKUP_TIME_KEY, new Date().toISOString());
2679+
if (showAlert) alert("已同步到雲端。");
2680+
} catch (error) {
2681+
console.error("Cloud sync failed", error);
2682+
const message = error.message || "請稍後再試";
2683+
updateCloudSyncStatus(`同步失敗:${message}`);
2684+
if (showAlert) alert(`同步失敗:${message}`);
2685+
} finally {
2686+
cloudSyncInProgress = false;
2687+
}
2688+
}
2689+
2690+
async function loadCloudStateAfterSignIn() {
2691+
if (!firebaseUser || !firebaseDb) return;
2692+
updateCloudSyncStatus(`已登入:${firebaseUser.email || "Google 帳號"},檢查雲端資料中...`);
2693+
try {
2694+
const snapshot = await cloudDocRef().get();
2695+
const remote = snapshot.exists ? snapshot.data() : null;
2696+
if (remote?.state) {
2697+
const useRemote = confirm("找到雲端同步資料,要用雲端資料取代這台裝置目前資料嗎?\n\n按「取消」會保留本機資料,並上傳成新的雲端資料。");
2698+
if (useRemote) {
2699+
applyingRemoteState = true;
2700+
state = normalizeState(remote.state);
2701+
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
2702+
selectedProjectId = state.projects[0]?.id || null;
2703+
selectedPatternId = state.patterns[0]?.id || null;
2704+
selectedYarnId = state.yarns[0]?.id || null;
2705+
selectedPartId = state.patterns[0]?.parts?.[0]?.id || null;
2706+
selectedProjectIds.clear();
2707+
selectedPatternIds.clear();
2708+
selectedYarnIds.clear();
2709+
activeProjectFolder = null;
2710+
activePatternFolder = null;
2711+
activeYarnFolder = null;
2712+
applyingRemoteState = false;
2713+
updateCloudSyncStatus(`已載入雲端資料:${firebaseUser.email || "Google 帳號"}`);
2714+
switchView("projects");
2715+
return;
2716+
}
2717+
}
2718+
await syncStateToCloud(false);
2719+
} catch (error) {
2720+
console.error("Cloud load failed", error);
2721+
updateCloudSyncStatus(`雲端資料讀取失敗:${error.message || "請稍後再試"}`);
2722+
} finally {
2723+
applyingRemoteState = false;
2724+
}
2725+
}
2726+
2727+
function initFirebaseSync() {
2728+
if (!window.firebase) {
2729+
updateCloudSyncStatus("Firebase 載入失敗,請確認網路後重新整理。");
2730+
updateCloudAuthButtons();
2731+
return;
2732+
}
2733+
try {
2734+
firebaseApp = firebase.apps?.length ? firebase.app() : firebase.initializeApp(FIREBASE_CONFIG);
2735+
firebaseAuth = firebase.auth();
2736+
firebaseDb = firebase.firestore();
2737+
updateCloudAuthButtons();
2738+
firebaseAuth.onAuthStateChanged((user) => {
2739+
firebaseUser = user;
2740+
updateCloudAuthButtons();
2741+
window.clearTimeout(cloudSyncTimer);
2742+
if (user) loadCloudStateAfterSignIn();
2743+
else updateCloudSyncStatus("尚未登入。登入後會自動同步完整資料。");
2744+
});
2745+
} catch (error) {
2746+
console.error("Firebase init failed", error);
2747+
updateCloudSyncStatus(`Firebase 初始化失敗:${error.message || "請稍後再試"}`);
2748+
}
2749+
}
2750+
2751+
async function signInWithGoogle() {
2752+
if (!firebaseAuth) {
2753+
alert("Firebase 尚未準備好,請重新整理後再試。");
2754+
return;
2755+
}
2756+
const provider = new firebase.auth.GoogleAuthProvider();
2757+
try {
2758+
await firebaseAuth.signInWithPopup(provider);
2759+
} catch (error) {
2760+
if (error.code === "auth/popup-blocked" || error.code === "auth/operation-not-supported-in-this-environment") {
2761+
await firebaseAuth.signInWithRedirect(provider);
2762+
return;
2763+
}
2764+
alert(`登入失敗:${error.message || "請稍後再試"}`);
2765+
}
2766+
}
2767+
2768+
async function signOutGoogle() {
2769+
if (!firebaseAuth) return;
2770+
await firebaseAuth.signOut();
2771+
}
2772+
26052773
function patternCloudShareUrl(dataUrl) {
26062774
const url = new URL(location.href);
26072775
url.hash = "";
@@ -5475,6 +5643,9 @@ els.resetDataBtn.addEventListener("click", () => {
54755643
els.backupDataBtn.addEventListener("click", () => downloadFullBackup());
54765644
els.restoreDataBtn.addEventListener("click", () => els.restoreDataInput.click());
54775645
els.updateAppBtn.addEventListener("click", () => forceAppUpdate());
5646+
els.copyCloudBackupLinkBtn?.addEventListener("click", signInWithGoogle);
5647+
els.cloudBackupNowBtn?.addEventListener("click", () => syncStateToCloud(true));
5648+
els.restoreCloudBackupBtn?.addEventListener("click", signOutGoogle);
54785649
els.restoreDataInput.addEventListener("change", async () => {
54795650
const file = els.restoreDataInput.files?.[0];
54805651
if (!file) return;
@@ -6077,6 +6248,7 @@ requestAnimationFrame(() => {
60776248
document.querySelector(".workspace")?.scrollTo({ top: 0 });
60786249
});
60796250
render();
6251+
initFirebaseSync();
60806252
checkSharedPatternImportUrl();
60816253
checkSharedPatternFromUrl();
60826254
checkSharedStashFromUrl();

index.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,16 @@ <h2>設定</h2>
410410
<input class="hidden" id="restoreDataInput" type="file" accept="application/json,text/plain,.json,.txt,.gogobackup" />
411411
</div>
412412
<p class="muted">更新 APP 只會重新抓取程式檔案,不會刪除作品、織圖或庫存資料。</p>
413+
<div class="cloud-backup-panel">
414+
<strong>Google 雲端同步</strong>
415+
<p class="muted" id="cloudBackupStatus">尚未登入。登入後會自動同步完整資料。</p>
416+
<div class="button-row data-actions">
417+
<button class="outline-button" id="copyCloudBackupLinkBtn">用 Google 登入</button>
418+
<button class="outline-button" id="cloudBackupNowBtn">立即同步</button>
419+
<button class="outline-button" id="restoreCloudBackupBtn">登出</button>
420+
</div>
421+
<p class="muted">登入後會用你的 Google 帳號分開保存資料,其他使用者看不到你的內容。</p>
422+
</div>
413423
<p class="warning-text">清除後會刪除這台裝置裡的所有作品、織圖與庫存,並回到目前 APP 內建的預設設定。這個動作無法復原。</p>
414424
<button class="danger-button full-width" id="resetDataBtn">清除所有資料</button>
415425
</details>
@@ -800,6 +810,9 @@ <h2>扣除庫存</h2>
800810
</nav>
801811
</div>
802812

813+
<script src="https://www.gstatic.com/firebasejs/10.12.5/firebase-app-compat.js"></script>
814+
<script src="https://www.gstatic.com/firebasejs/10.12.5/firebase-auth-compat.js"></script>
815+
<script src="https://www.gstatic.com/firebasejs/10.12.5/firebase-firestore-compat.js"></script>
803816
<script src="app.js"></script>
804817
</body>
805818
</html>

service-worker.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const VERSION = "127";
1+
const VERSION = "128";
22
const CACHE_NAME = `free-knit-workbench-v${VERSION}`;
33
const ASSETS = [
44
"./",

0 commit comments

Comments
 (0)