diff --git a/lib/Database/database_manager.dart b/lib/Database/database_manager.dart index 3410ad93..8b2a21a9 100644 --- a/lib/Database/database_manager.dart +++ b/lib/Database/database_manager.dart @@ -33,7 +33,7 @@ class DatabaseManager { if (lib != null) open.overrideForAll(() => lib!); }); static DatabaseFactory _currentDbFactory = cipherDbFactory; - static bool _isSqlcipherLoaded = false; + static bool isDatabaseEncrypted = false; static bool get initialized => _database != null; @@ -46,26 +46,24 @@ class DatabaseManager { static Future initDataBase(String password) async { if (_database == null) { - _currentDbFactory = cipherDbFactory; String path = join(await FileUtil.getDatabaseDir(), _dbName); File file = File(path); - bool isEncrypted = false; if (file.existsSync()) { final stream = file.openRead(0, _unencrypedFileHeader.length); String content = String.fromCharCodes(await stream.fold>( [], (previous, element) => previous..addAll(element))); if (content == _unencrypedFileHeader) { - isEncrypted = false; + isDatabaseEncrypted = false; _currentDbFactory = dbFactory; ILogger.info( "Database is an unencrypted SQLite database. File header is $content"); } else { - isEncrypted = true; + isDatabaseEncrypted = true; _currentDbFactory = cipherDbFactory; ILogger.info("Database is an encrypted SQLite database."); } } else { - isEncrypted = true; + isDatabaseEncrypted = true; _currentDbFactory = cipherDbFactory; password = await HiveUtil.regeneratePassword(); ILogger.info("Database not exist and new password is $password"); @@ -78,7 +76,7 @@ class DatabaseManager { version: _dbVersion, singleInstance: true, onConfigure: (db) async { - _onConfigure(db, password, isEncrypted); + _onConfigure(db, password); }, onUpgrade: _onUpgrade, onCreate: _onCreate, @@ -90,18 +88,32 @@ class DatabaseManager { static Future changePassword(String password) async { if (_database != null) { - List> res = - await _database!.rawQuery("PRAGMA rekey='$password'"); - if (res.isNotEmpty) { - return true; + if (isDatabaseEncrypted) { + List> res = + await _database!.rawQuery("PRAGMA rekey='$password'"); + ILogger.info("Change database password result is $res"); + if (res.isNotEmpty) { + return true; + } + } else { + try { + await _database!.rawQuery( + "ATTACH DATABASE 'encrypted.db' AS tmp KEY '$password'"); + await _database!.rawQuery("SELECT sqlcipher_export('tmp')"); + await _database!.rawQuery("DETACH DATABASE tmp"); + return true; + } catch (e) { + ILogger.error("Failed to change database password", e); + return false; + } } + return false; } return false; } - static Future _onConfigure( - Database db, String password, bool isEncrypted) async { - if (isEncrypted) { + static Future _onConfigure(Database db, String password) async { + if (isDatabaseEncrypted) { List> res = await db.rawQuery("PRAGMA KEY='$password'"); if (res.isNotEmpty) { @@ -248,12 +260,10 @@ class DatabaseManager { lib = DynamicLibrary.open('/usr/lib/libsqlite3.dylib'); } if (Platform.isWindows) { - lib = DynamicLibrary.open('sqlcipher.dll'); + lib = DynamicLibrary.open('sqlite_sqlcipher.dll'); } - _isSqlcipherLoaded = true; return lib; } catch (e) { - _isSqlcipherLoaded = false; return null; } } diff --git a/lib/Resources/fonts.dart b/lib/Resources/fonts.dart index 0c79c190..dc55d4f4 100644 --- a/lib/Resources/fonts.dart +++ b/lib/Resources/fonts.dart @@ -2,6 +2,7 @@ import "package:cloudotp/Utils/Tuple/tuple.dart"; import "package:cloudotp/Utils/responsive_util.dart"; import "package:flutter/cupertino.dart"; +import "../Utils/app_provider.dart"; import "../Utils/font_util.dart"; import "../Utils/hive_util.dart"; import "../Utils/itoast.dart"; @@ -117,8 +118,11 @@ enum FontEnum { await HiveUtil.put(HiveUtil.fontFamilyKey, item.index); await FontEnum.downloadFont( context: context, + showToast: false, onFinished: (value) { dialog.dismiss(); + appProvider.darkTheme = appProvider.darkTheme; + appProvider.lightTheme = appProvider.lightTheme; if (autoRestartApp) { ResponsiveUtil.restartApp(context); } diff --git a/lib/Screens/Lock/database_decrypt_screen.dart b/lib/Screens/Lock/database_decrypt_screen.dart index 02869bf9..0e49b2c2 100644 --- a/lib/Screens/Lock/database_decrypt_screen.dart +++ b/lib/Screens/Lock/database_decrypt_screen.dart @@ -15,7 +15,6 @@ import '../../Utils/ilogger.dart'; import '../../Utils/responsive_util.dart'; import '../../Utils/uri_util.dart'; import '../../Widgets/Item/input_item.dart'; -import '../../Widgets/Window/window_caption.dart'; import '../../generated/l10n.dart'; import '../main_screen.dart'; @@ -31,6 +30,22 @@ class DatabaseDecryptScreenState extends State final FocusNode _focusNode = FocusNode(); late InputValidateAsyncController validateAsyncController; GlobalKey formKey = GlobalKey(); + bool _isMaximized = false; + bool _isStayOnTop = false; + + @override + void onWindowMaximize() { + setState(() { + _isMaximized = true; + }); + } + + @override + void onWindowUnmaximize() { + setState(() { + _isMaximized = false; + }); + } @override Future onWindowResized() async { @@ -82,17 +97,33 @@ class DatabaseDecryptScreenState extends State Widget build(BuildContext context) { return MyScaffold( backgroundColor: MyTheme.getBackground(context), + appBar: ResponsiveUtil.isDesktop() + ? PreferredSize( + preferredSize: const Size(0, 82), + child: ItemBuilder.buildWindowTitle( + context, + forceClose: true, + backgroundColor: MyTheme.getBackground(context), + isStayOnTop: _isStayOnTop, + isMaximized: _isMaximized, + onStayOnTopTap: () { + setState(() { + _isStayOnTop = !_isStayOnTop; + windowManager.setAlwaysOnTop(_isStayOnTop); + }); + }, + ), + ) + : null, + bottomNavigationBar: Container( + height: 82, + color: MyTheme.getBackground(context), + ), body: SafeArea( right: false, - child: Stack( - children: [ - if (ResponsiveUtil.isDesktop()) const WindowMoveHandle(), - Center( - child: DatabaseManager.lib != null - ? _buildBody() - : _buildFailedBody(), - ), - ], + child: Center( + child: + DatabaseManager.lib != null ? _buildBody() : _buildFailedBody(), ), ), ); @@ -173,13 +204,23 @@ class DatabaseDecryptScreenState extends State mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ - Text(S.current.loadSqlcipherFailed, - style: Theme.of(context).textTheme.titleLarge), + IgnorePointer( + child: Text( + S.current.loadSqlcipherFailed, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleLarge, + ), + ), const SizedBox(height: 30), - SizedBox( - width: min(MediaQuery.sizeOf(context).width - 40, 500), - child: Text(S.current.loadSqlcipherFailedMessage, - style: Theme.of(context).textTheme.titleMedium), + IgnorePointer( + child: SizedBox( + width: min(MediaQuery.sizeOf(context).width - 40, 500), + child: Text( + S.current.loadSqlcipherFailedMessage, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleMedium, + ), + ), ), const SizedBox(height: 30), ItemBuilder.buildRoundButton( diff --git a/lib/Screens/Lock/pin_verify_screen.dart b/lib/Screens/Lock/pin_verify_screen.dart index cc4d51bf..fa0b0aeb 100644 --- a/lib/Screens/Lock/pin_verify_screen.dart +++ b/lib/Screens/Lock/pin_verify_screen.dart @@ -4,10 +4,10 @@ import 'package:cloudotp/Utils/route_util.dart'; import 'package:cloudotp/Utils/utils.dart'; import 'package:cloudotp/Widgets/General/Unlock/gesture_notifier.dart'; import 'package:cloudotp/Widgets/General/Unlock/gesture_unlock_view.dart'; -import 'package:cloudotp/Widgets/Window/window_caption.dart'; import 'package:flutter/material.dart'; import 'package:window_manager/window_manager.dart'; +import '../../Resources/theme.dart'; import '../../Utils/hive_util.dart'; import '../../Utils/responsive_util.dart'; import '../../Widgets/Item/item_builder.dart'; @@ -40,6 +40,22 @@ class PinVerifyScreenState extends State with WindowListener { late final GestureNotifier _notifier = GestureNotifier( status: GestureStatus.verify, gestureText: S.current.verifyGestureLock); final GlobalKey _gestureUnlockView = GlobalKey(); + bool _isMaximized = false; + bool _isStayOnTop = false; + + @override + void onWindowMaximize() { + setState(() { + _isMaximized = true; + }); + } + + @override + void onWindowUnmaximize() { + setState(() { + _isMaximized = false; + }); + } @override Future onWindowResized() async { @@ -86,63 +102,81 @@ class PinVerifyScreenState extends State with WindowListener { @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: MyTheme.getBackground(context), + appBar: ResponsiveUtil.isDesktop() && widget.jumpToMain + ? PreferredSize( + preferredSize: const Size(0, 82), + child: ItemBuilder.buildWindowTitle( + context, + forceClose: true, + backgroundColor: MyTheme.getBackground(context), + isStayOnTop: _isStayOnTop, + isMaximized: _isMaximized, + onStayOnTopTap: () { + setState(() { + _isStayOnTop = !_isStayOnTop; + windowManager.setAlwaysOnTop(_isStayOnTop); + }); + }, + ), + ) + : null, + bottomNavigationBar: Container( + height: widget.jumpToMain ? 82 : 0, + color: MyTheme.getBackground(context), + ), body: SafeArea( right: false, - child: Stack( - children: [ - if (ResponsiveUtil.isDesktop()) const WindowMoveHandle(), - Center( - child: PopScope( - canPop: !widget.isModal, - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const SizedBox(height: 50), - Text( - _notifier.gestureText, - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 30), - Flexible( - child: GestureUnlockView( - key: _gestureUnlockView, - size: min(MediaQuery.sizeOf(context).width, 400), - padding: 60, - roundSpace: 40, - defaultColor: Colors.grey.withOpacity(0.5), - selectedColor: Theme.of(context).primaryColor, - failedColor: Colors.redAccent, - disableColor: Colors.grey, - solidRadiusRatio: 0.3, - lineWidth: 2, - touchRadiusRatio: 0.3, - onCompleted: _gestureComplete, - ), - ), - Visibility( - visible: _isUseBiometric, - child: GestureDetector( - onTap: () { - auth(); - }, - child: ItemBuilder.buildClickItem( - Text( - ResponsiveUtil.isWindows() - ? S.current.biometricVerifyPin - : S.current.biometricVerifyFingerprint, - style: Theme.of(context).textTheme.titleSmall, - ), - ), + child: Center( + child: PopScope( + canPop: !widget.isModal, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 50), + Text( + _notifier.gestureText, + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 30), + Flexible( + child: GestureUnlockView( + key: _gestureUnlockView, + size: min(MediaQuery.sizeOf(context).width, 400), + padding: 60, + roundSpace: 40, + defaultColor: Colors.grey.withOpacity(0.5), + selectedColor: Theme.of(context).primaryColor, + failedColor: Colors.redAccent, + disableColor: Colors.grey, + solidRadiusRatio: 0.3, + lineWidth: 2, + touchRadiusRatio: 0.3, + onCompleted: _gestureComplete, + ), + ), + Visibility( + visible: _isUseBiometric, + child: GestureDetector( + onTap: () { + auth(); + }, + child: ItemBuilder.buildClickItem( + Text( + ResponsiveUtil.isWindows() + ? S.current.biometricVerifyPin + : S.current.biometricVerifyFingerprint, + style: Theme.of(context).textTheme.titleSmall, ), ), - const SizedBox(height: 50), - ], + ), ), - ), + const SizedBox(height: 50), + ], ), - ], + ), ), ), ); diff --git a/lib/Screens/Setting/setting_appearance_screen.dart b/lib/Screens/Setting/setting_appearance_screen.dart index 3b4017a3..30d5b0d5 100644 --- a/lib/Screens/Setting/setting_appearance_screen.dart +++ b/lib/Screens/Setting/setting_appearance_screen.dart @@ -168,7 +168,7 @@ class _AppearanceSettingScreenState extends State _currentFont = t; Navigator.pop(sheetContext); setState(() {}); - FontEnum.loadFont(context, t, autoRestartApp: true); + FontEnum.loadFont(context, t, autoRestartApp: false); }, selected: _currentFont, context: context, diff --git a/lib/Screens/Setting/setting_general_screen.dart b/lib/Screens/Setting/setting_general_screen.dart index e6bfe90c..ce28ddca 100644 --- a/lib/Screens/Setting/setting_general_screen.dart +++ b/lib/Screens/Setting/setting_general_screen.dart @@ -231,6 +231,7 @@ class GeneralSettingScreenState extends State } else { await LaunchAtStartup.instance.disable(); } + Utils.initTray(); }, ), ItemBuilder.buildRadioItem( diff --git a/lib/Screens/Setting/setting_safe_screen.dart b/lib/Screens/Setting/setting_safe_screen.dart index 469accde..df8ed627 100644 --- a/lib/Screens/Setting/setting_safe_screen.dart +++ b/lib/Screens/Setting/setting_safe_screen.dart @@ -175,49 +175,54 @@ class _SafeSettingScreenState extends State context: context, value: _enableSafeMode, topRadius: true, + bottomRadius: !DatabaseManager.isDatabaseEncrypted, title: S.current.safeMode, disabled: ResponsiveUtil.isDesktop(), description: S.current.safeModeTip, onTap: onSafeModeTapped, ), - ItemBuilder.buildEntryItem( - context: context, - bottomRadius: - _encryptDatabaseStatus == EncryptDatabaseStatus.defaultPassword, - title: S.current.editEncryptDatabasePassword, - description: S.current.encryptDatabaseTip, - tip: _encryptDatabaseStatus == EncryptDatabaseStatus.defaultPassword - ? S.current.defaultEncryptDatabasePassword - : S.current.customEncryptDatabasePassword, - onTap: () { - BottomSheetBuilder.showBottomSheet( - context, - responsive: true, - useWideLandscape: true, - (context) => InputPasswordBottomSheet( - title: S.current.editEncryptDatabasePassword, - message: S.current.editEncryptDatabasePasswordTip, - onConfirm: (passord, confirmPassword) async {}, - onValidConfirm: (passord, confirmPassword) async { - bool res = await DatabaseManager.changePassword(passord); - if (res) { - IToast.showTop(S.current.editSuccess); - HiveUtil.setEncryptDatabaseStatus( - EncryptDatabaseStatus.customPassword); - setState(() { - _encryptDatabaseStatus = - EncryptDatabaseStatus.customPassword; - }); - } else { - IToast.showTop(S.current.editFailed); - } - }, - ), - ); - }, + Visibility( + visible: DatabaseManager.isDatabaseEncrypted, + child: ItemBuilder.buildEntryItem( + context: context, + bottomRadius: + _encryptDatabaseStatus == EncryptDatabaseStatus.defaultPassword, + title: S.current.editEncryptDatabasePassword, + description: S.current.encryptDatabaseTip, + tip: _encryptDatabaseStatus == EncryptDatabaseStatus.defaultPassword + ? S.current.defaultEncryptDatabasePassword + : S.current.customEncryptDatabasePassword, + onTap: () { + BottomSheetBuilder.showBottomSheet( + context, + responsive: true, + useWideLandscape: true, + (context) => InputPasswordBottomSheet( + title: S.current.editEncryptDatabasePassword, + message: S.current.editEncryptDatabasePasswordTip, + onConfirm: (passord, confirmPassword) async {}, + onValidConfirm: (passord, confirmPassword) async { + bool res = await DatabaseManager.changePassword(passord); + if (res) { + IToast.showTop(S.current.editSuccess); + HiveUtil.setEncryptDatabaseStatus( + EncryptDatabaseStatus.customPassword); + setState(() { + _encryptDatabaseStatus = + EncryptDatabaseStatus.customPassword; + }); + } else { + IToast.showTop(S.current.editFailed); + } + }, + ), + ); + }, + ), ), Visibility( - visible: _encryptDatabaseStatus == EncryptDatabaseStatus.customPassword, + visible: DatabaseManager.isDatabaseEncrypted && + _encryptDatabaseStatus == EncryptDatabaseStatus.customPassword, child: ItemBuilder.buildEntryItem( context: context, title: S.current.clearEncryptDatabasePassword, diff --git a/lib/Screens/main_screen.dart b/lib/Screens/main_screen.dart index bd88f371..eca4d4da 100644 --- a/lib/Screens/main_screen.dart +++ b/lib/Screens/main_screen.dart @@ -32,7 +32,6 @@ import 'package:tray_manager/tray_manager.dart'; import 'package:window_manager/window_manager.dart'; import '../Resources/colors.dart'; -import '../Resources/fonts.dart'; import '../TokenUtils/import_token_util.dart'; import '../Utils/app_provider.dart'; import '../Utils/enums.dart'; @@ -172,7 +171,6 @@ class MainScreenState extends State Utils.initTray(); } WidgetsBinding.instance.addObserver(this); - FontEnum.downloadFont(showToast: false); HiveUtil.showCloudEntry().then((value) { appProvider.canShowCloudBackupButton = value; }); @@ -761,118 +759,68 @@ class MainScreenState extends State _titleBar() { return (ResponsiveUtil.isDesktop()) - ? Container( - color: Theme.of(context).scaffoldBackgroundColor, - child: WindowTitleBar( - useMoveHandle: ResponsiveUtil.isDesktop(), - titleBarHeightDelta: 34, - margin: const EdgeInsets.symmetric(vertical: 12), - child: Row( - children: [ - const SizedBox(width: 2.5), - _buildLogo(), - const SizedBox(width: 8), - Container( - constraints: - const BoxConstraints(maxWidth: 300, minWidth: 200), - child: ItemBuilder.buildDesktopSearchBar( - context: context, - borderRadius: 8, - bottomMargin: 18, - hintFontSizeDelta: 1, - focusNode: searchFocusNode, - controller: searchController, - background: Colors.grey.withAlpha(40), - hintText: S.current.searchToken, - onSubmitted: (text) { - homeScreenState?.performSearch(text); - }, - ), - ), - const Spacer(), - Row( - children: [ - Selector( - selector: (context, appProvider) => - appProvider.showBackupLogButton, - builder: (context, showBackupLogButton, child) => - showBackupLogButton - ? WindowButton( - colors: - MyColors.getNormalButtonColors(context), - borderRadius: BorderRadius.circular(8), - padding: EdgeInsets.zero, - iconBuilder: (buttonContext) => - Selector( - selector: (context, appProvider) => - appProvider.autoBackupLoadingStatus, - builder: (context, - autoBackupLoadingStatus, child) => - LoadingIcon( - status: autoBackupLoadingStatus, - normalIcon: const Icon( - Icons.history_rounded, - size: 25), - ), - ), - onPressed: () { - context.contextMenuOverlay - .show(const BackupLogScreen()); - }, - ) - : const SizedBox.shrink(), - ), - const SizedBox(width: 3), - StayOnTopWindowButton( - context: context, - rotateAngle: _isStayOnTop ? 0 : -pi / 4, - colors: _isStayOnTop - ? MyColors.getStayOnTopButtonColors(context) - : MyColors.getNormalButtonColors(context), - borderRadius: BorderRadius.circular(8), - onPressed: () { - setState(() { - _isStayOnTop = !_isStayOnTop; - windowManager.setAlwaysOnTop(_isStayOnTop); - }); - }, - ), - const SizedBox(width: 3), - MinimizeWindowButton( - colors: MyColors.getNormalButtonColors(context), - borderRadius: BorderRadius.circular(8), - ), - const SizedBox(width: 3), - _isMaximized - ? RestoreWindowButton( - colors: MyColors.getNormalButtonColors(context), - borderRadius: BorderRadius.circular(8), - onPressed: ResponsiveUtil.maximizeOrRestore, - ) - : MaximizeWindowButton( - colors: MyColors.getNormalButtonColors(context), - borderRadius: BorderRadius.circular(8), - onPressed: ResponsiveUtil.maximizeOrRestore, + ? ItemBuilder.buildWindowTitle( + context, + isStayOnTop: _isStayOnTop, + isMaximized: _isMaximized, + onStayOnTopTap: () { + setState(() { + _isStayOnTop = !_isStayOnTop; + windowManager.setAlwaysOnTop(_isStayOnTop); + }); + }, + leftWidgets: [ + const SizedBox(width: 2.5), + _buildLogo(), + const SizedBox(width: 8), + Container( + constraints: const BoxConstraints(maxWidth: 300, minWidth: 200), + child: ItemBuilder.buildDesktopSearchBar( + context: context, + borderRadius: 8, + bottomMargin: 18, + hintFontSizeDelta: 1, + focusNode: searchFocusNode, + controller: searchController, + background: Colors.grey.withAlpha(40), + hintText: S.current.searchToken, + onSubmitted: (text) { + homeScreenState?.performSearch(text); + }, + ), + ), + ], + rightButtons: [ + Selector( + selector: (context, appProvider) => + appProvider.showBackupLogButton, + builder: (context, showBackupLogButton, child) => + showBackupLogButton + ? WindowButton( + colors: MyColors.getNormalButtonColors(context), + borderRadius: BorderRadius.circular(8), + padding: EdgeInsets.zero, + iconBuilder: (buttonContext) => + Selector( + selector: (context, appProvider) => + appProvider.autoBackupLoadingStatus, + builder: + (context, autoBackupLoadingStatus, child) => + LoadingIcon( + status: autoBackupLoadingStatus, + normalIcon: + const Icon(Icons.history_rounded, size: 25), + ), ), - const SizedBox(width: 3), - CloseWindowButton( - colors: MyColors.getCloseButtonColors(context), - borderRadius: BorderRadius.circular(8), - onPressed: () { - if (HiveUtil.getBool(HiveUtil.showTrayKey) && - HiveUtil.getBool(HiveUtil.enableCloseToTrayKey)) { - windowManager.hide(); - } else { - windowManager.close(); - } - }, - ), - ], - ), - const SizedBox(width: 8), - ], + onPressed: () { + context.contextMenuOverlay + .show(const BackupLogScreen()); + }, + ) + : const SizedBox.shrink(), ), - ), + const SizedBox(width: 3), + ], ) : emptyWidget; } diff --git a/lib/Utils/app_provider.dart b/lib/Utils/app_provider.dart index fec3e56a..4b955348 100644 --- a/lib/Utils/app_provider.dart +++ b/lib/Utils/app_provider.dart @@ -254,6 +254,11 @@ class AppProvider with ChangeNotifier { ThemeColorData get lightTheme => _lightTheme; + set lightTheme(ThemeColorData value) { + _lightTheme = value; + notifyListeners(); + } + setLightTheme(int index) { HiveUtil.setLightTheme(index); _lightTheme = HiveUtil.getLightTheme(); @@ -264,6 +269,11 @@ class AppProvider with ChangeNotifier { ThemeColorData get darkTheme => _darkTheme; + set darkTheme(ThemeColorData value) { + _darkTheme = value; + notifyListeners(); + } + setDarkTheme(int index) { HiveUtil.setDarkTheme(index); _darkTheme = HiveUtil.getDarkTheme(); diff --git a/lib/Utils/file_util.dart b/lib/Utils/file_util.dart index 0ec2aa87..61ce1aa4 100644 --- a/lib/Utils/file_util.dart +++ b/lib/Utils/file_util.dart @@ -1,5 +1,4 @@ import 'dart:io'; -import 'dart:typed_data'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:cloudotp/Models/github_response.dart'; @@ -10,6 +9,7 @@ import 'package:device_info_plus/device_info_plus.dart'; import 'package:dio/dio.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:path/path.dart'; @@ -180,7 +180,10 @@ class FileUtil { static Future getApplicationDir() async { final dir = await getApplicationDocumentsDirectory(); - final appName = (await PackageInfo.fromPlatform()).appName; + var appName = (await PackageInfo.fromPlatform()).appName; + if (kDebugMode) { + appName += "-Debug"; + } String path = join(dir.path, appName); Directory directory = Directory(path); if (!await directory.exists()) { @@ -189,6 +192,14 @@ class FileUtil { return path; } + static Future getFontDir() async { + Directory directory = Directory(join(await getApplicationDir(), "Fonts")); + if (!await directory.exists()) { + await directory.create(recursive: true); + } + return directory.path; + } + static Future getBackupDir() async { Directory directory = Directory(join(await getApplicationDir(), "Backup")); if (!await directory.exists()) { @@ -207,7 +218,7 @@ class FileUtil { } static Future getLogDir() async { - Directory directory = Directory(join(await getApplicationDir(), "Log")); + Directory directory = Directory(join(await getApplicationDir(), "Logs")); if (!await directory.exists()) { await directory.create(recursive: true); } diff --git a/lib/Utils/font_util.dart b/lib/Utils/font_util.dart index 2091b00f..eedbdb37 100644 --- a/lib/Utils/font_util.dart +++ b/lib/Utils/font_util.dart @@ -2,12 +2,11 @@ import 'dart:async'; import 'dart:io'; import 'dart:ui'; -import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:http/http.dart' as http; -import 'package:path_provider/path_provider.dart'; import './ilogger.dart'; +import 'file_util.dart'; enum _FontSource { asset, file, url } @@ -87,7 +86,7 @@ Future downloadFont( }) async { final uri = Uri.parse(url); final filename = uri.pathSegments.last; - final dir = (await getApplicationSupportDirectory()).path; + final dir = await FileUtil.getFontDir(); final file = File('$dir/$filename'); if (await file.exists() && !overwrite) { @@ -132,7 +131,8 @@ Future downloadBytes( final percent = (bytes.length / response.contentLength!); onReceiveProgress?.call(percent); if (percent - prevPercent > 15 || percent > 99) { - ILogger.info('Downloading font: ${(percent * 100).toStringAsFixed(1)}%'); + ILogger.info( + 'Downloading font: ${(percent * 100).toStringAsFixed(1)}%'); prevPercent = percent; } } diff --git a/lib/Utils/ilogger.dart b/lib/Utils/ilogger.dart index c146f1a5..5c59db7b 100644 --- a/lib/Utils/ilogger.dart +++ b/lib/Utils/ilogger.dart @@ -20,20 +20,14 @@ class ILogger { output: ConsoleOutput(), ), Logger( + filter: ProductionFilter(), printer: PrettyPrinter( methodCount: 0, errorMethodCount: null, lineLength: 300, colors: false, printEmojis: false, - excludeBox: { - Level.trace: true, - Level.debug: true, - Level.info: true, - Level.warning: true, - Level.error: true, - Level.fatal: true, - }, + noBoxingByDefault: true, dateTimeFormat: (time) => DateFormat('yyyy-MM-dd HH:mm:ss:SSS').format(time), ), diff --git a/lib/Widgets/BottomSheet/input_bottom_sheet.dart b/lib/Widgets/BottomSheet/input_bottom_sheet.dart index 49fc4bb4..489489ad 100644 --- a/lib/Widgets/BottomSheet/input_bottom_sheet.dart +++ b/lib/Widgets/BottomSheet/input_bottom_sheet.dart @@ -113,7 +113,7 @@ class InputBottomSheetState extends State { runAlignment: WrapAlignment.center, children: [ Container( - padding: const EdgeInsets.symmetric(horizontal: 20), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20), decoration: BoxDecoration( borderRadius: BorderRadius.vertical( top: const Radius.circular(20), diff --git a/lib/Widgets/BottomSheet/input_password_bottom_sheet.dart b/lib/Widgets/BottomSheet/input_password_bottom_sheet.dart index f6744052..ec3a02b3 100644 --- a/lib/Widgets/BottomSheet/input_password_bottom_sheet.dart +++ b/lib/Widgets/BottomSheet/input_password_bottom_sheet.dart @@ -50,7 +50,7 @@ class InputPasswordBottomSheetState extends State { runAlignment: WrapAlignment.center, children: [ Container( - padding: const EdgeInsets.symmetric(horizontal: 20), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20), decoration: BoxDecoration( borderRadius: BorderRadius.vertical( top: const Radius.circular(20), diff --git a/lib/Widgets/Item/item_builder.dart b/lib/Widgets/Item/item_builder.dart index ec0865f2..43c1809b 100644 --- a/lib/Widgets/Item/item_builder.dart +++ b/lib/Widgets/Item/item_builder.dart @@ -12,12 +12,14 @@ import 'package:flutter/services.dart'; import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; import 'package:group_button/group_button.dart'; import 'package:provider/provider.dart'; +import 'package:window_manager/window_manager.dart'; import '../../Resources/colors.dart'; import '../../Utils/app_provider.dart'; import '../../Utils/asset_util.dart'; import '../../Utils/constant.dart'; import '../../Utils/enums.dart'; +import '../../Utils/hive_util.dart'; import '../../Utils/itoast.dart'; import '../../Utils/responsive_util.dart'; import '../../Utils/uri_util.dart'; @@ -29,6 +31,8 @@ import '../Selectable/my_selection_area.dart'; import '../Selectable/my_selection_toolbar.dart'; import '../Selectable/selection_transformer.dart'; import '../TextDrawable/text_drawable_widget.dart'; +import '../Window/window_button.dart'; +import '../Window/window_caption.dart'; enum TailingType { none, clear, password, icon, text, widget } @@ -62,11 +66,12 @@ class ItemBuilder { }) { bool showLeading = !ResponsiveUtil.isLandscape(); return PreferredSize( - preferredSize: const Size(0, kToolbarHeight), + preferredSize: const Size(0, 56), child: Selector( selector: (context, provider) => provider.enableFrostedGlassEffect, builder: (context, enableFrostedGlassEffect, child) => MyAppBar( key: key, + primary: !ResponsiveUtil.isWideLandscape(), backgroundColor: transparent ? Theme.of(context) .scaffoldBackgroundColor @@ -132,6 +137,7 @@ class ItemBuilder { selector: (context, provider) => provider.enableFrostedGlassEffect, builder: (context, enableFrostedGlassEffect, child) => MyAppBar( key: key, + primary: !ResponsiveUtil.isWideLandscape(), backgroundColor: transparent ? Colors.transparent : backgroundColor @@ -2215,6 +2221,109 @@ class ItemBuilder { child: child, ); } + + static buildWindowTitle( + BuildContext context, { + Color? backgroundColor, + List leftWidgets = const [], + List rightButtons = const [], + required bool isStayOnTop, + required bool isMaximized, + required Function() onStayOnTopTap, + bool showAppName = false, + bool forceClose = false, + }) { + return Container( + color: backgroundColor ?? Theme.of(context).scaffoldBackgroundColor, + child: WindowTitleBar( + useMoveHandle: ResponsiveUtil.isDesktop(), + titleBarHeightDelta: 26, + margin: const EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + ...leftWidgets, + if (showAppName) ...[ + const SizedBox(width: 4), + // IgnorePointer( + // child: ClipRRect( + // borderRadius: BorderRadius.circular(10), + // clipBehavior: Clip.antiAlias, + // child: Container( + // width: 24, + // height: 24, + // decoration: const BoxDecoration( + // image: DecorationImage( + // image: AssetImage('assets/logo-transparent.png'), + // fit: BoxFit.contain, + // ), + // ), + // ), + // ), + // ), + const SizedBox(width: 8), + Text( + S.current.appName, + style: Theme.of(context).textTheme.titleSmall?.apply( + fontSizeDelta: 4, + fontWeightDelta: 2, + ), + ), + ], + const Spacer(), + Row( + children: [ + ...rightButtons, + StayOnTopWindowButton( + context: context, + rotateAngle: isStayOnTop ? 0 : -pi / 4, + colors: isStayOnTop + ? MyColors.getStayOnTopButtonColors(context) + : MyColors.getNormalButtonColors(context), + borderRadius: BorderRadius.circular(8), + onPressed: onStayOnTopTap, + ), + const SizedBox(width: 3), + MinimizeWindowButton( + colors: MyColors.getNormalButtonColors(context), + borderRadius: BorderRadius.circular(8), + ), + const SizedBox(width: 3), + isMaximized + ? RestoreWindowButton( + colors: MyColors.getNormalButtonColors(context), + borderRadius: BorderRadius.circular(8), + onPressed: ResponsiveUtil.maximizeOrRestore, + ) + : MaximizeWindowButton( + colors: MyColors.getNormalButtonColors(context), + borderRadius: BorderRadius.circular(8), + onPressed: ResponsiveUtil.maximizeOrRestore, + ), + const SizedBox(width: 3), + CloseWindowButton( + colors: MyColors.getCloseButtonColors(context), + borderRadius: BorderRadius.circular(8), + onPressed: () { + if (forceClose) { + windowManager.close(); + } else { + if (HiveUtil.getBool(HiveUtil.showTrayKey) && + HiveUtil.getBool(HiveUtil.enableCloseToTrayKey)) { + windowManager.hide(); + } else { + windowManager.close(); + } + } + }, + ), + ], + ), + const SizedBox(width: 8), + ], + ), + ), + ); + } } class CustomImageFactory extends WidgetFactory { diff --git a/lib/Widgets/Scaffold/my_appbar.dart b/lib/Widgets/Scaffold/my_appbar.dart index ec8a9e28..9dbeae3b 100644 --- a/lib/Widgets/Scaffold/my_appbar.dart +++ b/lib/Widgets/Scaffold/my_appbar.dart @@ -1085,13 +1085,12 @@ class _MyAppBarState extends State { bottom: false, child: appBar, ); + appBar = Align( + alignment: Alignment.topCenter, + child: appBar, + ); } - appBar = Align( - alignment: Alignment.topCenter, - child: appBar, - ); - if (widget.flexibleSpace != null) { appBar = Stack( fit: StackFit.passthrough, diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 6dfbbf49..588b4ad9 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -338,6 +338,7 @@ "decryptingDatabasePassword": "Decrypting database...", "defaultEncryptDatabasePassword": "The current default database password", "customEncryptDatabasePassword": "The current custom database password", + "currentDatabaseUnencrypted": "Current database is not encrypted", "editEncryptDatabasePassword": "Change database password", "editEncryptDatabasePasswordTip": "Modify the database password to unlock the encrypted database; please remember the password, otherwise you will not be able to view the token", "inputEncryptDatabasePassword": "Enter database password", diff --git a/lib/l10n/intl_zh_CN.arb b/lib/l10n/intl_zh_CN.arb index 056b2a18..b0ec0113 100644 --- a/lib/l10n/intl_zh_CN.arb +++ b/lib/l10n/intl_zh_CN.arb @@ -335,6 +335,7 @@ "decryptingDatabasePassword": "解密数据库中...", "defaultEncryptDatabasePassword": "当前为默认数据库密码", "customEncryptDatabasePassword": "当前为自定义数据库密码", + "currentDatabaseUnencrypted": "当前数据库未加密", "editEncryptDatabasePassword": "修改数据库密码", "editEncryptDatabasePasswordTip": "请牢记数据库密码,打开应用后需要输入该密码用于解锁加密数据库,如果忘记将无法解密数据库", "inputEncryptDatabasePassword": "输入数据库密码", diff --git a/lib/main.dart b/lib/main.dart index f0a8b2e9..5e45861c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -26,6 +26,7 @@ import 'package:provider/provider.dart'; import 'package:window_manager/window_manager.dart'; import './Utils/ilogger.dart'; +import 'Resources/fonts.dart'; import 'Screens/main_screen.dart'; import 'TokenUtils/token_image_util.dart'; import 'Utils/constant.dart'; @@ -122,6 +123,7 @@ Future initApp(WidgetsBinding widgetsBinding) async { } await HotKeyManager.instance.unregisterAll(); } + FontEnum.downloadFont(showToast: false); } Future initWindow() async { @@ -231,8 +233,11 @@ class MyApp extends StatelessWidget { data: MediaQuery.of(context) .copyWith(textScaler: TextScaler.noScaling), child: Listener( - onPointerDown: (_) => - FocusManager.instance.primaryFocus?.unfocus(), + onPointerDown: (_) { + if (!ResponsiveUtil.isDesktop()) { + FocusManager.instance.primaryFocus?.unfocus(); + } + }, child: widget, ), ), diff --git a/tools/dll/sqlite3-unencrypt.dll b/tools/dll/sqlite3.dll similarity index 100% rename from tools/dll/sqlite3-unencrypt.dll rename to tools/dll/sqlite3.dll diff --git a/tools/dll/sqlite_sqlcipher.dll b/tools/dll/sqlite_sqlcipher.dll new file mode 100644 index 00000000..a8c93798 Binary files /dev/null and b/tools/dll/sqlite_sqlcipher.dll differ diff --git a/tools/generate.py b/tools/generate.py index 205d83b6..2190ad8a 100644 --- a/tools/generate.py +++ b/tools/generate.py @@ -9,7 +9,7 @@ "D:\\Repositories\\CloudOTP\\build\\windows\\x64\\runner\\Release" ) downloads_path = "D:\\Ruida\\Downloads" -dll_path = "D:\\Repositories\\CloudOTP\\tools\\dll\\sqlite3.dll" +dll_path = "D:\\Repositories\\CloudOTP\\tools\\dll" iss_path = "D:\\Repositories\\CloudOTP\\tools\\CloudOTP.iss" iscc_path = "D:\\Program Files\\Inno Setup 6\\ISCC.exe" @@ -44,9 +44,14 @@ def rename_apk(version): # zip the windows runner def zip_windows(version): print("start zip windows runner...") - print("copy sqlite3.dll to windows runner...") - shutil.copy(dll_path, windows_release_path) - print("copy sqlite3.dll done.") + print("copy dll to windows runner...") + for root, dirs, files in os.walk(dll_path): + for file in files: + shutil.copy( + os.path.join(root, file), + os.path.join(windows_release_path, file), + ) + print("copy dll done.") print("zip windows runner...") zip_path = os.path.join(get_downloads_path(version), "CloudOTP-" + version + ".zip") with zipfile.ZipFile( diff --git a/tools/dll/sqlcipher.dll b/tools/sqlcipher.dll similarity index 100% rename from tools/dll/sqlcipher.dll rename to tools/sqlcipher.dll