diff --git a/.gitignore b/.gitignore index d270852d..04c35875 100644 --- a/.gitignore +++ b/.gitignore @@ -57,7 +57,7 @@ test.dart # Linux release linux.AppDir -ServerBox-x86_64.AppImage +**/*.AppImage untranlated.json diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 2df37a20..2e54e59e 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -690,7 +690,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 936; + CURRENT_PROJECT_VERSION = 943; DEVELOPMENT_TEAM = BA88US33G6; ENABLE_BITCODE = NO; INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist"; @@ -700,7 +700,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.936; + MARKETING_VERSION = 1.0.943; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -826,7 +826,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 936; + CURRENT_PROJECT_VERSION = 943; DEVELOPMENT_TEAM = BA88US33G6; ENABLE_BITCODE = NO; INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist"; @@ -836,7 +836,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.936; + MARKETING_VERSION = 1.0.943; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -854,7 +854,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 936; + CURRENT_PROJECT_VERSION = 943; DEVELOPMENT_TEAM = BA88US33G6; ENABLE_BITCODE = NO; INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist"; @@ -864,7 +864,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.936; + MARKETING_VERSION = 1.0.943; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -885,7 +885,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 936; + CURRENT_PROJECT_VERSION = 943; DEVELOPMENT_TEAM = BA88US33G6; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -898,7 +898,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.936; + MARKETING_VERSION = 1.0.943; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget; @@ -924,7 +924,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 936; + CURRENT_PROJECT_VERSION = 943; DEVELOPMENT_TEAM = BA88US33G6; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -937,7 +937,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.936; + MARKETING_VERSION = 1.0.943; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -960,7 +960,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 936; + CURRENT_PROJECT_VERSION = 943; DEVELOPMENT_TEAM = BA88US33G6; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -973,7 +973,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.936; + MARKETING_VERSION = 1.0.943; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -996,7 +996,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 936; + CURRENT_PROJECT_VERSION = 943; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = BA88US33G6; ENABLE_PREVIEWS = YES; @@ -1008,7 +1008,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.936; + MARKETING_VERSION = 1.0.943; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd; @@ -1037,7 +1037,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 936; + CURRENT_PROJECT_VERSION = 943; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = BA88US33G6; ENABLE_PREVIEWS = YES; @@ -1049,7 +1049,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.936; + MARKETING_VERSION = 1.0.943; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd; PRODUCT_NAME = ServerBox; @@ -1075,7 +1075,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 936; + CURRENT_PROJECT_VERSION = 943; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = BA88US33G6; ENABLE_PREVIEWS = YES; @@ -1087,7 +1087,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.936; + MARKETING_VERSION = 1.0.943; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd; PRODUCT_NAME = ServerBox; diff --git a/lib/app.dart b/lib/app.dart index ca669926..8be9842b 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -15,7 +15,7 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { _setup(context); return ListenableBuilder( - listenable: RebuildNodes.app, + listenable: RNodes.app, builder: (context, _) { if (!Stores.setting.useSystemPrimaryColor.fetch()) { UIs.colorSeed = Color(Stores.setting.primaryColor.fetch()); diff --git a/lib/data/model/app/backup.dart b/lib/data/model/app/backup.dart index 8d296778..4bd4a303 100644 --- a/lib/data/model/app/backup.dart +++ b/lib/data/model/app/backup.dart @@ -170,7 +170,7 @@ class Backup { } Pros.reload(); - RebuildNodes.app.rebuild(); + RNodes.app.build(); _logger.info('Restore success'); } diff --git a/lib/data/model/app/github_id.dart b/lib/data/model/app/github_id.dart deleted file mode 100644 index 757bc1a9..00000000 --- a/lib/data/model/app/github_id.dart +++ /dev/null @@ -1,5 +0,0 @@ -typedef GhId = String; - -extension GhIdX on GhId { - String get url => 'https://github.com/$this'; -} diff --git a/lib/data/model/app/rebuild.dart b/lib/data/model/app/rebuild.dart deleted file mode 100644 index 5f160865..00000000 --- a/lib/data/model/app/rebuild.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:flutter/foundation.dart'; - -class RebuildNode implements Listenable { - final List _listeners = []; - - RebuildNode(); - - @override - void addListener(VoidCallback listener) { - _listeners.add(listener); - } - - @override - void removeListener(VoidCallback listener) { - _listeners.remove(listener); - } - - void rebuild() { - for (var listener in _listeners) { - listener(); - } - } -} diff --git a/lib/data/model/container/ps.dart b/lib/data/model/container/ps.dart index 30d639dd..3cc620f6 100644 --- a/lib/data/model/container/ps.dart +++ b/lib/data/model/container/ps.dart @@ -143,7 +143,6 @@ final class DockerPs implements ContainerPs { @override bool get running { if (state?.contains('Exited') == true) return false; - if (state?.contains('Up') == true) return true; return true; } @@ -162,9 +161,9 @@ final class DockerPs implements ContainerPs { final parts = raw.split(Miscs.multiBlankreg); return DockerPs( id: parts[0], - names: parts[1], - image: parts[2], - state: parts[3].trim(), + state: parts[1], + names: parts[2], + image: parts[3].trim(), ); } } diff --git a/lib/data/provider/container.dart b/lib/data/provider/container.dart index 2371906c..4541a485 100644 --- a/lib/data/provider/container.dart +++ b/lib/data/provider/container.dart @@ -276,9 +276,13 @@ enum ContainerCmdType { return switch (this) { ContainerCmdType.version => '$prefix version $_jsonFmt', ContainerCmdType.ps => switch (type) { + /// TODO: Rollback to json format when permformance recovers. /// Use [_jsonFmt] in Docker will cause the operation to slow down. ContainerType.docker => '$prefix ps -a --format "table {{printf \\"' - '%-15.15s ${"%-30.30s " * 3}\\" .ID .Names .Image .Status}}"', + '%-15.15s ' + '%-30.30s ' + '${"%-50.50s " * 2}\\"' + ' .ID .Status .Names .Image}}"', ContainerType.podman => '$prefix ps -a $_jsonFmt', }, ContainerCmdType.stats => diff --git a/lib/data/res/build_data.dart b/lib/data/res/build_data.dart index 5e8a176c..adfe5a89 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 = 936; + static const int build = 943; static const String engine = "3.22.1"; - static const String buildAt = "2024-06-06 16:18:25"; - static const int modifications = 2; + static const String buildAt = "2024-06-08 19:28:01"; + static const int modifications = 7; static const int script = 48; } diff --git a/lib/data/res/github_id.dart b/lib/data/res/github_id.dart index 73df0fed..01ee0ba3 100644 --- a/lib/data/res/github_id.dart +++ b/lib/data/res/github_id.dart @@ -1,5 +1,3 @@ -import 'package:toolbox/data/model/app/github_id.dart'; - abstract final class GithubIds { // Thanks // If you want to change your Github ID, please open an issue. @@ -75,3 +73,9 @@ abstract final class GithubIds { 'Jasonzhu1207', }; } + +typedef GhId = String; + +extension GhIdX on GhId { + String get url => 'https://github.com/$this'; +} diff --git a/lib/data/res/rebuild.dart b/lib/data/res/rebuild.dart index f78cd8a4..71c4019f 100644 --- a/lib/data/res/rebuild.dart +++ b/lib/data/res/rebuild.dart @@ -1,5 +1,6 @@ -import 'package:toolbox/data/model/app/rebuild.dart'; +import 'package:fl_lib/fl_lib.dart'; -abstract final class RebuildNodes { - static final app = RebuildNode(); +abstract final class RNodes { + static final app = RNode(); + static final dark = false.vn; } diff --git a/lib/view/page/home/home.dart b/lib/view/page/home/home.dart index ab4a7d7f..8a61b1b8 100644 --- a/lib/view/page/home/home.dart +++ b/lib/view/page/home/home.dart @@ -9,7 +9,6 @@ import 'package:toolbox/core/channel/home_widget.dart'; import 'package:toolbox/core/extension/build.dart'; import 'package:toolbox/core/extension/context/locale.dart'; import 'package:toolbox/core/route.dart'; -import 'package:toolbox/data/model/app/github_id.dart'; import 'package:toolbox/data/model/app/tab.dart'; import 'package:toolbox/data/res/build_data.dart'; import 'package:toolbox/data/res/github_id.dart'; @@ -17,6 +16,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/data/res/url.dart'; +import 'package:toolbox/view/page/ssh/page.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; part 'appbar.dart'; @@ -151,6 +151,7 @@ class _HomePageState extends State physics: const NeverScrollableScrollPhysics(), itemBuilder: (_, index) => AppTab.values[index].page, onPageChanged: (value) { + SSHPage.focusNode.unfocus(); if (!_switchingPage) { _selectIndex.value = value; } diff --git a/lib/view/page/setting/entry.dart b/lib/view/page/setting/entry.dart index f1d3eb60..dbcffe7a 100644 --- a/lib/view/page/setting/entry.dart +++ b/lib/view/page/setting/entry.dart @@ -305,7 +305,7 @@ class _SettingPageState extends State { _setting.primaryColor.put(color.value); context.pop(); context.pop(); - RebuildNodes.app.rebuild(); + RNodes.app.build(); } // Widget _buildLaunchPage() { @@ -393,7 +393,7 @@ class _SettingPageState extends State { ); if (selected != null) { _setting.themeMode.put(selected); - RebuildNodes.app.rebuild(); + RNodes.app.build(); } }, trailing: ValBuilder( @@ -442,7 +442,7 @@ class _SettingPageState extends State { onPressed: () { _setting.fontPath.delete(); context.pop(); - RebuildNodes.app.rebuild(); + RNodes.app.build(); }, child: Text(l10n.clear), ) @@ -466,7 +466,7 @@ class _SettingPageState extends State { } context.pop(); - RebuildNodes.app.rebuild(); + RNodes.app.build(); } Widget _buildTermFontSize() { @@ -536,7 +536,7 @@ class _SettingPageState extends State { if (selected != null) { _setting.locale.put(selected.code); context.pop(); - RebuildNodes.app.rebuild(); + RNodes.app.build(); } }, trailing: ListenBuilder( @@ -609,7 +609,7 @@ class _SettingPageState extends State { subtitle: Text(l10n.fullScreenTip, style: UIs.textGrey), trailing: StoreSwitch( prop: _setting.fullScreen, - callback: (_) => RebuildNodes.app.rebuild(), + callback: (_) => RNodes.app.build(), ), ); } @@ -820,7 +820,7 @@ class _SettingPageState extends State { return; } _setting.textFactor.put(val); - RebuildNodes.app.rebuild(); + RNodes.app.build(); context.pop(); } diff --git a/lib/view/page/ssh/page.dart b/lib/view/page/ssh/page.dart index 9eac3e9e..95f99fdb 100644 --- a/lib/view/page/ssh/page.dart +++ b/lib/view/page/ssh/page.dart @@ -41,13 +41,15 @@ class SSHPage extends StatefulWidget { this.terminalKey, }); + static final focusNode = FocusNode(); + @override - State createState() => _SSHPageState(); + State createState() => SSHPageState(); } const _horizonPadding = 7.0; -class _SSHPageState extends State with AutomaticKeepAliveClientMixin { +class SSHPageState extends State with AutomaticKeepAliveClientMixin { final _keyboard = VirtKeyProvider(); late final _terminal = Terminal(inputHandler: _keyboard); final TerminalController _terminalController = TerminalController(); @@ -159,6 +161,7 @@ class _SSHPageState extends State with AutomaticKeepAliveClientMixin { CustomAppBar.barHeight ?? _media.padding.top, ), hideScrollBar: false, + focusNode: SSHPage.focusNode, ), ), ); diff --git a/lib/view/page/ssh/tab.dart b/lib/view/page/ssh/tab.dart index 290e1996..cacd26a9 100644 --- a/lib/view/page/ssh/tab.dart +++ b/lib/view/page/ssh/tab.dart @@ -1,5 +1,6 @@ import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; +import 'package:icons_plus/icons_plus.dart'; import 'package:provider/provider.dart'; import 'package:toolbox/core/extension/context/locale.dart'; import 'package:toolbox/core/route.dart'; @@ -15,37 +16,37 @@ class SSHTabPage extends StatefulWidget { State createState() => _SSHTabPageState(); } +typedef _TabMap = Map? key})>; + class _SSHTabPageState extends State with TickerProviderStateMixin, AutomaticKeepAliveClientMixin { - late final _tabMap = { - l10n.add: (page: _buildAddPage()), + late final _TabMap _tabMap = { + l10n.add: (page: _buildAddPage(), key: null), }; - late var _tabController = TabController( - length: _tabMap.length, - vsync: this, - ); - final _fabRN = ValueNotifier(0); + final _pageCtrl = PageController(); + final _fabVN = 0.vn; + final _tabRN = RNode(); @override Widget build(BuildContext context) { super.build(context); return Scaffold( - appBar: TabBar( - controller: _tabController, - tabs: _tabMap.keys.map(_buildTabItem).toList(), - isScrollable: true, - tabAlignment: TabAlignment.start, - dividerColor: Colors.transparent, - onTap: (value) { - _fabRN.value = value; - FocusScope.of(context).unfocus(); + appBar: PreferredSizeListenBuilder( + listenable: _tabRN, + builder: () { + return _TabBar( + idxVN: _fabVN, + map: _tabMap, + onTap: _onTapTab, + onClose: _onTapClose, + ); }, ), body: _buildBody(), floatingActionButton: ListenableBuilder( - listenable: _fabRN, + listenable: _fabVN, builder: (_, __) { - if (_fabRN.value != 0) return const SizedBox(); + if (_fabVN.value != 0) return const SizedBox(); return FloatingActionButton( heroTag: 'sshAddServer', onPressed: () => AppRoutes.serverEdit().go(context), @@ -57,49 +58,37 @@ class _SSHTabPageState extends State ); } - Widget _buildTabItem(String e) { - if (e == l10n.add) { - return Tab(child: Text(e)); - } - return Tab( - child: Row( - children: [ - Text(e), - UIs.width7, - IconBtn( - icon: Icons.close, - onTap: () async { - final confirm = await showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: Text(l10n.attention), - content: Text('${l10n.close} SSH ${l10n.conn}($e) ?'), - actions: [ - TextButton( - onPressed: () => context.pop(true), - child: Text(l10n.ok, style: UIs.textRed), - ), - TextButton( - onPressed: () => context.pop(false), - child: Text(l10n.cancel), - ), - ], - ); - }, - ); - Future.delayed(const Duration(milliseconds: 50), - FocusScope.of(context).unfocus); - if (confirm != true) { - return; - } - _tabMap.remove(e); - _refreshTabs(); - }, - ), - ], - ), + void _onTapTab(int idx) async { + await _toPage(idx); + _fabVN.value = idx; + FocusScope.of(context).unfocus(); + } + + void _onTapClose(String name) async { + final confirm = await showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text(l10n.attention), + content: Text('${l10n.close} SSH ${l10n.conn}($name) ?'), + actions: [ + TextButton( + onPressed: () => context.pop(true), + child: Text(l10n.ok, style: UIs.textRed), + ), + TextButton( + onPressed: () => context.pop(false), + child: Text(l10n.cancel), + ), + ], + ); + }, ); + Future.delayed(Durations.short1, FocusScope.of(context).unfocus); + if (confirm != true) return; + + _tabMap.remove(name); + _tabRN.build(); } Widget _buildAddPage() { @@ -134,44 +123,155 @@ class _SSHTabPageState extends State } Widget _buildBody() { - return TabBarView( - physics: const NeverScrollableScrollPhysics(), - controller: _tabController, - children: _tabMap.values.map((e) => e.page).toList(), + return ListenBuilder( + listenable: _tabRN, + builder: () { + return PageView.builder( + physics: const NeverScrollableScrollPhysics(), + controller: _pageCtrl, + itemCount: _tabMap.length, + itemBuilder: (_, idx) { + final name = _tabMap.keys.elementAt(idx); + return _tabMap[name]?.page ?? UIs.placeholder; + }, + ); + }, ); } - void _onTapInitCard(ServerPrivateInfo spi) { + void _onTapInitCard(ServerPrivateInfo spi) async { final name = () { - if (_tabMap.containsKey(spi.name)) { - return '${spi.name}(${_tabMap.length + 1})'; + final reg = RegExp(r'\((\d+)\)'); + final idxs = _tabMap.keys + .map((e) => reg.firstMatch(e)) + .map((e) => e?.group(1)) + .where((e) => e != null); + if (idxs.isEmpty) return spi.name; + final biggest = idxs.reduce((a, b) => a!.length > b!.length ? a : b); + final biggestInt = int.tryParse(biggest ?? '0'); + if (biggestInt != null && biggestInt > 0) { + return '${spi.name}(${biggestInt + 1})'; } return spi.name; }(); + final key = GlobalKey(); _tabMap[name] = ( page: SSHPage( + // Keep it, or the Flutter will works unexpectedly + key: key, spi: spi, notFromTab: false, onSessionEnd: () { _tabMap.remove(name); - _refreshTabs(); }, ), + key: key, ); - _refreshTabs(); - final idx = _tabMap.length - 1; - _tabController.animateTo(idx); - _fabRN.value = idx; + _tabRN.build(); + // Wait for the page to be built + await Future.delayed(Durations.short3); + final idx = _tabMap.keys.toList().indexOf(name); + await _toPage(idx); + _fabVN.value = idx; } - void _refreshTabs() { - _tabController = TabController( - length: _tabMap.length, - vsync: this, - ); - setState(() {}); - } + Future _toPage(int idx) => _pageCtrl.animateToPage(idx, + duration: Durations.short3, curve: Curves.fastEaseInToSlowEaseOut); @override bool get wantKeepAlive => true; } + +final class _TabBar extends StatelessWidget implements PreferredSizeWidget { + const _TabBar({ + required this.idxVN, + required this.map, + required this.onTap, + required this.onClose, + }); + + final ValueNotifier idxVN; + final _TabMap map; + final void Function(int idx) onTap; + final void Function(String name) onClose; + + List get names => map.keys.toList(); + + @override + Size get preferredSize => const Size.fromHeight(48); + + @override + Widget build(BuildContext context) { + return ListenBuilder( + listenable: idxVN, + builder: () { + return ListView.separated( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 11, vertical: 5), + itemCount: names.length, + itemBuilder: (_, idx) => _buillItem(idx), + separatorBuilder: (_, __) => Padding( + padding: const EdgeInsets.symmetric(vertical: 17), + child: Container( + color: const Color.fromARGB(61, 158, 158, 158), + width: 3, + ), + ), + ); + }, + ); + } + + Widget _buillItem(int idx) { + final name = names[idx]; + final selected = idxVN.value == idx; + final color = selected ? null : Colors.grey; + + final Widget child; + if (idx == 0) { + child = Padding( + padding: const EdgeInsets.symmetric(horizontal: 13), + child: Icon(MingCute.add_circle_fill, size: 17, color: color), + ); + } else { + final text = Text( + name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(color: color), + softWrap: false, + textAlign: TextAlign.center, + textWidthBasis: TextWidthBasis.parent, + ); + child = AnimatedContainer( + width: selected ? 90 : 50, + duration: Durations.medium3, + curve: Curves.fastEaseInToSlowEaseOut, + child: switch (selected) { + true => Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox(width: 55, child: text), + if (selected) + FadeIn( + child: IconBtn( + icon: MingCute.close_circle_fill, + color: color, + onTap: () => onClose(name), + ), + ), + ], + ), + false => Center(child: text), + }, + ).paddingOnly(left: 3, right: 3); + } + + return InkWell( + borderRadius: BorderRadius.circular(13), + onTap: () => onTap(idx), + child: child, + ).paddingSymmetric(horizontal: 13); + } +} diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 12d9d675..fb05ea71 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -471,7 +471,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 936; + CURRENT_PROJECT_VERSION = 943; DEVELOPMENT_TEAM = BA88US33G6; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Server Box"; @@ -481,7 +481,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 1.0.936; + MARKETING_VERSION = 1.0.943; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_NAME = "Server Box"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -608,7 +608,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 936; + CURRENT_PROJECT_VERSION = 943; DEVELOPMENT_TEAM = BA88US33G6; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Server Box"; @@ -618,7 +618,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 1.0.936; + MARKETING_VERSION = 1.0.943; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_NAME = "Server Box"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -638,7 +638,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "3rd Party Mac Developer Application"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 936; + CURRENT_PROJECT_VERSION = 943; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=macosx*]" = BA88US33G6; INFOPLIST_FILE = Runner/Info.plist; @@ -649,7 +649,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 1.0.936; + MARKETING_VERSION = 1.0.943; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_NAME = "Server Box"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/pubspec.lock b/pubspec.lock index 743b14b9..14b30215 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -401,8 +401,8 @@ packages: dependency: "direct main" description: path: "." - ref: "v1.0.33" - resolved-ref: "7c4fdde33ec7c9ee226bfe0fd0c148f2d3f3ca54" + ref: "v1.0.39" + resolved-ref: "49fc10b39e390f4ecc3ee4f16f0926460b77adac" url: "https://github.com/lppcg/fl_lib" source: git version: "0.0.1" diff --git a/pubspec.yaml b/pubspec.yaml index 626bcc1d..cfe5a168 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -53,7 +53,7 @@ dependencies: fl_lib: git: url: https://github.com/lppcg/fl_lib - ref: v1.0.33 + ref: v1.0.39 dependency_overrides: # dartssh2: diff --git a/test/container_test.dart b/test/container_test.dart new file mode 100644 index 00000000..16a355af --- /dev/null +++ b/test/container_test.dart @@ -0,0 +1,32 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:toolbox/data/model/container/ps.dart'; + +void main() { + test('docker ps parse', () { + const raw = ''' +CONTAINER ID STATUS NAMES IMAGE +0e9e2ef860d2 Up 2 hours hbbs rustdesk/rustdesk-server:latest +9a4df3ed340c Up 41 minutes hbbr rustdesk/rustdesk-server:latest +fa1215b4be74 Up 12 hours firefly uusec/firefly:latest +'''; + final lines = raw.split('\n'); + const ids = ['0e9e2ef860d2', '9a4df3ed340c', 'fa1215b4be74']; + const names = ['hbbs', 'hbbr', 'firefly']; + const images = [ + 'rustdesk/rustdesk-server:latest', + 'rustdesk/rustdesk-server:latest', + 'uusec/firefly:latest' + ]; + const states = ['Up 2 hours', 'Up 41 minutes', 'Up 12 hours']; + for (var idx = 1; idx < lines.length; idx++) { + final raw = lines[idx]; + if (raw.isEmpty) continue; + final ps = DockerPs.parse(raw); + expect(ps.id, ids[idx - 1]); + expect(ps.names, names[idx - 1]); + expect(ps.image, images[idx - 1]); + expect(ps.state, states[idx - 1]); + expect(ps.running, true); + } + }); +}