optimization: desktop UI (#747)

This commit is contained in:
lollipopkit🏳️‍⚧️
2025-05-13 04:57:37 +08:00
committed by GitHub
parent e520929411
commit 8627ff823f
52 changed files with 2459 additions and 1990 deletions

View File

@@ -98,7 +98,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
app_links: 76b66b60cc809390ca1ad69bfd66b998d2387ac7
camera_avfoundation: 04b44aeb14070126c6529e5ab82cc7c9fca107cf
camera_avfoundation: be3be85408cd4126f250386828e9b1dfa40ab436
file_picker: fb04e739ae6239a76ce1f571863a196a922c87d4
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
@@ -112,7 +112,7 @@ SPEC CHECKSUMS:
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
wakelock_plus: 04623e3f525556020ebd4034310f20fe7fda8b49
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
watch_connectivity: 88e5bea25b473e66ef8d3f960954d154ed0356d6
PODFILE CHECKSUM: ec6ef69056f066e8b21a3391082f23b5ad2d37f8

View File

@@ -2,12 +2,13 @@ import 'package:dynamic_color/dynamic_color.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:fl_lib/generated/l10n/lib_l10n.dart';
import 'package:flutter/material.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/res/build_data.dart';
import 'package:server_box/data/res/store.dart';
import 'package:server_box/generated/l10n/l10n.dart';
import 'package:server_box/view/page/home/home.dart';
import 'package:server_box/view/page/home.dart';
import 'package:icons_plus/icons_plus.dart';
part 'intro.dart';
@@ -22,47 +23,67 @@ class MyApp extends StatelessWidget {
listenable: RNodes.app,
builder: (context, _) {
if (!Stores.setting.useSystemPrimaryColor.fetch()) {
final colorSeed = Color(Stores.setting.colorSeed.fetch());
UIs.colorSeed = colorSeed;
UIs.primaryColor = colorSeed;
return _buildApp(
context,
light: ThemeData(
useMaterial3: true,
colorSchemeSeed: UIs.colorSeed,
),
dark: ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorSchemeSeed: UIs.colorSeed,
),
);
return _build(context);
}
return DynamicColorBuilder(
builder: (light, dark) {
final lightTheme = ThemeData(
useMaterial3: true,
colorScheme: light,
);
final darkTheme = ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorScheme: dark,
);
if (context.isDark && dark != null) {
UIs.primaryColor = dark.primary;
} else if (!context.isDark && light != null) {
UIs.primaryColor = light.primary;
}
return _buildApp(context, light: lightTheme, dark: darkTheme);
},
);
return _buildDynamicColor(context);
},
);
}
Widget _buildApp(BuildContext ctx,
{required ThemeData light, required ThemeData dark}) {
Widget _build(BuildContext context) {
final colorSeed = Color(Stores.setting.colorSeed.fetch());
UIs.colorSeed = colorSeed;
UIs.primaryColor = colorSeed;
return _buildApp(
context,
light: ThemeData(
useMaterial3: true,
colorSchemeSeed: UIs.colorSeed,
appBarTheme: AppBarTheme(
scrolledUnderElevation: 0.0,
),
),
dark: ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorSchemeSeed: UIs.colorSeed,
appBarTheme: AppBarTheme(
scrolledUnderElevation: 0.0,
),
),
);
}
Widget _buildDynamicColor(BuildContext context) {
return DynamicColorBuilder(
builder: (light, dark) {
final lightTheme = ThemeData(
useMaterial3: true,
colorScheme: light,
);
final darkTheme = ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorScheme: dark,
);
if (context.isDark && dark != null) {
UIs.primaryColor = dark.primary;
} else if (!context.isDark && light != null) {
UIs.primaryColor = light.primary;
}
return _buildApp(context, light: lightTheme, dark: darkTheme);
},
);
}
Widget _buildApp(
BuildContext ctx, {
required ThemeData light,
required ThemeData dark,
}) {
final tMode = Stores.setting.themeMode.fetch();
// Issue #57
final themeMode = switch (tMode) {
@@ -74,6 +95,14 @@ class MyApp extends StatelessWidget {
return MaterialApp(
key: ValueKey(locale),
builder: (context, child) => ResponsiveBreakpoints.builder(
child: child ?? UIs.placeholder,
breakpoints: const [
Breakpoint(start: 0, end: 450, name: MOBILE),
Breakpoint(start: 451, end: 800, name: TABLET),
Breakpoint(start: 801, end: 1920, name: DESKTOP),
],
),
locale: locale,
localizationsDelegates: const [
LibLocalizations.delegate,
@@ -86,21 +115,25 @@ class MyApp extends StatelessWidget {
themeMode: themeMode,
theme: light.fixWindowsFont,
darkTheme: (tMode < 3 ? dark : dark.toAmoled).fixWindowsFont,
home: VirtualWindowFrame(
child: Builder(
builder: (context) {
context.setLibL10n();
final appL10n = AppLocalizations.of(context);
if (appL10n != null) l10n = appL10n;
home: Builder(
builder: (context) {
context.setLibL10n();
final appL10n = AppLocalizations.of(context);
if (appL10n != null) l10n = appL10n;
final intros = _IntroPage.builders;
if (intros.isNotEmpty) {
return _IntroPage(intros);
}
Widget child;
final intros = _IntroPage.builders;
if (intros.isNotEmpty) {
child = _IntroPage(intros);
}
return const HomePage();
},
),
child = const HomePage();
return VirtualWindowFrame(
title: BuildData.name,
child: child,
);
},
),
);
}

View File

@@ -1,154 +1,9 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:server_box/data/model/server/private_key_info.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/res/store.dart';
import 'package:server_box/view/page/container.dart';
import 'package:server_box/view/page/home/home.dart';
import 'package:server_box/view/page/ping.dart';
import 'package:server_box/view/page/private_key/edit.dart';
import 'package:server_box/view/page/server/detail/view.dart';
import 'package:server_box/view/page/setting/platform/android.dart';
import 'package:server_box/view/page/setting/platform/ios.dart';
import 'package:server_box/view/page/snippet/result.dart';
import 'package:server_box/view/page/ssh/page.dart';
import 'package:server_box/view/page/setting/seq/virt_key.dart';
import 'package:server_box/data/model/server/snippet.dart';
import 'package:server_box/view/page/process.dart';
import 'package:server_box/view/page/server/tab.dart';
import 'package:server_box/view/page/setting/seq/srv_detail_seq.dart';
import 'package:server_box/view/page/setting/seq/srv_seq.dart';
import 'package:server_box/view/page/snippet/edit.dart';
import 'package:server_box/view/page/storage/sftp.dart';
import 'package:server_box/view/page/storage/sftp_mission.dart';
class AppRoutes {
final Widget page;
final String title;
/// The args class for [AppRoute].
final class SpiRequiredArgs {
/// The only required argument for this class.
final Spi spi;
AppRoutes(this.page, this.title);
Future<T?> go<T>(BuildContext context) {
return Navigator.push<T>(
context,
Stores.setting.cupertinoRoute.fetch()
? CupertinoPageRoute(builder: (context) => page)
: MaterialPageRoute(builder: (context) => page),
);
}
Future<T?> checkGo<T>({
required BuildContext context,
required bool Function() check,
}) {
if (check()) {
return go(context);
}
return Future.value(null);
}
static AppRoutes serverDetail({Key? key, required Spi spi}) {
return AppRoutes(ServerDetailPage(key: key, spi: spi), 'server_detail');
}
static AppRoutes serverTab({Key? key}) {
return AppRoutes(ServerPage(key: key), 'server_tab');
}
static AppRoutes keyEdit({Key? key, PrivateKeyInfo? pki}) {
return AppRoutes(
PrivateKeyEditPage(pki: pki),
'key_${pki == null ? 'add' : 'edit'}',
);
}
static AppRoutes snippetEdit({Key? key, Snippet? snippet}) {
return AppRoutes(
SnippetEditPage(snippet: snippet),
'snippet_${snippet == null ? 'add' : 'edit'}',
);
}
static AppRoutes ssh({
Key? key,
required Spi spi,
String? initCmd,
Snippet? initSnippet,
}) {
return AppRoutes(
SSHPage(
key: key,
spi: spi,
initCmd: initCmd,
initSnippet: initSnippet,
),
'ssh_term',
);
}
static AppRoutes sshVirtKeySetting({Key? key}) {
return AppRoutes(SSHVirtKeySettingPage(key: key), 'ssh_virt_key_setting');
}
static AppRoutes sftpMission({Key? key}) {
return AppRoutes(SftpMissionPage(key: key), 'sftp_mission');
}
static AppRoutes sftp(
{Key? key, required Spi spi, String? initPath, bool isSelect = false}) {
return AppRoutes(
SftpPage(
key: key,
spi: spi,
initPath: initPath,
isSelect: isSelect,
),
'sftp');
}
static AppRoutes docker({Key? key, required Spi spi}) {
return AppRoutes(ContainerPage(key: key, spi: spi), 'docker');
}
// static AppRoutes fullscreen({Key? key}) {
// return AppRoutes(FullScreenPage(key: key), 'fullscreen');
// }
static AppRoutes home({Key? key}) {
return AppRoutes(HomePage(key: key), 'home');
}
static AppRoutes ping({Key? key}) {
return AppRoutes(PingPage(key: key), 'ping');
}
static AppRoutes process({Key? key, required Spi spi}) {
return AppRoutes(ProcessPage(key: key, spi: spi), 'process');
}
static AppRoutes serverOrder({Key? key}) {
return AppRoutes(ServerOrderPage(key: key), 'server_order');
}
static AppRoutes serverDetailOrder({Key? key}) {
return AppRoutes(ServerDetailOrderPage(key: key), 'server_detail_order');
}
static AppRoutes iosSettings({Key? key}) {
return AppRoutes(IOSSettingsPage(key: key), 'ios_setting');
}
static AppRoutes androidSettings({Key? key}) {
return AppRoutes(AndroidSettingsPage(key: key), 'android_setting');
}
static AppRoutes snippetResult(
{Key? key, required List<SnippetResult?> results}) {
return AppRoutes(
SnippetResultPage(
key: key,
results: results,
),
'snippet_result');
}
const SpiRequiredArgs(this.spi);
}

View File

@@ -1,7 +1,7 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/view/page/server/tab.dart';
import 'package:server_box/view/page/server/tab/tab.dart';
// import 'package:server_box/view/page/setting/entry.dart';
import 'package:server_box/view/page/snippet/list.dart';
import 'package:server_box/view/page/ssh/tab.dart';

View File

@@ -1,5 +1,6 @@
import 'dart:convert';
import 'package:equatable/equatable.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:json_annotation/json_annotation.dart';
@@ -20,7 +21,7 @@ part 'server_private_info.g.dart';
/// Nowaday, more fields are added to this class, and it's renamed to `Spi`.
@JsonSerializable()
@HiveType(typeId: 3)
class Spi {
class Spi with EquatableMixin {
@HiveField(0)
final String name;
@HiveField(1)
@@ -81,6 +82,10 @@ class Spi {
@override
String toString() => id;
@override
List<Object?> get props =>
[name, ip, port, user, pwd, keyId, tags, alterUrl, autoConnect, jumpId, custom, wolCfg, envs];
}
extension Spix on Spi {

View File

@@ -1,5 +1,6 @@
import 'dart:async';
import 'package:equatable/equatable.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:json_annotation/json_annotation.dart';
@@ -10,7 +11,7 @@ part 'snippet.g.dart';
@JsonSerializable()
@HiveType(typeId: 2)
class Snippet {
class Snippet with EquatableMixin {
@HiveField(0)
final String name;
@HiveField(1)
@@ -32,11 +33,21 @@ class Snippet {
this.autoRunOn,
});
factory Snippet.fromJson(Map<String, dynamic> json) =>
_$SnippetFromJson(json);
factory Snippet.fromJson(Map<String, dynamic> json) => _$SnippetFromJson(json);
Map<String, dynamic> toJson() => _$SnippetToJson(this);
@override
List<Object?> get props => [
name,
script,
tags,
note,
autoRunOn,
];
}
extension SnippetX on Snippet {
static final fmtFinder = RegExp(r'\$\{[^{}]+\}');
String fmtWithSpi(Spi spi) {

View File

@@ -1,7 +1,27 @@
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
final class AppProvider {
const AppProvider._();
part 'app.g.dart';
part 'app.freezed.dart';
static BuildContext? ctx;
@freezed
class AppState with _$AppState {
const factory AppState({
@Default(false) bool desktopMode,
}) = _AppState;
}
@Riverpod(keepAlive: true)
class AppProvider extends _$AppProvider {
static BuildContext? ctx;
@override
AppState build() {
return const AppState();
}
void setDesktop(bool desktopMode) {
state = state.copyWith(desktopMode: desktopMode);
}
}

View File

@@ -0,0 +1,144 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'app.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
/// @nodoc
mixin _$AppState {
bool get desktopMode => throw _privateConstructorUsedError;
/// Create a copy of AppState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$AppStateCopyWith<AppState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $AppStateCopyWith<$Res> {
factory $AppStateCopyWith(AppState value, $Res Function(AppState) then) =
_$AppStateCopyWithImpl<$Res, AppState>;
@useResult
$Res call({bool desktopMode});
}
/// @nodoc
class _$AppStateCopyWithImpl<$Res, $Val extends AppState>
implements $AppStateCopyWith<$Res> {
_$AppStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of AppState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? desktopMode = null,
}) {
return _then(_value.copyWith(
desktopMode: null == desktopMode
? _value.desktopMode
: desktopMode // ignore: cast_nullable_to_non_nullable
as bool,
) as $Val);
}
}
/// @nodoc
abstract class _$$AppStateImplCopyWith<$Res>
implements $AppStateCopyWith<$Res> {
factory _$$AppStateImplCopyWith(
_$AppStateImpl value, $Res Function(_$AppStateImpl) then) =
__$$AppStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({bool desktopMode});
}
/// @nodoc
class __$$AppStateImplCopyWithImpl<$Res>
extends _$AppStateCopyWithImpl<$Res, _$AppStateImpl>
implements _$$AppStateImplCopyWith<$Res> {
__$$AppStateImplCopyWithImpl(
_$AppStateImpl _value, $Res Function(_$AppStateImpl) _then)
: super(_value, _then);
/// Create a copy of AppState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? desktopMode = null,
}) {
return _then(_$AppStateImpl(
desktopMode: null == desktopMode
? _value.desktopMode
: desktopMode // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// @nodoc
class _$AppStateImpl implements _AppState {
const _$AppStateImpl({this.desktopMode = false});
@override
@JsonKey()
final bool desktopMode;
@override
String toString() {
return 'AppState(desktopMode: $desktopMode)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$AppStateImpl &&
(identical(other.desktopMode, desktopMode) ||
other.desktopMode == desktopMode));
}
@override
int get hashCode => Object.hash(runtimeType, desktopMode);
/// Create a copy of AppState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$AppStateImplCopyWith<_$AppStateImpl> get copyWith =>
__$$AppStateImplCopyWithImpl<_$AppStateImpl>(this, _$identity);
}
abstract class _AppState implements AppState {
const factory _AppState({final bool desktopMode}) = _$AppStateImpl;
@override
bool get desktopMode;
/// Create a copy of AppState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$AppStateImplCopyWith<_$AppStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,24 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'app.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$appProviderHash() => r'8378ec9d0a9c8d99cc05805047cd2d52ac4dbb56';
/// See also [AppProvider].
@ProviderFor(AppProvider)
final appProviderProvider = NotifierProvider<AppProvider, AppState>.internal(
AppProvider.new,
name: r'appProviderProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$appProviderHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$AppProvider = Notifier<AppState>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -14,15 +14,14 @@ class SnippetStore extends HiveStore {
}
List<Snippet> fetch() {
final keys = box.keys;
final ss = <Snippet>[];
for (final key in keys) {
final ss = <Snippet>{};
for (final key in keys()) {
final s = box.get(key);
if (s != null && s is Snippet) {
ss.add(s);
}
}
return ss;
return ss.toList();
}
void delete(Snippet s) {

View File

@@ -20,6 +20,11 @@ class BackupPage extends StatefulWidget {
@override
State<BackupPage> createState() => _BackupPageState();
static const route = AppRouteNoArg(
page: BackupPage.new,
path: '/backup',
);
}
final class _BackupPageState extends State<BackupPage>
@@ -246,7 +251,7 @@ final class _BackupPageState extends State<BackupPage>
onTap: () async {
final data = await context.showImportDialog(
title: l10n.snippet,
modelDef: Snippet.example.toJson(),
modelDef: SnippetX.example.toJson(),
);
if (data == null) return;
final str = String.fromCharCodes(data);

View File

@@ -14,22 +14,28 @@ import 'package:server_box/data/res/store.dart';
import 'package:server_box/data/model/container/ps.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/provider/container.dart';
import 'package:server_box/view/page/ssh/page.dart';
import 'package:server_box/view/widget/two_line_text.dart';
class ContainerPage extends StatefulWidget {
final Spi spi;
const ContainerPage({required this.spi, super.key});
final SpiRequiredArgs args;
const ContainerPage({required this.args, super.key});
@override
State<ContainerPage> createState() => _ContainerPageState();
static const route = AppRouteArg(
page: ContainerPage.new,
path: '/container',
);
}
class _ContainerPageState extends State<ContainerPage> {
final _textController = TextEditingController();
late final _container = ContainerProvider(
client: widget.spi.server?.value.client,
userName: widget.spi.user,
hostId: widget.spi.id,
client: widget.args.spi.server?.value.client,
userName: widget.args.spi.user,
hostId: widget.args.spi.id,
context: context,
);
late Size _size;
@@ -55,27 +61,23 @@ class _ContainerPageState extends State<ContainerPage> {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => _container,
builder: (_, __) => Consumer<ContainerProvider>(
builder: (_, ___, __) {
return Scaffold(
appBar: CustomAppBar(
centerTitle: true,
title: TwoLineText(up: l10n.container, down: widget.spi.name),
actions: [
IconButton(
onPressed: () =>
context.showLoadingDialog(fn: () => _container.refresh()),
icon: const Icon(Icons.refresh),
)
],
),
body: _buildMain(),
floatingActionButton: _container.error == null ? _buildFAB() : null,
);
},
),
return Consumer<ContainerProvider>(
builder: (_, ___, __) {
return Scaffold(
appBar: CustomAppBar(
centerTitle: true,
title: TwoLineText(up: l10n.container, down: widget.args.spi.name),
actions: [
IconButton(
onPressed: () => context.showLoadingDialog(fn: () => _container.refresh()),
icon: const Icon(Icons.refresh),
)
],
),
body: _buildMain(),
floatingActionButton: _container.error == null ? _buildFAB() : null,
);
},
);
}
@@ -234,8 +236,7 @@ class _ContainerPageState extends State<ContainerPage> {
),
Row(
children: [
_buildPsItemStatsItem(
'Mem', item.mem, Icons.settings_input_component),
_buildPsItemStatsItem('Mem', item.mem, Icons.settings_input_component),
UIs.width13,
_buildPsItemStatsItem('Disk', item.disk, Icons.storage),
],
@@ -263,9 +264,7 @@ class _ContainerPageState extends State<ContainerPage> {
Widget _buildMoreBtn(ContainerPs dItem) {
return PopupMenu(
items: ContainerMenu.items(dItem.running)
.map((e) => PopMenu.build(e, e.icon, e.toStr))
.toList(),
items: ContainerMenu.items(dItem.running).map((e) => PopMenu.build(e, e.icon, e.toStr)).toList(),
onSelected: (item) => _onTapMoreBtn(item, dItem),
);
}
@@ -410,7 +409,7 @@ class _ContainerPageState extends State<ContainerPage> {
}
Future<void> _showEditHostDialog() async {
final id = widget.spi.id;
final id = widget.args.spi.id;
final host = Stores.container.fetch(id);
final ctrl = TextEditingController(text: host);
await context.showRoundDialog(
@@ -428,7 +427,7 @@ class _ContainerPageState extends State<ContainerPage> {
void _onSaveDockerHost(String val) {
context.pop();
Stores.container.put(widget.spi.id, val.trim());
Stores.container.put(widget.args.spi.id, val.trim());
_container.refresh();
}
@@ -537,22 +536,24 @@ class _ContainerPageState extends State<ContainerPage> {
}
break;
case ContainerMenu.logs:
AppRoutes.ssh(
spi: widget.spi,
final args = SshPageArgs(
spi: widget.args.spi,
initCmd: '${switch (_container.type) {
ContainerType.podman => 'podman',
ContainerType.docker => 'docker',
}} logs -f --tail 100 ${dItem.id}',
).go(context);
);
SSHPage.route.go(context, args);
break;
case ContainerMenu.terminal:
AppRoutes.ssh(
spi: widget.spi,
final args = SshPageArgs(
spi: widget.args.spi,
initCmd: '${switch (_container.type) {
ContainerType.podman => 'podman',
ContainerType.docker => 'docker',
}} exec -it ${dItem.id} sh',
).go(context);
);
SSHPage.route.go(context, args);
break;
// case DockerMenuType.stats:
// showRoundDialog(

View File

@@ -1,6 +1,6 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:server_box/core/chan.dart';
import 'package:server_box/data/model/app/tab.dart';
import 'package:server_box/data/provider/app.dart';
@@ -8,22 +8,23 @@ import 'package:server_box/data/provider/server.dart';
import 'package:server_box/data/res/build_data.dart';
import 'package:server_box/data/res/store.dart';
import 'package:server_box/data/res/url.dart';
import 'package:server_box/view/page/setting/entry.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
part 'appbar.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
static const route = AppRouteNoArg(
page: HomePage.new,
path: '/',
);
}
class _HomePageState extends State<HomePage>
with
AutomaticKeepAliveClientMixin,
AfterLayoutMixin,
WidgetsBindingObserver {
with AutomaticKeepAliveClientMixin, AfterLayoutMixin, WidgetsBindingObserver {
late final PageController _pageController;
final _selectIndex = ValueNotifier(0);
@@ -92,98 +93,81 @@ class _HomePageState extends State<HomePage>
Widget build(BuildContext context) {
super.build(context);
AppProvider.ctx = context;
final sysPadding = MediaQuery.of(context).padding;
final isMobile = ResponsiveBreakpoints.of(context).isMobile;
return ColoredBox(
color: context.theme.colorScheme.surface,
child: AdaptiveLayout(
transitionDuration: const Duration(milliseconds: 250),
primaryNavigation: SlotLayout(
config: {
Breakpoints.medium: SlotLayout.from(
key: const Key('primaryNavigation'),
builder: (context) => _buildRailBar(),
return Scaffold(
appBar: _AppBar(MediaQuery.paddingOf(context).top),
body: Row(
children: [
if (!isMobile) _buildRailBar(),
Expanded(
child: PageView.builder(
controller: _pageController,
itemCount: AppTab.values.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (_, index) => AppTab.values[index].page,
onPageChanged: (value) {
FocusScope.of(context).unfocus();
if (!_switchingPage) {
_selectIndex.value = value;
}
},
),
Breakpoints.mediumLarge: SlotLayout.from(
key: const Key('MediumLarge primaryNavigation'),
builder: (context) => _buildRailBar(extended: true),
),
Breakpoints.large: SlotLayout.from(
key: const Key('Large primaryNavigation'),
builder: (context) => _buildRailBar(extended: true),
),
Breakpoints.extraLarge: SlotLayout.from(
key: const Key('ExtraLarge primaryNavigation'),
builder: (context) => _buildRailBar(extended: true),
),
},
),
body: SlotLayout(
config: {
Breakpoint.standard(): SlotLayout.from(
key: const Key('body'),
builder: (context) => Scaffold(
appBar: _AppBar(sysPadding.top),
body: PageView.builder(
controller: _pageController,
itemCount: AppTab.values.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (_, index) => AppTab.values[index].page,
onPageChanged: (value) {
FocusScope.of(context).unfocus();
if (!_switchingPage) {
_selectIndex.value = value;
}
},
),
),
),
},
),
bottomNavigation: SlotLayout(
config: {
Breakpoints.small: SlotLayout.from(
key: const Key('bottomNavigation'),
builder: (context) => _buildBottomBar(),
),
},
),
),
],
),
bottomNavigationBar: isMobile ? _buildBottomBar() : null,
);
}
Widget _buildBottomBar() {
return Stores.setting.fullScreen.fetch()
? UIs.placeholder
: ListenableBuilder(
listenable: _selectIndex,
builder: (context, child) => NavigationBar(
selectedIndex: _selectIndex.value,
height: kBottomNavigationBarHeight * 1.1,
animationDuration: const Duration(milliseconds: 250),
onDestinationSelected: _onDestinationSelected,
labelBehavior:
NavigationDestinationLabelBehavior.onlyShowSelected,
destinations: AppTab.navDestinations,
),
);
if (Stores.setting.fullScreen.fetch()) return UIs.placeholder;
return ListenableBuilder(
listenable: _selectIndex,
builder: (context, child) => NavigationBar(
selectedIndex: _selectIndex.value,
height: kBottomNavigationBarHeight * 1.1,
animationDuration: const Duration(milliseconds: 250),
onDestinationSelected: _onDestinationSelected,
labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected,
destinations: AppTab.navDestinations,
),
);
}
Widget _buildRailBar({bool extended = false}) {
return Stores.setting.fullScreen.fetch()
? UIs.placeholder
: ListenableBuilder(
listenable: _selectIndex,
builder: (context, child) =>
AdaptiveScaffold.standardNavigationRail(
extended: extended,
padding: EdgeInsets.only(top: CustomAppBar.sysStatusBarHeight),
selectedIndex: _selectIndex.value,
destinations: AppTab.navRailDestinations,
onDestinationSelected: _onDestinationSelected,
labelType: extended ? null : NavigationRailLabelType.selected,
),
);
final fullscreen = Stores.setting.fullScreen.fetch();
if (fullscreen) return UIs.placeholder;
return Stack(
children: [
_selectIndex.listenVal(
(idx) => NavigationRail(
extended: extended,
minExtendedWidth: 150,
leading: extended ? const SizedBox(height: 20) : null,
trailing: extended ? const SizedBox(height: 20) : null,
labelType: extended ? NavigationRailLabelType.none : NavigationRailLabelType.all,
selectedIndex: idx,
destinations: AppTab.navRailDestinations,
onDestinationSelected: _onDestinationSelected,
),
),
// Settings Btn
Positioned(
bottom: 10,
left: 0,
right: 0,
child: IconButton(
icon: const Icon(Icons.settings),
tooltip: libL10n.setting,
onPressed: () {
SettingsPage.route.go(context);
},
),
),
],
);
}
@override
@@ -231,7 +215,7 @@ class _HomePageState extends State<HomePage>
void _goAuth() {
if (Stores.setting.useBioAuth.fetch()) {
if (BioAuthPage.route.isAlreadyIn) return;
if (BioAuthPage.route.alreadyIn) return;
BioAuthPage.route.go(
context,
args: BioAuthPageArgs(onAuthSuccess: () => _shouldAuth = false),
@@ -253,3 +237,21 @@ class _HomePageState extends State<HomePage>
});
}
}
final class _AppBar extends StatelessWidget implements PreferredSizeWidget {
final double paddingTop;
const _AppBar(this.paddingTop);
@override
Widget build(BuildContext context) {
return SizedBox(
height: preferredSize.height,
);
}
@override
Size get preferredSize {
return Size.fromHeight(paddingTop);
}
}

View File

@@ -1,23 +0,0 @@
part of 'home.dart';
final class _AppBar extends StatelessWidget implements PreferredSizeWidget {
final double paddingTop;
const _AppBar(this.paddingTop);
@override
Widget build(BuildContext context) {
return SizedBox(
height: preferredSize.height,
);
}
@override
Size get preferredSize {
final height = switch (Pfs.type) {
Pfs.macos => paddingTop + CustomAppBar.sysStatusBarHeight,
_ => paddingTop,
};
return Size.fromHeight(height);
}
}

View File

@@ -2,23 +2,18 @@ import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/core/route.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/view/page/ssh/page.dart';
final class IPerfPageArgs {
final Spi spi;
const IPerfPageArgs({required this.spi});
}
class IPerfPage extends StatefulWidget {
final IPerfPageArgs args;
final SpiRequiredArgs args;
const IPerfPage({super.key, required this.args});
@override
State<IPerfPage> createState() => _IPerfPageState();
static const route = AppRouteArg<void, IPerfPageArgs>(
static const route = AppRouteArg<void, SpiRequiredArgs>(
page: IPerfPage.new,
path: '/iperf',
);
@@ -55,10 +50,11 @@ class _IPerfPageState extends State<IPerfPage> {
context.showSnackBar(libL10n.empty);
return;
}
AppRoutes.ssh(
final args = SshPageArgs(
spi: widget.args.spi,
initCmd: 'iperf -c ${_hostCtrl.text} -p ${_portCtrl.text}',
).go(context);
);
SSHPage.route.go(context, args);
},
);
}

View File

@@ -15,6 +15,11 @@ class PingPage extends StatefulWidget {
@override
State<PingPage> createState() => _PingPageState();
static const route = AppRouteNoArg(
page: PingPage.new,
path: '/ping',
);
}
class _PingPageState extends State<PingPage>

View File

@@ -13,13 +13,22 @@ import 'package:server_box/data/model/server/private_key_info.dart';
const _format = 'text/plain';
class PrivateKeyEditPage extends StatefulWidget {
const PrivateKeyEditPage({super.key, this.pki});
final class PrivateKeyEditPageArgs {
final PrivateKeyInfo? pki;
const PrivateKeyEditPageArgs({this.pki});
}
class PrivateKeyEditPage extends StatefulWidget {
final PrivateKeyEditPageArgs? args;
const PrivateKeyEditPage({super.key, this.args});
@override
State<PrivateKeyEditPage> createState() => _PrivateKeyEditPageState();
static const route = AppRoute(
page: PrivateKeyEditPage.new,
path: '/private_key/edit',
);
}
class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
@@ -34,6 +43,8 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
final _loading = ValueNotifier<Widget?>(null);
PrivateKeyInfo? get pki => widget.args?.pki;
@override
void dispose() {
super.dispose();
@@ -49,9 +60,10 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
@override
void initState() {
super.initState();
if (widget.pki != null) {
_nameController.text = widget.pki!.id;
_keyController.text = widget.pki!.key;
final pki = this.pki;
if (pki != null) {
_nameController.text = pki.id;
_keyController.text = pki.key;
} else {
Clipboard.getData(_format).then((value) {
if (value == null) return;
@@ -79,31 +91,34 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
}
CustomAppBar _buildAppBar() {
final actions = [
IconButton(
tooltip: libL10n.delete,
onPressed: () {
context.showRoundDialog(
title: libL10n.attention,
child: Text(libL10n.askContinue(
'${libL10n.delete} ${l10n.privateKey}(${widget.pki!.id})',
)),
actions: Btn.ok(
onTap: () {
PrivateKeyProvider.delete(widget.pki!);
context.pop();
context.pop();
final pki = this.pki;
final actions = pki != null
? [
IconButton(
tooltip: libL10n.delete,
onPressed: () {
context.showRoundDialog(
title: libL10n.attention,
child: Text(libL10n.askContinue(
'${libL10n.delete} ${l10n.privateKey}(${pki.id})',
)),
actions: Btn.ok(
onTap: () {
PrivateKeyProvider.delete(pki);
context.pop();
context.pop();
},
red: true,
).toList,
);
},
red: true,
).toList,
);
},
icon: const Icon(Icons.delete),
)
];
icon: const Icon(Icons.delete),
)
]
: null;
return CustomAppBar(
title: Text(libL10n.edit),
actions: widget.pki == null ? null : actions,
actions: actions,
);
}
@@ -120,8 +135,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
}
Widget _buildBody() {
return ListView(
padding: const EdgeInsets.all(13),
return AutoMultiList(
children: [
Input(
autoFocus: true,
@@ -204,7 +218,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
try {
final decrypted = await Computer.shared.start(decyptPem, [key, pwd]);
final pki = PrivateKeyInfo(id: name, key: decrypted);
final originPki = widget.pki;
final originPki = this.pki;
if (originPki != null) {
PrivateKeyProvider.update(originPki, pki);
} else {

View File

@@ -6,26 +6,30 @@ import 'package:flutter/material.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/res/store.dart';
import 'package:server_box/core/route.dart';
import 'package:server_box/data/model/server/private_key_info.dart';
import 'package:server_box/data/provider/private_key.dart';
import 'package:server_box/view/page/private_key/edit.dart';
class PrivateKeysListPage extends StatefulWidget {
const PrivateKeysListPage({super.key});
@override
State<PrivateKeysListPage> createState() => _PrivateKeyListState();
static const route = AppRouteNoArg(
page: PrivateKeysListPage.new,
path: '/private_key',
);
}
class _PrivateKeyListState extends State<PrivateKeysListPage>
with AfterLayoutMixin {
class _PrivateKeyListState extends State<PrivateKeysListPage> with AfterLayoutMixin {
@override
Widget build(BuildContext context) {
return Scaffold(
body: _buildBody(),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () => AppRoutes.keyEdit().go(context),
onPressed: () => PrivateKeyEditPage.route.go(context),
),
);
}
@@ -36,33 +40,33 @@ class _PrivateKeyListState extends State<PrivateKeysListPage>
if (pkis.isEmpty) {
return Center(child: Text(libL10n.empty));
}
return ListView.builder(
padding: const EdgeInsets.all(13),
itemCount: pkis.length,
itemBuilder: (context, idx) {
final item = pkis[idx];
return CardX(
child: ListTile(
leading: Text(
'#$idx',
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
),
),
title: Text(item.id),
subtitle: Text(item.type ?? l10n.unknown, style: UIs.textGrey),
onTap: () => AppRoutes.keyEdit(pki: item).go(context),
trailing: const Icon(Icons.edit),
),
);
},
);
final children = pkis.map(_buildKeyItem).toList();
return AutoMultiList(children: children);
},
);
}
void autoAddSystemPriavteKey() {
Widget _buildKeyItem(PrivateKeyInfo item) {
return ListTile(
title: Text(item.id),
subtitle: Text(item.type ?? l10n.unknown, style: UIs.textGrey),
onTap: () => PrivateKeyEditPage.route.go(
context,
args: PrivateKeyEditPageArgs(pki: item),
),
trailing: const Icon(Icons.edit),
).cardx;
}
@override
FutureOr<void> afterFirstLayout(BuildContext context) {
_autoAddSystemPriavteKey();
}
}
extension on _PrivateKeyListState {
void _autoAddSystemPriavteKey() async {
// Only trigger on desktop platform and no private key saved
if (isDesktop && Stores.snippet.box.keys.isEmpty) {
final home = Pfs.homeDir;
@@ -71,21 +75,19 @@ class _PrivateKeyListState extends State<PrivateKeysListPage>
if (!idRsaFile.existsSync()) return;
final sysPk = PrivateKeyInfo(
id: 'system',
key: idRsaFile.readAsStringSync(),
key: await idRsaFile.readAsString(),
);
context.showRoundDialog(
title: libL10n.attention,
child: Text(l10n.addSystemPrivateKeyTip),
actions: Btn.ok(onTap: () {
context.pop();
AppRoutes.keyEdit(pki: sysPk).go(context);
PrivateKeyEditPage.route.go(
context,
args: PrivateKeyEditPageArgs(pki: sysPk),
);
}).toList,
);
}
}
@override
FutureOr<void> afterFirstLayout(BuildContext context) {
autoAddSystemPriavteKey();
}
}

View File

@@ -4,6 +4,7 @@ import 'package:dartssh2/dartssh2.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/core/route.dart';
import 'package:server_box/data/res/store.dart';
import 'package:server_box/data/model/app/shell_func.dart';
@@ -12,11 +13,17 @@ import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/view/widget/two_line_text.dart';
class ProcessPage extends StatefulWidget {
final Spi spi;
const ProcessPage({super.key, required this.spi});
final SpiRequiredArgs args;
const ProcessPage({super.key, required this.args});
@override
State<ProcessPage> createState() => _ProcessPageState();
static const route = AppRouteArg(
page: ProcessPage.new,
path: '/process',
);
}
class _ProcessPageState extends State<ProcessPage> {
@@ -43,7 +50,7 @@ class _ProcessPageState extends State<ProcessPage> {
@override
void initState() {
super.initState();
_client = widget.spi.server?.value.client;
_client = widget.args.spi.server?.value.client;
final duration =
Duration(seconds: Stores.setting.serverStatusUpdateInterval.fetch());
_timer = Timer.periodic(duration, (_) => _refresh());
@@ -58,7 +65,7 @@ class _ProcessPageState extends State<ProcessPage> {
Future<void> _refresh() async {
if (mounted) {
final result =
await _client?.run(ShellFunc.process.exec(widget.spi.id)).string;
await _client?.run(ShellFunc.process.exec(widget.args.spi.id)).string;
if (result == null || result.isEmpty) {
context.showSnackBar(libL10n.empty);
return;
@@ -125,7 +132,7 @@ class _ProcessPageState extends State<ProcessPage> {
return Scaffold(
appBar: CustomAppBar(
centerTitle: true,
title: TwoLineText(up: widget.spi.name, down: l10n.process),
title: TwoLineText(up: widget.args.spi.name, down: l10n.process),
actions: actions,
),
body: child,

View File

@@ -24,10 +24,13 @@ final class PvePage extends StatefulWidget {
required this.args,
});
static const route = AppRouteArg<void, PvePageArgs>(page: PvePage.new, path: '/pve');
@override
State<PvePage> createState() => _PvePageState();
static const route = AppRouteArg<void, PvePageArgs>(
page: PvePage.new,
path: '/pve',
);
}
const _kHorziPadding = 11.0;
@@ -454,9 +457,7 @@ extension on _PvePageState {
}
void _initRefreshTimer() {
_timer = Timer.periodic(
Duration(seconds: Stores.setting.serverStatusUpdateInterval.fetch()),
(_) {
_timer = Timer.periodic(Duration(seconds: Stores.setting.serverStatusUpdateInterval.fetch()), (_) {
if (mounted) {
_pve.list();
}

View File

@@ -1,5 +1,83 @@
part of 'view.dart';
extension on _ServerDetailPageState {
void _onTapGpuItem(NvidiaSmiItem item) {
final processes = item.memory.processes;
final displayCount = processes.length > 5 ? 5 : processes.length;
final height = displayCount * 47.0;
context.showRoundDialog(
title: item.name,
child: SizedBox(
width: double.maxFinite,
height: height,
child: ListView.builder(
itemCount: processes.length,
itemBuilder: (_, idx) => _buildGpuProcessItem(processes[idx]),
),
),
actions: Btnx.oks,
);
}
void _nTapGpuProcessItem(NvidiaSmiMemProcess process) {
context.showRoundDialog(
title: '${process.pid}',
titleMaxLines: 1,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
UIs.height13,
Text('Memory: ${process.memory} MiB'),
UIs.height13,
Text('Process: ${process.name}')
],
),
actions: [
TextButton(
onPressed: () => context.pop(),
child: Text(libL10n.close),
)
],
);
}
void _onTapCustomItem(MapEntry<String, String> cmd) {
context.showRoundDialog(
title: cmd.key,
child: SingleChildScrollView(
child: Text(cmd.value, style: UIs.text13Grey),
),
actions: [
TextButton(
onPressed: () => context.pop(),
child: Text(libL10n.close),
),
],
);
}
void _onTapSensorItem(SensorItem si) {
context.showRoundDialog(
title: si.device,
child: SingleChildScrollView(
child: SimpleMarkdown(
data: si.toMarkdown,
styleSheet: MarkdownStyleSheet(
tableBorder: TableBorder.all(color: Colors.grey),
tableHead: const TextStyle(fontWeight: FontWeight.bold),
),
),
),
);
}
void _onTapTemperatureItem(String key) {
Pfs.copy(key);
context.showSnackBar('${libL10n.copy} ${libL10n.success}');
}
}
enum _NetSortType {
device,
trans,
@@ -26,13 +104,9 @@ enum _NetSortType {
case _NetSortType.device:
return (b, a) => a.compareTo(b);
case _NetSortType.recv:
return (b, a) => ns
.speedInBytes(ns.deviceIdx(a))
.compareTo(ns.speedInBytes(ns.deviceIdx(b)));
return (b, a) => ns.speedInBytes(ns.deviceIdx(a)).compareTo(ns.speedInBytes(ns.deviceIdx(b)));
case _NetSortType.trans:
return (b, a) => ns
.speedOutBytes(ns.deviceIdx(a))
.compareTo(ns.speedOutBytes(ns.deviceIdx(b)));
return (b, a) => ns.speedOutBytes(ns.deviceIdx(a)).compareTo(ns.speedOutBytes(ns.deviceIdx(b)));
}
}
}

View File

@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:icons_plus/icons_plus.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/core/route.dart';
import 'package:server_box/data/model/app/server_detail_card.dart';
import 'package:server_box/data/model/app/shell_func.dart';
import 'package:server_box/data/model/server/battery.dart';
@@ -25,16 +26,19 @@ import 'package:server_box/data/model/server/server.dart';
part 'misc.dart';
class ServerDetailPage extends StatefulWidget {
const ServerDetailPage({super.key, required this.spi});
final Spi spi;
final SpiRequiredArgs args;
const ServerDetailPage({super.key, required this.args});
@override
State<ServerDetailPage> createState() => _ServerDetailPageState();
static const route = AppRouteArg(
page: ServerDetailPage.new,
path: '/servers/detail',
);
}
class _ServerDetailPageState extends State<ServerDetailPage>
with SingleTickerProviderStateMixin {
class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerProviderStateMixin {
late final _cardBuildMap = Map.fromIterables(
ServerDetailCards.names,
[
@@ -49,7 +53,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
_buildTemperature,
_buildBatteries,
_buildPve,
_buildCustom,
_buildCustomCmd,
],
);
@@ -83,7 +87,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
@override
Widget build(BuildContext context) {
final s = widget.spi.server;
final s = widget.args.spi.server;
if (s == null) {
return Scaffold(
appBar: CustomAppBar(),
@@ -106,14 +110,10 @@ class _ServerDetailPageState extends State<ServerDetailPage>
children.add(buildFunc(si));
}
}
return Scaffold(
appBar: _buildAppBar(si),
body: ListView(
padding: EdgeInsets.only(
left: 13,
right: 13,
bottom: _media.padding.bottom + 77,
),
body: AutoMultiList(
children: children,
),
);
@@ -121,18 +121,11 @@ class _ServerDetailPageState extends State<ServerDetailPage>
CustomAppBar _buildAppBar(Server si) {
return CustomAppBar(
title: Hero(
tag: 'home_card_title_${si.spi.id}',
transitionOnUserGestures: true,
child: Material(
color: Colors.transparent,
child: Text(
si.spi.name,
style: TextStyle(
fontSize: 20,
color: context.isDark ? Colors.white : Colors.black,
),
),
title: Text(
si.spi.name,
style: TextStyle(
fontSize: 20,
color: context.isDark ? Colors.white : Colors.black,
),
),
actions: [
@@ -144,7 +137,10 @@ class _ServerDetailPageState extends State<ServerDetailPage>
IconButton(
icon: const Icon(Icons.edit),
onPressed: () async {
final delete = await ServerEditPage.route.go(context, args: si.spi);
final delete = await ServerEditPage.route.go(
context,
args: SpiRequiredArgs(si.spi),
);
if (delete == true) {
context.pop();
}
@@ -155,16 +151,14 @@ class _ServerDetailPageState extends State<ServerDetailPage>
}
Widget _buildLogo(Server si) {
var logoUrl = si.spi.custom?.logoUrl ??
_settings.serverLogoUrl.fetch().selfNotEmptyOrNull;
var logoUrl = si.spi.custom?.logoUrl ?? _settings.serverLogoUrl.fetch().selfNotEmptyOrNull;
if (logoUrl == null) return UIs.placeholder;
final dist = si.status.more[StatusCmdType.sys]?.dist;
if (dist != null) {
logoUrl = logoUrl.replaceFirst('{DIST}', dist.name);
}
logoUrl =
logoUrl.replaceFirst('{BRIGHT}', context.isDark ? 'dark' : 'light');
logoUrl = logoUrl.replaceFirst('{BRIGHT}', context.isDark ? 'dark' : 'light');
return Padding(
padding: const EdgeInsets.symmetric(vertical: 13),
@@ -194,8 +188,16 @@ class _ServerDetailPageState extends State<ServerDetailPage>
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(e.key.i18n, style: UIs.text13),
Text(e.value, style: UIs.text13Grey)
Text(
e.key.i18n,
style: UIs.text13,
overflow: TextOverflow.ellipsis,
),
Text(
e.value,
style: UIs.text13Grey,
overflow: TextOverflow.ellipsis,
),
],
),
),
@@ -267,15 +269,15 @@ class _ServerDetailPageState extends State<ServerDetailPage>
}
Widget _buildCpuModelItem(MapEntry<String, int> e) {
final name = e.key
.replaceFirst('Intel(R)', '')
.replaceFirst('AMD', '')
.replaceFirst('with Radeon Graphics', '');
final name =
e.key.replaceFirst('Intel(R)', '').replaceFirst('AMD', '').replaceFirst('with Radeon Graphics', '');
final child = Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox(
width: _media.size.width * .7,
ConstrainedBox(
constraints: BoxConstraints(
maxWidth: _media.size.width * .7,
),
child: Text(
name,
style: UIs.text13,
@@ -283,7 +285,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
maxLines: 1,
),
),
Text('x ${e.value}', style: UIs.text13Grey),
Text('x ${e.value}', style: UIs.text13Grey, overflow: TextOverflow.clip),
],
);
return child.paddingSymmetric(horizontal: 17);
@@ -312,41 +314,47 @@ class _ServerDetailPageState extends State<ServerDetailPage>
List<Widget> _buildCPUProgress(Cpus cs) {
const kMaxColumn = 2;
const kRowThreshold = 4;
const kCoresCount = kMaxColumn * kRowThreshold;
const kCoresCountThreshold = kMaxColumn * kRowThreshold;
final children = <Widget>[];
final displayCpuIndexSetting = Stores.setting.displayCpuIndex.fetch();
if (cs.coresCount > kCoresCount) {
final rows = cs.coresCount ~/ kMaxColumn;
for (var i = 0; i < rows; i++) {
if (cs.coresCount > kCoresCountThreshold) {
final numCoresToDisplay = cs.coresCount - 1;
final numRows = (numCoresToDisplay + kMaxColumn - 1) ~/ kMaxColumn;
for (var i = 0; i < numRows; i++) {
final rowChildren = <Widget>[];
for (var j = 0; j < kMaxColumn; j++) {
final idx = i * kMaxColumn + j + 1;
if (idx >= cs.coresCount) break;
if (Stores.setting.displayCpuIndex.fetch()) {
rowChildren.add(Text('$idx', style: UIs.text13Grey));
final coreListIndex = i * kMaxColumn + j;
if (coreListIndex >= numCoresToDisplay) break;
final coreNumberOneBased = coreListIndex + 1;
if (displayCpuIndexSetting) {
rowChildren.add(Text('$coreNumberOneBased', style: UIs.text13Grey));
}
rowChildren.add(
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: _buildProgress(cs.usedPercent(coreIdx: idx)),
child: _buildProgress(cs.usedPercent(coreIdx: coreNumberOneBased)),
),
),
);
}
rowChildren.joinWith(UIs.width7);
children.add(
Padding(
padding: const EdgeInsets.symmetric(horizontal: 17),
child: Row(
children: rowChildren,
if (rowChildren.isNotEmpty) {
children.add(
Padding(
padding: const EdgeInsets.symmetric(horizontal: 17),
child: Row(
children: rowChildren.joinWith(UIs.width7).toList(),
),
),
),
);
);
}
}
} else {
for (var i = 0; i < cs.coresCount; i++) {
if (i == 0) continue;
for (var i = 1; i < cs.coresCount; i++) {
children.add(
Padding(
padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 17),
@@ -377,6 +385,17 @@ class _ServerDetailPageState extends State<ServerDetailPage>
final used = ss.mem.usedPercent * 100;
final usedStr = used.toStringAsFixed(0);
final percentW = Row(
children: [
_buildAnimatedText(ValueKey(usedStr), '$usedStr%', UIs.text27),
UIs.width7,
Text(
'of ${(ss.mem.total * 1024).bytes2Str}',
style: UIs.text13Grey,
)
],
);
return CardX(
child: Padding(
padding: UIs.roundRectCardPadding,
@@ -387,20 +406,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
_buildAnimatedText(
ValueKey(usedStr),
'$usedStr%',
UIs.text27,
),
UIs.width7,
Text(
'of ${(ss.mem.total * 1024).bytes2Str}',
style: UIs.text13Grey,
)
],
),
percentW,
Row(
children: [
_buildDetailPercent(free, 'free'),
@@ -423,6 +429,18 @@ class _ServerDetailPageState extends State<ServerDetailPage>
if (ss.swap.total == 0) return UIs.placeholder;
final used = ss.swap.usedPercent * 100;
final cached = ss.swap.cached / ss.swap.total * 100;
final percentW = Row(
children: [
Text('${used.toStringAsFixed(0)}%', style: UIs.text27),
UIs.width7,
Text(
'of ${(ss.swap.total * 1024).bytes2Str} ',
style: UIs.text13Grey,
)
],
);
return CardX(
child: Padding(
padding: UIs.roundRectCardPadding,
@@ -433,16 +451,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Text('${used.toStringAsFixed(0)}%', style: UIs.text27),
UIs.width7,
Text(
'of ${(ss.swap.total * 1024).bytes2Str} ',
style: UIs.text13Grey,
)
],
),
percentW,
_buildDetailPercent(cached, 'cached'),
],
),
@@ -470,7 +479,6 @@ class _ServerDetailPageState extends State<ServerDetailPage>
Widget _buildGpuItem(NvidiaSmiItem item) {
final mem = item.memory;
final processes = mem.processes;
return ListTile(
title: Text(item.name, style: UIs.text13),
leading: Text(
@@ -490,32 +498,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: () {
final height = () {
if (processes.length > 5) {
return 5 * 47.0;
}
return processes.length * 47.0;
}();
context.showRoundDialog(
title: item.name,
child: SizedBox(
width: double.maxFinite,
height: height,
child: ListView.builder(
itemCount: processes.length,
itemBuilder: (_, idx) =>
_buildGpuProcessItem(processes[idx]),
),
),
actions: [
TextButton(
onPressed: () => context.pop(),
child: Text(libL10n.close),
)
],
);
},
onPressed: () => _onTapGpuItem(item),
icon: const Icon(Icons.info_outline, size: 17),
),
],
@@ -538,28 +521,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
textScaler: _textFactor,
),
trailing: InkWell(
onTap: () {
context.showRoundDialog(
title: '${process.pid}',
titleMaxLines: 1,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
UIs.height13,
Text('Memory: ${process.memory} MiB'),
UIs.height13,
Text('Process: ${process.name}')
],
),
actions: [
TextButton(
onPressed: () => context.pop(),
child: Text(libL10n.close),
)
],
);
},
onTap: () => _nTapGpuProcessItem(process),
child: const Icon(Icons.info_outline, size: 17),
),
);
@@ -567,8 +529,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
Widget _buildDiskView(Server si) {
final ss = si.status;
final children = List.generate(
ss.disk.length, (idx) => _buildDiskItem(ss.disk[idx], ss));
final children = List.generate(ss.disk.length, (idx) => _buildDiskItem(ss.disk[idx], ss));
return CardX(
child: ExpandTile(
title: Text(l10n.disk),
@@ -587,6 +548,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
if (read == null || write == null) return use;
return '$use\n${l10n.read} $read | ${l10n.write} $write';
}();
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 5),
child: Row(
@@ -638,43 +600,40 @@ class _ServerDetailPageState extends State<ServerDetailPage>
devices.sort(_netSortType.value.getSortFunc(ns));
children.addAll(devices.map((e) => _buildNetSpeedItem(ns, e)));
return CardX(
child: ExpandTile(
leading: Icon(ServerDetailCards.net.icon, size: 17),
title: Row(
children: [
Text(l10n.net),
UIs.width13,
ValBuilder(
listenable: _netSortType,
builder: (val) => InkWell(
onTap: () => _netSortType.value = val.next,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 377),
transitionBuilder: (child, animation) => FadeTransition(
opacity: animation,
child: child,
),
child: Row(
children: [
const Icon(Icons.sort, size: 17),
UIs.width7,
Text(
val.name,
style: UIs.text13Grey,
),
],
),
return ExpandTile(
leading: Icon(ServerDetailCards.net.icon, size: 17),
title: Row(
children: [
Text(l10n.net),
UIs.width13,
_netSortType.listenVal(
(val) => InkWell(
onTap: () => _netSortType.value = val.next,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 377),
transitionBuilder: (child, animation) => FadeTransition(
opacity: animation,
child: child,
),
child: Row(
children: [
const Icon(Icons.sort, size: 17),
UIs.width7,
Text(
val.name,
style: UIs.text13Grey,
),
],
),
),
),
],
),
childrenPadding: const EdgeInsets.only(bottom: 11),
initiallyExpanded: _getInitExpand(children.length),
children: children,
),
],
),
);
childrenPadding: const EdgeInsets.only(bottom: 11),
initiallyExpanded: _getInitExpand(children.length),
children: children,
).cardx;
}
Widget _buildNetSpeedItem(NetSpeed ns, String device) {
@@ -725,9 +684,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
leading: const Icon(Icons.ac_unit, size: 20),
initiallyExpanded: _getInitExpand(ss.temps.devices.length),
childrenPadding: const EdgeInsets.only(bottom: 7),
children: ss.temps.devices
.map((key) => _buildTemperatureItem(key, ss.temps.get(key)))
.toList(),
children: ss.temps.devices.map((key) => _buildTemperatureItem(key, ss.temps.get(key))).toList(),
),
);
}
@@ -738,12 +695,11 @@ class _ServerDetailPageState extends State<ServerDetailPage>
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(key, style: UIs.text15).paddingSymmetric(horizontal: 5).tap(
onTap: () {
Pfs.copy(key);
context.showSnackBar('${libL10n.copy} ${libL10n.success}');
},
),
Btn.text(
text: key,
textStyle: UIs.text15,
onTap: () => _onTapTemperatureItem(key),
).paddingSymmetric(horizontal: 5),
Text('${val?.toStringAsFixed(1)}°C', style: UIs.text13Grey),
],
),
@@ -813,41 +769,31 @@ class _ServerDetailPageState extends State<ServerDetailPage>
child: Text(si.device),
);
}
final itemW = Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Text(si.device, style: UIs.text15Bold),
UIs.width7,
Text('(${si.adapter.raw})', style: UIs.text13Grey),
],
),
Text(si.summary ?? '', style: UIs.text13Grey),
],
));
return InkWell(
onTap: () {
context.showRoundDialog(
title: si.device,
child: SingleChildScrollView(
child: SimpleMarkdown(
data: si.toMarkdown,
styleSheet: MarkdownStyleSheet(
tableBorder: TableBorder.all(color: Colors.grey),
tableHead: const TextStyle(fontWeight: FontWeight.bold),
),
),
),
);
},
onTap: () => _onTapSensorItem(si),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 7),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Text(si.device, style: UIs.text15Bold),
UIs.width7,
Text('(${si.adapter.raw})', style: UIs.text13Grey),
],
),
Text(si.summary ?? '', style: UIs.text13Grey),
],
)),
itemW,
UIs.width7,
const Icon(Icons.keyboard_arrow_right, color: Colors.grey),
],
@@ -869,7 +815,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
);
}
Widget _buildCustom(Server si) {
Widget _buildCustomCmd(Server si) {
final ss = si.status;
if (ss.customCmds.isEmpty) return UIs.placeholder;
return CardX(
@@ -877,12 +823,12 @@ class _ServerDetailPageState extends State<ServerDetailPage>
leading: const Icon(MingCute.command_line, size: 17),
title: Text(l10n.customCmd),
initiallyExpanded: _getInitExpand(ss.customCmds.length),
children: ss.customCmds.entries.map(_buildCustomItem).toList(),
children: ss.customCmds.entries.map(_buildCustomCmdItem).toList(),
),
);
}
Widget _buildCustomItem(MapEntry<String, String> cmd) {
Widget _buildCustomCmdItem(MapEntry<String, String> cmd) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 7),
child: KvRow(
@@ -891,20 +837,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
vBuilder: () {
if (!cmd.value.contains('\n')) return null;
return GestureDetector(
onTap: () {
context.showRoundDialog(
title: cmd.key,
child: SingleChildScrollView(
child: Text(cmd.value, style: UIs.text13Grey),
),
actions: [
TextButton(
onPressed: () => context.pop(),
child: Text(libL10n.close),
),
],
);
},
onTap: () => _onTapCustomItem(cmd),
child: const Icon(
Icons.info_outline,
size: 17,

View File

@@ -5,24 +5,25 @@ import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:icons_plus/icons_plus.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/core/route.dart';
import 'package:server_box/data/model/server/custom.dart';
import 'package:server_box/data/model/server/server.dart';
import 'package:server_box/data/model/server/wol_cfg.dart';
import 'package:server_box/data/provider/server.dart';
import 'package:server_box/core/route.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/provider/private_key.dart';
import 'package:server_box/data/store/server.dart';
import 'package:server_box/view/page/private_key/edit.dart';
class ServerEditPage extends StatefulWidget {
final Spi? args;
final SpiRequiredArgs? args;
const ServerEditPage({super.key, this.args});
static const route = AppRoute<bool, Spi>(
static const route = AppRoute<bool, SpiRequiredArgs>(
page: ServerEditPage.new,
path: '/server_edit',
path: '/servers/edit',
);
@override
@@ -30,7 +31,7 @@ class ServerEditPage extends StatefulWidget {
}
class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
late final spi = widget.args;
late final spi = widget.args?.spi;
final _nameController = TextEditingController();
final _ipController = TextEditingController();
final _altUrlController = TextEditingController();
@@ -187,14 +188,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
_buildJumpServer(),
_buildMore(),
];
return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(17, 7, 17, 47),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
),
);
return AutoMultiList(children: children);
}
Widget _buildAuth() {
@@ -259,7 +253,10 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
),
trailing: Btn.icon(
icon: const Icon(Icons.edit),
onTap: () => AppRoutes.keyEdit(pki: e).go(context),
onTap: () => PrivateKeyEditPage.route.go(
context,
args: PrivateKeyEditPageArgs(pki: e),
),
),
onTap: () => _keyIdx.value = index,
);
@@ -269,23 +266,17 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
title: Text(libL10n.add),
contentPadding: const EdgeInsets.only(left: 23, right: 23),
trailing: const Icon(Icons.add),
onTap: () => AppRoutes.keyEdit().go(context),
),
);
return CardX(
child: ListenableBuilder(
listenable: _keyIdx,
builder: (_, __) => Column(children: tiles),
onTap: () => PrivateKeyEditPage.route.go(context),
),
);
return _keyIdx.listenVal((_) => Column(children: tiles)).cardx;
},
);
}
Widget _buildEnvs() {
return _env.listenVal((val) {
final subtitle =
val.isEmpty ? null : Text(val.keys.join(','), style: UIs.textGrey);
final subtitle = val.isEmpty ? null : Text(val.keys.join(','), style: UIs.textGrey);
return ListTile(
leading: const Icon(HeroIcons.variable),
subtitle: subtitle,
@@ -419,18 +410,9 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
return ListTile(
leading: const Icon(BoxIcons.bxs_file_json),
title: const Text('JSON'),
subtitle: vals.isEmpty
? null
: Text(vals.keys.join(','), style: UIs.textGrey),
subtitle: vals.isEmpty ? null : Text(vals.keys.join(','), style: UIs.textGrey),
trailing: const Icon(Icons.keyboard_arrow_right),
onTap: () async {
final res = await KvEditor.route.go(
context,
KvEditorArgs(data: _customCmds.value),
);
if (res == null) return;
_customCmds.value = res;
},
onTap: _onTapCustomItem,
);
},
).cardx,
@@ -535,153 +517,6 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
).cardx;
}
void _onSave() async {
if (_ipController.text.isEmpty) {
context.showSnackBar('${libL10n.empty} ${l10n.host}');
return;
}
if (_keyIdx.value == null && _passwordController.text.isEmpty) {
final cancel = await context.showRoundDialog<bool>(
title: libL10n.attention,
child: Text(libL10n.askContinue(l10n.useNoPwd)),
actions: [
TextButton(
onPressed: () => context.pop(false),
child: Text(libL10n.ok),
),
TextButton(
onPressed: () => context.pop(true),
child: Text(libL10n.cancel),
)
],
);
if (cancel != false) return;
}
// If [_pubKeyIndex] is -1, it means that the user has not selected
if (_keyIdx.value == -1) {
context.showSnackBar(libL10n.empty);
return;
}
if (_usernameController.text.isEmpty) {
_usernameController.text = 'root';
}
if (_portController.text.isEmpty) {
_portController.text = '22';
}
final customCmds = _customCmds.value;
final custom = ServerCustom(
pveAddr: _pveAddrCtrl.text.selfNotEmptyOrNull,
pveIgnoreCert: _pveIgnoreCert.value,
cmds: customCmds.isEmpty ? null : customCmds,
preferTempDev: _preferTempDevCtrl.text.selfNotEmptyOrNull,
logoUrl: _logoUrlCtrl.text.selfNotEmptyOrNull,
netDev: _netDevCtrl.text.selfNotEmptyOrNull,
scriptDir: _scriptDirCtrl.text.selfNotEmptyOrNull,
);
final wolEmpty = _wolMacCtrl.text.isEmpty &&
_wolIpCtrl.text.isEmpty &&
_wolPwdCtrl.text.isEmpty;
final wol = wolEmpty
? null
: WakeOnLanCfg(
mac: _wolMacCtrl.text,
ip: _wolIpCtrl.text,
pwd: _wolPwdCtrl.text.selfNotEmptyOrNull,
);
if (wol != null) {
final wolValidation = wol.validate();
if (!wolValidation.$2) {
context.showSnackBar('${libL10n.fail}: ${wolValidation.$1}');
return;
}
}
final spi = Spi(
name: _nameController.text.isEmpty
? _ipController.text
: _nameController.text,
ip: _ipController.text,
port: int.parse(_portController.text),
user: _usernameController.text,
pwd: _passwordController.text.selfNotEmptyOrNull,
keyId: _keyIdx.value != null
? PrivateKeyProvider.pkis.value.elementAt(_keyIdx.value!).id
: null,
tags: _tags.value.isEmpty ? null : _tags.value.toList(),
alterUrl: _altUrlController.text.selfNotEmptyOrNull,
autoConnect: _autoConnect.value,
jumpId: _jumpServer.value,
custom: custom,
wolCfg: wol,
envs: _env.value.isEmpty ? null : _env.value,
);
if (this.spi == null) {
final existsIds = ServerStore.instance.box.keys;
if (existsIds.contains(spi.id)) {
context.showSnackBar('${l10n.sameIdServerExist}: ${spi.id}');
return;
}
ServerProvider.addServer(spi);
} else {
ServerProvider.updateServer(this.spi!, spi);
}
context.pop();
}
@override
void afterFirstLayout(BuildContext context) {
if (spi != null) {
_initWithSpi(spi!);
}
}
void _initWithSpi(Spi spi) {
_nameController.text = spi.name;
_ipController.text = spi.ip;
_portController.text = spi.port.toString();
_usernameController.text = spi.user;
if (spi.keyId == null) {
_passwordController.text = spi.pwd ?? '';
} else {
_keyIdx.value = PrivateKeyProvider.pkis.value.indexWhere(
(e) => e.id == spi.keyId,
);
}
/// List in dart is passed by pointer, so you need to copy it here
_tags.value = spi.tags?.toSet() ?? {};
_altUrlController.text = spi.alterUrl ?? '';
_autoConnect.value = spi.autoConnect;
_jumpServer.value = spi.jumpId;
final custom = spi.custom;
if (custom != null) {
_pveAddrCtrl.text = custom.pveAddr ?? '';
_pveIgnoreCert.value = custom.pveIgnoreCert;
_customCmds.value = custom.cmds ?? {};
_preferTempDevCtrl.text = custom.preferTempDev ?? '';
_logoUrlCtrl.text = custom.logoUrl ?? '';
}
final wol = spi.wolCfg;
if (wol != null) {
_wolMacCtrl.text = wol.mac;
_wolIpCtrl.text = wol.ip;
_wolPwdCtrl.text = wol.pwd ?? '';
}
_env.value = spi.envs ?? {};
_netDevCtrl.text = spi.custom?.netDev ?? '';
_scriptDirCtrl.text = spi.custom?.scriptDir ?? '';
}
Widget _buildWriteScriptTip() {
return Btn.tile(
text: libL10n.attention,
@@ -742,4 +577,156 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
icon: const Icon(Icons.delete),
);
}
@override
void afterFirstLayout(BuildContext context) {
if (spi != null) {
_initWithSpi(spi!);
}
}
}
extension on _ServerEditPageState {
void _onTapCustomItem() async {
final res = await KvEditor.route.go(
context,
KvEditorArgs(data: _customCmds.value),
);
if (res == null) return;
_customCmds.value = res;
}
void _onSave() async {
if (_ipController.text.isEmpty) {
context.showSnackBar('${libL10n.empty} ${l10n.host}');
return;
}
if (_keyIdx.value == null && _passwordController.text.isEmpty) {
final cancel = await context.showRoundDialog<bool>(
title: libL10n.attention,
child: Text(libL10n.askContinue(l10n.useNoPwd)),
actions: [
TextButton(
onPressed: () => context.pop(false),
child: Text(libL10n.ok),
),
TextButton(
onPressed: () => context.pop(true),
child: Text(libL10n.cancel),
)
],
);
if (cancel != false) return;
}
// If [_pubKeyIndex] is -1, it means that the user has not selected
if (_keyIdx.value == -1) {
context.showSnackBar(libL10n.empty);
return;
}
if (_usernameController.text.isEmpty) {
_usernameController.text = 'root';
}
if (_portController.text.isEmpty) {
_portController.text = '22';
}
final customCmds = _customCmds.value;
final custom = ServerCustom(
pveAddr: _pveAddrCtrl.text.selfNotEmptyOrNull,
pveIgnoreCert: _pveIgnoreCert.value,
cmds: customCmds.isEmpty ? null : customCmds,
preferTempDev: _preferTempDevCtrl.text.selfNotEmptyOrNull,
logoUrl: _logoUrlCtrl.text.selfNotEmptyOrNull,
netDev: _netDevCtrl.text.selfNotEmptyOrNull,
scriptDir: _scriptDirCtrl.text.selfNotEmptyOrNull,
);
final wolEmpty = _wolMacCtrl.text.isEmpty && _wolIpCtrl.text.isEmpty && _wolPwdCtrl.text.isEmpty;
final wol = wolEmpty
? null
: WakeOnLanCfg(
mac: _wolMacCtrl.text,
ip: _wolIpCtrl.text,
pwd: _wolPwdCtrl.text.selfNotEmptyOrNull,
);
if (wol != null) {
final wolValidation = wol.validate();
if (!wolValidation.$2) {
context.showSnackBar('${libL10n.fail}: ${wolValidation.$1}');
return;
}
}
final spi = Spi(
name: _nameController.text.isEmpty ? _ipController.text : _nameController.text,
ip: _ipController.text,
port: int.parse(_portController.text),
user: _usernameController.text,
pwd: _passwordController.text.selfNotEmptyOrNull,
keyId: _keyIdx.value != null ? PrivateKeyProvider.pkis.value.elementAt(_keyIdx.value!).id : null,
tags: _tags.value.isEmpty ? null : _tags.value.toList(),
alterUrl: _altUrlController.text.selfNotEmptyOrNull,
autoConnect: _autoConnect.value,
jumpId: _jumpServer.value,
custom: custom,
wolCfg: wol,
envs: _env.value.isEmpty ? null : _env.value,
);
if (this.spi == null) {
final existsIds = ServerStore.instance.box.keys;
if (existsIds.contains(spi.id)) {
context.showSnackBar('${l10n.sameIdServerExist}: ${spi.id}');
return;
}
ServerProvider.addServer(spi);
} else {
ServerProvider.updateServer(this.spi!, spi);
}
context.pop();
}
void _initWithSpi(Spi spi) {
_nameController.text = spi.name;
_ipController.text = spi.ip;
_portController.text = spi.port.toString();
_usernameController.text = spi.user;
if (spi.keyId == null) {
_passwordController.text = spi.pwd ?? '';
} else {
_keyIdx.value = PrivateKeyProvider.pkis.value.indexWhere(
(e) => e.id == spi.keyId,
);
}
/// List in dart is passed by pointer, so you need to copy it here
_tags.value = spi.tags?.toSet() ?? {};
_altUrlController.text = spi.alterUrl ?? '';
_autoConnect.value = spi.autoConnect;
_jumpServer.value = spi.jumpId;
final custom = spi.custom;
if (custom != null) {
_pveAddrCtrl.text = custom.pveAddr ?? '';
_pveIgnoreCert.value = custom.pveIgnoreCert;
_customCmds.value = custom.cmds ?? {};
_preferTempDevCtrl.text = custom.preferTempDev ?? '';
_logoUrlCtrl.text = custom.logoUrl ?? '';
}
final wol = spi.wolCfg;
if (wol != null) {
_wolMacCtrl.text = wol.mac;
_wolIpCtrl.text = wol.ip;
_wolPwdCtrl.text = wol.pwd ?? '';
}
_env.value = spi.envs ?? {};
_netDevCtrl.text = spi.custom?.netDev ?? '';
_scriptDirCtrl.text = spi.custom?.scriptDir ?? '';
}
}

View File

@@ -1,802 +0,0 @@
import 'dart:async';
import 'dart:math' as math;
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:icons_plus/icons_plus.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/core/extension/ssh_client.dart';
import 'package:server_box/data/model/app/shell_func.dart';
import 'package:server_box/data/model/server/try_limiter.dart';
import 'package:server_box/data/res/build_data.dart';
import 'package:server_box/data/res/store.dart';
import 'package:server_box/view/page/server/edit.dart';
import 'package:server_box/view/page/setting/entry.dart';
import 'package:server_box/view/widget/percent_circle.dart';
import 'package:server_box/core/route.dart';
import 'package:server_box/data/model/app/net_view.dart';
import 'package:server_box/data/model/server/server.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/provider/server.dart';
import 'package:server_box/view/widget/server_func_btns.dart';
part 'top_bar.dart';
class ServerPage extends StatefulWidget {
const ServerPage({super.key});
@override
State<ServerPage> createState() => _ServerPageState();
}
const _cardPad = 74.0;
const _cardPadSingle = 13.0;
class _ServerPageState extends State<ServerPage>
with AutomaticKeepAliveClientMixin, AfterLayoutMixin {
late MediaQueryData _media;
late double _textFactorDouble;
double _offset = 1;
late TextScaler _textFactor;
final _cardsStatus = <String, _CardNotifier>{};
Timer? _timer;
final _tag = ''.vn;
bool _useDoubleColumn = false;
final _scrollController = ScrollController();
final _autoHideCtrl = AutoHideController();
@override
void dispose() {
super.dispose();
_timer?.cancel();
_scrollController.dispose();
_autoHideCtrl.dispose();
_tag.dispose();
}
@override
void initState() {
super.initState();
if (!Stores.setting.fullScreenJitter.fetch()) return;
_timer = Timer.periodic(const Duration(seconds: 30), (_) {
if (mounted) {
_updateOffset();
setState(() {});
} else {
_timer?.cancel();
}
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_media = MediaQuery.of(context);
_updateOffset();
_updateTextScaler();
_useDoubleColumn = _media.size.width > 639 &&
Stores.setting.doubleColumnServersPage.fetch();
}
@override
Widget build(BuildContext context) {
super.build(context);
return OrientationBuilder(builder: (_, orientation) {
if (orientation == Orientation.landscape) {
final useFullScreen = Stores.setting.fullScreen.fetch();
if (useFullScreen) return _buildLandscape();
}
return _buildPortrait();
});
}
Widget _buildPortrait() {
return Scaffold(
appBar: _TopBar(
tags: ServerProvider.tags,
onTagChanged: (p0) => _tag.value = p0,
initTag: _tag.value,
),
body: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => _autoHideCtrl.show(),
child: ListenableBuilder(
listenable: Stores.setting.textFactor.listenable(),
builder: (_, __) {
_updateTextScaler();
return _buildBody();
},
),
),
floatingActionButton: AutoHide(
direction: AxisDirection.right,
offset: 75,
scrollController: _scrollController,
hideController: _autoHideCtrl,
child: FloatingActionButton(
heroTag: 'addServer',
onPressed: () => ServerEditPage.route.go(context),
tooltip: libL10n.add,
child: const Icon(Icons.add),
),
),
);
}
Widget _buildLandscape() {
final offset = Offset(_offset, _offset);
return Padding(
// Avoid display cutout
padding: EdgeInsets.all(_offset.abs()),
child: Transform.translate(
offset: offset,
child: Stack(
children: [
_buildLandscapeBody(),
Positioned(
top: 0,
left: 0,
child: IconButton(
onPressed: () => SettingsPage.route.go(context),
icon: const Icon(Icons.settings, color: Colors.grey),
),
),
],
),
),
);
}
Widget _buildLandscapeBody() {
return ServerProvider.serverOrder.listenVal((order) {
if (order.isEmpty) {
return Center(
child: Text(libL10n.empty, textAlign: TextAlign.center),
);
}
return PageView.builder(
itemCount: order.length,
itemBuilder: (_, idx) {
final id = order[idx];
final srv = ServerProvider.pick(id: id);
if (srv == null) return UIs.placeholder;
return srv.listenVal((srv) {
final title = _buildServerCardTitle(srv);
final List<Widget> children = [
title,
..._buildNormalCard(srv.status, srv.spi).joinWith(SizedBox(
height: _media.size.height / 10,
))
];
return Padding(
padding: _media.padding,
child: ListenableBuilder(
listenable: _getCardNoti(id),
builder: (_, __) {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: children,
);
},
),
);
});
},
);
});
}
Widget _buildBody() {
return ServerProvider.serverOrder.listenVal(
(order) {
if (order.isEmpty) {
return Center(
child: Text(libL10n.empty, textAlign: TextAlign.center),
);
}
return _tag.listenVal(
(val) {
final filtered = _filterServers(order);
if (_useDoubleColumn &&
Stores.setting.doubleColumnServersPage.fetch()) {
return _buildBodyMedium(filtered);
}
return _buildBodySmall(filtered: filtered);
},
);
},
);
}
Widget _buildBodySmall({
required List<String> filtered,
EdgeInsets? padding = const EdgeInsets.fromLTRB(7, 0, 7, 7),
}) {
final count = filtered.length + 1;
return ListView.builder(
controller: _scrollController,
padding: padding,
itemCount: count,
itemBuilder: (_, index) {
// Issue #130
if (index == count - 1) return UIs.height77;
final vnode = ServerProvider.pick(id: filtered[index]);
if (vnode == null) return UIs.placeholder;
return vnode.listenVal(_buildEachServerCard);
},
);
}
Widget _buildBodyMedium(List<String> filtered) {
final mid = (filtered.length / 2).ceil();
final filteredLeft = filtered.sublist(0, mid);
final filteredRight = filtered.sublist(mid);
return Row(
children: [
Expanded(
child: _buildBodySmall(
filtered: filteredLeft,
padding: const EdgeInsets.only(left: 7),
),
),
Expanded(
child: _buildBodySmall(
filtered: filteredRight,
padding: const EdgeInsets.only(right: 7),
),
)
],
);
}
Widget _buildEachServerCard(Server? srv) {
if (srv == null) {
return UIs.placeholder;
}
return CardX(
key: Key(srv.spi.id + _tag.value),
child: InkWell(
onTap: () {
if (srv.canViewDetails) {
AppRoutes.serverDetail(spi: srv.spi).go(context);
} else {
ServerEditPage.route.go(context, args: srv.spi);
}
},
onLongPress: () {
if (srv.conn == ServerConn.finished) {
final id = srv.spi.id;
final cardStatus = _getCardNoti(id);
cardStatus.value = cardStatus.value.copyWith(
flip: !cardStatus.value.flip,
);
} else {
ServerEditPage.route.go(context, args: srv.spi);
}
},
child: Padding(
padding: const EdgeInsets.only(
left: _cardPadSingle,
right: 3,
top: _cardPadSingle,
bottom: _cardPadSingle,
),
child: _buildRealServerCard(srv),
),
),
);
}
/// The child's width mat not equal to 1/4 of the screen width,
/// so we need to wrap it with a SizedBox.
Widget _wrapWithSizedbox(Widget child, [bool circle = false]) {
var width = (_media.size.width - _cardPad) / (circle ? 4 : 4.3);
if (_useDoubleColumn) width /= 2;
return SizedBox(
width: width,
child: child,
);
}
Widget _buildRealServerCard(Server srv) {
final id = srv.spi.id;
final cardStatus = _getCardNoti(id);
final title = _buildServerCardTitle(srv);
return cardStatus.listenVal((_) {
final List<Widget> children = [title];
if (srv.conn == ServerConn.finished) {
if (cardStatus.value.flip) {
children.add(_buildFlippedCard(srv));
} else {
children.addAll(_buildNormalCard(srv.status, srv.spi));
}
}
final height = _calcCardHeight(srv.conn, cardStatus.value.flip);
return AnimatedContainer(
duration: const Duration(milliseconds: 377),
curve: Curves.fastEaseInToSlowEaseOut,
height: height,
// Use [OverflowBox] to dismiss the warning of [Column] overflow.
child: OverflowBox(
// If `height == _kCardHeightMin`, the `maxHeight` will be ignored.
//
// You can comment the `maxHeight` then connect&disconnect the server
// to see the difference.
maxHeight: height != _kCardHeightMin ? height : null,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: children,
),
),
);
});
}
Widget _buildFlippedCard(Server srv) {
const color = Colors.grey;
const textStyle = TextStyle(fontSize: 13, color: color);
final children = [
Btn.column(
onTap: () => _askFor(
func: () async {
if (Stores.setting.showSuspendTip.fetch()) {
await context.showRoundDialog(
title: libL10n.attention,
child: Text(l10n.suspendTip),
);
Stores.setting.showSuspendTip.put(false);
}
srv.client?.execWithPwd(
ShellFunc.suspend.exec(srv.spi.id),
context: context,
id: srv.id,
);
},
typ: l10n.suspend,
name: srv.spi.name,
),
icon: const Icon(Icons.stop, color: color),
text: l10n.suspend,
textStyle: textStyle,
),
Btn.column(
onTap: () => _askFor(
func: () => srv.client?.execWithPwd(
ShellFunc.shutdown.exec(srv.spi.id),
context: context,
id: srv.id,
),
typ: l10n.shutdown,
name: srv.spi.name,
),
icon: const Icon(Icons.power_off, color: color),
text: l10n.shutdown,
textStyle: textStyle,
),
Btn.column(
onTap: () => _askFor(
func: () => srv.client?.execWithPwd(
ShellFunc.reboot.exec(srv.spi.id),
context: context,
id: srv.id,
),
typ: l10n.reboot,
name: srv.spi.name,
),
icon: const Icon(Icons.restart_alt, color: color),
text: l10n.reboot,
textStyle: textStyle,
),
Btn.column(
onTap: () => ServerEditPage.route.go(context, args: srv.spi),
icon: const Icon(Icons.edit, color: color),
text: libL10n.edit,
textStyle: textStyle,
)
];
final width = (_media.size.width - _cardPad) / children.length;
return Padding(
padding: const EdgeInsets.only(top: 9),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: children.map((e) {
if (width == 0) return e;
return SizedBox(width: width, child: e);
}).toList(),
),
);
}
List<Widget> _buildNormalCard(ServerStatus ss, Spi spi) {
return [
UIs.height13,
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_wrapWithSizedbox(PercentCircle(percent: ss.cpu.usedPercent()), true),
_wrapWithSizedbox(
PercentCircle(percent: ss.mem.usedPercent * 100),
true,
),
_wrapWithSizedbox(_buildNet(ss, spi.id)),
_wrapWithSizedbox(_buildDisk(ss, spi.id)),
],
),
UIs.height13,
if (Stores.setting.moveServerFuncs.fetch() &&
// Discussion #146
!Stores.setting.serverTabUseOldUI.fetch())
SizedBox(
height: 27,
child: ServerFuncBtns(spi: spi),
),
];
}
Widget _buildServerCardTitle(Server s) {
return Padding(
padding: const EdgeInsets.only(left: 7, right: 13),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ConstrainedBox(
constraints: BoxConstraints(maxWidth: _media.size.width / 2.3),
child: Hero(
tag: 'home_card_title_${s.spi.id}',
transitionOnUserGestures: true,
child: Material(
color: Colors.transparent,
child: Text(
s.spi.name,
style: UIs.text13Bold.copyWith(
color: context.isDark ? Colors.white : Colors.black,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
),
const Icon(
Icons.keyboard_arrow_right,
size: 17,
color: Colors.grey,
),
const Spacer(),
_buildTopRightText(s),
_buildTopRightWidget(s),
],
),
);
}
Widget _buildTopRightWidget(Server s) {
final (child, onTap) = switch (s.conn) {
ServerConn.connecting || ServerConn.loading || ServerConn.connected => (
SizedBox(
width: 19,
height: 19,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation(UIs.primaryColor),
),
),
null,
),
ServerConn.failed => (
const Icon(Icons.refresh, size: 21, color: Colors.grey),
() {
TryLimiter.reset(s.spi.id);
ServerProvider.refresh(spi: s.spi);
},
),
ServerConn.disconnected => (
const Icon(MingCute.link_3_line, size: 19, color: Colors.grey),
() => ServerProvider.refresh(spi: s.spi)
),
ServerConn.finished => (
const Icon(MingCute.unlink_2_line, size: 17, color: Colors.grey),
() => ServerProvider.closeServer(id: s.spi.id),
),
};
// Or the loading icon will be rescaled.
final wrapped = child is SizedBox
? child
: SizedBox(height: _kCardHeightMin, width: 27, child: child);
if (onTap == null) return wrapped.paddingOnly(left: 10);
return InkWell(
borderRadius: BorderRadius.circular(7),
onTap: onTap,
child: wrapped,
).paddingOnly(left: 5);
}
Widget _buildTopRightText(Server s) {
final hasErr = s.conn == ServerConn.failed && s.status.err != null;
final str = s.getTopRightStr(s.spi);
if (str == null) return UIs.placeholder;
return GestureDetector(
onTap: () {
if (!hasErr) return;
_showFailReason(s.status);
},
child: Text(str, style: UIs.text13Grey),
);
}
void _showFailReason(ServerStatus ss) {
final md = '''
${ss.err?.solution ?? l10n.unknown}
```sh
${ss.err?.message ?? 'null'}
''';
context.showRoundDialog(
title: libL10n.error,
child: SingleChildScrollView(child: SimpleMarkdown(data: md)),
actions: [
TextButton(
onPressed: () => Pfs.copy(md),
child: Text(libL10n.copy),
)
],
);
}
Widget _buildDisk(ServerStatus ss, String id) {
final cardNoti = _getCardNoti(id);
return ListenableBuilder(
listenable: cardNoti,
builder: (_, __) {
final isSpeed = cardNoti.value.diskIO ??
!Stores.setting.serverTabPreferDiskAmount.fetch();
final (r, w) = ss.diskIO.cachedAllSpeed;
return AnimatedSwitcher(
duration: const Duration(milliseconds: 377),
transitionBuilder: (Widget child, Animation<double> animation) {
return FadeTransition(opacity: animation, child: child);
},
child: _buildIOData(
isSpeed
? '${l10n.read}:\n$r'
: 'Total:\n${ss.diskUsage?.size.kb2Str}',
isSpeed
? '${l10n.write}:\n$w'
: 'Used:\n${ss.diskUsage?.used.kb2Str}',
onTap: () {
cardNoti.value = cardNoti.value.copyWith(diskIO: !isSpeed);
},
key: ValueKey(isSpeed),
),
);
},
);
}
Widget _buildNet(ServerStatus ss, String id) {
final cardNoti = _getCardNoti(id);
final type = cardNoti.value.net ?? Stores.setting.netViewType.fetch();
final device = ServerProvider.pick(id: id)?.value.spi.custom?.netDev;
final (a, b) = type.build(ss, dev: device);
return AnimatedSwitcher(
duration: const Duration(milliseconds: 377),
transitionBuilder: (c, anim) => FadeTransition(opacity: anim, child: c),
child: _buildIOData(
a,
b,
onTap: () => cardNoti.value = cardNoti.value.copyWith(net: type.next),
key: ValueKey(type),
),
);
}
Widget _buildIOData(
String up,
String down, {
void Function()? onTap,
Key? key,
}) {
final child = Column(
children: [
Text(
up,
style: const TextStyle(fontSize: 10, color: Colors.grey),
textAlign: TextAlign.center,
textScaler: _textFactor,
),
const SizedBox(height: 3),
Text(
down,
style: const TextStyle(fontSize: 10, color: Colors.grey),
textAlign: TextAlign.center,
textScaler: _textFactor,
)
],
);
if (onTap == null) return child;
return IconButton(
key: key,
padding: const EdgeInsets.symmetric(horizontal: 3),
onPressed: onTap,
icon: child,
);
}
@override
bool get wantKeepAlive => true;
@override
Future<void> afterFirstLayout(BuildContext context) async {
ServerProvider.refresh();
ServerProvider.startAutoRefresh();
}
List<String> _filterServers(List<String> order) {
final tag = _tag.value;
if (tag == TagSwitcher.kDefaultTag) return order;
return order.where((e) {
final tags = ServerProvider.pick(id: e)?.value.spi.tags;
if (tags == null) return false;
return tags.contains(tag);
}).toList();
}
static const _kCardHeightMin = 23.0;
static const _kCardHeightFlip = 99.0;
static const _kCardHeightNormal = 108.0;
static const _kCardHeightMoveOutFuncs = 135.0;
double? _calcCardHeight(ServerConn cs, bool flip) {
if (_textFactorDouble != 1.0) return null;
if (cs != ServerConn.finished) {
return _kCardHeightMin;
}
if (flip) {
return _kCardHeightFlip;
}
if (Stores.setting.moveServerFuncs.fetch() &&
// Discussion #146
!Stores.setting.serverTabUseOldUI.fetch()) {
return _kCardHeightMoveOutFuncs;
}
return _kCardHeightNormal;
}
void _askFor({
required void Function() func,
required String typ,
required String name,
}) {
context.showRoundDialog(
title: libL10n.attention,
child: Text(libL10n.askContinue('$typ ${l10n.server}($name)')),
actions: Btn.ok(
onTap: () {
context.pop();
func();
},
).toList,
);
}
_CardNotifier _getCardNoti(String id) => _cardsStatus.putIfAbsent(
id,
() => _CardNotifier(const _CardStatus()),
);
void _updateOffset() {
if (!Stores.setting.fullScreenJitter.fetch()) return;
final x = _media.size.height * 0.03;
final r = math.Random().nextDouble();
final n = math.Random().nextBool() ? 1 : -1;
_offset = x * r * n;
}
void _updateTextScaler() {
_textFactorDouble = Stores.setting.textFactor.fetch();
_textFactor = TextScaler.linear(_textFactorDouble);
}
}
typedef _CardNotifier = ValueNotifier<_CardStatus>;
class _CardStatus {
final bool flip;
final bool? diskIO;
final NetViewType? net;
const _CardStatus({
this.flip = false,
this.diskIO,
this.net,
});
_CardStatus copyWith({
bool? flip,
bool? diskIO,
NetViewType? net,
}) {
return _CardStatus(
flip: flip ?? this.flip,
diskIO: diskIO ?? this.diskIO,
net: net ?? this.net,
);
}
}
extension _ServerX on Server {
String? getTopRightStr(Spi spi) {
switch (conn) {
case ServerConn.disconnected:
return null;
case ServerConn.finished:
// Highest priority of temperature display
final cmdTemp = () {
final val = status.customCmds['server_card_top_right'];
if (val == null) return null;
// This returned value is used on server card top right, so it should
// be a single line string.
return val.split('\n').lastOrNull;
}();
final temperatureVal = () {
// Second priority
final preferTempDev = spi.custom?.preferTempDev;
if (preferTempDev != null) {
final preferTemp = status.sensors
.firstWhereOrNull((e) => e.device == preferTempDev)
?.summary
?.split(' ')
.firstOrNull;
if (preferTemp != null) {
return double.tryParse(preferTemp.replaceFirst('°C', ''));
}
}
// Last priority
final temp = status.temps.first;
if (temp != null) {
return temp;
}
return null;
}();
final upTime = status.more[StatusCmdType.uptime];
final items = [
cmdTemp ??
(temperatureVal != null
? '${temperatureVal.toStringAsFixed(1)}°C'
: null),
upTime
];
final str = items.where((e) => e != null && e.isNotEmpty).join(' | ');
if (str.isEmpty) return libL10n.empty;
return str;
case ServerConn.loading:
return null;
case ServerConn.connected:
return null;
case ServerConn.connecting:
return null;
case ServerConn.failed:
return status.err != null ? l10n.viewErr : libL10n.fail;
}
}
}

View File

@@ -0,0 +1,27 @@
part of 'tab.dart';
typedef _CardNotifier = ValueNotifier<_CardStatus>;
class _CardStatus {
final bool flip;
final bool? diskIO;
final NetViewType? net;
const _CardStatus({
this.flip = false,
this.diskIO,
this.net,
});
_CardStatus copyWith({
bool? flip,
bool? diskIO,
NetViewType? net,
}) {
return _CardStatus(
flip: flip ?? this.flip,
diskIO: diskIO ?? this.diskIO,
net: net ?? this.net,
);
}
}

View File

@@ -0,0 +1,195 @@
part of 'tab.dart';
extension on _ServerPageState {
Widget _buildServerCardTitle(Server s) {
return Padding(
padding: const EdgeInsets.only(left: 7, right: 13),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ConstrainedBox(
constraints: BoxConstraints(maxWidth: _media.size.width / 2.3),
child: Hero(
tag: 'home_card_title_${s.spi.id}',
transitionOnUserGestures: true,
child: Material(
color: Colors.transparent,
child: Text(
s.spi.name,
style: UIs.text13Bold.copyWith(
color: context.isDark ? Colors.white : Colors.black,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
),
const Icon(
Icons.keyboard_arrow_right,
size: 17,
color: Colors.grey,
),
const Spacer(),
_buildTopRightText(s),
_buildTopRightWidget(s),
],
),
);
}
Widget _buildTopRightWidget(Server s) {
final (child, onTap) = switch (s.conn) {
ServerConn.connecting || ServerConn.loading || ServerConn.connected => (
SizedBox(
width: 19,
height: 19,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation(UIs.primaryColor),
),
),
null,
),
ServerConn.failed => (
const Icon(Icons.refresh, size: 21, color: Colors.grey),
() {
TryLimiter.reset(s.spi.id);
ServerProvider.refresh(spi: s.spi);
},
),
ServerConn.disconnected => (
const Icon(MingCute.link_3_line, size: 19, color: Colors.grey),
() => ServerProvider.refresh(spi: s.spi)
),
ServerConn.finished => (
const Icon(MingCute.unlink_2_line, size: 17, color: Colors.grey),
() => ServerProvider.closeServer(id: s.spi.id),
),
};
// Or the loading icon will be rescaled.
final wrapped = child is SizedBox
? child
: SizedBox(height: _ServerPageState._kCardHeightMin, width: 27, child: child);
if (onTap == null) return wrapped.paddingOnly(left: 10);
return InkWell(
borderRadius: BorderRadius.circular(7),
onTap: onTap,
child: wrapped,
).paddingOnly(left: 5);
}
Widget _buildTopRightText(Server s) {
final hasErr = s.conn == ServerConn.failed && s.status.err != null;
final str = s._getTopRightStr(s.spi);
if (str == null) return UIs.placeholder;
return GestureDetector(
onTap: () {
if (!hasErr) return;
_showFailReason(s.status);
},
child: Text(str, style: UIs.text13Grey),
);
}
void _showFailReason(ServerStatus ss) {
final md = '''
${ss.err?.solution ?? l10n.unknown}
```sh
${ss.err?.message ?? 'null'}
''';
context.showRoundDialog(
title: libL10n.error,
child: SingleChildScrollView(child: SimpleMarkdown(data: md)),
actions: [
TextButton(
onPressed: () => Pfs.copy(md),
child: Text(libL10n.copy),
)
],
);
}
Widget _buildDisk(ServerStatus ss, String id) {
final cardNoti = _getCardNoti(id);
return ListenableBuilder(
listenable: cardNoti,
builder: (_, __) {
final isSpeed = cardNoti.value.diskIO ?? !Stores.setting.serverTabPreferDiskAmount.fetch();
final (r, w) = ss.diskIO.cachedAllSpeed;
return AnimatedSwitcher(
duration: const Duration(milliseconds: 377),
transitionBuilder: (Widget child, Animation<double> animation) {
return FadeTransition(opacity: animation, child: child);
},
child: _buildIOData(
isSpeed ? '${l10n.read}:\n$r' : 'Total:\n${ss.diskUsage?.size.kb2Str}',
isSpeed ? '${l10n.write}:\n$w' : 'Used:\n${ss.diskUsage?.used.kb2Str}',
onTap: () {
cardNoti.value = cardNoti.value.copyWith(diskIO: !isSpeed);
},
key: ValueKey(isSpeed),
),
);
},
);
}
Widget _buildNet(ServerStatus ss, String id) {
final cardNoti = _getCardNoti(id);
final type = cardNoti.value.net ?? Stores.setting.netViewType.fetch();
final device = ServerProvider.pick(id: id)?.value.spi.custom?.netDev;
final (a, b) = type.build(ss, dev: device);
return AnimatedSwitcher(
duration: const Duration(milliseconds: 377),
transitionBuilder: (c, anim) => FadeTransition(opacity: anim, child: c),
child: _buildIOData(
a,
b,
onTap: () => cardNoti.value = cardNoti.value.copyWith(net: type.next),
key: ValueKey(type),
),
);
}
Widget _buildIOData(
String up,
String down, {
void Function()? onTap,
Key? key,
int maxLines = 2
}) {
final child = Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
up,
style: const TextStyle(fontSize: 10, color: Colors.grey),
textAlign: TextAlign.center,
textScaler: _textFactor,
maxLines: maxLines,
),
const SizedBox(height: 3),
Text(
down,
style: const TextStyle(fontSize: 10, color: Colors.grey),
textAlign: TextAlign.center,
textScaler: _textFactor,
maxLines: maxLines,
)
],
);
if (onTap == null) return child;
return IconButton(
key: key,
padding: const EdgeInsets.symmetric(horizontal: 3),
onPressed: onTap,
icon: child,
);
}
}

View File

@@ -0,0 +1,69 @@
part of 'tab.dart';
extension on _ServerPageState {
Widget _buildLandscape() {
final offset = Offset(_offset, _offset);
return Padding(
// Avoid display cutout
padding: EdgeInsets.all(_offset.abs()),
child: Transform.translate(
offset: offset,
child: Stack(
children: [
_buildLandscapeBody(),
Positioned(
top: 0,
left: 0,
child: IconButton(
onPressed: () => SettingsPage.route.go(context),
icon: const Icon(Icons.settings, color: Colors.grey),
),
),
],
),
),
);
}
Widget _buildLandscapeBody() {
return ServerProvider.serverOrder.listenVal((order) {
if (order.isEmpty) {
return Center(
child: Text(libL10n.empty, textAlign: TextAlign.center),
);
}
return PageView.builder(
itemCount: order.length,
itemBuilder: (_, idx) {
final id = order[idx];
final srv = ServerProvider.pick(id: id);
if (srv == null) return UIs.placeholder;
return srv.listenVal((srv) {
final title = _buildServerCardTitle(srv);
final List<Widget> children = [
title,
_buildNormalCard(srv.status, srv.spi),
];
return Padding(
padding: _media.padding,
child: ListenableBuilder(
listenable: _getCardNoti(id),
builder: (_, __) {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: children,
);
},
),
);
});
},
);
});
}
}

View File

@@ -0,0 +1,369 @@
// ignore_for_file: invalid_use_of_protected_member
import 'dart:async';
import 'dart:math' as math;
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:icons_plus/icons_plus.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/core/extension/ssh_client.dart';
import 'package:server_box/core/route.dart';
import 'package:server_box/data/model/app/shell_func.dart';
import 'package:server_box/data/model/server/try_limiter.dart';
import 'package:server_box/data/res/build_data.dart';
import 'package:server_box/data/res/store.dart';
import 'package:server_box/view/page/server/detail/view.dart';
import 'package:server_box/view/page/server/edit.dart';
import 'package:server_box/view/page/setting/entry.dart';
import 'package:server_box/view/widget/percent_circle.dart';
import 'package:server_box/data/model/app/net_view.dart';
import 'package:server_box/data/model/server/server.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/provider/server.dart';
import 'package:server_box/view/widget/server_func_btns.dart';
part 'top_bar.dart';
part 'card_stat.dart';
part 'utils.dart';
part 'content.dart';
part 'landscape.dart';
class ServerPage extends StatefulWidget {
const ServerPage({super.key});
@override
State<ServerPage> createState() => _ServerPageState();
static const route = AppRouteNoArg(
page: ServerPage.new,
path: '/servers',
);
}
const _cardPad = 74.0;
const _cardPadSingle = 13.0;
class _ServerPageState extends State<ServerPage> with AutomaticKeepAliveClientMixin, AfterLayoutMixin {
late MediaQueryData _media;
late double _textFactorDouble;
double _offset = 1;
late TextScaler _textFactor;
final _cardsStatus = <String, _CardNotifier>{};
Timer? _timer;
final _tag = ''.vn;
final _scrollController = ScrollController();
final _autoHideCtrl = AutoHideController();
final _splitViewCtrl = SplitViewController();
@override
void dispose() {
super.dispose();
_timer?.cancel();
_scrollController.dispose();
_autoHideCtrl.dispose();
_tag.dispose();
}
@override
void initState() {
super.initState();
_startAvoidJitterTimer();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_media = MediaQuery.of(context);
_updateOffset();
_updateTextScaler();
}
@override
Widget build(BuildContext context) {
super.build(context);
return OrientationBuilder(builder: (_, orientation) {
if (orientation == Orientation.landscape) {
final useFullScreen = Stores.setting.fullScreen.fetch();
// Only enter landscape mode when the screen is wide enough and the
// full screen mode is enabled.
if (useFullScreen) return _buildLandscape();
}
return _buildPortrait();
});
}
Widget _buildScaffold(Widget child) {
return Scaffold(
appBar: _TopBar(
tags: ServerProvider.tags,
onTagChanged: (p0) => _tag.value = p0,
initTag: _tag.value,
),
body: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => _autoHideCtrl.show(),
child: ListenableBuilder(
listenable: Stores.setting.textFactor.listenable(),
builder: (_, __) {
_updateTextScaler();
return child;
},
),
),
floatingActionButton: AutoHide(
direction: AxisDirection.right,
offset: 75,
scrollController: _scrollController,
hideController: _autoHideCtrl,
child: FloatingActionButton(
heroTag: 'addServer',
onPressed: _onTapAddServer,
tooltip: libL10n.add,
child: const Icon(Icons.add),
),
),
);
}
Widget _buildPortrait() {
// final isMobile = ResponsiveBreakpoints.of(context).isMobile;
return ServerProvider.serverOrder.listenVal(
(order) {
return _tag.listenVal(
(val) {
final filtered = _filterServers(order);
final child = _buildScaffold(_buildBodySmall(filtered: filtered));
// if (isMobile) {
return child;
// }
// return SplitView(
// controller: _splitViewCtrl,
// leftWeight: 1,
// rightWeight: 1.3,
// initialRight: Center(child: CircularProgressIndicator()),
// leftBuilder: (_, __) => child,
// );
},
);
},
);
}
Widget _buildBodySmall({
required List<String> filtered,
EdgeInsets? padding = const EdgeInsets.fromLTRB(7, 0, 7, 7),
}) {
if (filtered.isEmpty) {
return Center(child: Text(libL10n.empty, textAlign: TextAlign.center));
}
// Calculate number of columns based on available width
final columnsCount = math.max(1, (_media.size.width / UIs.columnWidth).floor());
// Calculate number of rows needed
final rowCount = (filtered.length + columnsCount - 1) ~/ columnsCount;
return ListView.builder(
controller: _scrollController,
padding: padding,
itemCount: rowCount + 1, // +1 for the bottom space
itemBuilder: (_, rowIndex) {
// Bottom space
if (rowIndex == rowCount) return UIs.height77;
// Create a row of server cards
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: List.generate(columnsCount, (colIndex) {
final index = rowIndex * columnsCount + colIndex;
if (index >= filtered.length) return Expanded(child: Container());
final vnode = ServerProvider.pick(id: filtered[index]);
if (vnode == null) return Expanded(child: UIs.placeholder);
return Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: vnode.listenVal(_buildEachServerCard),
),
);
}),
),
);
},
);
}
Widget _buildEachServerCard(Server? srv) {
if (srv == null) {
return UIs.placeholder;
}
return CardX(
key: Key(srv.spi.id + _tag.value),
child: InkWell(
onTap: () => _onTapCard(srv),
onLongPress: () => _onLongPressCard(srv),
child: Padding(
padding: const EdgeInsets.only(
left: _cardPadSingle,
right: 3,
top: _cardPadSingle,
bottom: _cardPadSingle,
),
child: _buildRealServerCard(srv),
),
),
);
}
/// The child's width mat not equal to 1/4 of the screen width,
/// so we need to wrap it with a SizedBox.
Widget _wrapWithSizedbox(Widget child, double maxWidth, [bool circle = false]) {
return LayoutBuilder(builder: (_, cons) {
final width = (maxWidth - _cardPad) / 4;
return SizedBox(
width: width,
child: child,
);
});
}
Widget _buildRealServerCard(Server srv) {
final id = srv.spi.id;
final cardStatus = _getCardNoti(id);
final title = _buildServerCardTitle(srv);
return cardStatus.listenVal((_) {
final List<Widget> children = [title];
if (srv.conn == ServerConn.finished) {
if (cardStatus.value.flip) {
children.add(_buildFlippedCard(srv));
} else {
children.add(_buildNormalCard(srv.status, srv.spi));
}
}
final height = _calcCardHeight(srv.conn, cardStatus.value.flip);
return AnimatedContainer(
duration: const Duration(milliseconds: 377),
curve: Curves.fastEaseInToSlowEaseOut,
height: height,
// Use [OverflowBox] to dismiss the warning of [Column] overflow.
child: OverflowBox(
// If `height == _kCardHeightMin`, the `maxHeight` will be ignored.
//
// You can comment the `maxHeight` then connect&disconnect the server
// to see the difference.
maxHeight: height != _kCardHeightMin ? height : null,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: children,
),
),
);
});
}
Widget _buildFlippedCard(Server srv) {
const color = Colors.grey;
const textStyle = TextStyle(fontSize: 13, color: color);
final children = [
Btn.column(
onTap: () => _onTapSuspend(srv),
icon: const Icon(Icons.stop, color: color),
text: l10n.suspend,
textStyle: textStyle,
),
Btn.column(
onTap: () => _onTapShutdown(srv),
icon: const Icon(Icons.power_off, color: color),
text: l10n.shutdown,
textStyle: textStyle,
),
Btn.column(
onTap: () => _onTapReboot(srv),
icon: const Icon(Icons.restart_alt, color: color),
text: l10n.reboot,
textStyle: textStyle,
),
Btn.column(
onTap: () => _onTapEdit(srv),
icon: const Icon(Icons.edit, color: color),
text: libL10n.edit,
textStyle: textStyle,
)
];
final width = (_media.size.width - _cardPad) / children.length;
return Padding(
padding: const EdgeInsets.only(top: 9),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: children.map((e) {
if (width == 0) return e;
return SizedBox(width: width, child: e);
}).toList(),
),
);
}
Widget _buildNormalCard(ServerStatus ss, Spi spi) {
return LayoutBuilder(builder: (_, cons) {
final maxWidth = cons.maxWidth;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
UIs.height13,
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_wrapWithSizedbox(PercentCircle(percent: ss.cpu.usedPercent()), maxWidth, true),
_wrapWithSizedbox(
PercentCircle(percent: ss.mem.usedPercent * 100),
maxWidth,
true,
),
_wrapWithSizedbox(_buildNet(ss, spi.id), maxWidth),
_wrapWithSizedbox(_buildDisk(ss, spi.id), maxWidth),
],
),
UIs.height13,
if (Stores.setting.moveServerFuncs.fetch() &&
// Discussion #146
!Stores.setting.serverTabUseOldUI.fetch())
SizedBox(
height: 27,
child: ServerFuncBtns(spi: spi),
),
],
);
});
}
@override
bool get wantKeepAlive => true;
@override
Future<void> afterFirstLayout(BuildContext context) async {
ServerProvider.refresh();
ServerProvider.startAutoRefresh();
}
static const _kCardHeightMin = 23.0;
static const _kCardHeightFlip = 99.0;
static const _kCardHeightNormal = 108.0;
static const _kCardHeightMoveOutFuncs = 135.0;
}

View File

@@ -13,6 +13,9 @@ final class _TopBar extends StatelessWidget implements PreferredSizeWidget {
@override
Widget build(BuildContext context) {
final isMobile = ResponsiveBreakpoints.of(context).isMobile;
if (!isMobile) return UIs.placeholder;
return Padding(
padding: const EdgeInsets.only(left: 10),
child: Row(

View File

@@ -0,0 +1,243 @@
// ignore_for_file: invalid_use_of_protected_member
part of 'tab.dart';
extension _Actions on _ServerPageState {
void _onTapCard(Server srv) {
if (srv.canViewDetails) {
// _splitViewCtrl.replace(ServerDetailPage(
// key: ValueKey(srv.spi.id),
// args: SpiRequiredArgs(srv.spi),
// ));
ServerDetailPage.route.go(
context,
SpiRequiredArgs(srv.spi),
);
} else {
// _splitViewCtrl.replace(ServerEditPage(
// key: ValueKey(srv.spi.id),
// args: SpiRequiredArgs(srv.spi),
// ));
ServerEditPage.route.go(
context,
args: SpiRequiredArgs(srv.spi),
);
}
}
void _onLongPressCard(Server srv) {
if (srv.conn == ServerConn.finished) {
final id = srv.spi.id;
final cardStatus = _getCardNoti(id);
cardStatus.value = cardStatus.value.copyWith(
flip: !cardStatus.value.flip,
);
} else {
_splitViewCtrl.replace(ServerEditPage(
key: ValueKey(srv.spi.id),
args: SpiRequiredArgs(srv.spi),
));
}
}
void _onTapAddServer() {
// final isMobile = ResponsiveBreakpoints.of(context).isMobile;
// if (isMobile) {
ServerEditPage.route.go(context);
// } else {
// _splitViewCtrl.replace(const ServerEditPage(
// key: ValueKey('addServer'),
// ));
// }
}
}
extension _Operation on _ServerPageState {
void _onTapSuspend(Server srv) {
_askFor(
func: () async {
if (Stores.setting.showSuspendTip.fetch()) {
await context.showRoundDialog(
title: libL10n.attention,
child: Text(l10n.suspendTip),
);
Stores.setting.showSuspendTip.put(false);
}
srv.client?.execWithPwd(
ShellFunc.suspend.exec(srv.spi.id),
context: context,
id: srv.id,
);
},
typ: l10n.suspend,
name: srv.spi.name,
);
}
void _onTapShutdown(Server srv) {
_askFor(
func: () => srv.client?.execWithPwd(
ShellFunc.shutdown.exec(srv.spi.id),
context: context,
id: srv.id,
),
typ: l10n.shutdown,
name: srv.spi.name,
);
}
void _onTapReboot(Server srv) {
_askFor(
func: () => srv.client?.execWithPwd(
ShellFunc.reboot.exec(srv.spi.id),
context: context,
id: srv.id,
),
typ: l10n.reboot,
name: srv.spi.name,
);
}
void _onTapEdit(Server srv) {
if (srv.canViewDetails) {
_splitViewCtrl.replace(ServerDetailPage(
key: ValueKey(srv.spi.id),
args: SpiRequiredArgs(srv.spi),
));
} else {
_splitViewCtrl.replace(ServerEditPage(
key: ValueKey(srv.spi.id),
args: SpiRequiredArgs(srv.spi),
));
}
}
}
extension _Utils on _ServerPageState {
List<String> _filterServers(List<String> order) {
final tag = _tag.value;
if (tag == TagSwitcher.kDefaultTag) return order;
return order.where((e) {
final tags = ServerProvider.pick(id: e)?.value.spi.tags;
if (tags == null) return false;
return tags.contains(tag);
}).toList();
}
double? _calcCardHeight(ServerConn cs, bool flip) {
if (_textFactorDouble != 1.0) return null;
if (cs != ServerConn.finished) {
return _ServerPageState._kCardHeightMin;
}
if (flip) {
return _ServerPageState._kCardHeightFlip;
}
if (Stores.setting.moveServerFuncs.fetch() &&
// Discussion #146
!Stores.setting.serverTabUseOldUI.fetch()) {
return _ServerPageState._kCardHeightMoveOutFuncs;
}
return _ServerPageState._kCardHeightNormal;
}
void _askFor({
required void Function() func,
required String typ,
required String name,
}) {
context.showRoundDialog(
title: libL10n.attention,
child: Text(libL10n.askContinue('$typ ${l10n.server}($name)')),
actions: Btn.ok(
onTap: () {
context.pop();
func();
},
).toList,
);
}
_CardNotifier _getCardNoti(String id) => _cardsStatus.putIfAbsent(
id,
() => _CardNotifier(const _CardStatus()),
);
void _updateOffset() {
if (!Stores.setting.fullScreenJitter.fetch()) return;
final x = _media.size.height * 0.03;
final r = math.Random().nextDouble();
final n = math.Random().nextBool() ? 1 : -1;
_offset = x * r * n;
}
void _updateTextScaler() {
_textFactorDouble = Stores.setting.textFactor.fetch();
_textFactor = TextScaler.linear(_textFactorDouble);
}
void _startAvoidJitterTimer() {
if (!Stores.setting.fullScreenJitter.fetch()) return;
_timer = Timer.periodic(const Duration(seconds: 30), (_) {
if (mounted) {
_updateOffset();
setState(() {});
} else {
_timer?.cancel();
}
});
}
}
extension _ServerX on Server {
String? _getTopRightStr(Spi spi) {
switch (conn) {
case ServerConn.disconnected:
return null;
case ServerConn.finished:
// Highest priority of temperature display
final cmdTemp = () {
final val = status.customCmds['server_card_top_right'];
if (val == null) return null;
// This returned value is used on server card top right, so it should
// be a single line string.
return val.split('\n').lastOrNull;
}();
final temperatureVal = () {
// Second priority
final preferTempDev = spi.custom?.preferTempDev;
if (preferTempDev != null) {
final preferTemp = status.sensors
.firstWhereOrNull((e) => e.device == preferTempDev)
?.summary
?.split(' ')
.firstOrNull;
if (preferTemp != null) {
return double.tryParse(preferTemp.replaceFirst('°C', ''));
}
}
// Last priority
final temp = status.temps.first;
if (temp != null) {
return temp;
}
return null;
}();
final upTime = status.more[StatusCmdType.uptime];
final items = [
cmdTemp ?? (temperatureVal != null ? '${temperatureVal.toStringAsFixed(1)}°C' : null),
upTime
];
final str = items.where((e) => e != null && e.isNotEmpty).join(' | ');
if (str.isEmpty) return libL10n.empty;
return str;
case ServerConn.loading:
return null;
case ServerConn.connected:
return null;
case ServerConn.connecting:
return null;
case ServerConn.failed:
return status.err != null ? l10n.viewErr : libL10n.fail;
}
}
}

View File

@@ -17,8 +17,8 @@ extension _App on _AppSettingsPageState {
Widget? _buildPlatformSetting() {
final func = switch (Pfs.type) {
Pfs.android => AppRoutes.androidSettings().go,
Pfs.ios => AppRoutes.iosSettings().go,
Pfs.android => AndroidSettingsPage.route.go,
Pfs.ios => IosSettingsPage.route.go,
_ => null,
};
if (func == null) return null;

View File

@@ -142,7 +142,7 @@ extension _Server on _AppSettingsPageState {
leading: const Icon(OctIcons.sort_desc, size: _kIconSize),
title: Text(l10n.serverOrder),
trailing: const Icon(Icons.keyboard_arrow_right),
onTap: () => AppRoutes.serverOrder().go(context),
onTap: () => ServerOrderPage.route.go(context),
);
}
@@ -151,7 +151,7 @@ extension _Server on _AppSettingsPageState {
leading: const Icon(OctIcons.sort_desc, size: _kIconSize),
title: Text(l10n.serverDetailOrder),
trailing: const Icon(Icons.keyboard_arrow_right),
onTap: () => AppRoutes.serverDetailOrder().go(context),
onTap: () => ServerDetailOrderPage.route.go(context),
);
}

View File

@@ -20,7 +20,7 @@ extension _SSH on _AppSettingsPageState {
leading: const Icon(BoxIcons.bxs_keyboard),
title: Text(l10n.editVirtKeys),
trailing: const Icon(Icons.keyboard_arrow_right),
onTap: () => AppRoutes.sshVirtKeySetting().go(context),
onTap: () => SSHVirtKeySettingPage.route.go(context),
);
}

View File

@@ -13,13 +13,17 @@ import 'package:server_box/data/res/github_id.dart';
import 'package:server_box/data/res/store.dart';
import 'package:server_box/data/res/url.dart';
import 'package:server_box/core/route.dart';
import 'package:server_box/data/model/app/net_view.dart';
import 'package:server_box/data/res/build_data.dart';
import 'package:server_box/view/page/backup.dart';
import 'package:server_box/view/page/editor.dart';
import 'package:server_box/view/page/private_key/list.dart';
import 'package:server_box/view/page/setting/platform/android.dart';
import 'package:server_box/view/page/setting/platform/ios.dart';
import 'package:server_box/view/page/setting/seq/srv_detail_seq.dart';
import 'package:server_box/view/page/setting/seq/srv_func_seq.dart';
import 'package:server_box/view/page/setting/seq/srv_seq.dart';
import 'package:server_box/view/page/setting/seq/virt_key.dart';
part 'about.dart';
part 'entries/app.dart';
@@ -35,16 +39,17 @@ const _kIconSize = 23.0;
class SettingsPage extends StatefulWidget {
const SettingsPage({super.key});
static const route = AppRouteNoArg(page: SettingsPage.new, path: '/settings');
static const route = AppRouteNoArg(
page: SettingsPage.new,
path: '/settings',
);
@override
State<SettingsPage> createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage>
with SingleTickerProviderStateMixin {
late final _tabCtrl =
TabController(length: SettingsTabs.values.length, vsync: this);
class _SettingsPageState extends State<SettingsPage> with SingleTickerProviderStateMixin {
late final _tabCtrl = TabController(length: SettingsTabs.values.length, vsync: this);
@override
void dispose() {
@@ -62,9 +67,7 @@ class _SettingsPageState extends State<SettingsPage>
dividerHeight: 0,
tabAlignment: TabAlignment.center,
isScrollable: true,
tabs: SettingsTabs.values
.map((e) => Tab(text: e.i18n))
.toList(growable: false),
tabs: SettingsTabs.values.map((e) => Tab(text: e.i18n)).toList(growable: false),
),
actions: [
Btn.text(
@@ -120,12 +123,7 @@ final class _AppSettingsPageState extends State<AppSettingsPage> {
children: [
[const CenterGreyTitle('App'), _buildApp()],
[CenterGreyTitle(l10n.server), _buildServer()],
[
const CenterGreyTitle('SSH'),
_buildSSH(),
const CenterGreyTitle('SFTP'),
_buildSFTP()
],
[const CenterGreyTitle('SSH'), _buildSSH(), const CenterGreyTitle('SFTP'), _buildSFTP()],
[
CenterGreyTitle(l10n.container),
_buildContainer(),
@@ -162,6 +160,5 @@ enum SettingsTabs {
SettingsTabs.about => const _AppAboutPage(),
};
static final List<Widget> pages =
SettingsTabs.values.map((e) => e.page).toList();
static final List<Widget> pages = SettingsTabs.values.map((e) => e.page).toList();
}

View File

@@ -9,6 +9,11 @@ class AndroidSettingsPage extends StatefulWidget {
@override
State<AndroidSettingsPage> createState() => _AndroidSettingsPageState();
static const route = AppRouteNoArg(
page: AndroidSettingsPage.new,
path: '/settings/android',
);
}
const _homeWidgetPrefPrefix = 'widget_';
@@ -24,8 +29,7 @@ class _AndroidSettingsPageState extends State<AndroidSettingsPage> {
// _buildFgService(),
_buildBgRun(),
_buildAndroidWidgetSharedPreference(),
if (BioAuth.isPlatformSupported)
PlatformPublicSettings.buildBioAuth(),
if (BioAuth.isPlatformSupported) PlatformPublicSettings.buildBioAuth(),
].map((e) => CardX(child: e)).toList(),
),
);

View File

@@ -6,14 +6,19 @@ import 'package:server_box/data/res/store.dart';
import 'package:server_box/view/page/setting/platform/platform_pub.dart';
import 'package:watch_connectivity/watch_connectivity.dart';
class IOSSettingsPage extends StatefulWidget {
const IOSSettingsPage({super.key});
class IosSettingsPage extends StatefulWidget {
const IosSettingsPage({super.key});
@override
State<IOSSettingsPage> createState() => _IOSSettingsPageState();
State<IosSettingsPage> createState() => _IosSettingsPageState();
static const route = AppRouteNoArg(
page: IosSettingsPage.new,
path: '/settings/ios',
);
}
class _IOSSettingsPageState extends State<IOSSettingsPage> {
class _IosSettingsPageState extends State<IosSettingsPage> {
final _pushToken = ValueNotifier<String?>(null);
final wc = WatchConnectivity();

View File

@@ -9,6 +9,11 @@ class ServerDetailOrderPage extends StatefulWidget {
@override
State<ServerDetailOrderPage> createState() => _ServerDetailOrderPageState();
static const route = AppRouteNoArg(
page: ServerDetailOrderPage.new,
path: '/settings/order/server_detail',
);
}
class _ServerDetailOrderPageState extends State<ServerDetailOrderPage> {

View File

@@ -10,7 +10,10 @@ class ServerFuncBtnsOrderPage extends StatefulWidget {
@override
State<ServerFuncBtnsOrderPage> createState() => _ServerDetailOrderPageState();
static const route = AppRouteNoArg(page: ServerFuncBtnsOrderPage.new, path: '/setting/seq/srv_func');
static const route = AppRouteNoArg(
page: ServerFuncBtnsOrderPage.new,
path: '/setting/seq/srv_func',
);
}
class _ServerDetailOrderPageState extends State<ServerFuncBtnsOrderPage> {
@@ -28,10 +31,7 @@ class _ServerDetailOrderPageState extends State<ServerFuncBtnsOrderPage> {
return ValBuilder(
listenable: prop.listenable(),
builder: (keys) {
final disabled = ServerFuncBtn.values
.map((e) => e.index)
.where((e) => !keys.contains(e))
.toList();
final disabled = ServerFuncBtn.values.map((e) => e.index).where((e) => !keys.contains(e)).toList();
final allKeys = [...keys, ...disabled];
return ReorderableListView.builder(
padding: const EdgeInsets.all(7),

View File

@@ -10,6 +10,11 @@ class ServerOrderPage extends StatefulWidget {
@override
State<ServerOrderPage> createState() => _ServerOrderPageState();
static const route = AppRouteNoArg(
page: ServerOrderPage.new,
path: '/settings/order/server',
);
}
class _ServerOrderPageState extends State<ServerOrderPage> {

View File

@@ -9,6 +9,11 @@ class SSHVirtKeySettingPage extends StatefulWidget {
@override
State<SSHVirtKeySettingPage> createState() => _SSHVirtKeySettingPageState();
static const route = AppRouteNoArg(
page: SSHVirtKeySettingPage.new,
path: '/settings/ssh_virt_key',
);
}
class _SSHVirtKeySettingPageState extends State<SSHVirtKeySettingPage> {

View File

@@ -6,17 +6,26 @@ import 'package:server_box/data/model/server/snippet.dart';
import 'package:server_box/data/provider/server.dart';
import 'package:server_box/data/provider/snippet.dart';
class SnippetEditPage extends StatefulWidget {
const SnippetEditPage({super.key, this.snippet});
final class SnippetEditPageArgs {
final Snippet? snippet;
const SnippetEditPageArgs({this.snippet});
}
class SnippetEditPage extends StatefulWidget {
final SnippetEditPageArgs? args;
const SnippetEditPage({super.key, this.args});
@override
State<SnippetEditPage> createState() => _SnippetEditPageState();
static const route = AppRoute(
page: SnippetEditPage.new,
path: '/snippets/edit',
);
}
class _SnippetEditPageState extends State<SnippetEditPage>
with AfterLayoutMixin {
class _SnippetEditPageState extends State<SnippetEditPage> with AfterLayoutMixin {
final _nameController = TextEditingController();
final _scriptController = TextEditingController();
final _noteController = TextEditingController();
@@ -48,18 +57,19 @@ class _SnippetEditPageState extends State<SnippetEditPage>
}
List<Widget>? _buildAppBarActions() {
if (widget.snippet == null) return null;
final snippet = widget.args?.snippet;
if (snippet == null) return null;
return [
IconButton(
onPressed: () {
context.showRoundDialog(
title: libL10n.attention,
child: Text(libL10n.askContinue(
'${libL10n.delete} ${l10n.snippet}(${widget.snippet!.name})',
'${libL10n.delete} ${l10n.snippet}(${snippet.name})',
)),
actions: Btn.ok(
onTap: () {
SnippetProvider.del(widget.snippet!);
SnippetProvider.del(snippet);
context.pop();
context.pop();
},
@@ -92,8 +102,9 @@ class _SnippetEditPageState extends State<SnippetEditPage>
note: note.isEmpty ? null : note,
autoRunOn: _autoRunOn.value.isEmpty ? null : _autoRunOn.value,
);
if (widget.snippet != null) {
SnippetProvider.update(widget.snippet!, snippet);
final oldSnippet = widget.args?.snippet;
if (oldSnippet != null) {
SnippetProvider.update(oldSnippet, snippet);
} else {
SnippetProvider.add(snippet);
}
@@ -103,8 +114,7 @@ class _SnippetEditPageState extends State<SnippetEditPage>
}
Widget _buildBody() {
return ListView(
padding: const EdgeInsets.symmetric(horizontal: 13),
return AutoMultiList(
children: [
Input(
autoFocus: true,
@@ -148,9 +158,7 @@ class _SnippetEditPageState extends State<SnippetEditPage>
builder: (vals) {
final subtitle = vals.isEmpty
? null
: vals
.map((e) => ServerProvider.pick(id: e)?.value.spi.name ?? e)
.join(', ');
: vals.map((e) => ServerProvider.pick(id: e)?.value.spi.name ?? e).join(', ');
return ListTile(
leading: const Padding(
padding: EdgeInsets.only(left: 5),
@@ -167,8 +175,7 @@ class _SnippetEditPageState extends State<SnippetEditPage>
overflow: TextOverflow.ellipsis,
),
onTap: () async {
vals.removeWhere(
(e) => !ServerProvider.serverOrder.value.contains(e));
vals.removeWhere((e) => !ServerProvider.serverOrder.value.contains(e));
final serverIds = await context.showPickDialog(
title: l10n.autoRun,
items: ServerProvider.serverOrder.value,
@@ -193,9 +200,9 @@ class _SnippetEditPageState extends State<SnippetEditPage>
child: SimpleMarkdown(
data: '''
📌 ${l10n.supportFmtArgs}\n
${Snippet.fmtArgs.keys.map((e) => '`$e`').join(', ')}\n
${SnippetX.fmtArgs.keys.map((e) => '`$e`').join(', ')}\n
${Snippet.fmtTermKeys.keys.map((e) => '`$e+?}`').join(', ')}\n
${SnippetX.fmtTermKeys.keys.map((e) => '`$e+?}`').join(', ')}\n
${libL10n.example}:
- `\${ctrl+c}` (Control + C)
- `\${ctrl+b}d` (Tmux Detach)
@@ -212,7 +219,7 @@ ${libL10n.example}:
@override
void afterFirstLayout(BuildContext context) {
final snippet = widget.snippet;
final snippet = widget.args?.snippet;
if (snippet != null) {
_nameController.text = snippet.name;
_scriptController.text = snippet.script;

View File

@@ -1,123 +1,164 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:server_box/data/res/store.dart';
import 'package:flutter_reorderable_grid_view/widgets/widgets.dart';
import 'package:server_box/data/model/server/snippet.dart';
import 'package:server_box/core/route.dart';
import 'package:server_box/data/provider/snippet.dart';
import 'package:server_box/view/page/snippet/edit.dart';
class SnippetListPage extends StatefulWidget {
const SnippetListPage({super.key});
@override
State<SnippetListPage> createState() => _SnippetListPageState();
static const route = AppRouteNoArg(
page: SnippetListPage.new,
path: '/snippets',
);
}
class _SnippetListPageState extends State<SnippetListPage>
with AutomaticKeepAliveClientMixin {
class _SnippetListPageState extends State<SnippetListPage> with AutomaticKeepAliveClientMixin {
final _tag = ''.vn;
final _splitViewCtrl = SplitViewController();
@override
void dispose() {
super.dispose();
_tag.dispose();
_splitViewCtrl.dispose();
}
@override
Widget build(BuildContext context) {
super.build(context);
return _buildBody();
}
Widget _buildBody() {
// final isMobile = ResponsiveBreakpoints.of(context).isMobile;
return SnippetProvider.snippets.listenVal(
(snippets) {
return _tag.listenVal((tag) {
final child = _buildScaffold(snippets, tag);
// if (isMobile) {
return child;
// }
// return SplitView(
// controller: _splitViewCtrl,
// leftWeight: 1,
// rightWeight: 1.3,
// initialRight: Center(child: Text(libL10n.empty)),
// leftBuilder: (_, __) => child,
// );
});
},
);
}
Widget _buildScaffold(List<Snippet> snippets, String tag) {
return Scaffold(
appBar: TagSwitcher(
tags: SnippetProvider.tags,
onTagChanged: (tag) => _tag.value = tag,
initTag: _tag.value,
),
body: _buildBody(),
body: _buildSnippetList(snippets, tag),
floatingActionButton: FloatingActionButton(
heroTag: 'snippetAdd',
child: const Icon(Icons.add),
onPressed: () => AppRoutes.snippetEdit().go(context),
onPressed: () {
// if (ResponsiveBreakpoints.of(context).isMobile) {
SnippetEditPage.route.go(context);
// } else {
// _splitViewCtrl.replace(const SnippetEditPage());
// }
},
),
);
}
Widget _buildBody() {
return SnippetProvider.snippets.listenVal(
(snippets) {
if (snippets.isEmpty) return Center(child: Text(libL10n.empty));
return _tag.listenVal((tag) => _buildSnippetList(snippets, tag));
},
);
}
Widget _buildSnippetList(List<Snippet> snippets, String tag) {
if (snippets.isEmpty) return Center(child: Text(libL10n.empty));
final filtered = tag == TagSwitcher.kDefaultTag
? snippets
: snippets.where((e) => e.tags?.contains(tag) ?? false).toList();
return ReorderableListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 9),
itemCount: filtered.length,
onReorder: (oldIdx, newIdx) {
snippets.moveByItem(
oldIdx,
newIdx,
filtered: filtered,
onMove: (p0) {
Stores.setting.snippetOrder.put(p0.map((e) => e.name).toList());
},
);
SnippetProvider.snippets.notify();
},
footer: UIs.height77,
buildDefaultDragHandles: false,
itemBuilder: (context, idx) {
final snippet = filtered.elementAt(idx);
return ReorderableDelayedDragStartListener(
key: ValueKey(idx),
index: idx,
final generatedChildren = List.generate(
filtered.length,
(idx) {
final snippet = filtered.elementAtOrNull(idx);
if (snippet == null) return UIs.placeholder;
return Container(
key: ValueKey(snippet.name),
child: _buildSnippetItem(snippet),
);
},
);
return ReorderableBuilder(
children: generatedChildren,
onReorder: (ReorderedListFunction reorderedListFunction) {
setState(() {
final newFiltered = reorderedListFunction(filtered) as List<Snippet>;
snippets.moveByItem(
0,
0,
filtered: filtered,
onMove: (p0) {
Stores.setting.snippetOrder.put(newFiltered.map((e) => e.name).toList());
},
);
SnippetProvider.snippets.notify();
});
},
builder: (children) {
return GridView(
padding: const EdgeInsets.symmetric(horizontal: 9),
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 330,
childAspectRatio: 3.4,
),
children: children,
);
},
);
}
Widget _buildSnippetItem(Snippet snippet) {
return CardX(
child: ListTile(
contentPadding: const EdgeInsets.only(left: 23, right: 17),
title: Text(
snippet.name,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
subtitle: Text(
snippet.note ?? snippet.script,
overflow: TextOverflow.ellipsis,
maxLines: 3,
style: UIs.textGrey,
),
trailing: const Icon(Icons.keyboard_arrow_right),
onTap: () => AppRoutes.snippetEdit(snippet: snippet).go(context),
return ListTile(
contentPadding: const EdgeInsets.only(left: 23, right: 17),
title: Text(
snippet.name,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
);
subtitle: Text(
snippet.note ?? snippet.script,
overflow: TextOverflow.ellipsis,
maxLines: 3,
style: UIs.textGrey,
),
trailing: const Icon(Icons.keyboard_arrow_right),
onTap: () {
// final isMobile = ResponsiveBreakpoints.of(context).isMobile;
// if (isMobile) {
SnippetEditPage.route.go(
context,
args: SnippetEditPageArgs(snippet: snippet),
);
// } else {
// _splitViewCtrl.replace(SnippetEditPage(
// args: SnippetEditPageArgs(snippet: snippet),
// ));
// }
},
).cardx;
}
@override
bool get wantKeepAlive => true;
// Future<void> _runSnippet(Snippet snippet) async {
// final servers = await context.showPickDialog<Server>(
// items: Pros.server.servers.toList(),
// name: (e) => e.spi.name,
// );
// if (servers == null) {
// return;
// }
// final ids = servers.map((e) => e.spi.id).toList();
// final results = await Pros.server.runSnippetsMulti(ids, snippet);
// if (results.isNotEmpty) {
// AppRoutes.snippetResult(results: results).go(context);
// }
// }
}

View File

@@ -4,9 +4,14 @@ import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/model/server/snippet.dart';
class SnippetResultPage extends StatelessWidget {
final List<SnippetResult?> results;
final List<SnippetResult?> args;
const SnippetResultPage({super.key, required this.results});
const SnippetResultPage({super.key, required this.args});
static const route = AppRouteArg(
page: SnippetResultPage.new,
path: '/snippets/result',
);
@override
Widget build(BuildContext context) {
@@ -19,13 +24,13 @@ class SnippetResultPage extends StatelessWidget {
Widget _buildBody() {
return ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 17),
itemCount: results.length,
itemCount: args.length,
itemBuilder: (_, index) {
final item = results[index];
final item = args[index];
if (item == null) return UIs.placeholder;
return CardX(
child: ExpandTile(
initiallyExpanded: results.length == 1,
initiallyExpanded: args.length == 1,
title: Text(item.dest ?? ''),
subtitle: Text(item.time.toString(), style: UIs.textGrey),
children: [

View File

@@ -14,18 +14,18 @@ import 'package:server_box/data/model/server/snippet.dart';
import 'package:server_box/data/provider/snippet.dart';
import 'package:server_box/data/provider/virtual_keyboard.dart';
import 'package:server_box/data/res/store.dart';
import 'package:server_box/view/page/storage/sftp.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:xterm/core.dart';
import 'package:xterm/ui.dart' hide TerminalThemes;
import 'package:server_box/core/route.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/model/ssh/virtual_key.dart';
import 'package:server_box/data/res/terminal.dart';
const _echoPWD = 'echo \$PWD';
class SSHPage extends StatefulWidget {
final class SshPageArgs {
final Spi spi;
final String? initCmd;
final Snippet? initSnippet;
@@ -34,8 +34,7 @@ class SSHPage extends StatefulWidget {
final GlobalKey<TerminalViewState>? terminalKey;
final FocusNode? focusNode;
const SSHPage({
super.key,
const SshPageArgs({
required this.spi,
this.initCmd,
this.initSnippet,
@@ -44,20 +43,33 @@ class SSHPage extends StatefulWidget {
this.terminalKey,
this.focusNode,
});
}
class SSHPage extends StatefulWidget {
final SshPageArgs args;
const SSHPage({
super.key,
required this.args,
});
@override
State<SSHPage> createState() => SSHPageState();
static const route = AppRouteArg<void, SshPageArgs>(
page: SSHPage.new,
path: '/ssh/page',
);
}
const _horizonPadding = 7.0;
class SSHPageState extends State<SSHPage>
with AutomaticKeepAliveClientMixin, AfterLayoutMixin {
class SSHPageState extends State<SSHPage> with AutomaticKeepAliveClientMixin, AfterLayoutMixin {
final _keyboard = VirtKeyProvider();
late final _terminal = Terminal(inputHandler: _keyboard);
final TerminalController _terminalController = TerminalController();
final List<List<VirtKey>> _virtKeysList = [];
late final _termKey = widget.terminalKey ?? GlobalKey<TerminalViewState>();
late final _termKey = widget.args.terminalKey ?? GlobalKey<TerminalViewState>();
late MediaQueryData _media;
late TerminalStyle _terminalStyle;
@@ -68,7 +80,7 @@ class SSHPageState extends State<SSHPage>
bool _isDark = false;
Timer? _virtKeyLongPressTimer;
late SSHClient? _client = widget.spi.server?.value.client;
late SSHClient? _client = widget.args.spi.server?.value.client;
Timer? _discontinuityTimer;
/// Used for (de)activate the wake lock and forground service
@@ -148,13 +160,10 @@ class SSHPageState extends State<SSHPage>
Widget _buildBody() {
final letterCache = Stores.setting.letterCache.fetch();
return SizedBox(
height: _media.size.height -
_virtKeysHeight -
_media.padding.bottom -
_media.padding.top,
height: _media.size.height - _virtKeysHeight - _media.padding.bottom - _media.padding.top,
child: Padding(
padding: EdgeInsets.only(
top: widget.notFromTab ? CustomAppBar.sysStatusBarHeight : 0,
top: widget.args.notFromTab ? CustomAppBar.sysStatusBarHeight : 0,
left: _horizonPadding,
right: _horizonPadding,
),
@@ -175,7 +184,7 @@ class SSHPageState extends State<SSHPage>
CustomAppBar.sysStatusBarHeight,
),
hideScrollBar: false,
focusNode: widget.focusNode,
focusNode: widget.args.focusNode,
),
),
);
@@ -192,8 +201,7 @@ class SSHPageState extends State<SSHPage>
height: _virtKeysHeight,
child: ChangeNotifierProvider(
create: (_) => _keyboard,
builder: (_, __) =>
Consumer<VirtKeyProvider>(builder: (_, __, ___) {
builder: (_, __) => Consumer<VirtKeyProvider>(builder: (_, __, ___) {
return _buildVirtualKey();
}),
),
@@ -207,14 +215,11 @@ class SSHPageState extends State<SSHPage>
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children:
_virtKeysList.expand((e) => e).map(_buildVirtKeyItem).toList(),
children: _virtKeysList.expand((e) => e).map(_buildVirtKeyItem).toList(),
),
);
}
final rows = _virtKeysList
.map((e) => Row(children: e.map(_buildVirtKeyItem).toList()))
.toList();
final rows = _virtKeysList.map((e) => Row(children: e.map(_buildVirtKeyItem).toList())).toList();
return Column(
mainAxisSize: MainAxisSize.min,
children: rows,
@@ -243,9 +248,7 @@ class SSHPageState extends State<SSHPage>
: Text(
item.text,
style: TextStyle(
color: selected
? UIs.primaryColor
: (_isDark ? Colors.white : Colors.black),
color: selected ? UIs.primaryColor : (_isDark ? Colors.white : Colors.black),
fontSize: 15,
),
);
@@ -315,7 +318,7 @@ extension _Init on SSHPageState {
Future<void> _initTerminal() async {
_writeLn(l10n.waitConnection);
_client ??= await genClient(
widget.spi,
widget.args.spi,
onStatus: (p0) {
_writeLn(p0.toString());
},
@@ -328,7 +331,7 @@ extension _Init on SSHPageState {
width: _terminal.viewWidth,
height: _terminal.viewHeight,
),
environment: widget.spi.envs,
environment: widget.args.spi.envs,
);
//_setupDiscontinuityTimer();
@@ -352,37 +355,34 @@ extension _Init on SSHPageState {
_listen(session.stderr);
for (final snippet in SnippetProvider.snippets.value) {
if (snippet.autoRunOn?.contains(widget.spi.id) == true) {
snippet.runInTerm(_terminal, widget.spi);
if (snippet.autoRunOn?.contains(widget.args.spi.id) == true) {
snippet.runInTerm(_terminal, widget.args.spi);
}
}
if (widget.initCmd != null) {
_terminal.textInput(widget.initCmd!);
if (widget.args.initCmd != null) {
_terminal.textInput(widget.args.initCmd!);
_terminal.keyInput(TerminalKey.enter);
}
if (widget.initSnippet != null) {
widget.initSnippet!.runInTerm(_terminal, widget.spi);
if (widget.args.initSnippet != null) {
widget.args.initSnippet!.runInTerm(_terminal, widget.args.spi);
}
widget.focusNode?.requestFocus();
widget.args.focusNode?.requestFocus();
await session.done;
if (mounted && widget.notFromTab) {
if (mounted && widget.args.notFromTab) {
context.pop();
}
widget.onSessionEnd?.call();
widget.args.onSessionEnd?.call();
}
void _listen(Stream<Uint8List>? stream) {
if (stream == null) {
return;
}
stream
.cast<List<int>>()
.transform(const Utf8Decoder())
.listen(_terminal.write);
stream.cast<List<int>>().transform(const Utf8Decoder()).listen(_terminal.write);
}
void _setupDiscontinuityTimer() {
@@ -490,7 +490,7 @@ extension _VirtKey on SSHPageState {
final snippet = snippets.firstOrNull;
if (snippet == null) return;
snippet.runInTerm(_terminal, widget.spi);
snippet.runInTerm(_terminal, widget.args.spi);
break;
case VirtualKeyFunc.file:
// get $PWD from SSH session
@@ -509,7 +509,8 @@ extension _VirtKey on SSHPageState {
);
return;
}
AppRoutes.sftp(spi: widget.spi, initPath: initPath).go(context);
final args = SftpPageArgs(spi: widget.args.spi, initPath: initPath);
SftpPage.route.go(context, args);
}
}
@@ -547,6 +548,6 @@ extension _VirtKey on SSHPageState {
}
FutureOr<List<String>?> _onKeyboardInteractive(SSHUserInfoRequest req) {
return KeybordInteractive.defaultHandle(widget.spi, ctx: context);
return KeybordInteractive.defaultHandle(widget.args.spi, ctx: context);
}
}

View File

@@ -15,6 +15,11 @@ class SSHTabPage extends StatefulWidget {
@override
State<SSHTabPage> createState() => _SSHTabPageState();
static const route = AppRouteNoArg(
page: SSHTabPage.new,
path: '/ssh',
);
}
typedef _TabMap = Map<String, ({Widget page, FocusNode? focus})>;
@@ -93,10 +98,7 @@ extension on _SSHTabPageState {
void _onTapInitCard(Spi spi) async {
final name = () {
final reg = RegExp('${spi.name}\\((\\d+)\\)');
final idxs = _tabMap.keys
.map((e) => reg.firstMatch(e))
.map((e) => e?.group(1))
.whereType<String>();
final idxs = _tabMap.keys.map((e) => reg.firstMatch(e)).map((e) => e?.group(1)).whereType<String>();
if (idxs.isEmpty) {
return _tabMap.keys.contains(spi.name) ? '${spi.name}(1)' : spi.name;
}
@@ -108,15 +110,17 @@ extension on _SSHTabPageState {
return spi.name;
}();
final key = Key(name);
final args = SshPageArgs(
spi: spi,
notFromTab: false,
onSessionEnd: () {
_tabMap.remove(name);
},
);
_tabMap[name] = (
page: SSHPage(
// Keep it, or the Flutter will works unexpectedly
key: key,
spi: spi,
notFromTab: false,
onSessionEnd: () {
_tabMap.remove(name);
},
key: key, // Keep it, or the Flutter will works unexpectedly
args: args,
),
focus: FocusNode(),
);
@@ -128,8 +132,7 @@ extension on _SSHTabPageState {
}
Future<void> _toPage(int idx) async {
await _pageCtrl.animateToPage(idx,
duration: Durations.short3, curve: Curves.fastEaseInToSlowEaseOut);
await _pageCtrl.animateToPage(idx, duration: Durations.short3, curve: Curves.fastEaseInToSlowEaseOut);
final focus = _tabMap.values.elementAt(idx).focus;
if (focus != null) {
FocusScope.of(context).requestFocus(focus);
@@ -156,8 +159,7 @@ extension on _SSHTabPageState {
_tabMap.remove(name);
_tabRN.notify();
_pageCtrl.previousPage(
duration: Durations.medium1, curve: Curves.fastEaseInToSlowEaseOut);
_pageCtrl.previousPage(duration: Durations.medium1, curve: Curves.fastEaseInToSlowEaseOut);
}
}
@@ -278,8 +280,7 @@ class _AddPage extends StatelessWidget {
const itemHeight = 50.0;
final visualCrossCount = viewWidth / itemWidth;
final crossCount =
max(viewWidth ~/ (visualCrossCount * itemPadding + itemWidth), 1);
final crossCount = max(viewWidth ~/ (visualCrossCount * itemPadding + itemWidth), 1);
final mainCount = itemCount ~/ crossCount + 1;
return ServerProvider.serverOrder.listenVal((order) {

View File

@@ -9,9 +9,10 @@ import 'package:server_box/data/provider/server.dart';
import 'package:server_box/data/provider/sftp.dart';
import 'package:server_box/data/res/misc.dart';
import 'package:server_box/core/route.dart';
import 'package:server_box/data/model/app/path_with_prefix.dart';
import 'package:server_box/view/page/editor.dart';
import 'package:server_box/view/page/storage/sftp.dart';
import 'package:server_box/view/page/storage/sftp_mission.dart';
final class LocalFilePageArgs {
final bool? isPickFile;
@@ -29,15 +30,14 @@ class LocalFilePage extends StatefulWidget {
static const route = AppRoute<String, LocalFilePageArgs>(
page: LocalFilePage.new,
path: '/local_file',
path: '/files/local',
);
@override
State<LocalFilePage> createState() => _LocalFilePageState();
}
class _LocalFilePageState extends State<LocalFilePage>
with AutomaticKeepAliveClientMixin {
class _LocalFilePageState extends State<LocalFilePage> with AutomaticKeepAliveClientMixin {
late final _path = LocalPath(widget.args?.initDir ?? Paths.file);
final _sortType = _SortType.name.vn;
bool get isPickFile => widget.args?.isPickFile ?? false;
@@ -125,13 +125,9 @@ class _LocalFilePageState extends State<LocalFilePage>
return CardX(
child: ListTile(
leading: isDir
? const Icon(Icons.folder_open)
: const Icon(Icons.insert_drive_file),
leading: isDir ? const Icon(Icons.folder_open) : const Icon(Icons.insert_drive_file),
title: Text(fileName),
subtitle: isDir
? null
: Text(stat.size.bytes2Str, style: UIs.textGrey),
subtitle: isDir ? null : Text(stat.size.bytes2Str, style: UIs.textGrey),
trailing: Text(
stat.modified.ymdhms(),
style: UIs.textGrey,
@@ -262,10 +258,11 @@ class _LocalFilePageState extends State<LocalFilePage>
);
if (spi == null) return;
final remotePath = await AppRoutes.sftp(
final args = SftpPageArgs(
spi: spi,
isSelect: true,
).go<String>(context);
);
final remotePath = await SftpPage.route.go(context, args);
if (remotePath == null) {
return;
}
@@ -346,7 +343,7 @@ class _LocalFilePageState extends State<LocalFilePage>
Widget _buildMissionBtn() {
return IconButton(
icon: const Icon(Icons.downloading),
onPressed: () => AppRoutes.sftpMission().go(context),
onPressed: () => SftpMissionPage.route.go(context),
);
}
@@ -383,8 +380,7 @@ enum _SortType {
files.sort((a, b) => a.statSync().size.compareTo(b.statSync().size));
break;
case _SortType.time:
files.sort(
(a, b) => a.statSync().modified.compareTo(b.statSync().modified));
files.sort((a, b) => a.statSync().modified.compareTo(b.statSync().modified));
break;
}
return files;

View File

@@ -6,7 +6,6 @@ import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/core/extension/sftpfile.dart';
import 'package:server_box/core/route.dart';
import 'package:server_box/core/utils/comparator.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/model/sftp/browser_status.dart';
@@ -15,32 +14,48 @@ import 'package:server_box/data/provider/sftp.dart';
import 'package:server_box/data/res/misc.dart';
import 'package:server_box/data/res/store.dart';
import 'package:server_box/view/page/editor.dart';
import 'package:server_box/view/page/ssh/page.dart';
import 'package:server_box/view/page/storage/local.dart';
import 'package:server_box/view/page/storage/sftp_mission.dart';
import 'package:server_box/view/widget/omit_start_text.dart';
import 'package:server_box/view/widget/two_line_text.dart';
import 'package:server_box/view/widget/unix_perm.dart';
import 'package:icons_plus/icons_plus.dart';
class SftpPage extends StatefulWidget {
final class SftpPageArgs {
final Spi spi;
final String? initPath;
final bool isSelect;
final String? initPath;
const SftpPageArgs({
required this.spi,
this.isSelect = false,
this.initPath,
});
}
class SftpPage extends StatefulWidget {
final SftpPageArgs args;
const SftpPage({
super.key,
required this.spi,
required this.isSelect,
this.initPath,
required this.args,
});
@override
State<SftpPage> createState() => _SftpPageState();
static const route = AppRouteArg<String, SftpPageArgs>(
page: SftpPage.new,
path: '/sftp',
);
}
class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
late final _status = SftpBrowserStatus(_client);
late final _client = widget.spi.server!.value.client!;
late final _client = widget.args.spi.server!.value.client!;
final _sortOption = _SortOption().vn;
@override
@@ -54,7 +69,7 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
final children = [
Btn.icon(
icon: const Icon(Icons.downloading),
onTap: () => AppRoutes.sftpMission().go(context),
onTap: () => SftpMissionPage.route.go(context),
),
_buildSortMenu(),
_buildSearchBtn(),
@@ -63,7 +78,7 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
return Scaffold(
appBar: CustomAppBar(
title: TwoLineText(up: 'SFTP', down: widget.spi.name),
title: TwoLineText(up: 'SFTP', down: widget.args.spi.name),
actions: children,
),
body: _buildFileView(),
@@ -75,13 +90,13 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
FutureOr<void> afterFirstLayout(BuildContext context) {
var initPath = '/';
if (Stores.setting.sftpOpenLastPath.fetch()) {
final history = Stores.history.sftpLastPath.fetch(widget.spi.id);
final history = Stores.history.sftpLastPath.fetch(widget.args.spi.id);
if (history != null) {
initPath = history;
}
}
_status.path.path = widget.initPath ?? initPath;
_status.path.path = widget.args.initPath ?? initPath;
_listDir();
}
}
@@ -93,9 +108,7 @@ extension _UI on _SftpPageState {
(_SortType.size, l10n.size),
(_SortType.time, l10n.time),
];
return ValBuilder(
listenable: _sortOption,
builder: (value) {
return _sortOption.listenVal((value) {
return PopupMenuButton<_SortType>(
icon: const Icon(Icons.sort),
itemBuilder: (context) {
@@ -130,7 +143,7 @@ extension _UI on _SftpPageState {
}
Widget _buildBottom() {
final children = widget.isSelect
final children = widget.args.isSelect
? [
IconButton(
onPressed: () => context.pop(_status.path.path),
@@ -168,7 +181,7 @@ extension _UI on _SftpPageState {
return RefreshIndicator(
onRefresh: _listDir,
child: FadeIn(
key: Key(widget.spi.name + _status.path.path),
key: Key(widget.args.spi.name + _status.path.path),
child: ValBuilder(
listenable: _sortOption,
builder: (sortOption) {
@@ -305,7 +318,8 @@ extension _Actions on _SftpPageState {
if (editor.isNotEmpty) {
// Use single quote to avoid escape
final cmd = "$editor '${_getRemotePath(name)}'";
await AppRoutes.ssh(spi: widget.spi, initCmd: cmd).go(context);
final args = SshPageArgs(spi: widget.args.spi, initCmd: cmd);
await SSHPage.route.go(context, args);
await _listDir();
return;
}
@@ -324,7 +338,7 @@ extension _Actions on _SftpPageState {
final localPath = _getLocalPath(remotePath);
final completer = Completer();
final req = SftpReq(
widget.spi,
widget.args.spi,
remotePath,
localPath,
SftpReqType.download,
@@ -368,7 +382,7 @@ extension _Actions on _SftpPageState {
SftpProvider.add(
SftpReq(
widget.spi,
widget.args.spi,
remotePath,
_getLocalPath(remotePath),
SftpReqType.download,
@@ -604,7 +618,8 @@ extension _Actions on _SftpPageState {
);
if (confirm != true) return;
await AppRoutes.ssh(spi: widget.spi, initCmd: cmd).go(context);
final args = SshPageArgs(spi: widget.args.spi, initCmd: cmd);
await SSHPage.route.go(context, args);
_listDir();
}
@@ -616,7 +631,7 @@ extension _Actions on _SftpPageState {
/// Local file dir + server id + remote path
String _getLocalPath(String remotePath) {
return Paths.file.joinPath(widget.spi.id).joinPath(remotePath);
return Paths.file.joinPath(widget.args.spi.id).joinPath(remotePath);
}
/// Only return true if the path is changed
@@ -653,7 +668,7 @@ extension _Actions on _SftpPageState {
// Only update history when success
if (Stores.setting.sftpOpenLastPath.fetch()) {
Stores.history.sftpLastPath.put(widget.spi.id, listPath);
Stores.history.sftpLastPath.put(widget.args.spi.id, listPath);
}
return true;
@@ -735,7 +750,7 @@ extension _Actions on _SftpPageState {
final remotePath = '$remoteDir/$fileName';
Loggers.app.info('SFTP upload local: $path, remote: $remotePath');
SftpProvider.add(
SftpReq(widget.spi, remotePath, path, SftpReqType.upload),
SftpReq(widget.args.spi, remotePath, path, SftpReqType.upload),
);
},
icon: const Icon(Icons.upload_file),
@@ -817,7 +832,7 @@ extension _Actions on _SftpPageState {
Widget _buildHomeBtn() {
return IconButton(
onPressed: () {
final user = widget.spi.user;
final user = widget.args.spi.user;
_status.path.path = user != 'root' ? '/home/$user' : '/root';
_listDir();
},

View File

@@ -10,6 +10,11 @@ class SftpMissionPage extends StatefulWidget {
@override
State<SftpMissionPage> createState() => _SftpMissionPageState();
static const route = AppRouteNoArg(
page: SftpMissionPage.new,
path: '/sftp/mission',
);
}
class _SftpMissionPageState extends State<SftpMissionPage> {

View File

@@ -4,24 +4,17 @@ import 'package:server_box/core/route.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/model/server/systemd.dart';
import 'package:server_box/data/provider/systemd.dart';
final class SystemdPageArgs {
final Spi spi;
const SystemdPageArgs({
required this.spi,
});
}
import 'package:server_box/view/page/ssh/page.dart';
final class SystemdPage extends StatefulWidget {
final SystemdPageArgs args;
final SpiRequiredArgs args;
const SystemdPage({
super.key,
required this.args,
});
static const route = AppRouteArg<void, SystemdPageArgs>(
static const route = AppRouteArg<void, SpiRequiredArgs>(
page: SystemdPage.new,
path: '/systemd',
);
@@ -123,7 +116,8 @@ final class _SystemdPageState extends State<SystemdPage> {
);
if (sure != true) return;
AppRoutes.ssh(spi: widget.args.spi, initCmd: cmd).go(context);
final args = SshPageArgs(spi: widget.args.spi, initCmd: cmd);
SSHPage.route.go(context, args);
},
);
}

View File

@@ -9,7 +9,11 @@ import 'package:server_box/data/model/server/snippet.dart';
import 'package:server_box/data/provider/server.dart';
import 'package:server_box/data/provider/snippet.dart';
import 'package:server_box/data/res/store.dart';
import 'package:server_box/view/page/container.dart';
import 'package:server_box/view/page/iperf.dart';
import 'package:server_box/view/page/process.dart';
import 'package:server_box/view/page/ssh/page.dart';
import 'package:server_box/view/page/storage/sftp.dart';
import 'package:server_box/view/page/systemd.dart';
import 'package:server_box/core/route.dart';
@@ -27,9 +31,7 @@ class ServerFuncBtnsTopRight extends StatelessWidget {
@override
Widget build(BuildContext context) {
return PopupMenu<ServerFuncBtn>(
items: ServerFuncBtn.values
.map((e) => PopMenu.build(e, e.icon, e.toStr))
.toList(),
items: ServerFuncBtn.values.map((e) => PopMenu.build(e, e.icon, e.toStr)).toList(),
padding: const EdgeInsets.symmetric(horizontal: 10),
onSelected: (val) => _onTapMoreBtns(val, spi, context),
);
@@ -46,48 +48,64 @@ class ServerFuncBtns extends StatelessWidget {
@override
Widget build(BuildContext context) {
final btns = () {
try {
final vals = <ServerFuncBtn>[];
final list = Stores.setting.serverFuncBtns.fetch();
for (final idx in list) {
if (idx < 0 || idx >= ServerFuncBtn.values.length) continue;
vals.add(ServerFuncBtn.values[idx]);
}
return vals;
} catch (e) {
return ServerFuncBtn.values;
}
}();
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: btns
.map(
(e) => Stores.setting.moveServerFuncs.fetch()
? IconButton(
onPressed: () => _onTapMoreBtns(e, spi, context),
padding: EdgeInsets.zero,
tooltip: e.toStr,
icon: Icon(e.icon, size: 15),
)
: Padding(
padding: const EdgeInsets.only(bottom: 13),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: () => _onTapMoreBtns(e, spi, context),
padding: EdgeInsets.zero,
icon: Icon(e.icon, size: 17),
),
Text(e.toStr, style: UIs.text11Grey)
],
),
),
)
.toList(),
final btns = this.btns;
if (btns.isEmpty) return UIs.placeholder;
return SizedBox(
height: 70,
child: ListView.builder(
itemCount: btns.length,
scrollDirection: Axis.horizontal,
padding: EdgeInsets.symmetric(horizontal: 13),
itemBuilder: (context, index) {
final value = btns[index];
final item = _buildItem(context, value);
return item.paddingSymmetric(horizontal: 7);
},
),
);
}
Widget _buildItem(BuildContext context, ServerFuncBtn e) {
final move = Stores.setting.moveServerFuncs.fetch();
if (move) {
return IconButton(
onPressed: () => _onTapMoreBtns(e, spi, context),
padding: EdgeInsets.zero,
tooltip: e.toStr,
icon: Icon(e.icon, size: 15),
);
}
return Padding(
padding: const EdgeInsets.only(bottom: 13),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: () => _onTapMoreBtns(e, spi, context),
padding: EdgeInsets.zero,
icon: Icon(e.icon, size: 17),
),
Text(e.toStr, style: UIs.text11Grey)
],
),
);
}
List<ServerFuncBtn> get btns {
try {
final vals = <ServerFuncBtn>[];
final list = Stores.setting.serverFuncBtns.fetch();
for (final idx in list) {
if (idx < 0 || idx >= ServerFuncBtn.values.length) continue;
vals.add(ServerFuncBtn.values[idx]);
}
return vals;
} catch (e) {
return ServerFuncBtn.values;
}
}
}
void _onTapMoreBtns(
@@ -95,15 +113,22 @@ void _onTapMoreBtns(
Spi spi,
BuildContext context,
) async {
// final isMobile = ResponsiveBreakpoints.of(context).isMobile;
switch (value) {
// case ServerFuncBtn.pkg:
// _onPkg(context, spi);
// break;
case ServerFuncBtn.sftp:
AppRoutes.sftp(spi: spi).checkGo(
context: context,
check: () => _checkClient(context, spi.id),
);
if (!_checkClient(context, spi.id)) return;
final args = SftpPageArgs(spi: spi);
// if (isMobile) {
SftpPage.route.go(context, args);
// } else {
// SplitViewNavigator.of(context)?.replace(
// SftpPage.route.toWidget(args: args),
// );
// }
break;
case ServerFuncBtn.snippet:
if (SnippetProvider.snippets.value.isEmpty) {
@@ -141,32 +166,62 @@ void _onTapMoreBtns(
],
);
if (sure != true) return;
AppRoutes.ssh(spi: spi, initSnippet: snippet).checkGo(
context: context,
check: () => _checkClient(context, spi.id),
);
if (!_checkClient(context, spi.id)) return;
final args = SshPageArgs(spi: spi, initSnippet: snippet);
// if (isMobile) {
SSHPage.route.go(context, args);
// } else {
// SplitViewNavigator.of(context)?.replace(
// SSHPage.route.toWidget(args: args),
// );
// }
break;
case ServerFuncBtn.container:
AppRoutes.docker(spi: spi).checkGo(
context: context,
check: () => _checkClient(context, spi.id),
);
if (!_checkClient(context, spi.id)) return;
final args = SpiRequiredArgs(spi);
if (isMobile) {
ContainerPage.route.go(context, args);
} else {
SplitViewNavigator.of(context)?.replace(
ContainerPage.route.toWidget(args: args),
);
}
break;
case ServerFuncBtn.process:
AppRoutes.process(spi: spi).checkGo(
context: context,
check: () => _checkClient(context, spi.id),
);
if (!_checkClient(context, spi.id)) return;
final args = SpiRequiredArgs(spi);
// if (isMobile) {
ProcessPage.route.go(context, args);
// } else {
// SplitViewNavigator.of(context)?.replace(
// ProcessPage.route.toWidget(args: args),
// );
// }
break;
case ServerFuncBtn.terminal:
_gotoSSH(spi, context);
break;
case ServerFuncBtn.iperf:
if (!_checkClient(context, spi.id)) return;
IPerfPage.route.go(context, IPerfPageArgs(spi: spi));
final args = SpiRequiredArgs(spi);
// if (isMobile) {
IPerfPage.route.go(context, args);
// } else {
// SplitViewNavigator.of(context)?.replace(
// IPerfPage.route.toWidget(args: args),
// );
// }
break;
case ServerFuncBtn.systemd:
SystemdPage.route.go(context, SystemdPageArgs(spi: spi));
if (!_checkClient(context, spi.id)) return;
final args = SpiRequiredArgs(spi);
// if (isMobile) {
SystemdPage.route.go(context, args);
// } else {
// SplitViewNavigator.of(context)?.replace(
// SystemdPage.route.toWidget(args: args),
// );
// }
break;
}
}
@@ -174,7 +229,8 @@ void _onTapMoreBtns(
void _gotoSSH(Spi spi, BuildContext context) async {
// run built-in ssh on macOS due to incompatibility
if (isMobile || isMacOS) {
AppRoutes.ssh(spi: spi).go(context);
final args = SshPageArgs(spi: spi);
SSHPage.route.go(context, args);
return;
}
final extraArgs = <String>[];

View File

@@ -22,6 +22,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.11.0"
analyzer_plugin:
dependency: transitive
description:
name: analyzer_plugin
sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161"
url: "https://pub.dev"
source: hosted
version: "0.11.3"
animations:
dependency: transitive
description:
name: animations
sha256: d3d6dcfb218225bbe68e87ccf6378bbb2e32a94900722c5f81611dad089911cb
url: "https://pub.dev"
source: hosted
version: "2.0.11"
ansicolor:
dependency: transitive
description:
@@ -238,6 +254,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.2"
ci:
dependency: transitive
description:
name: ci
sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13"
url: "https://pub.dev"
source: hosted
version: "0.1.0"
circle_chart:
dependency: "direct main"
description:
@@ -247,6 +271,14 @@ packages:
url: "https://github.com/lollipopkit/circle_chart"
source: git
version: "0.0.3"
cli_util:
dependency: transitive
description:
name: cli_util
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
url: "https://pub.dev"
source: hosted
version: "0.4.2"
clock:
dependency: transitive
description:
@@ -328,6 +360,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.2"
custom_lint:
dependency: "direct dev"
description:
name: custom_lint
sha256: "3486c470bb93313a9417f926c7dd694a2e349220992d7b9d14534dc49c15bba9"
url: "https://pub.dev"
source: hosted
version: "0.7.0"
custom_lint_builder:
dependency: transitive
description:
name: custom_lint_builder
sha256: "42cdc41994eeeddab0d7a722c7093ec52bd0761921eeb2cbdbf33d192a234759"
url: "https://pub.dev"
source: hosted
version: "0.7.0"
custom_lint_core:
dependency: transitive
description:
name: custom_lint_core
sha256: "02450c3e45e2a6e8b26c4d16687596ab3c4644dd5792e3313aa9ceba5a49b7f5"
url: "https://pub.dev"
source: hosted
version: "0.7.0"
custom_lint_visitor:
dependency: transitive
description:
name: custom_lint_visitor
sha256: bfe9b7a09c4775a587b58d10ebb871d4fe618237639b1e84d5ec62d7dfef25f9
url: "https://pub.dev"
source: hosted
version: "1.0.0+6.11.0"
dart_style:
dependency: transitive
description:
@@ -394,7 +458,7 @@ packages:
source: hosted
version: "5.0.3"
equatable:
dependency: transitive
dependency: "direct main"
description:
name: equatable
sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7"
@@ -478,8 +542,8 @@ packages:
dependency: "direct main"
description:
path: "."
ref: "v1.0.269"
resolved-ref: "338eb9135ea24f1d936ec023511a22d6c409576d"
ref: "v1.0.281"
resolved-ref: cff32a02af3622926d10c209fe07abe64408675c
url: "https://github.com/lppcg/fl_lib"
source: git
version: "0.0.1"
@@ -488,14 +552,6 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_adaptive_scaffold:
dependency: "direct main"
description:
name: flutter_adaptive_scaffold
sha256: "7279d74da2f2531a16d21c2ec327308778c3aedd672dfe4eaf3bf416463501f8"
url: "https://pub.dev"
source: hosted
version: "0.3.2"
flutter_displaymode:
dependency: "direct main"
description:
@@ -638,7 +694,7 @@ packages:
source: hosted
version: "5.5.0"
flutter_riverpod:
dependency: transitive
dependency: "direct main"
description:
name: flutter_riverpod
sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1"
@@ -663,8 +719,16 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
freezed:
dependency: "direct dev"
description:
name: freezed
sha256: "44c19278dd9d89292cf46e97dc0c1e52ce03275f40a97c5a348e802a924bf40e"
url: "https://pub.dev"
source: hosted
version: "2.5.7"
freezed_annotation:
dependency: transitive
dependency: "direct main"
description:
name: freezed_annotation
sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
@@ -735,6 +799,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.1"
hotreloader:
dependency: transitive
description:
name: hotreloader
sha256: bc167a1163807b03bada490bfe2df25b0d744df359227880220a5cbd04e5734b
url: "https://pub.dev"
source: hosted
version: "4.3.0"
html:
dependency: transitive
description:
@@ -983,6 +1055,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.0"
multi_split_view:
dependency: transitive
description:
name: multi_split_view
sha256: "99c02f128e7423818d13b8f2e01e3027e953b35508019dcee214791bd0525db5"
url: "https://pub.dev"
source: hosted
version: "3.6.0"
nested:
dependency: transitive
description:
@@ -1200,6 +1280,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.2.2"
responsive_framework:
dependency: "direct main"
description:
name: responsive_framework
sha256: a8e1c13d4ba980c60cbf6fa1e9907cd60662bf2585184d7c96ca46c43de91552
url: "https://pub.dev"
source: hosted
version: "1.5.1"
riverpod:
dependency: transitive
description:
@@ -1208,14 +1296,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.6.1"
riverpod_annotation:
riverpod_analyzer_utils:
dependency: transitive
description:
name: riverpod_analyzer_utils
sha256: c6b8222b2b483cb87ae77ad147d6408f400c64f060df7a225b127f4afef4f8c8
url: "https://pub.dev"
source: hosted
version: "0.5.8"
riverpod_annotation:
dependency: "direct main"
description:
name: riverpod_annotation
sha256: e14b0bf45b71326654e2705d462f21b958f987087be850afd60578fcd502d1b8
url: "https://pub.dev"
source: hosted
version: "2.6.1"
riverpod_generator:
dependency: "direct dev"
description:
name: riverpod_generator
sha256: "63546d70952015f0981361636bf8f356d9cfd9d7f6f0815e3c07789a41233188"
url: "https://pub.dev"
source: hosted
version: "2.6.3"
riverpod_lint:
dependency: "direct dev"
description:
name: riverpod_lint
sha256: "83e4caa337a9840469b7b9bd8c2351ce85abad80f570d84146911b32086fbd99"
url: "https://pub.dev"
source: hosted
version: "2.6.3"
rxdart:
dependency: transitive
description:
name: rxdart
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
url: "https://pub.dev"
source: hosted
version: "0.28.0"
screen_retriever:
dependency: transitive
description:

View File

@@ -1,6 +1,6 @@
name: server_box
description: server status & toolbox app.
publish_to: 'none'
publish_to: "none"
version: 1.0.1128+1128
environment:
@@ -16,6 +16,7 @@ dependencies:
easy_isolate: ^1.3.0
intl: ^0.19.0
highlight: ^0.7.0
equatable: ^2.0.7
flutter_highlight: ^0.7.0
code_text_field: ^1.1.0
shared_preferences: ^2.1.1
@@ -31,7 +32,10 @@ dependencies:
choice: ^2.3.2
flutter_reorderable_grid_view: ^5.1.0
webdav_client_plus: ^1.0.2
flutter_adaptive_scaffold: ^0.3.2
freezed_annotation: ^2.4.4
flutter_riverpod: ^2.6.1
riverpod_annotation: ^2.6.1
responsive_framework: ^1.5.1
dartssh2:
git:
url: https://github.com/lollipopkit/dartssh2
@@ -59,7 +63,7 @@ dependencies:
fl_lib:
git:
url: https://github.com/lppcg/fl_lib
ref: v1.0.269
ref: v1.0.281
dependency_overrides:
# webdav_client_plus:
@@ -76,9 +80,13 @@ dependency_overrides:
dev_dependencies:
flutter_native_splash: ^2.1.6
hive_generator: ^2.0.0
build_runner: ^2.3.2
build_runner: ^2.4.15
flutter_lints: ^5.0.0
json_serializable: ^6.8.0
freezed: ^2.5.7
riverpod_generator: ^2.6.3
custom_lint: ^0.7.0
riverpod_lint: ^2.6.3
flutter_test:
sdk: flutter
fl_build:
@@ -132,21 +140,21 @@ flutter_native_splash:
# size of the app. Only one parameter can be used, color and background_image cannot both be set.
color: "#ffffff"
#background_image: "assets/background.png"
# Optional parameters are listed below. To enable a parameter, uncomment the line by removing
# Optional parameters are listed below. To enable a parameter, uncomment the line by removing
# the leading # character.
# The image parameter allows you to specify an image used in the splash screen. It must be a
# The image parameter allows you to specify an image used in the splash screen. It must be a
# png file and should be sized for 4x pixel density.
image: assets/app_icon.png
# The color_dark, background_image_dark, and image_dark are parameters that set the background
# and image when the device is in dark mode. If they are not specified, the app will use the
# parameters from above. If the image_dark parameter is specified, color_dark or
# parameters from above. If the image_dark parameter is specified, color_dark or
# background_image_dark must be specified. color_dark and background_image_dark cannot both be
# set.
color_dark: "#121212"
#background_image_dark: "assets/dark-background.png"
#image_dark: assets/splash-invert.png
# The android, ios and web parameters can be used to disable generating a splash screen on a given
# The android, ios and web parameters can be used to disable generating a splash screen on a given
# platform.
#android: false
#ios: false
@@ -154,33 +162,33 @@ flutter_native_splash:
# The position of the splash image can be set with android_gravity, ios_content_mode, and
# web_image_mode parameters. All default to center.
#
# android_gravity can be one of the following Android Gravity (see
# https://developer.android.com/reference/android/view/Gravity): bottom, center,
# android_gravity can be one of the following Android Gravity (see
# https://developer.android.com/reference/android/view/Gravity): bottom, center,
# center_horizontal, center_vertical, clip_horizontal, clip_vertical, end, fill, fill_horizontal,
# fill_vertical, left, right, start, or top.
#android_gravity: center
#
# ios_content_mode can be one of the following iOS UIView.ContentMode (see
# https://developer.apple.com/documentation/uikit/uiview/contentmode): scaleToFill,
# scaleAspectFit, scaleAspectFill, center, top, bottom, left, right, topLeft, topRight,
# ios_content_mode can be one of the following iOS UIView.ContentMode (see
# https://developer.apple.com/documentation/uikit/uiview/contentmode): scaleToFill,
# scaleAspectFit, scaleAspectFill, center, top, bottom, left, right, topLeft, topRight,
# bottomLeft, or bottomRight.
#ios_content_mode: center
#
# web_image_mode can be one of the following modes: center, contain, stretch, and cover.
#web_image_mode: center
# To hide the notification bar, use the fullscreen parameter. Has no affect in web since web
# To hide the notification bar, use the fullscreen parameter. Has no affect in web since web
# has no notification bar. Defaults to false.
# NOTE: Unlike Android, iOS will not automatically show the notification bar when the app loads.
# To show the notification bar, add the following code to your Flutter app:
# WidgetsFlutterBinding.ensureInitialized();
# SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom, SystemUiOverlay.top]);
#fullscreen: true
# If you have changed the name(s) of your info.plist file(s), you can specify the filename(s)
# If you have changed the name(s) of your info.plist file(s), you can specify the filename(s)
# with the info_plist_files parameter. Remove only the # characters in the three lines below,
# do not remove any spaces:
info_plist_files:
- 'ios/Runner/Info-Debug.plist'
- 'ios/Runner/Info-Profile.plist'
- 'ios/Runner/Info-Release.plist'
- "ios/Runner/Info-Debug.plist"
- "ios/Runner/Info-Profile.plist"
- "ios/Runner/Info-Release.plist"
# To enable support for Android 12, set the following parameter to true. Defaults to false.
android12: true