mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 07:14:28 +01:00
optimization: desktop UI (#747)
This commit is contained in:
@@ -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
|
||||
|
||||
133
lib/app.dart
133
lib/app.dart
@@ -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,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
144
lib/data/provider/app.freezed.dart
Normal file
144
lib/data/provider/app.freezed.dart
Normal 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;
|
||||
}
|
||||
24
lib/data/provider/app.g.dart
Normal file
24
lib/data/provider/app.g.dart
Normal 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
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
27
lib/view/page/server/tab/card_stat.dart
Normal file
27
lib/view/page/server/tab/card_stat.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
195
lib/view/page/server/tab/content.dart
Normal file
195
lib/view/page/server/tab/content.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
69
lib/view/page/server/tab/landscape.dart
Normal file
69
lib/view/page/server/tab/landscape.dart
Normal 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,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
369
lib/view/page/server/tab/tab.dart
Normal file
369
lib/view/page/server/tab/tab.dart
Normal 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;
|
||||
}
|
||||
@@ -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(
|
||||
243
lib/view/page/server/tab/utils.dart
Normal file
243
lib/view/page/server/tab/utils.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>[];
|
||||
|
||||
148
pubspec.lock
148
pubspec.lock
@@ -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:
|
||||
|
||||
44
pubspec.yaml
44
pubspec.yaml
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user