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, {
|
ServerPrivateInfo spi, {
|
||||||
void Function(GenSSHClientStatus)? onStatus,
|
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,
|
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,
|
String? jumpPrivateKey,
|
||||||
Duration timeout = const Duration(seconds: 5),
|
Duration timeout = const Duration(seconds: 5),
|
||||||
|
|
||||||
/// [ServerPrivateInfo] of the jump server
|
/// [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,
|
ServerPrivateInfo? jumpSpi,
|
||||||
|
|
||||||
/// Handle keyboard-interactive authentication
|
/// Handle keyboard-interactive authentication
|
||||||
@@ -113,8 +113,8 @@ Future<SSHClient> genClient(
|
|||||||
username: spi.user,
|
username: spi.user,
|
||||||
onPasswordRequest: () => spi.pwd,
|
onPasswordRequest: () => spi.pwd,
|
||||||
onUserInfoRequest: onKeyboardInteractive,
|
onUserInfoRequest: onKeyboardInteractive,
|
||||||
printDebug: debugPrint,
|
// printDebug: debugPrint,
|
||||||
printTrace: debugPrint,
|
// printTrace: debugPrint,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
privateKey ??= getPrivateKey(keyId);
|
privateKey ??= getPrivateKey(keyId);
|
||||||
@@ -126,7 +126,7 @@ Future<SSHClient> genClient(
|
|||||||
// Must use [compute] here, instead of [Computer.shared.start]
|
// Must use [compute] here, instead of [Computer.shared.start]
|
||||||
identities: await compute(loadIndentity, privateKey),
|
identities: await compute(loadIndentity, privateKey),
|
||||||
onUserInfoRequest: onKeyboardInteractive,
|
onUserInfoRequest: onKeyboardInteractive,
|
||||||
printDebug: debugPrint,
|
// printDebug: debugPrint,
|
||||||
printTrace: debugPrint,
|
// printTrace: debugPrint,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import 'package:dartssh2/dartssh2.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:toolbox/core/extension/ssh_client.dart';
|
import 'package:toolbox/core/extension/ssh_client.dart';
|
||||||
import 'package:toolbox/core/extension/stringx.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/core/utils/platform/path.dart';
|
||||||
import 'package:toolbox/data/model/app/shell_func.dart';
|
import 'package:toolbox/data/model/app/shell_func.dart';
|
||||||
import 'package:toolbox/data/model/server/system.dart';
|
import 'package:toolbox/data/model/server/system.dart';
|
||||||
|
|||||||
@@ -127,22 +127,37 @@ class _HomePageState extends State<HomePage>
|
|||||||
super.build(context);
|
super.build(context);
|
||||||
Pros.app.ctx = 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(
|
return Scaffold(
|
||||||
drawer: _buildDrawer(),
|
drawer: _buildDrawer(),
|
||||||
appBar: widget.landscape
|
appBar: appBar,
|
||||||
? 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),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: PageView.builder(
|
body: PageView.builder(
|
||||||
controller: _pageController,
|
controller: _pageController,
|
||||||
itemCount: AppTab.values.length,
|
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/media_queryx.dart';
|
||||||
import 'package:toolbox/core/extension/numx.dart';
|
import 'package:toolbox/core/extension/numx.dart';
|
||||||
import 'package:toolbox/core/extension/ssh_client.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/core/utils/share.dart';
|
||||||
import 'package:toolbox/data/model/app/shell_func.dart';
|
import 'package:toolbox/data/model/app/shell_func.dart';
|
||||||
import 'package:toolbox/data/model/server/try_limiter.dart';
|
import 'package:toolbox/data/model/server/try_limiter.dart';
|
||||||
import 'package:toolbox/data/res/color.dart';
|
import 'package:toolbox/data/res/color.dart';
|
||||||
import 'package:toolbox/data/res/provider.dart';
|
import 'package:toolbox/data/res/provider.dart';
|
||||||
import 'package:toolbox/data/res/store.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 'package:toolbox/view/widget/percent_circle.dart';
|
||||||
|
|
||||||
import '../../../core/route.dart';
|
import '../../../core/route.dart';
|
||||||
@@ -53,6 +53,8 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
String? _tag;
|
String? _tag;
|
||||||
bool _useDoubleColumn = false;
|
bool _useDoubleColumn = false;
|
||||||
|
|
||||||
|
final _scrollController = ScrollController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -99,11 +101,14 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
return _buildBody();
|
return _buildBody();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: AutoHideFab(
|
||||||
heroTag: 'addServer',
|
controller: _scrollController,
|
||||||
onPressed: () => AppRoute.serverEdit().go(context),
|
child: FloatingActionButton(
|
||||||
tooltip: l10n.addAServer,
|
heroTag: 'addServer',
|
||||||
child: const Icon(Icons.add),
|
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
|
return child;
|
||||||
if (isDesktop) return child;
|
|
||||||
|
|
||||||
return RefreshIndicator(
|
|
||||||
onRefresh: () async => await Pros.server.refresh(onlyFailed: true),
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TagSwitcher _buildTagsSwitcher(ServerProvider provider) {
|
TagSwitcher _buildTagsSwitcher(ServerProvider provider) {
|
||||||
@@ -228,6 +227,7 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
}) {
|
}) {
|
||||||
final count = filtered.length + 1;
|
final count = filtered.length + 1;
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
|
controller: _scrollController,
|
||||||
padding: padding,
|
padding: padding,
|
||||||
itemCount: count,
|
itemCount: count,
|
||||||
itemBuilder: (_, index) {
|
itemBuilder: (_, index) {
|
||||||
@@ -461,66 +461,63 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTopRightWidget(Server s) {
|
Widget _buildTopRightWidget(Server s) {
|
||||||
Widget rightCorner = UIs.placeholder;
|
return switch (s.state) {
|
||||||
if (s.state == ServerState.connecting) {
|
ServerState.connecting ||
|
||||||
rightCorner = Padding(
|
ServerState.loading ||
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 7),
|
ServerState.connected =>
|
||||||
child: SizedBox(
|
Padding(
|
||||||
width: 21,
|
padding: const EdgeInsets.symmetric(horizontal: 7),
|
||||||
height: 21,
|
child: SizedBox(
|
||||||
child: CircularProgressIndicator(
|
width: 19,
|
||||||
strokeWidth: 2,
|
height: 19,
|
||||||
valueColor: AlwaysStoppedAnimation(primaryColor),
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 3,
|
||||||
|
valueColor: AlwaysStoppedAnimation(primaryColor),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
ServerState.failed => InkWell(
|
||||||
} else if (s.state == ServerState.failed) {
|
onTap: () {
|
||||||
rightCorner = InkWell(
|
TryLimiter.reset(s.spi.id);
|
||||||
onTap: () {
|
Pros.server.refresh(spi: s.spi);
|
||||||
TryLimiter.reset(s.spi.id);
|
},
|
||||||
Pros.server.refresh(spi: s.spi);
|
child: const Padding(
|
||||||
},
|
padding: EdgeInsets.symmetric(horizontal: 7),
|
||||||
child: const Padding(
|
child: Icon(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 7),
|
Icons.refresh,
|
||||||
child: Icon(
|
size: 21,
|
||||||
Icons.refresh,
|
color: Colors.grey,
|
||||||
size: 21,
|
),
|
||||||
color: Colors.grey,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
ServerState.disconnected when !(s.spi.autoConnect ?? true) => InkWell(
|
||||||
} else if (!(s.spi.autoConnect ?? true) &&
|
onTap: () => Pros.server.refresh(spi: s.spi),
|
||||||
s.state == ServerState.disconnected) {
|
child: const Padding(
|
||||||
rightCorner = InkWell(
|
padding: EdgeInsets.symmetric(horizontal: 7),
|
||||||
onTap: () => Pros.server.refresh(spi: s.spi),
|
child: Icon(
|
||||||
child: const Padding(
|
Icons.link,
|
||||||
padding: EdgeInsets.symmetric(horizontal: 7),
|
size: 21,
|
||||||
child: Icon(
|
color: Colors.grey,
|
||||||
Icons.link,
|
),
|
||||||
size: 21,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
_ when Stores.setting.serverTabUseOldUI.fetch() =>
|
||||||
} else if (Stores.setting.serverTabUseOldUI.fetch()) {
|
ServerFuncBtnsTopRight(spi: s.spi),
|
||||||
rightCorner = ServerFuncBtnsTopRight(spi: s.spi);
|
_ => UIs.placeholder,
|
||||||
}
|
};
|
||||||
return rightCorner;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTopRightText(Server s) {
|
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(
|
return GestureDetector(
|
||||||
onTap: () => _showFailReason(s.status),
|
onTap: () {
|
||||||
child: Text(
|
if (!hasErr) return;
|
||||||
l10n.viewErr,
|
_showFailReason(s.status);
|
||||||
style: UIs.text13Grey,
|
},
|
||||||
),
|
child: Text(
|
||||||
);
|
hasErr ? l10n.viewErr : s.getTopRightStr(s.spi),
|
||||||
}
|
style: UIs.text13Grey,
|
||||||
return Text(
|
),
|
||||||
s.getTopRightStr(s.spi),
|
|
||||||
style: UIs.text13Grey,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:toolbox/core/extension/context/common.dart';
|
import 'package:toolbox/core/extension/context/common.dart';
|
||||||
import 'package:toolbox/core/extension/context/dialog.dart';
|
import 'package:toolbox/core/extension/context/dialog.dart';
|
||||||
import 'package:toolbox/core/extension/context/locale.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/platform/base.dart';
|
||||||
import 'package:toolbox/core/utils/server.dart';
|
import 'package:toolbox/core/utils/server.dart';
|
||||||
import 'package:toolbox/core/utils/share.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';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
|
class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
|
/// System status bar height
|
||||||
static double? barHeight;
|
static double? barHeight;
|
||||||
static bool drawTitlebar = false;
|
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