# 口語復健APP 工作項目及進度 ## APP端: ### 主畫面 - [ ] 螢幕下排固定顯示首頁、使用紀錄、新訊息、關於 - [ ] 首頁 - [ ] 跳回首頁頁面 - [ ] 使用紀錄 - [ ] 顯示近期一周內使用紀錄(復健次數和使用時間) - [ ] 通知(功能非頁面) - [ ] 復健訓練提醒、問卷填寫通知 - [ ] 關於 - [ ] 合作夥伴 - [ ] 最後更新時間 - [ ] APP使用教學 ### 病人頁面 - [ ] 復健功能 - [ ] 語言訓練使用教學 - [ ] 連結到YT的教學影片(暫定) - [ ] 語言訓練 - [ ] 錄製使用者語音 - [ ] 資料儲存 - [ ] 判斷結果 - [ ] 數據傳送至後端 - [ ] 生理需求 - [ ] 選擇圖卡(利用表達生理需求) - [ ] 吃飯 - [ ] 睡覺 - [ ] 喝水 - [ ] 吃藥 - [ ] 洗澡 - [ ] 我好冷 - [ ] 我好熱 - [ ] 上廁所 - [ ] 換尿布 - [ ] 認識失語症 - [ ] 相關連結(衛教) - [ ] YouTube相關連結 - [ ] google相關資訊 - [ ] 高醫失語症相關連結 - [ ] 台灣腦中風醫學會 - [ ] 台灣腦中風病友協會 - [ ] 衛服部福利諮詢網 - [ ] 衛服部長照專區 - [ ] 病後人生|一站式服務網 - [ ] Q&A常見問題 - [ ] 失語症相關之認識 - [ ] 失語症相關之探討 - [ ] 失語症相關之問題 - [ ] 社群交流 - [ ] 病友交流社群 - [ ] 醫療社群(LINE) - [ ] 基本設定 - [ ] 使用者個人資料 - [ ] 姓名 - [ ] 性別 - [ ] 年齡 - [ ] 診斷 - [ ] 連絡電話 - [ ] 個案編號 - [ ] 使用日期 - [ ] 個案來源(醫院、診所、住家) - [ ] 緊急聯絡人 - [ ] 設定 - [ ] 通知 - [ ] 主題 - [ ] 字體大小 - [ ] 醫院資訊 - [ ] 高雄醫學院網站 - [ ] 問卷系統 - [ ] 測量問卷填寫畫面 - [ ] 每日任務 ### 照顧者頁面 - [ ] 居家照護小知識 - [ ] 放鬆音樂 - [ ] 社群交流 - [ ] 相關連結 - [ ] 問卷系統 ## 網頁部分(管理): - [x] 醫師登入畫面 - [x] 登入功能 - [x] 忘記密碼功能 - [x] 管理頁面 - [x] 病患醫護管理系統 - [x] 病患資料庫管理 - [x] 上方功能列 - [x] 新增 - [x] 刪除 - [x] 查詢 - [x] 修改 - [x] 輸入欄 - [x] 中間資料顯示(資料欄) - [x] 病人編號 - [x] 病人姓名 - [x] 詳細資料(Button可連結到其他頁面顯示詳情) - [x] 紀錄(Button可連結到其他頁面顯示復健紀錄) - [x] 醫師管理系統 - [x] 醫師資料庫管理 - [x] 上方功能列 - [x] 新增 - [x] 刪除 - [x] 查詢 - [x] 修改 - [x] 輸入欄 - [x] 中間資料顯示(資料欄) - [x] 管理員編號 - [x] 管理員姓名 - [x] 詳細資料(Button可連結到其他頁面顯示詳情) - [x] 紀錄(Button可連結到其他頁面顯示管理紀錄) ## APP程式 ### 設定所有頁面傳值的數據的格式 ``` // 設定所有頁面傳值的數據的格式 //DataMenu class AllPagesNeedData { AllPagesNeedData(this.id, this.account, this.Carer, this.RehabilitationNotice, this.QuestionnaireNotice, this.isdark); String id; String account; bool Carer; bool RehabilitationNotice; bool QuestionnaireNotice; bool isdark; } ``` ### 設定登入的MysqlData的格式 ``` //設定登入的MysqlData的格式 //MysqlMenu class MysqlDataOflogin_patient_database { MysqlDataOflogin_patient_database(this.id, this.account, this.password); String id; String account; String password; } ``` ### 設定病患復健紀錄的MysqlData的格式 ``` //設定病患復健紀錄的MysqlData的格式 //MysqlMenu class MysqlDataOfpatient_rehabilitation { MysqlDataOfpatient_rehabilitation( this.id, this.name, this.time, this.type, this.score); final String id; final String name; final DateTime time; final String type; final int score; } ``` ### 設定暗黑模式與非暗黑模式的物件顏色 ``` //設定暗黑模式與非暗黑模式的物件顏色 DarkMode(bool isdark, String object, [Color deep_color = Colors.black, Color pale_color = Colors.white]) { Color? self_color; switch (object) { case "background": //是深色模式嗎?是的話背景黑色,不是的話背景白色(自訂色) isdark ? self_color = deep_color : self_color = pale_color; break; //是深色模式嗎?不是的話字黑色(自訂色),是的話字白色 case "Text": !isdark ? self_color = deep_color : self_color = pale_color; break; default: print("ERROR,for "); } // print("DarkMode(bool isdark=$isdark," // " String object=$object," // " [Color deep_color = $deep_color," // " Color pale_color = $pale_color])"); return self_color; } ``` ### 印出list的function ``` PrintList(String page, String class_object, List list) { switch (class_object) { //個人資料 case "MysqlDataOfPersonal": print("$page is id:${list[0].id}, " "name:${list[0].name}, "); break; //頁面所需資料 case "AllPagesNeedData": print("$page is id:${list[0].id}, " "account:${list[0].account}, " "isdark:${list[0].isdark}, "); break; //病患復健資料 case "MysqlDataOfpatient_rehabilitation": print("$page is id:${list[0].id}, " "name:${list[0].name}, " "time:${list[0].time}, " "type:${list[0].type}, " "score:${list[0].score}, "); break; default: print("ERROR,for List list=$list, String object=$class_object"); return; } } ``` ### 連接Mysql ``` import 'package:mysql1/mysql1.dart'; class Mysql { static String host = '192.168.10.10', user = 'MyProject', password = '123', db = 'project'; static int port = 3306; Mysql(); Future<MySqlConnection> getConnection() async{ print('嘗試連線資料庫中...'); var settings = new ConnectionSettings( host: host, user: user, password: password, db: db, ); return await MySqlConnection.connect(settings); } } ``` ### 設定GridViewMenuData格式 ``` //設定GridViewMenuData格式 class GridViewMenuData { GridViewMenuData(this.index, this.icon, this.title, this.self_color); final int index; final IconData icon; final String title; final Color self_color; } ``` ### BottomNavigationBarItem模板 ``` //BottomNavigationBarItem模板 BottomNavigationBarItem buildBottomNavigationBarView( IconData icon, Color color, String label) { return BottomNavigationBarItem( icon: Icon( icon, color: color, ), label: label); } ``` ### ListTile模板 ``` //ListTile模板 ListTile buildListTile(BuildContext context, int index, IconData icon, String title, List<AllPagesNeedData> DataMenu) { return ListTile( leading: Icon( icon, size: 30, color: DarkMode( DataMenu[0].isdark, "Text", Colors.grey.shade800, Colors.white), ), title: Text(title, style: TextStyle( color: DarkMode( DataMenu[0].isdark, "Text", Colors.grey.shade800, Colors.white), fontSize: 20, )), onTap: () { switch (index) { //社區交流頁面 case 0: Navigator.of(context).push(MaterialPageRoute( builder: (context) => CommunityCommunicationPage(DataMenu))); break; //相關連結頁面 case 1: Navigator.of(context).push(MaterialPageRoute( builder: (context) => RelateLinkPage(DataMenu))); break; //問卷系統頁面 case 2: Navigator.of(context).push(MaterialPageRoute( builder: (context) => QuestionnairePage(DataMenu))); break; //居家照護小知識頁面 case 3: Navigator.of(context).push(MaterialPageRoute( builder: (context) => HomeCarePage(DataMenu))); break; //放鬆音樂頁面 case 4: Navigator.of(context).push(MaterialPageRoute( builder: (context) => RelaxMusicPage(DataMenu))); break; //回首頁 case 5: Navigator.of(context).push( MaterialPageRoute(builder: (context) => MainPage(DataMenu))); break; //登出 case 6: showAlertDialog(context); //顯示登出提示對話框 break; } }, ); } ``` ### Ink+GridView模板 ``` //Ink+GridView模板 Widget buildGridView(List<GridViewMenuData> menu, BuildContext context, List<AllPagesNeedData> DataMenu) { return MaterialApp( debugShowCheckedModeBanner: false, home: Scaffold( body: Container( //是深色模式嗎?是的話背景黑色,不是的話背景白色 color: DataMenu[0].isdark ? Colors.black : Colors.amber.shade50, padding: const EdgeInsets.only(top: 0, right: 20, left: 20, bottom: 20), child: GridView.builder( itemCount: menu.length, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( //寬高比 childAspectRatio: 5 / 7, crossAxisCount: 2, crossAxisSpacing: 20.0, mainAxisSpacing: 20.0), itemBuilder: (BuildContext context, int index) { return Material( //是深色模式嗎?是的話背景黑色,不是的話背景白色 color: DarkMode(DataMenu[0].isdark, "background"), borderRadius: const BorderRadius.all(Radius.circular(30)), child: Column( children: [ Ink( decoration: BoxDecoration( color: menu[index].self_color, borderRadius: const BorderRadius.all(Radius.circular(30.0)), ), child: InkResponse( borderRadius: const BorderRadius.all(Radius.circular(30.0)), //控制高亮的參數 highlightColor: Colors.white24, highlightShape: BoxShape.rectangle, radius: 0.0, splashColor: Colors.red, //true表示要剪裁水波紋響應的界面;false不剪裁 ,如果控件是圓角不剪裁的話水波紋是矩形 containedInkWell: true, onTap: () { print(menu[index].title); ChoosePage(context, menu[index].title, DataMenu); }, child: Icon( menu[index].icon, size: 140, color: Colors.white, ), ), ), Text( menu[index].title, textAlign: TextAlign.center, textScaleFactor: 1, style: TextStyle( fontSize: 26, //是深色模式嗎?不是的話字黑色,是的話字白色 color: DarkMode(DataMenu[0].isdark, "Text"), fontWeight: FontWeight.bold), ), ], )); }, ), ), ), ); } ``` ### 跳轉首頁方格頁面 ``` //跳轉首頁方格頁面 void ChoosePage( BuildContext context, int index, List<AllPagesNeedData> DataMenu) { switch (index) { //訓練頁面 case 0: Navigator.of(context).push( MaterialPageRoute(builder: (context) => TrainPage()), ); break; //生理需求頁面 case 1: Navigator.of(context).push( MaterialPageRoute(builder: (context) => PhysiologicalPage()), ); break; //認識失語症頁面 case 2: Navigator.of(context).push( MaterialPageRoute(builder: (context) => RecognizePage(DataMenu)), ); break; //基本設定頁面 case 3: Navigator.of(context).push( MaterialPageRoute(builder: (context) => BasicSettingsPage(DataMenu)), ); break; } } ``` ### 顯示確認登出對話框 ``` // 顯示確認登出對話框 void showAlertDialog(BuildContext context) { // Init AlertDialog dialog = AlertDialog( shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(15)), ), title: RichText( text: const TextSpan( children: [ WidgetSpan( child: Icon( Icons.warning, size: 30, color: Colors.yellow, ), ), TextSpan( text: "您確定要登出嗎?", style: TextStyle(fontSize: 25, color: Colors.black), ), ], ), ), actions: [ Center( child: Container( child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( style: ButtonStyle( backgroundColor: MaterialStateProperty.all<Color>(Colors.white), ), child: const Text( "取消", style: TextStyle(fontSize: 20, color: Colors.blue), ), onPressed: () { Navigator.pop(context); }, ), const SizedBox( width: 30, ), ElevatedButton( style: ButtonStyle( backgroundColor: MaterialStateProperty.all<Color>(Colors.blue), ), child: const Text( "登出", style: TextStyle(fontSize: 20), ), onPressed: () { Navigator.push( context, MaterialPageRoute(builder: (context) => const LoginPage()), ); }, ), ], ), ), ), ], ); // Show the dialog (showDialog() => showGeneralDialog()) //登出確認框的動畫 showGeneralDialog( context: context, pageBuilder: (context, anim1, anim2) { return Wrap(); }, transitionBuilder: (context, anim1, anim2, child) { return Transform( transform: Matrix4.translationValues( 0.0, (1.0 - Curves.easeInOut.transform(anim1.value)) * 400, 0.0, ), child: dialog, ); }, transitionDuration: const Duration(milliseconds: 400), ); } ``` ### 判斷是否有按眼睛 ``` //判斷是否有按眼睛 void _toggleVisibility() { setState(() { _isHidden = !_isHidden; }); } ``` ### 取得Mysql裡patient_database資料表的資料 ``` //取得Mysql裡patient_database資料表的資料 _getMysqlData(String login_account) { MysqlMenu.clear(); db.getConnection().then((conn) { String sql = "SELECT * FROM patient_database WHERE account='$login_account'"; conn.query(sql).then((results) { print("連線成功!"); for (var row in results) { // print(row); setState(() { MysqlMenu.add(MysqlDataOfpatient_database( row['id'], row['account'], row['password'])); }); } }); conn.close(); }); } ``` ### 登入條件判斷 ``` //登入條件判斷 _LoginJudgment(String account, String password, BuildContext context) { setState(() { //先將狀態清空 login_state = ""; }); if (account == "") { //如果帳號是空的 print('帳號不得為空!'); setState(() { login_state = "帳號不得為空!"; }); return; } if (password == "") { //如果密碼是空的 print('密碼不得為空!'); setState(() { login_state = "密碼不得為空!"; }); return; } //連線資料庫 _getMysqlData(account); //進度圖案框 final progress = ProgressHUD.of(context); progress?.showWithText("登入中..."); Future.delayed(const Duration(milliseconds: 1000), () { //延遲1秒進行判斷 progress?.dismiss(); if (MysqlMenu.isEmpty) { //如果取得的列表是空的,也就是帳號錯誤 print('帳號輸入有誤!'); setState(() { login_state = "帳號輸入有誤!"; }); return; } if (password == MysqlMenu[0].password) { //如果帳號密碼都對 setState(() { DataMenu[0].id=MysqlMenu[0].id; DataMenu[0].account=MysqlMenu[0].account; DataMenu[0].password=MysqlMenu[0].password; }); print('成功登入'); setState(() { login_state = ""; }); Navigator.push(context, MaterialPageRoute(builder: (context) => MainPage(DataMenu))); } else { //如果密碼錯誤 print('密碼輸入有誤'); setState(() { login_state = "密碼輸入有誤"; }); return; } }); } ``` ### 帳號密碼輸入框格式 ``` //輸入框格式 Widget buildTextField(String hintText) { return TextField( controller: hintText == "帳號" ? _accountcontroller : _pwcontroller, decoration: InputDecoration( hintText: hintText, hintStyle: const TextStyle( color: Colors.grey, fontSize: 16.0, ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(20.0), ), prefixIcon: hintText == "帳號" ? const Icon(Icons.email) : const Icon(Icons.lock), suffixIcon: hintText == "密碼" ? IconButton( onPressed: _toggleVisibility, icon: _isHidden ? const Icon(Icons.visibility_off) : const Icon(Icons.visibility), ) : null, ), obscureText: hintText == "密碼" ? _isHidden : false, ); } ``` ### 登入按鈕 ``` //登入按鈕 Widget buildButtonContainer(BuildContext context) { return SizedBox( //取得裝置的數據 width: MediaQuery.of(context).size.width - 48.0, height: 48.0, child: ElevatedButton( style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(1000), ), primary: Colors.blueAccent, // background onPrimary: Colors.white, // foreground ), child: const Text( "登入", style: TextStyle(fontSize: 25), ), onPressed: () { _LoginJudgment(_accountcontroller.text, _pwcontroller.text, context); }, ), ); } ``` ### 顯示退出APP對話框 ``` // 顯示退出APP對話框 void showAlertDialog(BuildContext context) { // Init AlertDialog dialog = AlertDialog( shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(15)), ), title: RichText( text: const TextSpan( children: [ TextSpan( text: "確定退出APP?", style: TextStyle( fontSize: 25, color: Colors.black, ), ), WidgetSpan( child: SizedBox( width: 40, ), ), WidgetSpan( child: Icon( Icons.logout, size: 30, color: Colors.blue, ), ), ], ), ), actions: [ Center( child: Container( child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( style: ButtonStyle( backgroundColor: MaterialStateProperty.all<Color>(Colors.white), ), child: const Text( "取消", style: TextStyle(fontSize: 20, color: Colors.blue), ), onPressed: () { Navigator.pop(context); }, ), const SizedBox( width: 30, ), ElevatedButton( style: ButtonStyle( backgroundColor: MaterialStateProperty.all<Color>(Colors.blue), ), child: const Text( "確認退出", style: TextStyle(fontSize: 20), ), onPressed: () { //引入import 'package:flutter/services.dart'; //以及import 'dart:io'; // if (Platform.isAndroid) { // SystemNavigator.pop(); // } else if (Platform.isIOS) { // exit(0); // } exit(0); }, ), ], ), ), ), ], ); // Show the dialog (showDialog() => showGeneralDialog()) //確認框的動畫 showGeneralDialog( context: context, pageBuilder: (context, anim1, anim2) { return Wrap(); }, transitionBuilder: (context, anim1, anim2, child) { return Transform( transform: Matrix4.translationValues( 0.0, (1.0 - Curves.easeInOut.transform(anim1.value)) * 400, 0.0, ), child: dialog, ); }, transitionDuration: const Duration(milliseconds: 400), ); } ``` ## 模型 ### RNNoise: - [ ] 環境架設 - [ ] demo - [ ] 導出 ### 語音模型: - [ ] 環境架設 - [x] demo - [ ] 導出 Congratulations! :tada: You have created a team workspace, where all team members collaborate on their shared notes. [toc] ## Team roles and note permission All members in the workspace have read access to every note in it. Only writers and admins have write access, and only admins can delete a team note. Team members receive notifications when someone else edit or mention them in a team note. Team members are all considered **Owners** to notes in the workspace when configuring note permissions. ## How do I invite team members? 1. In the Overview page, click the :gear: at the right of **Team space**, and click **Team settings**. <img src="https://hackmd.io/_uploads/HyJfG7jYK.png" width=300 style="display:block;margin:30px auto" /> 2. In **Team Settings** page, 1. Fill in the HackMD username or the email address of the member you'd like to add. S/he will receive an invitation email and join the workspace by following the instructions. 3. Set her/his role. - Admin: can manage members and billings (with a billing role), read and write notes. - Writer: read and write notes. - Reader: read notes. 4. Click **Add**. :::warning :bulb: This role takes a higher priority to the default Owners of the note (that is, all workspace members). So a Reader member can't write a note in the workspace even if it's set to "Owners can write". ::: <img src="https://hackmd.io/_uploads/ryxdzmjtt.png" width=600 style="display:block;margin: 30px auto" /> Invite more members to get the most out of your Team Workspace. If you have met the limit of workspace members, you can [puchase more](https://hackmd.io/c/tutorials/%2F%40docs%2Funlock-team-member-limit?utm_source=prebuilt-note&utm-medium=inline-link). ## How do I work with someone not in my Team Workspace? Invite them as invitees on the note. Here's [how](https://hackmd.io/c/tutorials/%2Fs%2Finvite). ## How do I backup the notes? In **Team settings** page, navigate to **Team notes** and click **Download all notes**. <img src="https://hackmd.io/_uploads/rygDBQoFY.png" width=300 style="display:block;margin: 30px auto" /> ## How do I set default permissions? :::info This is a Prime only feature. ::: Set default permissions so you don't have to worry if any of your team members would ever forgot to set them properly. 1. Set default permissions of the notes in **Team Settings** > **Team notes**. 2. Navigate down and click **Save**. <img src="https://hackmd.io/_uploads/Syo3rXoKF.png" width=600 style="display:block;margin: 30px auto" /> ## How do I switch between a public or private team? There is no limit counts to members of a Public Team, so it's great for sharing knowledge and working with the public. For the greater good of our communities, HackMD lets you switch between the two, so you can switch between them as your community grows. Here's [how](https://hackmd.io/@docs/switch-public-private-team) to switch. # How to get in touch? Feel free to ping us via: - :mailbox: support@hackmd.io - Mention `@hackmdio` on [Twitter](https://twitter.com/hackmdio)