Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
.vscode/settings.json
*.ffs_db
*.ffs_gui
js/firebase-config.js
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@

[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/Aiuanyu/HakSpring)

## Firebase 設定(開發人員用)

本專案使用 Firebase Realtime Database 來同步使用者資料。若您需要在本地端開發或自行部署,請依照以下步驟設定:

1. **建立 Firebase 專案**:前往 [Firebase Console](https://console.firebase.google.com/) 建立一個新專案。
2. **設定 Realtime Database**:在您个專案中,建立一個 Realtime Database。
3. **啟用匿名登入**:在 Authentication > Sign-in method 分頁,啟用「匿名」登入。
4. **取得設定資訊**:在專案設定中,尋到您个 Web App 个 Firebase 設定物件。
5. **建立設定檔**:
* 在 `js/` 資料夾內,將 `firebase-config.js.example` 複製一份並改名做 `firebase-config.js`。
* 用您在步驟 4 取得个真實設定,取代 `firebase-config.js` 內个 placeholder 值。

`firebase-config.js` 已被加入 `.gitignore`,做毋會提交到版本控制中。

## 鍵盤控制

- 空白鍵
Expand Down
7 changes: 7 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@
<link rel="stylesheet" href="style.css" />
<link href="https://tauhu.tw/tauhu-oo.css" rel="stylesheet" />

<!-- Firebase SDK -->
<script src="https://www.gstatic.com/firebasejs/9.6.1/firebase-app-compat.js" defer></script>
<script src="https://www.gstatic.com/firebasejs/9.6.1/firebase-auth-compat.js" defer></script>
<script src="https://www.gstatic.com/firebasejs/9.6.1/firebase-database-compat.js" defer></script>

<script type="text/javascript" src="js/firebase-config.js" defer></script>
<script type="text/javascript" src="js/userData.js" defer></script>
<script type="text/javascript" src="main.js" defer></script>
<script type="text/javascript" src="js/romanizer.js" defer></script>
<script
Expand Down
20 changes: 20 additions & 0 deletions js/firebase-config.js.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// js/firebase-config.js

// IMPORTANT: Replace the following with your app's Firebase project configuration
const firebaseConfig = {
apiKey: "AIzaSyXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
authDomain: "your-project-id.firebaseapp.com",
databaseURL: "https://your-project-id-default-rtdb.firebaseio.com",
projectId: "your-project-id",
storageBucket: "your-project-id.appspot.com",
messagingSenderId: "123456789012",
appId: "1:123456789012:web:XXXXXXXXXXXXXXXXXXXXXX"
};

// Initialize Firebase
if (typeof firebase !== 'undefined') {
firebase.initializeApp(firebaseConfig);
console.log("Firebase initialized successfully.");
} else {
console.error("Firebase SDK not loaded. Please check your script tags.");
}
4 changes: 2 additions & 2 deletions js/romanizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ document.addEventListener('DOMContentLoaded', () => {
const romanizerUndoBtn = document.getElementById('romanizer-undo-btn');
const romanizerRedoBtn = document.getElementById('romanizer-redo-btn');

const savedJoiningMode = localStorage.getItem(ROMANIZER_JOINING_MODE_KEY);
const savedJoiningMode = getUserData(ROMANIZER_JOINING_MODE_KEY);
if (savedJoiningMode && romanizerJoiningModeSelector) {
romanizerJoiningMode = savedJoiningMode;
romanizerJoiningModeSelector.value = savedJoiningMode;
Expand All @@ -51,7 +51,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (romanizerJoiningModeSelector) {
romanizerJoiningModeSelector.addEventListener('change', function() {
romanizerJoiningMode = this.value;
localStorage.setItem(ROMANIZER_JOINING_MODE_KEY, romanizerJoiningMode);
setUserData(ROMANIZER_JOINING_MODE_KEY, romanizerJoiningMode);
console.log(`連詞模式已切換並儲存: ${romanizerJoiningMode}`);

// 淨更新分隔符,毋儲存到歷史紀錄
Expand Down
197 changes: 197 additions & 0 deletions js/userData.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
// js/userData.js

// --- 全域變數 ---
let firebaseUser = null;
let userRef = null;
let localUserDataCache = {}; // 本地快取
let isFirebaseDataLoaded = false;
let firebaseDataLoadedPromise = null;

// --- Firebase 初始化同使用者驗證 ---

async function initFirebase() {
firebaseDataLoadedPromise = new Promise((resolve, reject) => {
if (!firebase || !firebase.auth || !firebase.database) {
console.warn("Firebase SDK 尚未載入,資料同步功能將無法使用。");
isFirebaseDataLoaded = true;
resolve();
return;
}

const unsubscribe = firebase.auth().onAuthStateChanged(async (user) => {
unsubscribe(); // 確保只執行一次
if (user) {
firebaseUser = user;
console.log("Firebase 使用者已驗證 (匿名),UID:", firebaseUser.uid);
userRef = firebase.database().ref('users/' + firebaseUser.uid);
await loadDataFromFirebase();
resolve();
} else {
console.log("使用者尚未登入,嘗試匿名登入...");
try {
await firebase.auth().signInAnonymously();
// 登入成功後,onAuthStateChanged 會再次被觸發,並由上面个 if (user) 區塊處理
} catch (error) {
console.error("Firebase 匿名登入失敗:", error);
isFirebaseDataLoaded = true;
reject(error);
}
}
});
});
return firebaseDataLoadedPromise;
}

/**
* 從 Firebase 載入使用者所有資料到本地快取,並處理舊資料徙竇。
* @returns {Promise<void>}
*/
async function loadDataFromFirebase() {
return new Promise(resolve => {
if (!userRef) {
isFirebaseDataLoaded = true;
return resolve();
}
userRef.once('value', async (snapshot) => {
const data = snapshot.val();
if (data) {
localUserDataCache = data;
console.log("已從 Firebase 載入使用者資料到快取:", localUserDataCache);
} else {
console.log("Firebase 肚尚無使用者資料,檢查係無係有舊資料愛徙竇...");
await migrateLocalStorageToFirebase();
}
isFirebaseDataLoaded = true;
resolve();
}, (error) => {
console.error("從 Firebase 讀取資料失敗:", error);
isFirebaseDataLoaded = true;
resolve();
});
});
}

/**
* 將 localStorage 內个舊資料搬上 Firebase。
* @returns {Promise<void>}
*/
async function migrateLocalStorageToFirebase() {
const keysToMigrate = ['hakkaBookmarks', 'dontShowInfoModalAgain', 'lastSearchMode', 'lastSearchDialect', 'whatsNewVersion', 'romanizerJoiningMode'];
let dataToMigrate = {};
let hasDataToMigrate = false;

keysToMigrate.forEach(key => {
const localValueRaw = localStorage.getItem(key);
if (localValueRaw !== null) {
try {
dataToMigrate[key] = JSON.parse(localValueRaw);
} catch (e) {
dataToMigrate[key] = localValueRaw;
}
hasDataToMigrate = true;
}
});

if (hasDataToMigrate) {
console.log("尋著 localStorage 舊資料,當在該搬上 Firebase...", dataToMigrate);
if (userRef) {
await userRef.set(dataToMigrate);
localUserDataCache = dataToMigrate; // 更新本地快取
console.log("舊資料徙竇成功!");
// 徙竇成功後,清理舊資料
keysToMigrate.forEach(key => {
localStorage.removeItem(key);
});
console.log("已清理 localStorage 舊資料。");
}
} else {
console.log("無尋著任何愛徙竇个舊資料。");
}
}

// --- 核心資料管理 ---

function getUserData(key, defaultValue = null) {
if (localUserDataCache.hasOwnProperty(key)) {
return localUserDataCache[key];
}
// 在 Firebase 資料載入前,或徙竇過程中,回退到 localStorage
if (!isFirebaseDataLoaded) {
const localValue = localStorage.getItem(key);
if (localValue !== null) {
try {
return JSON.parse(localValue);
} catch(e) {
return localValue;
}
}
}
return defaultValue;
}

function setUserData(key, value) {
localUserDataCache[key] = value;
if (userRef) {
userRef.child(key).set(value).catch(error => {
console.error(`同步資料到 Firebase 失敗 (key: ${key}):`, error);
});
}
}

// --- 書籤 (Bookmarks) 相關功能 ---

function getBookmarks() {
return getUserData('hakkaBookmarks', []);
}

function saveBookmark(rowId, percentage, category, tableName) {
let bookmarks = getBookmarks();
const newBookmark = {
rowId: rowId,
percentage: percentage,
cat: category,
tableName: tableName,
timestamp: Date.now(),
};

const existingIndex = bookmarks.findIndex(
(bm) => bm.tableName === newBookmark.tableName && bm.cat === newBookmark.cat
);
if (existingIndex > -1) {
bookmarks.splice(existingIndex, 1);
}
bookmarks.unshift(newBookmark);

if (bookmarks.length > 10) {
let indexToDelete = -1;
for (let i = bookmarks.length - 1; i >= 1; i--) {
if (
bookmarks[i].tableName === newBookmark.tableName &&
bookmarks[i].cat !== newBookmark.cat
) {
indexToDelete = i;
break;
}
}
if (indexToDelete > -1) {
bookmarks.splice(indexToDelete, 1);
} else {
bookmarks.pop();
}
}

setUserData('hakkaBookmarks', bookmarks);
}

function removeBookmarkForCompletedCategory(tableName, category) {
let bookmarks = getBookmarks();
const indexToRemove = bookmarks.findIndex(
(bm) => bm.tableName === tableName && bm.cat === category
);

if (indexToRemove > -1) {
console.log(`移除已完成類別个書籤: ${tableName} - ${category}`);
bookmarks.splice(indexToRemove, 1);
setUserData('hakkaBookmarks', bookmarks);
}
}
Loading