diff --git a/imgs/detail.png b/imgs/detail.png index 8aa706ed..fa313a9e 100644 Binary files a/imgs/detail.png and b/imgs/detail.png differ diff --git a/imgs/docker.png b/imgs/docker.png index e499cdf8..760bf48c 100644 Binary files a/imgs/docker.png and b/imgs/docker.png differ diff --git a/imgs/editor.png b/imgs/editor.png index eeef4158..a96ea7c1 100644 Binary files a/imgs/editor.png and b/imgs/editor.png differ diff --git a/imgs/server.png b/imgs/server.png index 8fa46c9b..785adb07 100644 Binary files a/imgs/server.png and b/imgs/server.png differ diff --git a/imgs/sftp.png b/imgs/sftp.png index 018ba13f..e38dd9b4 100644 Binary files a/imgs/sftp.png and b/imgs/sftp.png differ diff --git a/imgs/ssh.png b/imgs/ssh.png index b6b7ab3a..3017c717 100644 Binary files a/imgs/ssh.png and b/imgs/ssh.png differ diff --git a/lib/core/extension/listx.dart b/lib/core/extension/listx.dart index a828185e..b5b0396e 100644 --- a/lib/core/extension/listx.dart +++ b/lib/core/extension/listx.dart @@ -6,4 +6,12 @@ extension ListX on List { } return list; } + + List combine(List other, [bool self = true]) { + final list = self ? this : List.from(this); + for (var i = 0; i < length; i++) { + list[i] = other[i]; + } + return list; + } } diff --git a/lib/core/persistant_store.dart b/lib/core/persistant_store.dart index 18555cba..8e491e05 100644 --- a/lib/core/persistant_store.dart +++ b/lib/core/persistant_store.dart @@ -1,9 +1,8 @@ import 'dart:async'; -import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:hive_flutter/hive_flutter.dart'; -import 'package:toolbox/data/res/path.dart'; +import 'package:toolbox/core/utils/misc.dart'; // abstract final class SecureStore { // static const _secureStorage = FlutterSecureStorage(); @@ -30,8 +29,8 @@ import 'package:toolbox/data/res/path.dart'; // } // } -class PersistentStore { - late final Box box; +class PersistentStore { + late final Box box; final String boxName; @@ -41,35 +40,48 @@ class PersistentStore { boxName, //encryptionCipher: SecureStore._cipher, ); +} - /// Get all db filenames. - /// - /// - [suffixs] defaults to ['.hive'] - /// - /// - If [hideSetting] is true, hide 'setting.hive' - static Future> getFileNames({ - bool hideSetting = false, - List? suffixs, - }) async { - final docPath = await Paths.doc; - final dir = Directory(docPath); - final files = await dir.list().toList(); - if (suffixs != null) { - files.removeWhere((e) => !suffixs.contains(e.path.split('.').last)); - } else { - // filter out non-hive(db) files - files.removeWhere((e) => !e.path.endsWith('.hive')); +extension BoxX on Box { + static const _internalPreffix = '_sbi_'; + + /// Last modified timestamp + static const String lastModifiedKey = '${_internalPreffix}lastModified'; + int? get lastModified { + final val = get(lastModifiedKey); + if (val == null || val is! int) { + final time = timeStamp; + put(lastModifiedKey, time); + return time; } - if (hideSetting) { - files.removeWhere((e) => e.path.endsWith('setting.hive')); - } - final paths = - files.map((e) => e.path.replaceFirst('$docPath/', '')).toList(); - return paths; + return val; } + Future updateLastModified() => put(lastModifiedKey, timeStamp); + /// Convert db to json - Map toJson() => {for (var e in box.keys) e: box.get(e)}; + Map toJson({bool includeInternal = true}) { + final json = {}; + for (final key in keys) { + if (key is String && + key.startsWith(_internalPreffix) && + !includeInternal) { + continue; + } + json[key] = get(key); + } + return json; + } +} + +extension StoreX on PersistentStore { + _StoreProperty property(String key, T defaultValue) { + return _StoreProperty(box, key, defaultValue); + } + + _StoreListProperty listProperty(String key, List defaultValue) { + return _StoreListProperty(box, key, defaultValue); + } } abstract class StorePropertyBase { @@ -79,8 +91,8 @@ abstract class StorePropertyBase { Future delete(); } -class StoreProperty implements StorePropertyBase { - StoreProperty(this._box, this._key, this.defaultValue); +class _StoreProperty implements StorePropertyBase { + _StoreProperty(this._box, this._key, this.defaultValue); final Box _box; final String _key; @@ -102,6 +114,7 @@ class StoreProperty implements StorePropertyBase { @override Future put(T value) { + _box.updateLastModified(); return _box.put(_key, value); } @@ -111,8 +124,8 @@ class StoreProperty implements StorePropertyBase { } } -class StoreListProperty implements StorePropertyBase> { - StoreListProperty(this._box, this._key, this.defaultValue); +class _StoreListProperty implements StorePropertyBase> { + _StoreListProperty(this._box, this._key, this.defaultValue); final Box _box; final String _key; diff --git a/lib/core/utils/icloud.dart b/lib/core/utils/icloud.dart index deab1a69..10fa394f 100644 --- a/lib/core/utils/icloud.dart +++ b/lib/core/utils/icloud.dart @@ -2,29 +2,13 @@ import 'dart:async'; import 'dart:io'; import 'package:icloud_storage/icloud_storage.dart'; +import 'package:toolbox/data/model/app/sync.dart'; import 'package:toolbox/data/res/logger.dart'; import '../../data/model/app/error.dart'; import '../../data/model/app/json.dart'; import '../../data/res/path.dart'; -class SyncResult { - final List up; - final List down; - final Map err; - - const SyncResult({ - required this.up, - required this.down, - required this.err, - }); - - @override - String toString() { - return 'SyncResult{up: $up, down: $down, err: $err}'; - } -} - abstract final class ICloud { static const _containerId = 'iCloud.tech.lolli.serverbox'; @@ -111,7 +95,7 @@ abstract final class ICloud { /// Return `null` if upload success, `ICloudErr` otherwise /// /// TODO: consider merge strategy, use [SyncAble] and [JsonSerializable] - static Future> sync({ + static Future> syncFiles({ required Iterable relativePaths, String? bakPrefix, }) async { @@ -196,4 +180,5 @@ abstract final class ICloud { Loggers.app.info('iCloud sync, up: $uploadFiles, down: $downloadFiles'); } } + } diff --git a/lib/core/utils/misc.dart b/lib/core/utils/misc.dart index d332ce2a..ac7c74fd 100644 --- a/lib/core/utils/misc.dart +++ b/lib/core/utils/misc.dart @@ -37,3 +37,5 @@ String pathJoin(String path1, String path2) { /// Check if a url is a file url (ends with a file extension) bool isFileUrl(String url) => url.split('/').last.contains('.'); + +int get timeStamp => DateTime.now().millisecondsSinceEpoch; diff --git a/lib/data/model/app/backup.dart b/lib/data/model/app/backup.dart index 34d19172..f9706a17 100644 --- a/lib/data/model/app/backup.dart +++ b/lib/data/model/app/backup.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; +import 'package:toolbox/core/persistant_store.dart'; import 'package:toolbox/data/model/server/private_key_info.dart'; import 'package:toolbox/data/model/server/server_private_info.dart'; import 'package:toolbox/data/model/server/snippet.dart'; @@ -60,8 +61,8 @@ class Backup { spis = Stores.server.fetch(), snippets = Stores.snippet.fetch(), keys = Stores.key.fetch(), - dockerHosts = Stores.docker.toJson(), - settings = Stores.setting.toJson(); + dockerHosts = Stores.docker.box.toJson(), + settings = Stores.setting.box.toJson(); static Future backup() async { final result = _diyEncrypt(json.encode(Backup.loadFromStore())); diff --git a/lib/data/model/app/sync.dart b/lib/data/model/app/sync.dart new file mode 100644 index 00000000..ccb06147 --- /dev/null +++ b/lib/data/model/app/sync.dart @@ -0,0 +1,24 @@ +import 'dart:async'; + +class SyncResult { + final List up; + final List down; + final Map err; + + const SyncResult({ + required this.up, + required this.down, + required this.err, + }); + + @override + String toString() { + return 'SyncResult{up: $up, down: $down, err: $err}'; + } +} + +abstract class SyncIface { + /// Merge [other] into [this], return [this] after merge. + /// Data in [other] has higher priority than [this]. + FutureOr sync(T other); +} diff --git a/lib/data/model/app/syncable.dart b/lib/data/model/app/syncable.dart deleted file mode 100644 index f2829d07..00000000 --- a/lib/data/model/app/syncable.dart +++ /dev/null @@ -1,9 +0,0 @@ -abstract class SyncAble { - /// If [other] is newer than [this] then return true, - /// else return false - bool needSync(T other); - - /// Merge [other] into [this], - /// return [this] after merge - T merge(T other); -} diff --git a/lib/data/store/docker.dart b/lib/data/store/docker.dart index 3fb2ccb4..186800c6 100644 --- a/lib/data/store/docker.dart +++ b/lib/data/store/docker.dart @@ -1,6 +1,6 @@ import '../../core/persistant_store.dart'; -class DockerStore extends PersistentStore { +class DockerStore extends PersistentStore { DockerStore() : super('docker'); String? fetch(String? id) { diff --git a/lib/data/store/private_key.dart b/lib/data/store/private_key.dart index 62f40efb..444e1c7a 100644 --- a/lib/data/store/private_key.dart +++ b/lib/data/store/private_key.dart @@ -1,7 +1,7 @@ import '../../core/persistant_store.dart'; import '../model/server/private_key_info.dart'; -class PrivateKeyStore extends PersistentStore { +class PrivateKeyStore extends PersistentStore { PrivateKeyStore() : super('key'); void put(PrivateKeyInfo info) { diff --git a/lib/data/store/server.dart b/lib/data/store/server.dart index ea9184a9..fdff6eb9 100644 --- a/lib/data/store/server.dart +++ b/lib/data/store/server.dart @@ -1,7 +1,7 @@ import '../../core/persistant_store.dart'; import '../model/server/server_private_info.dart'; -class ServerStore extends PersistentStore { +class ServerStore extends PersistentStore { ServerStore() : super('server'); void put(ServerPrivateInfo info) { diff --git a/lib/data/store/setting.dart b/lib/data/store/setting.dart index fb9c3935..5fb366bc 100644 --- a/lib/data/store/setting.dart +++ b/lib/data/store/setting.dart @@ -15,22 +15,19 @@ class SettingStore extends PersistentStore { // item in the drawer of the home page) /// Discussion #146 - late final serverTabUseOldUI = StoreProperty( - box, + late final serverTabUseOldUI = property( 'serverTabUseOldUI', false, ); /// Time out for server connect and more... - late final timeout = StoreProperty( - box, + late final timeout = property( 'timeOut', 5, ); /// Record history of SFTP path and etc. - late final recordHistory = StoreProperty( - box, + late final recordHistory = property( 'recordHistory', true, ); @@ -38,142 +35,125 @@ class SettingStore extends PersistentStore { /// Bigger for bigger font size /// 1.0 means 100% /// Warning: This may cause some UI issues - late final textFactor = StoreProperty( - box, + late final textFactor = property( 'textFactor', 1.0, ); /// Lanch page idx - late final launchPage = StoreProperty( - box, + late final launchPage = property( 'launchPage', Defaults.launchPageIdx, ); /// Server detail disk ignore path late final diskIgnorePath = - StoreListProperty(box, 'diskIgnorePath', Defaults.diskIgnorePath); + property('diskIgnorePath', Defaults.diskIgnorePath); /// Use double column servers page on Desktop - late final doubleColumnServersPage = StoreProperty( - box, + late final doubleColumnServersPage = property( 'doubleColumnServersPage', isDesktop, ); /// Disk view: amount / IO - late final serverTabPreferDiskAmount = StoreProperty( - box, + late final serverTabPreferDiskAmount = property( 'serverTabPreferDiskAmount', false, ); // ------END------ - late final primaryColor = StoreProperty( - box, + late final primaryColor = property( 'primaryColor', 4287106639, ); - late final serverStatusUpdateInterval = StoreProperty( - box, + late final serverStatusUpdateInterval = property( 'serverStatusUpdateInterval', Defaults.updateInterval, ); // Max retry count when connect to server - late final maxRetryCount = StoreProperty(box, 'maxRetryCount', 2); + late final maxRetryCount = property('maxRetryCount', 2); // Night mode: 0 -> auto, 1 -> light, 2 -> dark, 3 -> AMOLED, 4 -> AUTO-AMOLED - late final themeMode = StoreProperty(box, 'themeMode', 0); + late final themeMode = property('themeMode', 0); // Font file path - late final fontPath = StoreProperty(box, 'fontPath', ''); + late final fontPath = property('fontPath', ''); // Backgroud running (Android) - late final bgRun = StoreProperty(box, 'bgRun', isAndroid); + late final bgRun = property('bgRun', isAndroid); // Server order - late final serverOrder = StoreListProperty(box, 'serverOrder', []); + late final serverOrder = listProperty('serverOrder', []); - late final snippetOrder = StoreListProperty(box, 'snippetOrder', []); + late final snippetOrder = listProperty('snippetOrder', []); // Server details page cards order late final detailCardOrder = - StoreListProperty(box, 'detailCardPrder', Defaults.detailCardOrder); + listProperty('detailCardPrder', Defaults.detailCardOrder); // SSH term font size - late final termFontSize = StoreProperty(box, 'termFontSize', 13.0); + late final termFontSize = property('termFontSize', 13.0); // Locale - late final locale = StoreProperty(box, 'locale', ''); + late final locale = property('locale', ''); // SSH virtual key (ctrl | alt) auto turn off - late final sshVirtualKeyAutoOff = - StoreProperty(box, 'sshVirtualKeyAutoOff', true); + late final sshVirtualKeyAutoOff = property('sshVirtualKeyAutoOff', true); - late final editorFontSize = StoreProperty(box, 'editorFontSize', 13.0); + late final editorFontSize = property('editorFontSize', 13.0); // Editor theme - late final editorTheme = StoreProperty( - box, + late final editorTheme = property( 'editorTheme', Defaults.editorTheme, ); - late final editorDarkTheme = StoreProperty( - box, + late final editorDarkTheme = property( 'editorDarkTheme', Defaults.editorDarkTheme, ); - late final fullScreen = StoreProperty( - box, + late final fullScreen = property( 'fullScreen', false, ); - late final fullScreenJitter = StoreProperty( - box, + late final fullScreenJitter = property( 'fullScreenJitter', true, ); - late final fullScreenRotateQuarter = StoreProperty( - box, + late final fullScreenRotateQuarter = property( 'fullScreenRotateQuarter', 1, ); - late final keyboardType = StoreProperty( - box, + late final keyboardType = property( 'keyboardType', TextInputType.text.index, ); - late final sshVirtKeys = StoreListProperty( - box, + late final sshVirtKeys = listProperty( 'sshVirtKeys', Defaults.sshVirtKeys, ); - late final netViewType = StoreProperty( - box, + late final netViewType = property( 'netViewType', NetViewType.speed, ); // Only valid on iOS - late final autoUpdateHomeWidget = StoreProperty( - box, + late final autoUpdateHomeWidget = property( 'autoUpdateHomeWidget', isIOS, ); - late final autoCheckAppUpdate = StoreProperty( - box, + late final autoCheckAppUpdate = property( 'autoCheckAppUpdate', true, ); @@ -181,59 +161,54 @@ class SettingStore extends PersistentStore { /// Display server tab function buttons on the bottom of each server card if [true] /// /// Otherwise, display them on the top of server detail page - late final moveOutServerTabFuncBtns = StoreProperty( - box, + late final moveOutServerTabFuncBtns = property( 'moveOutServerTabFuncBtns', true, ); /// Whether use `rm -r` to delete directory on SFTP - late final sftpRmrDir = StoreProperty( - box, + late final sftpRmrDir = property( 'sftpRmrDir', false, ); /// Whether use system's primary color as the app's primary color - late final useSystemPrimaryColor = StoreProperty( - box, + late final useSystemPrimaryColor = property( 'useSystemPrimaryColor', false, ); /// Only valid on iOS and macOS - late final icloudSync = StoreProperty( - box, + late final icloudSync = property( 'icloudSync', false, ); /// Only valid on iOS / Android / Windows - late final useBioAuth = StoreProperty( - box, + late final useBioAuth = property( 'useBioAuth', false, ); /// The performance of highlight is bad - late final editorHighlight = StoreProperty(box, 'editorHighlight', true); + late final editorHighlight = property('editorHighlight', true); /// Open SFTP with last viewed path - late final sftpOpenLastPath = StoreProperty(box, 'sftpOpenLastPath', true); + late final sftpOpenLastPath = property('sftpOpenLastPath', true); /// Show tip of suspend - late final showSuspendTip = StoreProperty(box, 'showSuspendTip', true); + late final showSuspendTip = property('showSuspendTip', true); /// Server func btns display name late final serverFuncBtnsDisplayName = - StoreProperty(box, 'serverFuncBtnsDisplayName', false); + property('serverFuncBtnsDisplayName', false); // Never show these settings for users // // ------BEGIN------ /// Version of store db - late final storeVersion = StoreProperty(box, 'storeVersion', 0); + late final storeVersion = property('storeVersion', 0); // ------END------ } diff --git a/lib/data/store/snippet.dart b/lib/data/store/snippet.dart index ffaba724..bf120d2b 100644 --- a/lib/data/store/snippet.dart +++ b/lib/data/store/snippet.dart @@ -1,7 +1,7 @@ import '../../core/persistant_store.dart'; import '../model/server/snippet.dart'; -class SnippetStore extends PersistentStore { +class SnippetStore extends PersistentStore { SnippetStore() : super('snippet'); void put(Snippet snippet) { diff --git a/lib/view/page/backup.dart b/lib/view/page/backup.dart index ec1c9a0b..73cc2a26 100644 --- a/lib/view/page/backup.dart +++ b/lib/view/page/backup.dart @@ -95,8 +95,6 @@ class BackupPage extends StatelessWidget { prop: Stores.setting.icloudSync, func: (val) async { if (val) { - final relativePaths = await PersistentStore.getFileNames(); - await ICloud.sync(relativePaths: relativePaths); } }, ), @@ -109,35 +107,25 @@ class BackupPage extends StatelessWidget { if (icloudLoading.value) { return UIs.centerSizedLoadingSmall; } - return ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 137), - child: Row( - children: [ - TextButton( - onPressed: () async { - icloudLoading.value = true; - final files = await PersistentStore.getFileNames(); - for (final file in files) { - await ICloud.download(relativePath: file); - } - icloudLoading.value = false; - }, - child: Text(l10n.download), - ), - UIs.width7, - TextButton( - onPressed: () async { - icloudLoading.value = true; - final files = await PersistentStore.getFileNames(); - for (final file in files) { - await ICloud.upload(relativePath: file); - } - icloudLoading.value = false; - }, - child: Text(l10n.upload), - ), - ], - ), + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + TextButton( + onPressed: () async { + icloudLoading.value = true; + icloudLoading.value = false; + }, + child: Text(l10n.download), + ), + UIs.width7, + TextButton( + onPressed: () async { + icloudLoading.value = true; + icloudLoading.value = false; + }, + child: Text(l10n.upload), + ), + ], ); }, ), diff --git a/lib/view/page/home.dart b/lib/view/page/home.dart index 4002c9a8..253c4a8f 100644 --- a/lib/view/page/home.dart +++ b/lib/view/page/home.dart @@ -8,6 +8,7 @@ import 'package:toolbox/core/channel/bg_run.dart'; import 'package:toolbox/core/channel/home_widget.dart'; import 'package:toolbox/core/extension/context/dialog.dart'; import 'package:toolbox/core/extension/context/locale.dart'; +import 'package:toolbox/core/persistant_store.dart'; import 'package:toolbox/core/utils/platform/auth.dart'; import 'package:toolbox/core/utils/platform/base.dart'; import 'package:toolbox/data/res/github_id.dart'; @@ -340,7 +341,7 @@ class _HomePageState extends State } Future _onLongPressSetting() async { - final map = Stores.setting.toJson(); + final map = Stores.setting.box.toJson(includeInternal: false); final keys = map.keys; /// Encode [map] to String with indent `\t` diff --git a/lib/view/page/server/tab.dart b/lib/view/page/server/tab.dart index 44ee252d..bf7f2f60 100644 --- a/lib/view/page/server/tab.dart +++ b/lib/view/page/server/tab.dart @@ -219,29 +219,31 @@ class _ServerPageState extends State final cardStatus = getCardNoti(id); final title = _buildServerCardTitle(srv.status, srv.state, srv.spi); - return AnimatedContainer( - duration: const Duration(milliseconds: 377), - curve: Curves.fastEaseInToSlowEaseOut, - height: _calcCardHeight(srv.state, cardStatus.value.flip), - child: ValueBuilder( - listenable: cardStatus, - build: () { - final List children = [title]; - if (srv.state == ServerState.finished) { - if (cardStatus.value.flip) { - children.addAll(_buildFlipedCard(srv)); - } else { - children.addAll(_buildNormalCard(srv.status, srv.spi)); - } + return ValueBuilder( + listenable: cardStatus, + build: () { + late final List children; + if (srv.state == ServerState.finished) { + if (cardStatus.value.flip) { + children = [title, ..._buildFlipedCard(srv)]; + } else { + children = [title, ..._buildNormalCard(srv.status, srv.spi)]; } - return Column( + } else { + children = [title]; + } + return AnimatedContainer( + duration: const Duration(milliseconds: 377), + curve: Curves.fastEaseInToSlowEaseOut, + height: _calcCardHeight(srv.state, cardStatus.value.flip), + child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: children, - ); - }, - ), + ), + ); + }, ); }