From cf7a8261ebd52f08109f71583cb7d22e962b5833 Mon Sep 17 00:00:00 2001 From: Robert-Stackflow Date: Tue, 20 Aug 2024 00:21:11 +0800 Subject: [PATCH] Release 2.1.0 --- README.md | 35 +-- README_CN.md | 22 ++ android/app/build.gradle | 4 + android/build.gradle | 7 +- certificate/CloudOTP.iss | 4 +- encrypted.db | Bin 28672 -> 0 bytes lib/Screens/Backup/cloud_service_screen.dart | 107 ++++------ .../Backup/dropbox_service_screen.dart | 88 ++------ .../Backup/googledrive_service_screen.dart | 83 ++----- .../Backup/onedrive_service_screen.dart | 83 ++----- lib/Screens/Backup/s3_service_screen.dart | 76 +------ lib/Screens/Backup/webdav_service_screen.dart | 83 ++----- lib/Screens/Lock/database_decrypt_screen.dart | 1 - .../Setting/setting_backup_screen.dart | 11 +- .../Setting/setting_general_screen.dart | 114 +++++----- lib/Screens/Setting/setting_screen.dart | 1 - lib/Screens/Token/add_token_screen.dart | 1 - lib/Screens/Token/category_screen.dart | 86 ++++---- .../Token/import_export_token_screen.dart | 34 +-- lib/Screens/home_screen.dart | 50 +++-- lib/TokenUtils/Cloud/cloud_service.dart | 2 +- .../Cloud/dropbox_cloud_service.dart | 39 ++-- .../Cloud/googledrive_cloud_service.dart | 35 +-- .../Cloud/onedrive_cloud_service.dart | 33 +-- lib/TokenUtils/Cloud/s3_cloud_service.dart | 42 ++-- .../Cloud/webdav_cloud_service.dart | 74 ++++--- lib/TokenUtils/import_token_util.dart | 73 +++++++ lib/Utils/file_util.dart | 37 +++- lib/Utils/utils.dart | 11 +- .../BottomSheet/input_bottom_sheet.dart | 39 ++-- .../input_password_bottom_sheet.dart | 1 - .../select_category_bottom_sheet.dart | 4 +- lib/Widgets/Item/input_item.dart | 14 +- lib/Widgets/Item/item_builder.dart | 17 +- lib/l10n/intl_en.arb | 3 +- lib/l10n/intl_zh_CN.arb | 3 +- pubspec.lock | 7 +- pubspec.yaml | 5 +- .../flutter_dropbox/lib/flutter_dropbox.dart | 9 +- third-party/flutter_dropbox/lib/token.dart | 8 +- .../flutter_googledrive/lib/token.dart | 6 +- third-party/flutter_onedrive/lib/token.dart | 4 +- .../flutter_windowmanager/CHANGELOG.md | 21 ++ third-party/flutter_windowmanager/LICENSE | 202 ++++++++++++++++++ third-party/flutter_windowmanager/README.md | 108 ++++++++++ .../android/build.gradle | 35 +++ .../android/gradle.properties | 2 + .../gradle/wrapper/gradle-wrapper.properties | 5 + .../android/local.properties | 2 + .../android/settings.gradle | 1 + .../android/src/main/AndroidManifest.xml | 3 + .../FlutterWindowManagerPlugin.java | 166 ++++++++++++++ .../ios/Classes/FlutterWindowmanagerPlugin.h | 4 + .../ios/Classes/FlutterWindowmanagerPlugin.m | 15 ++ .../SwiftFlutterWindowmanagerPlugin.swift | 14 ++ .../ios/flutter_windowmanager.podspec | 23 ++ .../lib/flutter_windowmanager.dart | 122 +++++++++++ .../flutter_windowmanager/pubspec.yaml | 27 +++ .../webdav_client/lib/src/webdav_dio.dart | 2 - 59 files changed, 1375 insertions(+), 733 deletions(-) create mode 100644 README_CN.md delete mode 100644 encrypted.db create mode 100644 third-party/flutter_windowmanager/CHANGELOG.md create mode 100644 third-party/flutter_windowmanager/LICENSE create mode 100644 third-party/flutter_windowmanager/README.md create mode 100644 third-party/flutter_windowmanager/android/build.gradle create mode 100644 third-party/flutter_windowmanager/android/gradle.properties create mode 100644 third-party/flutter_windowmanager/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 third-party/flutter_windowmanager/android/local.properties create mode 100644 third-party/flutter_windowmanager/android/settings.gradle create mode 100644 third-party/flutter_windowmanager/android/src/main/AndroidManifest.xml create mode 100644 third-party/flutter_windowmanager/android/src/main/java/io/adaptant/labs/flutter_windowmanager/FlutterWindowManagerPlugin.java create mode 100644 third-party/flutter_windowmanager/ios/Classes/FlutterWindowmanagerPlugin.h create mode 100644 third-party/flutter_windowmanager/ios/Classes/FlutterWindowmanagerPlugin.m create mode 100644 third-party/flutter_windowmanager/ios/Classes/SwiftFlutterWindowmanagerPlugin.swift create mode 100644 third-party/flutter_windowmanager/ios/flutter_windowmanager.podspec create mode 100644 third-party/flutter_windowmanager/lib/flutter_windowmanager.dart create mode 100644 third-party/flutter_windowmanager/pubspec.yaml diff --git a/README.md b/README.md index 340476dd..8ab8d079 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,17 @@ ## Introduction -This is a awesome two-factor authenticator for Android which supports dropbox. - -The algorithm part comes from https://github.com/freeotp/freeotp-android. +This is an awesome two-factor authenticator based on Flutter for Android and Windows which supports cloud backup. ## Highlights -- Support TOTP and HOTP -- Support manual filling and QR code scanning to add tokens -- Support import/export of JSON/URI file -- Support import/export of encrypted files (using standard AES-256 algorithm) -- Support backing up encrypted files to Dropbox -- Support password lock and biometric identification -- Support dark mode and switching theme colors -- Support sort;batch export;batch delete -- Support multiple languages: English, Simplified Chinese, Traditional Chinese, Japanese +- Reconstructed based on Flutter architecture, supports Android and Windows +- Support TOTP, HOTP, MOTP, Steam, Yandex +- Supports scanning QR code to add, identify pictures, and manually enter keys +- Supports custom icons and categories, supports sorting and multiple token layouts +- Supports dark mode, multiple languages, and multiple themes +- Supports local backup and automatic backup, supports WebDav, Onedrive, GoogleDrive, Dropbox, S3 storage and other cloud backup methods +- Supports import/export of encrypted files and URI lists +- Supports database encryption and gesture password ## Screenshots @@ -22,16 +19,4 @@ The algorithm part comes from https://github.com/freeotp/freeotp-android. SettingThemeLock -Export and  ImportDropbox -## TODO - -- [ ] Support Google Drive -- [ ] Support WebDAV services such as Box -- [ ] Support more encryption algorithms -- [ ] Support encrypting local SQLite database -- [ ] ~~Support desktop widgets(No longer implemented due to security considerations)~~ - -### Known Bugs - -- [ ] When exporting a file, if you overwrite an existing file, the original article content cannot be cleared. -- [ ] When importing an encrypted file, if the file name is illegal (such as containing spaces), the import will fail. \ No newline at end of file +Export and  ImportDropbox \ No newline at end of file diff --git a/README_CN.md b/README_CN.md new file mode 100644 index 00000000..58a17b3f --- /dev/null +++ b/README_CN.md @@ -0,0 +1,22 @@ +## 介绍 + +基于 Flutter 的双因素验证器,支持Android和Windows平台,支持云备份。 + +## Highlights + +- 基于Flutter架构重构,支持Android和Windows +- 支持TOTP、HOTP、MOTP、Steam、Yandex +- 支持扫码添加、识别图片、手动输入密钥 +- 支持自定义图标和分类、支持排序和多种令牌布局 +- 支持深色模式、多种语言、多种主题 +- 支持本地备份和自动备份、支持WebDav、Onedrive、GoogleDrive、Dropbox、S3存储等多种云备份方式 +- 支持导入/导出加密文件、URI列表 +- 支持数据库加密、手势密码 + +## Screenshots + +Light ModeDark ModeAdd Token + +SettingThemeLock + +Export and  ImportDropbox \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index f9b6de46..5813fb20 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,3 +1,7 @@ +buildscript { + ext.kotlin_version = '1.8.0' +} + plugins { id "com.android.application" id "kotlin-android" diff --git a/android/build.gradle b/android/build.gradle index 619283de..fcaa6c50 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -16,9 +16,14 @@ allprojects { rootProject.buildDir = '../build' subprojects { - project.buildDir = "${rootProject.buildDir}/${project.name}" + afterEvaluate { + android { + compileSdkVersion 34 + } + } } subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" project.evaluationDependsOn(':app') } diff --git a/certificate/CloudOTP.iss b/certificate/CloudOTP.iss index c1dc271e..e47de617 100644 --- a/certificate/CloudOTP.iss +++ b/certificate/CloudOTP.iss @@ -2,7 +2,7 @@ ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "CloudOTP" -#define MyAppVersion "2.0.0" +#define MyAppVersion "2.1.0" #define MyAppPublisher "Cloudchewie" #define MyAppURL "https://apps.cloudchewie.com/cloudotp" #define MyAppExeName "CloudOTP.exe" @@ -33,7 +33,7 @@ DisableProgramGroupPage=yes LicenseFile=D:\Repositories\CloudOTP\LICENSE ; Remove the following line to run in administrative install mode (install for all users.) PrivilegesRequiredOverridesAllowed=commandline -OutputDir=C:\Users\dell\Downloads +OutputDir=D:\Ruida\Downloads OutputBaseFilename={#MyAppName}-{#MyAppVersion} SetupIconFile=D:\Repositories\CloudOTP\assets\logo-transparent.ico Compression=lzma diff --git a/encrypted.db b/encrypted.db deleted file mode 100644 index f759e6e0667f0f4012965ab64caba27d44682d6a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28672 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 } _buildBody() { - return ListView( - shrinkWrap: true, + return SingleChildScrollView( physics: const BouncingScrollPhysics(), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), - children: [ - Column( - children: [ - // Container( - // constraints: const BoxConstraints(maxWidth: 82), - // alignment: Alignment.center, - // decoration: BoxDecoration( - // borderRadius: BorderRadius.circular(16), - // border: - // Border.all(color: Colors.grey.withOpacity(0.1), width: 0.5), - // ), - // child: ClipRRect( - // borderRadius: BorderRadius.circular(16), - // child: Image.asset( - // 'assets/logo.png', - // height: 80, - // width: 80, - // fit: BoxFit.contain, - // ), - // ), - // ), - // const SizedBox(height: 20), - _typeInfo(), - const SizedBox(height: 10), - SizedBox( - height: MediaQuery.sizeOf(context).height - 150, - child: PageView( - physics: const NeverScrollableScrollPhysics(), - controller: pageController, - children: const [ - WebDavServiceScreen(), - OneDriveServiceScreen(), - GoogleDriveServiceScreen(), - DropboxServiceScreen(), - S3CloudServiceScreen(), - ], - ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _typeInfo(), + SizedBox( + height: MediaQuery.of(context).size.height, + child: PageView( + physics: const NeverScrollableScrollPhysics(), + controller: pageController, + children: const [ + WebDavServiceScreen(), + OneDriveServiceScreen(), + GoogleDriveServiceScreen(), + DropboxServiceScreen(), + S3CloudServiceScreen(), + ], ), - ], - ), - ], + ), + ], + ), ); } _typeInfo() { - return ItemBuilder.buildContainerItem( - context: context, - topRadius: true, - bottomRadius: true, - padding: const EdgeInsets.only(right: 10), - child: Column( - children: [ - ItemBuilder.buildGroupTile( - context: context, - controller: _typeController, - constraintWidth: false, - buttons: CloudServiceType.toStrings(), - onSelected: (value, index, isSelected) { - setState(() { - _currentType = index.toCloudServiceType; - }); - pageController.jumpToPage(index); - }, - title: '', - ), - ], + return Container( + margin: const EdgeInsets.only(bottom: 10), + child: ItemBuilder.buildContainerItem( + context: context, + topRadius: true, + bottomRadius: true, + padding: const EdgeInsets.only(right: 10), + child: Column( + children: [ + ItemBuilder.buildGroupTile( + context: context, + controller: _typeController, + constraintWidth: false, + buttons: CloudServiceType.toStrings(), + onSelected: (value, index, isSelected) { + setState(() { + _currentType = index.toCloudServiceType; + }); + pageController.jumpToPage(index); + }, + title: '', + ), + ], + ), ), ); } diff --git a/lib/Screens/Backup/dropbox_service_screen.dart b/lib/Screens/Backup/dropbox_service_screen.dart index 4ee7732e..9f10aa9d 100644 --- a/lib/Screens/Backup/dropbox_service_screen.dart +++ b/lib/Screens/Backup/dropbox_service_screen.dart @@ -14,7 +14,6 @@ import '../../TokenUtils/export_token_util.dart'; import '../../TokenUtils/import_token_util.dart'; import '../../Widgets/BottomSheet/bottom_sheet_builder.dart'; import '../../Widgets/BottomSheet/dropbox_backups_bottom_sheet.dart'; -import '../../Widgets/BottomSheet/input_bottom_sheet.dart'; import '../../Widgets/Dialog/custom_dialog.dart'; import '../../Widgets/Dialog/progress_dialog.dart'; import '../../Widgets/Item/input_item.dart'; @@ -53,7 +52,7 @@ class _DropboxServiceScreenState extends State @override void initState() { super.initState(); - loadConfig(); + if (!ResponsiveUtil.isDesktop()) loadConfig(); } loadConfig() async { @@ -80,6 +79,9 @@ class _DropboxServiceScreenState extends State if (_dropboxCloudService != null) { _dropboxCloudServiceConfig!.configured = _dropboxCloudServiceConfig! .connected = await _dropboxCloudService!.isConnected(); + if (!_dropboxCloudServiceConfig!.connected) { + IToast.showTop(S.current.cloudConnectionError); + } updateConfig(_dropboxCloudServiceConfig!); } inited = true; @@ -107,6 +109,8 @@ class _DropboxServiceScreenState extends State context, background: Colors.transparent, text: S.current.cloudConnecting, + mainAxisAlignment: MainAxisAlignment.start, + topPadding: 100, ); } @@ -115,7 +119,7 @@ class _DropboxServiceScreenState extends State child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ - const SizedBox(height: 50), + const SizedBox(height: 100), Text(S.current.cloudTypeNotSupport(S.current.cloudTypeDropbox)), const SizedBox(height: 10), ], @@ -132,7 +136,7 @@ class _DropboxServiceScreenState extends State } await currentService.authenticate().then((value) async { setState(() { - currentConfig.connected = value == CloudServiceStatus.success; + currentConfig.connected = (value == CloudServiceStatus.success); }); if (!currentConfig.connected) { switch (value) { @@ -147,8 +151,7 @@ class _DropboxServiceScreenState extends State break; } } else { - _dropboxCloudServiceConfig!.configured = - await _dropboxCloudService!.isConnected(); + _dropboxCloudServiceConfig!.configured = true; updateConfig(_dropboxCloudServiceConfig!); if (showSuccessToast) IToast.show(S.current.cloudAuthSuccess); } @@ -258,13 +261,15 @@ class _DropboxServiceScreenState extends State color: Theme.of(context).primaryColor, fontSizeDelta: 2, onTap: () async { - CustomLoadingDialog.showLoading( - title: S.current.webDavPulling, - dismissible: true, - ); + CustomLoadingDialog.showLoading(title: S.current.webDavPulling); try { - List files = + List? files = await _dropboxCloudService!.listBackups(); + if (files == null) { + CustomLoadingDialog.dismissLoading(); + IToast.show(S.current.webDavPullFailed); + return; + } CloudServiceConfigDao.updateLastPullTime( _dropboxCloudServiceConfig!); CustomLoadingDialog.dismissLoading(); @@ -282,71 +287,14 @@ class _DropboxServiceScreenState extends State msg: S.current.webDavPulling, showProgress: true, ); - Uint8List res = + Uint8List? res = await _dropboxCloudService!.downloadFile( selectedFile.id, onProgress: (c, t) { dialog.updateProgress(progress: c / t); }, ); - - dialog.updateMessage( - msg: S.current.importing, - showProgress: false, - ); - - bool success = await ImportTokenUtil.importBackupFile( - res, - showLoading: false, - ); - dialog.dismiss(); - if (!success) { - BottomSheetBuilder.showBottomSheet( - context, - responsive: true, - (context) => InputBottomSheet( - validator: (value) { - if (value.isEmpty) { - return S - .current.autoBackupPasswordCannotBeEmpty; - } - return null; - }, - validateAsyncController: - InputValidateAsyncController( - listen: false, - validator: (text) async { - dialog.show( - msg: S.current.importing, - showProgress: false, - ); - bool success = - await ImportTokenUtil.importBackupFile( - password: text, - res, - showLoading: false, - ); - dialog.dismiss(); - if (success) { - return null; - } else { - return S - .current.invalidPasswordOrDataCorrupted; - } - }, - controller: TextEditingController(), - ), - title: S.current.inputImportPasswordTitle, - message: S.current.inputImportPasswordTip, - hint: S.current.inputImportPasswordHint, - inputFormatters: [ - RegexInputFormatter.onlyNumberAndLetter, - ], - tailingType: InputItemTailingType.password, - onValidConfirm: (password) async {}, - ), - ); - } + ImportTokenUtil.importFromCloud(context, res, dialog); }, ), ); diff --git a/lib/Screens/Backup/googledrive_service_screen.dart b/lib/Screens/Backup/googledrive_service_screen.dart index 8d57c144..5cd65933 100644 --- a/lib/Screens/Backup/googledrive_service_screen.dart +++ b/lib/Screens/Backup/googledrive_service_screen.dart @@ -14,7 +14,6 @@ import '../../TokenUtils/export_token_util.dart'; import '../../TokenUtils/import_token_util.dart'; import '../../Widgets/BottomSheet/bottom_sheet_builder.dart'; import '../../Widgets/BottomSheet/googledrive_backups_bottom_sheet.dart'; -import '../../Widgets/BottomSheet/input_bottom_sheet.dart'; import '../../Widgets/Dialog/custom_dialog.dart'; import '../../Widgets/Dialog/progress_dialog.dart'; import '../../Widgets/Item/input_item.dart'; @@ -54,7 +53,7 @@ class _GoogleDriveServiceScreenState extends State @override void initState() { super.initState(); - loadConfig(); + if (!ResponsiveUtil.isDesktop()) loadConfig(); } loadConfig() async { @@ -83,6 +82,9 @@ class _GoogleDriveServiceScreenState extends State _googledriveCloudServiceConfig!.configured = _googledriveCloudServiceConfig!.connected = await _googledriveCloudService!.isConnected(); + if(!_googledriveCloudServiceConfig!.connected) { + IToast.showTop(S.current.cloudConnectionError); + } updateConfig(_googledriveCloudServiceConfig!); } inited = true; @@ -110,6 +112,8 @@ class _GoogleDriveServiceScreenState extends State context, background: Colors.transparent, text: S.current.cloudConnecting, + mainAxisAlignment: MainAxisAlignment.start, + topPadding: 100, ); } @@ -118,7 +122,7 @@ class _GoogleDriveServiceScreenState extends State child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ - const SizedBox(height: 50), + const SizedBox(height: 100), Text(S.current.cloudTypeNotSupport(S.current.cloudTypeGoogleDrive)), const SizedBox(height: 10), ], @@ -261,13 +265,15 @@ class _GoogleDriveServiceScreenState extends State color: Theme.of(context).primaryColor, fontSizeDelta: 2, onTap: () async { - CustomLoadingDialog.showLoading( - title: S.current.webDavPulling, - dismissible: true, - ); + CustomLoadingDialog.showLoading(title: S.current.webDavPulling); try { - List files = + List? files = await _googledriveCloudService!.listBackups(); + if (files == null) { + CustomLoadingDialog.dismissLoading(); + IToast.show(S.current.webDavPullFailed); + return; + } CloudServiceConfigDao.updateLastPullTime( _googledriveCloudServiceConfig!); CustomLoadingDialog.dismissLoading(); @@ -285,71 +291,14 @@ class _GoogleDriveServiceScreenState extends State msg: S.current.webDavPulling, showProgress: true, ); - Uint8List res = + Uint8List? res = await _googledriveCloudService!.downloadFile( selectedFile.id, onProgress: (c, t) { dialog.updateProgress(progress: c / t); }, ); - - dialog.updateMessage( - msg: S.current.importing, - showProgress: false, - ); - - bool success = await ImportTokenUtil.importBackupFile( - res, - showLoading: false, - ); - dialog.dismiss(); - if (!success) { - BottomSheetBuilder.showBottomSheet( - context, - responsive: true, - (context) => InputBottomSheet( - validator: (value) { - if (value.isEmpty) { - return S - .current.autoBackupPasswordCannotBeEmpty; - } - return null; - }, - validateAsyncController: - InputValidateAsyncController( - listen: false, - validator: (text) async { - dialog.show( - msg: S.current.importing, - showProgress: false, - ); - bool success = - await ImportTokenUtil.importBackupFile( - password: text, - res, - showLoading: false, - ); - dialog.dismiss(); - if (success) { - return null; - } else { - return S - .current.invalidPasswordOrDataCorrupted; - } - }, - controller: TextEditingController(), - ), - title: S.current.inputImportPasswordTitle, - message: S.current.inputImportPasswordTip, - hint: S.current.inputImportPasswordHint, - inputFormatters: [ - RegexInputFormatter.onlyNumberAndLetter, - ], - tailingType: InputItemTailingType.password, - onValidConfirm: (password) async {}, - ), - ); - } + ImportTokenUtil.importFromCloud(context, res, dialog); }, ), ); diff --git a/lib/Screens/Backup/onedrive_service_screen.dart b/lib/Screens/Backup/onedrive_service_screen.dart index ca6baccb..f68b0d56 100644 --- a/lib/Screens/Backup/onedrive_service_screen.dart +++ b/lib/Screens/Backup/onedrive_service_screen.dart @@ -14,7 +14,6 @@ import '../../TokenUtils/Cloud/onedrive_cloud_service.dart'; import '../../TokenUtils/export_token_util.dart'; import '../../TokenUtils/import_token_util.dart'; import '../../Widgets/BottomSheet/bottom_sheet_builder.dart'; -import '../../Widgets/BottomSheet/input_bottom_sheet.dart'; import '../../Widgets/Dialog/custom_dialog.dart'; import '../../Widgets/Dialog/progress_dialog.dart'; import '../../Widgets/Item/input_item.dart'; @@ -53,7 +52,7 @@ class _OneDriveServiceScreenState extends State @override void initState() { super.initState(); - loadConfig(); + if (!ResponsiveUtil.isDesktop()) loadConfig(); } loadConfig() async { @@ -81,6 +80,9 @@ class _OneDriveServiceScreenState extends State if (_oneDriveCloudService != null) { _oneDriveCloudServiceConfig!.configured = _oneDriveCloudServiceConfig! .connected = await _oneDriveCloudService!.isConnected(); + if (!_oneDriveCloudServiceConfig!.connected) { + IToast.showTop(S.current.cloudConnectionError); + } updateConfig(_oneDriveCloudServiceConfig!); } inited = true; @@ -108,6 +110,8 @@ class _OneDriveServiceScreenState extends State context, background: Colors.transparent, text: S.current.cloudConnecting, + mainAxisAlignment: MainAxisAlignment.start, + topPadding: 100, ); } @@ -116,7 +120,7 @@ class _OneDriveServiceScreenState extends State child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ - const SizedBox(height: 50), + const SizedBox(height: 100), Text(S.current.cloudTypeNotSupport(S.current.cloudTypeOneDrive)), const SizedBox(height: 10), ], @@ -259,13 +263,15 @@ class _OneDriveServiceScreenState extends State color: Theme.of(context).primaryColor, fontSizeDelta: 2, onTap: () async { - CustomLoadingDialog.showLoading( - title: S.current.webDavPulling, - dismissible: true, - ); + CustomLoadingDialog.showLoading(title: S.current.webDavPulling); try { - List files = + List? files = await _oneDriveCloudService!.listBackups(); + if (files == null) { + CustomLoadingDialog.dismissLoading(); + IToast.show(S.current.webDavPullFailed); + return; + } CloudServiceConfigDao.updateLastPullTime( _oneDriveCloudServiceConfig!); CustomLoadingDialog.dismissLoading(); @@ -283,71 +289,14 @@ class _OneDriveServiceScreenState extends State msg: S.current.webDavPulling, showProgress: true, ); - Uint8List res = + Uint8List? res = await _oneDriveCloudService!.downloadFile( selectedFile.id, onProgress: (c, t) { dialog.updateProgress(progress: c / t); }, ); - - dialog.updateMessage( - msg: S.current.importing, - showProgress: false, - ); - - bool success = await ImportTokenUtil.importBackupFile( - res, - showLoading: false, - ); - dialog.dismiss(); - if (!success) { - BottomSheetBuilder.showBottomSheet( - context, - responsive: true, - (context) => InputBottomSheet( - validator: (value) { - if (value.isEmpty) { - return S - .current.autoBackupPasswordCannotBeEmpty; - } - return null; - }, - validateAsyncController: - InputValidateAsyncController( - listen: false, - validator: (text) async { - dialog.show( - msg: S.current.importing, - showProgress: false, - ); - bool success = - await ImportTokenUtil.importBackupFile( - password: text, - res, - showLoading: false, - ); - dialog.dismiss(); - if (success) { - return null; - } else { - return S - .current.invalidPasswordOrDataCorrupted; - } - }, - controller: TextEditingController(), - ), - title: S.current.inputImportPasswordTitle, - message: S.current.inputImportPasswordTip, - hint: S.current.inputImportPasswordHint, - inputFormatters: [ - RegexInputFormatter.onlyNumberAndLetter, - ], - tailingType: InputItemTailingType.password, - onValidConfirm: (password) async {}, - ), - ); - } + ImportTokenUtil.importFromCloud(context, res, dialog); }, ), ); diff --git a/lib/Screens/Backup/s3_service_screen.dart b/lib/Screens/Backup/s3_service_screen.dart index d6c974cb..186c5ddd 100644 --- a/lib/Screens/Backup/s3_service_screen.dart +++ b/lib/Screens/Backup/s3_service_screen.dart @@ -13,7 +13,6 @@ import 'package:flutter/material.dart'; import '../../Database/cloud_service_config_dao.dart'; import '../../Models/s3_cloud_file_info.dart'; import '../../TokenUtils/Cloud/s3_cloud_service.dart'; -import '../../Widgets/BottomSheet/input_bottom_sheet.dart'; import '../../Widgets/BottomSheet/s3_backups_bottom_sheet.dart'; import '../../Widgets/Dialog/custom_dialog.dart'; import '../../Widgets/Item/input_item.dart'; @@ -122,6 +121,8 @@ class _S3CloudServiceScreenState extends State context, background: Colors.transparent, text: S.current.cloudConnecting, + mainAxisAlignment: MainAxisAlignment.start, + topPadding: 100, ); } @@ -193,7 +194,6 @@ class _S3CloudServiceScreenState extends State padding: const EdgeInsets.only(top: 15, bottom: 5, right: 10), child: Form( key: formKey, - autovalidateMode: AutovalidateMode.always, child: Column( children: [ InputItem( @@ -322,13 +322,15 @@ class _S3CloudServiceScreenState extends State color: Theme.of(context).primaryColor, fontSizeDelta: 2, onTap: () async { - CustomLoadingDialog.showLoading( - title: S.current.webDavPulling, - dismissible: true, - ); + CustomLoadingDialog.showLoading(title: S.current.webDavPulling); try { - List files = + List? files = await _s3CloudService!.listBackups(); + if (files == null) { + CustomLoadingDialog.dismissLoading(); + IToast.show(S.current.webDavPullFailed); + return; + } CloudServiceConfigDao.updateLastPullTime( _s3CloudServiceConfig!); CustomLoadingDialog.dismissLoading(); @@ -346,69 +348,13 @@ class _S3CloudServiceScreenState extends State msg: S.current.webDavPulling, showProgress: true, ); - Uint8List res = await _s3CloudService!.downloadFile( + Uint8List? res = await _s3CloudService!.downloadFile( selectedFile.path, onProgress: (c, t) { dialog.updateProgress(progress: c / t); }, ); - dialog.updateMessage( - msg: S.current.importing, - showProgress: false, - ); - - bool success = await ImportTokenUtil.importBackupFile( - res, - showLoading: false, - ); - dialog.dismiss(); - if (!success) { - BottomSheetBuilder.showBottomSheet( - context, - responsive: true, - (context) => InputBottomSheet( - validator: (value) { - if (value.isEmpty) { - return S - .current.autoBackupPasswordCannotBeEmpty; - } - return null; - }, - validateAsyncController: - InputValidateAsyncController( - listen: false, - validator: (text) async { - dialog.show( - msg: S.current.importing, - showProgress: false, - ); - bool success = - await ImportTokenUtil.importBackupFile( - password: text, - res, - showLoading: false, - ); - dialog.dismiss(); - if (success) { - return null; - } else { - return S - .current.invalidPasswordOrDataCorrupted; - } - }, - controller: TextEditingController(), - ), - title: S.current.inputImportPasswordTitle, - message: S.current.inputImportPasswordTip, - hint: S.current.inputImportPasswordHint, - inputFormatters: [ - RegexInputFormatter.onlyNumberAndLetter, - ], - tailingType: InputItemTailingType.password, - onValidConfirm: (password) async {}, - ), - ); - } + ImportTokenUtil.importFromCloud(context, res, dialog); }, ), ); diff --git a/lib/Screens/Backup/webdav_service_screen.dart b/lib/Screens/Backup/webdav_service_screen.dart index 859cbda9..ab25ef11 100644 --- a/lib/Screens/Backup/webdav_service_screen.dart +++ b/lib/Screens/Backup/webdav_service_screen.dart @@ -14,7 +14,6 @@ import 'package:webdav_client/webdav_client.dart'; import '../../Database/cloud_service_config_dao.dart'; import '../../TokenUtils/Cloud/webdav_cloud_service.dart'; -import '../../Widgets/BottomSheet/input_bottom_sheet.dart'; import '../../Widgets/Dialog/custom_dialog.dart'; import '../../Widgets/Item/input_item.dart'; import '../../generated/l10n.dart'; @@ -75,9 +74,12 @@ class _WebDavServiceScreenState extends State if (_webDavCloudService != null) { _webDavCloudServiceConfig!.connected = await _webDavCloudService!.isConnected(); + if (!_webDavCloudServiceConfig!.connected) { + IToast.showTop(S.current.cloudConnectionError); + } } inited = true; - setState(() {}); + if (mounted) setState(() {}); } RegExp urlRegex = RegExp( @@ -108,6 +110,8 @@ class _WebDavServiceScreenState extends State context, background: Colors.transparent, text: S.current.cloudConnecting, + mainAxisAlignment: MainAxisAlignment.start, + topPadding: 100, ); } @@ -180,7 +184,6 @@ class _WebDavServiceScreenState extends State padding: const EdgeInsets.only(top: 15, bottom: 5, right: 10), child: Form( key: formKey, - autovalidateMode: AutovalidateMode.always, child: Column( children: [ InputItem( @@ -282,13 +285,15 @@ class _WebDavServiceScreenState extends State color: Theme.of(context).primaryColor, fontSizeDelta: 2, onTap: () async { - CustomLoadingDialog.showLoading( - title: S.current.webDavPulling, - dismissible: true, - ); + CustomLoadingDialog.showLoading(title: S.current.webDavPulling); try { - List files = + List? files = await _webDavCloudService!.listBackups(); + if (files == null) { + CustomLoadingDialog.dismissLoading(); + IToast.show(S.current.webDavPullFailed); + return; + } CloudServiceConfigDao.updateLastPullTime( _webDavCloudServiceConfig!); CustomLoadingDialog.dismissLoading(); @@ -305,70 +310,14 @@ class _WebDavServiceScreenState extends State msg: S.current.webDavPulling, showProgress: true, ); - Uint8List res = await _webDavCloudService!.downloadFile( + Uint8List? res = + await _webDavCloudService!.downloadFile( selectedFile.name!, onProgress: (c, t) { dialog.updateProgress(progress: c / t); }, ); - - dialog.updateMessage( - msg: S.current.importing, - showProgress: false, - ); - - bool success = await ImportTokenUtil.importBackupFile( - res, - showLoading: false, - ); - dialog.dismiss(); - if (!success) { - BottomSheetBuilder.showBottomSheet( - context, - responsive: true, - (context) => InputBottomSheet( - validator: (value) { - if (value.isEmpty) { - return S - .current.autoBackupPasswordCannotBeEmpty; - } - return null; - }, - validateAsyncController: - InputValidateAsyncController( - listen: false, - validator: (text) async { - dialog.show( - msg: S.current.importing, - showProgress: false, - ); - bool success = - await ImportTokenUtil.importBackupFile( - password: text, - res, - showLoading: false, - ); - dialog.dismiss(); - if (success) { - return null; - } else { - return S - .current.invalidPasswordOrDataCorrupted; - } - }, - controller: TextEditingController(), - ), - title: S.current.inputImportPasswordTitle, - message: S.current.inputImportPasswordTip, - hint: S.current.inputImportPasswordHint, - inputFormatters: [ - RegexInputFormatter.onlyNumberAndLetter, - ], - tailingType: InputItemTailingType.password, - onValidConfirm: (password) async {}, - ), - ); - } + ImportTokenUtil.importFromCloud(context, res, dialog); }, ), ); diff --git a/lib/Screens/Lock/database_decrypt_screen.dart b/lib/Screens/Lock/database_decrypt_screen.dart index e30eae32..f0ea6b10 100644 --- a/lib/Screens/Lock/database_decrypt_screen.dart +++ b/lib/Screens/Lock/database_decrypt_screen.dart @@ -120,7 +120,6 @@ class DatabaseDecryptScreenState extends State ), child: Form( key: formKey, - autovalidateMode: AutovalidateMode.always, child: InputItem( validator: (value) { if (value.isEmpty) { diff --git a/lib/Screens/Setting/setting_backup_screen.dart b/lib/Screens/Setting/setting_backup_screen.dart index 3f1cf9ba..a8888fae 100644 --- a/lib/Screens/Setting/setting_backup_screen.dart +++ b/lib/Screens/Setting/setting_backup_screen.dart @@ -282,11 +282,12 @@ class _BackupSettingScreenState extends State CustomLoadingDialog.dismissLoading(); InputValidateAsyncController validateAsyncController = InputValidateAsyncController( - controller: TextEditingController( - text: _maxBackupsCount.toString()), - validator: (text) async { - return null; - }); + controller: + TextEditingController(text: _maxBackupsCount.toString()), + validator: (text) async { + return null; + }, + ); BottomSheetBuilder.showBottomSheet( context, responsive: true, diff --git a/lib/Screens/Setting/setting_general_screen.dart b/lib/Screens/Setting/setting_general_screen.dart index dda3313d..2a5cdd6d 100644 --- a/lib/Screens/Setting/setting_general_screen.dart +++ b/lib/Screens/Setting/setting_general_screen.dart @@ -36,7 +36,7 @@ class GeneralSettingScreen extends StatefulWidget { class _GeneralSettingScreenState extends State with TickerProviderStateMixin { bool _enableLandscapeInTablet = - HiveUtil.getBool(HiveUtil.enableLandscapeInTabletKey, defaultValue: true); + HiveUtil.getBool(HiveUtil.enableLandscapeInTabletKey, defaultValue: true); FontEnum _currentFont = FontEnum.getCurrentFont(); bool enableMinimizeToTray = HiveUtil.getBool(HiveUtil.enableCloseToTrayKey); bool recordWindowState = HiveUtil.getBool(HiveUtil.recordWindowStateKey); @@ -50,9 +50,9 @@ class _GeneralSettingScreenState extends State bool inAppBrowser = HiveUtil.getBool(HiveUtil.inappWebviewKey); bool hideAppbarWhenScrolling = - HiveUtil.getBool(HiveUtil.hideAppbarWhenScrollingKey); + HiveUtil.getBool(HiveUtil.hideAppbarWhenScrollingKey); bool hideBottombarWhenScrolling = - HiveUtil.getBool(HiveUtil.hideBottombarWhenScrollingKey); + HiveUtil.getBool(HiveUtil.hideBottombarWhenScrollingKey); final GlobalKey _setAutoBackupPasswordKey = GlobalKey(); @override @@ -79,30 +79,30 @@ class _GeneralSettingScreenState extends State child: Scaffold( appBar: ResponsiveUtil.isLandscape() ? ItemBuilder.buildSimpleAppBar( - title: S.current.generalSetting, - context: context, - transparent: true, - ) + title: S.current.generalSetting, + context: context, + transparent: true, + ) : ItemBuilder.buildAppBar( - context: context, - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - leading: Icons.arrow_back_rounded, - onLeadingTap: () { - Navigator.pop(context); - }, - title: Text( - S.current.generalSetting, - style: Theme.of(context) - .textTheme - .titleMedium - ?.apply(fontWeightDelta: 2), - ), - center: true, - actions: [ - ItemBuilder.buildBlankIconButton(context), - const SizedBox(width: 5), - ], - ), + context: context, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + leading: Icons.arrow_back_rounded, + onLeadingTap: () { + Navigator.pop(context); + }, + title: Text( + S.current.generalSetting, + style: Theme.of(context) + .textTheme + .titleMedium + ?.apply(fontWeightDelta: 2), + ), + center: true, + actions: [ + ItemBuilder.buildBlankIconButton(context), + const SizedBox(width: 5), + ], + ), body: EasyRefresh( child: ListView( physics: const BouncingScrollPhysics(), @@ -134,9 +134,9 @@ class _GeneralSettingScreenState extends State onTap: () { BottomSheetBuilder.showListBottomSheet( context, - (context) => TileList.fromOptions( + (context) => TileList.fromOptions( AppProvider.getSupportedThemeMode(), - (item2) { + (item2) { appProvider.themeMode = item2; Navigator.pop(context); }, @@ -153,16 +153,16 @@ class _GeneralSettingScreenState extends State selector: (context, appProvider) => appProvider.lightTheme, builder: (context, lightTheme, child) => Selector( - selector: (context, appProvider) => appProvider.darkTheme, - builder: (context, darkTheme, child) => ItemBuilder.buildEntryItem( - context: context, - title: S.current.selectTheme, - tip: "${lightTheme.intlName}/${darkTheme.intlName}", - onTap: () { - RouteUtil.pushCupertinoRoute(context, const SelectThemeScreen()); - }, - ), - ), + selector: (context, appProvider) => appProvider.darkTheme, + builder: (context, darkTheme, child) => ItemBuilder.buildEntryItem( + context: context, + title: S.current.selectTheme, + tip: "${lightTheme.intlName}/${darkTheme.intlName}", + onTap: () { + RouteUtil.pushCupertinoRoute(context, const SelectThemeScreen()); + }, + ), + ), ), ItemBuilder.buildEntryItem( context: context, @@ -172,9 +172,9 @@ class _GeneralSettingScreenState extends State onTap: () { BottomSheetBuilder.showListBottomSheet( context, - (sheetContext) => TileList.fromOptions( + (sheetContext) => TileList.fromOptions( FontEnum.getFontList(), - (item2) async { + (item2) async { FontEnum t = item2 as FontEnum; _currentFont = t; Navigator.pop(sheetContext); @@ -206,9 +206,9 @@ class _GeneralSettingScreenState extends State filterLocale(); BottomSheetBuilder.showListBottomSheet( context, - (context) => TileList.fromOptions( + (context) => TileList.fromOptions( _supportedLocaleTuples, - (item2) { + (item2) { appProvider.locale = item2; Navigator.pop(context); }, @@ -240,9 +240,9 @@ class _GeneralSettingScreenState extends State ? S.current.newVersion(latestVersion) : S.current.alreadyLatestVersion, descriptionColor: - Utils.compareVersion(latestVersion, currentVersion) > 0 - ? Colors.redAccent - : null, + Utils.compareVersion(latestVersion, currentVersion) > 0 + ? Colors.redAccent + : null, tip: currentVersion, onTap: () { fetchReleases(true); @@ -260,7 +260,7 @@ class _GeneralSettingScreenState extends State context: context, title: S.current.closeWindowOption, tip: - enableMinimizeToTray ? S.current.minimizeToTray : S.current.exitApp, + enableMinimizeToTray ? S.current.minimizeToTray : S.current.exitApp, onTap: () { List> options = [ Tuple2(S.current.minimizeToTray, 0), @@ -268,9 +268,9 @@ class _GeneralSettingScreenState extends State ]; BottomSheetBuilder.showListBottomSheet( context, - (sheetContext) => TileList.fromOptions( + (sheetContext) => TileList.fromOptions( options, - (idx) { + (idx) { Navigator.pop(sheetContext); if (idx == 0) { setState(() { @@ -415,15 +415,19 @@ class _GeneralSettingScreenState extends State showUpdateDialog: showTip, showNoUpdateToast: showTip, onGetCurrentVersion: (currentVersion) { - setState(() { - this.currentVersion = currentVersion; - }); + if (mounted) { + setState(() { + this.currentVersion = currentVersion; + }); + } }, onGetLatestRelease: (latestVersion, latestReleaseItem) { - setState(() { - this.latestVersion = latestVersion; - this.latestReleaseItem = latestReleaseItem; - }); + if (mounted) { + setState(() { + this.latestVersion = latestVersion; + this.latestReleaseItem = latestReleaseItem; + }); + } }, ); } diff --git a/lib/Screens/Setting/setting_screen.dart b/lib/Screens/Setting/setting_screen.dart index 5266a82a..d8c8e3cf 100644 --- a/lib/Screens/Setting/setting_screen.dart +++ b/lib/Screens/Setting/setting_screen.dart @@ -533,7 +533,6 @@ class _SettingScreenState extends State deleteOldBackups(count); return true; } - print("ddddddd"); if (count > 0 && (counts[0] > count || counts[1] > count)) { DialogBuilder.showConfirmDialog( context, diff --git a/lib/Screens/Token/add_token_screen.dart b/lib/Screens/Token/add_token_screen.dart index b9c2e5f7..ac518c7b 100644 --- a/lib/Screens/Token/add_token_screen.dart +++ b/lib/Screens/Token/add_token_screen.dart @@ -250,7 +250,6 @@ class _AddTokenScreenState extends State children: [ Form( key: formKey, - autovalidateMode: AutovalidateMode.always, child: Column( children: [ _iconInfo(), diff --git a/lib/Screens/Token/category_screen.dart b/lib/Screens/Token/category_screen.dart index cbe76bf7..47abea7a 100644 --- a/lib/Screens/Token/category_screen.dart +++ b/lib/Screens/Token/category_screen.dart @@ -75,36 +75,39 @@ class _CategoryScreenState extends State icon: Icon(Icons.add_rounded, color: Theme.of(context).iconTheme.color), onTap: () { + InputValidateAsyncController validateAsyncController = + InputValidateAsyncController( + validator: (text) async { + if (text.isEmpty) { + return S.current.categoryNameCannotBeEmpty; + } + if (await CategoryDao.isCategoryExist(text)) { + return S.current.categoryNameDuplicate; + } + return null; + }, + controller: TextEditingController(), + ); + GlobalKey key = GlobalKey(); BottomSheetBuilder.showBottomSheet( context, responsive: true, - (context) => InputBottomSheet( - title: S.current.addCategory, - hint: S.current.inputCategory, - validateAsyncController: InputValidateAsyncController( - validator: (text) async { - if (await CategoryDao.isCategoryExist(text)) { - return S.current.categoryNameDuplicate; - } - return null; + (context) { + return InputBottomSheet( + key: key, + title: S.current.addCategory, + hint: S.current.inputCategory, + validateAsyncController: validateAsyncController, + maxLength: 32, + onValidConfirm: (text) async { + TokenCategory category = TokenCategory.title(title: text); + await CategoryDao.insertCategory(category); + categories.add(category); + setState(() {}); + homeScreenState?.refreshCategories(); }, - controller: TextEditingController(), - ), - validator: (text) { - if (text.isEmpty) { - return S.current.categoryNameCannotBeEmpty; - } - return null; - }, - maxLength: 32, - onValidConfirm: (text) async { - TokenCategory category = TokenCategory.title(title: text); - await CategoryDao.insertCategory(category); - categories.add(category); - setState(() {}); - homeScreenState?.refreshCategories(); - }, - ), + ); + } ); }, ), @@ -198,6 +201,20 @@ class _CategoryScreenState extends State context: context, icon: const Icon(Icons.edit_rounded, size: 20), onTap: () { + InputValidateAsyncController validateAsyncController = + InputValidateAsyncController( + validator: (text) async { + if (text.isEmpty) { + return S.current.categoryNameCannotBeEmpty; + } + if (text != category.title && + await CategoryDao.isCategoryExist(text)) { + return S.current.categoryNameDuplicate; + } + return null; + }, + controller: TextEditingController(), + ); BottomSheetBuilder.showBottomSheet( context, responsive: true, @@ -206,22 +223,7 @@ class _CategoryScreenState extends State hint: S.current.inputCategory, maxLength: 32, text: category.title, - validateAsyncController: InputValidateAsyncController( - validator: (text) async { - if (text != category.title && - await CategoryDao.isCategoryExist(text)) { - return S.current.categoryNameDuplicate; - } - return null; - }, - controller: TextEditingController(), - ), - validator: (text) { - if (text.isEmpty) { - return S.current.categoryNameCannotBeEmpty; - } - return null; - }, + validateAsyncController: validateAsyncController, onValidConfirm: (text) async { category.title = text; await CategoryDao.updateCategory(category); diff --git a/lib/Screens/Token/import_export_token_screen.dart b/lib/Screens/Token/import_export_token_screen.dart index da204a37..11bb5c76 100644 --- a/lib/Screens/Token/import_export_token_screen.dart +++ b/lib/Screens/Token/import_export_token_screen.dart @@ -82,6 +82,22 @@ class _ImportExportTokenScreenState extends State } _showImportPasswordDialog(String path) { + InputValidateAsyncController validateAsyncController = + InputValidateAsyncController( + controller: TextEditingController(), + listen: false, + validator: (text) async { + if (text.isEmpty) { + return S.current.autoBackupPasswordCannotBeEmpty; + } + bool success = await ImportTokenUtil.importEncryptFile(path, text); + if (success) { + return null; + } else { + return S.current.invalidPasswordOrDataCorrupted; + } + }, + ); BottomSheetBuilder.showBottomSheet( context, responsive: true, @@ -92,18 +108,8 @@ class _ImportExportTokenScreenState extends State } return null; }, - validateAsyncController: InputValidateAsyncController( - controller: TextEditingController(), - listen: false, - validator: (text) async { - bool success = await ImportTokenUtil.importEncryptFile(path, text); - if (success) { - return null; - } else { - return S.current.encryptDatabasePasswordWrong; - } - }, - ), + checkSyncValidator: false, + validateAsyncController: validateAsyncController, title: S.current.inputImportPasswordTitle, message: S.current.inputImportPasswordTip, hint: S.current.inputImportPasswordHint, @@ -111,9 +117,7 @@ class _ImportExportTokenScreenState extends State RegexInputFormatter.onlyNumberAndLetter, ], tailingType: InputItemTailingType.password, - onValidConfirm: (password) async { - return null; - }, + onValidConfirm: (password) async {}, ), ); } diff --git a/lib/Screens/home_screen.dart b/lib/Screens/home_screen.dart index 7706d285..aecd935f 100644 --- a/lib/Screens/home_screen.dart +++ b/lib/Screens/home_screen.dart @@ -196,7 +196,7 @@ class HomeScreenState extends State with TickerProviderStateMixin { int updateIndex = tokens.indexWhere((element) => element.id == token.id); tokens[updateIndex] = token; tokenKeyMap - .putIfAbsent(updateIndex, () => GlobalKey()) + .putIfAbsent(token.id, () => GlobalKey()) .currentState ?.updateInfo(); if (pinnedStateChanged) performSort(); @@ -796,6 +796,19 @@ class HomeScreenState extends State with TickerProviderStateMixin { } processEditCategory(TokenCategory category) { + InputValidateAsyncController validateAsyncController = + InputValidateAsyncController( + validator: (text) async { + if (text.isEmpty) { + return S.current.categoryNameCannotBeEmpty; + } + if (text != category.title && await CategoryDao.isCategoryExist(text)) { + return S.current.categoryNameDuplicate; + } + return null; + }, + controller: TextEditingController(), + ); BottomSheetBuilder.showBottomSheet( context, responsive: true, @@ -804,22 +817,13 @@ class HomeScreenState extends State with TickerProviderStateMixin { hint: S.current.inputCategory, maxLength: 32, text: category.title, - validateAsyncController: InputValidateAsyncController( - validator: (text) async { - if (text != category.title && - await CategoryDao.isCategoryExist(text)) { - return S.current.categoryNameDuplicate; - } - return null; - }, - controller: TextEditingController(), - ), validator: (text) { if (text.isEmpty) { return S.current.categoryNameCannotBeEmpty; } return null; }, + validateAsyncController: validateAsyncController, onValidConfirm: (text) async { category.title = text; await CategoryDao.updateCategory(category); @@ -831,6 +835,19 @@ class HomeScreenState extends State with TickerProviderStateMixin { _buildTabContextMenuButtons(TokenCategory? category) { addCategory() async { + InputValidateAsyncController validateAsyncController = + InputValidateAsyncController( + validator: (text) async { + if (text.isEmpty) { + return S.current.categoryNameCannotBeEmpty; + } + if (await CategoryDao.isCategoryExist(text)) { + return S.current.categoryNameDuplicate; + } + return null; + }, + controller: TextEditingController(), + ); BottomSheetBuilder.showBottomSheet( context, responsive: true, @@ -843,15 +860,7 @@ class HomeScreenState extends State with TickerProviderStateMixin { } return null; }, - validateAsyncController: InputValidateAsyncController( - validator: (text) async { - if (await CategoryDao.isCategoryExist(text)) { - return S.current.categoryNameDuplicate; - } - return null; - }, - controller: TextEditingController(), - ), + validateAsyncController: validateAsyncController, maxLength: 32, onValidConfirm: (text) async { await CategoryDao.insertCategory(TokenCategory.title(title: text)); @@ -889,6 +898,7 @@ class HomeScreenState extends State with TickerProviderStateMixin { }), ContextMenuButtonConfig.warning( S.current.deleteCategory, + textColor: Colors.red, onPressed: () { DialogBuilder.showConfirmDialog( context, diff --git a/lib/TokenUtils/Cloud/cloud_service.dart b/lib/TokenUtils/Cloud/cloud_service.dart index aae09704..bd994051 100644 --- a/lib/TokenUtils/Cloud/cloud_service.dart +++ b/lib/TokenUtils/Cloud/cloud_service.dart @@ -29,7 +29,7 @@ abstract class CloudService { Function(int, int)? onProgress, }); - Future downloadFile( + Future downloadFile( String path, { Function(int, int)? onProgress, }); diff --git a/lib/TokenUtils/Cloud/dropbox_cloud_service.dart b/lib/TokenUtils/Cloud/dropbox_cloud_service.dart index d7a3d2ea..66e0059d 100644 --- a/lib/TokenUtils/Cloud/dropbox_cloud_service.dart +++ b/lib/TokenUtils/Cloud/dropbox_cloud_service.dart @@ -44,14 +44,14 @@ class DropboxCloudService extends CloudService { context, windowName: S.current.cloudTypeDropboxAuthenticateWindowName, ); - if (isAuthorized) { - await fetchInfo(); - return CloudServiceStatus.success; - } else { - return CloudServiceStatus.unauthorized; - } + } + if (isAuthorized) { + DropboxUserInfo? info = await fetchInfo(); + return info != null + ? CloudServiceStatus.success + : CloudServiceStatus.connectionError; } else { - return CloudServiceStatus.success; + return CloudServiceStatus.unauthorized; } } @@ -71,22 +71,23 @@ class DropboxCloudService extends CloudService { Future isConnected() async { bool connected = await dropbox.isConnected(); if (connected) { - await fetchInfo(); + DropboxUserInfo? info = await fetchInfo(); + return info != null; } return connected; } @override Future deleteFile(String path) async { - DropboxResponse response = - await dropbox.delete(join(_dropboxPath, path)); + DropboxResponse response = await dropbox.delete(join(_dropboxPath, path)); return response.isSuccess; } @override Future deleteOldBackup([int? maxCount]) async { maxCount ??= HiveUtil.getMaxBackupsCount(); - List list = await listBackups(); + List? list = await listBackups(); + if (list == null) return false; list.sort((a, b) { return a.lastModifiedDateTime.compareTo(b.lastModifiedDateTime); }); @@ -101,22 +102,23 @@ class DropboxCloudService extends CloudService { } @override - Future downloadFile( + Future downloadFile( String path, { Function(int p1, int p2)? onProgress, }) async { DropboxResponse response = await dropbox.pull(path); - return response.bodyBytes ?? Uint8List(0); + return response.isSuccess ? response.bodyBytes ?? Uint8List(0) : null; } @override Future getBackupsCount() async { - return (await listBackups()).length; + return (await listBackups())?.length ?? 0; } @override - Future> listBackups() async { + Future?> listBackups() async { var list = await listFiles(); + if (list == null) return null; list = list .where((element) => ExportTokenUtil.isBackup(element.name)) .toList(); @@ -124,8 +126,10 @@ class DropboxCloudService extends CloudService { } @override - Future> listFiles() async { - List files = (await dropbox.list(_dropboxEmptyPath)).files; + Future?> listFiles() async { + DropboxResponse response = await dropbox.list(_dropboxEmptyPath); + if (!response.isSuccess) return null; + List files = response.files; return files; } @@ -147,5 +151,4 @@ class DropboxCloudService extends CloudService { deleteOldBackup(); return response.isSuccess; } - } diff --git a/lib/TokenUtils/Cloud/googledrive_cloud_service.dart b/lib/TokenUtils/Cloud/googledrive_cloud_service.dart index 6985bee9..ed7292eb 100644 --- a/lib/TokenUtils/Cloud/googledrive_cloud_service.dart +++ b/lib/TokenUtils/Cloud/googledrive_cloud_service.dart @@ -45,14 +45,12 @@ class GoogleDriveCloudService extends CloudService { context, windowName: S.current.cloudTypeGoogleDriveAuthenticateWindowName, ); - if (isAuthorized) { - await fetchInfo(); - return CloudServiceStatus.success; - } else { - return CloudServiceStatus.unauthorized; - } - } else { + } + if (isAuthorized) { + await fetchInfo(); return CloudServiceStatus.success; + } else { + return CloudServiceStatus.unauthorized; } } @@ -72,7 +70,8 @@ class GoogleDriveCloudService extends CloudService { Future isConnected() async { bool connected = await googledrive.isConnected(); if (connected) { - await fetchInfo(); + GoogleDriveUserInfo? info = await fetchInfo(); + return info != null; } return connected; } @@ -86,7 +85,8 @@ class GoogleDriveCloudService extends CloudService { @override Future deleteOldBackup([int? maxCount]) async { maxCount ??= HiveUtil.getMaxBackupsCount(); - List list = await listBackups(); + List? list = await listBackups(); + if (list == null) return false; list.sort((a, b) { return a.lastModifiedDateTime.compareTo(b.lastModifiedDateTime); }); @@ -98,22 +98,23 @@ class GoogleDriveCloudService extends CloudService { } @override - Future downloadFile( + Future downloadFile( String path, { Function(int p1, int p2)? onProgress, }) async { GoogleDriveResponse response = await googledrive.pullById(path); - return response.bodyBytes ?? Uint8List(0); + return response.isSuccess ? response.bodyBytes ?? Uint8List(0) : null; } @override Future getBackupsCount() async { - return (await listBackups()).length; + return (await listBackups())?.length ?? 0; } @override - Future> listBackups() async { + Future?> listBackups() async { var list = await listFiles(); + if (list == null) return null; list = list .where((element) => ExportTokenUtil.isBackup(element.name)) .toList(); @@ -121,9 +122,10 @@ class GoogleDriveCloudService extends CloudService { } @override - Future> listFiles() async { - List files = - (await googledrive.list(_googledrivePath)).files; + Future?> listFiles() async { + GoogleDriveResponse response = await googledrive.list(_googledrivePath); + if (!response.isSuccess) return null; + List files = response.files; return files; } @@ -146,5 +148,4 @@ class GoogleDriveCloudService extends CloudService { deleteOldBackup(); return response.isSuccess; } - } diff --git a/lib/TokenUtils/Cloud/onedrive_cloud_service.dart b/lib/TokenUtils/Cloud/onedrive_cloud_service.dart index 2fd2db49..cc8e928b 100644 --- a/lib/TokenUtils/Cloud/onedrive_cloud_service.dart +++ b/lib/TokenUtils/Cloud/onedrive_cloud_service.dart @@ -43,14 +43,12 @@ class OneDriveCloudService extends CloudService { context, windowName: S.current.cloudTypeOneDriveAuthenticateWindowName, ); - if (isAuthorized) { - await fetchInfo(); - return CloudServiceStatus.success; - } else { - return CloudServiceStatus.unauthorized; - } - } else { + } + if (isAuthorized) { + await fetchInfo(); return CloudServiceStatus.success; + } else { + return CloudServiceStatus.unauthorized; } } @@ -71,7 +69,8 @@ class OneDriveCloudService extends CloudService { Future isConnected() async { bool connected = await onedrive.isConnected(); if (connected) { - await fetchInfo(); + OneDriveUserInfo? info = await fetchInfo(); + return info != null; } return connected; } @@ -85,7 +84,8 @@ class OneDriveCloudService extends CloudService { @override Future deleteOldBackup([int? maxCount]) async { maxCount ??= HiveUtil.getMaxBackupsCount(); - List list = await listBackups(); + List? list = await listBackups(); + if (list == null) return false; list.sort((a, b) { return a.lastModifiedDateTime.compareTo(b.lastModifiedDateTime); }); @@ -97,22 +97,23 @@ class OneDriveCloudService extends CloudService { } @override - Future downloadFile( + Future downloadFile( String path, { Function(int p1, int p2)? onProgress, }) async { OneDriveResponse response = await onedrive.pullById(path); - return response.bodyBytes ?? Uint8List(0); + return response.isSuccess ? response.bodyBytes ?? Uint8List(0) : null; } @override Future getBackupsCount() async { - return (await listBackups()).length; + return (await listBackups())?.length ?? 0; } @override - Future> listBackups() async { + Future?> listBackups() async { var list = await listFiles(); + if (list == null) return null; list = list .where((element) => ExportTokenUtil.isBackup(element.name)) .toList(); @@ -120,8 +121,10 @@ class OneDriveCloudService extends CloudService { } @override - Future> listFiles() async { - List files = (await onedrive.list(_onedrivePath)).files; + Future?> listFiles() async { + OneDriveResponse response = await onedrive.list(_onedrivePath); + if (!response.isSuccess) return null; + List files = response.files; return files; } diff --git a/lib/TokenUtils/Cloud/s3_cloud_service.dart b/lib/TokenUtils/Cloud/s3_cloud_service.dart index 4bff0a73..409e25da 100644 --- a/lib/TokenUtils/Cloud/s3_cloud_service.dart +++ b/lib/TokenUtils/Cloud/s3_cloud_service.dart @@ -53,12 +53,11 @@ class S3CloudService extends CloudService { try { bool isAuthorized = await s3Storage.bucketExists(bucket); if (!isAuthorized) { - return CloudServiceStatus.unauthorized; + return CloudServiceStatus.connectionError; } else { return CloudServiceStatus.success; } - } catch (e, t) { - print("$e $t"); + } catch (e) { return CloudServiceStatus.unknownError; } } @@ -92,7 +91,8 @@ class S3CloudService extends CloudService { Future deleteOldBackup([int? maxCount]) async { try { maxCount ??= HiveUtil.getMaxBackupsCount(); - List list = await listBackups(); + List? list = await listBackups(); + if (list == null) return false; list.sort((a, b) { return a.modifyTimestamp.compareTo(b.modifyTimestamp); }); @@ -108,7 +108,7 @@ class S3CloudService extends CloudService { } @override - Future downloadFile( + Future downloadFile( String path, { Function(int p1, int p2)? onProgress, }) async { @@ -118,20 +118,20 @@ class S3CloudService extends CloudService { path, ); return await response.toBytes(); - } catch (e, t) { - print("$e\n$t"); - return Uint8List(0); + } catch (e) { + return null; } } @override Future getBackupsCount() async { - return (await listBackups()).length; + return (await listBackups())?.length ?? 0; } @override - Future> listBackups() async { + Future?> listBackups() async { var list = await listFiles(); + if (list == null) return null; list = list .where((element) => ExportTokenUtil.isBackup(element.name)) .toList(); @@ -139,7 +139,7 @@ class S3CloudService extends CloudService { } @override - Future> listFiles() async { + Future?> listFiles() async { try { ListObjectsResult res = await s3Storage.listAllObjects( bucket, @@ -159,7 +159,7 @@ class S3CloudService extends CloudService { return files; } catch (e, t) { print("$e\n$t"); - return []; + return null; } } @@ -172,12 +172,16 @@ class S3CloudService extends CloudService { Uint8List fileData, { Function(int p1, int p2)? onProgress, }) async { - String response = await s3Storage - .putObject(bucket, join(_s3CloudPath, fileName), Stream.value(fileData), - onProgress: (bytes) { - onProgress?.call(bytes, fileData.length); - }); - deleteOldBackup(); - return response.isNotEmpty; + try { + String response = await s3Storage.putObject( + bucket, join(_s3CloudPath, fileName), Stream.value(fileData), + onProgress: (bytes) { + onProgress?.call(bytes, fileData.length); + }); + deleteOldBackup(); + return response.isNotEmpty; + } catch (e) { + return false; + } } } diff --git a/lib/TokenUtils/Cloud/webdav_cloud_service.dart b/lib/TokenUtils/Cloud/webdav_cloud_service.dart index 45e4390e..b194a1e6 100644 --- a/lib/TokenUtils/Cloud/webdav_cloud_service.dart +++ b/lib/TokenUtils/Cloud/webdav_cloud_service.dart @@ -81,14 +81,19 @@ class WebDavCloudService extends CloudService { } @override - Future listFiles() async { - var list = await client.readDir(_webdavPath); - return list; + Future?> listFiles() async { + try { + var list = await client.readDir(_webdavPath); + return list; + } catch (e) { + return null; + } } @override - Future listBackups() async { + Future?> listBackups() async { var list = await listFiles(); + if (list == null) return null; list = list .where((element) => ExportTokenUtil.isBackup(element.path ?? "")) .toList(); @@ -97,7 +102,7 @@ class WebDavCloudService extends CloudService { @override Future getBackupsCount() async { - return (await listBackups()).length; + return (await listBackups())?.length ?? 0; } @override @@ -106,41 +111,49 @@ class WebDavCloudService extends CloudService { Uint8List fileData, { Function(int, int)? onProgress, }) async { - CancelToken c = CancelToken(); - double progress = 0; - await client.write( - join(_webdavPath, fileName), - fileData, - onProgress: (c, t) { - onProgress?.call(c, t); - progress = c / t; - }, - cancelToken: c, - ); - deleteOldBackup(); - if (progress >= 1) { - return true; - } else { + try { + CancelToken c = CancelToken(); + double progress = 0; + await client.write( + join(_webdavPath, fileName), + fileData, + onProgress: (c, t) { + onProgress?.call(c, t); + progress = c / t; + }, + cancelToken: c, + ); + deleteOldBackup(); + if (progress >= 1) { + return true; + } else { + return false; + } + } catch (e) { return false; } } @override - Future downloadFile( + Future downloadFile( String path, { Function(int, int)? onProgress, }) async { if (!path.startsWith(_webdavPath)) { path = join(_webdavPath, path); } - return Uint8List.fromList( - await client.read( - path, - onProgress: (c, t) { - onProgress?.call(c, t); - }, - ), - ); + try { + return Uint8List.fromList( + await client.read( + path, + onProgress: (c, t) { + onProgress?.call(c, t); + }, + ), + ); + } catch (e) { + return null; + } } @override @@ -158,7 +171,8 @@ class WebDavCloudService extends CloudService { @override Future deleteOldBackup([int? maxCount]) async { maxCount ??= HiveUtil.getMaxBackupsCount(); - List list = await listBackups(); + List? list = await listBackups(); + if (list == null) return false; list.sort((a, b) { if (a.mTime == null || b.mTime == null) return 0; return a.mTime!.compareTo(b.mTime!); diff --git a/lib/TokenUtils/import_token_util.dart b/lib/TokenUtils/import_token_util.dart index 5790555f..5e271d23 100644 --- a/lib/TokenUtils/import_token_util.dart +++ b/lib/TokenUtils/import_token_util.dart @@ -8,6 +8,7 @@ import 'package:cloudotp/Utils/app_provider.dart'; import 'package:cloudotp/Utils/itoast.dart'; import 'package:cloudotp/Utils/responsive_util.dart'; import 'package:cloudotp/Widgets/Dialog/custom_dialog.dart'; +import 'package:cloudotp/Widgets/Dialog/progress_dialog.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:image/image.dart' as img; @@ -20,7 +21,9 @@ import '../Utils/constant.dart'; import '../Utils/file_util.dart'; import '../Utils/utils.dart'; import '../Widgets/BottomSheet/bottom_sheet_builder.dart'; +import '../Widgets/BottomSheet/input_bottom_sheet.dart'; import '../Widgets/BottomSheet/token_option_bottom_sheet.dart'; +import '../Widgets/Item/input_item.dart'; import '../generated/l10n.dart'; import 'Backup/backup.dart'; import 'Backup/backup_encrypt_interface.dart'; @@ -473,4 +476,74 @@ class ImportTokenUtil { } return newCategoryList.length; } + + static importFromCloud( + BuildContext context, + Uint8List? res, + ProgressDialog dialog, + ) async { + dialog.updateMessage( + msg: S.current.importing, + showProgress: false, + ); + if (res == null) { + dialog.dismiss(); + IToast.showTop(S.current.webDavPullFailed); + return; + } + bool success = await ImportTokenUtil.importBackupFile( + res, + showLoading: false, + ); + dialog.dismiss(); + if (!success) { + InputValidateAsyncController validateAsyncController = + InputValidateAsyncController( + listen: false, + validator: (text) async { + if (text.isEmpty) { + return S.current.autoBackupPasswordCannotBeEmpty; + } + dialog.show( + msg: S.current.importing, + showProgress: false, + ); + bool success = await ImportTokenUtil.importBackupFile( + password: text, + res, + showLoading: false, + ); + dialog.dismiss(); + if (success) { + return null; + } else { + return S.current.invalidPasswordOrDataCorrupted; + } + }, + controller: TextEditingController(), + ); + BottomSheetBuilder.showBottomSheet( + context, + responsive: true, + (context) => InputBottomSheet( + validator: (value) { + if (value.isEmpty) { + return S.current.autoBackupPasswordCannotBeEmpty; + } + return null; + }, + checkSyncValidator: false, + validateAsyncController: validateAsyncController, + title: S.current.inputImportPasswordTitle, + message: S.current.inputImportPasswordTip, + hint: S.current.inputImportPasswordHint, + inputFormatters: [ + RegexInputFormatter.onlyNumberAndLetter, + ], + tailingType: InputItemTailingType.password, + onValidConfirm: (password) async {}, + ), + ); + } + } } diff --git a/lib/Utils/file_util.dart b/lib/Utils/file_util.dart index e23953c0..bcce2275 100644 --- a/lib/Utils/file_util.dart +++ b/lib/Utils/file_util.dart @@ -4,6 +4,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:cloudotp/Models/github_response.dart'; import 'package:cloudotp/Utils/uri_util.dart'; import 'package:cloudotp/Utils/utils.dart'; +import 'package:device_info_plus/device_info_plus.dart'; import 'package:dio/dio.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; @@ -32,7 +33,7 @@ class FileUtil { } static Future getBackupDir() async { - Directory directory = Directory(join(await getApplicationDir(), "Backup" )); + Directory directory = Directory(join(await getApplicationDir(), "Backup")); if (!await directory.exists()) { await directory.create(recursive: true); } @@ -40,7 +41,8 @@ class FileUtil { } static Future getScreenshotDir() async { - Directory directory = Directory(join(await getApplicationDir(), "Screenshots")); + Directory directory = + Directory(join(await getApplicationDir(), "Screenshots")); if (!await directory.exists()) { await directory.create(recursive: true); } @@ -64,7 +66,8 @@ class FileUtil { } static Future getDatabaseDir() async { - Directory directory = Directory(join(await getApplicationDir(), "Database")); + Directory directory = + Directory(join(await getApplicationDir(), "Database")); if (!await directory.exists()) { await directory.create(recursive: true); } @@ -201,10 +204,30 @@ class FileUtil { return copiedFile; } - static ReleaseAsset getAndroidAsset(ReleaseItem item) { - return item.assets.firstWhere((element) => - element.contentType == "application/vnd.android.package-archive" && - element.name.endsWith(".zip")); + static Future getAndroidAsset( + String latestVersion, ReleaseItem item) async { + List assets = item.assets + .where((element) => + element.contentType == "application/vnd.android.package-archive" && + element.name.endsWith(".apk")) + .toList(); + ReleaseAsset generalAsset = assets.firstWhere( + (element) => element.name == "CloudOTP-$latestVersion.apk", + orElse: () => assets.first); + try { + DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; + List supportedAbis = + androidInfo.supportedAbis.map((e) => e.toLowerCase()).toList(); + for (var asset in assets) { + String abi = + asset.name.split("CloudOTP-$latestVersion-").last.split(".").first; + if (supportedAbis.contains(abi.toLowerCase())) { + return asset; + } + } + } finally {} + return generalAsset; } static ReleaseAsset getWindowsPortableAsset(ReleaseItem item) { diff --git a/lib/Utils/utils.dart b/lib/Utils/utils.dart index 7b5d37db..f7018afd 100644 --- a/lib/Utils/utils.dart +++ b/lib/Utils/utils.dart @@ -409,7 +409,8 @@ class Utils { if (showLoading) { CustomLoadingDialog.showLoading(title: S.current.checkingUpdates); } - String currentVersion = (await PackageInfo.fromPlatform()).version; + String currentVersion = + "2.0.0" ?? (await PackageInfo.fromPlatform()).version; onGetCurrentVersion?.call(currentVersion); String latestVersion = "0.0.0"; await GithubApi.getReleases("Robert-Stackflow", "CloudOTP") @@ -445,13 +446,13 @@ class Utils { messageTextAlign: TextAlign.start, confirmButtonText: S.current.immediatelyDownload, cancelButtonText: S.current.updateLater, - onTapConfirm: () { + onTapConfirm: () async { if (ResponsiveUtil.isDesktop()) { UriUtil.openExternal(latestReleaseItem!.htmlUrl); return; - } else { - ReleaseAsset androidAssset = - FileUtil.getAndroidAsset(latestReleaseItem!); + } else if (ResponsiveUtil.isAndroid()) { + ReleaseAsset androidAssset = await FileUtil.getAndroidAsset( + latestVersion, latestReleaseItem!); if (ResponsiveUtil.isAndroid()) { FileUtil.downloadAndUpdate( context, diff --git a/lib/Widgets/BottomSheet/input_bottom_sheet.dart b/lib/Widgets/BottomSheet/input_bottom_sheet.dart index 746c5f83..6b8a160f 100644 --- a/lib/Widgets/BottomSheet/input_bottom_sheet.dart +++ b/lib/Widgets/BottomSheet/input_bottom_sheet.dart @@ -43,6 +43,7 @@ class InputBottomSheet extends StatefulWidget { this.preventPop = false, this.validator, this.validateAsyncController, + this.checkSyncValidator = true, }); final String? hint; @@ -51,6 +52,7 @@ class InputBottomSheet extends StatefulWidget { final String message; final int maxLines; final int minLines; + final bool checkSyncValidator; final InputValidateAsyncController? validateAsyncController; final FormFieldValidator? validator; final Function()? onCancel; @@ -91,13 +93,14 @@ class InputBottomSheetState extends State { @override void initState() { super.initState(); - controller = widget.validateAsyncController?.controller??TextEditingController(); - controller.value = TextEditingValue(text: widget.text); + controller = + widget.validateAsyncController?.controller ?? TextEditingController(); + if (mounted) controller.value = TextEditingValue(text: widget.text); widget.validateAsyncController?.doPop = () { Navigator.of(context).pop(); }; Future.delayed(const Duration(milliseconds: 200), () { - FocusScope.of(context).requestFocus(_focusNode); + if (mounted) FocusScope.of(context).requestFocus(_focusNode); }); } @@ -131,7 +134,6 @@ class InputBottomSheetState extends State { Center( child: Form( key: formKey, - autovalidateMode: AutovalidateMode.always, child: InputItem( controller: controller, focusNode: _focusNode, @@ -165,6 +167,7 @@ class InputBottomSheetState extends State { maxLength: widget.maxLength, inputFormatters: widget.inputFormatters, leadingMinWidth: widget.leadingMinWidth, + onSubmit: (_) => processConfirm(), ), ), ), @@ -199,6 +202,20 @@ class InputBottomSheetState extends State { ); } + processConfirm() async { + bool isValid = widget.checkSyncValidator + ? formKey.currentState?.validate() ?? false + : true; + bool isValidAsync = await widget.validateAsyncController?.isValid() ?? true; + widget.onConfirm?.call(controller.text); + if (isValid && isValidAsync) { + await widget.onValidConfirm?.call(controller.text); + if (!widget.preventPop) { + Navigator.of(context).pop(); + } + } + } + _buildFooter() { return Container( padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 0), @@ -230,19 +247,7 @@ class InputBottomSheetState extends State { background: Theme.of(context).primaryColor, color: Colors.white, text: S.current.confirm, - onTap: () async { - bool isValid = formKey.currentState?.validate() ?? false; - String? error = - await widget.validateAsyncController?.validate(); - bool isValidAsync = (error == null); - widget.onConfirm?.call(controller.text); - if (isValid && isValidAsync) { - await widget.onValidConfirm?.call(controller.text); - if (!widget.preventPop) { - Navigator.of(context).pop(); - } - } - }, + onTap: processConfirm, fontSizeDelta: 2, ), ), diff --git a/lib/Widgets/BottomSheet/input_password_bottom_sheet.dart b/lib/Widgets/BottomSheet/input_password_bottom_sheet.dart index 76ed1ce6..a7f4ecdf 100644 --- a/lib/Widgets/BottomSheet/input_password_bottom_sheet.dart +++ b/lib/Widgets/BottomSheet/input_password_bottom_sheet.dart @@ -61,7 +61,6 @@ class InputPasswordBottomSheetState extends State { ), child: Form( key: formKey, - autovalidateMode: AutovalidateMode.always, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, diff --git a/lib/Widgets/BottomSheet/select_category_bottom_sheet.dart b/lib/Widgets/BottomSheet/select_category_bottom_sheet.dart index 24cbc5b1..3f3e801c 100644 --- a/lib/Widgets/BottomSheet/select_category_bottom_sheet.dart +++ b/lib/Widgets/BottomSheet/select_category_bottom_sheet.dart @@ -99,7 +99,9 @@ class SelectCategoryBottomSheetState extends State { alignment: Alignment.center, child: Text( textAlign: TextAlign.center, - S.current.setCategoryForToken(widget.token.issuer), + widget.token.issuer.isNotEmpty + ? S.current.setCategoryForTokenDetail(widget.token.issuer) + : S.current.setCategoryForToken, style: Theme.of(context).textTheme.titleLarge, ), ); diff --git a/lib/Widgets/Item/input_item.dart b/lib/Widgets/Item/input_item.dart index 50c0495d..ca5a9ce4 100644 --- a/lib/Widgets/Item/input_item.dart +++ b/lib/Widgets/Item/input_item.dart @@ -23,7 +23,9 @@ class InputValidateAsyncController { required this.controller, bool listen = true, }) { - if (listen) controller.addListener(validate); + if (listen) { + controller.addListener(validate); + } } Future validate() async { @@ -128,7 +130,7 @@ class InputItemState extends State { String? get hint => widget.hint; - TextEditingController? controller; + late TextEditingController controller; late bool obscureText; @@ -175,8 +177,9 @@ class InputItemState extends State { @override void initState() { super.initState(); - controller = - widget.validateAsyncController?.controller ?? widget.controller; + controller = widget.validateAsyncController?.controller ?? + widget.controller ?? + TextEditingController(); obscureText = widget.obscureText ?? false; widget.validateAsyncController?.onError = () { if (mounted) setState(() {}); @@ -218,6 +221,7 @@ class InputItemState extends State { color: Colors.transparent, child: TextFormField( focusNode: focusNode, + autovalidateMode: AutovalidateMode.onUserInteraction, controller: controller, textInputAction: textInputAction, keyboardType: keyboardType, @@ -324,7 +328,7 @@ class InputItemState extends State { tailing = Icon(Icons.clear_rounded, color: theme.iconTheme.color?.withAlpha(120)); defaultTapFunction = () { - if (controller != null) controller!.clear(); + controller.clear(); }; } if (tailingType == InputItemTailingType.password) { diff --git a/lib/Widgets/Item/item_builder.dart b/lib/Widgets/Item/item_builder.dart index 7da491b6..c612a824 100644 --- a/lib/Widgets/Item/item_builder.dart +++ b/lib/Widgets/Item/item_builder.dart @@ -238,8 +238,8 @@ class ItemBuilder { title, style: Theme.of(context) .textTheme - .labelMedium - ?.apply(fontWeightDelta: 2, fontSizeDelta: 1), + .titleMedium + ?.apply(fontWeightDelta: 2, fontSizeDelta: -2), ), ), SizedBox( @@ -251,6 +251,7 @@ class ItemBuilder { constraintWidth: constraintWidth, radius: 8, enableDeselect: enableDeselect, + mainGroupAlignment: MainGroupAlignment.start, onSelected: onSelected, ), ), @@ -266,14 +267,15 @@ class ItemBuilder { bool disabled = false, bool isRadio = true, double? radius, + MainGroupAlignment mainGroupAlignment = MainGroupAlignment.center, bool constraintWidth = true, Function(dynamic value, int index, bool isSelected)? onSelected, }) { return GroupButton( isRadio: isRadio, enableDeselect: enableDeselect, - options: const GroupButtonOptions( - mainGroupAlignment: MainGroupAlignment.start, + options: GroupButtonOptions( + mainGroupAlignment: mainGroupAlignment, runSpacing: 6, spacing: 6, ), @@ -316,7 +318,9 @@ class ItemBuilder { isRadio: isRadio, enableDeselect: enableDeselect, options: const GroupButtonOptions( - mainGroupAlignment: MainGroupAlignment.start, + mainGroupAlignment: MainGroupAlignment.center, + runSpacing: 6, + spacing: 6, ), disabled: disabled, onSelected: onSelected, @@ -1363,6 +1367,7 @@ class ItemBuilder { String? text, bool forceDark = false, Color? background, + MainAxisAlignment mainAxisAlignment = MainAxisAlignment.center, }) { return Center( child: Container( @@ -1371,7 +1376,7 @@ class ItemBuilder { padding: EdgeInsets.only(top: topPadding, bottom: bottomPadding), child: Column( mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, + mainAxisAlignment: mainAxisAlignment, children: [ LottieUtil.load( LottieUtil.getLoadingPath(context, forceDark: forceDark), diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 7080953e..40c9cd1c 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -455,7 +455,8 @@ "searchIconName": "Search icon name", "setIconForToken": "Select token icon", "setIconForTokenDetail": "Select the icon of token {issuer}", - "setCategoryForToken": "Select the category of token {issuer}", + "setCategoryForToken": "Select the category of token", + "setCategoryForTokenDetail": "Select the category of token {issuer}", "setTokenForCategory": "Select the token with category {category}", "category": "Category", "inputCategory": "Enter category name", diff --git a/lib/l10n/intl_zh_CN.arb b/lib/l10n/intl_zh_CN.arb index 343392ed..d54ddaa5 100644 --- a/lib/l10n/intl_zh_CN.arb +++ b/lib/l10n/intl_zh_CN.arb @@ -454,7 +454,8 @@ "searchIconName": "搜索图标名称", "setIconForToken": "选择令牌图标", "setIconForTokenDetail": "选择令牌「{issuer}」的图标", - "setCategoryForToken": "选择令牌「{issuer}」的分类", + "setCategoryForToken": "选择令牌分类", + "setCategoryForTokenDetail": "选择令牌「{issuer}」的分类", "setTokenForCategory": "选择分类为「{category}」的令牌", "category": "分类", "addCategory": "新建分类", diff --git a/pubspec.lock b/pubspec.lock index 56cf875e..968c2301 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -716,10 +716,9 @@ packages: flutter_windowmanager: dependency: "direct main" description: - name: flutter_windowmanager - sha256: b4d0bc06f6777952b729c0cdb7ce9ad1ecabd8b8b1cb0acb57a36621457dab1b - url: "https://pub.flutter-io.cn" - source: hosted + path: "third-party/flutter_windowmanager" + relative: true + source: path version: "0.2.0" fluttertoast: dependency: "direct main" diff --git a/pubspec.yaml b/pubspec.yaml index e0d3bb66..95dbf7bf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: cloudotp -version: 2.0.0 +version: 2.1.0 description: An awesome two-factor authenticator which supports cloud storage and multiple platforms. publish_to: none @@ -84,10 +84,11 @@ dependencies: permission_handler: ^11.3.1 # 权限管理 responsive_builder: ^0.7.1 # 响应式布局 flutter_displaymode: ^0.6.0 # 设置刷新率 - flutter_windowmanager: ^0.2.0 # 窗口管理 flutter_local_notifications: ^17.2.1+2 # 本地通知 flutter_resizable_container: ^2.0.0 # 可调整大小的容器 desktop_webview_window: ^0.2.3 # Webview窗口 + flutter_windowmanager: + path: third-party/flutter_windowmanager window_manager: path: third-party/window_manager diff --git a/third-party/flutter_dropbox/lib/flutter_dropbox.dart b/third-party/flutter_dropbox/lib/flutter_dropbox.dart index c3d2e9d2..13ab1b04 100644 --- a/third-party/flutter_dropbox/lib/flutter_dropbox.dart +++ b/third-party/flutter_dropbox/lib/flutter_dropbox.dart @@ -181,8 +181,7 @@ class Dropbox with ChangeNotifier { Future list(String remotePath) async { final accessToken = await _tokenManager.getAccessToken(); if (accessToken == null) { - return DropboxResponse( - message: "Null access token", bodyBytes: Uint8List(0)); + return DropboxResponse(message: "Null access token"); } final url = Uri.parse("$apiEndpoint/files/list_folder"); @@ -225,14 +224,12 @@ class Dropbox with ChangeNotifier { return DropboxResponse( statusCode: resp.statusCode, body: resp.body, - message: "Url not found.", - bodyBytes: Uint8List(0)); + message: "Url not found."); } else { return DropboxResponse( statusCode: resp.statusCode, body: resp.body, - message: "Error while listing files.", - bodyBytes: Uint8List(0)); + message: "Error while listing files."); } } catch (err) { debugPrint("# Dropbox -> list: $err"); diff --git a/third-party/flutter_dropbox/lib/token.dart b/third-party/flutter_dropbox/lib/token.dart index ceb916aa..304318cd 100644 --- a/third-party/flutter_dropbox/lib/token.dart +++ b/third-party/flutter_dropbox/lib/token.dart @@ -72,7 +72,6 @@ class DefaultTokenManager extends ITokenManager { if ((accessToken?.isEmpty) ?? true) { return null; } - final accessTokenExpiresAt = await _secureStorage.read(key: _expireInKey); if ((accessTokenExpiresAt?.isEmpty) ?? true) { return null; @@ -82,13 +81,10 @@ class DefaultTokenManager extends ITokenManager { .add(const Duration(minutes: -2)); if (DateTime.now().toUtc().isAfter(expAt)) { - // expired, refresh final tokenMap = await _refreshToken(); if (tokenMap == null) { - // refresh failed return null; } - // refresh success return tokenMap['access_token']; } @@ -129,7 +125,9 @@ class DefaultTokenManager extends ITokenManager { return tokenMap; } catch (err) { debugPrint("# DefaultTokenManager -> _refreshToken: $err"); - await clearStoredToken(); + if (err is! http.ClientException) { + await clearStoredToken(); + } } return null; diff --git a/third-party/flutter_googledrive/lib/token.dart b/third-party/flutter_googledrive/lib/token.dart index 20f6c263..4af52a3a 100644 --- a/third-party/flutter_googledrive/lib/token.dart +++ b/third-party/flutter_googledrive/lib/token.dart @@ -60,6 +60,7 @@ class DefaultTokenManager extends ITokenManager { _secureStorage.delete(key: _accessTokenKey), _secureStorage.delete(key: _refreshTokenKey), ]); + debugPrint("# DefaultTokenManager -> clearStoredToken: Token has been cleared"); } catch (err) { debugPrint("# DefaultTokenManager -> clearStoredToken: $err"); } @@ -114,7 +115,6 @@ class DefaultTokenManager extends ITokenManager { 'redirect_uri': redirectURL, }); if (resp.statusCode != 200) { - // refresh failed debugPrint( "# DefaultTokenManager -> _refreshToken: ${resp.statusCode}\n# Body: ${resp.body}"); @@ -129,7 +129,9 @@ class DefaultTokenManager extends ITokenManager { return tokenMap; } catch (err) { debugPrint("# DefaultTokenManager -> _refreshToken: $err"); - await clearStoredToken(); + if (err is! http.ClientException) { + await clearStoredToken(); + } } return null; diff --git a/third-party/flutter_onedrive/lib/token.dart b/third-party/flutter_onedrive/lib/token.dart index 714fa55b..7e072eb4 100644 --- a/third-party/flutter_onedrive/lib/token.dart +++ b/third-party/flutter_onedrive/lib/token.dart @@ -127,7 +127,9 @@ class DefaultTokenManager extends ITokenManager { return tokenMap; } catch (err) { debugPrint("# DefaultTokenManager -> _refreshToken: $err"); - await clearStoredToken(); + if (err is! http.ClientException) { + await clearStoredToken(); + } } return null; diff --git a/third-party/flutter_windowmanager/CHANGELOG.md b/third-party/flutter_windowmanager/CHANGELOG.md new file mode 100644 index 00000000..65a16028 --- /dev/null +++ b/third-party/flutter_windowmanager/CHANGELOG.md @@ -0,0 +1,21 @@ +## 0.2.0 + +* Finish flutter embedding v2 migration. #17 + +## 0.1.0 + +* null-safety migration (@ValeteTech, PR#16) +* Switch to version 2 of flutter embedding. +* Add documentation + +## 0.0.2 + +* Update pubspec.yaml format for newer versions of Flutter, require 1.10. + +## 0.0.1+1 + +* Suppress deprecation warnings in build, all uses are safe. + +## 0.0.1 + +* Initial release. diff --git a/third-party/flutter_windowmanager/LICENSE b/third-party/flutter_windowmanager/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/third-party/flutter_windowmanager/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/third-party/flutter_windowmanager/README.md b/third-party/flutter_windowmanager/README.md new file mode 100644 index 00000000..6fb0fcff --- /dev/null +++ b/third-party/flutter_windowmanager/README.md @@ -0,0 +1,108 @@ +# flutter_windowmanager + +[![Build Status](https://travis-ci.com/adaptant-labs/flutter_windowmanager.svg?branch=master)](https://app.travis-ci.com/github/adaptant-labs/flutter_windowmanager) +[![Pub](https://img.shields.io/pub/v/flutter_windowmanager.svg)](https://pub.dartlang.org/packages/flutter_windowmanager) + +A Flutter plugin for manipulating Android WindowManager LayoutParams +dynamically at application run-time. + +Example App Use + +## Motivation + +While Android natively supports a range of window modes, there was no +good way to set these dynamically within a running Flutter application - +instead requiring that these flags are set within the native +`MainActivity` of the Flutter application itself. + +In our App, we only wished to disable screenshots for specific screens, +rather than across the entire application lifecycle. This can now be +accomplished by simply calling: + +``` +await FlutterWindowManager.addFlags(FlutterWindowManager.FLAG_SECURE); +``` + +for the relevant screen. + +This can further be toggled for a specific screen by either using a +[RouteAware] mixin, or through direct toggling in `initState()` and +`dispose()` methods in the case of stateful widgets. + +[RouteAware]: https://api.flutter.dev/flutter/widgets/RouteAware-class.html + +## Flags + +The full range of [LayoutParams] flags are passed through. The plugin +will carry out basic API level checking and throw an error on any +unsupported flag specification. Flags are implemented using a bitmask, +and may be specified individually or ORed together for setting/clearing +multiple flags at once. + +The current list of flags is: + +``` +FLAG_ALLOW_LOCK_WHILE_SCREEN_ON +FLAG_ALT_FOCUSABLE_IM +FLAG_DIM_BEHIND +FLAG_FORCE_NOT_FULLSCREEN +FLAG_FULLSCREEN +FLAG_HARDWARE_ACCELERATED +FLAG_IGNORE_CHEEK_PRESSES +FLAG_KEEP_SCREEN_ON +FLAG_LAYOUT_INSET_DECOR +FLAG_LAYOUT_IN_SCREEN +FLAG_LAYOUT_NO_LIMITS +FLAG_NOT_FOCUSABLE +FLAG_NOT_TOUCHABLE +FLAG_NOT_TOUCH_MODAL +FLAG_SCALED +FLAG_SECURE +FLAG_SHOW_WALLPAPER +FLAG_SPLIT_TOUCH +FLAG_WATCH_OUTSIDE_TOUCH +FLAG_BLUR_BEHIND +FLAG_DISMISS_KEYGUARD +FLAG_DITHER +FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS +FLAG_LAYOUT_ATTACHED_IN_DECOR +FLAG_LAYOUT_IN_OVERSCAN +FLAG_LOCAL_FOCUS_MODE +FLAG_SHOW_WHEN_LOCKED +FLAG_TOUCHABLE_WHEN_WAKING +FLAG_TRANSLUCENT_NAVIGATION +FLAG_TRANSLUCENT_STATUS +FLAG_TURN_SCREEN_ON +``` + +In practice, this plugin was developed primarily for the toggling of +`FLAG_SECURE`. Other flags have not been tested, and we make no +guarantees that toggling with any of the other flags will interact well +with Flutter - if you find specific problems with any particular flag, +please let us know in the [issue tracker][tracker]. + +[LayoutParams]: https://developer.android.com/reference/android/view/WindowManager.LayoutParams.html + +## iOS Support + +As `flutter_windowmanager` only wraps and exposes an underlying Android-specific +interface, there is no iOS support planned or possible. For those interested in +cross-platform `FLAG_SECURE` functionality, this functionality has been re-created +in the third-party [secure_application] package. Cross-platform `FLAG_KEEP_SCREEN_ON` +functionality is provided by the third-party [keep_screen_on] package. + +[secure_application]: https://pub.dev/packages/secure_application +[keep_screen_on]: https://pub.dev/packages/keep_screen_on + +## Features and bugs + +Please file feature requests and bugs at the [issue tracker][tracker]. + +[tracker]: https://github.com/adaptant-labs/flutter_windowmanager/issues + +## License + +Licensed under the terms of the Apache 2.0 license, the full version of which can be found in the +[LICENSE] file included in the distribution. + +[LICENSE]: https://raw.githubusercontent.com/adaptant-labs/flutter_windowmanager/master/LICENSE \ No newline at end of file diff --git a/third-party/flutter_windowmanager/android/build.gradle b/third-party/flutter_windowmanager/android/build.gradle new file mode 100644 index 00000000..2afa881c --- /dev/null +++ b/third-party/flutter_windowmanager/android/build.gradle @@ -0,0 +1,35 @@ +group 'io.adaptant.labs.flutter_windowmanager' +version '1.0' + +buildscript { + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.2.1' + } +} + +rootProject.allprojects { + repositories { + google() + jcenter() + } +} + +apply plugin: 'com.android.library' + +android { + compileSdkVersion 34 + + defaultConfig { + targetSdkVersion 34 + minSdkVersion 21 + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + lintOptions { + disable 'InvalidPackage' + } +} diff --git a/third-party/flutter_windowmanager/android/gradle.properties b/third-party/flutter_windowmanager/android/gradle.properties new file mode 100644 index 00000000..2bd6f4fd --- /dev/null +++ b/third-party/flutter_windowmanager/android/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx1536M + diff --git a/third-party/flutter_windowmanager/android/gradle/wrapper/gradle-wrapper.properties b/third-party/flutter_windowmanager/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..019065d1 --- /dev/null +++ b/third-party/flutter_windowmanager/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip diff --git a/third-party/flutter_windowmanager/android/local.properties b/third-party/flutter_windowmanager/android/local.properties new file mode 100644 index 00000000..a3a4c8a2 --- /dev/null +++ b/third-party/flutter_windowmanager/android/local.properties @@ -0,0 +1,2 @@ +sdk.dir=/opt/sdk/Android/Sdk +flutter.sdk=/opt/sdk/flutter \ No newline at end of file diff --git a/third-party/flutter_windowmanager/android/settings.gradle b/third-party/flutter_windowmanager/android/settings.gradle new file mode 100644 index 00000000..a0a74dea --- /dev/null +++ b/third-party/flutter_windowmanager/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'flutter_windowmanager' diff --git a/third-party/flutter_windowmanager/android/src/main/AndroidManifest.xml b/third-party/flutter_windowmanager/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..07f0c40c --- /dev/null +++ b/third-party/flutter_windowmanager/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/third-party/flutter_windowmanager/android/src/main/java/io/adaptant/labs/flutter_windowmanager/FlutterWindowManagerPlugin.java b/third-party/flutter_windowmanager/android/src/main/java/io/adaptant/labs/flutter_windowmanager/FlutterWindowManagerPlugin.java new file mode 100644 index 00000000..ae3a95ca --- /dev/null +++ b/third-party/flutter_windowmanager/android/src/main/java/io/adaptant/labs/flutter_windowmanager/FlutterWindowManagerPlugin.java @@ -0,0 +1,166 @@ +package io.adaptant.labs.flutter_windowmanager; + +import android.app.Activity; +import android.os.Build; +import android.view.WindowManager; + +import androidx.annotation.NonNull; + +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.embedding.engine.plugins.activity.ActivityAware; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.MethodChannel.MethodCallHandler; +import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.plugin.common.PluginRegistry.Registrar; + +/** FlutterWindowManagerPlugin */ +public class FlutterWindowManagerPlugin implements MethodCallHandler, FlutterPlugin, ActivityAware { + private Activity activity; + + @SuppressWarnings("unused") + public FlutterWindowManagerPlugin() { } + + private FlutterWindowManagerPlugin(Activity activity) { + this.activity = activity; + } + + /** Plugin registration. */ + @Deprecated + public static void registerWith(Registrar registrar) { + new FlutterWindowManagerPlugin(registrar.activity()).registerWith(registrar.messenger()); + } + + private void registerWith(BinaryMessenger binaryMessenger) { + final MethodChannel channel = new MethodChannel(binaryMessenger, "flutter_windowmanager"); + channel.setMethodCallHandler(this); + } + + + @Override + public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) { + registerWith(flutterPluginBinding.getBinaryMessenger()); + } + + @Override + public void onDetachedFromEngine(@NonNull FlutterPluginBinding flutterPluginBinding) { + + } + + /** + * Validate flag specification against WindowManager.LayoutParams and API levels, as per: + * https://developer.android.com/reference/android/view/WindowManager.LayoutParams + */ + @SuppressWarnings("deprecation") + private boolean validLayoutParam(int flag) { + switch (flag) { + case WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON: + case WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM: + case WindowManager.LayoutParams.FLAG_DIM_BEHIND: + case WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN: + case WindowManager.LayoutParams.FLAG_FULLSCREEN: + case WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED: + case WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES: + case WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON: + case WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR: + case WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN: + case WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS: + case WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE: + case WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE: + case WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL: + case WindowManager.LayoutParams.FLAG_SCALED: + case WindowManager.LayoutParams.FLAG_SECURE: + case WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER: + case WindowManager.LayoutParams.FLAG_SPLIT_TOUCH: + case WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH: + return true; + case WindowManager.LayoutParams.FLAG_BLUR_BEHIND: + return !(Build.VERSION.SDK_INT >= 15); + case WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD: + return (Build.VERSION.SDK_INT >= 5 && Build.VERSION.SDK_INT < 26); + case WindowManager.LayoutParams.FLAG_DITHER: + return !(Build.VERSION.SDK_INT >= 17); + case WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS: + return (Build.VERSION.SDK_INT >= 21); + case WindowManager.LayoutParams.FLAG_LAYOUT_ATTACHED_IN_DECOR: + return (Build.VERSION.SDK_INT >= 22); + case WindowManager.LayoutParams.FLAG_LAYOUT_IN_OVERSCAN: + return (Build.VERSION.SDK_INT >= 18); + case WindowManager.LayoutParams.FLAG_LOCAL_FOCUS_MODE: + return (Build.VERSION.SDK_INT >= 19); + case WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED: + return !(Build.VERSION.SDK_INT >= 27); + case WindowManager.LayoutParams.FLAG_TOUCHABLE_WHEN_WAKING: + return !(Build.VERSION.SDK_INT >= 20); + case WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION: + case WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS: + return (Build.VERSION.SDK_INT >= 19); + case WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON: + return !(Build.VERSION.SDK_INT >= 27); + default: + return false; + } + } + + private boolean validLayoutParams(Result result, int flags) { + for (int i = 0; i < Integer.SIZE; i++) { + int flag = (1 << i); + if ((flags & flag) == 1) { + if (!validLayoutParam(flag)) { + result.error("FlutterWindowManagerPlugin","FlutterWindowManagerPlugin: invalid flag specification: " + Integer.toHexString(flag), null); + return false; + } + } + } + + return true; + } + + @Override + public void onMethodCall(MethodCall call, Result result) { + final int flags = call.argument("flags"); + + if (activity == null) { + result.error("FlutterWindowManagerPlugin", "FlutterWindowManagerPlugin: ignored flag state change, current activity is null", null); + } + + if (!validLayoutParams(result, flags)) { + return; + } + + switch (call.method) { + case "addFlags": + activity.getWindow().addFlags(flags); + result.success(true); + break; + case "clearFlags": + activity.getWindow().clearFlags(flags); + result.success(true); + break; + default: + result.notImplemented(); + } + } + + @Override + public void onAttachedToActivity(@NonNull ActivityPluginBinding activityPluginBinding) { + activity = activityPluginBinding.getActivity(); + } + + @Override + public void onDetachedFromActivityForConfigChanges() { + activity = null; + } + + @Override + public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding activityPluginBinding) { + onAttachedToActivity(activityPluginBinding); + } + + @Override + public void onDetachedFromActivity() { + activity = null; + } +} diff --git a/third-party/flutter_windowmanager/ios/Classes/FlutterWindowmanagerPlugin.h b/third-party/flutter_windowmanager/ios/Classes/FlutterWindowmanagerPlugin.h new file mode 100644 index 00000000..bd0a6348 --- /dev/null +++ b/third-party/flutter_windowmanager/ios/Classes/FlutterWindowmanagerPlugin.h @@ -0,0 +1,4 @@ +#import + +@interface FlutterWindowmanagerPlugin : NSObject +@end diff --git a/third-party/flutter_windowmanager/ios/Classes/FlutterWindowmanagerPlugin.m b/third-party/flutter_windowmanager/ios/Classes/FlutterWindowmanagerPlugin.m new file mode 100644 index 00000000..fd2d7f25 --- /dev/null +++ b/third-party/flutter_windowmanager/ios/Classes/FlutterWindowmanagerPlugin.m @@ -0,0 +1,15 @@ +#import "FlutterWindowmanagerPlugin.h" +#if __has_include() +#import +#else +// Support project import fallback if the generated compatibility header +// is not copied when this plugin is created as a library. +// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 +#import "flutter_windowmanager-Swift.h" +#endif + +@implementation FlutterWindowmanagerPlugin ++ (void)registerWithRegistrar:(NSObject*)registrar { + [SwiftFlutterWindowmanagerPlugin registerWithRegistrar:registrar]; +} +@end diff --git a/third-party/flutter_windowmanager/ios/Classes/SwiftFlutterWindowmanagerPlugin.swift b/third-party/flutter_windowmanager/ios/Classes/SwiftFlutterWindowmanagerPlugin.swift new file mode 100644 index 00000000..49be7e03 --- /dev/null +++ b/third-party/flutter_windowmanager/ios/Classes/SwiftFlutterWindowmanagerPlugin.swift @@ -0,0 +1,14 @@ +import Flutter +import UIKit + +public class SwiftFlutterWindowmanagerPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "flutter_windowmanager", binaryMessenger: registrar.messenger()) + let instance = SwiftFlutterWindowmanagerPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + result("iOS " + UIDevice.current.systemVersion) + } +} diff --git a/third-party/flutter_windowmanager/ios/flutter_windowmanager.podspec b/third-party/flutter_windowmanager/ios/flutter_windowmanager.podspec new file mode 100644 index 00000000..f1c17883 --- /dev/null +++ b/third-party/flutter_windowmanager/ios/flutter_windowmanager.podspec @@ -0,0 +1,23 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint flutter_windowmanager.podspec' to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'flutter_windowmanager' + s.version = '0.0.1' + s.summary = 'A new flutter plugin project.' + s.description = <<-DESC +A new flutter plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.platform = :ios, '8.0' + + # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } + s.swift_version = '5.0' +end diff --git a/third-party/flutter_windowmanager/lib/flutter_windowmanager.dart b/third-party/flutter_windowmanager/lib/flutter_windowmanager.dart new file mode 100644 index 00000000..3f8829ee --- /dev/null +++ b/third-party/flutter_windowmanager/lib/flutter_windowmanager.dart @@ -0,0 +1,122 @@ +import 'dart:async'; + +import 'package:flutter/services.dart'; + +/// A base class for manipulating Android WindowManager.LayoutParams. +/// +/// The class does not need to be instantiated directly, as it provides all +/// static flags and methods. +class FlutterWindowManager { + // Flags for WindowManager.LayoutParams, as per + // https://developer.android.com/reference/android/view/WindowManager.LayoutParams.html + + /// Window flag: as long as this window is visible to the user, allow the lock screen to activate while the screen is on. + static const int FLAG_ALLOW_LOCK_WHILE_SCREEN_ON = 0x00000001; + + /// Window flag: when set, inverts the input method focusability of the window. + static const int FLAG_ALT_FOCUSABLE_IM = 0x00020000; + + /// Window flag: everything behind this window will be dimmed. + static const int FLAG_DIM_BEHIND = 0x00000002; + + /// This constant was deprecated in API level 30. This value became API "by accident", and shouldn't be used by 3rd party applications. + static const int FLAG_FORCE_NOT_FULLSCREEN = 0x00000800; + + /// This constant was deprecated in API level 30. Use WindowInsetsController#hide(int) with Type#statusBars() instead. + static const int FLAG_FULLSCREEN = 0x00000400; + + /// Indicates whether this window should be hardware accelerated. + static const int FLAG_HARDWARE_ACCELERATED = 0x01000000; + + /// Window flag: intended for windows that will often be used when the user is holding the screen against their face, it will aggressively filter the event stream to prevent unintended presses in this situation that may not be desired for a particular window, when such an event stream is detected, the application will receive a CANCEL motion event to indicate this so applications can handle this accordingly by taking no action on the event until the finger is released. + static const int FLAG_IGNORE_CHEEK_PRESSES = 0x00008000; + + /// Window flag: as long as this window is visible to the user, keep the device's screen turned on and bright. + static const int FLAG_KEEP_SCREEN_ON = 0x00000080; + + /// This constant was deprecated in API level 30. Insets will always be delivered to your application. + static const int FLAG_LAYOUT_INSET_DECOR = 0x00010000; + + /// Window flag for attached windows: Place the window within the entire screen, ignoring any constraints from the parent window. + static const int FLAG_LAYOUT_IN_SCREEN = 0x00000100; + + /// Window flag: allow window to extend outside of the screen. + static const int FLAG_LAYOUT_NO_LIMITS = 0x00000200; + + /// Window flag: this window won't ever get key input focus, so the user can not send key or other button events to it. + static const int FLAG_NOT_FOCUSABLE = 0x00000008; + + /// Window flag: this window can never receive touch events. + static const int FLAG_NOT_TOUCHABLE = 0x00000010; + + /// Window flag: even when this window is focusable (its FLAG_NOT_FOCUSABLE is not set), allow any pointer events outside of the window to be sent to the windows behind it. + static const int FLAG_NOT_TOUCH_MODAL = 0x00000020; + + /// Window flag: a special mode where the layout parameters are used to perform scaling of the surface when it is composited to the screen. + static const int FLAG_SCALED = 0x00004000; + + /// Window flag: treat the content of the window as secure, preventing it from appearing in screenshots or from being viewed on non-secure displays. + static const int FLAG_SECURE = 0x00002000; + + /// Window flag: ask that the system wallpaper be shown behind your window. + static const int FLAG_SHOW_WALLPAPER = 0x00100000; + + /// Window flag: when set the window will accept for touch events outside of its bounds to be sent to other windows that also support split touch. + static const int FLAG_SPLIT_TOUCH = 0x00800000; + + /// Window flag: if you have set FLAG_NOT_TOUCH_MODAL, you can set this flag to receive a single special MotionEvent with the action MotionEvent.ACTION_OUTSIDE for touches that occur outside of your window. + static const int FLAG_WATCH_OUTSIDE_TOUCH = 0x00040000; + + /// Window flag: enable blur behind for this window. + static const int FLAG_BLUR_BEHIND = 0x00000004; + + /// This constant was deprecated in API level 26. Use FLAG_SHOW_WHEN_LOCKED or KeyguardManager#requestDismissKeyguard instead. Since keyguard was dismissed all the time as long as an activity with this flag on its window was focused, keyguard couldn't guard against unintentional touches on the screen, which isn't desired. + static const int FLAG_DISMISS_KEYGUARD = 0x00400000; + + /// This constant was deprecated in API level 17. This flag is no longer used. + static const int FLAG_DITHER = 0x00001000; + + /// Flag indicating that this Window is responsible for drawing the background for the system bars. + static const int FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS = 0x80000000; + + /// Window flag: When requesting layout with an attached window, the attached window may overlap with the screen decorations of the parent window such as the navigation bar. By including this flag, the window manager will layout the attached window within the decor frame of the parent window such that it doesn't overlap with screen decorations. + static const int FLAG_LAYOUT_ATTACHED_IN_DECOR = 0x40000000; + + /// Window flag: allow window contents to extend in to the screen's overscan area, if there is one. The window should still correctly position its contents to take the overscan area into account. + static const int FLAG_LAYOUT_IN_OVERSCAN = 0x02000000; + + /// Flag for a window in local focus mode. + static const int FLAG_LOCAL_FOCUS_MODE = 0x10000000; + + /// Window flag: special flag to let windows be shown when the screen is locked. This will let application windows take precedence over key guard or any other lock screens. Can be used with FLAG_KEEP_SCREEN_ON to turn screen on and display windows directly before showing the key guard window. Can be used with FLAG_DISMISS_KEYGUARD to automatically fully dismisss non-secure keyguards. This flag only applies to the top-most full-screen window. + static const int FLAG_SHOW_WHEN_LOCKED = 0x00080000; + + /// Window flag: when set, if the device is asleep when the touch screen is pressed, you will receive this first touch event. Usually the first touch event is consumed by the system since the user can not see what they are pressing on. + static const int FLAG_TOUCHABLE_WHEN_WAKING = 0x00000040; + + /// Window flag: request a translucent navigation bar with minimal system-provided background protection. + static const int FLAG_TRANSLUCENT_NAVIGATION = 0x08000000; + + /// Window flag: request a translucent status bar with minimal system-provided background protection. + static const int FLAG_TRANSLUCENT_STATUS = 0x04000000; + + /// Window flag: when set as a window is being added or made visible, once the window has been shown then the system will poke the power manager's user activity (as if the user had woken up the device) to turn the screen on. + static const int FLAG_TURN_SCREEN_ON = 0x00200000; + + static const MethodChannel _channel = + const MethodChannel('flutter_windowmanager'); + + /// Adds flags [flags] to the WindowManager.LayoutParams + static Future addFlags(int flags) async { + return await _channel.invokeMethod("addFlags", { + "flags": flags, + }); + } + + /// Clears flags [flags] from the WindowManager.LayoutParams + static Future clearFlags(int flags) async { + return await _channel.invokeMethod("clearFlags", { + "flags": flags, + }); + } +} diff --git a/third-party/flutter_windowmanager/pubspec.yaml b/third-party/flutter_windowmanager/pubspec.yaml new file mode 100644 index 00000000..9d9be720 --- /dev/null +++ b/third-party/flutter_windowmanager/pubspec.yaml @@ -0,0 +1,27 @@ +name: flutter_windowmanager +description: A Flutter plugin for manipulating Android WindowManager LayoutParams. +version: 0.2.0 +homepage: https://github.com/adaptant-labs/flutter_windowmanager +repository: https://github.com/adaptant-labs/flutter_windowmanager +issue_tracker: https://github.com/adaptant-labs/flutter_windowmanager/issues + +environment: + sdk: ">=2.12.0 <3.0.0" + # Flutter versions prior to 1.10 did not support + # the flutter.plugin.platforms map. + flutter: ">=1.10.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + plugin: + platforms: + android: + package: io.adaptant.labs.flutter_windowmanager + pluginClass: FlutterWindowManagerPlugin diff --git a/third-party/webdav_client/lib/src/webdav_dio.dart b/third-party/webdav_client/lib/src/webdav_dio.dart index b9f7cfdc..c38e1840 100644 --- a/third-party/webdav_client/lib/src/webdav_dio.dart +++ b/third-party/webdav_client/lib/src/webdav_dio.dart @@ -107,7 +107,6 @@ class WdDio with DioMixin implements Dio { } // error else { - print('error1: $w3AHeader'); throw newResponseError(resp); } } @@ -119,7 +118,6 @@ class WdDio with DioMixin implements Dio { pwd: self.auth.pwd, dParts: DigestParts(w3AHeader)); } else { - print('error2: $w3AHeader'); await req( self, method,