From faacbe088b05194ea729884bb967fd58ed539b23 Mon Sep 17 00:00:00 2001 From: lollipopkit Date: Thu, 9 May 2024 12:31:59 +0800 Subject: [PATCH] feat: auto hide fab --- lib/core/utils/server.dart | 14 +-- lib/core/utils/{auth.dart => ssh_auth.dart} | 0 lib/data/provider/server.dart | 2 +- lib/view/page/home/home.dart | 43 ++++--- lib/view/page/server/tab.dart | 125 ++++++++++---------- lib/view/page/ssh/page.dart | 2 +- lib/view/widget/appbar.dart | 1 + lib/view/widget/auto_hide_fab.dart | 86 ++++++++++++++ 8 files changed, 186 insertions(+), 87 deletions(-) rename lib/core/utils/{auth.dart => ssh_auth.dart} (100%) create mode 100644 lib/view/widget/auto_hide_fab.dart diff --git a/lib/core/utils/server.dart b/lib/core/utils/server.dart index 433064a1..a6292b34 100644 --- a/lib/core/utils/server.dart +++ b/lib/core/utils/server.dart @@ -45,16 +45,16 @@ Future genClient( ServerPrivateInfo spi, { void Function(GenSSHClientStatus)? onStatus, - /// Must pass this param when use multi-thread and key login + /// Only pass this param if using multi-threading and key login String? privateKey, - /// Must pass this param when use multi-thread and key login + /// Only pass this param if using multi-threading and key login String? jumpPrivateKey, Duration timeout = const Duration(seconds: 5), /// [ServerPrivateInfo] of the jump server /// - /// Must pass this param when use multi-thread and key login + /// Must pass this param if using multi-threading and key login ServerPrivateInfo? jumpSpi, /// Handle keyboard-interactive authentication @@ -113,8 +113,8 @@ Future genClient( username: spi.user, onPasswordRequest: () => spi.pwd, onUserInfoRequest: onKeyboardInteractive, - printDebug: debugPrint, - printTrace: debugPrint, + // printDebug: debugPrint, + // printTrace: debugPrint, ); } privateKey ??= getPrivateKey(keyId); @@ -126,7 +126,7 @@ Future genClient( // Must use [compute] here, instead of [Computer.shared.start] identities: await compute(loadIndentity, privateKey), onUserInfoRequest: onKeyboardInteractive, - printDebug: debugPrint, - printTrace: debugPrint, + // printDebug: debugPrint, + // printTrace: debugPrint, ); } diff --git a/lib/core/utils/auth.dart b/lib/core/utils/ssh_auth.dart similarity index 100% rename from lib/core/utils/auth.dart rename to lib/core/utils/ssh_auth.dart diff --git a/lib/data/provider/server.dart b/lib/data/provider/server.dart index b77a5e2d..11eccac0 100644 --- a/lib/data/provider/server.dart +++ b/lib/data/provider/server.dart @@ -6,7 +6,7 @@ import 'package:dartssh2/dartssh2.dart'; import 'package:flutter/material.dart'; import 'package:toolbox/core/extension/ssh_client.dart'; import 'package:toolbox/core/extension/stringx.dart'; -import 'package:toolbox/core/utils/auth.dart'; +import 'package:toolbox/core/utils/ssh_auth.dart'; import 'package:toolbox/core/utils/platform/path.dart'; import 'package:toolbox/data/model/app/shell_func.dart'; import 'package:toolbox/data/model/server/system.dart'; diff --git a/lib/view/page/home/home.dart b/lib/view/page/home/home.dart index 44142130..1bda878e 100644 --- a/lib/view/page/home/home.dart +++ b/lib/view/page/home/home.dart @@ -127,22 +127,37 @@ class _HomePageState extends State super.build(context); Pros.app.ctx = context; + final appBar = widget.landscape + ? null + : _AppBar( + selectIndex: _selectIndex, + centerTitle: false, + title: const Text(BuildData.name), + actions: [ + ValBuilder( + listenable: + Stores.setting.serverStatusUpdateInterval.listenable(), + builder: (interval) { + if (interval != 0) return UIs.placeholder; + return IconButton( + icon: const Icon(Icons.refresh), + tooltip: 'Refresh', + onPressed: () async { + await Pros.server.refresh(); + }, + ); + }, + ), + IconButton( + icon: const Icon(Icons.developer_mode, size: 21), + tooltip: l10n.debug, + onPressed: () => AppRoute.debug().go(context), + ), + ], + ); return Scaffold( drawer: _buildDrawer(), - appBar: widget.landscape - ? null - : _AppBar( - selectIndex: _selectIndex, - centerTitle: false, - title: const Text(BuildData.name), - actions: [ - IconButton( - icon: const Icon(Icons.developer_mode, size: 21), - tooltip: l10n.debug, - onPressed: () => AppRoute.debug().go(context), - ), - ], - ), + appBar: appBar, body: PageView.builder( controller: _pageController, itemCount: AppTab.values.length, diff --git a/lib/view/page/server/tab.dart b/lib/view/page/server/tab.dart index a578e6ad..57fa512c 100644 --- a/lib/view/page/server/tab.dart +++ b/lib/view/page/server/tab.dart @@ -12,13 +12,13 @@ import 'package:toolbox/core/extension/listx.dart'; import 'package:toolbox/core/extension/media_queryx.dart'; import 'package:toolbox/core/extension/numx.dart'; import 'package:toolbox/core/extension/ssh_client.dart'; -import 'package:toolbox/core/utils/platform/base.dart'; import 'package:toolbox/core/utils/share.dart'; import 'package:toolbox/data/model/app/shell_func.dart'; import 'package:toolbox/data/model/server/try_limiter.dart'; import 'package:toolbox/data/res/color.dart'; import 'package:toolbox/data/res/provider.dart'; import 'package:toolbox/data/res/store.dart'; +import 'package:toolbox/view/widget/auto_hide_fab.dart'; import 'package:toolbox/view/widget/percent_circle.dart'; import '../../../core/route.dart'; @@ -53,6 +53,8 @@ class _ServerPageState extends State String? _tag; bool _useDoubleColumn = false; + final _scrollController = ScrollController(); + @override void initState() { super.initState(); @@ -99,11 +101,14 @@ class _ServerPageState extends State return _buildBody(); }, ), - floatingActionButton: FloatingActionButton( - heroTag: 'addServer', - onPressed: () => AppRoute.serverEdit().go(context), - tooltip: l10n.addAServer, - child: const Icon(Icons.add), + floatingActionButton: AutoHideFab( + controller: _scrollController, + child: FloatingActionButton( + heroTag: 'addServer', + onPressed: () => AppRoute.serverEdit().go(context), + tooltip: l10n.addAServer, + child: const Icon(Icons.add), + ), ), ); } @@ -201,13 +206,7 @@ class _ServerPageState extends State }, ); - // Desktop doesn't support pull to refresh - if (isDesktop) return child; - - return RefreshIndicator( - onRefresh: () async => await Pros.server.refresh(onlyFailed: true), - child: child, - ); + return child; } TagSwitcher _buildTagsSwitcher(ServerProvider provider) { @@ -228,6 +227,7 @@ class _ServerPageState extends State }) { final count = filtered.length + 1; return ListView.builder( + controller: _scrollController, padding: padding, itemCount: count, itemBuilder: (_, index) { @@ -461,66 +461,63 @@ class _ServerPageState extends State } Widget _buildTopRightWidget(Server s) { - Widget rightCorner = UIs.placeholder; - if (s.state == ServerState.connecting) { - rightCorner = Padding( - padding: const EdgeInsets.symmetric(horizontal: 7), - child: SizedBox( - width: 21, - height: 21, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(primaryColor), + return switch (s.state) { + ServerState.connecting || + ServerState.loading || + ServerState.connected => + Padding( + padding: const EdgeInsets.symmetric(horizontal: 7), + child: SizedBox( + width: 19, + height: 19, + child: CircularProgressIndicator( + strokeWidth: 3, + valueColor: AlwaysStoppedAnimation(primaryColor), + ), ), ), - ); - } else if (s.state == ServerState.failed) { - rightCorner = InkWell( - onTap: () { - TryLimiter.reset(s.spi.id); - Pros.server.refresh(spi: s.spi); - }, - child: const Padding( - padding: EdgeInsets.symmetric(horizontal: 7), - child: Icon( - Icons.refresh, - size: 21, - color: Colors.grey, + ServerState.failed => InkWell( + onTap: () { + TryLimiter.reset(s.spi.id); + Pros.server.refresh(spi: s.spi); + }, + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 7), + child: Icon( + Icons.refresh, + size: 21, + color: Colors.grey, + ), ), ), - ); - } else if (!(s.spi.autoConnect ?? true) && - s.state == ServerState.disconnected) { - rightCorner = InkWell( - onTap: () => Pros.server.refresh(spi: s.spi), - child: const Padding( - padding: EdgeInsets.symmetric(horizontal: 7), - child: Icon( - Icons.link, - size: 21, - color: Colors.grey, + ServerState.disconnected when !(s.spi.autoConnect ?? true) => InkWell( + onTap: () => Pros.server.refresh(spi: s.spi), + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 7), + child: Icon( + Icons.link, + size: 21, + color: Colors.grey, + ), ), ), - ); - } else if (Stores.setting.serverTabUseOldUI.fetch()) { - rightCorner = ServerFuncBtnsTopRight(spi: s.spi); - } - return rightCorner; + _ when Stores.setting.serverTabUseOldUI.fetch() => + ServerFuncBtnsTopRight(spi: s.spi), + _ => UIs.placeholder, + }; } Widget _buildTopRightText(Server s) { - if (s.state == ServerState.failed && s.status.err != null) { - return GestureDetector( - onTap: () => _showFailReason(s.status), - child: Text( - l10n.viewErr, - style: UIs.text13Grey, - ), - ); - } - return Text( - s.getTopRightStr(s.spi), - style: UIs.text13Grey, + final hasErr = s.state == ServerState.failed && s.status.err != null; + return GestureDetector( + onTap: () { + if (!hasErr) return; + _showFailReason(s.status); + }, + child: Text( + hasErr ? l10n.viewErr : s.getTopRightStr(s.spi), + style: UIs.text13Grey, + ), ); } diff --git a/lib/view/page/ssh/page.dart b/lib/view/page/ssh/page.dart index fc34f022..14f804ab 100644 --- a/lib/view/page/ssh/page.dart +++ b/lib/view/page/ssh/page.dart @@ -9,7 +9,7 @@ import 'package:provider/provider.dart'; 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/utils/auth.dart'; +import 'package:toolbox/core/utils/ssh_auth.dart'; import 'package:toolbox/core/utils/platform/base.dart'; import 'package:toolbox/core/utils/server.dart'; import 'package:toolbox/core/utils/share.dart'; diff --git a/lib/view/widget/appbar.dart b/lib/view/widget/appbar.dart index cd039f42..c3ed0fe7 100644 --- a/lib/view/widget/appbar.dart +++ b/lib/view/widget/appbar.dart @@ -5,6 +5,7 @@ import 'package:toolbox/data/res/store.dart'; import 'package:window_manager/window_manager.dart'; class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { + /// System status bar height static double? barHeight; static bool drawTitlebar = false; diff --git a/lib/view/widget/auto_hide_fab.dart b/lib/view/widget/auto_hide_fab.dart new file mode 100644 index 00000000..089e6527 --- /dev/null +++ b/lib/view/widget/auto_hide_fab.dart @@ -0,0 +1,86 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +final class AutoHideFab extends StatefulWidget { + final Widget child; + final ScrollController controller; + + const AutoHideFab({ + super.key, + required this.child, + required this.controller, + }); + + @override + State createState() => _AutoHideFabState(); +} + +final class _AutoHideFabState extends State { + bool _visible = true; + bool _isScrolling = false; + Timer? _timer; + + @override + void initState() { + super.initState(); + widget.controller.addListener(_scrollListener); + } + + @override + void dispose() { + widget.controller.removeListener(_scrollListener); + _timer?.cancel(); + _timer = null; + super.dispose(); + } + + void _setupTimer() { + if (_timer != null) { + _timer!.cancel(); + _timer = null; + } + _timer = Timer.periodic(const Duration(seconds: 3), (_) { + if (_isScrolling) return; + if (!_visible) return; + if (widget.controller.position.maxScrollExtent <= 0) return; + setState(() { + _visible = false; + }); + }); + } + + void _scrollListener() { + if (_isScrolling) return; + _isScrolling = true; + if (widget.controller.position.userScrollDirection == + ScrollDirection.reverse) { + if (_visible) { + setState(() { + _visible = false; + }); + _timer?.cancel(); + _timer = null; + } + } else { + if (!_visible) { + setState(() { + _visible = true; + }); + _setupTimer(); + } + } + _isScrolling = false; + } + + @override + Widget build(BuildContext context) { + return AnimatedContainer( + duration: Durations.medium1, + curve: Curves.easeInOutCubic, + transform: Matrix4.translationValues(_visible ? 0.0 : 55, 0.0, 0.0), + child: widget.child, + ); + } +}