diff --git a/lib/core/persistant_store.dart b/lib/core/persistant_store.dart index 0fe967ab..5a9c3e4b 100644 --- a/lib/core/persistant_store.dart +++ b/lib/core/persistant_store.dart @@ -1,14 +1,43 @@ 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'; class PersistentStore { - late Box box; + late final Box box; - Future> init({String boxName = 'defaultBox'}) async { - box = await Hive.openBox(boxName); - return this; + final String boxName; + + PersistentStore(this.boxName); + + Future init() async => box = await Hive.openBox(boxName); + + /// 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')); + } + if (hideSetting) { + files.removeWhere((e) => e.path.endsWith('setting.hive')); + } + final paths = + files.map((e) => e.path.replaceFirst('$docPath/', '')).toList(); + return paths; } } diff --git a/lib/core/utils/icloud.dart b/lib/core/utils/icloud.dart index 785413aa..34537375 100644 --- a/lib/core/utils/icloud.dart +++ b/lib/core/utils/icloud.dart @@ -2,13 +2,29 @@ import 'dart:async'; import 'dart:io'; import 'package:icloud_storage/icloud_storage.dart'; -import 'package:toolbox/core/utils/platform/base.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}'; + } +} + class ICloud { static const _containerId = 'iCloud.tech.lolli.serverbox'; @@ -91,41 +107,44 @@ class ICloud { /// /// - [relativePath] is the path relative to [docDir], /// must not starts with `/` + /// - [bakSuffix] is the suffix of backup file, default to [null]. + /// All files downloaded from cloud will be suffixed with [bakSuffix]. /// /// Return `null` if upload success, `ICloudErr` otherwise /// /// TODO: consider merge strategy, use [SyncAble] and [JsonSerializable] - static Future?> sync({ + static Future> sync({ required Iterable relativePaths, + String? bakPrefix, }) async { final uploadFiles = []; final downloadFiles = []; try { - final errs = []; + final errs = {}; final allFiles = await getAll(); /// remove files not in relativePaths allFiles.removeWhere((e) => !relativePaths.contains(e.relativePath)); - final mission = >[]; + final missions = >[]; /// upload files not in iCloud final missed = relativePaths.where((e) { return !allFiles.any((f) => f.relativePath == e); }); - mission.addAll(missed.map((e) async { + missions.addAll(missed.map((e) async { final err = await upload(relativePath: e); if (err != null) { - errs.add(err); + errs[e] = err; } })); final docPath = await Paths.doc; /// compare files in iCloud and local - mission.addAll(allFiles.map((file) async { + missions.addAll(allFiles.map((file) async { final relativePath = file.relativePath; /// Check date @@ -134,7 +153,7 @@ class ICloud { /// Local file not found, download remote file final err = await download(relativePath: relativePath); if (err != null) { - errs.add(err); + errs[relativePath] = err; } return; } @@ -149,39 +168,34 @@ class ICloud { await delete(relativePath); final err = await upload(relativePath: relativePath); if (err != null) { - errs.add(err); + errs[relativePath] = err; } uploadFiles.add(relativePath); return; } /// Remote is newer than local, so download remote - final err = await download(relativePath: relativePath); + final localPath = '$docPath/${bakPrefix ?? ''}$relativePath'; + final err = await download( + relativePath: relativePath, + localPath: localPath, + ); if (err != null) { - errs.add(err); + errs[relativePath] = err; } downloadFiles.add(relativePath); })); - await Future.wait(mission); + await Future.wait(missions); - return errs.isEmpty ? null : errs; + return SyncResult(up: uploadFiles, down: downloadFiles, err: errs); } catch (e, s) { Loggers.app.warning('iCloud sync: $relativePaths failed', e, s); - return [ICloudErr(type: ICloudErrType.generic, message: '$e')]; + return SyncResult(up: uploadFiles, down: downloadFiles, err: { + 'Generic': ICloudErr(type: ICloudErrType.generic, message: '$e') + }); } finally { Loggers.app.info('iCloud sync, up: $uploadFiles, down: $downloadFiles'); } } - - static Future syncDb() async { - if (!isIOS && !isMacOS) return; - final docPath = await Paths.doc; - final dir = Directory(docPath); - final files = await dir.list().toList(); - // filter out non-hive(db) files - files.removeWhere((e) => !e.path.endsWith('.hive')); - final paths = files.map((e) => e.path.replaceFirst('$docPath/', '')); - await ICloud.sync(relativePaths: paths); - } } diff --git a/lib/data/res/build_data.dart b/lib/data/res/build_data.dart index d664d5f3..8ce97131 100644 --- a/lib/data/res/build_data.dart +++ b/lib/data/res/build_data.dart @@ -2,9 +2,9 @@ class BuildData { static const String name = "ServerBox"; - static const int build = 597; + static const int build = 599; static const String engine = "3.13.7"; - static const String buildAt = "2023-10-15 13:38:49"; - static const int modifications = 9; + static const String buildAt = "2023-10-15 21:24:51"; + static const int modifications = 4; static const int script = 21; } diff --git a/lib/data/res/store.dart b/lib/data/res/store.dart index 5b7622de..552e583e 100644 --- a/lib/data/res/store.dart +++ b/lib/data/res/store.dart @@ -1,3 +1,4 @@ +import 'package:toolbox/core/persistant_store.dart'; import 'package:toolbox/data/store/docker.dart'; import 'package:toolbox/data/store/first.dart'; import 'package:toolbox/data/store/history.dart'; @@ -17,4 +18,14 @@ class Stores { static final key = locator(); static final snippet = locator(); static final first = locator(); + + static final List all = [ + setting, + server, + docker, + history, + key, + snippet, + first, + ]; } diff --git a/lib/data/store/docker.dart b/lib/data/store/docker.dart index 04eca6a5..80cb4c6f 100644 --- a/lib/data/store/docker.dart +++ b/lib/data/store/docker.dart @@ -1,6 +1,8 @@ import '../../core/persistant_store.dart'; class DockerStore extends PersistentStore { + DockerStore() : super('docker'); + String? fetch(String id) { return box.get(id); } diff --git a/lib/data/store/first.dart b/lib/data/store/first.dart index 53349eaa..7a120c68 100644 --- a/lib/data/store/first.dart +++ b/lib/data/store/first.dart @@ -2,6 +2,8 @@ import 'package:toolbox/core/persistant_store.dart'; /// It stores whether is the first time of some. class FirstStore extends PersistentStore { + FirstStore() : super('first'); + /// Add Snippet `Install ServerBoxMonitor` late final iSSBM = StoreProperty(box, 'installMonitorSnippet', true); diff --git a/lib/data/store/history.dart b/lib/data/store/history.dart index 1d1aa14c..96c259ae 100644 --- a/lib/data/store/history.dart +++ b/lib/data/store/history.dart @@ -47,6 +47,8 @@ class _MapHistory { } class HistoryStore extends PersistentStore { + HistoryStore() : super('history'); + /// Paths that user has visited by 'Locate' button late final sftpGoPath = _ListHistory(box: box, name: 'sftpPath'); diff --git a/lib/data/store/private_key.dart b/lib/data/store/private_key.dart index 8bfe3f15..62f40efb 100644 --- a/lib/data/store/private_key.dart +++ b/lib/data/store/private_key.dart @@ -2,6 +2,8 @@ import '../../core/persistant_store.dart'; import '../model/server/private_key_info.dart'; class PrivateKeyStore extends PersistentStore { + PrivateKeyStore() : super('key'); + void put(PrivateKeyInfo info) { box.put(info.id, info); } diff --git a/lib/data/store/server.dart b/lib/data/store/server.dart index e9b0e34b..ea9184a9 100644 --- a/lib/data/store/server.dart +++ b/lib/data/store/server.dart @@ -2,6 +2,8 @@ import '../../core/persistant_store.dart'; import '../model/server/server_private_info.dart'; class ServerStore extends PersistentStore { + ServerStore() : super('server'); + void put(ServerPrivateInfo info) { box.put(info.id, info); } diff --git a/lib/data/store/setting.dart b/lib/data/store/setting.dart index 69148824..a146506a 100644 --- a/lib/data/store/setting.dart +++ b/lib/data/store/setting.dart @@ -6,6 +6,8 @@ import '../model/app/net_view.dart'; import '../res/default.dart'; class SettingStore extends PersistentStore { + SettingStore() : super('setting'); + /// Convert all settings into json Map toJson() => {for (var e in box.keys) e: box.get(e)}; diff --git a/lib/data/store/snippet.dart b/lib/data/store/snippet.dart index 62584148..ffaba724 100644 --- a/lib/data/store/snippet.dart +++ b/lib/data/store/snippet.dart @@ -2,6 +2,8 @@ import '../../core/persistant_store.dart'; import '../model/server/snippet.dart'; class SnippetStore extends PersistentStore { + SnippetStore() : super('snippet'); + void put(Snippet snippet) { box.put(snippet.name, snippet); } diff --git a/lib/locator.dart b/lib/locator.dart index 183cc345..6640816f 100644 --- a/lib/locator.dart +++ b/lib/locator.dart @@ -36,31 +36,31 @@ void _setupLocatorForProviders() { Future _setupLocatorForStores() async { final setting = SettingStore(); - await setting.init(boxName: 'setting'); + await setting.init(); locator.registerSingleton(setting); final server = ServerStore(); - await server.init(boxName: 'server'); + await server.init(); locator.registerSingleton(server); final key = PrivateKeyStore(); - await key.init(boxName: 'key'); + await key.init(); locator.registerSingleton(key); final snippet = SnippetStore(); - await snippet.init(boxName: 'snippet'); + await snippet.init(); locator.registerSingleton(snippet); final docker = DockerStore(); - await docker.init(boxName: 'docker'); + await docker.init(); locator.registerSingleton(docker); final history = HistoryStore(); - await history.init(boxName: 'history'); + await history.init(); locator.registerSingleton(history); final first = FirstStore(); - await first.init(boxName: 'first'); + await first.init(); locator.registerSingleton(first); } diff --git a/lib/main.dart b/lib/main.dart index ba3bfe3d..fff3be2f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -13,7 +13,6 @@ import 'package:toolbox/data/res/store.dart'; import 'app.dart'; import 'core/analysis.dart'; -import 'core/utils/icloud.dart'; import 'core/utils/ui.dart'; import 'data/model/app/net_view.dart'; import 'data/model/server/private_key_info.dart'; @@ -78,9 +77,6 @@ Future initApp() async { primaryColor = Color(Stores.setting.primaryColor.fetch()); loadFontFile(Stores.setting.fontPath.fetch()); - // Don't call it via `await`, it will block the main thread. - if (Stores.setting.icloudSync.fetch()) ICloud.syncDb(); - if (isAndroid) { // Only start service when [bgRun] is true. if (Stores.setting.bgRun.fetch()) { diff --git a/lib/view/page/backup.dart b/lib/view/page/backup.dart index fd70640d..e2c81770 100644 --- a/lib/view/page/backup.dart +++ b/lib/view/page/backup.dart @@ -7,6 +7,8 @@ import 'package:toolbox/core/extension/context/common.dart'; import 'package:toolbox/core/extension/context/dialog.dart'; import 'package:toolbox/core/extension/context/locale.dart'; import 'package:toolbox/core/extension/context/snackbar.dart'; +import 'package:toolbox/core/persistant_store.dart'; +import 'package:toolbox/core/utils/icloud.dart'; import 'package:toolbox/core/utils/platform/base.dart'; import 'package:toolbox/core/utils/rebuild.dart'; import 'package:toolbox/data/model/app/backup.dart'; @@ -14,15 +16,19 @@ import 'package:toolbox/data/res/logger.dart'; import 'package:toolbox/data/res/path.dart'; import 'package:toolbox/data/res/provider.dart'; import 'package:toolbox/data/res/store.dart'; -import 'package:toolbox/view/widget/round_rect_card.dart'; +import 'package:toolbox/view/widget/expand_tile.dart'; +import 'package:toolbox/view/widget/cardx.dart'; +import 'package:toolbox/view/widget/store_switch.dart'; +import 'package:toolbox/view/widget/value_notifier.dart'; import '../../core/utils/misc.dart'; import '../../data/res/ui.dart'; import '../widget/custom_appbar.dart'; -import '../widget/store_switch.dart'; class BackupPage extends StatelessWidget { - const BackupPage({Key? key}) : super(key: key); + BackupPage({Key? key}) : super(key: key); + + final icloudLoading = ValueNotifier(false); @override Widget build(BuildContext context) { @@ -35,66 +41,38 @@ class BackupPage extends StatelessWidget { } Widget _buildBody(BuildContext context) { - final tip = () { - if (isMacOS || isIOS) { - return '${l10n.syncTip}\n${l10n.backupTip}'; - } - return l10n.backupTip; - }(); - return Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, + return ListView( + padding: const EdgeInsets.all(17), children: [ if (isMacOS || isIOS) _buildIcloudSync(context), - UIs.height13, - Padding( - padding: const EdgeInsets.all(37), - child: Text( - tip, - textAlign: TextAlign.center, - ), - ), - UIs.height77, - _buildCard( - l10n.restore, - Icons.download, - () => _onRestore(context), - ), - UIs.height13, - const SizedBox( - width: 37, - child: Divider(), - ), - UIs.height13, - _buildCard( - l10n.backup, - Icons.save, - () async { - await Backup.backup(); - await shareFiles([await Paths.bak]); - }, - ) + _buildManual(context), ], ); } - Widget _buildCard( - String text, - IconData icon, - FutureOr Function() onTap, - ) { - return RoundRectCard( - InkWell( - onTap: onTap, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 7, horizontal: 17), + Widget _buildManual(BuildContext context) { + return CardX( + ListTile( + title: Text(l10n.files), + subtitle: Text( + l10n.backupTip, + style: UIs.textGrey, + ), + trailing: SizedBox( + width: 120, child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(icon, size: 20), - UIs.width7, - Text(text), + TextButton( + onPressed: () => _onRestore(context), + child: Text(l10n.restore), + ), + TextButton( + onPressed: () async { + await Backup.backup(); + await shareFiles([await Paths.bak]); + }, + child: Text(l10n.backup), + ), ], ), ), @@ -103,27 +81,72 @@ class BackupPage extends StatelessWidget { } Widget _buildIcloudSync(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - 'iCloud', - textAlign: TextAlign.center, + return CardX( + ExpandTile( + title: const Text('iCloud'), + initiallyExpanded: true, + subtitle: Text( + l10n.syncTip, + style: UIs.textGrey, ), - UIs.width13, - // Hive db only save data into local file after app exit, - // so this button is useless - // IconButton( - // onPressed: () async { - // showLoadingDialog(context); - // await ICloud.syncDb(); - // context.pop(); - // showRestartSnackbar(context, btn: s.restart, msg: s.icloudSynced); - // }, - // icon: const Icon(Icons.sync)), - // width13, - StoreSwitch(prop: Stores.setting.icloudSync) - ], + children: [ + ListTile( + title: Text(l10n.auto), + subtitle: const Text( + 'Please wait for optimization :)', + style: UIs.textGrey, + ), + trailing: StoreSwitch( + prop: Stores.setting.icloudSync, + func: (val) async { + if (val) { + final relativePaths = await PersistentStore.getFileNames(); + await ICloud.sync(relativePaths: relativePaths); + } + }, + ), + ), + ListTile( + title: Text('Manual'), + trailing: ValueBuilder( + listenable: icloudLoading, + build: () { + if (icloudLoading.value) { + return UIs.centerSizedLoading; + } + return SizedBox( + width: 120, + child: Row( + children: [ + TextButton( + onPressed: () async { + icloudLoading.value = true; + final files = await PersistentStore.getFileNames(); + for (final file in files) { + await ICloud.download(relativePath: file); + } + }, + 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); + } + }, + child: Text(l10n.upload), + ), + ], + ), + ); + }, + ), + ), + ], + ), ); } diff --git a/lib/view/page/docker.dart b/lib/view/page/docker.dart index 8497686a..9410499e 100644 --- a/lib/view/page/docker.dart +++ b/lib/view/page/docker.dart @@ -20,7 +20,7 @@ import '../../data/res/ui.dart'; import '../../data/res/url.dart'; import '../widget/custom_appbar.dart'; import '../widget/popup_menu.dart'; -import '../widget/round_rect_card.dart'; +import '../widget/cardx.dart'; import '../widget/two_line_text.dart'; import '../widget/url_text.dart'; @@ -213,7 +213,7 @@ class _DockerManagePageState extends State { _buildPs(), _buildImage(), _buildEditHost(), - ].map((e) => RoundRectCard(e)); + ].map((e) => CardX(e)); return ListView( padding: const EdgeInsets.all(7), children: items.toList(), diff --git a/lib/view/page/home.dart b/lib/view/page/home.dart index 7e3ce951..6b4c0ca2 100644 --- a/lib/view/page/home.dart +++ b/lib/view/page/home.dart @@ -26,7 +26,7 @@ import '../../data/res/misc.dart'; import '../../data/res/ui.dart'; import '../../data/res/url.dart'; import '../widget/custom_appbar.dart'; -import '../widget/round_rect_card.dart'; +import '../widget/cardx.dart'; import '../widget/url_text.dart'; import '../widget/value_notifier.dart'; @@ -256,7 +256,7 @@ class _HomePageState extends State title: Text('${l10n.about} & ${l10n.feedback}'), onTap: _showAboutDialog, ) - ].map((e) => RoundRectCard(e)).toList(), + ].map((e) => CardX(e)).toList(), ), ); } diff --git a/lib/view/page/ping.dart b/lib/view/page/ping.dart index e9432ee0..bad0511b 100644 --- a/lib/view/page/ping.dart +++ b/lib/view/page/ping.dart @@ -14,7 +14,7 @@ import '../../data/model/server/ping_result.dart'; import '../../data/res/color.dart'; import '../../data/res/ui.dart'; import '../widget/input_field.dart'; -import '../widget/round_rect_card.dart'; +import '../widget/cardx.dart'; /// Only permit ipv4 / ipv6 / domain chars final targetReg = RegExp(r'[a-zA-Z0-9\.-_:]+'); @@ -103,7 +103,7 @@ class _PingPageState extends State return Center( child: Text( l10n.noResult, - style: const TextStyle(fontSize: 18), + style: const TextStyle(fontSize: 15), ), ); } @@ -118,7 +118,7 @@ class _PingPageState extends State Widget _buildResultItem(PingResult result) { final unknown = l10n.unknown; final ms = l10n.ms; - return RoundRectCard( + return CardX( ListTile( contentPadding: const EdgeInsets.symmetric(vertical: 7, horizontal: 17), title: Text( diff --git a/lib/view/page/private_key/list.dart b/lib/view/page/private_key/list.dart index 3c4db0c3..8351bc89 100644 --- a/lib/view/page/private_key/list.dart +++ b/lib/view/page/private_key/list.dart @@ -15,7 +15,7 @@ import '../../../data/model/server/private_key_info.dart'; import '../../../data/provider/private_key.dart'; import '../../../data/res/ui.dart'; import '../../widget/custom_appbar.dart'; -import '../../../view/widget/round_rect_card.dart'; +import '../../widget/cardx.dart'; class PrivateKeysListPage extends StatefulWidget { const PrivateKeysListPage({Key? key}) : super(key: key); @@ -53,7 +53,7 @@ class _PrivateKeyListState extends State itemCount: key.pkis.length, itemBuilder: (context, idx) { final item = key.pkis[idx]; - return RoundRectCard( + return CardX( ListTile( leading: Text( '#$idx', diff --git a/lib/view/page/process.dart b/lib/view/page/process.dart index cd71dede..af95915e 100644 --- a/lib/view/page/process.dart +++ b/lib/view/page/process.dart @@ -15,7 +15,7 @@ import '../../data/model/server/proc.dart'; import '../../data/model/server/server_private_info.dart'; import '../../data/res/ui.dart'; import '../widget/custom_appbar.dart'; -import '../widget/round_rect_card.dart'; +import '../widget/cardx.dart'; import '../widget/two_line_text.dart'; class ProcessPage extends StatefulWidget { @@ -142,7 +142,7 @@ class _ProcessPageState extends State { final leading = proc.user == null ? Text(proc.pid.toString()) : TwoLineText(up: proc.pid.toString(), down: proc.user!); - return RoundRectCard( + return CardX( ListTile( leading: SizedBox( width: _media.size.width / 6, diff --git a/lib/view/page/server/detail.dart b/lib/view/page/server/detail.dart index 055b2775..62df8270 100644 --- a/lib/view/page/server/detail.dart +++ b/lib/view/page/server/detail.dart @@ -20,7 +20,7 @@ import '../../../data/res/color.dart'; import '../../../data/res/default.dart'; import '../../../data/res/ui.dart'; import '../../widget/custom_appbar.dart'; -import '../../widget/round_rect_card.dart'; +import '../../widget/cardx.dart'; class ServerDetailPage extends StatefulWidget { const ServerDetailPage({Key? key, required this.spi}) : super(key: key); @@ -131,7 +131,7 @@ class _ServerDetailPageState extends State ]); } - return RoundRectCard( + return CardX( Padding( padding: UIs.roundRectCardPadding, child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -201,7 +201,7 @@ class _ServerDetailPageState extends State } Widget _buildUpTimeAndSys(ServerStatus ss) { - return RoundRectCard( + return CardX( Padding( padding: UIs.roundRectCardPadding, child: Row( @@ -229,7 +229,7 @@ class _ServerDetailPageState extends State final used = ss.mem.usedPercent * 100; final usedStr = used.toStringAsFixed(0); - return RoundRectCard( + return CardX( Padding( padding: UIs.roundRectCardPadding, child: Column( @@ -274,7 +274,7 @@ class _ServerDetailPageState extends State if (ss.swap.total == 0) return UIs.placeholder; final used = ss.swap.usedPercent * 100; final cached = ss.swap.cached / ss.swap.total * 100; - return RoundRectCard( + return CardX( Padding( padding: UIs.roundRectCardPadding, child: Column( @@ -339,7 +339,7 @@ class _ServerDetailPageState extends State ), )) .toList(); - return RoundRectCard( + return CardX( Padding( padding: UIs.roundRectCardPadding, child: Column( @@ -351,7 +351,7 @@ class _ServerDetailPageState extends State } Widget _buildNetView(ServerStatus ss) { - return RoundRectCard( + return CardX( Padding( padding: UIs.roundRectCardPadding, child: ValueBuilder( @@ -503,7 +503,7 @@ class _ServerDetailPageState extends State ), ], ))); - return RoundRectCard( + return CardX( Padding( padding: UIs.roundRectCardPadding, child: Column(children: children), diff --git a/lib/view/page/server/edit.dart b/lib/view/page/server/edit.dart index a169a62f..5793bbae 100644 --- a/lib/view/page/server/edit.dart +++ b/lib/view/page/server/edit.dart @@ -13,7 +13,7 @@ import '../../../data/provider/private_key.dart'; import '../../../data/res/ui.dart'; import '../../widget/custom_appbar.dart'; import '../../widget/input_field.dart'; -import '../../widget/round_rect_card.dart'; +import '../../widget/cardx.dart'; import '../../widget/tag.dart'; import '../../widget/value_notifier.dart'; @@ -283,7 +283,7 @@ class _ServerEditPageState extends State { ), ), ); - return RoundRectCard( + return CardX( Padding( padding: const EdgeInsets.symmetric(horizontal: 17), child: Column( diff --git a/lib/view/page/server/tab.dart b/lib/view/page/server/tab.dart index 7a51a332..c749d175 100644 --- a/lib/view/page/server/tab.dart +++ b/lib/view/page/server/tab.dart @@ -23,7 +23,7 @@ import '../../../data/model/server/server_status.dart'; import '../../../data/provider/server.dart'; import '../../../data/res/color.dart'; import '../../../data/res/ui.dart'; -import '../../widget/round_rect_card.dart'; +import '../../widget/cardx.dart'; import '../../widget/server_func_btns.dart'; import '../../widget/tag.dart'; @@ -173,7 +173,7 @@ class _ServerPageState extends State return UIs.placeholder; } - return RoundRectCard( + return CardX( key: Key(si.spi.id + (_tag ?? '')), InkWell( onTap: () { diff --git a/lib/view/page/setting/android.dart b/lib/view/page/setting/android.dart index 6373d849..390bc2ae 100644 --- a/lib/view/page/setting/android.dart +++ b/lib/view/page/setting/android.dart @@ -11,7 +11,7 @@ import 'package:toolbox/data/res/store.dart'; import 'package:toolbox/view/page/setting/platform_pub.dart'; import 'package:toolbox/view/widget/custom_appbar.dart'; import 'package:toolbox/view/widget/input_field.dart'; -import 'package:toolbox/view/widget/round_rect_card.dart'; +import 'package:toolbox/view/widget/cardx.dart'; import 'package:toolbox/view/widget/store_switch.dart'; class AndroidSettingsPage extends StatefulWidget { @@ -43,7 +43,7 @@ class _AndroidSettingsPageState extends State { _buildAndroidWidgetSharedPreference(), if (BioAuth.isPlatformSupported) PlatformPublicSettings.buildBioAuth(), - ].map((e) => RoundRectCard(e)).toList(), + ].map((e) => CardX(e)).toList(), ), ); } diff --git a/lib/view/page/setting/entry.dart b/lib/view/page/setting/entry.dart index ad447dc7..004db553 100644 --- a/lib/view/page/setting/entry.dart +++ b/lib/view/page/setting/entry.dart @@ -30,7 +30,7 @@ import '../../../data/res/ui.dart'; import '../../widget/color_picker.dart'; import '../../widget/custom_appbar.dart'; import '../../widget/input_field.dart'; -import '../../widget/round_rect_card.dart'; +import '../../widget/cardx.dart'; import '../../widget/store_switch.dart'; import '../../widget/value_notifier.dart'; @@ -194,7 +194,7 @@ class _SettingPageState extends State { children.add(_buildPlatformSetting()); } return Column( - children: children.map((e) => RoundRectCard(e)).toList(), + children: children.map((e) => CardX(e)).toList(), ); } @@ -204,7 +204,7 @@ class _SettingPageState extends State { _buildFullScreenSwitch(), _buildFullScreenJitter(), _buildFulScreenRotateQuarter(), - ].map((e) => RoundRectCard(e)).toList(), + ].map((e) => CardX(e)).toList(), ); } @@ -219,7 +219,7 @@ class _SettingPageState extends State { //_buildDiskIgnorePath(), _buildDeleteServers(), //if (isDesktop) _buildDoubleColumnServersPage(), - ].map((e) => RoundRectCard(e)).toList(), + ].map((e) => CardX(e)).toList(), ); } @@ -232,7 +232,7 @@ class _SettingPageState extends State { // Use hardware keyboard on desktop, so there is no need to set it if (isMobile) _buildKeyboardType(), _buildSSHVirtKeys(), - ].map((e) => RoundRectCard(e)).toList(), + ].map((e) => CardX(e)).toList(), ); } @@ -243,7 +243,7 @@ class _SettingPageState extends State { _buildEditorTheme(), _buildEditorDarkTheme(), _buildEditorHighlight(), - ].map((e) => RoundRectCard(e)).toList(), + ].map((e) => CardX(e)).toList(), ); } @@ -848,7 +848,7 @@ class _SettingPageState extends State { children: [ _buildSftpRmrDir(), _buildSftpOpenLastPath(), - ].map((e) => RoundRectCard(e)).toList(), + ].map((e) => CardX(e)).toList(), ); } diff --git a/lib/view/page/setting/ios.dart b/lib/view/page/setting/ios.dart index 976d648e..3502e862 100644 --- a/lib/view/page/setting/ios.dart +++ b/lib/view/page/setting/ios.dart @@ -14,7 +14,7 @@ import 'package:toolbox/data/res/ui.dart'; import 'package:toolbox/view/page/setting/platform_pub.dart'; import 'package:toolbox/view/widget/custom_appbar.dart'; import 'package:toolbox/view/widget/future_widget.dart'; -import 'package:toolbox/view/widget/round_rect_card.dart'; +import 'package:toolbox/view/widget/cardx.dart'; import 'package:toolbox/view/widget/store_switch.dart'; import 'package:watch_connectivity/watch_connectivity.dart'; @@ -44,7 +44,7 @@ class _IOSSettingsPageState extends State { _buildWatchApp(), if (BioAuth.isPlatformSupported) PlatformPublicSettings.buildBioAuth(), - ].map((e) => RoundRectCard(e)).toList(), + ].map((e) => CardX(e)).toList(), ), ); } diff --git a/lib/view/page/setting/srv_detail_seq.dart b/lib/view/page/setting/srv_detail_seq.dart index d85673cd..2331b7a6 100644 --- a/lib/view/page/setting/srv_detail_seq.dart +++ b/lib/view/page/setting/srv_detail_seq.dart @@ -4,7 +4,7 @@ import 'package:toolbox/data/res/store.dart'; import '../../../core/extension/order.dart'; import '../../widget/custom_appbar.dart'; -import '../../widget/round_rect_card.dart'; +import '../../widget/cardx.dart'; class ServerDetailOrderPage extends StatefulWidget { const ServerDetailOrderPage({super.key}); @@ -53,7 +53,7 @@ class _ServerDetailOrderPageState extends State { return ReorderableDelayedDragStartListener( key: ValueKey('$index'), index: index, - child: RoundRectCard(ListTile( + child: CardX(ListTile( title: Text(id), trailing: const Icon(Icons.drag_handle), )), diff --git a/lib/view/page/setting/srv_seq.dart b/lib/view/page/setting/srv_seq.dart index 3172544a..436cd1ae 100644 --- a/lib/view/page/setting/srv_seq.dart +++ b/lib/view/page/setting/srv_seq.dart @@ -3,7 +3,7 @@ import 'package:toolbox/core/extension/context/locale.dart'; import 'package:toolbox/core/extension/order.dart'; import 'package:toolbox/data/res/provider.dart'; import 'package:toolbox/data/res/store.dart'; -import 'package:toolbox/view/widget/round_rect_card.dart'; +import 'package:toolbox/view/widget/cardx.dart'; import '../../widget/custom_appbar.dart'; @@ -54,7 +54,7 @@ class _ServerOrderPageState extends State { return ReorderableDelayedDragStartListener( key: ValueKey('$index'), index: index, - child: RoundRectCard(ListTile( + child: CardX(ListTile( title: Text(spi.name), subtitle: Text(spi.id), leading: CircleAvatar( diff --git a/lib/view/page/setting/virt_key.dart b/lib/view/page/setting/virt_key.dart index 27a9b215..538d8810 100644 --- a/lib/view/page/setting/virt_key.dart +++ b/lib/view/page/setting/virt_key.dart @@ -6,7 +6,7 @@ import 'package:toolbox/core/utils/platform/base.dart'; import 'package:toolbox/data/model/ssh/virtual_key.dart'; import 'package:toolbox/data/res/store.dart'; import 'package:toolbox/data/res/ui.dart'; -import 'package:toolbox/view/widget/round_rect_card.dart'; +import 'package:toolbox/view/widget/cardx.dart'; import '../../widget/custom_appbar.dart'; @@ -41,7 +41,7 @@ class _SSHVirtKeySettingPageState extends State { itemBuilder: (_, idx) { final key = allKeys[idx]; final help = key.help; - return RoundRectCard( + return CardX( key: ValueKey(idx), ListTile( title: _buildTitle(key), diff --git a/lib/view/page/snippet/list.dart b/lib/view/page/snippet/list.dart index 4318a27d..89396629 100644 --- a/lib/view/page/snippet/list.dart +++ b/lib/view/page/snippet/list.dart @@ -13,7 +13,7 @@ import '../../../data/res/ui.dart'; import '../../widget/tag.dart'; import '/core/route.dart'; import '/data/provider/snippet.dart'; -import '/view/widget/round_rect_card.dart'; +import '../../widget/cardx.dart'; class SnippetListPage extends StatefulWidget { const SnippetListPage({Key? key}) : super(key: key); @@ -94,7 +94,7 @@ class _SnippetListPageState extends State { } Widget _buildSnippetItem(Snippet snippet) { - return RoundRectCard( + return CardX( ListTile( contentPadding: const EdgeInsets.only(left: 23, right: 17), title: Text( diff --git a/lib/view/page/storage/local.dart b/lib/view/page/storage/local.dart index 1725d93a..dd9931c6 100644 --- a/lib/view/page/storage/local.dart +++ b/lib/view/page/storage/local.dart @@ -11,7 +11,7 @@ import 'package:toolbox/data/res/misc.dart'; import 'package:toolbox/data/res/provider.dart'; import 'package:toolbox/view/widget/input_field.dart'; import 'package:toolbox/view/widget/omit_start_text.dart'; -import 'package:toolbox/view/widget/round_rect_card.dart'; +import 'package:toolbox/view/widget/cardx.dart'; import '../../../core/extension/numx.dart'; import '../../../core/route.dart'; @@ -153,7 +153,7 @@ class _LocalStoragePageState extends State { var stat = file.statSync(); var isDir = stat.type == FileSystemEntityType.directory; - return RoundRectCard(ListTile( + return CardX(ListTile( leading: isDir ? const Icon(Icons.folder) : const Icon(Icons.insert_drive_file), diff --git a/lib/view/page/storage/sftp.dart b/lib/view/page/storage/sftp.dart index 5894c49e..9786150d 100644 --- a/lib/view/page/storage/sftp.dart +++ b/lib/view/page/storage/sftp.dart @@ -14,7 +14,7 @@ import 'package:toolbox/data/res/misc.dart'; import 'package:toolbox/data/res/provider.dart'; import 'package:toolbox/data/res/store.dart'; import 'package:toolbox/view/widget/omit_start_text.dart'; -import 'package:toolbox/view/widget/round_rect_card.dart'; +import 'package:toolbox/view/widget/cardx.dart'; import '../../../core/extension/numx.dart'; import '../../../core/route.dart'; @@ -277,7 +277,7 @@ class _SftpPageState extends State with AfterLayoutMixin { style: UIs.textGrey, textAlign: TextAlign.right, ); - return RoundRectCard(ListTile( + return CardX(ListTile( leading: Icon(isDir ? Icons.folder : Icons.insert_drive_file), title: Text(file.filename), trailing: trailing, diff --git a/lib/view/page/storage/sftp_mission.dart b/lib/view/page/storage/sftp_mission.dart index 5b32c36c..e03058c7 100644 --- a/lib/view/page/storage/sftp_mission.dart +++ b/lib/view/page/storage/sftp_mission.dart @@ -13,7 +13,7 @@ import '../../../data/model/sftp/req.dart'; import '../../../data/provider/sftp.dart'; import '../../../data/res/ui.dart'; import '../../widget/custom_appbar.dart'; -import '../../widget/round_rect_card.dart'; +import '../../widget/cardx.dart'; class SftpMissionPage extends StatefulWidget { const SftpMissionPage({Key? key}) : super(key: key); @@ -119,7 +119,7 @@ class _SftpMissionPageState extends State { Widget? trailing, }) { final time = DateTime.fromMicrosecondsSinceEpoch(status.id); - return RoundRectCard( + return CardX( ListTile( leading: Text(time.hourMinute), title: Text( diff --git a/lib/view/widget/round_rect_card.dart b/lib/view/widget/cardx.dart similarity index 75% rename from lib/view/widget/round_rect_card.dart rename to lib/view/widget/cardx.dart index 16a35b41..ef354735 100644 --- a/lib/view/widget/round_rect_card.dart +++ b/lib/view/widget/cardx.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -class RoundRectCard extends StatelessWidget { - const RoundRectCard(this.child, {Key? key, this.color}) : super(key: key); +class CardX extends StatelessWidget { + const CardX(this.child, {Key? key, this.color}) : super(key: key); final Widget child; final Color? color; diff --git a/lib/view/widget/expand_tile.dart b/lib/view/widget/expand_tile.dart index cbf02457..c703b22a 100644 --- a/lib/view/widget/expand_tile.dart +++ b/lib/view/widget/expand_tile.dart @@ -8,5 +8,6 @@ class ExpandTile extends ExpansionTile { required super.title, super.children, super.subtitle, + super.initiallyExpanded, }) : super(shape: _shape, collapsedShape: _shape); } diff --git a/lib/view/widget/input_field.dart b/lib/view/widget/input_field.dart index bec153c2..795b4550 100644 --- a/lib/view/widget/input_field.dart +++ b/lib/view/widget/input_field.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'round_rect_card.dart'; +import 'cardx.dart'; class Input extends StatelessWidget { final TextEditingController? controller; @@ -41,7 +41,7 @@ class Input extends StatelessWidget { }); @override Widget build(BuildContext context) { - return RoundRectCard( + return CardX( Padding( padding: const EdgeInsets.symmetric(horizontal: 17), child: TextField( diff --git a/lib/view/widget/tag.dart b/lib/view/widget/tag.dart index 6a744a1c..20e8e183 100644 --- a/lib/view/widget/tag.dart +++ b/lib/view/widget/tag.dart @@ -4,7 +4,7 @@ import 'package:toolbox/core/extension/context/dialog.dart'; import 'package:toolbox/core/extension/context/locale.dart'; import 'package:toolbox/data/res/ui.dart'; import 'package:toolbox/view/widget/input_field.dart'; -import 'package:toolbox/view/widget/round_rect_card.dart'; +import 'package:toolbox/view/widget/cardx.dart'; import '../../data/res/color.dart'; @@ -57,7 +57,7 @@ class TagEditor extends StatefulWidget { class _TagEditorState extends State { @override Widget build(BuildContext context) { - return RoundRectCard(ListTile( + return CardX(ListTile( leading: const Icon(Icons.tag), title: _buildTags(widget.tags), trailing: InkWell(