mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 07:14:28 +01:00
feat: auto hide fab
This commit is contained in:
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -127,22 +127,37 @@ class _HomePageState extends State<HomePage>
|
||||
super.build(context);
|
||||
Pros.app.ctx = context;
|
||||
|
||||
return Scaffold(
|
||||
drawer: _buildDrawer(),
|
||||
appBar: widget.landscape
|
||||
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: appBar,
|
||||
body: PageView.builder(
|
||||
controller: _pageController,
|
||||
itemCount: AppTab.values.length,
|
||||
|
||||
@@ -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,12 +101,15 @@ class _ServerPageState extends State<ServerPage>
|
||||
return _buildBody();
|
||||
},
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
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,21 +461,22 @@ class _ServerPageState extends State<ServerPage>
|
||||
}
|
||||
|
||||
Widget _buildTopRightWidget(Server s) {
|
||||
Widget rightCorner = UIs.placeholder;
|
||||
if (s.state == ServerState.connecting) {
|
||||
rightCorner = Padding(
|
||||
return switch (s.state) {
|
||||
ServerState.connecting ||
|
||||
ServerState.loading ||
|
||||
ServerState.connected =>
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 7),
|
||||
child: SizedBox(
|
||||
width: 21,
|
||||
height: 21,
|
||||
width: 19,
|
||||
height: 19,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
strokeWidth: 3,
|
||||
valueColor: AlwaysStoppedAnimation(primaryColor),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (s.state == ServerState.failed) {
|
||||
rightCorner = InkWell(
|
||||
),
|
||||
ServerState.failed => InkWell(
|
||||
onTap: () {
|
||||
TryLimiter.reset(s.spi.id);
|
||||
Pros.server.refresh(spi: s.spi);
|
||||
@@ -488,10 +489,8 @@ class _ServerPageState extends State<ServerPage>
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (!(s.spi.autoConnect ?? true) &&
|
||||
s.state == ServerState.disconnected) {
|
||||
rightCorner = InkWell(
|
||||
),
|
||||
ServerState.disconnected when !(s.spi.autoConnect ?? true) => InkWell(
|
||||
onTap: () => Pros.server.refresh(spi: s.spi),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 7),
|
||||
@@ -501,28 +500,26 @@ class _ServerPageState extends State<ServerPage>
|
||||
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) {
|
||||
final hasErr = s.state == ServerState.failed && s.status.err != null;
|
||||
return GestureDetector(
|
||||
onTap: () => _showFailReason(s.status),
|
||||
onTap: () {
|
||||
if (!hasErr) return;
|
||||
_showFailReason(s.status);
|
||||
},
|
||||
child: Text(
|
||||
l10n.viewErr,
|
||||
hasErr ? l10n.viewErr : s.getTopRightStr(s.spi),
|
||||
style: UIs.text13Grey,
|
||||
),
|
||||
);
|
||||
}
|
||||
return Text(
|
||||
s.getTopRightStr(s.spi),
|
||||
style: UIs.text13Grey,
|
||||
);
|
||||
}
|
||||
|
||||
void _showFailReason(ServerStatus ss) {
|
||||
context.showRoundDialog(
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
86
lib/view/widget/auto_hide_fab.dart
Normal file
86
lib/view/widget/auto_hide_fab.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user