From d504e08874fb004ed8f0f92196c75dc124a34f8b Mon Sep 17 00:00:00 2001 From: TU-Lin Date: Thu, 26 Jan 2023 16:48:32 +0800 Subject: [PATCH] Improve bottom bar and file downloads (#158) * feat: change bottom nav bar style to fixed * refactor: migrate file store to null safety * fix: illegal access firebase for auto roll call while feature not enabled * refactor: migrate toast to null safety * refactor: migrate permission util to null safety * refactor: migrate file download to null safety, and add dialog while download complete * refactor: migrade iplus file page to null safety * chore: remove unneeded aos permissions --- android/app/src/main/AndroidManifest.xml | 7 +- lib/src/file/file_download.dart | 155 +++++++------- lib/src/file/file_store.dart | 11 +- ...to_roll_call_schedule_repository_impl.dart | 7 +- lib/src/util/permissions_util.dart | 10 +- lib/ui/other/my_toast.dart | 17 +- .../screen/ischoolplus/iplus_file_page.dart | 191 +++++++++--------- lib/ui/screen/main_screen.dart | 2 +- 8 files changed, 198 insertions(+), 202 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e3cc0215..a6562099 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -9,10 +9,6 @@ - - - - @@ -21,8 +17,7 @@ android:icon="@mipmap/ic_launcher" android:usesCleartextTraffic="true" android:requestLegacyExternalStorage="true" - android:label="TAT" - tools:targetApi="m"> + android:label="TAT"> download(String url, dirName, [String name = "", String referer]) async { - String path = await FileStore.getDownloadDir(dirName); //取得下載路徑 - String realFileName; - String fileExtension; + static Future download(String url, dirName, [String name = "", String? referer]) async { + final path = await FileStore.getDownloadDir(dirName); + String? realFileName = ""; + String? fileExtension = ""; referer = referer ?? url; Log.d("file download \n url: $url \n referer: $referer"); - //顯示下載通知窗 - ReceivedNotification value = - ReceivedNotification(title: name, body: R.current.prepareDownload, payload: null); //通知窗訊息 - CancelToken cancelToken; //取消下載用 - ProgressCallback onReceiveProgress; //下載進度回調 + final value = ReceivedNotification(title: name, body: R.current.prepareDownload, payload: null); //通知窗訊息 + final cancelToken = CancelToken(); await Notifications.instance.showIndeterminateProgressNotification(value); - //顯示下載進度通知窗 value.title = name; int nowSize = 0; - onReceiveProgress = (int count, int total) async { + onReceiveProgress(int count, int total) { value.body = FileUtils.formatBytes(count, 2); if ((nowSize + 1024 * 128) > count && nowSize != 0) { - //128KB顯示一次 return; } nowSize = count; if (count < total) { Notifications.instance.showProgressNotification(value, 100, (count * 100 / total).round()); //顯示下載進度 } else { - Notifications.instance.showIndeterminateProgressNotification(value); //顯示下載進度 + Notifications.instance.showIndeterminateProgressNotification(value); } - }; - //開始下載檔案 - DioConnector.instance.download(url, (Headers responseHeaders) { - Map> headers = responseHeaders.map; - if (headers.containsKey("content-disposition")) { - //代表有名字 - List name = headers["content-disposition"]; - RegExp exp = RegExp("['|\"](?.+)['|\"]"); //尋找 'name' , "name" 的name - RegExpMatch matches = exp.firstMatch(name[0]); - realFileName = matches.group(1); - } else if (headers.containsKey("content-type")) { - List name = headers["content-type"]; - if (name[0].toLowerCase().contains("pdf")) { - //是application/pdf - realFileName = '.pdf'; + } + + // This flag is used to prevent the dialog from being displayed multiple times. + bool hasError = false; + await DioConnector.instance.download( + url, + (responseHeaders) { + final Map> headers = responseHeaders.map; + if (headers.containsKey("content-disposition")) { + final name = headers["content-disposition"]; + final exp = RegExp("['|\"](?.+)['|\"]"); + final matches = name != null ? exp.firstMatch(name[0]) : null; + realFileName = matches?.group(1); + } else if (headers.containsKey("content-type")) { + final name = headers["content-type"]; + if (name?[0].toLowerCase().contains("pdf") == true) { + realFileName = '.pdf'; + } } - } - if (!name.contains(".")) { - //代表名字不包含副檔名 - if (realFileName != null) { - //代表可以從網路取得副檔名 - fileExtension = realFileName.split(".").reversed.toList()[0]; - realFileName = "$name.$fileExtension"; - } else { - //嘗試使用網址後面找出附檔名 - String maybeName = url.split("/").toList().last; - if (maybeName.contains(".")) { - fileExtension = maybeName.split(".").toList().last; + if (!name.contains(".")) { + if (realFileName != null) { + fileExtension = realFileName?.split(".").reversed.toList()[0]; realFileName = "$name.$fileExtension"; + } else { + final maybeName = url.split("/").toList().last; + if (maybeName.contains(".")) { + fileExtension = maybeName.split(".").toList().last; + realFileName = "$name.$fileExtension"; + } + } + } else { + final List s = name.split("."); + s.removeLast(); + if (realFileName != null && realFileName?.contains(".") == true) { + realFileName = '${s.join()}.${realFileName?.split(".").last ?? ""}'; } - //realFileName = name + "." + fileExtension; - } - } else { - //代表包含. - List s = name.split("."); - s.removeLast(); - if (realFileName != null && realFileName.contains(".")) { - realFileName = '${s.join()}.${realFileName.split(".").last}'; } - } - realFileName = realFileName ?? name; //如果還是沒有找到副檔名直接使用原始名稱 - //print(path + "/" + realFileName); - return "$path/$realFileName"; - }, progressCallback: onReceiveProgress, cancelToken: cancelToken, header: {"referer": referer}).whenComplete( - () async { - //顯示下載萬完成通知窗 - await Notifications.instance.cancelNotification(value.id); - value.body = R.current.downloadComplete; - value.id = Notifications.instance.notificationId; //取得新的id - String filePath = '$path/$realFileName'; - int id = value.id; - value.payload = json.encode({ - "type": "download_complete", - "path": filePath, - "id": id, - }); - await Notifications.instance.showNotification(value); //顯示下載完成 + realFileName = realFileName ?? name; + return "$path/$realFileName"; }, + progressCallback: onReceiveProgress, + cancelToken: cancelToken, + header: {"referer": referer}, ).catchError( (onError) async { - //顯示下載萬完成通知窗 + hasError = true; Log.d(onError.toString()); await Future.delayed(const Duration(milliseconds: 100)); Notifications.instance.cancelNotification(value.id); - value.body = "下載失敗"; - value.id = Notifications.instance.notificationId; //取得新的id - int id = value.id; + + value.body = R.current.downloadError; + value.id = Notifications.instance.notificationId; value.payload = json.encode({ "type": "download_fail", - "id": id, + "id": value.id, }); - await Notifications.instance.showNotification(value); //顯示下載完成 + + await Notifications.instance.showNotification(value); + ErrorDialog(ErrorDialogParameter( + desc: realFileName, + title: R.current.downloadError, + offCancelBtn: true, + dialogType: DialogType.warning, + )).show(); }, ); + + if (!hasError) { + await Notifications.instance.cancelNotification(value.id); + value.body = R.current.downloadComplete; + value.id = Notifications.instance.notificationId; + value.payload = json.encode({ + "type": "download_complete", + "path": '$path/$realFileName', + "id": value.id, + }); + await Notifications.instance.showNotification(value); + ErrorDialog(ErrorDialogParameter( + desc: realFileName, + title: R.current.downloadComplete, + offCancelBtn: true, + dialogType: DialogType.success, + )).show(); + } } } diff --git a/lib/src/file/file_store.dart b/lib/src/file/file_store.dart index 9f1cd112..49625890 100644 --- a/lib/src/file/file_store.dart +++ b/lib/src/file/file_store.dart @@ -1,5 +1,3 @@ -// TODO: remove sdk version selector after migrating to null-safety. -// @dart=2.10 import 'dart:convert'; import 'dart:io'; @@ -19,11 +17,12 @@ class FileStore { return ''; } - final directory = await _getFilePath() ?? Platform.isAndroid + final filePath = await _getFilePath(); + final directory = filePath != null || Platform.isAndroid ? await getExternalStorageDirectory() : await getApplicationSupportDirectory(); - final targetDir = Directory('${directory.path}/TAT'); + final targetDir = Directory('${directory?.path ?? ''}/TAT'); final hasExisted = await targetDir.exists(); if (!hasExisted) { targetDir.create(); @@ -44,7 +43,7 @@ class FileStore { return savedDir.path; } - static Future setFilePath(String directory) async { + static Future setFilePath(String? directory) async { if (directory != null) { final pref = await SharedPreferences.getInstance(); pref.setString(storeKey, base64Encode(directory.codeUnits)); @@ -54,7 +53,7 @@ class FileStore { return false; } - static Future _getFilePath() async { + static Future _getFilePath() async { final pref = await SharedPreferences.getInstance(); final path = pref.getString(storeKey); if (path != null && path.isNotEmpty) { diff --git a/lib/src/repositories/auto_roll_call_schedule_repository_impl.dart b/lib/src/repositories/auto_roll_call_schedule_repository_impl.dart index 1924b23f..324c78a2 100644 --- a/lib/src/repositories/auto_roll_call_schedule_repository_impl.dart +++ b/lib/src/repositories/auto_roll_call_schedule_repository_impl.dart @@ -19,9 +19,7 @@ class AutoRollCallScheduleRepositoryImpl implements AutoRollCallScheduleReposito required FirebaseMessaging firebaseMessaging, }) : assert(firebaseAuth.currentUser != null), _firebaseAuth = firebaseAuth, - _firebaseMessaging = firebaseMessaging { - _createUserDocumentIfNotExists(); - } + _firebaseMessaging = firebaseMessaging; final FirebaseAuth _firebaseAuth; final FirebaseMessaging _firebaseMessaging; @@ -39,7 +37,8 @@ class AutoRollCallScheduleRepositoryImpl implements AutoRollCallScheduleReposito return true; } - Future _createUserDocumentIfNotExists() async { + // TODO: call this method when user sign in, if the auto roll call feature is enabled. + Future createUserDocumentIfNotExists() async { final isSignedIn = _checkUserSignedIn(); if (!isSignedIn) return; diff --git a/lib/src/util/permissions_util.dart b/lib/src/util/permissions_util.dart index 487b2d83..fe2643de 100644 --- a/lib/src/util/permissions_util.dart +++ b/lib/src/util/permissions_util.dart @@ -1,5 +1,3 @@ -// TODO: remove sdk version selector after migrating to null-safety. -// @dart=2.10 import 'dart:io'; import 'package:permission_handler/permission_handler.dart'; @@ -15,12 +13,6 @@ class PermissionsUtil { if (Platform.isIOS) return true; assert(Platform.isAndroid, 'The platform most be either aos or ios.'); - final storagePermissionStatus = await Permission.storage.status; - if (storagePermissionStatus != PermissionStatus.granted) { - final requestedPermissions = await [Permission.storage].request(); - return requestedPermissions[Permission.storage] == PermissionStatus.granted; - } - - return true; + return await Permission.storage.request().isGranted; } } diff --git a/lib/ui/other/my_toast.dart b/lib/ui/other/my_toast.dart index 25298db3..cbb8689a 100644 --- a/lib/ui/other/my_toast.dart +++ b/lib/ui/other/my_toast.dart @@ -1,5 +1,3 @@ -// TODO: remove sdk version selector after migrating to null-safety. -// @dart=2.10 import 'package:flutter/material.dart'; import 'package:flutter_app/src/config/app_colors.dart'; import 'package:fluttertoast/fluttertoast.dart'; @@ -7,12 +5,13 @@ import 'package:fluttertoast/fluttertoast.dart'; class MyToast { static void show(String message) { Fluttertoast.showToast( - msg: message, - toastLength: Toast.LENGTH_SHORT, - gravity: ToastGravity.BOTTOM, - timeInSecForIosWeb: 1, - backgroundColor: AppColors.mainColor, - textColor: Colors.white, - fontSize: 16.0); + msg: message, + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + timeInSecForIosWeb: 1, + backgroundColor: AppColors.mainColor, + textColor: Colors.white, + fontSize: 16.0, + ); } } diff --git a/lib/ui/pages/coursedetail/screen/ischoolplus/iplus_file_page.dart b/lib/ui/pages/coursedetail/screen/ischoolplus/iplus_file_page.dart index c86260af..ac41ed8f 100644 --- a/lib/ui/pages/coursedetail/screen/ischoolplus/iplus_file_page.dart +++ b/lib/ui/pages/coursedetail/screen/ischoolplus/iplus_file_page.dart @@ -1,5 +1,5 @@ -// TODO: remove sdk version selector after migrating to null-safety. -// @dart=2.10 +// ignore_for_file: import_of_legacy_library_into_null_safe + import 'package:awesome_dialog/awesome_dialog.dart'; import 'package:back_button_interceptor/back_button_interceptor.dart'; import 'package:flutter/material.dart'; @@ -24,16 +24,20 @@ class IPlusFilePage extends StatefulWidget { final CourseInfoJson courseInfo; final String studentId; - const IPlusFilePage(this.studentId, this.courseInfo, {Key key}) : super(key: key); + const IPlusFilePage( + this.studentId, + this.courseInfo, { + super.key, + }); @override State createState() => _IPlusFilePage(); } class _IPlusFilePage extends State with AutomaticKeepAliveClientMixin { - List courseFileList = []; - SelectList selectList = SelectList(); - bool isSupport; + final List courseFileList = []; + final selectList = SelectList(); + bool isSupport = false; @override void initState() { @@ -56,52 +60,59 @@ class _IPlusFilePage extends State with AutomaticKeepAliveClientM bool myInterceptor(bool stopDefaultButtonEvent, RouteInfo routeInfo) { if (selectList.inSelectMode) { - selectList.leaveSelectMode(); - setState(() {}); + setState(() { + selectList.leaveSelectMode(); + }); return true; - } else { - return false; } + + return false; } void _addTask() async { await Future.delayed(const Duration(microseconds: 500)); - String courseId = widget.courseInfo.main.course.id; + final courseId = widget.courseInfo.main.course.id; - TaskFlow taskFlow = TaskFlow(); - var task = IPlusCourseFileTask(courseId); + final taskFlow = TaskFlow(); + final task = IPlusCourseFileTask(courseId); taskFlow.addTask(task); + if (await taskFlow.start()) { - courseFileList = task.result; + final result = task.result; + if (result != null) { + courseFileList.addAll(result); + } } - courseFileList = courseFileList ?? []; - selectList.addItems(courseFileList.length); - setState(() {}); + + setState(() { + selectList.addItems(courseFileList.length); + }); } @override Widget build(BuildContext context) { - super.build(context); //如果使用AutomaticKeepAliveClientMixin需要呼叫 + super.build(context); return Scaffold( - body: (courseFileList.isNotEmpty) - ? _buildFileList() - : (isSupport) - ? Center( - child: Text(R.current.noAnyFile), - ) - : Center( - child: Text(R.current.notSupport), - ), - floatingActionButton: (selectList.inSelectMode) - ? FloatingActionButton( - // FloatingActionButton: 浮動按鈕 - onPressed: _floatingDownloadPress, - // 按下觸發的方式名稱: void _incrementCounter() - tooltip: R.current.download, - // 按住按鈕時出現的提示字 - child: const Icon(Icons.file_download), - ) - : null); + body: (courseFileList.isNotEmpty) + ? _buildFileList() + : (isSupport) + ? Center( + child: Text(R.current.noAnyFile), + ) + : Center( + child: Text(R.current.notSupport), + ), + floatingActionButton: (selectList.inSelectMode) + ? FloatingActionButton( + // FloatingActionButton: 浮動按鈕 + onPressed: _floatingDownloadPress, + // 按下觸發的方式名稱: void _incrementCounter() + tooltip: R.current.download, + // 按住按鈕時出現的提示字 + child: const Icon(Icons.file_download), + ) + : null, + ); } Future _floatingDownloadPress() async { @@ -111,48 +122,45 @@ class _IPlusFilePage extends State with AutomaticKeepAliveClientM await _downloadOneFile(i, false); } } - selectList.leaveSelectMode(); - setState(() {}); + + setState(() { + selectList.leaveSelectMode(); + }); } - Widget _buildFileList() { - return Column( - children: [ - Expanded( - child: ListView.separated( - itemCount: courseFileList.length, - itemBuilder: (context, index) { - return GestureDetector( - behavior: HitTestBehavior.opaque, //讓透明部分有反應 + Widget _buildFileList() => Column( + children: [ + Expanded( + child: ListView.separated( + itemCount: courseFileList.length, + itemBuilder: (context, index) => GestureDetector( + behavior: HitTestBehavior.opaque, child: _buildCourseFile(index, courseFileList[index]), onTap: () { if (selectList.inSelectMode) { - selectList.setItemReverse(index); - setState(() {}); + setState(() { + selectList.setItemReverse(index); + }); } else { _downloadOneFile(index); } }, onLongPress: () { if (!selectList.inSelectMode) { - selectList.setItemReverse(index); - setState(() {}); + setState(() { + selectList.setItemReverse(index); + }); } }, - ); - }, - separatorBuilder: (context, index) { - // 顯示格線 - return Container( + ), + separatorBuilder: (context, index) => Container( color: Colors.black12, height: 1, - ); - }, + ), + ), ), - ), - ], - ); - } + ], + ); List iconList = [ const Icon( @@ -185,19 +193,17 @@ class _IPlusFilePage extends State with AutomaticKeepAliveClientM ) ]; - Widget _buildCourseFile(int index, CourseFileJson courseFile) { - return Container( - color: selectList.getItemSelect(index) ? Colors.grey : Theme.of(context).backgroundColor, - padding: const EdgeInsets.all(10), - child: Column( - children: _buildFileItem(courseFile), - )); - } + Widget _buildCourseFile(int index, CourseFileJson courseFile) => Container( + color: selectList.getItemSelect(index) ? Colors.grey : Theme.of(context).backgroundColor, + padding: const EdgeInsets.all(10), + child: Column( + children: _buildFileItem(courseFile), + )); List _buildFileItem(CourseFileJson courseFile) { - List widgetList = []; - List iconWidgetList = []; - for (FileType fileType in courseFile.fileType) { + final List widgetList = []; + final List iconWidgetList = []; + for (final fileType in courseFile.fileType) { iconWidgetList.add(iconList[fileType.type.index]); } widgetList.add( @@ -222,27 +228,26 @@ class _IPlusFilePage extends State with AutomaticKeepAliveClientM } Future _downloadOneFile(int index, [showToast = true]) async { - CourseFileJson courseFile = courseFileList[index]; - FileType fileType = courseFile.fileType[0]; - String dirName = widget.courseInfo.main.course.name; - String url; - String referer; - List urlList = []; + final courseFile = courseFileList[index]; + final fileType = courseFile.fileType[0]; + final dirName = widget.courseInfo.main.course.name; + String url = ""; + String referer = ""; + await AnalyticsUtils.logDownloadFileEvent(); if (showToast) { MyToast.show(R.current.downloadWillStart); } - urlList = await ISchoolPlusConnector.getRealFileUrl(fileType.postData); + final urlList = await ISchoolPlusConnector.getRealFileUrl(fileType.postData) as List?; if (urlList == null) { MyToast.show(sprintf("%s%s", [courseFile.name, R.current.downloadError])); return; } url = urlList[0]; referer = urlList[1]; - Uri urlParse = Uri.parse(url); + final urlParse = Uri.parse(url); if (!urlParse.host.toLowerCase().contains("ntut.edu.tw")) { - //代表可能是一個連結 - ErrorDialogParameter errorDialogParameter = ErrorDialogParameter(context: context, desc: R.current.isALink); + final errorDialogParameter = ErrorDialogParameter(context: context, desc: R.current.isALink); errorDialogParameter.title = R.current.AreYouSureToOpen; errorDialogParameter.dialogType = DialogType.info; errorDialogParameter.btnOkText = R.current.sure; @@ -251,8 +256,10 @@ class _IPlusFilePage extends State with AutomaticKeepAliveClientM }; ErrorDialog(errorDialogParameter).show(); return; - } else if (urlParse.host.contains("istream.ntut.edu.tw")) { - ErrorDialogParameter errorDialogParameter = ErrorDialogParameter( + } + + if (urlParse.host.contains("istream.ntut.edu.tw")) { + final errorDialogParameter = ErrorDialogParameter( context: context, desc: '${R.current.isVideo}\n${R.current.videoMayLoadFailedWarningMsg}', ); @@ -296,30 +303,28 @@ class SelectList { void setItemSelect(int index, bool value) { if (index >= _selectList.length) { return; - } else { - _selectList[index] = value; } + _selectList[index] = value; } void setItemReverse(int index) { if (index >= _selectList.length) { return; - } else { - _selectList[index] = !_selectList[index]; } + _selectList[index] = !_selectList[index]; } bool getItemSelect(int index) { if (index >= _selectList.length) { return false; - } else { - return _selectList[index]; } + + return _selectList[index]; } bool get inSelectMode { bool select = false; - for (bool value in _selectList) { + for (final value in _selectList) { select |= value; } return select; diff --git a/lib/ui/screen/main_screen.dart b/lib/ui/screen/main_screen.dart index 6ebdb8ae..51d434e5 100644 --- a/lib/ui/screen/main_screen.dart +++ b/lib/ui/screen/main_screen.dart @@ -149,7 +149,7 @@ class _MainScreenState extends State with RouteAware { return BottomNavigationBar( currentIndex: _currentIndex, - type: BottomNavigationBarType.shifting, + type: BottomNavigationBarType.fixed, onTap: _onTap, selectedItemColor: selectedItemColor, unselectedItemColor: unSelectedItemColor,