feat: auto hide fab

This commit is contained in:
lollipopkit
2024-05-09 12:31:59 +08:00
parent f70449d67d
commit faacbe088b
8 changed files with 186 additions and 87 deletions

View File

@@ -45,16 +45,16 @@ Future<SSHClient> 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<SSHClient> 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<SSHClient> 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,
);
}

View File

@@ -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';

View File

@@ -127,22 +127,37 @@ class _HomePageState extends State<HomePage>
super.build(context);
Pros.app.ctx = context;
final appBar = widget.landscape
? null
: _AppBar(
selectIndex: _selectIndex,
centerTitle: false,
title: const Text(BuildData.name),
actions: <Widget>[
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: <Widget>[
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,

View File

@@ -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<ServerPage>
String? _tag;
bool _useDoubleColumn = false;
final _scrollController = ScrollController();
@override
void initState() {
super.initState();
@@ -99,11 +101,14 @@ class _ServerPageState extends State<ServerPage>
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<ServerPage>
},
);
// 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<ServerPage>
}) {
final count = filtered.length + 1;
return ListView.builder(
controller: _scrollController,
padding: padding,
itemCount: count,
itemBuilder: (_, index) {
@@ -461,66 +461,63 @@ class _ServerPageState extends State<ServerPage>
}
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,
),
);
}

View File

@@ -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';

View File

@@ -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;

View File

@@ -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<AutoHideFab> createState() => _AutoHideFabState();
}
final class _AutoHideFabState extends State<AutoHideFab> {
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,
);
}
}