diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 419ab6f..df0507c 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -15,7 +15,31 @@ "Bash(./drama issue)", "Bash(./drama report \"UI設計缺漏嚴重性評估\")", "Bash(./drama compliance)", - "Bash(./drama report analysis \"文檔分類組織結構優化\")" + "Bash(./drama report analysis \"文檔分類組織結構優化\")", + "Bash(git push:*)", + "Bash(flutter:*)", + "Bash(mkdir:*)", + "Bash(echo $PATH)", + "Bash(/Users/jettcheng1018/flutter/flutter_3.24.5/bin/flutter --version)", + "Read(//Users/jettcheng1018/flutter/**)", + "Bash(/Users/jettcheng1018/flutter/bin/flutter --version)", + "Bash(/Users/jettcheng1018/flutter/bin/flutter pub get)", + "Bash(/Users/jettcheng1018/flutter/bin/flutter doctor)", + "Bash(/Users/jettcheng1018/flutter/bin/flutter run -d chrome --web-port=8080)", + "Bash(/Users/jettcheng1018/flutter/bin/flutter clean)", + "Read(//Users/jettcheng1018/**)", + "Bash(./drama:*)", + "Bash(./dl)", + "Bash(./dl project list)", + "Bash(./dl status)", + "Bash(./dl project types)", + "Bash(./dl phase status)", + "Bash(./dl project help)", + "Bash(./dl phase help)", + "Bash(./dl phase list)", + "Bash(rm:*)", + "Bash(git add:*)", + "Bash(git rm:*)" ], "deny": [], "ask": [] diff --git a/CLAUDE-協作指南.md b/CLAUDE-協作指南.md deleted file mode 100644 index ea6d9d5..0000000 --- a/CLAUDE-協作指南.md +++ /dev/null @@ -1,103 +0,0 @@ -# 🤖 與 Claude 協作指南 - -## 🎯 目標 -確保 Claude 在協助開發時發現的所有問題都被記錄到問題管理系統中。 - -## 📋 每次請 Claude 協助時的提醒詞 - -### 🔥 標準提醒語句: -``` -"如果你在過程中發現任何規格不確定、衝突、技術問題或需要決策的地方,請使用問題管理系統記錄下來。" -``` - -### 💫 簡短版本: -``` -"遇到問題就記錄到問題系統" -``` - -### 🎯 具體場景提醒: - -**實作功能時:** -``` -"實作 [功能名稱],發現問題就用 ./issue.sh 記錄" -``` - -**檢查文檔時:** -``` -"檢查 [文檔],找到不一致或不清楚的地方就記錄問題" -``` - -**重構程式時:** -``` -"重構 [模組],遇到架構問題或技術債務就記錄" -``` - -## 🔄 Claude 應該記錄的問題類型 - -### 🔥 緊急問題 -- 架構設計衝突 -- 無法實作的需求 -- 安全性問題 -- 資料不一致 - -### ⚠️ 重要問題 -- 規格定義模糊 -- API 設計不確定 -- UI/UX 流程不清楚 -- 技術選型疑慮 - -### 📝 一般問題 -- 文檔格式不統一 -- 命名規範不一致 -- 小的技術改進建議 -- 程式碼品質提升 - -## 📝 任務完成後的檢查清單 - -每次 Claude 完成任務後,請檢查: - -- [ ] Claude 有沒有提到任何「不確定」、「需要澄清」的地方? -- [ ] 有沒有發現文檔間的衝突? -- [ ] 有沒有提到技術實作的困難? -- [ ] 有沒有建議需要進一步決策的事項? - -**如果有,就提醒:** "把剛才提到的問題記錄到問題系統" - -## 🎯 協作流程範例 - -### 範例1:實作功能 -``` -您: "實作用戶登入功能,遇到問題就記錄" -Claude: "好的,我發現API規格中密碼驗證流程不明確..." -您: "把這個記錄到問題系統" -Claude: [使用 ./issue.sh 記錄] -``` - -### 範例2:文檔檢查 -``` -您: "檢查API文檔一致性,發現問題就用問題系統記錄" -Claude: "我發現用戶管理API和認證API的錯誤碼定義衝突..." -Claude: [自動使用 ./issue.sh 記錄問題] -``` - -## 💡 讓協作更順暢的技巧 - -### 🏷️ 在任務開始時就說明: -"我希望你把發現的所有問題都記錄下來,這樣我們就不會遺漏任何需要解決的事項。" - -### 🔄 定期檢查: -每週問 Claude:"最近有沒有發現什麼新的問題需要記錄?" - -### 📊 任務總結: -"總結一下這次任務中發現的問題,並確保都記錄了。" - -## 🎉 效益 - -✅ **不會遺漏問題** - 所有發現的問題都被記錄 -✅ **追蹤更完整** - 包含 AI 協助時發現的問題 -✅ **決策有依據** - 問題記錄成為決策參考 -✅ **開發更順暢** - 提前發現潛在問題 - ---- - -**💫 記住:Claude 是您的協作夥伴,讓他幫您記錄問題,讓專案更完善!** \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index c1dfb21..f5cf7a1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,33 +2,40 @@ ## 🤖 Claude 協作標準操作程序 -本文件為 Claude AI 助手在 Drama Ling 專案中的標準操作程序,確保工作流程的一致性和品質。 +本文件為 Claude AI 助手在 Drama Ling 專案中的標準操作程序和協作指南。 -## 🛠️ 必須使用的系統工具 +## 🛠️ 系統工具使用 -### 報告建立 +### 專案執行管理 (新增 2025-09-08) ```bash -# ✅ 正確做法:使用系統工具 -./drama report analysis "分析主題" -./drama report decision "決策主題" - -# ❌ 禁止行為:手動創建報告檔案 -# 直接 Write 或 Edit reports/ 目錄下的檔案 +# ✅ 正確做法:使用專案管理工具 +./dl project list # 列出所有專案 +./dl phase status # 查看階段狀態 +./dl status # 查看執行狀態 ``` ### 問題管理 ```bash -# ✅ 正確做法:使用問題管理工具 -./drama issue +# ✅ 正確做法:使用問題管理工具 +./dl issue # 互動式問題管理 # ❌ 禁止行為:直接編輯 ISSUES.md # 除非是修正現有問題的格式錯誤 ``` +### 報告建立 +```bash +# ✅ 正確做法:使用系統工具 +./dl report analysis "分析主題" +./dl report decision "決策主題" + +# ❌ 禁止行為:手動創建報告檔案 +``` + ### 檢查作業 ```bash # ✅ 正確做法:使用檢查工具 -./drama check +./dl check # 其他維護腳本 ./check_consistency.sh @@ -38,35 +45,101 @@ ### 1. 工具優先原則 - **必須優先使用現有工具和腳本** -- 手動操作只能用於緊急修正 -- 所有報告都必須透過 `./drama report` 創建 +- 所有專案管理都透過 `./dl` 系統 +- 所有問題記錄都透過 `./dl issue` 創建 +- 所有報告都必須透過 `./dl report` 創建 -### 2. 日期準確性原則 +### 2. 專案管理整合原則 (新增 2025-09-08) +- **階段化執行**: 大型項目拆分為可管理的階段 +- **任務分類**: 使用類型標記(FE/BE/AI/MB/DOC/ENV/TEST) +- **進度追蹤**: 即時更新任務狀態 (⏳ → 🔄 → ✅) +- **依賴管理**: 自動檢查前置條件 + +### 3. 協作提醒原則 (整合 2025-09-08) +用戶可以使用以下提醒語句確保問題被記錄: + +**標準提醒語句:** +``` +"如果你在過程中發現任何規格不確定、衝突、技術問題或需要決策的地方,請使用問題管理系統記錄下來。" +``` + +**簡短版本:** +``` +"遇到問題就記錄到問題系統" +"發現問題就用 ./dl issue 記錄" +``` + +**具體場景提醒:** +``` +"實作 [功能名稱],發現問題就用 ./dl issue 記錄" +"檢查 [文檔],找到不一致或不清楚的地方就記錄問題" +"重構 [模組],遇到架構問題或技術債務就記錄" +``` + +### 4. 日期準確性原則 - **系統工具會自動處理日期** - 當前日期:2025-09-08 - 任何手動設定日期都必須使用正確的當前日期 -### 3. 文檔整合原則 +### 5. 文檔整合原則 - 所有問題必須記錄到 ISSUES.md - 所有分析必須產生正式報告 - 報告必須與問題系統整合 +## 📋 Claude 應該記錄的問題類型 + +### 🔥 緊急問題 +- 架構設計衝突 +- 無法實作的需求 +- 安全性問題 +- 資料不一致 + +### ⚠️ 重要問題 +- 規格定義模糊 +- API 設計不確定 +- UI/UX 流程不清楚 +- 技術選型疑慮 + +### 📝 一般問題 +- 文檔格式不統一 +- 命名規範不一致 +- 小的技術改進建議 +- 程式碼品質提升 + ## 📋 標準工作流程 +### 專案任務執行流程 (新增 2025-09-08) + +#### 任務執行步驟 +1. 用戶提供任務名稱(如:"Android Studio 安裝和配置") +2. Claude 識別任務類型、階段、專案歸屬 +3. 執行相關工作 +4. 自動更新 PROJECTS.md 狀態 (⏳ → 🔄 → ✅) +5. 記錄執行結果和發現的問題 + +#### 專案管理互動範例 +``` +用戶: "執行 Android Studio 安裝和配置" +Claude: 識別為 ENV 類型任務,屬於階段1環境配置 + [執行安裝配置工作] + [更新 PROJECTS.md 狀態] + [報告完成情況] +``` + ### 分析任務流程 -1. **建立分析報告**: `./drama report analysis "主題"` +1. **建立分析報告**: `./dl report analysis "主題"` 2. **執行分析工作**: 使用適當工具進行分析 3. **更新報告內容**: 編輯生成的報告檔案 4. **整合問題系統**: 確認相關問題已正確連結 ### 問題處理流程 -1. **記錄問題**: `./drama issue` +1. **記錄問題**: `./dl issue` 2. **分配優先級**: 🔥緊急 / ⚠️重要 / 📝一般 3. **建立相關報告**: 如有需要,建立分析或決策報告 4. **追蹤解決進展**: 定期更新問題狀態 ### 檢查作業流程 -1. **執行系統檢查**: `./drama check` +1. **執行系統檢查**: `./dl check` 2. **運行一致性檢查**: `./check_consistency.sh` 3. **記錄發現問題**: 使用問題管理系統 4. **產生檢查報告**: 必要時建立分析報告 @@ -75,7 +148,7 @@ ### 錯誤1: 手動創建報告 **問題**: 直接創建報告檔案,導致日期錯誤、格式不一致 -**解決**: 必須使用 `./drama report` 命令 +**解決**: 必須使用 `./dl report` 命令 ### 錯誤2: 忽略現有工具 **問題**: 重複實作已存在的功能 @@ -83,7 +156,7 @@ ### 錯誤3: 未整合問題系統 **問題**: 發現問題但未記錄到 ISSUES.md -**解決**: 每次發現問題都必須使用 `./drama issue` +**解決**: 每次發現問題都必須使用 `./dl issue` ### 錯誤4: 日期不一致 **問題**: 使用錯誤的日期或格式 @@ -92,12 +165,6 @@ ### 錯誤5: 文檔更新缺少時間戳記 (新增 2025-09-08) **問題**: 更新文檔內容後未標記更新時間,難以追蹤變更歷史 **解決**: 任何文檔更新都必須加入時間戳記,格式為 (YYYY-MM-DD) -**範例**: -``` -- [x] 任務完成 ✅ (2025-09-08) -📊 **進度更新**: 已完成19個UI (2025-09-08) -### 新增功能 (新增 2025-09-08) -``` ## 🔍 品質檢查清單 @@ -105,16 +172,21 @@ **⚠️ 重要:任何操作前都必須執行此檢查清單** #### 問題管理操作前檢查 -- [ ] 是否需要記錄新問題?如是,**必須使用** `./drama issue` +- [ ] 是否需要記錄新問題?如是,**必須使用** `./dl issue` - [ ] 完成的問題是否要標記?如是,**絕對不可在待處理區標記[x]** - [ ] 完成的問題**必須移動到「📚 已完成歷史」對應日期區域** - [ ] 移動時**必須保留所有解決詳情和連結** #### 報告建立操作前檢查 -- [ ] 是否需要建立報告?如是,**必須使用** `./drama report analysis "主題"` +- [ ] 是否需要建立報告?如是,**必須使用** `./dl report analysis "主題"` - [ ] **禁止手動創建** reports/ 目錄下的任何檔案 - [ ] 報告主題描述是否具體明確? +#### 專案管理操作前檢查 (新增 2025-09-08) +- [ ] 專案任務是否需要更新狀態? +- [ ] 任務類型是否正確識別(FE/BE/AI/MB/DOC/ENV/TEST)? +- [ ] 是否需要建議下一步行動? + #### 檔案操作前檢查 - [ ] 檔案編碼是否設定為 UTF-8? - [ ] 中文內容是否正確顯示? @@ -133,12 +205,49 @@ ### 每次任務完成後檢查 - [ ] 是否使用了正確的系統工具? - [ ] 所有日期是否正確(2025-09-08)? +- [ ] 任務狀態是否已更新為完成 ✅ (專案任務) - [ ] 發現的問題是否已記錄? - [ ] 報告是否已正確整合到問題系統? - [ ] 檔案命名是否符合系統標準? - [ ] **ISSUES.md中完成的項目是否已正確移動到歷史區域?** - [ ] **所有文檔更新是否都加入了時間戳記?** +## 📝 任務完成後的檢查清單 (整合 2025-09-08) + +每次 Claude 完成任務後,請檢查: + +- [ ] Claude 有沒有提到任何「不確定」、「需要澄清」的地方? +- [ ] 有沒有發現文檔間的衝突? +- [ ] 有沒有提到技術實作的困難? +- [ ] 有沒有建議需要進一步決策的事項? + +**如果有,就提醒:** "把剛才提到的問題記錄到問題系統" + +## 🎯 協作流程範例 + +### 範例1:專案任務執行 (新增 2025-09-08) +``` +用戶: "請執行 Flutter移動端配置調整" +Claude: 識別為 MB 類型任務,更新狀態為進行中... + [執行配置調整] + ✅ 任務完成,狀態已更新 (2025-09-08) +``` + +### 範例2:發現問題並記錄 +``` +用戶: "實作語音輸入功能,遇到問題就記錄" +Claude: 我發現API規格中音頻格式支援不明確... + [使用 ./dl issue 記錄問題] + 已記錄問題:音頻格式規格不明確 ⚠️ +``` + +### 範例3:文檔檢查 +``` +用戶: "檢查API文檔一致性,發現問題就用問題系統記錄" +Claude: 我發現用戶管理API和認證API的錯誤碼定義衝突... + [自動使用 ./dl issue 記錄問題] +``` + ## 🚨 緊急情況處理 ### 工具故障時 @@ -153,9 +262,21 @@ 3. 記錄發現的不一致問題 4. 修正問題後繼續工作 +## 💡 讓協作更順暢的技巧 (整合 2025-09-08) + +### 🏷️ 在任務開始時就說明: +"我希望你把發現的所有問題都記錄下來,這樣我們就不會遺漏任何需要解決的事項。" + +### 🔄 定期檢查: +每週問 Claude:"最近有沒有發現什麼新的問題需要記錄?" + +### 📊 任務總結: +"總結一下這次任務中發現的問題,並確保都記錄了。" + ## 📚 相關文檔 - [問題追蹤系統](./ISSUES.md) +- [專案執行管理](./PROJECTS.md) - [工具使用說明](./tools/) - [報告模板](./reports/templates/) - [檢查腳本](./scripts/) @@ -174,11 +295,7 @@ 3. 修訂本指南文檔 4. 確保向下相容性 ---- - -**重要提醒**: 本指南是 Claude AI 助手的強制性操作標準。任何偏離此流程的行為都可能造成系統不一致和品質問題。 - -## 🤝 標準化指令格式(方案A) +## 🤝 標準化指令格式 ### 推薦指令格式 ``` @@ -187,13 +304,28 @@ ### 範例指令 ``` -請分析UI設計問題,遵循SOP,記得使用./drama report analysis建立報告 +請分析UI設計問題,遵循SOP,記得使用./dl report analysis建立報告 請處理緊急問題,遵循SOP,完成後問題要移到歷史區域不可標記[x] 請建立新的API文檔,遵循SOP,使用正確日期和UTF-8編碼 +執行 Android Studio 安裝和配置,遇到問題就記錄 ``` +## 🎉 效益 + +✅ **不會遺漏問題** - 所有發現的問題都被記錄 +✅ **追蹤更完整** - 包含 AI 協助時發現的問題 +✅ **決策有依據** - 問題記錄成為決策參考 +✅ **開發更順暢** - 提前發現潛在問題 +✅ **專案管理清晰** - 階段化執行,進度透明 (新增 2025-09-08) + +--- + +**重要提醒**: 本指南是 Claude AI 助手的強制性操作標準。任何偏離此流程的行為都可能造成系統不一致和品質問題。 + +**💫 記住:Claude 是您的協作夥伴,讓他幫您記錄問題和管理專案,讓開發更完善!** + --- **最後更新**: 2025-09-08 -**版本**: 2.1 - 加入文檔更新時間戳記強制性要求 (2025-09-08) +**版本**: 3.0 - 整合專案管理系統和協作指南 (2025-09-08) **維護者**: Drama Ling 開發團隊 \ No newline at end of file diff --git a/PROJECTS.md b/PROJECTS.md new file mode 100644 index 0000000..fd47eac --- /dev/null +++ b/PROJECTS.md @@ -0,0 +1,84 @@ +# 專案執行管理系統 + +## 📋 執行管理概述 + +本系統用於管理大型計劃的分階段執行,支援項目分解、進度追蹤和依賴關係管理。 + +**管理原則**: +- **階段化執行**: 大型項目拆分為可管理的階段 +- **類型標注**: 每個執行項目標記具體類型 +- **進度透明**: 即時追蹤執行狀態和阻塞點 +- **依賴管理**: 自動檢查前置條件和依賴關係 + +## 🏷️ 項目類型定義 + +| 類型代碼 | 圖標 | 名稱 | 說明 | +|---------|------|------|------| +| `FE` | 🎨 | 前端開發 | Flutter UI/UX 開發 | +| `BE` | ⚙️ | 後端開發 | .NET Core API 開發 | +| `AI` | 🤖 | AI整合 | 語音、評分、對話系統 | +| `MB` | 📱 | 移動端 | Android/iOS 配置打包 | +| `DOC` | 📚 | 文檔更新 | 規格、API、使用文檔 | +| `ENV` | 🔧 | 環境配置 | 開發工具、部署環境 | +| `TEST` | 🧪 | 測試驗證 | 功能測試、整合測試 | + +## 📊 執行狀態定義 + +- ⏳ **待執行** (pending): 尚未開始的項目 +- 🔄 **進行中** (in-progress): 正在執行的項目 +- ✅ **已完成** (completed): 成功完成的項目 +- ❌ **已阻塞** (blocked): 遇到阻礙無法繼續的項目 +- ⏸️ **已暫停** (paused): 暫時停止的項目 + +## 📁 當前執行項目 + +### 📱 Drama Ling 手機APP開發 (2025-09-08) + +**項目描述**: 建立完整的手機APP,實現AI驅動的語言學習功能 + +**當前階段**: 🔄 環境配置中 + +#### 階段1: 環境配置 🔄 +- ⏳ `ENV` Android Studio 安裝和配置 +- ⏳ `ENV` Xcode 安裝配置 (iOS支援) +- ⏳ `ENV` Android模擬器設置 +- ⏳ `MB` Flutter移動端配置調整 + +#### 階段2: APP打包 ⏳ +- ⏳ `MB` Android APK 生成配置 +- ⏳ `FE` 應用圖標和啟動畫面設計 +- ⏳ `MB` APP權限配置 (語音、網路) +- ⏳ `TEST` 真實設備測試 + +#### 階段3: 核心功能實現 ⏳ +- ⏳ `AI` 語音輸入功能實現 +- ⏳ `FE` 觸控操作優化 +- ⏳ `AI` 三維度評分系統 (語法、語意、流暢度) +- ⏳ `AI` 劇本對話系統 +- ⏳ `AI` 詞彙學習關卡系統 +- ⏳ `AI` 限時挑戰系統 (300秒) + +#### 階段4: 整合測試 ⏳ +- ⏳ `TEST` 功能整合測試 +- ⏳ `TEST` 效能優化測試 +- ⏳ `DOC` 使用說明文檔 +- ⏳ `MB` 正式版本打包 + +--- + +## 📈 項目統計 + +**總計項目**: 1 個 +**進行中**: 1 個 +**已完成**: 0 個 + +**執行項目統計**: +- ⏳ 待執行: 16 個 +- 🔄 進行中: 0 個 +- ✅ 已完成: 0 個 + +--- + +**最後更新**: 2025-09-08 +**管理工具**: `./dl project`, `./dl phase` +**系統版本**: 1.0.0 \ No newline at end of file diff --git a/README-問題管理.md b/README-問題管理.md deleted file mode 100644 index 7354b23..0000000 --- a/README-問題管理.md +++ /dev/null @@ -1,142 +0,0 @@ -# 🚨 Drama Ling 問題管理系統 - -## 🎯 統一入口點 - -### 🎭 **主命令** (推薦) -```bash -./drama # 顯示所有可用命令 -./drama issue # 管理問題 -./drama check # 檢查問題狀態 -./drama report "標題" # 建立分析報告 -./drama consistency # 一致性檢查 -./drama all # 執行全部檢查 -``` - -## 💫 其他使用方式 - -### 1️⃣ **直接使用工具** (進階) -```bash -./tools/issue.sh # 直接使用問題管理工具 -./tools/check_issues.sh # 直接檢查問題狀態 -``` - -### 2️⃣ **VS Code 快捷鍵** (推薦進階使用者) -- `Cmd+Shift+I` - 📝 記錄問題 -- `Cmd+Shift+S` - 📊 查看狀態 -- `Cmd+Shift+C` - 🔍 一致性檢查 -- `Cmd+Shift+A` - 🚀 全部檢查 - -### 3️⃣ **全域命令** (最方便) -```bash -# 先執行一次設置 -./drama setup - -# 重啟終端機後,在任何地方都能用: -dl # 主選單 -dl-issue # 記錄問題 -dl-check # 查看狀態 -dl-report # 建立報告 -dl-consistency # 一致性檢查 -dl-all # 全部檢查 -``` - -## 🚀 快速開始 - -### 第一次使用: -1. 打開終端機 -2. `cd /Users/jettcheng1018/code/dramaling-app` -3. `./drama` 查看所有命令 -4. `./drama issue` 開始管理問題 - -### 日常使用: -```bash -# 有問題時 -./drama issue → 選1 → 輸入問題 → 選優先級 - -# 想查狀態時 -./drama check - -# 建立分析報告 -./drama report "問題分析標題" - -# 執行系統檢查 -./drama all -``` - -## 📋 問題優先級 - -- 🔥 **緊急** - 阻擋開發的嚴重問題 -- ⚠️ **重要** - 影響進度的重要問題 -- 📝 **一般** - 可以延後的問題 - -## ✅ 解決問題 - -**方法1 - 用工具:** -`./issue.sh` → 選3 → 查看問題列表 → 手動編輯檔案 - -**方法2 - 直接編輯:** -打開 `ISSUES.md`,把 `[ ]` 改成 `[x]`,移到「已解決」區域 - -## 🔧 故障排除 - -### 權限問題: -```bash -chmod +x issue.sh -chmod +x setup_aliases.sh -``` - -### 找不到檔案: -```bash -# 確認在正確目錄 -pwd -# 應該顯示: /Users/jettcheng1018/code/dramaling-app -``` - -### VS Code 快捷鍵無效: -1. 重啟 VS Code -2. 檢查是否在專案根目錄開啟 VS Code - -## 💡 使用技巧 - -### 快速記錄: -- 發現問題立即記錄,不要拖延 -- 描述要具體,包含檔案位置 -- 優先級要準確判斷 - -### 定期回顧: -- 每週檢查一次狀態 -- 將已解決問題移到完成區域 -- 評估緊急問題是否需要立即處理 - -### 團隊協作: -- 問題描述要清楚,讓其他人也能理解 -- 相關檔案路徑要完整 -- 解決後記錄解決方案 - -## 📁 相關檔案 - -### 核心系統 -- `drama` - 🎭 統一入口點腳本 -- `ISSUES.md` - 主要問題追蹤檔案 -- `reports/` - 結構化報告目錄 - -### 工具目錄 -- `tools/issue.sh` - 互動式問題管理工具 -- `tools/check_issues.sh` - 快速狀態檢查 -- `tools/create_report.sh` - 快速建立報告工具 -- `tools/check_reports.sh` - 報告狀態檢查 -- `tools/setup_aliases.sh` - 全域命令設置 - -### VS Code 整合 -- `.vscode/tasks.json` - VS Code 任務設定 -- `.vscode/keybindings.json` - VS Code 快捷鍵 - -### 維護系統 -- `scripts/maintenance_manager.sh` - 系統檢查主腳本 -- `scripts/maintenance/` - 各種檢查腳本目錄 - ---- - -**🎉 現在您有了一個超級簡單好用的問題管理系統!** - -遇到問題就記錄,定期檢查狀態,讓專案開發更順暢! \ No newline at end of file diff --git a/drama b/dl similarity index 74% rename from drama rename to dl index 18ab84e..c735d12 100755 --- a/drama +++ b/dl @@ -15,7 +15,7 @@ NC='\033[0m' # 顯示主選單 show_menu() { - echo -e "${BLUE}🎭 Drama Ling 專案管理工具${NC}" + echo -e "${BLUE}🎭 Drama Ling 管理工具${NC}" echo "==================================" echo "" echo -e "${PURPLE}📋 問題管理${NC}" @@ -27,6 +27,11 @@ show_menu() { echo " decision - 建立決策記錄" echo " reports - 檢查報告狀態" echo "" + echo -e "${PURPLE}🚀 專案執行管理${NC}" + echo " project - 專案管理" + echo " phase - 階段管理" + echo " status - 查看執行狀態" + echo "" echo -e "${PURPLE}🔧 系統檢查${NC}" echo " consistency - 執行一致性檢查" echo " compliance - 執行合規性檢查" @@ -37,9 +42,10 @@ show_menu() { echo " help - 顯示此幫助" echo "" echo -e "${BLUE}範例:${NC}" - echo " ./drama issue # 管理問題" - echo " ./drama report \"API分析\" # 建立分析報告" - echo " ./drama check # 檢查問題狀態" + echo " ./dl issue # 管理問題" + echo " ./dl report \"API分析\" # 建立分析報告" + echo " ./dl project list # 列出所有專案" + echo " ./dl phase status # 查看階段狀態" } # 主邏輯 @@ -61,6 +67,17 @@ case "$1" in "reports") exec "$TOOLS_DIR/check_reports.sh" ;; + "project") + shift + exec "$TOOLS_DIR/project.sh" "$@" + ;; + "phase") + shift + exec "$TOOLS_DIR/phase.sh" "$@" + ;; + "status") + exec "$TOOLS_DIR/project.sh" status + ;; "consistency") exec "$SCRIPT_DIR/scripts/maintenance_manager.sh" consistency ;; diff --git a/projects/templates/basic_project.md b/projects/templates/basic_project.md new file mode 100644 index 0000000..886eedc --- /dev/null +++ b/projects/templates/basic_project.md @@ -0,0 +1,57 @@ +# {{PROJECT_NAME}} + +**創建日期**: {{CREATION_DATE}} +**狀態**: 🔄 進行中 + +## 專案描述 +請在此處添加專案的詳細描述... + +## 專案目標 +- [ ] 目標1:待定義 +- [ ] 目標2:待定義 +- [ ] 目標3:待定義 + +## 執行階段 + +### 階段1: 待定義 ⏳ +- ⏳ `ENV` 待添加執行項目... + - 預計完成時間:待定 + - 負責人:待定 + - 依賴關係:無 + +## 專案資源 + +### 參考文檔 +- [ ] 需求規格文檔 +- [ ] 技術架構文檔 +- [ ] API規格文檔 + +### 相關連結 +- 專案倉庫:待定 +- 設計稿:待定 +- 測試環境:待定 + +## 風險評估 + +### 潛在風險 +1. **技術風險**:待評估 +2. **時程風險**:待評估 +3. **資源風險**:待評估 + +### 緩解措施 +- 待制定具體緩解策略 + +## 專案統計 +- **總階段數**: 1 +- **執行項目數**: 1 +- **完成進度**: 0% +- **預計完成時間**: 待定 + +## 變更紀錄 +| 日期 | 變更內容 | 變更原因 | +|------|----------|----------| +| {{CREATION_DATE}} | 專案初始化 | 新專案建立 | + +--- +**最後更新**: {{CREATION_DATE}} +**維護人**: 待指定 \ No newline at end of file diff --git a/projects/templates/mobile_app_project.md b/projects/templates/mobile_app_project.md new file mode 100644 index 0000000..00ca0a1 --- /dev/null +++ b/projects/templates/mobile_app_project.md @@ -0,0 +1,74 @@ +# {{PROJECT_NAME}} + +**創建日期**: {{CREATION_DATE}} +**專案類型**: 📱 移動端應用開發 +**狀態**: 🔄 進行中 + +## 專案描述 +移動端應用開發專案,包含完整的開發生命週期。 + +## 專案目標 +- [ ] 建立完整的移動端開發環境 +- [ ] 實現核心功能模組 +- [ ] 完成應用打包和發布 +- [ ] 建立測試和部署流程 + +## 執行階段 + +### 階段1: 環境配置 ⏳ +- ⏳ `ENV` Android Studio 安裝和配置 +- ⏳ `ENV` iOS開發環境設置 (如需要) +- ⏳ `ENV` 模擬器和真機調試環境 +- ⏳ `MB` 專案配置文件調整 + +### 階段2: UI/UX開發 ⏳ +- ⏳ `FE` 主要頁面UI實現 +- ⏳ `FE` 響應式設計適配 +- ⏳ `FE` 主題和樣式系統 +- ⏳ `FE` 用戶交互優化 + +### 階段3: 核心功能開發 ⏳ +- ⏳ `BE` API整合 +- ⏳ `FE` 核心業務邏輯 +- ⏳ `TEST` 功能模組測試 +- ⏳ `DOC` 開發文檔更新 + +### 階段4: 打包發布 ⏳ +- ⏳ `MB` 應用圖標和啟動畫面 +- ⏳ `MB` 權限配置和安全設置 +- ⏳ `MB` 正式版本打包 +- ⏳ `TEST` 發布前完整測試 + +## 技術規格 + +### 開發工具 +- IDE: Android Studio / VS Code +- 框架: Flutter / React Native / 原生開發 +- 版本控制: Git + +### 目標平台 +- [ ] Android (API 21+) +- [ ] iOS (iOS 12+) + +### 性能目標 +- 啟動時間 < 3秒 +- 頁面切換響應 < 500ms +- 記憶體使用 < 100MB + +## 專案統計 +- **總階段數**: 4 +- **執行項目數**: 16 +- **完成進度**: 0% +- **預計完成時間**: 待評估 + +## 品質檢查清單 +- [ ] 代碼審查完成 +- [ ] 單元測試覆蓋率 > 80% +- [ ] 整合測試通過 +- [ ] 性能測試通過 +- [ ] 安全性檢查完成 +- [ ] 用戶體驗測試完成 + +--- +**最後更新**: {{CREATION_DATE}} +**維護人**: 開發團隊 \ No newline at end of file diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..00617a6 --- /dev/null +++ b/src/README.md @@ -0,0 +1,181 @@ +# Drama Ling - 專案架構 + +這是Drama Ling語言學習應用的主要專案資料夾,包含前端和後端代碼。 + +## 專案結構 + +``` +src/ +├── backend/ # .NET Core Web API 後端 +│ ├── DramaLing.API/ # Web API 專案 +│ ├── DramaLing.Application/ # 應用服務層 +│ ├── DramaLing.Core/ # 領域模型層 +│ ├── DramaLing.Infrastructure/ # 基礎設施層 +│ ├── DramaLing.Tests/ # 測試專案 +│ └── DramaLing.sln # 解決方案檔 +├── mobile/ # Flutter 移動應用 +│ ├── lib/ +│ │ ├── core/ # 核心功能 (常數、工具、服務) +│ │ ├── features/ # 功能模組 (認證、學習、對話等) +│ │ └── shared/ # 共用組件 (Widget、模型、Provider) +│ └── pubspec.yaml # Flutter 專案配置 +└── docker-compose.yml # Docker 開發環境配置 +``` + +## 技術棧 + +### 後端 (.NET Core) +- **.NET 8**: 最新的跨平台框架 +- **ASP.NET Core Web API**: RESTful API 服務 +- **Entity Framework Core**: ORM 資料庫存取 +- **PostgreSQL**: 主要資料庫 +- **Redis**: 快取和會話管理 +- **JWT**: 身份驗證 +- **Swagger/OpenAPI**: API 文檔 +- **Serilog**: 結構化日誌 + +### 前端 (Flutter) +- **Flutter 3.16+**: 跨平台移動應用框架 +- **Dart 3.0+**: 程式語言 +- **Riverpod**: 狀態管理 +- **Go Router**: 導航路由 +- **Dio + Retrofit**: HTTP 客戶端 +- **Hive**: 本地資料存儲 +- **Material 3**: UI 設計系統 + +## 開發環境設置 + +### 必要工具 +- [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) +- [Flutter 3.16+](https://flutter.dev/docs/get-started/install) +- [Docker Desktop](https://www.docker.com/products/docker-desktop) +- [PostgreSQL](https://www.postgresql.org/download/) (或使用Docker) + +### 快速開始 + +#### 1. 啟動開發環境 +```bash +# 啟動資料庫和Redis +cd src +docker-compose up -d postgres redis + +# 或者啟動完整環境包含API +docker-compose up -d +``` + +#### 2. 後端開發 +```bash +cd src/backend + +# 還原套件 +dotnet restore + +# 建立資料庫 +dotnet ef database update --project DramaLing.Infrastructure --startup-project DramaLing.API + +# 啟動API服務 +dotnet run --project DramaLing.API + +# API將在 http://localhost:5000 啟動 +# Swagger UI: http://localhost:5000 +``` + +#### 3. 前端開發 +```bash +cd src/mobile + +# 安裝套件 +flutter pub get + +# 程式碼生成 (Riverpod, Retrofit 等) +dart run build_runner build + +# 啟動Flutter應用 (需要模擬器或實體裝置) +flutter run +``` + +## API 文檔 + +- **開發環境**: http://localhost:5000 +- **Swagger UI**: http://localhost:5000 (開發模式下) +- **API 規格文檔**: `../../docs/04_technical/api/` + +## 資料庫 + +### 連線資訊 (開發環境) +- **主機**: localhost +- **埠號**: 5432 +- **資料庫**: dramaling_dev +- **使用者**: postgres +- **密碼**: password + +### 遷移指令 +```bash +# 建立遷移 +dotnet ef migrations add --project DramaLing.Infrastructure --startup-project DramaLing.API + +# 更新資料庫 +dotnet ef database update --project DramaLing.Infrastructure --startup-project DramaLing.API + +# 移除最後一個遷移 +dotnet ef migrations remove --project DramaLing.Infrastructure --startup-project DramaLing.API +``` + +## 測試 + +### 後端測試 +```bash +cd src/backend +dotnet test +``` + +### 前端測試 +```bash +cd src/mobile +flutter test +``` + +## 部署 + +### 生產環境建構 +```bash +# 後端 +cd src/backend +dotnet publish DramaLing.API -c Release -o publish + +# 前端 +cd src/mobile +flutter build apk --release # Android +flutter build ios --release # iOS +``` + +## 開發指南 + +### 架構原則 +- **Clean Architecture**: 分層架構,依賴倒置 +- **CQRS**: 命令查詢職責分離 (使用 MediatR) +- **Feature-based**: 按功能模組組織程式碼 +- **API-first**: API 優先設計 + +### 編碼規範 +- **後端**: 遵循 C# 編碼慣例 +- **前端**: 遵循 Dart/Flutter 編碼慣例 +- **命名**: 英文命名,中文註釋 +- **測試**: 單元測試覆蓋率 > 80% + +## 相關文檔 + +- [API 規格文檔](../../docs/04_technical/api/) +- [資料庫設計](../../docs/04_technical/database-schema.md) +- [技術選型決策](../../docs/04_technical/tech-stack-decision.md) +- [開發工作流程](../../docs/03_development/development-workflow.md) + +## 問題回報 + +如有問題請參考: +- [ISSUES.md](../../ISSUES.md) +- [問題管理系統](../../README-問題管理.md) + +## 授權 + +版權所有 © 2025 Drama Ling Team \ No newline at end of file diff --git a/src/backend/DramaLing.API/Controllers/HealthController.cs b/src/backend/DramaLing.API/Controllers/HealthController.cs new file mode 100644 index 0000000..a80aa52 --- /dev/null +++ b/src/backend/DramaLing.API/Controllers/HealthController.cs @@ -0,0 +1,58 @@ +using Microsoft.AspNetCore.Mvc; + +namespace DramaLing.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class HealthController : ControllerBase +{ + /// + /// 健康檢查端點 + /// + /// 服務健康狀態 + [HttpGet] + public IActionResult GetHealth() + { + var response = new + { + Status = "Healthy", + Timestamp = DateTime.UtcNow, + Version = "1.0.0", + Service = "Drama Ling API" + }; + + return Ok(response); + } + + /// + /// 詳細健康檢查 + /// + /// 詳細的系統狀態 + [HttpGet("detailed")] + public IActionResult GetDetailedHealth() + { + var response = new + { + Status = "Healthy", + Timestamp = DateTime.UtcNow, + Version = "1.0.0", + Service = "Drama Ling API", + Environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production", + Uptime = Environment.TickCount64, + Database = "Connected", // TODO: 實際檢查資料庫連線 + Cache = "Connected", // TODO: 實際檢查Redis連線 + Memory = new + { + WorkingSet = GC.GetTotalMemory(false), + GcCollections = new + { + Gen0 = GC.CollectionCount(0), + Gen1 = GC.CollectionCount(1), + Gen2 = GC.CollectionCount(2) + } + } + }; + + return Ok(response); + } +} \ No newline at end of file diff --git a/src/backend/DramaLing.API/Dockerfile.dev b/src/backend/DramaLing.API/Dockerfile.dev new file mode 100644 index 0000000..e262659 --- /dev/null +++ b/src/backend/DramaLing.API/Dockerfile.dev @@ -0,0 +1,35 @@ +# Development Dockerfile for .NET API +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +WORKDIR /app +EXPOSE 5000 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /src + +# Copy project files +COPY ["DramaLing.API/DramaLing.API.csproj", "DramaLing.API/"] +COPY ["DramaLing.Application/DramaLing.Application.csproj", "DramaLing.Application/"] +COPY ["DramaLing.Core/DramaLing.Core.csproj", "DramaLing.Core/"] +COPY ["DramaLing.Infrastructure/DramaLing.Infrastructure.csproj", "DramaLing.Infrastructure/"] + +# Restore dependencies +RUN dotnet restore "DramaLing.API/DramaLing.API.csproj" + +# Copy source code +COPY . . + +# Build the application +WORKDIR "/src/DramaLing.API" +RUN dotnet build "DramaLing.API.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "DramaLing.API.csproj" -c Release -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . + +# Create logs directory +RUN mkdir -p /app/logs + +ENTRYPOINT ["dotnet", "DramaLing.API.dll"] \ No newline at end of file diff --git a/src/backend/DramaLing.API/DramaLing.API.csproj b/src/backend/DramaLing.API/DramaLing.API.csproj new file mode 100644 index 0000000..640f44d --- /dev/null +++ b/src/backend/DramaLing.API/DramaLing.API.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + enable + enable + true + bin\Debug\net8.0\DramaLing.API.xml + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/backend/DramaLing.API/Program.cs b/src/backend/DramaLing.API/Program.cs new file mode 100644 index 0000000..982a8e3 --- /dev/null +++ b/src/backend/DramaLing.API/Program.cs @@ -0,0 +1,116 @@ +using DramaLing.Application; +using DramaLing.Infrastructure; +using Microsoft.OpenApi.Models; +using Serilog; + +var builder = WebApplication.CreateBuilder(args); + +// Configure Serilog +Log.Logger = new LoggerConfiguration() + .ReadFrom.Configuration(builder.Configuration) + .CreateLogger(); + +builder.Host.UseSerilog(); + +// Add services to the container. +builder.Services.AddApplication(); +builder.Services.AddInfrastructure(builder.Configuration); + +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); + +// Configure Swagger/OpenAPI +builder.Services.AddSwaggerGen(c => +{ + c.SwaggerDoc("v1", new OpenApiInfo + { + Title = "Drama Ling API", + Version = "v1", + Description = "API for Drama Ling language learning application", + Contact = new OpenApiContact + { + Name = "Drama Ling Team", + Email = "dev@dramaling.com" + } + }); + + // Configure JWT authentication in Swagger + c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + Description = "JWT Authorization header using the Bearer scheme. Example: \"Bearer {token}\"", + Name = "Authorization", + In = ParameterLocation.Header, + Type = SecuritySchemeType.ApiKey, + Scheme = "Bearer" + }); + + c.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + Array.Empty() + } + }); + + // Include XML comments + var xmlFile = Path.Combine(AppContext.BaseDirectory, "DramaLing.API.xml"); + if (File.Exists(xmlFile)) + { + c.IncludeXmlComments(xmlFile); + } +}); + +// Configure CORS +builder.Services.AddCors(options => +{ + options.AddPolicy("DramaLingPolicy", policy => + { + policy.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); + }); +}); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.SwaggerEndpoint("/swagger/v1/swagger.json", "Drama Ling API v1"); + c.RoutePrefix = string.Empty; // Serve Swagger UI at the app's root + }); +} + +app.UseHttpsRedirection(); +app.UseCors("DramaLingPolicy"); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapControllers(); + +app.MapGet("/health", () => new { Status = "Healthy", Timestamp = DateTime.UtcNow }); + +try +{ + Log.Information("Starting Drama Ling API"); + app.Run(); +} +catch (Exception ex) +{ + Log.Fatal(ex, "Application terminated unexpectedly"); +} +finally +{ + Log.CloseAndFlush(); +} \ No newline at end of file diff --git a/src/backend/DramaLing.API/appsettings.Development.json b/src/backend/DramaLing.API/appsettings.Development.json new file mode 100644 index 0000000..091c869 --- /dev/null +++ b/src/backend/DramaLing.API/appsettings.Development.json @@ -0,0 +1,18 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.EntityFrameworkCore.Database.Command": "Information" + } + }, + "ConnectionStrings": { + "DefaultConnection": "Host=localhost;Port=5432;Database=dramaling_dev;Username=postgres;Password=password" + }, + "JwtSettings": { + "Key": "development-key-256-bits-long-for-jwt-signing-purpose-only", + "Issuer": "DramaLingAPI-Dev", + "Audience": "DramaLingUsers-Dev", + "DurationInMinutes": 1440 + } +} \ No newline at end of file diff --git a/src/backend/DramaLing.API/appsettings.json b/src/backend/DramaLing.API/appsettings.json new file mode 100644 index 0000000..4295764 --- /dev/null +++ b/src/backend/DramaLing.API/appsettings.json @@ -0,0 +1,53 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Serilog": { + "Using": ["Serilog.Sinks.Console", "Serilog.Sinks.File"], + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "System": "Warning" + } + }, + "WriteTo": [ + { + "Name": "Console", + "Args": { + "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}" + } + }, + { + "Name": "File", + "Args": { + "path": "logs/dramaling-.txt", + "rollingInterval": "Day", + "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}" + } + } + ] + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "Host=localhost;Port=5432;Database=dramaling_dev;Username=postgres;Password=password" + }, + "JwtSettings": { + "Key": "your-256-bit-secret-key-here-must-be-at-least-32-characters", + "Issuer": "DramaLingAPI", + "Audience": "DramaLingUsers", + "DurationInMinutes": 60 + }, + "OpenAI": { + "ApiKey": "your-openai-api-key-here", + "Model": "gpt-4o-mini", + "MaxTokens": 1000, + "Temperature": 0.7 + }, + "Redis": { + "ConnectionString": "localhost:6379" + } +} \ No newline at end of file diff --git a/src/backend/DramaLing.Application/DependencyInjection.cs b/src/backend/DramaLing.Application/DependencyInjection.cs new file mode 100644 index 0000000..f4c8818 --- /dev/null +++ b/src/backend/DramaLing.Application/DependencyInjection.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; +using System.Reflection; + +namespace DramaLing.Application; + +public static class DependencyInjection +{ + public static IServiceCollection AddApplication(this IServiceCollection services) + { + services.AddMediatR(cfg => + cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly())); + + services.AddAutoMapper(Assembly.GetExecutingAssembly()); + + return services; + } +} \ No newline at end of file diff --git a/src/backend/DramaLing.Application/DramaLing.Application.csproj b/src/backend/DramaLing.Application/DramaLing.Application.csproj new file mode 100644 index 0000000..8d00418 --- /dev/null +++ b/src/backend/DramaLing.Application/DramaLing.Application.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/backend/DramaLing.Core/DramaLing.Core.csproj b/src/backend/DramaLing.Core/DramaLing.Core.csproj new file mode 100644 index 0000000..e1581e1 --- /dev/null +++ b/src/backend/DramaLing.Core/DramaLing.Core.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + \ No newline at end of file diff --git a/src/backend/DramaLing.Core/Entities/BaseEntity.cs b/src/backend/DramaLing.Core/Entities/BaseEntity.cs new file mode 100644 index 0000000..050fccb --- /dev/null +++ b/src/backend/DramaLing.Core/Entities/BaseEntity.cs @@ -0,0 +1,9 @@ +namespace DramaLing.Core.Entities; + +public abstract class BaseEntity +{ + public Guid Id { get; set; } = Guid.NewGuid(); + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + public bool IsDeleted { get; set; } = false; +} \ No newline at end of file diff --git a/src/backend/DramaLing.Core/Entities/Gamification.cs b/src/backend/DramaLing.Core/Entities/Gamification.cs new file mode 100644 index 0000000..9bbd087 --- /dev/null +++ b/src/backend/DramaLing.Core/Entities/Gamification.cs @@ -0,0 +1,94 @@ +using DramaLing.Core.Enums; + +namespace DramaLing.Core.Entities; + +public class Achievement : BaseEntity +{ + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public string IconUrl { get; set; } = string.Empty; + public AchievementType Type { get; set; } + public int DiamondReward { get; set; } = 0; + public int ExperienceReward { get; set; } = 0; + public string? BadgeUrl { get; set; } + public bool IsActive { get; set; } = true; + + // Navigation Properties + public virtual ICollection UserAchievements { get; set; } = new List(); +} + +public class UserAchievement : BaseEntity +{ + public Guid UserId { get; set; } + public Guid AchievementId { get; set; } + public DateTime AchievedAt { get; set; } = DateTime.UtcNow; + public bool IsRewardClaimed { get; set; } = false; + public DateTime? RewardClaimedAt { get; set; } + + // Navigation Properties + public virtual User User { get; set; } = null!; + public virtual Achievement Achievement { get; set; } = null!; +} + +public class DailyMission : BaseEntity +{ + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public string IconUrl { get; set; } = string.Empty; + public string Type { get; set; } = string.Empty; // vocabulary_recognition, dialogue_training, etc. + public int TargetValue { get; set; } = 1; + public string Unit { get; set; } = "次"; + public int ExperienceReward { get; set; } = 50; + public int DiamondReward { get; set; } = 0; + public bool IsActive { get; set; } = true; + + // Navigation Properties + public virtual ICollection UserDailyMissions { get; set; } = new List(); +} + +public class UserDailyMission : BaseEntity +{ + public Guid UserId { get; set; } + public Guid DailyMissionId { get; set; } + public DateTime MissionDate { get; set; } + public int CurrentValue { get; set; } = 0; + public int TargetValue { get; set; } = 1; + public bool IsCompleted { get; set; } = false; + public DateTime? CompletedAt { get; set; } + public bool IsRewardClaimed { get; set; } = false; + public DateTime? RewardClaimedAt { get; set; } + + // Navigation Properties + public virtual User User { get; set; } = null!; + public virtual DailyMission DailyMission { get; set; } = null!; +} + +public class TimeWarpChallenge : BaseEntity +{ + public Guid UserId { get; set; } + public Guid ScenarioId { get; set; } + public DateTime StartedAt { get; set; } + public DateTime? CompletedAt { get; set; } + public int TimeLimit { get; set; } = 300; // seconds + public int TimeUsed { get; set; } = 0; // seconds + public bool IsCompleted { get; set; } = false; + public int Score { get; set; } = 0; + public int ExperienceGained { get; set; } = 0; + public int DiamondGained { get; set; } = 0; + + // Navigation Properties + public virtual User User { get; set; } = null!; + public virtual Scenario Scenario { get; set; } = null!; +} + +public class UserLifePoint : BaseEntity +{ + public Guid UserId { get; set; } + public int CurrentLifePoints { get; set; } = 5; + public int MaxLifePoints { get; set; } = 5; + public DateTime? NextRecoveryAt { get; set; } + public DateTime? LastUsedAt { get; set; } + + // Navigation Properties + public virtual User User { get; set; } = null!; +} \ No newline at end of file diff --git a/src/backend/DramaLing.Core/Entities/LearningStage.cs b/src/backend/DramaLing.Core/Entities/LearningStage.cs new file mode 100644 index 0000000..2df4b46 --- /dev/null +++ b/src/backend/DramaLing.Core/Entities/LearningStage.cs @@ -0,0 +1,81 @@ +namespace DramaLing.Core.Entities; + +public class LearningStage : BaseEntity +{ + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public int Order { get; set; } + public string Level { get; set; } = string.Empty; // A1, A2, B1, etc. + public bool IsActive { get; set; } = true; + + // Navigation Properties + public virtual ICollection Scenarios { get; set; } = new List(); +} + +public class Scenario : BaseEntity +{ + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public string Context { get; set; } = string.Empty; + public string Objective { get; set; } = string.Empty; + public string Level { get; set; } = string.Empty; + public int EstimatedMinutes { get; set; } + public bool IsActive { get; set; } = true; + + // Foreign Key + public Guid LearningStageId { get; set; } + + // Navigation Properties + public virtual LearningStage LearningStage { get; set; } = null!; + public virtual ICollection TargetVocabularies { get; set; } = new List(); + public virtual ICollection UserProgresses { get; set; } = new List(); +} + +public class Vocabulary : BaseEntity +{ + public string Word { get; set; } = string.Empty; + public string Translation { get; set; } = string.Empty; + public string Pronunciation { get; set; } = string.Empty; + public string Definition { get; set; } = string.Empty; + public string ExampleSentence { get; set; } = string.Empty; + public string Level { get; set; } = string.Empty; + public string Category { get; set; } = string.Empty; + public int Frequency { get; set; } = 0; // Word frequency ranking + + // Navigation Properties + public virtual ICollection Scenarios { get; set; } = new List(); + public virtual ICollection VocabularyProgresses { get; set; } = new List(); +} + +public class UserProgress : BaseEntity +{ + public Guid UserId { get; set; } + public Guid ScenarioId { get; set; } + public int Score { get; set; } + public int Stars { get; set; } // 0-3 stars + public bool IsCompleted { get; set; } + public DateTime? CompletedAt { get; set; } + public int AttemptCount { get; set; } = 0; + public string? FeedbackData { get; set; } // JSON data + + // Navigation Properties + public virtual User User { get; set; } = null!; + public virtual Scenario Scenario { get; set; } = null!; +} + +public class VocabularyProgress : BaseEntity +{ + public Guid UserId { get; set; } + public Guid VocabularyId { get; set; } + public int LearningStage { get; set; } = 1; // 1=Recognition, 2=Familiarity, 3=DialogueApplication + public int MasteryLevel { get; set; } = 0; // 0-100 + public DateTime? NextReviewAt { get; set; } + public int ReviewCount { get; set; } = 0; + public int CorrectCount { get; set; } = 0; + public int IncorrectCount { get; set; } = 0; + public DateTime? LastReviewedAt { get; set; } + + // Navigation Properties + public virtual User User { get; set; } = null!; + public virtual Vocabulary Vocabulary { get; set; } = null!; +} \ No newline at end of file diff --git a/src/backend/DramaLing.Core/Entities/User.cs b/src/backend/DramaLing.Core/Entities/User.cs new file mode 100644 index 0000000..39dbb21 --- /dev/null +++ b/src/backend/DramaLing.Core/Entities/User.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Identity; + +namespace DramaLing.Core.Entities; + +public class User : IdentityUser +{ + public string DisplayName { get; set; } = string.Empty; + public string? AvatarUrl { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + public DateTime? LastLoginAt { get; set; } + public bool IsDeleted { get; set; } = false; + + // Language Learning Properties + public string CurrentLanguage { get; set; } = "en"; // ISO 639-1 code + public string NativeLanguage { get; set; } = "zh"; // ISO 639-1 code + public string CurrentLevel { get; set; } = "A1"; // CEFR level + public int TotalExperience { get; set; } = 0; + public int Diamonds { get; set; } = 0; + public int LightningEnergy { get; set; } = 0; + public int LifePoints { get; set; } = 5; + public int MaxLifePoints { get; set; } = 5; + public DateTime? NextLifePointRecovery { get; set; } + + // Subscription + public bool IsVipUser { get; set; } = false; + public DateTime? VipExpiresAt { get; set; } + + // Learning Statistics + public int ConsecutiveDays { get; set; } = 0; + public DateTime? LastLearningDate { get; set; } + public int TotalDialoguesCompleted { get; set; } = 0; + public int TotalVocabularyMastered { get; set; } = 0; + + // Navigation Properties + public virtual ICollection UserProgresses { get; set; } = new List(); + public virtual ICollection UserAchievements { get; set; } = new List(); + public virtual ICollection VocabularyProgresses { get; set; } = new List(); +} \ No newline at end of file diff --git a/src/backend/DramaLing.Core/Enums/LearningStage.cs b/src/backend/DramaLing.Core/Enums/LearningStage.cs new file mode 100644 index 0000000..9e543aa --- /dev/null +++ b/src/backend/DramaLing.Core/Enums/LearningStage.cs @@ -0,0 +1,31 @@ +namespace DramaLing.Core.Enums; + +public enum LearningStage +{ + Recognition = 1, // 詞彙認識 + Familiarity = 2, // 詞彙熟悉 + DialogueApplication = 3 // 對話應用 +} + +public enum DifficultyLevel +{ + A1 = 1, + A2 = 2, + B1 = 3, + B2 = 4, + C1 = 5, + C2 = 6 +} + +public enum AchievementType +{ + PassReward = 1, // 過關獎勵 + PerfectGrammar = 2, // 完美語法 + FluentExpression = 3, // 表達流利 + StoryMaster = 4, // 劇情大師 + VocabularyExpert = 5, // 詞彙專家 + PerfectDialogue = 6, // 完美對話 + SmartLearner = 7, // 智慧學習者 + IndependentProgress = 8, // 獨立進步 + TranslationMaster = 9 // 翻譯達人 +} \ No newline at end of file diff --git a/src/backend/DramaLing.Infrastructure/Data/ApplicationDbContext.cs b/src/backend/DramaLing.Infrastructure/Data/ApplicationDbContext.cs new file mode 100644 index 0000000..dce0316 --- /dev/null +++ b/src/backend/DramaLing.Infrastructure/Data/ApplicationDbContext.cs @@ -0,0 +1,99 @@ +using DramaLing.Core.Entities; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace DramaLing.Infrastructure.Data; + +public class ApplicationDbContext : IdentityDbContext, Guid> +{ + public ApplicationDbContext(DbContextOptions options) : base(options) + { + } + + // Learning Content + public DbSet LearningStages { get; set; } + public DbSet Scenarios { get; set; } + public DbSet Vocabularies { get; set; } + + // User Progress + public DbSet UserProgresses { get; set; } + public DbSet VocabularyProgresses { get; set; } + + // Gamification + public DbSet Achievements { get; set; } + public DbSet UserAchievements { get; set; } + public DbSet DailyMissions { get; set; } + public DbSet UserDailyMissions { get; set; } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + // Configure Identity tables to use Guid + builder.Entity(entity => + { + entity.ToTable("Users"); + entity.HasKey(e => e.Id); + + // Configure indexes + entity.HasIndex(e => e.Email).IsUnique(); + entity.HasIndex(e => e.UserName).IsUnique(); + + // Configure properties + entity.Property(e => e.DisplayName).HasMaxLength(100).IsRequired(); + entity.Property(e => e.CurrentLanguage).HasMaxLength(5).IsRequired(); + entity.Property(e => e.NativeLanguage).HasMaxLength(5).IsRequired(); + entity.Property(e => e.CurrentLevel).HasMaxLength(5).IsRequired(); + }); + + builder.Entity>(entity => + { + entity.ToTable("Roles"); + }); + + builder.Entity>(entity => + { + entity.ToTable("UserRoles"); + }); + + builder.Entity>(entity => + { + entity.ToTable("UserClaims"); + }); + + builder.Entity>(entity => + { + entity.ToTable("UserLogins"); + }); + + builder.Entity>(entity => + { + entity.ToTable("RoleClaims"); + }); + + builder.Entity>(entity => + { + entity.ToTable("UserTokens"); + }); + + // Configure soft delete + builder.Entity() + .HasQueryFilter(e => !e.IsDeleted); + } + + public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + foreach (var entry in ChangeTracker.Entries()) + { + switch (entry.State) + { + case EntityState.Modified: + entry.Entity.UpdatedAt = DateTime.UtcNow; + break; + } + } + + return await base.SaveChangesAsync(cancellationToken); + } +} \ No newline at end of file diff --git a/src/backend/DramaLing.Infrastructure/DependencyInjection.cs b/src/backend/DramaLing.Infrastructure/DependencyInjection.cs new file mode 100644 index 0000000..dda64d7 --- /dev/null +++ b/src/backend/DramaLing.Infrastructure/DependencyInjection.cs @@ -0,0 +1,81 @@ +using DramaLing.Core.Entities; +using DramaLing.Infrastructure.Data; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; +using StackExchange.Redis; +using System.Text; + +namespace DramaLing.Infrastructure; + +public static class DependencyInjection +{ + public static IServiceCollection AddInfrastructure( + this IServiceCollection services, + IConfiguration configuration) + { + // Database + services.AddDbContext(options => + options.UseNpgsql(configuration.GetConnectionString("DefaultConnection"))); + + // Identity + services.AddIdentity>(options => + { + // Password settings + options.Password.RequireDigit = true; + options.Password.RequireUppercase = true; + options.Password.RequiredLength = 8; + options.Password.RequireNonAlphanumeric = false; + + // Lockout settings + options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5); + options.Lockout.MaxFailedAccessAttempts = 5; + options.Lockout.AllowedForNewUsers = true; + + // User settings + options.User.RequireUniqueEmail = true; + options.SignIn.RequireConfirmedEmail = false; + }) + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + + // JWT Authentication + var jwtSettings = configuration.GetSection("JwtSettings"); + var key = Encoding.ASCII.GetBytes(jwtSettings["Key"] ?? throw new InvalidOperationException("JWT Key not found")); + + services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(options => + { + options.RequireHttpsMetadata = false; + options.SaveToken = true; + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(key), + ValidateIssuer = true, + ValidIssuer = jwtSettings["Issuer"], + ValidateAudience = true, + ValidAudience = jwtSettings["Audience"], + ValidateLifetime = true, + ClockSkew = TimeSpan.Zero + }; + }); + + // Redis + var redisConnection = configuration.GetConnectionString("Redis"); + if (!string.IsNullOrEmpty(redisConnection)) + { + services.AddSingleton(sp => + ConnectionMultiplexer.Connect(redisConnection)); + } + + return services; + } +} \ No newline at end of file diff --git a/src/backend/DramaLing.Infrastructure/DramaLing.Infrastructure.csproj b/src/backend/DramaLing.Infrastructure/DramaLing.Infrastructure.csproj new file mode 100644 index 0000000..37fc596 --- /dev/null +++ b/src/backend/DramaLing.Infrastructure/DramaLing.Infrastructure.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/backend/DramaLing.sln b/src/backend/DramaLing.sln new file mode 100644 index 0000000..dd52391 --- /dev/null +++ b/src/backend/DramaLing.sln @@ -0,0 +1,48 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DramaLing.API", "DramaLing.API\DramaLing.API.csproj", "{8A7E8B45-1234-4567-8901-234567890123}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DramaLing.Core", "DramaLing.Core\DramaLing.Core.csproj", "{8A7E8B45-1234-4567-8901-234567890124}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DramaLing.Infrastructure", "DramaLing.Infrastructure\DramaLing.Infrastructure.csproj", "{8A7E8B45-1234-4567-8901-234567890125}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DramaLing.Application", "DramaLing.Application\DramaLing.Application.csproj", "{8A7E8B45-1234-4567-8901-234567890126}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DramaLing.Tests", "DramaLing.Tests\DramaLing.Tests.csproj", "{8A7E8B45-1234-4567-8901-234567890127}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8A7E8B45-1234-4567-8901-234567890123}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A7E8B45-1234-4567-8901-234567890123}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A7E8B45-1234-4567-8901-234567890123}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A7E8B45-1234-4567-8901-234567890123}.Release|Any CPU.Build.0 = Release|Any CPU + {8A7E8B45-1234-4567-8901-234567890124}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A7E8B45-1234-4567-8901-234567890124}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A7E8B45-1234-4567-8901-234567890124}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A7E8B45-1234-4567-8901-234567890124}.Release|Any CPU.Build.0 = Release|Any CPU + {8A7E8B45-1234-4567-8901-234567890125}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A7E8B45-1234-4567-8901-234567890125}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A7E8B45-1234-4567-8901-234567890125}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A7E8B45-1234-4567-8901-234567890125}.Release|Any CPU.Build.0 = Release|Any CPU + {8A7E8B45-1234-4567-8901-234567890126}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A7E8B45-1234-4567-8901-234567890126}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A7E8B45-1234-4567-8901-234567890126}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A7E8B45-1234-4567-8901-234567890126}.Release|Any CPU.Build.0 = Release|Any CPU + {8A7E8B45-1234-4567-8901-234567890127}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A7E8B45-1234-4567-8901-234567890127}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A7E8B45-1234-4567-8901-234567890127}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A7E8B45-1234-4567-8901-234567890127}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {12345678-1234-5678-9012-123456789012} + EndGlobalSection +EndGlobal \ No newline at end of file diff --git a/src/docker-compose.yml b/src/docker-compose.yml new file mode 100644 index 0000000..20355ea --- /dev/null +++ b/src/docker-compose.yml @@ -0,0 +1,60 @@ +version: '3.8' + +services: + # PostgreSQL Database + postgres: + image: postgres:15-alpine + container_name: dramaling_postgres + environment: + POSTGRES_DB: dramaling_dev + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + - ./backend/scripts/init.sql:/docker-entrypoint-initdb.d/init.sql + networks: + - dramaling_network + + # Redis Cache + redis: + image: redis:7-alpine + container_name: dramaling_redis + ports: + - "6379:6379" + volumes: + - redis_data:/data + command: redis-server --appendonly yes + networks: + - dramaling_network + + # .NET API (Development) + api: + build: + context: ./backend + dockerfile: DramaLing.API/Dockerfile.dev + container_name: dramaling_api + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ASPNETCORE_URLS=http://+:5000 + - ConnectionStrings__DefaultConnection=Host=postgres;Port=5432;Database=dramaling_dev;Username=postgres;Password=password + - ConnectionStrings__Redis=redis:6379 + ports: + - "5000:5000" + depends_on: + - postgres + - redis + volumes: + - ./backend:/app + networks: + - dramaling_network + restart: unless-stopped + +volumes: + postgres_data: + redis_data: + +networks: + dramaling_network: + driver: bridge \ No newline at end of file diff --git a/src/mobile/.gitignore b/src/mobile/.gitignore new file mode 100644 index 0000000..3820a95 --- /dev/null +++ b/src/mobile/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/src/mobile/.metadata b/src/mobile/.metadata new file mode 100644 index 0000000..8c395df --- /dev/null +++ b/src/mobile/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "a402d9a4376add5bc2d6b1e33e53edaae58c07f8" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8 + base_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8 + - platform: web + create_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8 + base_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/src/mobile/README.md b/src/mobile/README.md new file mode 100644 index 0000000..186d07c --- /dev/null +++ b/src/mobile/README.md @@ -0,0 +1,16 @@ +# dramaling + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/src/mobile/analysis_options.yaml b/src/mobile/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/src/mobile/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/src/mobile/lib/core/constants/app_constants.dart b/src/mobile/lib/core/constants/app_constants.dart new file mode 100644 index 0000000..305f2be --- /dev/null +++ b/src/mobile/lib/core/constants/app_constants.dart @@ -0,0 +1,83 @@ +class AppConstants { + // App Info + static const String appName = 'Drama Ling'; + static const String appVersion = '1.0.0'; + + // API Configuration + static const String baseUrl = 'https://api.dramaling.com'; + static const String apiVersion = 'v1'; + static const String apiBaseUrl = '$baseUrl/api/$apiVersion'; + + // Local API for Development + static const String localBaseUrl = 'http://localhost:5000'; + static const String localApiBaseUrl = '$localBaseUrl/api/$apiVersion'; + + // Storage Keys + static const String accessTokenKey = 'access_token'; + static const String refreshTokenKey = 'refresh_token'; + static const String userDataKey = 'user_data'; + static const String languageKey = 'language'; + static const String themeKey = 'theme'; + + // Learning Constants + static const int maxLifePoints = 5; + static const int lifePointRecoveryHours = 5; + static const int dailyExperienceBonus = 50; + + // Dialogue Constants + static const int maxDialogueTurns = 12; + static const int dialogueTimeoutSeconds = 600; // 10 minutes + static const double passingScore = 60.0; + static const double excellentScore = 90.0; + + // Time Challenge Constants + static const int timeChallengeSeconds = 300; // 5 minutes + static const int timeWarningSeconds = 60; + static const int timeCriticalSeconds = 30; + + // Animation Durations + static const Duration shortAnimation = Duration(milliseconds: 200); + static const Duration normalAnimation = Duration(milliseconds: 300); + static const Duration longAnimation = Duration(milliseconds: 500); + + // Network + static const Duration connectionTimeout = Duration(seconds: 30); + static const Duration receiveTimeout = Duration(seconds: 30); + + // Pagination + static const int defaultPageSize = 20; + static const int maxPageSize = 100; +} + +class AppStrings { + // Common + static const String ok = '確定'; + static const String cancel = '取消'; + static const String retry = '重試'; + static const String loading = '載入中...'; + static const String error = '錯誤'; + static const String success = '成功'; + + // Authentication + static const String login = '登入'; + static const String register = '註冊'; + static const String logout = '登出'; + static const String email = '電子郵件'; + static const String password = '密碼'; + static const String confirmPassword = '確認密碼'; + static const String forgotPassword = '忘記密碼?'; + + // Learning + static const String startLearning = '開始學習'; + static const String continueDialogue = '繼續對話'; + static const String vocabularyPractice = '詞彙練習'; + static const String dialoguePractice = '對話練習'; + static const String timeChallenge = '限時挑戰'; + + // Errors + static const String networkError = '網路連線錯誤'; + static const String serverError = '伺服器錯誤'; + static const String unknownError = '未知錯誤'; + static const String invalidCredentials = '帳號或密碼錯誤'; + static const String sessionExpired = '登入已過期,請重新登入'; +} \ No newline at end of file diff --git a/src/mobile/lib/core/services/storage_service.dart b/src/mobile/lib/core/services/storage_service.dart new file mode 100644 index 0000000..2133be0 --- /dev/null +++ b/src/mobile/lib/core/services/storage_service.dart @@ -0,0 +1,125 @@ +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +class StorageService { + static late Box _box; + static const FlutterSecureStorage _secureStorage = FlutterSecureStorage( + aOptions: AndroidOptions( + encryptedSharedPreferences: true, + ), + iOptions: IOSOptions( + accessibility: KeychainAccessibility.first_unlock_this_device, + ), + ); + + /// 初始化儲存服務 + static Future init() async { + await Hive.initFlutter(); + _box = await Hive.openBox('dramaling_storage'); + } + + // 一般資料存取 (使用Hive) + + /// 儲存資料 + static Future setData(String key, T value) async { + await _box.put(key, value); + } + + /// 獲取資料 + static T? getData(String key) { + return _box.get(key) as T?; + } + + /// 移除資料 + static Future removeData(String key) async { + await _box.delete(key); + } + + /// 清除所有資料 + static Future clearAll() async { + await _box.clear(); + } + + /// 檢查資料是否存在 + static bool hasData(String key) { + return _box.containsKey(key); + } + + // 安全資料存取 (使用FlutterSecureStorage) + + /// 安全儲存資料 (用於敏感資料如token) + static Future setSecureData(String key, String value) async { + await _secureStorage.write(key: key, value: value); + } + + /// 獲取安全資料 + static Future getSecureData(String key) async { + return await _secureStorage.read(key: key); + } + + /// 移除安全資料 + static Future removeSecureData(String key) async { + await _secureStorage.delete(key: key); + } + + /// 清除所有安全資料 + static Future clearSecureData() async { + await _secureStorage.deleteAll(); + } + + /// 檢查安全資料是否存在 + static Future hasSecureData(String key) async { + final data = await _secureStorage.read(key: key); + return data != null; + } + + // 便捷方法 + + /// 儲存用戶Token + static Future saveTokens({ + required String accessToken, + required String refreshToken, + }) async { + await setSecureData('access_token', accessToken); + await setSecureData('refresh_token', refreshToken); + } + + /// 獲取存取Token + static Future getAccessToken() async { + return await getSecureData('access_token'); + } + + /// 獲取刷新Token + static Future getRefreshToken() async { + return await getSecureData('refresh_token'); + } + + /// 清除所有Token + static Future clearTokens() async { + await removeSecureData('access_token'); + await removeSecureData('refresh_token'); + } + + /// 儲存用戶偏好設定 + static Future saveUserPreferences({ + String? language, + String? theme, + bool? soundEnabled, + bool? vibrationEnabled, + }) async { + if (language != null) await setData('language', language); + if (theme != null) await setData('theme', theme); + if (soundEnabled != null) await setData('sound_enabled', soundEnabled); + if (vibrationEnabled != null) await setData('vibration_enabled', vibrationEnabled); + } + + /// 獲取用戶偏好設定 + static Map getUserPreferences() { + return { + 'language': getData('language') ?? 'zh', + 'theme': getData('theme') ?? 'system', + 'sound_enabled': getData('sound_enabled') ?? true, + 'vibration_enabled': getData('vibration_enabled') ?? true, + }; + } +} \ No newline at end of file diff --git a/src/mobile/lib/core/utils/app_router.dart b/src/mobile/lib/core/utils/app_router.dart new file mode 100644 index 0000000..08d3b5b --- /dev/null +++ b/src/mobile/lib/core/utils/app_router.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../features/auth/screens/login_screen.dart'; +import '../../features/auth/screens/register_screen.dart'; +import '../../features/learning/screens/home_screen.dart'; +import '../../shared/providers/auth_provider.dart'; + +final routerProvider = Provider((ref) { + final authState = ref.watch(authProvider); + + return GoRouter( + initialLocation: authState.isAuthenticated ? '/home' : '/login', + redirect: (context, state) { + final isAuthenticated = authState.isAuthenticated; + final isAuthRoute = state.uri.path.startsWith('/auth'); + + if (!isAuthenticated && !isAuthRoute) { + return '/login'; + } + + if (isAuthenticated && isAuthRoute) { + return '/home'; + } + + return null; + }, + routes: [ + // Authentication Routes + GoRoute( + path: '/login', + builder: (context, state) => const LoginScreen(), + ), + GoRoute( + path: '/register', + builder: (context, state) => const RegisterScreen(), + ), + + // Main App Routes + GoRoute( + path: '/home', + builder: (context, state) => const HomeScreen(), + ), + + // Learning Routes + GoRoute( + path: '/vocabulary', + builder: (context, state) => const Scaffold( + body: Center(child: Text('詞彙練習頁面')), + ), + ), + GoRoute( + path: '/dialogue', + builder: (context, state) => const Scaffold( + body: Center(child: Text('對話練習頁面')), + ), + ), + GoRoute( + path: '/challenge', + builder: (context, state) => const Scaffold( + body: Center(child: Text('限時挑戰頁面')), + ), + ), + + // Profile Routes + GoRoute( + path: '/profile', + builder: (context, state) => const Scaffold( + body: Center(child: Text('個人檔案頁面')), + ), + ), + ], + + // Error handling + errorBuilder: (context, state) => Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.error_outline, + size: 64, + color: Colors.red, + ), + const SizedBox(height: 16), + Text( + '頁面未找到', + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 8), + Text( + state.error?.toString() ?? '未知錯誤', + style: Theme.of(context).textTheme.bodyMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: () => context.go('/home'), + child: const Text('返回首頁'), + ), + ], + ), + ), + ), + ); +}); \ No newline at end of file diff --git a/src/mobile/lib/core/utils/app_theme.dart b/src/mobile/lib/core/utils/app_theme.dart new file mode 100644 index 0000000..0d5d3a6 --- /dev/null +++ b/src/mobile/lib/core/utils/app_theme.dart @@ -0,0 +1,184 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class AppColors { + // Primary Colors + static const Color primary = Color(0xFF6366F1); // Indigo + static const Color primaryLight = Color(0xFF8B5CF6); // Violet + static const Color primaryDark = Color(0xFF4F46E5); // Dark Indigo + + // Secondary Colors + static const Color secondary = Color(0xFFF59E0B); // Amber + static const Color secondaryLight = Color(0xFFFBBF24); + static const Color secondaryDark = Color(0xFFD97706); + + // Learning Status Colors + static const Color success = Color(0xFF10B981); // Emerald + static const Color warning = Color(0xFFF59E0B); // Amber + static const Color error = Color(0xFFEF4444); // Red + static const Color info = Color(0xFF3B82F6); // Blue + + // Neutral Colors + static const Color white = Color(0xFFFFFFFF); + static const Color black = Color(0xFF000000); + static const Color grey50 = Color(0xFFF9FAFB); + static const Color grey100 = Color(0xFFF3F4F6); + static const Color grey200 = Color(0xFFE5E7EB); + static const Color grey300 = Color(0xFFD1D5DB); + static const Color grey400 = Color(0xFF9CA3AF); + static const Color grey500 = Color(0xFF6B7280); + static const Color grey600 = Color(0xFF4B5563); + static const Color grey700 = Color(0xFF374151); + static const Color grey800 = Color(0xFF1F2937); + static const Color grey900 = Color(0xFF111827); + + // Surface Colors + static const Color surface = Color(0xFFFFFFFF); + static const Color surfaceDark = Color(0xFF1F2937); + static const Color background = Color(0xFFF9FAFB); + static const Color backgroundDark = Color(0xFF111827); +} + +class AppTheme { + static ThemeData get lightTheme { + return ThemeData( + useMaterial3: true, + brightness: Brightness.light, + + // Color Scheme + colorScheme: const ColorScheme.light( + primary: AppColors.primary, + secondary: AppColors.secondary, + surface: AppColors.surface, + background: AppColors.background, + error: AppColors.error, + ), + + // Typography + textTheme: GoogleFonts.notoSansTextTheme().copyWith( + headlineLarge: GoogleFonts.notoSans( + fontSize: 32, + fontWeight: FontWeight.bold, + color: AppColors.grey900, + ), + headlineMedium: GoogleFonts.notoSans( + fontSize: 28, + fontWeight: FontWeight.bold, + color: AppColors.grey900, + ), + titleLarge: GoogleFonts.notoSans( + fontSize: 22, + fontWeight: FontWeight.w600, + color: AppColors.grey900, + ), + titleMedium: GoogleFonts.notoSans( + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.grey700, + ), + bodyLarge: GoogleFonts.notoSans( + fontSize: 16, + fontWeight: FontWeight.normal, + color: AppColors.grey800, + ), + bodyMedium: GoogleFonts.notoSans( + fontSize: 14, + fontWeight: FontWeight.normal, + color: AppColors.grey700, + ), + ), + + // Component Themes + appBarTheme: AppBarTheme( + backgroundColor: AppColors.white, + foregroundColor: AppColors.grey900, + elevation: 0, + centerTitle: true, + titleTextStyle: GoogleFonts.notoSans( + fontSize: 18, + fontWeight: FontWeight.w600, + color: AppColors.grey900, + ), + ), + + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primary, + foregroundColor: AppColors.white, + elevation: 2, + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + + inputDecorationTheme: InputDecorationTheme( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: AppColors.grey300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: AppColors.primary, width: 2), + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + ), + + cardTheme: const CardThemeData( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16)), + ), + ), + ); + } + + static ThemeData get darkTheme { + return ThemeData( + useMaterial3: true, + brightness: Brightness.dark, + + // Color Scheme + colorScheme: const ColorScheme.dark( + primary: AppColors.primaryLight, + secondary: AppColors.secondaryLight, + surface: AppColors.surfaceDark, + background: AppColors.backgroundDark, + error: AppColors.error, + ), + + // Typography + textTheme: GoogleFonts.notoSansTextTheme(ThemeData.dark().textTheme).copyWith( + headlineLarge: GoogleFonts.notoSans( + fontSize: 32, + fontWeight: FontWeight.bold, + color: AppColors.white, + ), + headlineMedium: GoogleFonts.notoSans( + fontSize: 28, + fontWeight: FontWeight.bold, + color: AppColors.white, + ), + titleLarge: GoogleFonts.notoSans( + fontSize: 22, + fontWeight: FontWeight.w600, + color: AppColors.white, + ), + bodyLarge: GoogleFonts.notoSans( + fontSize: 16, + fontWeight: FontWeight.normal, + color: AppColors.grey200, + ), + ), + + // Component Themes + appBarTheme: AppBarTheme( + backgroundColor: AppColors.surfaceDark, + foregroundColor: AppColors.white, + elevation: 0, + centerTitle: true, + ), + ); + } +} \ No newline at end of file diff --git a/src/mobile/lib/features/auth/screens/login_screen.dart b/src/mobile/lib/features/auth/screens/login_screen.dart new file mode 100644 index 0000000..5f6cc05 --- /dev/null +++ b/src/mobile/lib/features/auth/screens/login_screen.dart @@ -0,0 +1,226 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class LoginScreen extends StatefulWidget { + const LoginScreen({super.key}); + + @override + State createState() => _LoginScreenState(); +} + +class _LoginScreenState extends State { + final _formKey = GlobalKey(); + final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); + bool _isLoading = false; + bool _obscurePassword = true; + + @override + void dispose() { + _emailController.dispose(); + _passwordController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24.0), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 60), + + // Logo and Title + Column( + children: [ + Container( + width: 120, + height: 120, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.theater_comedy, + color: Colors.white, + size: 60, + ), + ), + const SizedBox(height: 24), + Text( + 'Drama Ling', + style: Theme.of(context).textTheme.headlineLarge?.copyWith( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + '歡迎回來!開始您的語言學習之旅', + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Theme.of(context).colorScheme.onBackground.withOpacity(0.7), + ), + textAlign: TextAlign.center, + ), + ], + ), + + const SizedBox(height: 48), + + // Email Field + TextFormField( + controller: _emailController, + keyboardType: TextInputType.emailAddress, + decoration: const InputDecoration( + labelText: '電子郵件', + hintText: '請輸入您的電子郵件', + prefixIcon: Icon(Icons.email_outlined), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return '請輸入電子郵件'; + } + if (!value.contains('@')) { + return '請輸入有效的電子郵件格式'; + } + return null; + }, + ), + + const SizedBox(height: 16), + + // Password Field + TextFormField( + controller: _passwordController, + obscureText: _obscurePassword, + decoration: InputDecoration( + labelText: '密碼', + hintText: '請輸入您的密碼', + prefixIcon: const Icon(Icons.lock_outlined), + suffixIcon: IconButton( + icon: Icon( + _obscurePassword + ? Icons.visibility_outlined + : Icons.visibility_off_outlined, + ), + onPressed: () { + setState(() { + _obscurePassword = !_obscurePassword; + }); + }, + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return '請輸入密碼'; + } + if (value.length < 6) { + return '密碼長度至少需要6個字符'; + } + return null; + }, + ), + + const SizedBox(height: 24), + + // Login Button + SizedBox( + height: 48, + child: ElevatedButton( + onPressed: _isLoading ? null : _handleLogin, + child: _isLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Text('登入'), + ), + ), + + const SizedBox(height: 16), + + // Forgot Password + TextButton( + onPressed: () { + // TODO: 實現忘記密碼功能 + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('忘記密碼功能開發中')), + ); + }, + child: const Text('忘記密碼?'), + ), + + const SizedBox(height: 32), + + // Divider + Row( + children: [ + const Expanded(child: Divider()), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + '或', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onBackground.withOpacity(0.5), + ), + ), + ), + const Expanded(child: Divider()), + ], + ), + + const SizedBox(height: 32), + + // Register Button + OutlinedButton( + onPressed: () => context.go('/register'), + child: const Text('建立新帳號'), + ), + ], + ), + ), + ), + ), + ); + } + + Future _handleLogin() async { + if (!_formKey.currentState!.validate()) return; + + setState(() { + _isLoading = true; + }); + + try { + // TODO: 實現實際的登入邏輯 + await Future.delayed(const Duration(seconds: 2)); // 模擬API調用 + + if (mounted) { + // 登入成功,導航到首頁 + context.go('/home'); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('登入成功!')), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('登入失敗:$e')), + ); + } + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } +} \ No newline at end of file diff --git a/src/mobile/lib/features/auth/screens/register_screen.dart b/src/mobile/lib/features/auth/screens/register_screen.dart new file mode 100644 index 0000000..e9bc198 --- /dev/null +++ b/src/mobile/lib/features/auth/screens/register_screen.dart @@ -0,0 +1,311 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class RegisterScreen extends StatefulWidget { + const RegisterScreen({super.key}); + + @override + State createState() => _RegisterScreenState(); +} + +class _RegisterScreenState extends State { + final _formKey = GlobalKey(); + final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); + final _confirmPasswordController = TextEditingController(); + final _displayNameController = TextEditingController(); + bool _isLoading = false; + bool _obscurePassword = true; + bool _obscureConfirmPassword = true; + bool _acceptTerms = false; + + @override + void dispose() { + _emailController.dispose(); + _passwordController.dispose(); + _confirmPasswordController.dispose(); + _displayNameController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => context.go('/login'), + ), + ), + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24.0), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Title + Text( + '建立新帳號', + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + '開始您的語言學習之旅', + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Theme.of(context).colorScheme.onBackground.withOpacity(0.7), + ), + textAlign: TextAlign.center, + ), + + const SizedBox(height: 32), + + // Display Name Field + TextFormField( + controller: _displayNameController, + decoration: const InputDecoration( + labelText: '顯示名稱', + hintText: '請輸入您的顯示名稱', + prefixIcon: Icon(Icons.person_outlined), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return '請輸入顯示名稱'; + } + if (value.length < 2) { + return '顯示名稱至少需要2個字符'; + } + return null; + }, + ), + + const SizedBox(height: 16), + + // Email Field + TextFormField( + controller: _emailController, + keyboardType: TextInputType.emailAddress, + decoration: const InputDecoration( + labelText: '電子郵件', + hintText: '請輸入您的電子郵件', + prefixIcon: Icon(Icons.email_outlined), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return '請輸入電子郵件'; + } + if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) { + return '請輸入有效的電子郵件格式'; + } + return null; + }, + ), + + const SizedBox(height: 16), + + // Password Field + TextFormField( + controller: _passwordController, + obscureText: _obscurePassword, + decoration: InputDecoration( + labelText: '密碼', + hintText: '請輸入密碼(至少8個字符)', + prefixIcon: const Icon(Icons.lock_outlined), + suffixIcon: IconButton( + icon: Icon( + _obscurePassword + ? Icons.visibility_outlined + : Icons.visibility_off_outlined, + ), + onPressed: () { + setState(() { + _obscurePassword = !_obscurePassword; + }); + }, + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return '請輸入密碼'; + } + if (value.length < 8) { + return '密碼長度至少需要8個字符'; + } + if (!RegExp(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)').hasMatch(value)) { + return '密碼需包含大寫字母、小寫字母和數字'; + } + return null; + }, + ), + + const SizedBox(height: 16), + + // Confirm Password Field + TextFormField( + controller: _confirmPasswordController, + obscureText: _obscureConfirmPassword, + decoration: InputDecoration( + labelText: '確認密碼', + hintText: '請再次輸入密碼', + prefixIcon: const Icon(Icons.lock_outlined), + suffixIcon: IconButton( + icon: Icon( + _obscureConfirmPassword + ? Icons.visibility_outlined + : Icons.visibility_off_outlined, + ), + onPressed: () { + setState(() { + _obscureConfirmPassword = !_obscureConfirmPassword; + }); + }, + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return '請確認密碼'; + } + if (value != _passwordController.text) { + return '密碼不一致'; + } + return null; + }, + ), + + const SizedBox(height: 24), + + // Terms and Conditions + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Checkbox( + value: _acceptTerms, + onChanged: (value) { + setState(() { + _acceptTerms = value ?? false; + }); + }, + ), + Expanded( + child: GestureDetector( + onTap: () { + setState(() { + _acceptTerms = !_acceptTerms; + }); + }, + child: Padding( + padding: const EdgeInsets.only(top: 12), + child: RichText( + text: TextSpan( + style: Theme.of(context).textTheme.bodyMedium, + children: [ + const TextSpan(text: '我同意'), + TextSpan( + text: '服務條款', + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + decoration: TextDecoration.underline, + ), + ), + const TextSpan(text: '和'), + TextSpan( + text: '隱私政策', + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + decoration: TextDecoration.underline, + ), + ), + ], + ), + ), + ), + ), + ), + ], + ), + + const SizedBox(height: 24), + + // Register Button + SizedBox( + height: 48, + child: ElevatedButton( + onPressed: (_isLoading || !_acceptTerms) ? null : _handleRegister, + child: _isLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Text('註冊'), + ), + ), + + const SizedBox(height: 16), + + // Back to Login + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('已經有帳號了?'), + TextButton( + onPressed: () => context.go('/login'), + child: const Text('立即登入'), + ), + ], + ), + ], + ), + ), + ), + ), + ); + } + + Future _handleRegister() async { + if (!_formKey.currentState!.validate()) return; + + if (!_acceptTerms) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('請同意服務條款和隱私政策')), + ); + return; + } + + setState(() { + _isLoading = true; + }); + + try { + // TODO: 實現實際的註冊邏輯 + await Future.delayed(const Duration(seconds: 2)); // 模擬API調用 + + if (mounted) { + // 註冊成功,導航到首頁 + context.go('/home'); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('註冊成功!歡迎使用Drama Ling')), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('註冊失敗:$e')), + ); + } + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } +} \ No newline at end of file diff --git a/src/mobile/lib/features/learning/screens/home_screen.dart b/src/mobile/lib/features/learning/screens/home_screen.dart new file mode 100644 index 0000000..08854ea --- /dev/null +++ b/src/mobile/lib/features/learning/screens/home_screen.dart @@ -0,0 +1,285 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../shared/providers/auth_provider.dart'; + +class HomeScreen extends ConsumerWidget { + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final authState = ref.watch(authProvider); + + return Scaffold( + appBar: AppBar( + title: const Text('Drama Ling'), + actions: [ + IconButton( + icon: const Icon(Icons.person), + onPressed: () => context.go('/profile'), + ), + ], + ), + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Welcome Section + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Theme.of(context).colorScheme.primary, + Theme.of(context).colorScheme.primary.withOpacity(0.8), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '歡迎回來,${authState.displayName ?? '學習者'}!', + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + '繼續您的語言學習之旅', + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Colors.white.withOpacity(0.9), + ), + ), + const SizedBox(height: 16), + Row( + children: [ + _buildStatCard('今日學習', '25分鐘', Icons.timer, context), + const SizedBox(width: 16), + _buildStatCard('連續天數', '7天', Icons.local_fire_department, context), + ], + ), + ], + ), + ), + + const SizedBox(height: 32), + + // Learning Options + Text( + '選擇學習方式', + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + + // Learning Cards + _buildLearningCard( + context, + '詞彙練習', + '學習新詞彙,擴充詞彙量', + Icons.book_outlined, + Colors.blue, + () => context.go('/vocabulary'), + ), + + const SizedBox(height: 16), + + _buildLearningCard( + context, + '對話練習', + 'AI互動對話,提升口說能力', + Icons.chat_bubble_outline, + Colors.green, + () => context.go('/dialogue'), + ), + + const SizedBox(height: 16), + + _buildLearningCard( + context, + '限時挑戰', + '測試反應速度和語言技能', + Icons.timer_outlined, + Colors.orange, + () => context.go('/challenge'), + ), + + const SizedBox(height: 32), + + // Progress Section + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '學習進度', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + _buildProgressItem('詞彙掌握', 0.7, '70%', context), + const SizedBox(height: 12), + _buildProgressItem('對話流暢度', 0.5, '50%', context), + const SizedBox(height: 12), + _buildProgressItem('聽力理解', 0.8, '80%', context), + ], + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildStatCard(String title, String value, IconData icon, BuildContext context) { + return Expanded( + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + children: [ + Icon(icon, color: Colors.white, size: 24), + const SizedBox(height: 4), + Text( + value, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + Text( + title, + style: TextStyle( + color: Colors.white.withOpacity(0.8), + fontSize: 12, + ), + ), + ], + ), + ), + ); + } + + Widget _buildLearningCard( + BuildContext context, + String title, + String subtitle, + IconData icon, + Color color, + VoidCallback onTap, + ) { + return Card( + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(16), + child: Container( + padding: const EdgeInsets.all(20), + child: Row( + children: [ + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + icon, + color: color, + size: 28, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + subtitle, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), + ), + ], + ), + ), + Icon( + Icons.arrow_forward_ios, + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5), + size: 16, + ), + ], + ), + ), + ), + ); + } + + Widget _buildProgressItem(String title, double progress, String percentage, BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + Text( + percentage, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 8), + LinearProgressIndicator( + value: progress, + backgroundColor: Theme.of(context).colorScheme.primary.withOpacity(0.1), + valueColor: AlwaysStoppedAnimation( + Theme.of(context).colorScheme.primary, + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/src/mobile/lib/main.dart b/src/mobile/lib/main.dart new file mode 100644 index 0000000..ba22430 --- /dev/null +++ b/src/mobile/lib/main.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hive_flutter/hive_flutter.dart'; + +import 'core/constants/app_constants.dart'; +import 'core/services/storage_service.dart'; +import 'core/utils/app_router.dart'; +import 'core/utils/app_theme.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // Initialize Hive + await Hive.initFlutter(); + + // Initialize Storage Service + await StorageService.init(); + + runApp( + ProviderScope( + child: const DramaLingApp(), + ), + ); +} + +class DramaLingApp extends ConsumerWidget { + const DramaLingApp({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final router = ref.watch(routerProvider); + + return ScreenUtilInit( + designSize: const Size(375, 812), + minTextAdapt: true, + splitScreenMode: true, + builder: (context, child) { + return MaterialApp.router( + title: AppConstants.appName, + debugShowCheckedModeBanner: false, + theme: AppTheme.lightTheme, + darkTheme: AppTheme.darkTheme, + themeMode: ThemeMode.system, + routerConfig: router, + builder: (context, widget) { + return MediaQuery( + data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0), + child: widget!, + ); + }, + ); + }, + ); + } +} \ No newline at end of file diff --git a/src/mobile/lib/shared/providers/auth_provider.dart b/src/mobile/lib/shared/providers/auth_provider.dart new file mode 100644 index 0000000..2f3ff43 --- /dev/null +++ b/src/mobile/lib/shared/providers/auth_provider.dart @@ -0,0 +1,91 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class AuthState { + final bool isAuthenticated; + final String? userId; + final String? email; + final String? displayName; + final bool isLoading; + + const AuthState({ + required this.isAuthenticated, + this.userId, + this.email, + this.displayName, + this.isLoading = false, + }); + + AuthState copyWith({ + bool? isAuthenticated, + String? userId, + String? email, + String? displayName, + bool? isLoading, + }) { + return AuthState( + isAuthenticated: isAuthenticated ?? this.isAuthenticated, + userId: userId ?? this.userId, + email: email ?? this.email, + displayName: displayName ?? this.displayName, + isLoading: isLoading ?? this.isLoading, + ); + } +} + +class AuthNotifier extends StateNotifier { + AuthNotifier() : super(const AuthState(isAuthenticated: false)); + + Future login(String email, String password) async { + state = state.copyWith(isLoading: true); + + try { + // TODO: 實現實際的登入邏輯 + await Future.delayed(const Duration(seconds: 1)); + + state = state.copyWith( + isAuthenticated: true, + userId: 'user_123', + email: email, + displayName: email.split('@').first, + isLoading: false, + ); + } catch (e) { + state = state.copyWith(isLoading: false); + rethrow; + } + } + + Future register(String email, String password, String displayName) async { + state = state.copyWith(isLoading: true); + + try { + // TODO: 實現實際的註冊邏輯 + await Future.delayed(const Duration(seconds: 1)); + + state = state.copyWith( + isAuthenticated: true, + userId: 'user_${DateTime.now().millisecondsSinceEpoch}', + email: email, + displayName: displayName, + isLoading: false, + ); + } catch (e) { + state = state.copyWith(isLoading: false); + rethrow; + } + } + + Future logout() async { + state = const AuthState(isAuthenticated: false); + } + + void checkAuthStatus() { + // TODO: 檢查本地儲存的登入狀態 + // 暫時保持未登入狀態供展示 + } +} + +final authProvider = StateNotifierProvider((ref) { + return AuthNotifier(); +}); \ No newline at end of file diff --git a/src/mobile/pubspec.lock b/src/mobile/pubspec.lock new file mode 100644 index 0000000..ca0eddf --- /dev/null +++ b/src/mobile/pubspec.lock @@ -0,0 +1,1202 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + url: "https://pub.dev" + source: hosted + version: "67.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + analyzer_plugin: + dependency: transitive + description: + name: analyzer_plugin + sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161" + url: "https://pub.dev" + source: hosted + version: "0.11.3" + archive: + dependency: transitive + description: + name: archive + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d + url: "https://pub.dev" + source: hosted + version: "3.6.1" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + audio_session: + dependency: transitive + description: + name: audio_session + sha256: "2b7fff16a552486d078bfc09a8cde19f426dc6d6329262b684182597bec5b1ac" + url: "https://pub.dev" + source: hosted + version: "0.1.25" + audioplayers: + dependency: "direct main" + description: + name: audioplayers + sha256: c05c6147124cd63e725e861335a8b4d57300b80e6e92cea7c145c739223bbaef + url: "https://pub.dev" + source: hosted + version: "5.2.1" + audioplayers_android: + dependency: transitive + description: + name: audioplayers_android + sha256: b00e1a0e11365d88576320ec2d8c192bc21f1afb6c0e5995d1c57ae63156acb5 + url: "https://pub.dev" + source: hosted + version: "4.0.3" + audioplayers_darwin: + dependency: transitive + description: + name: audioplayers_darwin + sha256: "3034e99a6df8d101da0f5082dcca0a2a99db62ab1d4ddb3277bed3f6f81afe08" + url: "https://pub.dev" + source: hosted + version: "5.0.2" + audioplayers_linux: + dependency: transitive + description: + name: audioplayers_linux + sha256: "60787e73fefc4d2e0b9c02c69885402177e818e4e27ef087074cf27c02246c9e" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + audioplayers_platform_interface: + dependency: transitive + description: + name: audioplayers_platform_interface + sha256: "365c547f1bb9e77d94dd1687903a668d8f7ac3409e48e6e6a3668a1ac2982adb" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + audioplayers_web: + dependency: transitive + description: + name: audioplayers_web + sha256: "22cd0173e54d92bd9b2c80b1204eb1eb159ece87475ab58c9788a70ec43c2a62" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + audioplayers_windows: + dependency: transitive + description: + name: audioplayers_windows + sha256: "9536812c9103563644ada2ef45ae523806b0745f7a78e89d1b5fb1951de90e1a" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + build: + dependency: transitive + description: + name: build + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" + url: "https://pub.dev" + source: hosted + version: "4.0.4" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + url: "https://pub.dev" + source: hosted + version: "2.4.13" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 + url: "https://pub.dev" + source: hosted + version: "7.3.2" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: a30f0a0e38671e89a492c44d005b5545b830a961575bbd8336d42869ff71066d + url: "https://pub.dev" + source: hosted + version: "8.12.0" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" + url: "https://pub.dev" + source: hosted + version: "4.10.1" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + crypto: + dependency: "direct main" + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + custom_lint_core: + dependency: transitive + description: + name: custom_lint_core + sha256: a85e8f78f4c52f6c63cdaf8c872eb573db0231dcdf3c3a5906d493c1f8bc20e6 + url: "https://pub.dev" + source: hosted + version: "0.6.3" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + url: "https://pub.dev" + source: hosted + version: "2.3.6" + dio: + dependency: "direct main" + description: + name: dio + sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9 + url: "https://pub.dev" + source: hosted + version: "5.9.0" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + flutter_riverpod: + dependency: "direct main" + description: + name: flutter_riverpod + sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1" + url: "https://pub.dev" + source: hosted + version: "2.6.1" + flutter_screenutil: + dependency: "direct main" + description: + name: flutter_screenutil + sha256: "8239210dd68bee6b0577aa4a090890342d04a136ce1c81f98ee513fc0ce891de" + url: "https://pub.dev" + source: hosted + version: "5.9.3" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea" + url: "https://pub.dev" + source: hosted + version: "9.2.4" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247" + url: "https://pub.dev" + source: hosted + version: "3.1.3" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + freezed: + dependency: "direct dev" + description: + name: freezed + sha256: a434911f643466d78462625df76fd9eb13e57348ff43fe1f77bbe909522c67a1 + url: "https://pub.dev" + source: hosted + version: "2.5.2" + freezed_annotation: + dependency: "direct main" + description: + name: freezed_annotation + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + url: "https://pub.dev" + source: hosted + version: "2.4.4" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: c5fa45fa502ee880839e3b2152d987c44abae26d064a2376d4aad434cf0f7b15 + url: "https://pub.dev" + source: hosted + version: "12.1.3" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: ebc94ed30fd13cefd397cb1658b593f21571f014b7d1197eeb41fb95f05d899a + url: "https://pub.dev" + source: hosted + version: "6.3.1" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + hive: + dependency: "direct main" + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + hive_flutter: + dependency: "direct main" + description: + name: hive_flutter + sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc + url: "https://pub.dev" + source: hosted + version: "1.1.0" + hive_generator: + dependency: "direct dev" + description: + name: hive_generator + sha256: "06cb8f58ace74de61f63500564931f9505368f45f98958bd7a6c35ba24159db4" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + http: + dependency: transitive + description: + name: http + sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 + url: "https://pub.dev" + source: hosted + version: "1.5.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + url: "https://pub.dev" + source: hosted + version: "0.18.1" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b + url: "https://pub.dev" + source: hosted + version: "6.8.0" + just_audio: + dependency: "direct main" + description: + name: just_audio + sha256: f978d5b4ccea08f267dae0232ec5405c1b05d3f3cd63f82097ea46c015d5c09e + url: "https://pub.dev" + source: hosted + version: "0.9.46" + just_audio_platform_interface: + dependency: transitive + description: + name: just_audio_platform_interface + sha256: "2532c8d6702528824445921c5ff10548b518b13f808c2e34c2fd54793b999a6a" + url: "https://pub.dev" + source: hosted + version: "4.6.0" + just_audio_web: + dependency: transitive + description: + name: just_audio_web + sha256: "6ba8a2a7e87d57d32f0f7b42856ade3d6a9fbe0f1a11fabae0a4f00bb73f0663" + url: "https://pub.dev" + source: hosted + version: "0.4.16" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0" + url: "https://pub.dev" + source: hosted + version: "11.0.1" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + url: "https://pub.dev" + source: hosted + version: "3.0.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + lottie: + dependency: "direct main" + description: + name: lottie + sha256: a93542cc2d60a7057255405f62252533f8e8956e7e06754955669fd32fb4b216 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "993381400e94d18469750e5b9dcb8206f15bc09f9da86b9e44a9b0092a0066db" + url: "https://pub.dev" + source: hosted + version: "2.2.18" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + protobuf: + dependency: transitive + description: + name: protobuf + sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + retrofit: + dependency: "direct main" + description: + name: retrofit + sha256: "699cf44ec6c7fc7d248740932eca75d334e36bdafe0a8b3e9ff93100591c8a25" + url: "https://pub.dev" + source: hosted + version: "4.7.2" + retrofit_generator: + dependency: "direct dev" + description: + name: retrofit_generator + sha256: "8dfc406cdfa171f33cbd21bf5bd8b6763548cc217de19cdeaa07a76727fac4ca" + url: "https://pub.dev" + source: hosted + version: "8.2.1" + riverpod: + dependency: transitive + description: + name: riverpod + sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959" + url: "https://pub.dev" + source: hosted + version: "2.6.1" + riverpod_analyzer_utils: + dependency: transitive + description: + name: riverpod_analyzer_utils + sha256: "8b71f03fc47ae27d13769496a1746332df4cec43918aeba9aff1e232783a780f" + url: "https://pub.dev" + source: hosted + version: "0.5.1" + riverpod_annotation: + dependency: "direct main" + description: + name: riverpod_annotation + sha256: e14b0bf45b71326654e2705d462f21b958f987087be850afd60578fcd502d1b8 + url: "https://pub.dev" + source: hosted + version: "2.6.1" + riverpod_generator: + dependency: "direct dev" + description: + name: riverpod_generator + sha256: d451608bf17a372025fc36058863737636625dfdb7e3cbf6142e0dfeb366ab22 + url: "https://pub.dev" + source: hosted + version: "2.4.0" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: a2608114b1ffdcbc9c120eb71a0e207c71da56202852d4aab8a5e30a82269e74 + url: "https://pub.dev" + source: hosted + version: "2.4.12" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + shimmer: + dependency: "direct main" + description: + name: shimmer + sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" + url: "https://pub.dev" + source: hosted + version: "1.3.5" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88 + url: "https://pub.dev" + source: hosted + version: "2.4.2+2" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + state_notifier: + dependency: transitive + description: + name: state_notifier + sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb + url: "https://pub.dev" + source: hosted + version: "1.0.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + url: "https://pub.dev" + source: hosted + version: "3.4.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + url: "https://pub.dev" + source: hosted + version: "0.7.6" + timing: + dependency: transitive + description: + name: timing + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + tuple: + dependency: transitive + description: + name: tuple + sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 + url: "https://pub.dev" + source: hosted + version: "2.0.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + very_good_analysis: + dependency: "direct dev" + description: + name: very_good_analysis + sha256: "9ae7f3a3bd5764fb021b335ca28a34f040cd0ab6eec00a1b213b445dae58a4b8" + url: "https://pub.dev" + source: hosted + version: "5.1.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + watcher: + dependency: transitive + description: + name: watcher + sha256: "5bf046f41320ac97a469d506261797f35254fa61c641741ef32dacda98b7d39c" + url: "https://pub.dev" + source: hosted + version: "1.1.3" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + win32: + dependency: transitive + description: + name: win32 + sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03" + url: "https://pub.dev" + source: hosted + version: "5.14.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.9.0 <4.0.0" + flutter: ">=3.29.0" diff --git a/src/mobile/pubspec.yaml b/src/mobile/pubspec.yaml new file mode 100644 index 0000000..1a62d5b --- /dev/null +++ b/src/mobile/pubspec.yaml @@ -0,0 +1,85 @@ +name: dramaling +description: "Drama Ling - AI-powered language learning app with immersive dialogue practice" +publish_to: 'none' + +version: 1.0.0+1 + +environment: + sdk: '>=3.0.0 <4.0.0' + flutter: ">=3.16.0" + +dependencies: + flutter: + sdk: flutter + + # State Management + flutter_riverpod: ^2.4.9 + riverpod_annotation: ^2.3.3 + + # Navigation + go_router: ^12.1.3 + + # Network + dio: ^5.3.3 + retrofit: ^4.0.3 + json_annotation: ^4.8.1 + + # Local Storage + hive: ^2.2.3 + hive_flutter: ^1.1.0 + shared_preferences: ^2.2.2 + + # UI Components + flutter_screenutil: ^5.9.0 + cached_network_image: ^3.3.0 + shimmer: ^3.0.0 + lottie: ^2.7.0 + + # Audio + just_audio: ^0.9.35 + audioplayers: ^5.2.1 + + # Authentication & Security + flutter_secure_storage: ^9.0.0 + crypto: ^3.0.3 + + # Utils + intl: ^0.18.1 + equatable: ^2.0.5 + freezed_annotation: ^2.4.1 + + # Icons & Fonts + cupertino_icons: ^1.0.6 + google_fonts: ^6.1.0 + +dev_dependencies: + flutter_test: + sdk: flutter + + # Code Generation + build_runner: ^2.4.7 + retrofit_generator: ^8.0.4 + riverpod_generator: ^2.3.9 + json_serializable: ^6.7.1 + hive_generator: ^2.0.1 + freezed: ^2.4.6 + + # Linting + flutter_lints: ^3.0.1 + very_good_analysis: ^5.1.0 + +flutter: + uses-material-design: true + + assets: + - assets/images/ + - assets/animations/ + - assets/audio/ + - assets/icons/ + + # fonts: + # - family: NotoSans + # fonts: + # - asset: assets/fonts/NotoSans-Regular.ttf + # - asset: assets/fonts/NotoSans-Bold.ttf + # weight: 700 \ No newline at end of file diff --git a/src/mobile/test/widget_test.dart b/src/mobile/test/widget_test.dart new file mode 100644 index 0000000..3d5ec84 --- /dev/null +++ b/src/mobile/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:dramaling/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} diff --git a/src/mobile/web/favicon.png b/src/mobile/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/src/mobile/web/favicon.png differ diff --git a/src/mobile/web/index.html b/src/mobile/web/index.html new file mode 100644 index 0000000..6460d68 --- /dev/null +++ b/src/mobile/web/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + dramaling + + + + + + diff --git a/src/mobile/web/manifest.json b/src/mobile/web/manifest.json new file mode 100644 index 0000000..db815e0 --- /dev/null +++ b/src/mobile/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "dramaling", + "short_name": "dramaling", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/tools/phase.sh b/tools/phase.sh new file mode 100755 index 0000000..f346c2e --- /dev/null +++ b/tools/phase.sh @@ -0,0 +1,270 @@ +#!/bin/bash + +# 階段管理工具 +# 使用方法: ./drama phase [命令] + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +PROJECTS_FILE="$PROJECT_DIR/PROJECTS.md" +PROJECTS_DATA_DIR="$PROJECT_DIR/projects" + +# 顏色定義 +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +PURPLE='\033[0;35m' +RED='\033[0;31m' +NC='\033[0m' + +# 類型圖標函數 (Bash 3.2 兼容) +get_type_icon() { + case "$1" in + "FE") echo "🎨" ;; + "BE") echo "⚙️" ;; + "AI") echo "🤖" ;; + "MB") echo "📱" ;; + "DOC") echo "📚" ;; + "ENV") echo "🔧" ;; + "TEST") echo "🧪" ;; + *) echo "🔧" ;; + esac +} + +# 狀態圖標函數 +get_status_icon() { + case "$1" in + "pending") echo "⏳" ;; + "in-progress") echo "🔄" ;; + "completed") echo "✅" ;; + "blocked") echo "❌" ;; + "paused") echo "⏸️" ;; + *) echo "⏳" ;; + esac +} + +# 確保必要文件和目錄存在 +init_phase_structure() { + if [[ ! -f "$PROJECTS_FILE" ]]; then + echo -e "${RED}❌ PROJECTS.md 不存在${NC}" + exit 1 + fi + + if [[ ! -d "$PROJECTS_DATA_DIR" ]]; then + mkdir -p "$PROJECTS_DATA_DIR" + fi +} + +# 列出所有階段 +list_phases() { + local project_name="$1" + + if [[ -z "$project_name" ]]; then + echo -e "${BLUE}📋 所有專案的階段列表${NC}" + echo "==================================" + + # 從PROJECTS.md中提取階段信息 + grep -E "^#### 階段[0-9]+:" "$PROJECTS_FILE" | while read -r line; do + phase_info=$(echo "$line" | sed 's/^#### //') + echo -e " ${GREEN}📊 $phase_info${NC}" + done + else + echo -e "${BLUE}📋 專案 '$project_name' 的階段列表${NC}" + echo "==================================" + + local project_file="$PROJECTS_DATA_DIR/${project_name}.md" + if [[ -f "$project_file" ]]; then + grep -E "^### 階段[0-9]+:" "$project_file" | while read -r line; do + phase_info=$(echo "$line" | sed 's/^### //') + echo -e " ${GREEN}📊 $phase_info${NC}" + done + else + echo -e "${RED}❌ 專案 '$project_name' 不存在${NC}" + fi + fi +} + +# 新增階段 +add_phase() { + local project_name="$1" + local phase_name="$2" + + if [[ -z "$project_name" || -z "$phase_name" ]]; then + echo -e "${RED}❌ 請提供專案名稱和階段名稱${NC}" + echo "使用方法: ./dl phase add \"專案名稱\" \"階段名稱\"" + return 1 + fi + + local project_file="$PROJECTS_DATA_DIR/${project_name}.md" + if [[ ! -f "$project_file" ]]; then + echo -e "${RED}❌ 專案 '$project_name' 不存在${NC}" + echo "使用 ./dl project init \"$project_name\" 先建立專案" + return 1 + fi + + echo -e "${BLUE}➕ 為專案 '$project_name' 新增階段: $phase_name${NC}" + + # 計算新階段編號 + local phase_count=$(grep -c "^### 階段[0-9]*:" "$project_file") + local new_phase_num=$((phase_count + 1)) + + # 在項目文件中新增階段 + cat >> "$project_file" << EOF + +### 階段$new_phase_num: $phase_name ⏳ +- ⏳ \`ENV\` 請使用 ./drama phase items 新增執行項目... + +EOF + + echo -e "${GREEN}✅ 階段 '$phase_name' 新增完成${NC}" + echo " 💡 使用 ./dl phase items \"$project_name\" \"$phase_name\" 管理執行項目" +} + +# 管理階段執行項目 +manage_phase_items() { + local project_name="$1" + local phase_name="$2" + local item_type="$3" + local item_desc="$4" + + if [[ -z "$project_name" || -z "$phase_name" ]]; then + echo -e "${RED}❌ 請提供專案名稱和階段名稱${NC}" + echo "使用方法: ./dl phase items \"專案名稱\" \"階段名稱\" [類型] [描述]" + return 1 + fi + + local project_file="$PROJECTS_DATA_DIR/${project_name}.md" + if [[ ! -f "$project_file" ]]; then + echo -e "${RED}❌ 專案 '$project_name' 不存在${NC}" + return 1 + fi + + if [[ -n "$item_type" && -n "$item_desc" ]]; then + # 新增執行項目 + local type_icon=$(get_type_icon "$item_type") + echo -e "${BLUE}➕ 新增執行項目到階段 '$phase_name'${NC}" + echo " ${type_icon} [$item_type] $item_desc" + + # TODO: 實作新增項目到特定階段的邏輯 + echo -e "${YELLOW}⚠️ 此功能開發中...${NC}" + else + # 顯示階段的執行項目 + echo -e "${BLUE}📋 階段 '$phase_name' 的執行項目${NC}" + echo "==================================" + + # 尋找並顯示階段的執行項目 + local in_phase=false + while IFS= read -r line; do + if [[ "$line" =~ ^###.*$phase_name ]]; then + in_phase=true + continue + elif [[ "$line" =~ ^###.*階段[0-9]+: ]] && [[ "$in_phase" == true ]]; then + break + elif [[ "$in_phase" == true && "$line" =~ ^-.*\`.*\` ]]; then + echo " $line" + fi + done < "$project_file" + fi +} + +# 開始執行階段 +start_phase() { + local project_name="$1" + local phase_name="$2" + + if [[ -z "$project_name" || -z "$phase_name" ]]; then + echo -e "${RED}❌ 請提供專案名稱和階段名稱${NC}" + echo "使用方法: ./dl phase start \"專案名稱\" \"階段名稱\"" + return 1 + fi + + echo -e "${GREEN}🚀 開始執行階段: $phase_name${NC}" + echo -e "${BLUE}專案: $project_name${NC}" + echo "" + echo -e "${YELLOW}⚠️ 此功能開發中,將來會更新項目狀態...${NC}" +} + +# 完成階段 +complete_phase() { + local project_name="$1" + local phase_name="$2" + + if [[ -z "$project_name" || -z "$phase_name" ]]; then + echo -e "${RED}❌ 請提供專案名稱和階段名稱${NC}" + echo "使用方法: ./dl phase complete \"專案名稱\" \"階段名稱\"" + return 1 + fi + + echo -e "${GREEN}✅ 完成階段: $phase_name${NC}" + echo -e "${BLUE}專案: $project_name${NC}" + echo "" + echo -e "${YELLOW}⚠️ 此功能開發中,將來會更新項目狀態和產生報告...${NC}" +} + +# 顯示階段狀態 +show_phase_status() { + echo -e "${BLUE}📊 階段執行狀態總覽${NC}" + echo "==================================" + echo "" + echo -e "${GREEN}當前進行中的階段:${NC}" + echo " 🔄 環境配置 (Drama Ling 手機APP開發)" + echo "" + echo -e "${PURPLE}階段統計:${NC}" + echo " 📊 總階段數: 4" + echo " 🔄 進行中: 1" + echo " ⏳ 待執行: 3" + echo " ✅ 已完成: 0" +} + +# 顯示幫助 +show_help() { + echo -e "${BLUE}🚀 階段管理工具使用指南${NC}" + echo "==================================" + echo "" + echo -e "${PURPLE}階段管理命令:${NC}" + echo " list [專案名] - 列出階段" + echo " add \"專案\" \"階段名\" - 新增階段" + echo " items \"專案\" \"階段\" [類型] [描述] - 管理執行項目" + echo " start \"專案\" \"階段\" - 開始執行階段" + echo " complete \"專案\" \"階段\" - 完成階段" + echo " status - 查看階段狀態" + echo "" + echo -e "${PURPLE}範例:${NC}" + echo " ./dl phase add \"我的專案\" \"開發階段\"" + echo " ./dl phase items \"我的專案\" \"開發階段\" FE \"建立登入頁面\"" + echo " ./dl phase start \"我的專案\" \"開發階段\"" + echo "" + echo -e "${BLUE}提示:${NC} 使用 ./dl project types 查看所有項目類型" +} + +# 主邏輯 +init_phase_structure + +case "$1" in + "list") + list_phases "$2" + ;; + "add") + add_phase "$2" "$3" + ;; + "items") + manage_phase_items "$2" "$3" "$4" "$5" + ;; + "start") + start_phase "$2" "$3" + ;; + "complete") + complete_phase "$2" "$3" + ;; + "status") + show_phase_status + ;; + "help"|"--help"|"-h"|"") + show_help + ;; + *) + echo -e "${RED}❌ 未知命令: $1${NC}" + echo "" + show_help + exit 1 + ;; +esac \ No newline at end of file diff --git a/tools/project.sh b/tools/project.sh new file mode 100755 index 0000000..b2ecf8f --- /dev/null +++ b/tools/project.sh @@ -0,0 +1,221 @@ +#!/bin/bash + +# 專案管理工具 +# 使用方法: ./drama project [命令] + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +PROJECTS_FILE="$PROJECT_DIR/PROJECTS.md" +PROJECTS_DATA_DIR="$PROJECT_DIR/projects" + +# 顏色定義 +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +PURPLE='\033[0;35m' +RED='\033[0;31m' +NC='\033[0m' + +# 類型圖標和名稱函數 (Bash 3.2 兼容) +get_type_icon() { + case "$1" in + "FE") echo "🎨" ;; + "BE") echo "⚙️" ;; + "AI") echo "🤖" ;; + "MB") echo "📱" ;; + "DOC") echo "📚" ;; + "ENV") echo "🔧" ;; + "TEST") echo "🧪" ;; + *) echo "🔧" ;; + esac +} + +get_type_name() { + case "$1" in + "FE") echo "前端開發" ;; + "BE") echo "後端開發" ;; + "AI") echo "AI整合" ;; + "MB") echo "移動端" ;; + "DOC") echo "文檔更新" ;; + "ENV") echo "環境配置" ;; + "TEST") echo "測試驗證" ;; + *) echo "環境配置" ;; + esac +} + +# 狀態圖標函數 +get_status_icon() { + case "$1" in + "pending") echo "⏳" ;; + "in-progress") echo "🔄" ;; + "completed") echo "✅" ;; + "blocked") echo "❌" ;; + "paused") echo "⏸️" ;; + *) echo "⏳" ;; + esac +} + +# 確保必要文件和目錄存在 +init_project_structure() { + if [[ ! -f "$PROJECTS_FILE" ]]; then + echo -e "${RED}❌ PROJECTS.md 不存在${NC}" + exit 1 + fi + + if [[ ! -d "$PROJECTS_DATA_DIR" ]]; then + mkdir -p "$PROJECTS_DATA_DIR" + fi +} + +# 顯示項目列表 +show_project_list() { + echo -e "${BLUE}📋 所有專案列表${NC}" + echo "==================================" + + if [[ ! -f "$PROJECTS_FILE" ]]; then + echo -e "${RED}❌ 專案文件不存在${NC}" + return 1 + fi + + # 從PROJECTS.md中提取項目信息 + grep -E "^### " "$PROJECTS_FILE" | while read -r line; do + # 提取項目名稱和狀態 + project_name=$(echo "$line" | sed 's/^### [^ ]* \(.*\) (.*$/\1/') + status_line=$(echo "$line" | grep -o '([^)]*)$' | tr -d '()') + + if [[ -n "$project_name" ]]; then + echo -e " ${GREEN}📁 $project_name${NC} ($status_line)" + fi + done +} + +# 顯示項目狀態 +show_project_status() { + local project_name="$1" + + echo -e "${BLUE}📊 專案執行狀態${NC}" + echo "==================================" + + if [[ -n "$project_name" ]]; then + echo -e "${PURPLE}專案: $project_name${NC}" + # TODO: 顯示特定項目的詳細狀態 + else + # 顯示總體統計 + echo -e "${GREEN}總體統計:${NC}" + echo " 📱 進行中專案: 1 個" + echo " ⏳ 待執行項目: 16 個" + echo " 🔄 進行中項目: 0 個" + echo " ✅ 已完成項目: 0 個" + fi +} + +# 初始化新項目 +init_new_project() { + local project_name="$1" + + if [[ -z "$project_name" ]]; then + echo -e "${RED}❌ 請提供項目名稱${NC}" + echo "使用方法: ./drama project init \"項目名稱\"" + return 1 + fi + + echo -e "${BLUE}🚀 初始化新專案: $project_name${NC}" + + # 創建項目數據文件 + local project_file="$PROJECTS_DATA_DIR/${project_name}.md" + if [[ -f "$project_file" ]]; then + echo -e "${YELLOW}⚠️ 專案 '$project_name' 已存在${NC}" + return 1 + fi + + # 創建項目詳細文件 + cat > "$project_file" << EOF +# $project_name + +**創建日期**: $(date +"%Y-%m-%d") +**狀態**: 🔄 進行中 + +## 專案描述 +請在此處添加專案的詳細描述... + +## 執行階段 + +### 階段1: 待定義 +- ⏳ \`ENV\` 待添加執行項目... + +## 專案統計 +- **總階段數**: 1 +- **執行項目數**: 1 +- **完成進度**: 0% + +--- +**最後更新**: $(date +"%Y-%m-%d") +EOF + + echo -e "${GREEN}✅ 專案 '$project_name' 初始化完成${NC}" + echo " 📁 專案文件: $project_file" + echo " 💡 使用 ./dl phase add \"$project_name\" \"階段名稱\" 新增階段" +} + +# 列出項目類型 +show_project_types() { + echo -e "${BLUE}🏷️ 專案類型定義${NC}" + echo "==================================" + + local types="FE BE AI MB DOC ENV TEST" + for type_code in $types; do + local icon=$(get_type_icon "$type_code") + local name=$(get_type_name "$type_code") + echo -e " ${icon} ${PURPLE}$type_code${NC} - $name" + done + + echo "" + echo -e "${BLUE}使用範例:${NC}" + echo " ./dl phase add \"專案名\" \"階段名\" FE \"前端任務描述\"" +} + +# 顯示幫助 +show_help() { + echo -e "${BLUE}🚀 專案管理工具使用指南${NC}" + echo "==================================" + echo "" + echo -e "${PURPLE}基本命令:${NC}" + echo " init [項目名稱] - 初始化新專案" + echo " list - 列出所有專案" + echo " status [項目名稱] - 查看執行狀態" + echo " types - 顯示專案類型定義" + echo "" + echo -e "${PURPLE}範例:${NC}" + echo " ./dl project init \"我的新專案\"" + echo " ./dl project list" + echo " ./dl project status" + echo "" + echo -e "${BLUE}提示:${NC} 使用 ./dl phase 管理專案階段和執行項目" +} + +# 主邏輯 +init_project_structure + +case "$1" in + "init") + init_new_project "$2" + ;; + "list") + show_project_list + ;; + "status") + show_project_status "$2" + ;; + "types") + show_project_types + ;; + "help"|"--help"|"-h"|"") + show_help + ;; + *) + echo -e "${RED}❌ 未知命令: $1${NC}" + echo "" + show_help + exit 1 + ;; +esac \ No newline at end of file