diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 57a07cb0..5ee4b9a6 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -586,7 +586,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 634; + CURRENT_PROJECT_VERSION = 636; DEVELOPMENT_TEAM = BA88US33G6; ENABLE_BITCODE = NO; INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist"; @@ -596,7 +596,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.634; + MARKETING_VERSION = 1.0.636; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -720,7 +720,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 634; + CURRENT_PROJECT_VERSION = 636; DEVELOPMENT_TEAM = BA88US33G6; ENABLE_BITCODE = NO; INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist"; @@ -730,7 +730,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.634; + MARKETING_VERSION = 1.0.636; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -748,7 +748,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 634; + CURRENT_PROJECT_VERSION = 636; DEVELOPMENT_TEAM = BA88US33G6; ENABLE_BITCODE = NO; INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist"; @@ -758,7 +758,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.634; + MARKETING_VERSION = 1.0.636; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -779,7 +779,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 634; + CURRENT_PROJECT_VERSION = 636; DEVELOPMENT_TEAM = BA88US33G6; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -792,7 +792,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.634; + MARKETING_VERSION = 1.0.636; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget; @@ -818,7 +818,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 634; + CURRENT_PROJECT_VERSION = 636; DEVELOPMENT_TEAM = BA88US33G6; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -831,7 +831,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.634; + MARKETING_VERSION = 1.0.636; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -854,7 +854,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 634; + CURRENT_PROJECT_VERSION = 636; DEVELOPMENT_TEAM = BA88US33G6; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -867,7 +867,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.634; + MARKETING_VERSION = 1.0.636; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -890,7 +890,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 634; + CURRENT_PROJECT_VERSION = 636; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = BA88US33G6; ENABLE_PREVIEWS = YES; @@ -902,7 +902,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.634; + MARKETING_VERSION = 1.0.636; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd; @@ -931,7 +931,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 634; + CURRENT_PROJECT_VERSION = 636; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = BA88US33G6; ENABLE_PREVIEWS = YES; @@ -943,7 +943,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.634; + MARKETING_VERSION = 1.0.636; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd; PRODUCT_NAME = ServerBox; @@ -969,7 +969,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 634; + CURRENT_PROJECT_VERSION = 636; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = BA88US33G6; ENABLE_PREVIEWS = YES; @@ -981,7 +981,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.634; + MARKETING_VERSION = 1.0.636; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd; PRODUCT_NAME = ServerBox; diff --git a/lib/core/route.dart b/lib/core/route.dart index f36dbaff..cba00e6a 100644 --- a/lib/core/route.dart +++ b/lib/core/route.dart @@ -12,7 +12,7 @@ import 'package:toolbox/view/page/server/detail.dart'; import 'package:toolbox/view/page/setting/android.dart'; import 'package:toolbox/view/page/setting/ios.dart'; import 'package:toolbox/view/page/snippet/result.dart'; -import 'package:toolbox/view/page/ssh_term.dart'; +import 'package:toolbox/view/page/ssh/page.dart'; import 'package:toolbox/view/page/setting/virt_key.dart'; import 'package:toolbox/view/page/storage/local.dart'; diff --git a/lib/data/model/app/tab.dart b/lib/data/model/app/tab.dart index cbe34b2f..336e78fa 100644 --- a/lib/data/model/app/tab.dart +++ b/lib/data/model/app/tab.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; -import 'package:toolbox/view/page/ping.dart'; import 'package:toolbox/view/page/server/tab.dart'; import 'package:toolbox/view/page/snippet/list.dart'; +import 'package:toolbox/view/page/ssh/tab.dart'; enum AppTab { server, + ssh, snippet, - ping; + ; Widget get page { switch (this) { @@ -14,8 +15,8 @@ enum AppTab { return const ServerPage(); case snippet: return const SnippetListPage(); - case ping: - return const PingPage(); + case ssh: + return const SSHTabPage(); } } } diff --git a/lib/data/provider/server.dart b/lib/data/provider/server.dart index ecb73edd..27463d38 100644 --- a/lib/data/provider/server.dart +++ b/lib/data/provider/server.dart @@ -21,10 +21,8 @@ import '../model/server/snippet.dart'; import '../model/server/try_limiter.dart'; import '../res/status.dart'; -typedef ServersMap = Map; - class ServerProvider extends ChangeNotifier { - final ServersMap _servers = {}; + final Map _servers = {}; Iterable get servers => _servers.values; final Order _serverOrder = []; Order get serverOrder => _serverOrder; @@ -102,7 +100,7 @@ class ServerProvider extends ChangeNotifier { } Server genServer(ServerPrivateInfo spi) { - return Server(spi, InitStatus.status, null, ServerState.disconnected); + return Server(spi, InitStatus.status, ServerState.disconnected); } /// if [spi] is specificed then only refresh this server diff --git a/lib/data/res/build_data.dart b/lib/data/res/build_data.dart index 55337501..b839744e 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 = 634; + static const int build = 636; static const String engine = "3.13.8"; - static const String buildAt = "2023-11-03 22:14:11"; + static const String buildAt = "2023-11-07 19:05:38"; static const int modifications = 2; - static const int script = 25; + static const int script = 26; } diff --git a/lib/view/page/home.dart b/lib/view/page/home.dart index a0e78b92..73e53774 100644 --- a/lib/view/page/home.dart +++ b/lib/view/page/home.dart @@ -111,7 +111,16 @@ class _HomePageState extends State return Scaffold( drawer: _buildDrawer(), - appBar: _buildAppBar(), + appBar: CustomAppBar( + title: const Text(BuildData.name), + actions: [ + IconButton( + icon: const Icon(Icons.developer_mode, size: 23), + tooltip: l10n.debug, + onPressed: () => AppRoute.debug().go(context), + ), + ], + ), body: PageView.builder( controller: _pageController, itemCount: AppTab.values.length, @@ -129,37 +138,6 @@ class _HomePageState extends State ); } - PreferredSizeWidget _buildAppBar() { - final actions = [ - IconButton( - icon: const Icon(Icons.developer_mode, size: 23), - tooltip: l10n.debug, - onPressed: () => AppRoute.debug().go(context), - ), - ]; - if (isDesktop && _selectIndex.value == AppTab.server.index) { - actions.add( - ValueBuilder( - listenable: _selectIndex, - build: () { - if (_selectIndex.value != AppTab.server.index) { - return const SizedBox(); - } - return IconButton( - icon: const Icon(Icons.refresh, size: 23), - tooltip: 'Refresh', - onPressed: () => Pros.server.refreshData(onlyFailed: true), - ); - }, - ), - ); - } - return CustomAppBar( - title: const Text(BuildData.name), - actions: actions, - ); - } - Widget _buildBottomBar() { return NavigationBar( selectedIndex: _selectIndex.value, @@ -185,16 +163,16 @@ class _HomePageState extends State label: l10n.server, selectedIcon: const Icon(Icons.cloud), ), + const NavigationDestination( + icon: Icon(Icons.terminal_outlined), + label: 'SSH', + selectedIcon: Icon(Icons.terminal), + ), NavigationDestination( icon: const Icon(Icons.snippet_folder_outlined), label: l10n.snippet, selectedIcon: const Icon(Icons.snippet_folder), ), - const NavigationDestination( - icon: Icon(Icons.network_check_outlined), - label: 'Ping', - selectedIcon: Icon(Icons.network_check), - ), ], ); } diff --git a/lib/view/page/ssh_term.dart b/lib/view/page/ssh/page.dart similarity index 87% rename from lib/view/page/ssh_term.dart rename to lib/view/page/ssh/page.dart index 082b70cd..79fde63f 100644 --- a/lib/view/page/ssh_term.dart +++ b/lib/view/page/ssh/page.dart @@ -12,6 +12,7 @@ import 'package:toolbox/core/extension/context/locale.dart'; import 'package:toolbox/core/extension/context/snackbar.dart'; import 'package:toolbox/core/utils/platform/base.dart'; import 'package:toolbox/core/utils/share.dart'; +import 'package:toolbox/data/model/server/server.dart'; import 'package:toolbox/data/model/server/snippet.dart'; import 'package:toolbox/data/provider/virtual_keyboard.dart'; import 'package:toolbox/data/res/provider.dart'; @@ -19,13 +20,12 @@ import 'package:toolbox/data/res/store.dart'; import 'package:xterm/core.dart'; import 'package:xterm/ui.dart' hide TerminalThemes; -import '../../core/route.dart'; -import '../../core/utils/misc.dart'; -import '../../core/utils/server.dart'; -import '../../data/model/server/server_private_info.dart'; -import '../../data/model/ssh/virtual_key.dart'; -import '../../data/res/color.dart'; -import '../../data/res/terminal.dart'; +import '../../../core/route.dart'; +import '../../../core/utils/misc.dart'; +import '../../../data/model/server/server_private_info.dart'; +import '../../../data/model/ssh/virtual_key.dart'; +import '../../../data/res/color.dart'; +import '../../../data/res/terminal.dart'; const echoPWD = 'echo \$PWD'; @@ -38,7 +38,7 @@ class SSHPage extends StatefulWidget { _SSHPageState createState() => _SSHPageState(); } -class _SSHPageState extends State { +class _SSHPageState extends State with AutomaticKeepAliveClientMixin { final _keyboard = VirtKeyProvider(); late final _terminal = Terminal(inputHandler: _keyboard); final TerminalController _terminalController = TerminalController(); @@ -53,8 +53,9 @@ class _SSHPageState extends State { bool _isDark = false; Timer? _virtKeyLongPressTimer; - SSHClient? _client; - SSHSession? _session; + late final Server? _server = widget.spi.server; + late final SSHClient? _client = _server?.client; + late final SSHSession? _session; Timer? _discontinuityTimer; @override @@ -76,11 +77,13 @@ class _SSHPageState extends State { super.dispose(); _virtKeyLongPressTimer?.cancel(); _terminalController.dispose(); - if (_client?.isClosed == false) { - try { - _client?.close(); - } catch (_) {} - } + + /// Use the same [SSHClient], so don't close it + // if (_client?.isClosed == false) { + // try { + // _client?.close(); + // } catch (_) {} + // } _discontinuityTimer?.cancel(); } @@ -100,6 +103,7 @@ class _SSHPageState extends State { @override Widget build(BuildContext context) { + super.build(context); Widget child = Scaffold( backgroundColor: _terminalTheme.background, body: _buildBody(), @@ -320,26 +324,26 @@ class _SSHPageState extends State { Future _initTerminal() async { _write('Connecting...\r\n'); - _client = await genClient( - widget.spi, - onStatus: (p0) { - switch (p0) { - case GenSSHClientStatus.socket: - _write('Destination: ${widget.spi.id}'); - return _write('Establishing socket...'); - case GenSSHClientStatus.key: - return _write('Using private key to connect...'); - case GenSSHClientStatus.pwd: - return _write('Sending password to auth...'); - } - }, - timeout: Stores.setting.timeoutD, - ); - _write('Connected\r\n'); + // _client = await genClient( + // widget.spi, + // onStatus: (p0) { + // switch (p0) { + // case GenSSHClientStatus.socket: + // _write('Destination: ${widget.spi.id}'); + // return _write('Establishing socket...'); + // case GenSSHClientStatus.key: + // return _write('Using private key to connect...'); + // case GenSSHClientStatus.pwd: + // return _write('Sending password to auth...'); + // } + // }, + // timeout: Stores.setting.timeoutD, + // ); + // _write('Connected\r\n'); _write('Terminal size: ${_terminal.viewWidth}x${_terminal.viewHeight}\r\n'); _write('Starting shell...\r\n'); - _session = await _client!.shell( + _session = await _client?.shell( pty: SSHPtyConfig( width: _terminal.viewWidth, height: _terminal.viewHeight, @@ -353,8 +357,8 @@ class _SSHPageState extends State { return; } - _terminal.buffer.clear(); - _terminal.buffer.setCursor(0, 0); + // _terminal.buffer.clear(); + // _terminal.buffer.setCursor(0, 0); _terminal.onOutput = (data) { _session?.write(utf8.encode(data) as Uint8List); @@ -424,4 +428,7 @@ class _SSHPageState extends State { ], ); } + + @override + bool get wantKeepAlive => true; } diff --git a/lib/view/page/ssh/tab.dart b/lib/view/page/ssh/tab.dart new file mode 100644 index 00000000..439849d2 --- /dev/null +++ b/lib/view/page/ssh/tab.dart @@ -0,0 +1,110 @@ +import 'package:flutter/material.dart'; +import 'package:toolbox/core/extension/context/dialog.dart'; +import 'package:toolbox/data/model/server/server.dart'; +import 'package:toolbox/data/res/provider.dart'; +import 'package:toolbox/view/page/ssh/page.dart'; + +class SSHTabPage extends StatefulWidget { + const SSHTabPage({super.key}); + + @override + _SSHTabPageState createState() => _SSHTabPageState(); +} + +class _SSHTabPageState extends State + with TickerProviderStateMixin, AutomaticKeepAliveClientMixin { + final _tabIds = {}; + final _tabKeys = {}; + late var _tabController = TabController( + length: _tabIds.length, + vsync: this, + ); + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + appBar: TabBar( + controller: _tabController, + tabs: _tabIds.keys.map(_buildTabItem).toList(), + isScrollable: true, + ), + body: _buildBody(), + floatingActionButton: _buildFAB(), + ); + } + + Widget _buildTabItem(String e) { + return Tab( + child: Row( + children: [ + Text(e), + IconButton( + icon: const Icon(Icons.close), + padding: EdgeInsets.zero, + onPressed: () { + _tabKeys[e]?.currentState?.dispose(); + _tabIds.remove(e); + _refreshTabs(); + }, + ), + ], + ), + ); + } + + Widget _buildFAB() { + return FloatingActionButton( + onPressed: () async { + final spi = (await context.showPickDialog( + items: Pros.server.servers.toList(), + name: (e) => e.spi.name, + multi: false, + )) + ?.first + .spi; + if (spi == null) { + return; + } + final name = () { + if (_tabIds.containsKey(spi.name)) { + return '${spi.name}(${_tabIds.length + 1})'; + } + return spi.name; + }(); + final key = GlobalKey(debugLabel: 'sshTabPage_$name'); + _tabIds[name] = SSHPage( + key: key, + spi: spi, + ); + _tabKeys[name] = key; + _refreshTabs(); + _tabController.animateTo(_tabIds.length - 1); + }, + child: const Icon(Icons.add), + ); + } + + Widget _buildBody() { + if (_tabIds.isEmpty) { + return const Center( + child: Text('Click the fab to open a session'), + ); + } + return TabBarView( + controller: _tabController, + children: _tabIds.values.toList(), + ); + } + + void _refreshTabs() { + _tabController = TabController( + length: _tabIds.length, + vsync: this, + ); + setState(() {}); + } + + @override + bool get wantKeepAlive => true; +}