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:
|
SPEC CHECKSUMS:
|
||||||
app_links: 76b66b60cc809390ca1ad69bfd66b998d2387ac7
|
app_links: 76b66b60cc809390ca1ad69bfd66b998d2387ac7
|
||||||
camera_avfoundation: 04b44aeb14070126c6529e5ab82cc7c9fca107cf
|
camera_avfoundation: be3be85408cd4126f250386828e9b1dfa40ab436
|
||||||
file_picker: fb04e739ae6239a76ce1f571863a196a922c87d4
|
file_picker: fb04e739ae6239a76ce1f571863a196a922c87d4
|
||||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||||
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
|
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
|
||||||
@@ -112,7 +112,7 @@ SPEC CHECKSUMS:
|
|||||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||||
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
||||||
wakelock_plus: 04623e3f525556020ebd4034310f20fe7fda8b49
|
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||||
watch_connectivity: 88e5bea25b473e66ef8d3f960954d154ed0356d6
|
watch_connectivity: 88e5bea25b473e66ef8d3f960954d154ed0356d6
|
||||||
|
|
||||||
PODFILE CHECKSUM: ec6ef69056f066e8b21a3391082f23b5ad2d37f8
|
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/fl_lib.dart';
|
||||||
import 'package:fl_lib/generated/l10n/lib_l10n.dart';
|
import 'package:fl_lib/generated/l10n/lib_l10n.dart';
|
||||||
import 'package:flutter/material.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/core/extension/context/locale.dart';
|
||||||
import 'package:server_box/data/res/build_data.dart';
|
import 'package:server_box/data/res/build_data.dart';
|
||||||
import 'package:server_box/data/res/store.dart';
|
import 'package:server_box/data/res/store.dart';
|
||||||
import 'package:server_box/generated/l10n/l10n.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';
|
import 'package:icons_plus/icons_plus.dart';
|
||||||
|
|
||||||
part 'intro.dart';
|
part 'intro.dart';
|
||||||
@@ -22,47 +23,67 @@ class MyApp extends StatelessWidget {
|
|||||||
listenable: RNodes.app,
|
listenable: RNodes.app,
|
||||||
builder: (context, _) {
|
builder: (context, _) {
|
||||||
if (!Stores.setting.useSystemPrimaryColor.fetch()) {
|
if (!Stores.setting.useSystemPrimaryColor.fetch()) {
|
||||||
final colorSeed = Color(Stores.setting.colorSeed.fetch());
|
return _build(context);
|
||||||
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 DynamicColorBuilder(
|
|
||||||
builder: (light, dark) {
|
return _buildDynamicColor(context);
|
||||||
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,
|
Widget _build(BuildContext context) {
|
||||||
{required ThemeData light, required ThemeData dark}) {
|
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();
|
final tMode = Stores.setting.themeMode.fetch();
|
||||||
// Issue #57
|
// Issue #57
|
||||||
final themeMode = switch (tMode) {
|
final themeMode = switch (tMode) {
|
||||||
@@ -74,6 +95,14 @@ class MyApp extends StatelessWidget {
|
|||||||
|
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
key: ValueKey(locale),
|
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,
|
locale: locale,
|
||||||
localizationsDelegates: const [
|
localizationsDelegates: const [
|
||||||
LibLocalizations.delegate,
|
LibLocalizations.delegate,
|
||||||
@@ -86,21 +115,25 @@ class MyApp extends StatelessWidget {
|
|||||||
themeMode: themeMode,
|
themeMode: themeMode,
|
||||||
theme: light.fixWindowsFont,
|
theme: light.fixWindowsFont,
|
||||||
darkTheme: (tMode < 3 ? dark : dark.toAmoled).fixWindowsFont,
|
darkTheme: (tMode < 3 ? dark : dark.toAmoled).fixWindowsFont,
|
||||||
home: VirtualWindowFrame(
|
home: Builder(
|
||||||
child: Builder(
|
builder: (context) {
|
||||||
builder: (context) {
|
context.setLibL10n();
|
||||||
context.setLibL10n();
|
final appL10n = AppLocalizations.of(context);
|
||||||
final appL10n = AppLocalizations.of(context);
|
if (appL10n != null) l10n = appL10n;
|
||||||
if (appL10n != null) l10n = appL10n;
|
|
||||||
|
|
||||||
final intros = _IntroPage.builders;
|
Widget child;
|
||||||
if (intros.isNotEmpty) {
|
final intros = _IntroPage.builders;
|
||||||
return _IntroPage(intros);
|
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/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 {
|
/// The args class for [AppRoute].
|
||||||
final Widget page;
|
final class SpiRequiredArgs {
|
||||||
final String title;
|
/// The only required argument for this class.
|
||||||
|
final Spi spi;
|
||||||
|
|
||||||
AppRoutes(this.page, this.title);
|
const SpiRequiredArgs(this.spi);
|
||||||
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:fl_lib/fl_lib.dart';
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:server_box/core/extension/context/locale.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/setting/entry.dart';
|
||||||
import 'package:server_box/view/page/snippet/list.dart';
|
import 'package:server_box/view/page/snippet/list.dart';
|
||||||
import 'package:server_box/view/page/ssh/tab.dart';
|
import 'package:server_box/view/page/ssh/tab.dart';
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:fl_lib/fl_lib.dart';
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:json_annotation/json_annotation.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`.
|
/// Nowaday, more fields are added to this class, and it's renamed to `Spi`.
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
@HiveType(typeId: 3)
|
@HiveType(typeId: 3)
|
||||||
class Spi {
|
class Spi with EquatableMixin {
|
||||||
@HiveField(0)
|
@HiveField(0)
|
||||||
final String name;
|
final String name;
|
||||||
@HiveField(1)
|
@HiveField(1)
|
||||||
@@ -81,6 +82,10 @@ class Spi {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => id;
|
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 {
|
extension Spix on Spi {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:fl_lib/fl_lib.dart';
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
@@ -10,7 +11,7 @@ part 'snippet.g.dart';
|
|||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
@HiveType(typeId: 2)
|
@HiveType(typeId: 2)
|
||||||
class Snippet {
|
class Snippet with EquatableMixin {
|
||||||
@HiveField(0)
|
@HiveField(0)
|
||||||
final String name;
|
final String name;
|
||||||
@HiveField(1)
|
@HiveField(1)
|
||||||
@@ -32,11 +33,21 @@ class Snippet {
|
|||||||
this.autoRunOn,
|
this.autoRunOn,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory Snippet.fromJson(Map<String, dynamic> json) =>
|
factory Snippet.fromJson(Map<String, dynamic> json) => _$SnippetFromJson(json);
|
||||||
_$SnippetFromJson(json);
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$SnippetToJson(this);
|
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'\$\{[^{}]+\}');
|
static final fmtFinder = RegExp(r'\$\{[^{}]+\}');
|
||||||
|
|
||||||
String fmtWithSpi(Spi spi) {
|
String fmtWithSpi(Spi spi) {
|
||||||
|
|||||||
@@ -1,7 +1,27 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
final class AppProvider {
|
part 'app.g.dart';
|
||||||
const AppProvider._();
|
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() {
|
List<Snippet> fetch() {
|
||||||
final keys = box.keys;
|
final ss = <Snippet>{};
|
||||||
final ss = <Snippet>[];
|
for (final key in keys()) {
|
||||||
for (final key in keys) {
|
|
||||||
final s = box.get(key);
|
final s = box.get(key);
|
||||||
if (s != null && s is Snippet) {
|
if (s != null && s is Snippet) {
|
||||||
ss.add(s);
|
ss.add(s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ss;
|
return ss.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
void delete(Snippet s) {
|
void delete(Snippet s) {
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ class BackupPage extends StatefulWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
State<BackupPage> createState() => _BackupPageState();
|
State<BackupPage> createState() => _BackupPageState();
|
||||||
|
|
||||||
|
static const route = AppRouteNoArg(
|
||||||
|
page: BackupPage.new,
|
||||||
|
path: '/backup',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final class _BackupPageState extends State<BackupPage>
|
final class _BackupPageState extends State<BackupPage>
|
||||||
@@ -246,7 +251,7 @@ final class _BackupPageState extends State<BackupPage>
|
|||||||
onTap: () async {
|
onTap: () async {
|
||||||
final data = await context.showImportDialog(
|
final data = await context.showImportDialog(
|
||||||
title: l10n.snippet,
|
title: l10n.snippet,
|
||||||
modelDef: Snippet.example.toJson(),
|
modelDef: SnippetX.example.toJson(),
|
||||||
);
|
);
|
||||||
if (data == null) return;
|
if (data == null) return;
|
||||||
final str = String.fromCharCodes(data);
|
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/container/ps.dart';
|
||||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||||
import 'package:server_box/data/provider/container.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';
|
import 'package:server_box/view/widget/two_line_text.dart';
|
||||||
|
|
||||||
class ContainerPage extends StatefulWidget {
|
class ContainerPage extends StatefulWidget {
|
||||||
final Spi spi;
|
final SpiRequiredArgs args;
|
||||||
const ContainerPage({required this.spi, super.key});
|
const ContainerPage({required this.args, super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ContainerPage> createState() => _ContainerPageState();
|
State<ContainerPage> createState() => _ContainerPageState();
|
||||||
|
|
||||||
|
static const route = AppRouteArg(
|
||||||
|
page: ContainerPage.new,
|
||||||
|
path: '/container',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ContainerPageState extends State<ContainerPage> {
|
class _ContainerPageState extends State<ContainerPage> {
|
||||||
final _textController = TextEditingController();
|
final _textController = TextEditingController();
|
||||||
late final _container = ContainerProvider(
|
late final _container = ContainerProvider(
|
||||||
client: widget.spi.server?.value.client,
|
client: widget.args.spi.server?.value.client,
|
||||||
userName: widget.spi.user,
|
userName: widget.args.spi.user,
|
||||||
hostId: widget.spi.id,
|
hostId: widget.args.spi.id,
|
||||||
context: context,
|
context: context,
|
||||||
);
|
);
|
||||||
late Size _size;
|
late Size _size;
|
||||||
@@ -55,27 +61,23 @@ class _ContainerPageState extends State<ContainerPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ChangeNotifierProvider(
|
return Consumer<ContainerProvider>(
|
||||||
create: (_) => _container,
|
builder: (_, ___, __) {
|
||||||
builder: (_, __) => Consumer<ContainerProvider>(
|
return Scaffold(
|
||||||
builder: (_, ___, __) {
|
appBar: CustomAppBar(
|
||||||
return Scaffold(
|
centerTitle: true,
|
||||||
appBar: CustomAppBar(
|
title: TwoLineText(up: l10n.container, down: widget.args.spi.name),
|
||||||
centerTitle: true,
|
actions: [
|
||||||
title: TwoLineText(up: l10n.container, down: widget.spi.name),
|
IconButton(
|
||||||
actions: [
|
onPressed: () => context.showLoadingDialog(fn: () => _container.refresh()),
|
||||||
IconButton(
|
icon: const Icon(Icons.refresh),
|
||||||
onPressed: () =>
|
)
|
||||||
context.showLoadingDialog(fn: () => _container.refresh()),
|
],
|
||||||
icon: const Icon(Icons.refresh),
|
),
|
||||||
)
|
body: _buildMain(),
|
||||||
],
|
floatingActionButton: _container.error == null ? _buildFAB() : null,
|
||||||
),
|
);
|
||||||
body: _buildMain(),
|
},
|
||||||
floatingActionButton: _container.error == null ? _buildFAB() : null,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,8 +236,7 @@ class _ContainerPageState extends State<ContainerPage> {
|
|||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
_buildPsItemStatsItem(
|
_buildPsItemStatsItem('Mem', item.mem, Icons.settings_input_component),
|
||||||
'Mem', item.mem, Icons.settings_input_component),
|
|
||||||
UIs.width13,
|
UIs.width13,
|
||||||
_buildPsItemStatsItem('Disk', item.disk, Icons.storage),
|
_buildPsItemStatsItem('Disk', item.disk, Icons.storage),
|
||||||
],
|
],
|
||||||
@@ -263,9 +264,7 @@ class _ContainerPageState extends State<ContainerPage> {
|
|||||||
|
|
||||||
Widget _buildMoreBtn(ContainerPs dItem) {
|
Widget _buildMoreBtn(ContainerPs dItem) {
|
||||||
return PopupMenu(
|
return PopupMenu(
|
||||||
items: ContainerMenu.items(dItem.running)
|
items: ContainerMenu.items(dItem.running).map((e) => PopMenu.build(e, e.icon, e.toStr)).toList(),
|
||||||
.map((e) => PopMenu.build(e, e.icon, e.toStr))
|
|
||||||
.toList(),
|
|
||||||
onSelected: (item) => _onTapMoreBtn(item, dItem),
|
onSelected: (item) => _onTapMoreBtn(item, dItem),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -410,7 +409,7 @@ class _ContainerPageState extends State<ContainerPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _showEditHostDialog() async {
|
Future<void> _showEditHostDialog() async {
|
||||||
final id = widget.spi.id;
|
final id = widget.args.spi.id;
|
||||||
final host = Stores.container.fetch(id);
|
final host = Stores.container.fetch(id);
|
||||||
final ctrl = TextEditingController(text: host);
|
final ctrl = TextEditingController(text: host);
|
||||||
await context.showRoundDialog(
|
await context.showRoundDialog(
|
||||||
@@ -428,7 +427,7 @@ class _ContainerPageState extends State<ContainerPage> {
|
|||||||
|
|
||||||
void _onSaveDockerHost(String val) {
|
void _onSaveDockerHost(String val) {
|
||||||
context.pop();
|
context.pop();
|
||||||
Stores.container.put(widget.spi.id, val.trim());
|
Stores.container.put(widget.args.spi.id, val.trim());
|
||||||
_container.refresh();
|
_container.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -537,22 +536,24 @@ class _ContainerPageState extends State<ContainerPage> {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case ContainerMenu.logs:
|
case ContainerMenu.logs:
|
||||||
AppRoutes.ssh(
|
final args = SshPageArgs(
|
||||||
spi: widget.spi,
|
spi: widget.args.spi,
|
||||||
initCmd: '${switch (_container.type) {
|
initCmd: '${switch (_container.type) {
|
||||||
ContainerType.podman => 'podman',
|
ContainerType.podman => 'podman',
|
||||||
ContainerType.docker => 'docker',
|
ContainerType.docker => 'docker',
|
||||||
}} logs -f --tail 100 ${dItem.id}',
|
}} logs -f --tail 100 ${dItem.id}',
|
||||||
).go(context);
|
);
|
||||||
|
SSHPage.route.go(context, args);
|
||||||
break;
|
break;
|
||||||
case ContainerMenu.terminal:
|
case ContainerMenu.terminal:
|
||||||
AppRoutes.ssh(
|
final args = SshPageArgs(
|
||||||
spi: widget.spi,
|
spi: widget.args.spi,
|
||||||
initCmd: '${switch (_container.type) {
|
initCmd: '${switch (_container.type) {
|
||||||
ContainerType.podman => 'podman',
|
ContainerType.podman => 'podman',
|
||||||
ContainerType.docker => 'docker',
|
ContainerType.docker => 'docker',
|
||||||
}} exec -it ${dItem.id} sh',
|
}} exec -it ${dItem.id} sh',
|
||||||
).go(context);
|
);
|
||||||
|
SSHPage.route.go(context, args);
|
||||||
break;
|
break;
|
||||||
// case DockerMenuType.stats:
|
// case DockerMenuType.stats:
|
||||||
// showRoundDialog(
|
// showRoundDialog(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:fl_lib/fl_lib.dart';
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
import 'package:flutter/material.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/core/chan.dart';
|
||||||
import 'package:server_box/data/model/app/tab.dart';
|
import 'package:server_box/data/model/app/tab.dart';
|
||||||
import 'package:server_box/data/provider/app.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/build_data.dart';
|
||||||
import 'package:server_box/data/res/store.dart';
|
import 'package:server_box/data/res/store.dart';
|
||||||
import 'package:server_box/data/res/url.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';
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||||
|
|
||||||
part 'appbar.dart';
|
|
||||||
|
|
||||||
class HomePage extends StatefulWidget {
|
class HomePage extends StatefulWidget {
|
||||||
const HomePage({super.key});
|
const HomePage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<HomePage> createState() => _HomePageState();
|
State<HomePage> createState() => _HomePageState();
|
||||||
|
|
||||||
|
static const route = AppRouteNoArg(
|
||||||
|
page: HomePage.new,
|
||||||
|
path: '/',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HomePageState extends State<HomePage>
|
class _HomePageState extends State<HomePage>
|
||||||
with
|
with AutomaticKeepAliveClientMixin, AfterLayoutMixin, WidgetsBindingObserver {
|
||||||
AutomaticKeepAliveClientMixin,
|
|
||||||
AfterLayoutMixin,
|
|
||||||
WidgetsBindingObserver {
|
|
||||||
late final PageController _pageController;
|
late final PageController _pageController;
|
||||||
|
|
||||||
final _selectIndex = ValueNotifier(0);
|
final _selectIndex = ValueNotifier(0);
|
||||||
@@ -92,98 +93,81 @@ class _HomePageState extends State<HomePage>
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context);
|
super.build(context);
|
||||||
AppProvider.ctx = context;
|
AppProvider.ctx = context;
|
||||||
final sysPadding = MediaQuery.of(context).padding;
|
final isMobile = ResponsiveBreakpoints.of(context).isMobile;
|
||||||
|
|
||||||
return ColoredBox(
|
return Scaffold(
|
||||||
color: context.theme.colorScheme.surface,
|
appBar: _AppBar(MediaQuery.paddingOf(context).top),
|
||||||
child: AdaptiveLayout(
|
body: Row(
|
||||||
transitionDuration: const Duration(milliseconds: 250),
|
children: [
|
||||||
primaryNavigation: SlotLayout(
|
if (!isMobile) _buildRailBar(),
|
||||||
config: {
|
Expanded(
|
||||||
Breakpoints.medium: SlotLayout.from(
|
child: PageView.builder(
|
||||||
key: const Key('primaryNavigation'),
|
controller: _pageController,
|
||||||
builder: (context) => _buildRailBar(),
|
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() {
|
Widget _buildBottomBar() {
|
||||||
return Stores.setting.fullScreen.fetch()
|
if (Stores.setting.fullScreen.fetch()) return UIs.placeholder;
|
||||||
? UIs.placeholder
|
return ListenableBuilder(
|
||||||
: ListenableBuilder(
|
listenable: _selectIndex,
|
||||||
listenable: _selectIndex,
|
builder: (context, child) => NavigationBar(
|
||||||
builder: (context, child) => NavigationBar(
|
selectedIndex: _selectIndex.value,
|
||||||
selectedIndex: _selectIndex.value,
|
height: kBottomNavigationBarHeight * 1.1,
|
||||||
height: kBottomNavigationBarHeight * 1.1,
|
animationDuration: const Duration(milliseconds: 250),
|
||||||
animationDuration: const Duration(milliseconds: 250),
|
onDestinationSelected: _onDestinationSelected,
|
||||||
onDestinationSelected: _onDestinationSelected,
|
labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected,
|
||||||
labelBehavior:
|
destinations: AppTab.navDestinations,
|
||||||
NavigationDestinationLabelBehavior.onlyShowSelected,
|
),
|
||||||
destinations: AppTab.navDestinations,
|
);
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildRailBar({bool extended = false}) {
|
Widget _buildRailBar({bool extended = false}) {
|
||||||
return Stores.setting.fullScreen.fetch()
|
final fullscreen = Stores.setting.fullScreen.fetch();
|
||||||
? UIs.placeholder
|
if (fullscreen) return UIs.placeholder;
|
||||||
: ListenableBuilder(
|
|
||||||
listenable: _selectIndex,
|
return Stack(
|
||||||
builder: (context, child) =>
|
children: [
|
||||||
AdaptiveScaffold.standardNavigationRail(
|
_selectIndex.listenVal(
|
||||||
extended: extended,
|
(idx) => NavigationRail(
|
||||||
padding: EdgeInsets.only(top: CustomAppBar.sysStatusBarHeight),
|
extended: extended,
|
||||||
selectedIndex: _selectIndex.value,
|
minExtendedWidth: 150,
|
||||||
destinations: AppTab.navRailDestinations,
|
leading: extended ? const SizedBox(height: 20) : null,
|
||||||
onDestinationSelected: _onDestinationSelected,
|
trailing: extended ? const SizedBox(height: 20) : null,
|
||||||
labelType: extended ? null : NavigationRailLabelType.selected,
|
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
|
@override
|
||||||
@@ -231,7 +215,7 @@ class _HomePageState extends State<HomePage>
|
|||||||
|
|
||||||
void _goAuth() {
|
void _goAuth() {
|
||||||
if (Stores.setting.useBioAuth.fetch()) {
|
if (Stores.setting.useBioAuth.fetch()) {
|
||||||
if (BioAuthPage.route.isAlreadyIn) return;
|
if (BioAuthPage.route.alreadyIn) return;
|
||||||
BioAuthPage.route.go(
|
BioAuthPage.route.go(
|
||||||
context,
|
context,
|
||||||
args: BioAuthPageArgs(onAuthSuccess: () => _shouldAuth = false),
|
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:flutter/material.dart';
|
||||||
import 'package:server_box/core/extension/context/locale.dart';
|
import 'package:server_box/core/extension/context/locale.dart';
|
||||||
import 'package:server_box/core/route.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 {
|
class IPerfPage extends StatefulWidget {
|
||||||
final IPerfPageArgs args;
|
final SpiRequiredArgs args;
|
||||||
|
|
||||||
const IPerfPage({super.key, required this.args});
|
const IPerfPage({super.key, required this.args});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<IPerfPage> createState() => _IPerfPageState();
|
State<IPerfPage> createState() => _IPerfPageState();
|
||||||
|
|
||||||
static const route = AppRouteArg<void, IPerfPageArgs>(
|
static const route = AppRouteArg<void, SpiRequiredArgs>(
|
||||||
page: IPerfPage.new,
|
page: IPerfPage.new,
|
||||||
path: '/iperf',
|
path: '/iperf',
|
||||||
);
|
);
|
||||||
@@ -55,10 +50,11 @@ class _IPerfPageState extends State<IPerfPage> {
|
|||||||
context.showSnackBar(libL10n.empty);
|
context.showSnackBar(libL10n.empty);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
AppRoutes.ssh(
|
final args = SshPageArgs(
|
||||||
spi: widget.args.spi,
|
spi: widget.args.spi,
|
||||||
initCmd: 'iperf -c ${_hostCtrl.text} -p ${_portCtrl.text}',
|
initCmd: 'iperf -c ${_hostCtrl.text} -p ${_portCtrl.text}',
|
||||||
).go(context);
|
);
|
||||||
|
SSHPage.route.go(context, args);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ class PingPage extends StatefulWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
State<PingPage> createState() => _PingPageState();
|
State<PingPage> createState() => _PingPageState();
|
||||||
|
|
||||||
|
static const route = AppRouteNoArg(
|
||||||
|
page: PingPage.new,
|
||||||
|
path: '/ping',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PingPageState extends State<PingPage>
|
class _PingPageState extends State<PingPage>
|
||||||
|
|||||||
@@ -13,13 +13,22 @@ import 'package:server_box/data/model/server/private_key_info.dart';
|
|||||||
|
|
||||||
const _format = 'text/plain';
|
const _format = 'text/plain';
|
||||||
|
|
||||||
class PrivateKeyEditPage extends StatefulWidget {
|
final class PrivateKeyEditPageArgs {
|
||||||
const PrivateKeyEditPage({super.key, this.pki});
|
|
||||||
|
|
||||||
final PrivateKeyInfo? pki;
|
final PrivateKeyInfo? pki;
|
||||||
|
const PrivateKeyEditPageArgs({this.pki});
|
||||||
|
}
|
||||||
|
|
||||||
|
class PrivateKeyEditPage extends StatefulWidget {
|
||||||
|
final PrivateKeyEditPageArgs? args;
|
||||||
|
const PrivateKeyEditPage({super.key, this.args});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PrivateKeyEditPage> createState() => _PrivateKeyEditPageState();
|
State<PrivateKeyEditPage> createState() => _PrivateKeyEditPageState();
|
||||||
|
|
||||||
|
static const route = AppRoute(
|
||||||
|
page: PrivateKeyEditPage.new,
|
||||||
|
path: '/private_key/edit',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
|
class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
|
||||||
@@ -34,6 +43,8 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
|
|||||||
|
|
||||||
final _loading = ValueNotifier<Widget?>(null);
|
final _loading = ValueNotifier<Widget?>(null);
|
||||||
|
|
||||||
|
PrivateKeyInfo? get pki => widget.args?.pki;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
@@ -49,9 +60,10 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
if (widget.pki != null) {
|
final pki = this.pki;
|
||||||
_nameController.text = widget.pki!.id;
|
if (pki != null) {
|
||||||
_keyController.text = widget.pki!.key;
|
_nameController.text = pki.id;
|
||||||
|
_keyController.text = pki.key;
|
||||||
} else {
|
} else {
|
||||||
Clipboard.getData(_format).then((value) {
|
Clipboard.getData(_format).then((value) {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
@@ -79,31 +91,34 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CustomAppBar _buildAppBar() {
|
CustomAppBar _buildAppBar() {
|
||||||
final actions = [
|
final pki = this.pki;
|
||||||
IconButton(
|
final actions = pki != null
|
||||||
tooltip: libL10n.delete,
|
? [
|
||||||
onPressed: () {
|
IconButton(
|
||||||
context.showRoundDialog(
|
tooltip: libL10n.delete,
|
||||||
title: libL10n.attention,
|
onPressed: () {
|
||||||
child: Text(libL10n.askContinue(
|
context.showRoundDialog(
|
||||||
'${libL10n.delete} ${l10n.privateKey}(${widget.pki!.id})',
|
title: libL10n.attention,
|
||||||
)),
|
child: Text(libL10n.askContinue(
|
||||||
actions: Btn.ok(
|
'${libL10n.delete} ${l10n.privateKey}(${pki.id})',
|
||||||
onTap: () {
|
)),
|
||||||
PrivateKeyProvider.delete(widget.pki!);
|
actions: Btn.ok(
|
||||||
context.pop();
|
onTap: () {
|
||||||
context.pop();
|
PrivateKeyProvider.delete(pki);
|
||||||
|
context.pop();
|
||||||
|
context.pop();
|
||||||
|
},
|
||||||
|
red: true,
|
||||||
|
).toList,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
red: true,
|
icon: const Icon(Icons.delete),
|
||||||
).toList,
|
)
|
||||||
);
|
]
|
||||||
},
|
: null;
|
||||||
icon: const Icon(Icons.delete),
|
|
||||||
)
|
|
||||||
];
|
|
||||||
return CustomAppBar(
|
return CustomAppBar(
|
||||||
title: Text(libL10n.edit),
|
title: Text(libL10n.edit),
|
||||||
actions: widget.pki == null ? null : actions,
|
actions: actions,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,8 +135,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBody() {
|
Widget _buildBody() {
|
||||||
return ListView(
|
return AutoMultiList(
|
||||||
padding: const EdgeInsets.all(13),
|
|
||||||
children: [
|
children: [
|
||||||
Input(
|
Input(
|
||||||
autoFocus: true,
|
autoFocus: true,
|
||||||
@@ -204,7 +218,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
|
|||||||
try {
|
try {
|
||||||
final decrypted = await Computer.shared.start(decyptPem, [key, pwd]);
|
final decrypted = await Computer.shared.start(decyptPem, [key, pwd]);
|
||||||
final pki = PrivateKeyInfo(id: name, key: decrypted);
|
final pki = PrivateKeyInfo(id: name, key: decrypted);
|
||||||
final originPki = widget.pki;
|
final originPki = this.pki;
|
||||||
if (originPki != null) {
|
if (originPki != null) {
|
||||||
PrivateKeyProvider.update(originPki, pki);
|
PrivateKeyProvider.update(originPki, pki);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -6,26 +6,30 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:server_box/core/extension/context/locale.dart';
|
import 'package:server_box/core/extension/context/locale.dart';
|
||||||
import 'package:server_box/data/res/store.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/model/server/private_key_info.dart';
|
||||||
import 'package:server_box/data/provider/private_key.dart';
|
import 'package:server_box/data/provider/private_key.dart';
|
||||||
|
import 'package:server_box/view/page/private_key/edit.dart';
|
||||||
|
|
||||||
class PrivateKeysListPage extends StatefulWidget {
|
class PrivateKeysListPage extends StatefulWidget {
|
||||||
const PrivateKeysListPage({super.key});
|
const PrivateKeysListPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PrivateKeysListPage> createState() => _PrivateKeyListState();
|
State<PrivateKeysListPage> createState() => _PrivateKeyListState();
|
||||||
|
|
||||||
|
static const route = AppRouteNoArg(
|
||||||
|
page: PrivateKeysListPage.new,
|
||||||
|
path: '/private_key',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PrivateKeyListState extends State<PrivateKeysListPage>
|
class _PrivateKeyListState extends State<PrivateKeysListPage> with AfterLayoutMixin {
|
||||||
with AfterLayoutMixin {
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: _buildBody(),
|
body: _buildBody(),
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
child: const Icon(Icons.add),
|
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) {
|
if (pkis.isEmpty) {
|
||||||
return Center(child: Text(libL10n.empty));
|
return Center(child: Text(libL10n.empty));
|
||||||
}
|
}
|
||||||
return ListView.builder(
|
|
||||||
padding: const EdgeInsets.all(13),
|
final children = pkis.map(_buildKeyItem).toList();
|
||||||
itemCount: pkis.length,
|
return AutoMultiList(children: children);
|
||||||
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),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
// Only trigger on desktop platform and no private key saved
|
||||||
if (isDesktop && Stores.snippet.box.keys.isEmpty) {
|
if (isDesktop && Stores.snippet.box.keys.isEmpty) {
|
||||||
final home = Pfs.homeDir;
|
final home = Pfs.homeDir;
|
||||||
@@ -71,21 +75,19 @@ class _PrivateKeyListState extends State<PrivateKeysListPage>
|
|||||||
if (!idRsaFile.existsSync()) return;
|
if (!idRsaFile.existsSync()) return;
|
||||||
final sysPk = PrivateKeyInfo(
|
final sysPk = PrivateKeyInfo(
|
||||||
id: 'system',
|
id: 'system',
|
||||||
key: idRsaFile.readAsStringSync(),
|
key: await idRsaFile.readAsString(),
|
||||||
);
|
);
|
||||||
context.showRoundDialog(
|
context.showRoundDialog(
|
||||||
title: libL10n.attention,
|
title: libL10n.attention,
|
||||||
child: Text(l10n.addSystemPrivateKeyTip),
|
child: Text(l10n.addSystemPrivateKeyTip),
|
||||||
actions: Btn.ok(onTap: () {
|
actions: Btn.ok(onTap: () {
|
||||||
context.pop();
|
context.pop();
|
||||||
AppRoutes.keyEdit(pki: sysPk).go(context);
|
PrivateKeyEditPage.route.go(
|
||||||
|
context,
|
||||||
|
args: PrivateKeyEditPageArgs(pki: sysPk),
|
||||||
|
);
|
||||||
}).toList,
|
}).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:fl_lib/fl_lib.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:server_box/core/extension/context/locale.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/res/store.dart';
|
||||||
|
|
||||||
import 'package:server_box/data/model/app/shell_func.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';
|
import 'package:server_box/view/widget/two_line_text.dart';
|
||||||
|
|
||||||
class ProcessPage extends StatefulWidget {
|
class ProcessPage extends StatefulWidget {
|
||||||
final Spi spi;
|
final SpiRequiredArgs args;
|
||||||
const ProcessPage({super.key, required this.spi});
|
|
||||||
|
const ProcessPage({super.key, required this.args});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ProcessPage> createState() => _ProcessPageState();
|
State<ProcessPage> createState() => _ProcessPageState();
|
||||||
|
|
||||||
|
static const route = AppRouteArg(
|
||||||
|
page: ProcessPage.new,
|
||||||
|
path: '/process',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ProcessPageState extends State<ProcessPage> {
|
class _ProcessPageState extends State<ProcessPage> {
|
||||||
@@ -43,7 +50,7 @@ class _ProcessPageState extends State<ProcessPage> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_client = widget.spi.server?.value.client;
|
_client = widget.args.spi.server?.value.client;
|
||||||
final duration =
|
final duration =
|
||||||
Duration(seconds: Stores.setting.serverStatusUpdateInterval.fetch());
|
Duration(seconds: Stores.setting.serverStatusUpdateInterval.fetch());
|
||||||
_timer = Timer.periodic(duration, (_) => _refresh());
|
_timer = Timer.periodic(duration, (_) => _refresh());
|
||||||
@@ -58,7 +65,7 @@ class _ProcessPageState extends State<ProcessPage> {
|
|||||||
Future<void> _refresh() async {
|
Future<void> _refresh() async {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
final result =
|
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) {
|
if (result == null || result.isEmpty) {
|
||||||
context.showSnackBar(libL10n.empty);
|
context.showSnackBar(libL10n.empty);
|
||||||
return;
|
return;
|
||||||
@@ -125,7 +132,7 @@ class _ProcessPageState extends State<ProcessPage> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: CustomAppBar(
|
appBar: CustomAppBar(
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
title: TwoLineText(up: widget.spi.name, down: l10n.process),
|
title: TwoLineText(up: widget.args.spi.name, down: l10n.process),
|
||||||
actions: actions,
|
actions: actions,
|
||||||
),
|
),
|
||||||
body: child,
|
body: child,
|
||||||
|
|||||||
@@ -24,10 +24,13 @@ final class PvePage extends StatefulWidget {
|
|||||||
required this.args,
|
required this.args,
|
||||||
});
|
});
|
||||||
|
|
||||||
static const route = AppRouteArg<void, PvePageArgs>(page: PvePage.new, path: '/pve');
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PvePage> createState() => _PvePageState();
|
State<PvePage> createState() => _PvePageState();
|
||||||
|
|
||||||
|
static const route = AppRouteArg<void, PvePageArgs>(
|
||||||
|
page: PvePage.new,
|
||||||
|
path: '/pve',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const _kHorziPadding = 11.0;
|
const _kHorziPadding = 11.0;
|
||||||
@@ -454,9 +457,7 @@ extension on _PvePageState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _initRefreshTimer() {
|
void _initRefreshTimer() {
|
||||||
_timer = Timer.periodic(
|
_timer = Timer.periodic(Duration(seconds: Stores.setting.serverStatusUpdateInterval.fetch()), (_) {
|
||||||
Duration(seconds: Stores.setting.serverStatusUpdateInterval.fetch()),
|
|
||||||
(_) {
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
_pve.list();
|
_pve.list();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,83 @@
|
|||||||
part of 'view.dart';
|
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 {
|
enum _NetSortType {
|
||||||
device,
|
device,
|
||||||
trans,
|
trans,
|
||||||
@@ -26,13 +104,9 @@ enum _NetSortType {
|
|||||||
case _NetSortType.device:
|
case _NetSortType.device:
|
||||||
return (b, a) => a.compareTo(b);
|
return (b, a) => a.compareTo(b);
|
||||||
case _NetSortType.recv:
|
case _NetSortType.recv:
|
||||||
return (b, a) => ns
|
return (b, a) => ns.speedInBytes(ns.deviceIdx(a)).compareTo(ns.speedInBytes(ns.deviceIdx(b)));
|
||||||
.speedInBytes(ns.deviceIdx(a))
|
|
||||||
.compareTo(ns.speedInBytes(ns.deviceIdx(b)));
|
|
||||||
case _NetSortType.trans:
|
case _NetSortType.trans:
|
||||||
return (b, a) => ns
|
return (b, a) => ns.speedOutBytes(ns.deviceIdx(a)).compareTo(ns.speedOutBytes(ns.deviceIdx(b)));
|
||||||
.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:flutter_markdown/flutter_markdown.dart';
|
||||||
import 'package:icons_plus/icons_plus.dart';
|
import 'package:icons_plus/icons_plus.dart';
|
||||||
import 'package:server_box/core/extension/context/locale.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/server_detail_card.dart';
|
||||||
import 'package:server_box/data/model/app/shell_func.dart';
|
import 'package:server_box/data/model/app/shell_func.dart';
|
||||||
import 'package:server_box/data/model/server/battery.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';
|
part 'misc.dart';
|
||||||
|
|
||||||
class ServerDetailPage extends StatefulWidget {
|
class ServerDetailPage extends StatefulWidget {
|
||||||
const ServerDetailPage({super.key, required this.spi});
|
final SpiRequiredArgs args;
|
||||||
|
const ServerDetailPage({super.key, required this.args});
|
||||||
final Spi spi;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ServerDetailPage> createState() => _ServerDetailPageState();
|
State<ServerDetailPage> createState() => _ServerDetailPageState();
|
||||||
|
|
||||||
|
static const route = AppRouteArg(
|
||||||
|
page: ServerDetailPage.new,
|
||||||
|
path: '/servers/detail',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ServerDetailPageState extends State<ServerDetailPage>
|
class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerProviderStateMixin {
|
||||||
with SingleTickerProviderStateMixin {
|
|
||||||
late final _cardBuildMap = Map.fromIterables(
|
late final _cardBuildMap = Map.fromIterables(
|
||||||
ServerDetailCards.names,
|
ServerDetailCards.names,
|
||||||
[
|
[
|
||||||
@@ -49,7 +53,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
|||||||
_buildTemperature,
|
_buildTemperature,
|
||||||
_buildBatteries,
|
_buildBatteries,
|
||||||
_buildPve,
|
_buildPve,
|
||||||
_buildCustom,
|
_buildCustomCmd,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -83,7 +87,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final s = widget.spi.server;
|
final s = widget.args.spi.server;
|
||||||
if (s == null) {
|
if (s == null) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: CustomAppBar(),
|
appBar: CustomAppBar(),
|
||||||
@@ -106,14 +110,10 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
|||||||
children.add(buildFunc(si));
|
children.add(buildFunc(si));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: _buildAppBar(si),
|
appBar: _buildAppBar(si),
|
||||||
body: ListView(
|
body: AutoMultiList(
|
||||||
padding: EdgeInsets.only(
|
|
||||||
left: 13,
|
|
||||||
right: 13,
|
|
||||||
bottom: _media.padding.bottom + 77,
|
|
||||||
),
|
|
||||||
children: children,
|
children: children,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -121,18 +121,11 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
|||||||
|
|
||||||
CustomAppBar _buildAppBar(Server si) {
|
CustomAppBar _buildAppBar(Server si) {
|
||||||
return CustomAppBar(
|
return CustomAppBar(
|
||||||
title: Hero(
|
title: Text(
|
||||||
tag: 'home_card_title_${si.spi.id}',
|
si.spi.name,
|
||||||
transitionOnUserGestures: true,
|
style: TextStyle(
|
||||||
child: Material(
|
fontSize: 20,
|
||||||
color: Colors.transparent,
|
color: context.isDark ? Colors.white : Colors.black,
|
||||||
child: Text(
|
|
||||||
si.spi.name,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 20,
|
|
||||||
color: context.isDark ? Colors.white : Colors.black,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
@@ -144,7 +137,10 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.edit),
|
icon: const Icon(Icons.edit),
|
||||||
onPressed: () async {
|
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) {
|
if (delete == true) {
|
||||||
context.pop();
|
context.pop();
|
||||||
}
|
}
|
||||||
@@ -155,16 +151,14 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildLogo(Server si) {
|
Widget _buildLogo(Server si) {
|
||||||
var logoUrl = si.spi.custom?.logoUrl ??
|
var logoUrl = si.spi.custom?.logoUrl ?? _settings.serverLogoUrl.fetch().selfNotEmptyOrNull;
|
||||||
_settings.serverLogoUrl.fetch().selfNotEmptyOrNull;
|
|
||||||
if (logoUrl == null) return UIs.placeholder;
|
if (logoUrl == null) return UIs.placeholder;
|
||||||
|
|
||||||
final dist = si.status.more[StatusCmdType.sys]?.dist;
|
final dist = si.status.more[StatusCmdType.sys]?.dist;
|
||||||
if (dist != null) {
|
if (dist != null) {
|
||||||
logoUrl = logoUrl.replaceFirst('{DIST}', dist.name);
|
logoUrl = logoUrl.replaceFirst('{DIST}', dist.name);
|
||||||
}
|
}
|
||||||
logoUrl =
|
logoUrl = logoUrl.replaceFirst('{BRIGHT}', context.isDark ? 'dark' : 'light');
|
||||||
logoUrl.replaceFirst('{BRIGHT}', context.isDark ? 'dark' : 'light');
|
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 13),
|
padding: const EdgeInsets.symmetric(vertical: 13),
|
||||||
@@ -194,8 +188,16 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(e.key.i18n, style: UIs.text13),
|
Text(
|
||||||
Text(e.value, style: UIs.text13Grey)
|
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) {
|
Widget _buildCpuModelItem(MapEntry<String, int> e) {
|
||||||
final name = e.key
|
final name =
|
||||||
.replaceFirst('Intel(R)', '')
|
e.key.replaceFirst('Intel(R)', '').replaceFirst('AMD', '').replaceFirst('with Radeon Graphics', '');
|
||||||
.replaceFirst('AMD', '')
|
|
||||||
.replaceFirst('with Radeon Graphics', '');
|
|
||||||
final child = Row(
|
final child = Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
ConstrainedBox(
|
||||||
width: _media.size.width * .7,
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: _media.size.width * .7,
|
||||||
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
name,
|
name,
|
||||||
style: UIs.text13,
|
style: UIs.text13,
|
||||||
@@ -283,7 +285,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
|||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text('x ${e.value}', style: UIs.text13Grey),
|
Text('x ${e.value}', style: UIs.text13Grey, overflow: TextOverflow.clip),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
return child.paddingSymmetric(horizontal: 17);
|
return child.paddingSymmetric(horizontal: 17);
|
||||||
@@ -312,41 +314,47 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
|||||||
List<Widget> _buildCPUProgress(Cpus cs) {
|
List<Widget> _buildCPUProgress(Cpus cs) {
|
||||||
const kMaxColumn = 2;
|
const kMaxColumn = 2;
|
||||||
const kRowThreshold = 4;
|
const kRowThreshold = 4;
|
||||||
const kCoresCount = kMaxColumn * kRowThreshold;
|
const kCoresCountThreshold = kMaxColumn * kRowThreshold;
|
||||||
final children = <Widget>[];
|
final children = <Widget>[];
|
||||||
|
final displayCpuIndexSetting = Stores.setting.displayCpuIndex.fetch();
|
||||||
|
|
||||||
if (cs.coresCount > kCoresCount) {
|
if (cs.coresCount > kCoresCountThreshold) {
|
||||||
final rows = cs.coresCount ~/ kMaxColumn;
|
final numCoresToDisplay = cs.coresCount - 1;
|
||||||
for (var i = 0; i < rows; i++) {
|
final numRows = (numCoresToDisplay + kMaxColumn - 1) ~/ kMaxColumn;
|
||||||
|
|
||||||
|
for (var i = 0; i < numRows; i++) {
|
||||||
final rowChildren = <Widget>[];
|
final rowChildren = <Widget>[];
|
||||||
for (var j = 0; j < kMaxColumn; j++) {
|
for (var j = 0; j < kMaxColumn; j++) {
|
||||||
final idx = i * kMaxColumn + j + 1;
|
final coreListIndex = i * kMaxColumn + j;
|
||||||
if (idx >= cs.coresCount) break;
|
if (coreListIndex >= numCoresToDisplay) break;
|
||||||
if (Stores.setting.displayCpuIndex.fetch()) {
|
|
||||||
rowChildren.add(Text('$idx', style: UIs.text13Grey));
|
final coreNumberOneBased = coreListIndex + 1;
|
||||||
|
|
||||||
|
if (displayCpuIndexSetting) {
|
||||||
|
rowChildren.add(Text('$coreNumberOneBased', style: UIs.text13Grey));
|
||||||
}
|
}
|
||||||
rowChildren.add(
|
rowChildren.add(
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 3),
|
padding: const EdgeInsets.symmetric(vertical: 3),
|
||||||
child: _buildProgress(cs.usedPercent(coreIdx: idx)),
|
child: _buildProgress(cs.usedPercent(coreIdx: coreNumberOneBased)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
rowChildren.joinWith(UIs.width7);
|
if (rowChildren.isNotEmpty) {
|
||||||
children.add(
|
children.add(
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 17),
|
padding: const EdgeInsets.symmetric(horizontal: 17),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: rowChildren,
|
children: rowChildren.joinWith(UIs.width7).toList(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (var i = 0; i < cs.coresCount; i++) {
|
for (var i = 1; i < cs.coresCount; i++) {
|
||||||
if (i == 0) continue;
|
|
||||||
children.add(
|
children.add(
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 17),
|
padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 17),
|
||||||
@@ -377,6 +385,17 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
|||||||
final used = ss.mem.usedPercent * 100;
|
final used = ss.mem.usedPercent * 100;
|
||||||
final usedStr = used.toStringAsFixed(0);
|
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(
|
return CardX(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: UIs.roundRectCardPadding,
|
padding: UIs.roundRectCardPadding,
|
||||||
@@ -387,20 +406,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
percentW,
|
||||||
children: [
|
|
||||||
_buildAnimatedText(
|
|
||||||
ValueKey(usedStr),
|
|
||||||
'$usedStr%',
|
|
||||||
UIs.text27,
|
|
||||||
),
|
|
||||||
UIs.width7,
|
|
||||||
Text(
|
|
||||||
'of ${(ss.mem.total * 1024).bytes2Str}',
|
|
||||||
style: UIs.text13Grey,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
_buildDetailPercent(free, 'free'),
|
_buildDetailPercent(free, 'free'),
|
||||||
@@ -423,6 +429,18 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
|||||||
if (ss.swap.total == 0) return UIs.placeholder;
|
if (ss.swap.total == 0) return UIs.placeholder;
|
||||||
final used = ss.swap.usedPercent * 100;
|
final used = ss.swap.usedPercent * 100;
|
||||||
final cached = ss.swap.cached / ss.swap.total * 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(
|
return CardX(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: UIs.roundRectCardPadding,
|
padding: UIs.roundRectCardPadding,
|
||||||
@@ -433,16 +451,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
percentW,
|
||||||
children: [
|
|
||||||
Text('${used.toStringAsFixed(0)}%', style: UIs.text27),
|
|
||||||
UIs.width7,
|
|
||||||
Text(
|
|
||||||
'of ${(ss.swap.total * 1024).bytes2Str} ',
|
|
||||||
style: UIs.text13Grey,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
_buildDetailPercent(cached, 'cached'),
|
_buildDetailPercent(cached, 'cached'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -470,7 +479,6 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
|||||||
|
|
||||||
Widget _buildGpuItem(NvidiaSmiItem item) {
|
Widget _buildGpuItem(NvidiaSmiItem item) {
|
||||||
final mem = item.memory;
|
final mem = item.memory;
|
||||||
final processes = mem.processes;
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(item.name, style: UIs.text13),
|
title: Text(item.name, style: UIs.text13),
|
||||||
leading: Text(
|
leading: Text(
|
||||||
@@ -490,32 +498,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () => _onTapGpuItem(item),
|
||||||
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),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.info_outline, size: 17),
|
icon: const Icon(Icons.info_outline, size: 17),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -538,28 +521,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
|||||||
textScaler: _textFactor,
|
textScaler: _textFactor,
|
||||||
),
|
),
|
||||||
trailing: InkWell(
|
trailing: InkWell(
|
||||||
onTap: () {
|
onTap: () => _nTapGpuProcessItem(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),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: const Icon(Icons.info_outline, size: 17),
|
child: const Icon(Icons.info_outline, size: 17),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -567,8 +529,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
|||||||
|
|
||||||
Widget _buildDiskView(Server si) {
|
Widget _buildDiskView(Server si) {
|
||||||
final ss = si.status;
|
final ss = si.status;
|
||||||
final children = List.generate(
|
final children = List.generate(ss.disk.length, (idx) => _buildDiskItem(ss.disk[idx], ss));
|
||||||
ss.disk.length, (idx) => _buildDiskItem(ss.disk[idx], ss));
|
|
||||||
return CardX(
|
return CardX(
|
||||||
child: ExpandTile(
|
child: ExpandTile(
|
||||||
title: Text(l10n.disk),
|
title: Text(l10n.disk),
|
||||||
@@ -587,6 +548,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
|||||||
if (read == null || write == null) return use;
|
if (read == null || write == null) return use;
|
||||||
return '$use\n${l10n.read} $read | ${l10n.write} $write';
|
return '$use\n${l10n.read} $read | ${l10n.write} $write';
|
||||||
}();
|
}();
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 5),
|
padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 5),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -638,43 +600,40 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
|||||||
devices.sort(_netSortType.value.getSortFunc(ns));
|
devices.sort(_netSortType.value.getSortFunc(ns));
|
||||||
children.addAll(devices.map((e) => _buildNetSpeedItem(ns, e)));
|
children.addAll(devices.map((e) => _buildNetSpeedItem(ns, e)));
|
||||||
|
|
||||||
return CardX(
|
return ExpandTile(
|
||||||
child: ExpandTile(
|
leading: Icon(ServerDetailCards.net.icon, size: 17),
|
||||||
leading: Icon(ServerDetailCards.net.icon, size: 17),
|
title: Row(
|
||||||
title: Row(
|
children: [
|
||||||
children: [
|
Text(l10n.net),
|
||||||
Text(l10n.net),
|
UIs.width13,
|
||||||
UIs.width13,
|
_netSortType.listenVal(
|
||||||
ValBuilder(
|
(val) => InkWell(
|
||||||
listenable: _netSortType,
|
onTap: () => _netSortType.value = val.next,
|
||||||
builder: (val) => InkWell(
|
child: AnimatedSwitcher(
|
||||||
onTap: () => _netSortType.value = val.next,
|
duration: const Duration(milliseconds: 377),
|
||||||
child: AnimatedSwitcher(
|
transitionBuilder: (child, animation) => FadeTransition(
|
||||||
duration: const Duration(milliseconds: 377),
|
opacity: animation,
|
||||||
transitionBuilder: (child, animation) => FadeTransition(
|
child: child,
|
||||||
opacity: animation,
|
),
|
||||||
child: child,
|
child: Row(
|
||||||
),
|
children: [
|
||||||
child: Row(
|
const Icon(Icons.sort, size: 17),
|
||||||
children: [
|
UIs.width7,
|
||||||
const Icon(Icons.sort, size: 17),
|
Text(
|
||||||
UIs.width7,
|
val.name,
|
||||||
Text(
|
style: UIs.text13Grey,
|
||||||
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) {
|
Widget _buildNetSpeedItem(NetSpeed ns, String device) {
|
||||||
@@ -725,9 +684,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
|||||||
leading: const Icon(Icons.ac_unit, size: 20),
|
leading: const Icon(Icons.ac_unit, size: 20),
|
||||||
initiallyExpanded: _getInitExpand(ss.temps.devices.length),
|
initiallyExpanded: _getInitExpand(ss.temps.devices.length),
|
||||||
childrenPadding: const EdgeInsets.only(bottom: 7),
|
childrenPadding: const EdgeInsets.only(bottom: 7),
|
||||||
children: ss.temps.devices
|
children: ss.temps.devices.map((key) => _buildTemperatureItem(key, ss.temps.get(key))).toList(),
|
||||||
.map((key) => _buildTemperatureItem(key, ss.temps.get(key)))
|
|
||||||
.toList(),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -738,12 +695,11 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(key, style: UIs.text15).paddingSymmetric(horizontal: 5).tap(
|
Btn.text(
|
||||||
onTap: () {
|
text: key,
|
||||||
Pfs.copy(key);
|
textStyle: UIs.text15,
|
||||||
context.showSnackBar('${libL10n.copy} ${libL10n.success}');
|
onTap: () => _onTapTemperatureItem(key),
|
||||||
},
|
).paddingSymmetric(horizontal: 5),
|
||||||
),
|
|
||||||
Text('${val?.toStringAsFixed(1)}°C', style: UIs.text13Grey),
|
Text('${val?.toStringAsFixed(1)}°C', style: UIs.text13Grey),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -813,41 +769,31 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
|||||||
child: Text(si.device),
|
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(
|
return InkWell(
|
||||||
onTap: () {
|
onTap: () => _onTapSensorItem(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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 7),
|
padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 7),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
itemW,
|
||||||
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),
|
|
||||||
],
|
|
||||||
)),
|
|
||||||
UIs.width7,
|
UIs.width7,
|
||||||
const Icon(Icons.keyboard_arrow_right, color: Colors.grey),
|
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;
|
final ss = si.status;
|
||||||
if (ss.customCmds.isEmpty) return UIs.placeholder;
|
if (ss.customCmds.isEmpty) return UIs.placeholder;
|
||||||
return CardX(
|
return CardX(
|
||||||
@@ -877,12 +823,12 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
|||||||
leading: const Icon(MingCute.command_line, size: 17),
|
leading: const Icon(MingCute.command_line, size: 17),
|
||||||
title: Text(l10n.customCmd),
|
title: Text(l10n.customCmd),
|
||||||
initiallyExpanded: _getInitExpand(ss.customCmds.length),
|
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(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 7),
|
padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 7),
|
||||||
child: KvRow(
|
child: KvRow(
|
||||||
@@ -891,20 +837,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
|||||||
vBuilder: () {
|
vBuilder: () {
|
||||||
if (!cmd.value.contains('\n')) return null;
|
if (!cmd.value.contains('\n')) return null;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () => _onTapCustomItem(cmd),
|
||||||
context.showRoundDialog(
|
|
||||||
title: cmd.key,
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Text(cmd.value, style: UIs.text13Grey),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => context.pop(),
|
|
||||||
child: Text(libL10n.close),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
Icons.info_outline,
|
Icons.info_outline,
|
||||||
size: 17,
|
size: 17,
|
||||||
|
|||||||
@@ -5,24 +5,25 @@ import 'package:fl_lib/fl_lib.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:icons_plus/icons_plus.dart';
|
import 'package:icons_plus/icons_plus.dart';
|
||||||
import 'package:server_box/core/extension/context/locale.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/custom.dart';
|
||||||
import 'package:server_box/data/model/server/server.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/model/server/wol_cfg.dart';
|
||||||
import 'package:server_box/data/provider/server.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/model/server/server_private_info.dart';
|
||||||
import 'package:server_box/data/provider/private_key.dart';
|
import 'package:server_box/data/provider/private_key.dart';
|
||||||
import 'package:server_box/data/store/server.dart';
|
import 'package:server_box/data/store/server.dart';
|
||||||
|
import 'package:server_box/view/page/private_key/edit.dart';
|
||||||
|
|
||||||
class ServerEditPage extends StatefulWidget {
|
class ServerEditPage extends StatefulWidget {
|
||||||
final Spi? args;
|
final SpiRequiredArgs? args;
|
||||||
|
|
||||||
const ServerEditPage({super.key, this.args});
|
const ServerEditPage({super.key, this.args});
|
||||||
|
|
||||||
static const route = AppRoute<bool, Spi>(
|
static const route = AppRoute<bool, SpiRequiredArgs>(
|
||||||
page: ServerEditPage.new,
|
page: ServerEditPage.new,
|
||||||
path: '/server_edit',
|
path: '/servers/edit',
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -30,7 +31,7 @@ class ServerEditPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||||
late final spi = widget.args;
|
late final spi = widget.args?.spi;
|
||||||
final _nameController = TextEditingController();
|
final _nameController = TextEditingController();
|
||||||
final _ipController = TextEditingController();
|
final _ipController = TextEditingController();
|
||||||
final _altUrlController = TextEditingController();
|
final _altUrlController = TextEditingController();
|
||||||
@@ -187,14 +188,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
|||||||
_buildJumpServer(),
|
_buildJumpServer(),
|
||||||
_buildMore(),
|
_buildMore(),
|
||||||
];
|
];
|
||||||
return SingleChildScrollView(
|
return AutoMultiList(children: children);
|
||||||
padding: const EdgeInsets.fromLTRB(17, 7, 17, 47),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: children,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAuth() {
|
Widget _buildAuth() {
|
||||||
@@ -259,7 +253,10 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
|||||||
),
|
),
|
||||||
trailing: Btn.icon(
|
trailing: Btn.icon(
|
||||||
icon: const Icon(Icons.edit),
|
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,
|
onTap: () => _keyIdx.value = index,
|
||||||
);
|
);
|
||||||
@@ -269,23 +266,17 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
|||||||
title: Text(libL10n.add),
|
title: Text(libL10n.add),
|
||||||
contentPadding: const EdgeInsets.only(left: 23, right: 23),
|
contentPadding: const EdgeInsets.only(left: 23, right: 23),
|
||||||
trailing: const Icon(Icons.add),
|
trailing: const Icon(Icons.add),
|
||||||
onTap: () => AppRoutes.keyEdit().go(context),
|
onTap: () => PrivateKeyEditPage.route.go(context),
|
||||||
),
|
|
||||||
);
|
|
||||||
return CardX(
|
|
||||||
child: ListenableBuilder(
|
|
||||||
listenable: _keyIdx,
|
|
||||||
builder: (_, __) => Column(children: tiles),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
return _keyIdx.listenVal((_) => Column(children: tiles)).cardx;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEnvs() {
|
Widget _buildEnvs() {
|
||||||
return _env.listenVal((val) {
|
return _env.listenVal((val) {
|
||||||
final subtitle =
|
final subtitle = val.isEmpty ? null : Text(val.keys.join(','), style: UIs.textGrey);
|
||||||
val.isEmpty ? null : Text(val.keys.join(','), style: UIs.textGrey);
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: const Icon(HeroIcons.variable),
|
leading: const Icon(HeroIcons.variable),
|
||||||
subtitle: subtitle,
|
subtitle: subtitle,
|
||||||
@@ -419,18 +410,9 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
|||||||
return ListTile(
|
return ListTile(
|
||||||
leading: const Icon(BoxIcons.bxs_file_json),
|
leading: const Icon(BoxIcons.bxs_file_json),
|
||||||
title: const Text('JSON'),
|
title: const Text('JSON'),
|
||||||
subtitle: vals.isEmpty
|
subtitle: vals.isEmpty ? null : Text(vals.keys.join(','), style: UIs.textGrey),
|
||||||
? null
|
|
||||||
: Text(vals.keys.join(','), style: UIs.textGrey),
|
|
||||||
trailing: const Icon(Icons.keyboard_arrow_right),
|
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||||
onTap: () async {
|
onTap: _onTapCustomItem,
|
||||||
final res = await KvEditor.route.go(
|
|
||||||
context,
|
|
||||||
KvEditorArgs(data: _customCmds.value),
|
|
||||||
);
|
|
||||||
if (res == null) return;
|
|
||||||
_customCmds.value = res;
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
).cardx,
|
).cardx,
|
||||||
@@ -535,153 +517,6 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
|||||||
).cardx;
|
).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() {
|
Widget _buildWriteScriptTip() {
|
||||||
return Btn.tile(
|
return Btn.tile(
|
||||||
text: libL10n.attention,
|
text: libL10n.attention,
|
||||||
@@ -742,4 +577,156 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
|||||||
icon: const Icon(Icons.delete),
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final isMobile = ResponsiveBreakpoints.of(context).isMobile;
|
||||||
|
if (!isMobile) return UIs.placeholder;
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(left: 10),
|
padding: const EdgeInsets.only(left: 10),
|
||||||
child: Row(
|
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() {
|
Widget? _buildPlatformSetting() {
|
||||||
final func = switch (Pfs.type) {
|
final func = switch (Pfs.type) {
|
||||||
Pfs.android => AppRoutes.androidSettings().go,
|
Pfs.android => AndroidSettingsPage.route.go,
|
||||||
Pfs.ios => AppRoutes.iosSettings().go,
|
Pfs.ios => IosSettingsPage.route.go,
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
if (func == null) return null;
|
if (func == null) return null;
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ extension _Server on _AppSettingsPageState {
|
|||||||
leading: const Icon(OctIcons.sort_desc, size: _kIconSize),
|
leading: const Icon(OctIcons.sort_desc, size: _kIconSize),
|
||||||
title: Text(l10n.serverOrder),
|
title: Text(l10n.serverOrder),
|
||||||
trailing: const Icon(Icons.keyboard_arrow_right),
|
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),
|
leading: const Icon(OctIcons.sort_desc, size: _kIconSize),
|
||||||
title: Text(l10n.serverDetailOrder),
|
title: Text(l10n.serverDetailOrder),
|
||||||
trailing: const Icon(Icons.keyboard_arrow_right),
|
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),
|
leading: const Icon(BoxIcons.bxs_keyboard),
|
||||||
title: Text(l10n.editVirtKeys),
|
title: Text(l10n.editVirtKeys),
|
||||||
trailing: const Icon(Icons.keyboard_arrow_right),
|
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/store.dart';
|
||||||
import 'package:server_box/data/res/url.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/model/app/net_view.dart';
|
||||||
import 'package:server_box/data/res/build_data.dart';
|
import 'package:server_box/data/res/build_data.dart';
|
||||||
import 'package:server_box/view/page/backup.dart';
|
import 'package:server_box/view/page/backup.dart';
|
||||||
import 'package:server_box/view/page/editor.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/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_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 'about.dart';
|
||||||
part 'entries/app.dart';
|
part 'entries/app.dart';
|
||||||
@@ -35,16 +39,17 @@ const _kIconSize = 23.0;
|
|||||||
class SettingsPage extends StatefulWidget {
|
class SettingsPage extends StatefulWidget {
|
||||||
const SettingsPage({super.key});
|
const SettingsPage({super.key});
|
||||||
|
|
||||||
static const route = AppRouteNoArg(page: SettingsPage.new, path: '/settings');
|
static const route = AppRouteNoArg(
|
||||||
|
page: SettingsPage.new,
|
||||||
|
path: '/settings',
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<SettingsPage> createState() => _SettingsPageState();
|
State<SettingsPage> createState() => _SettingsPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SettingsPageState extends State<SettingsPage>
|
class _SettingsPageState extends State<SettingsPage> with SingleTickerProviderStateMixin {
|
||||||
with SingleTickerProviderStateMixin {
|
late final _tabCtrl = TabController(length: SettingsTabs.values.length, vsync: this);
|
||||||
late final _tabCtrl =
|
|
||||||
TabController(length: SettingsTabs.values.length, vsync: this);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@@ -62,9 +67,7 @@ class _SettingsPageState extends State<SettingsPage>
|
|||||||
dividerHeight: 0,
|
dividerHeight: 0,
|
||||||
tabAlignment: TabAlignment.center,
|
tabAlignment: TabAlignment.center,
|
||||||
isScrollable: true,
|
isScrollable: true,
|
||||||
tabs: SettingsTabs.values
|
tabs: SettingsTabs.values.map((e) => Tab(text: e.i18n)).toList(growable: false),
|
||||||
.map((e) => Tab(text: e.i18n))
|
|
||||||
.toList(growable: false),
|
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
Btn.text(
|
Btn.text(
|
||||||
@@ -120,12 +123,7 @@ final class _AppSettingsPageState extends State<AppSettingsPage> {
|
|||||||
children: [
|
children: [
|
||||||
[const CenterGreyTitle('App'), _buildApp()],
|
[const CenterGreyTitle('App'), _buildApp()],
|
||||||
[CenterGreyTitle(l10n.server), _buildServer()],
|
[CenterGreyTitle(l10n.server), _buildServer()],
|
||||||
[
|
[const CenterGreyTitle('SSH'), _buildSSH(), const CenterGreyTitle('SFTP'), _buildSFTP()],
|
||||||
const CenterGreyTitle('SSH'),
|
|
||||||
_buildSSH(),
|
|
||||||
const CenterGreyTitle('SFTP'),
|
|
||||||
_buildSFTP()
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
CenterGreyTitle(l10n.container),
|
CenterGreyTitle(l10n.container),
|
||||||
_buildContainer(),
|
_buildContainer(),
|
||||||
@@ -162,6 +160,5 @@ enum SettingsTabs {
|
|||||||
SettingsTabs.about => const _AppAboutPage(),
|
SettingsTabs.about => const _AppAboutPage(),
|
||||||
};
|
};
|
||||||
|
|
||||||
static final List<Widget> pages =
|
static final List<Widget> pages = SettingsTabs.values.map((e) => e.page).toList();
|
||||||
SettingsTabs.values.map((e) => e.page).toList();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ class AndroidSettingsPage extends StatefulWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
State<AndroidSettingsPage> createState() => _AndroidSettingsPageState();
|
State<AndroidSettingsPage> createState() => _AndroidSettingsPageState();
|
||||||
|
|
||||||
|
static const route = AppRouteNoArg(
|
||||||
|
page: AndroidSettingsPage.new,
|
||||||
|
path: '/settings/android',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const _homeWidgetPrefPrefix = 'widget_';
|
const _homeWidgetPrefPrefix = 'widget_';
|
||||||
@@ -24,8 +29,7 @@ class _AndroidSettingsPageState extends State<AndroidSettingsPage> {
|
|||||||
// _buildFgService(),
|
// _buildFgService(),
|
||||||
_buildBgRun(),
|
_buildBgRun(),
|
||||||
_buildAndroidWidgetSharedPreference(),
|
_buildAndroidWidgetSharedPreference(),
|
||||||
if (BioAuth.isPlatformSupported)
|
if (BioAuth.isPlatformSupported) PlatformPublicSettings.buildBioAuth(),
|
||||||
PlatformPublicSettings.buildBioAuth(),
|
|
||||||
].map((e) => CardX(child: e)).toList(),
|
].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:server_box/view/page/setting/platform/platform_pub.dart';
|
||||||
import 'package:watch_connectivity/watch_connectivity.dart';
|
import 'package:watch_connectivity/watch_connectivity.dart';
|
||||||
|
|
||||||
class IOSSettingsPage extends StatefulWidget {
|
class IosSettingsPage extends StatefulWidget {
|
||||||
const IOSSettingsPage({super.key});
|
const IosSettingsPage({super.key});
|
||||||
|
|
||||||
@override
|
@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 _pushToken = ValueNotifier<String?>(null);
|
||||||
|
|
||||||
final wc = WatchConnectivity();
|
final wc = WatchConnectivity();
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ class ServerDetailOrderPage extends StatefulWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
State<ServerDetailOrderPage> createState() => _ServerDetailOrderPageState();
|
State<ServerDetailOrderPage> createState() => _ServerDetailOrderPageState();
|
||||||
|
|
||||||
|
static const route = AppRouteNoArg(
|
||||||
|
page: ServerDetailOrderPage.new,
|
||||||
|
path: '/settings/order/server_detail',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ServerDetailOrderPageState extends State<ServerDetailOrderPage> {
|
class _ServerDetailOrderPageState extends State<ServerDetailOrderPage> {
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ class ServerFuncBtnsOrderPage extends StatefulWidget {
|
|||||||
@override
|
@override
|
||||||
State<ServerFuncBtnsOrderPage> createState() => _ServerDetailOrderPageState();
|
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> {
|
class _ServerDetailOrderPageState extends State<ServerFuncBtnsOrderPage> {
|
||||||
@@ -28,10 +31,7 @@ class _ServerDetailOrderPageState extends State<ServerFuncBtnsOrderPage> {
|
|||||||
return ValBuilder(
|
return ValBuilder(
|
||||||
listenable: prop.listenable(),
|
listenable: prop.listenable(),
|
||||||
builder: (keys) {
|
builder: (keys) {
|
||||||
final disabled = ServerFuncBtn.values
|
final disabled = ServerFuncBtn.values.map((e) => e.index).where((e) => !keys.contains(e)).toList();
|
||||||
.map((e) => e.index)
|
|
||||||
.where((e) => !keys.contains(e))
|
|
||||||
.toList();
|
|
||||||
final allKeys = [...keys, ...disabled];
|
final allKeys = [...keys, ...disabled];
|
||||||
return ReorderableListView.builder(
|
return ReorderableListView.builder(
|
||||||
padding: const EdgeInsets.all(7),
|
padding: const EdgeInsets.all(7),
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ class ServerOrderPage extends StatefulWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
State<ServerOrderPage> createState() => _ServerOrderPageState();
|
State<ServerOrderPage> createState() => _ServerOrderPageState();
|
||||||
|
|
||||||
|
static const route = AppRouteNoArg(
|
||||||
|
page: ServerOrderPage.new,
|
||||||
|
path: '/settings/order/server',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ServerOrderPageState extends State<ServerOrderPage> {
|
class _ServerOrderPageState extends State<ServerOrderPage> {
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ class SSHVirtKeySettingPage extends StatefulWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
State<SSHVirtKeySettingPage> createState() => _SSHVirtKeySettingPageState();
|
State<SSHVirtKeySettingPage> createState() => _SSHVirtKeySettingPageState();
|
||||||
|
|
||||||
|
static const route = AppRouteNoArg(
|
||||||
|
page: SSHVirtKeySettingPage.new,
|
||||||
|
path: '/settings/ssh_virt_key',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SSHVirtKeySettingPageState extends State<SSHVirtKeySettingPage> {
|
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/server.dart';
|
||||||
import 'package:server_box/data/provider/snippet.dart';
|
import 'package:server_box/data/provider/snippet.dart';
|
||||||
|
|
||||||
class SnippetEditPage extends StatefulWidget {
|
final class SnippetEditPageArgs {
|
||||||
const SnippetEditPage({super.key, this.snippet});
|
|
||||||
|
|
||||||
final Snippet? snippet;
|
final Snippet? snippet;
|
||||||
|
const SnippetEditPageArgs({this.snippet});
|
||||||
|
}
|
||||||
|
|
||||||
|
class SnippetEditPage extends StatefulWidget {
|
||||||
|
final SnippetEditPageArgs? args;
|
||||||
|
|
||||||
|
const SnippetEditPage({super.key, this.args});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<SnippetEditPage> createState() => _SnippetEditPageState();
|
State<SnippetEditPage> createState() => _SnippetEditPageState();
|
||||||
|
|
||||||
|
static const route = AppRoute(
|
||||||
|
page: SnippetEditPage.new,
|
||||||
|
path: '/snippets/edit',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SnippetEditPageState extends State<SnippetEditPage>
|
class _SnippetEditPageState extends State<SnippetEditPage> with AfterLayoutMixin {
|
||||||
with AfterLayoutMixin {
|
|
||||||
final _nameController = TextEditingController();
|
final _nameController = TextEditingController();
|
||||||
final _scriptController = TextEditingController();
|
final _scriptController = TextEditingController();
|
||||||
final _noteController = TextEditingController();
|
final _noteController = TextEditingController();
|
||||||
@@ -48,18 +57,19 @@ class _SnippetEditPageState extends State<SnippetEditPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<Widget>? _buildAppBarActions() {
|
List<Widget>? _buildAppBarActions() {
|
||||||
if (widget.snippet == null) return null;
|
final snippet = widget.args?.snippet;
|
||||||
|
if (snippet == null) return null;
|
||||||
return [
|
return [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.showRoundDialog(
|
context.showRoundDialog(
|
||||||
title: libL10n.attention,
|
title: libL10n.attention,
|
||||||
child: Text(libL10n.askContinue(
|
child: Text(libL10n.askContinue(
|
||||||
'${libL10n.delete} ${l10n.snippet}(${widget.snippet!.name})',
|
'${libL10n.delete} ${l10n.snippet}(${snippet.name})',
|
||||||
)),
|
)),
|
||||||
actions: Btn.ok(
|
actions: Btn.ok(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
SnippetProvider.del(widget.snippet!);
|
SnippetProvider.del(snippet);
|
||||||
context.pop();
|
context.pop();
|
||||||
context.pop();
|
context.pop();
|
||||||
},
|
},
|
||||||
@@ -92,8 +102,9 @@ class _SnippetEditPageState extends State<SnippetEditPage>
|
|||||||
note: note.isEmpty ? null : note,
|
note: note.isEmpty ? null : note,
|
||||||
autoRunOn: _autoRunOn.value.isEmpty ? null : _autoRunOn.value,
|
autoRunOn: _autoRunOn.value.isEmpty ? null : _autoRunOn.value,
|
||||||
);
|
);
|
||||||
if (widget.snippet != null) {
|
final oldSnippet = widget.args?.snippet;
|
||||||
SnippetProvider.update(widget.snippet!, snippet);
|
if (oldSnippet != null) {
|
||||||
|
SnippetProvider.update(oldSnippet, snippet);
|
||||||
} else {
|
} else {
|
||||||
SnippetProvider.add(snippet);
|
SnippetProvider.add(snippet);
|
||||||
}
|
}
|
||||||
@@ -103,8 +114,7 @@ class _SnippetEditPageState extends State<SnippetEditPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBody() {
|
Widget _buildBody() {
|
||||||
return ListView(
|
return AutoMultiList(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 13),
|
|
||||||
children: [
|
children: [
|
||||||
Input(
|
Input(
|
||||||
autoFocus: true,
|
autoFocus: true,
|
||||||
@@ -148,9 +158,7 @@ class _SnippetEditPageState extends State<SnippetEditPage>
|
|||||||
builder: (vals) {
|
builder: (vals) {
|
||||||
final subtitle = vals.isEmpty
|
final subtitle = vals.isEmpty
|
||||||
? null
|
? null
|
||||||
: vals
|
: vals.map((e) => ServerProvider.pick(id: e)?.value.spi.name ?? e).join(', ');
|
||||||
.map((e) => ServerProvider.pick(id: e)?.value.spi.name ?? e)
|
|
||||||
.join(', ');
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: const Padding(
|
leading: const Padding(
|
||||||
padding: EdgeInsets.only(left: 5),
|
padding: EdgeInsets.only(left: 5),
|
||||||
@@ -167,8 +175,7 @@ class _SnippetEditPageState extends State<SnippetEditPage>
|
|||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
vals.removeWhere(
|
vals.removeWhere((e) => !ServerProvider.serverOrder.value.contains(e));
|
||||||
(e) => !ServerProvider.serverOrder.value.contains(e));
|
|
||||||
final serverIds = await context.showPickDialog(
|
final serverIds = await context.showPickDialog(
|
||||||
title: l10n.autoRun,
|
title: l10n.autoRun,
|
||||||
items: ServerProvider.serverOrder.value,
|
items: ServerProvider.serverOrder.value,
|
||||||
@@ -193,9 +200,9 @@ class _SnippetEditPageState extends State<SnippetEditPage>
|
|||||||
child: SimpleMarkdown(
|
child: SimpleMarkdown(
|
||||||
data: '''
|
data: '''
|
||||||
📌 ${l10n.supportFmtArgs}\n
|
📌 ${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}:
|
${libL10n.example}:
|
||||||
- `\${ctrl+c}` (Control + C)
|
- `\${ctrl+c}` (Control + C)
|
||||||
- `\${ctrl+b}d` (Tmux Detach)
|
- `\${ctrl+b}d` (Tmux Detach)
|
||||||
@@ -212,7 +219,7 @@ ${libL10n.example}:
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void afterFirstLayout(BuildContext context) {
|
void afterFirstLayout(BuildContext context) {
|
||||||
final snippet = widget.snippet;
|
final snippet = widget.args?.snippet;
|
||||||
if (snippet != null) {
|
if (snippet != null) {
|
||||||
_nameController.text = snippet.name;
|
_nameController.text = snippet.name;
|
||||||
_scriptController.text = snippet.script;
|
_scriptController.text = snippet.script;
|
||||||
|
|||||||
@@ -1,123 +1,164 @@
|
|||||||
import 'package:fl_lib/fl_lib.dart';
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:server_box/data/res/store.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/data/model/server/snippet.dart';
|
||||||
import 'package:server_box/core/route.dart';
|
|
||||||
import 'package:server_box/data/provider/snippet.dart';
|
import 'package:server_box/data/provider/snippet.dart';
|
||||||
|
import 'package:server_box/view/page/snippet/edit.dart';
|
||||||
|
|
||||||
class SnippetListPage extends StatefulWidget {
|
class SnippetListPage extends StatefulWidget {
|
||||||
const SnippetListPage({super.key});
|
const SnippetListPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<SnippetListPage> createState() => _SnippetListPageState();
|
State<SnippetListPage> createState() => _SnippetListPageState();
|
||||||
|
|
||||||
|
static const route = AppRouteNoArg(
|
||||||
|
page: SnippetListPage.new,
|
||||||
|
path: '/snippets',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SnippetListPageState extends State<SnippetListPage>
|
class _SnippetListPageState extends State<SnippetListPage> with AutomaticKeepAliveClientMixin {
|
||||||
with AutomaticKeepAliveClientMixin {
|
|
||||||
final _tag = ''.vn;
|
final _tag = ''.vn;
|
||||||
|
final _splitViewCtrl = SplitViewController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
_tag.dispose();
|
_tag.dispose();
|
||||||
|
_splitViewCtrl.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(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(
|
return Scaffold(
|
||||||
appBar: TagSwitcher(
|
appBar: TagSwitcher(
|
||||||
tags: SnippetProvider.tags,
|
tags: SnippetProvider.tags,
|
||||||
onTagChanged: (tag) => _tag.value = tag,
|
onTagChanged: (tag) => _tag.value = tag,
|
||||||
initTag: _tag.value,
|
initTag: _tag.value,
|
||||||
),
|
),
|
||||||
body: _buildBody(),
|
body: _buildSnippetList(snippets, tag),
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
heroTag: 'snippetAdd',
|
heroTag: 'snippetAdd',
|
||||||
child: const Icon(Icons.add),
|
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) {
|
Widget _buildSnippetList(List<Snippet> snippets, String tag) {
|
||||||
|
if (snippets.isEmpty) return Center(child: Text(libL10n.empty));
|
||||||
|
|
||||||
final filtered = tag == TagSwitcher.kDefaultTag
|
final filtered = tag == TagSwitcher.kDefaultTag
|
||||||
? snippets
|
? snippets
|
||||||
: snippets.where((e) => e.tags?.contains(tag) ?? false).toList();
|
: snippets.where((e) => e.tags?.contains(tag) ?? false).toList();
|
||||||
|
|
||||||
return ReorderableListView.builder(
|
final generatedChildren = List.generate(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 9),
|
filtered.length,
|
||||||
itemCount: filtered.length,
|
(idx) {
|
||||||
onReorder: (oldIdx, newIdx) {
|
final snippet = filtered.elementAtOrNull(idx);
|
||||||
snippets.moveByItem(
|
if (snippet == null) return UIs.placeholder;
|
||||||
oldIdx,
|
return Container(
|
||||||
newIdx,
|
key: ValueKey(snippet.name),
|
||||||
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,
|
|
||||||
child: _buildSnippetItem(snippet),
|
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) {
|
Widget _buildSnippetItem(Snippet snippet) {
|
||||||
return CardX(
|
return ListTile(
|
||||||
child: ListTile(
|
contentPadding: const EdgeInsets.only(left: 23, right: 17),
|
||||||
contentPadding: const EdgeInsets.only(left: 23, right: 17),
|
title: Text(
|
||||||
title: Text(
|
snippet.name,
|
||||||
snippet.name,
|
overflow: TextOverflow.ellipsis,
|
||||||
overflow: TextOverflow.ellipsis,
|
maxLines: 1,
|
||||||
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),
|
|
||||||
),
|
),
|
||||||
);
|
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
|
@override
|
||||||
bool get wantKeepAlive => true;
|
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';
|
import 'package:server_box/data/model/server/snippet.dart';
|
||||||
|
|
||||||
class SnippetResultPage extends StatelessWidget {
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -19,13 +24,13 @@ class SnippetResultPage extends StatelessWidget {
|
|||||||
Widget _buildBody() {
|
Widget _buildBody() {
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 17),
|
padding: const EdgeInsets.symmetric(horizontal: 17),
|
||||||
itemCount: results.length,
|
itemCount: args.length,
|
||||||
itemBuilder: (_, index) {
|
itemBuilder: (_, index) {
|
||||||
final item = results[index];
|
final item = args[index];
|
||||||
if (item == null) return UIs.placeholder;
|
if (item == null) return UIs.placeholder;
|
||||||
return CardX(
|
return CardX(
|
||||||
child: ExpandTile(
|
child: ExpandTile(
|
||||||
initiallyExpanded: results.length == 1,
|
initiallyExpanded: args.length == 1,
|
||||||
title: Text(item.dest ?? ''),
|
title: Text(item.dest ?? ''),
|
||||||
subtitle: Text(item.time.toString(), style: UIs.textGrey),
|
subtitle: Text(item.time.toString(), style: UIs.textGrey),
|
||||||
children: [
|
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/snippet.dart';
|
||||||
import 'package:server_box/data/provider/virtual_keyboard.dart';
|
import 'package:server_box/data/provider/virtual_keyboard.dart';
|
||||||
import 'package:server_box/data/res/store.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:wakelock_plus/wakelock_plus.dart';
|
||||||
import 'package:xterm/core.dart';
|
import 'package:xterm/core.dart';
|
||||||
import 'package:xterm/ui.dart' hide TerminalThemes;
|
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/server/server_private_info.dart';
|
||||||
import 'package:server_box/data/model/ssh/virtual_key.dart';
|
import 'package:server_box/data/model/ssh/virtual_key.dart';
|
||||||
import 'package:server_box/data/res/terminal.dart';
|
import 'package:server_box/data/res/terminal.dart';
|
||||||
|
|
||||||
const _echoPWD = 'echo \$PWD';
|
const _echoPWD = 'echo \$PWD';
|
||||||
|
|
||||||
class SSHPage extends StatefulWidget {
|
final class SshPageArgs {
|
||||||
final Spi spi;
|
final Spi spi;
|
||||||
final String? initCmd;
|
final String? initCmd;
|
||||||
final Snippet? initSnippet;
|
final Snippet? initSnippet;
|
||||||
@@ -34,8 +34,7 @@ class SSHPage extends StatefulWidget {
|
|||||||
final GlobalKey<TerminalViewState>? terminalKey;
|
final GlobalKey<TerminalViewState>? terminalKey;
|
||||||
final FocusNode? focusNode;
|
final FocusNode? focusNode;
|
||||||
|
|
||||||
const SSHPage({
|
const SshPageArgs({
|
||||||
super.key,
|
|
||||||
required this.spi,
|
required this.spi,
|
||||||
this.initCmd,
|
this.initCmd,
|
||||||
this.initSnippet,
|
this.initSnippet,
|
||||||
@@ -44,20 +43,33 @@ class SSHPage extends StatefulWidget {
|
|||||||
this.terminalKey,
|
this.terminalKey,
|
||||||
this.focusNode,
|
this.focusNode,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class SSHPage extends StatefulWidget {
|
||||||
|
final SshPageArgs args;
|
||||||
|
|
||||||
|
const SSHPage({
|
||||||
|
super.key,
|
||||||
|
required this.args,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<SSHPage> createState() => SSHPageState();
|
State<SSHPage> createState() => SSHPageState();
|
||||||
|
|
||||||
|
static const route = AppRouteArg<void, SshPageArgs>(
|
||||||
|
page: SSHPage.new,
|
||||||
|
path: '/ssh/page',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const _horizonPadding = 7.0;
|
const _horizonPadding = 7.0;
|
||||||
|
|
||||||
class SSHPageState extends State<SSHPage>
|
class SSHPageState extends State<SSHPage> with AutomaticKeepAliveClientMixin, AfterLayoutMixin {
|
||||||
with AutomaticKeepAliveClientMixin, AfterLayoutMixin {
|
|
||||||
final _keyboard = VirtKeyProvider();
|
final _keyboard = VirtKeyProvider();
|
||||||
late final _terminal = Terminal(inputHandler: _keyboard);
|
late final _terminal = Terminal(inputHandler: _keyboard);
|
||||||
final TerminalController _terminalController = TerminalController();
|
final TerminalController _terminalController = TerminalController();
|
||||||
final List<List<VirtKey>> _virtKeysList = [];
|
final List<List<VirtKey>> _virtKeysList = [];
|
||||||
late final _termKey = widget.terminalKey ?? GlobalKey<TerminalViewState>();
|
late final _termKey = widget.args.terminalKey ?? GlobalKey<TerminalViewState>();
|
||||||
|
|
||||||
late MediaQueryData _media;
|
late MediaQueryData _media;
|
||||||
late TerminalStyle _terminalStyle;
|
late TerminalStyle _terminalStyle;
|
||||||
@@ -68,7 +80,7 @@ class SSHPageState extends State<SSHPage>
|
|||||||
|
|
||||||
bool _isDark = false;
|
bool _isDark = false;
|
||||||
Timer? _virtKeyLongPressTimer;
|
Timer? _virtKeyLongPressTimer;
|
||||||
late SSHClient? _client = widget.spi.server?.value.client;
|
late SSHClient? _client = widget.args.spi.server?.value.client;
|
||||||
Timer? _discontinuityTimer;
|
Timer? _discontinuityTimer;
|
||||||
|
|
||||||
/// Used for (de)activate the wake lock and forground service
|
/// Used for (de)activate the wake lock and forground service
|
||||||
@@ -148,13 +160,10 @@ class SSHPageState extends State<SSHPage>
|
|||||||
Widget _buildBody() {
|
Widget _buildBody() {
|
||||||
final letterCache = Stores.setting.letterCache.fetch();
|
final letterCache = Stores.setting.letterCache.fetch();
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: _media.size.height -
|
height: _media.size.height - _virtKeysHeight - _media.padding.bottom - _media.padding.top,
|
||||||
_virtKeysHeight -
|
|
||||||
_media.padding.bottom -
|
|
||||||
_media.padding.top,
|
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
top: widget.notFromTab ? CustomAppBar.sysStatusBarHeight : 0,
|
top: widget.args.notFromTab ? CustomAppBar.sysStatusBarHeight : 0,
|
||||||
left: _horizonPadding,
|
left: _horizonPadding,
|
||||||
right: _horizonPadding,
|
right: _horizonPadding,
|
||||||
),
|
),
|
||||||
@@ -175,7 +184,7 @@ class SSHPageState extends State<SSHPage>
|
|||||||
CustomAppBar.sysStatusBarHeight,
|
CustomAppBar.sysStatusBarHeight,
|
||||||
),
|
),
|
||||||
hideScrollBar: false,
|
hideScrollBar: false,
|
||||||
focusNode: widget.focusNode,
|
focusNode: widget.args.focusNode,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -192,8 +201,7 @@ class SSHPageState extends State<SSHPage>
|
|||||||
height: _virtKeysHeight,
|
height: _virtKeysHeight,
|
||||||
child: ChangeNotifierProvider(
|
child: ChangeNotifierProvider(
|
||||||
create: (_) => _keyboard,
|
create: (_) => _keyboard,
|
||||||
builder: (_, __) =>
|
builder: (_, __) => Consumer<VirtKeyProvider>(builder: (_, __, ___) {
|
||||||
Consumer<VirtKeyProvider>(builder: (_, __, ___) {
|
|
||||||
return _buildVirtualKey();
|
return _buildVirtualKey();
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -207,14 +215,11 @@ class SSHPageState extends State<SSHPage>
|
|||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: Row(
|
child: Row(
|
||||||
children:
|
children: _virtKeysList.expand((e) => e).map(_buildVirtKeyItem).toList(),
|
||||||
_virtKeysList.expand((e) => e).map(_buildVirtKeyItem).toList(),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final rows = _virtKeysList
|
final rows = _virtKeysList.map((e) => Row(children: e.map(_buildVirtKeyItem).toList())).toList();
|
||||||
.map((e) => Row(children: e.map(_buildVirtKeyItem).toList()))
|
|
||||||
.toList();
|
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: rows,
|
children: rows,
|
||||||
@@ -243,9 +248,7 @@ class SSHPageState extends State<SSHPage>
|
|||||||
: Text(
|
: Text(
|
||||||
item.text,
|
item.text,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: selected
|
color: selected ? UIs.primaryColor : (_isDark ? Colors.white : Colors.black),
|
||||||
? UIs.primaryColor
|
|
||||||
: (_isDark ? Colors.white : Colors.black),
|
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -315,7 +318,7 @@ extension _Init on SSHPageState {
|
|||||||
Future<void> _initTerminal() async {
|
Future<void> _initTerminal() async {
|
||||||
_writeLn(l10n.waitConnection);
|
_writeLn(l10n.waitConnection);
|
||||||
_client ??= await genClient(
|
_client ??= await genClient(
|
||||||
widget.spi,
|
widget.args.spi,
|
||||||
onStatus: (p0) {
|
onStatus: (p0) {
|
||||||
_writeLn(p0.toString());
|
_writeLn(p0.toString());
|
||||||
},
|
},
|
||||||
@@ -328,7 +331,7 @@ extension _Init on SSHPageState {
|
|||||||
width: _terminal.viewWidth,
|
width: _terminal.viewWidth,
|
||||||
height: _terminal.viewHeight,
|
height: _terminal.viewHeight,
|
||||||
),
|
),
|
||||||
environment: widget.spi.envs,
|
environment: widget.args.spi.envs,
|
||||||
);
|
);
|
||||||
|
|
||||||
//_setupDiscontinuityTimer();
|
//_setupDiscontinuityTimer();
|
||||||
@@ -352,37 +355,34 @@ extension _Init on SSHPageState {
|
|||||||
_listen(session.stderr);
|
_listen(session.stderr);
|
||||||
|
|
||||||
for (final snippet in SnippetProvider.snippets.value) {
|
for (final snippet in SnippetProvider.snippets.value) {
|
||||||
if (snippet.autoRunOn?.contains(widget.spi.id) == true) {
|
if (snippet.autoRunOn?.contains(widget.args.spi.id) == true) {
|
||||||
snippet.runInTerm(_terminal, widget.spi);
|
snippet.runInTerm(_terminal, widget.args.spi);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget.initCmd != null) {
|
if (widget.args.initCmd != null) {
|
||||||
_terminal.textInput(widget.initCmd!);
|
_terminal.textInput(widget.args.initCmd!);
|
||||||
_terminal.keyInput(TerminalKey.enter);
|
_terminal.keyInput(TerminalKey.enter);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget.initSnippet != null) {
|
if (widget.args.initSnippet != null) {
|
||||||
widget.initSnippet!.runInTerm(_terminal, widget.spi);
|
widget.args.initSnippet!.runInTerm(_terminal, widget.args.spi);
|
||||||
}
|
}
|
||||||
|
|
||||||
widget.focusNode?.requestFocus();
|
widget.args.focusNode?.requestFocus();
|
||||||
|
|
||||||
await session.done;
|
await session.done;
|
||||||
if (mounted && widget.notFromTab) {
|
if (mounted && widget.args.notFromTab) {
|
||||||
context.pop();
|
context.pop();
|
||||||
}
|
}
|
||||||
widget.onSessionEnd?.call();
|
widget.args.onSessionEnd?.call();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _listen(Stream<Uint8List>? stream) {
|
void _listen(Stream<Uint8List>? stream) {
|
||||||
if (stream == null) {
|
if (stream == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
stream
|
stream.cast<List<int>>().transform(const Utf8Decoder()).listen(_terminal.write);
|
||||||
.cast<List<int>>()
|
|
||||||
.transform(const Utf8Decoder())
|
|
||||||
.listen(_terminal.write);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setupDiscontinuityTimer() {
|
void _setupDiscontinuityTimer() {
|
||||||
@@ -490,7 +490,7 @@ extension _VirtKey on SSHPageState {
|
|||||||
|
|
||||||
final snippet = snippets.firstOrNull;
|
final snippet = snippets.firstOrNull;
|
||||||
if (snippet == null) return;
|
if (snippet == null) return;
|
||||||
snippet.runInTerm(_terminal, widget.spi);
|
snippet.runInTerm(_terminal, widget.args.spi);
|
||||||
break;
|
break;
|
||||||
case VirtualKeyFunc.file:
|
case VirtualKeyFunc.file:
|
||||||
// get $PWD from SSH session
|
// get $PWD from SSH session
|
||||||
@@ -509,7 +509,8 @@ extension _VirtKey on SSHPageState {
|
|||||||
);
|
);
|
||||||
return;
|
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) {
|
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
|
@override
|
||||||
State<SSHTabPage> createState() => _SSHTabPageState();
|
State<SSHTabPage> createState() => _SSHTabPageState();
|
||||||
|
|
||||||
|
static const route = AppRouteNoArg(
|
||||||
|
page: SSHTabPage.new,
|
||||||
|
path: '/ssh',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef _TabMap = Map<String, ({Widget page, FocusNode? focus})>;
|
typedef _TabMap = Map<String, ({Widget page, FocusNode? focus})>;
|
||||||
@@ -93,10 +98,7 @@ extension on _SSHTabPageState {
|
|||||||
void _onTapInitCard(Spi spi) async {
|
void _onTapInitCard(Spi spi) async {
|
||||||
final name = () {
|
final name = () {
|
||||||
final reg = RegExp('${spi.name}\\((\\d+)\\)');
|
final reg = RegExp('${spi.name}\\((\\d+)\\)');
|
||||||
final idxs = _tabMap.keys
|
final idxs = _tabMap.keys.map((e) => reg.firstMatch(e)).map((e) => e?.group(1)).whereType<String>();
|
||||||
.map((e) => reg.firstMatch(e))
|
|
||||||
.map((e) => e?.group(1))
|
|
||||||
.whereType<String>();
|
|
||||||
if (idxs.isEmpty) {
|
if (idxs.isEmpty) {
|
||||||
return _tabMap.keys.contains(spi.name) ? '${spi.name}(1)' : spi.name;
|
return _tabMap.keys.contains(spi.name) ? '${spi.name}(1)' : spi.name;
|
||||||
}
|
}
|
||||||
@@ -108,15 +110,17 @@ extension on _SSHTabPageState {
|
|||||||
return spi.name;
|
return spi.name;
|
||||||
}();
|
}();
|
||||||
final key = Key(name);
|
final key = Key(name);
|
||||||
|
final args = SshPageArgs(
|
||||||
|
spi: spi,
|
||||||
|
notFromTab: false,
|
||||||
|
onSessionEnd: () {
|
||||||
|
_tabMap.remove(name);
|
||||||
|
},
|
||||||
|
);
|
||||||
_tabMap[name] = (
|
_tabMap[name] = (
|
||||||
page: SSHPage(
|
page: SSHPage(
|
||||||
// Keep it, or the Flutter will works unexpectedly
|
key: key, // Keep it, or the Flutter will works unexpectedly
|
||||||
key: key,
|
args: args,
|
||||||
spi: spi,
|
|
||||||
notFromTab: false,
|
|
||||||
onSessionEnd: () {
|
|
||||||
_tabMap.remove(name);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
focus: FocusNode(),
|
focus: FocusNode(),
|
||||||
);
|
);
|
||||||
@@ -128,8 +132,7 @@ extension on _SSHTabPageState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _toPage(int idx) async {
|
Future<void> _toPage(int idx) async {
|
||||||
await _pageCtrl.animateToPage(idx,
|
await _pageCtrl.animateToPage(idx, duration: Durations.short3, curve: Curves.fastEaseInToSlowEaseOut);
|
||||||
duration: Durations.short3, curve: Curves.fastEaseInToSlowEaseOut);
|
|
||||||
final focus = _tabMap.values.elementAt(idx).focus;
|
final focus = _tabMap.values.elementAt(idx).focus;
|
||||||
if (focus != null) {
|
if (focus != null) {
|
||||||
FocusScope.of(context).requestFocus(focus);
|
FocusScope.of(context).requestFocus(focus);
|
||||||
@@ -156,8 +159,7 @@ extension on _SSHTabPageState {
|
|||||||
|
|
||||||
_tabMap.remove(name);
|
_tabMap.remove(name);
|
||||||
_tabRN.notify();
|
_tabRN.notify();
|
||||||
_pageCtrl.previousPage(
|
_pageCtrl.previousPage(duration: Durations.medium1, curve: Curves.fastEaseInToSlowEaseOut);
|
||||||
duration: Durations.medium1, curve: Curves.fastEaseInToSlowEaseOut);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,8 +280,7 @@ class _AddPage extends StatelessWidget {
|
|||||||
const itemHeight = 50.0;
|
const itemHeight = 50.0;
|
||||||
|
|
||||||
final visualCrossCount = viewWidth / itemWidth;
|
final visualCrossCount = viewWidth / itemWidth;
|
||||||
final crossCount =
|
final crossCount = max(viewWidth ~/ (visualCrossCount * itemPadding + itemWidth), 1);
|
||||||
max(viewWidth ~/ (visualCrossCount * itemPadding + itemWidth), 1);
|
|
||||||
final mainCount = itemCount ~/ crossCount + 1;
|
final mainCount = itemCount ~/ crossCount + 1;
|
||||||
|
|
||||||
return ServerProvider.serverOrder.listenVal((order) {
|
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/provider/sftp.dart';
|
||||||
import 'package:server_box/data/res/misc.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/data/model/app/path_with_prefix.dart';
|
||||||
import 'package:server_box/view/page/editor.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 class LocalFilePageArgs {
|
||||||
final bool? isPickFile;
|
final bool? isPickFile;
|
||||||
@@ -29,15 +30,14 @@ class LocalFilePage extends StatefulWidget {
|
|||||||
|
|
||||||
static const route = AppRoute<String, LocalFilePageArgs>(
|
static const route = AppRoute<String, LocalFilePageArgs>(
|
||||||
page: LocalFilePage.new,
|
page: LocalFilePage.new,
|
||||||
path: '/local_file',
|
path: '/files/local',
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<LocalFilePage> createState() => _LocalFilePageState();
|
State<LocalFilePage> createState() => _LocalFilePageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _LocalFilePageState extends State<LocalFilePage>
|
class _LocalFilePageState extends State<LocalFilePage> with AutomaticKeepAliveClientMixin {
|
||||||
with AutomaticKeepAliveClientMixin {
|
|
||||||
late final _path = LocalPath(widget.args?.initDir ?? Paths.file);
|
late final _path = LocalPath(widget.args?.initDir ?? Paths.file);
|
||||||
final _sortType = _SortType.name.vn;
|
final _sortType = _SortType.name.vn;
|
||||||
bool get isPickFile => widget.args?.isPickFile ?? false;
|
bool get isPickFile => widget.args?.isPickFile ?? false;
|
||||||
@@ -125,13 +125,9 @@ class _LocalFilePageState extends State<LocalFilePage>
|
|||||||
|
|
||||||
return CardX(
|
return CardX(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: isDir
|
leading: isDir ? const Icon(Icons.folder_open) : const Icon(Icons.insert_drive_file),
|
||||||
? const Icon(Icons.folder_open)
|
|
||||||
: const Icon(Icons.insert_drive_file),
|
|
||||||
title: Text(fileName),
|
title: Text(fileName),
|
||||||
subtitle: isDir
|
subtitle: isDir ? null : Text(stat.size.bytes2Str, style: UIs.textGrey),
|
||||||
? null
|
|
||||||
: Text(stat.size.bytes2Str, style: UIs.textGrey),
|
|
||||||
trailing: Text(
|
trailing: Text(
|
||||||
stat.modified.ymdhms(),
|
stat.modified.ymdhms(),
|
||||||
style: UIs.textGrey,
|
style: UIs.textGrey,
|
||||||
@@ -262,10 +258,11 @@ class _LocalFilePageState extends State<LocalFilePage>
|
|||||||
);
|
);
|
||||||
if (spi == null) return;
|
if (spi == null) return;
|
||||||
|
|
||||||
final remotePath = await AppRoutes.sftp(
|
final args = SftpPageArgs(
|
||||||
spi: spi,
|
spi: spi,
|
||||||
isSelect: true,
|
isSelect: true,
|
||||||
).go<String>(context);
|
);
|
||||||
|
final remotePath = await SftpPage.route.go(context, args);
|
||||||
if (remotePath == null) {
|
if (remotePath == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -346,7 +343,7 @@ class _LocalFilePageState extends State<LocalFilePage>
|
|||||||
Widget _buildMissionBtn() {
|
Widget _buildMissionBtn() {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
icon: const Icon(Icons.downloading),
|
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));
|
files.sort((a, b) => a.statSync().size.compareTo(b.statSync().size));
|
||||||
break;
|
break;
|
||||||
case _SortType.time:
|
case _SortType.time:
|
||||||
files.sort(
|
files.sort((a, b) => a.statSync().modified.compareTo(b.statSync().modified));
|
||||||
(a, b) => a.statSync().modified.compareTo(b.statSync().modified));
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return files;
|
return files;
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import 'package:fl_lib/fl_lib.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:server_box/core/extension/context/locale.dart';
|
import 'package:server_box/core/extension/context/locale.dart';
|
||||||
import 'package:server_box/core/extension/sftpfile.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/core/utils/comparator.dart';
|
||||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||||
import 'package:server_box/data/model/sftp/browser_status.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/misc.dart';
|
||||||
import 'package:server_box/data/res/store.dart';
|
import 'package:server_box/data/res/store.dart';
|
||||||
import 'package:server_box/view/page/editor.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/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/omit_start_text.dart';
|
||||||
import 'package:server_box/view/widget/two_line_text.dart';
|
import 'package:server_box/view/widget/two_line_text.dart';
|
||||||
import 'package:server_box/view/widget/unix_perm.dart';
|
import 'package:server_box/view/widget/unix_perm.dart';
|
||||||
|
|
||||||
import 'package:icons_plus/icons_plus.dart';
|
import 'package:icons_plus/icons_plus.dart';
|
||||||
|
|
||||||
class SftpPage extends StatefulWidget {
|
|
||||||
|
final class SftpPageArgs {
|
||||||
final Spi spi;
|
final Spi spi;
|
||||||
final String? initPath;
|
|
||||||
final bool isSelect;
|
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({
|
const SftpPage({
|
||||||
super.key,
|
super.key,
|
||||||
required this.spi,
|
required this.args,
|
||||||
required this.isSelect,
|
|
||||||
this.initPath,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<SftpPage> createState() => _SftpPageState();
|
State<SftpPage> createState() => _SftpPageState();
|
||||||
|
|
||||||
|
static const route = AppRouteArg<String, SftpPageArgs>(
|
||||||
|
page: SftpPage.new,
|
||||||
|
path: '/sftp',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
||||||
late final _status = SftpBrowserStatus(_client);
|
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;
|
final _sortOption = _SortOption().vn;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -54,7 +69,7 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
|||||||
final children = [
|
final children = [
|
||||||
Btn.icon(
|
Btn.icon(
|
||||||
icon: const Icon(Icons.downloading),
|
icon: const Icon(Icons.downloading),
|
||||||
onTap: () => AppRoutes.sftpMission().go(context),
|
onTap: () => SftpMissionPage.route.go(context),
|
||||||
),
|
),
|
||||||
_buildSortMenu(),
|
_buildSortMenu(),
|
||||||
_buildSearchBtn(),
|
_buildSearchBtn(),
|
||||||
@@ -63,7 +78,7 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
|||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: CustomAppBar(
|
appBar: CustomAppBar(
|
||||||
title: TwoLineText(up: 'SFTP', down: widget.spi.name),
|
title: TwoLineText(up: 'SFTP', down: widget.args.spi.name),
|
||||||
actions: children,
|
actions: children,
|
||||||
),
|
),
|
||||||
body: _buildFileView(),
|
body: _buildFileView(),
|
||||||
@@ -75,13 +90,13 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
|||||||
FutureOr<void> afterFirstLayout(BuildContext context) {
|
FutureOr<void> afterFirstLayout(BuildContext context) {
|
||||||
var initPath = '/';
|
var initPath = '/';
|
||||||
if (Stores.setting.sftpOpenLastPath.fetch()) {
|
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) {
|
if (history != null) {
|
||||||
initPath = history;
|
initPath = history;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_status.path.path = widget.initPath ?? initPath;
|
_status.path.path = widget.args.initPath ?? initPath;
|
||||||
_listDir();
|
_listDir();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,9 +108,7 @@ extension _UI on _SftpPageState {
|
|||||||
(_SortType.size, l10n.size),
|
(_SortType.size, l10n.size),
|
||||||
(_SortType.time, l10n.time),
|
(_SortType.time, l10n.time),
|
||||||
];
|
];
|
||||||
return ValBuilder(
|
return _sortOption.listenVal((value) {
|
||||||
listenable: _sortOption,
|
|
||||||
builder: (value) {
|
|
||||||
return PopupMenuButton<_SortType>(
|
return PopupMenuButton<_SortType>(
|
||||||
icon: const Icon(Icons.sort),
|
icon: const Icon(Icons.sort),
|
||||||
itemBuilder: (context) {
|
itemBuilder: (context) {
|
||||||
@@ -130,7 +143,7 @@ extension _UI on _SftpPageState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBottom() {
|
Widget _buildBottom() {
|
||||||
final children = widget.isSelect
|
final children = widget.args.isSelect
|
||||||
? [
|
? [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => context.pop(_status.path.path),
|
onPressed: () => context.pop(_status.path.path),
|
||||||
@@ -168,7 +181,7 @@ extension _UI on _SftpPageState {
|
|||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: _listDir,
|
onRefresh: _listDir,
|
||||||
child: FadeIn(
|
child: FadeIn(
|
||||||
key: Key(widget.spi.name + _status.path.path),
|
key: Key(widget.args.spi.name + _status.path.path),
|
||||||
child: ValBuilder(
|
child: ValBuilder(
|
||||||
listenable: _sortOption,
|
listenable: _sortOption,
|
||||||
builder: (sortOption) {
|
builder: (sortOption) {
|
||||||
@@ -305,7 +318,8 @@ extension _Actions on _SftpPageState {
|
|||||||
if (editor.isNotEmpty) {
|
if (editor.isNotEmpty) {
|
||||||
// Use single quote to avoid escape
|
// Use single quote to avoid escape
|
||||||
final cmd = "$editor '${_getRemotePath(name)}'";
|
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();
|
await _listDir();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -324,7 +338,7 @@ extension _Actions on _SftpPageState {
|
|||||||
final localPath = _getLocalPath(remotePath);
|
final localPath = _getLocalPath(remotePath);
|
||||||
final completer = Completer();
|
final completer = Completer();
|
||||||
final req = SftpReq(
|
final req = SftpReq(
|
||||||
widget.spi,
|
widget.args.spi,
|
||||||
remotePath,
|
remotePath,
|
||||||
localPath,
|
localPath,
|
||||||
SftpReqType.download,
|
SftpReqType.download,
|
||||||
@@ -368,7 +382,7 @@ extension _Actions on _SftpPageState {
|
|||||||
|
|
||||||
SftpProvider.add(
|
SftpProvider.add(
|
||||||
SftpReq(
|
SftpReq(
|
||||||
widget.spi,
|
widget.args.spi,
|
||||||
remotePath,
|
remotePath,
|
||||||
_getLocalPath(remotePath),
|
_getLocalPath(remotePath),
|
||||||
SftpReqType.download,
|
SftpReqType.download,
|
||||||
@@ -604,7 +618,8 @@ extension _Actions on _SftpPageState {
|
|||||||
);
|
);
|
||||||
if (confirm != true) return;
|
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();
|
_listDir();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -616,7 +631,7 @@ extension _Actions on _SftpPageState {
|
|||||||
|
|
||||||
/// Local file dir + server id + remote path
|
/// Local file dir + server id + remote path
|
||||||
String _getLocalPath(String remotePath) {
|
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
|
/// Only return true if the path is changed
|
||||||
@@ -653,7 +668,7 @@ extension _Actions on _SftpPageState {
|
|||||||
|
|
||||||
// Only update history when success
|
// Only update history when success
|
||||||
if (Stores.setting.sftpOpenLastPath.fetch()) {
|
if (Stores.setting.sftpOpenLastPath.fetch()) {
|
||||||
Stores.history.sftpLastPath.put(widget.spi.id, listPath);
|
Stores.history.sftpLastPath.put(widget.args.spi.id, listPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -735,7 +750,7 @@ extension _Actions on _SftpPageState {
|
|||||||
final remotePath = '$remoteDir/$fileName';
|
final remotePath = '$remoteDir/$fileName';
|
||||||
Loggers.app.info('SFTP upload local: $path, remote: $remotePath');
|
Loggers.app.info('SFTP upload local: $path, remote: $remotePath');
|
||||||
SftpProvider.add(
|
SftpProvider.add(
|
||||||
SftpReq(widget.spi, remotePath, path, SftpReqType.upload),
|
SftpReq(widget.args.spi, remotePath, path, SftpReqType.upload),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.upload_file),
|
icon: const Icon(Icons.upload_file),
|
||||||
@@ -817,7 +832,7 @@ extension _Actions on _SftpPageState {
|
|||||||
Widget _buildHomeBtn() {
|
Widget _buildHomeBtn() {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final user = widget.spi.user;
|
final user = widget.args.spi.user;
|
||||||
_status.path.path = user != 'root' ? '/home/$user' : '/root';
|
_status.path.path = user != 'root' ? '/home/$user' : '/root';
|
||||||
_listDir();
|
_listDir();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ class SftpMissionPage extends StatefulWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
State<SftpMissionPage> createState() => _SftpMissionPageState();
|
State<SftpMissionPage> createState() => _SftpMissionPageState();
|
||||||
|
|
||||||
|
static const route = AppRouteNoArg(
|
||||||
|
page: SftpMissionPage.new,
|
||||||
|
path: '/sftp/mission',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SftpMissionPageState extends State<SftpMissionPage> {
|
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/server_private_info.dart';
|
||||||
import 'package:server_box/data/model/server/systemd.dart';
|
import 'package:server_box/data/model/server/systemd.dart';
|
||||||
import 'package:server_box/data/provider/systemd.dart';
|
import 'package:server_box/data/provider/systemd.dart';
|
||||||
|
import 'package:server_box/view/page/ssh/page.dart';
|
||||||
final class SystemdPageArgs {
|
|
||||||
final Spi spi;
|
|
||||||
|
|
||||||
const SystemdPageArgs({
|
|
||||||
required this.spi,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
final class SystemdPage extends StatefulWidget {
|
final class SystemdPage extends StatefulWidget {
|
||||||
final SystemdPageArgs args;
|
final SpiRequiredArgs args;
|
||||||
|
|
||||||
const SystemdPage({
|
const SystemdPage({
|
||||||
super.key,
|
super.key,
|
||||||
required this.args,
|
required this.args,
|
||||||
});
|
});
|
||||||
|
|
||||||
static const route = AppRouteArg<void, SystemdPageArgs>(
|
static const route = AppRouteArg<void, SpiRequiredArgs>(
|
||||||
page: SystemdPage.new,
|
page: SystemdPage.new,
|
||||||
path: '/systemd',
|
path: '/systemd',
|
||||||
);
|
);
|
||||||
@@ -123,7 +116,8 @@ final class _SystemdPageState extends State<SystemdPage> {
|
|||||||
);
|
);
|
||||||
if (sure != true) return;
|
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/server.dart';
|
||||||
import 'package:server_box/data/provider/snippet.dart';
|
import 'package:server_box/data/provider/snippet.dart';
|
||||||
import 'package:server_box/data/res/store.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/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/view/page/systemd.dart';
|
||||||
|
|
||||||
import 'package:server_box/core/route.dart';
|
import 'package:server_box/core/route.dart';
|
||||||
@@ -27,9 +31,7 @@ class ServerFuncBtnsTopRight extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return PopupMenu<ServerFuncBtn>(
|
return PopupMenu<ServerFuncBtn>(
|
||||||
items: ServerFuncBtn.values
|
items: ServerFuncBtn.values.map((e) => PopMenu.build(e, e.icon, e.toStr)).toList(),
|
||||||
.map((e) => PopMenu.build(e, e.icon, e.toStr))
|
|
||||||
.toList(),
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||||
onSelected: (val) => _onTapMoreBtns(val, spi, context),
|
onSelected: (val) => _onTapMoreBtns(val, spi, context),
|
||||||
);
|
);
|
||||||
@@ -46,48 +48,64 @@ class ServerFuncBtns extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final btns = () {
|
final btns = this.btns;
|
||||||
try {
|
if (btns.isEmpty) return UIs.placeholder;
|
||||||
final vals = <ServerFuncBtn>[];
|
|
||||||
final list = Stores.setting.serverFuncBtns.fetch();
|
return SizedBox(
|
||||||
for (final idx in list) {
|
height: 70,
|
||||||
if (idx < 0 || idx >= ServerFuncBtn.values.length) continue;
|
child: ListView.builder(
|
||||||
vals.add(ServerFuncBtn.values[idx]);
|
itemCount: btns.length,
|
||||||
}
|
scrollDirection: Axis.horizontal,
|
||||||
return vals;
|
padding: EdgeInsets.symmetric(horizontal: 13),
|
||||||
} catch (e) {
|
itemBuilder: (context, index) {
|
||||||
return ServerFuncBtn.values;
|
final value = btns[index];
|
||||||
}
|
final item = _buildItem(context, value);
|
||||||
}();
|
return item.paddingSymmetric(horizontal: 7);
|
||||||
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(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
void _onTapMoreBtns(
|
||||||
@@ -95,15 +113,22 @@ void _onTapMoreBtns(
|
|||||||
Spi spi,
|
Spi spi,
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
) async {
|
) async {
|
||||||
|
// final isMobile = ResponsiveBreakpoints.of(context).isMobile;
|
||||||
switch (value) {
|
switch (value) {
|
||||||
// case ServerFuncBtn.pkg:
|
// case ServerFuncBtn.pkg:
|
||||||
// _onPkg(context, spi);
|
// _onPkg(context, spi);
|
||||||
// break;
|
// break;
|
||||||
case ServerFuncBtn.sftp:
|
case ServerFuncBtn.sftp:
|
||||||
AppRoutes.sftp(spi: spi).checkGo(
|
if (!_checkClient(context, spi.id)) return;
|
||||||
context: context,
|
final args = SftpPageArgs(spi: spi);
|
||||||
check: () => _checkClient(context, spi.id),
|
// if (isMobile) {
|
||||||
);
|
SftpPage.route.go(context, args);
|
||||||
|
// } else {
|
||||||
|
// SplitViewNavigator.of(context)?.replace(
|
||||||
|
// SftpPage.route.toWidget(args: args),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case ServerFuncBtn.snippet:
|
case ServerFuncBtn.snippet:
|
||||||
if (SnippetProvider.snippets.value.isEmpty) {
|
if (SnippetProvider.snippets.value.isEmpty) {
|
||||||
@@ -141,32 +166,62 @@ void _onTapMoreBtns(
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
if (sure != true) return;
|
if (sure != true) return;
|
||||||
AppRoutes.ssh(spi: spi, initSnippet: snippet).checkGo(
|
if (!_checkClient(context, spi.id)) return;
|
||||||
context: context,
|
final args = SshPageArgs(spi: spi, initSnippet: snippet);
|
||||||
check: () => _checkClient(context, spi.id),
|
// if (isMobile) {
|
||||||
);
|
SSHPage.route.go(context, args);
|
||||||
|
// } else {
|
||||||
|
// SplitViewNavigator.of(context)?.replace(
|
||||||
|
// SSHPage.route.toWidget(args: args),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
break;
|
break;
|
||||||
case ServerFuncBtn.container:
|
case ServerFuncBtn.container:
|
||||||
AppRoutes.docker(spi: spi).checkGo(
|
if (!_checkClient(context, spi.id)) return;
|
||||||
context: context,
|
final args = SpiRequiredArgs(spi);
|
||||||
check: () => _checkClient(context, spi.id),
|
if (isMobile) {
|
||||||
);
|
ContainerPage.route.go(context, args);
|
||||||
|
} else {
|
||||||
|
SplitViewNavigator.of(context)?.replace(
|
||||||
|
ContainerPage.route.toWidget(args: args),
|
||||||
|
);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case ServerFuncBtn.process:
|
case ServerFuncBtn.process:
|
||||||
AppRoutes.process(spi: spi).checkGo(
|
if (!_checkClient(context, spi.id)) return;
|
||||||
context: context,
|
final args = SpiRequiredArgs(spi);
|
||||||
check: () => _checkClient(context, spi.id),
|
// if (isMobile) {
|
||||||
);
|
ProcessPage.route.go(context, args);
|
||||||
|
// } else {
|
||||||
|
// SplitViewNavigator.of(context)?.replace(
|
||||||
|
// ProcessPage.route.toWidget(args: args),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
break;
|
break;
|
||||||
case ServerFuncBtn.terminal:
|
case ServerFuncBtn.terminal:
|
||||||
_gotoSSH(spi, context);
|
_gotoSSH(spi, context);
|
||||||
break;
|
break;
|
||||||
case ServerFuncBtn.iperf:
|
case ServerFuncBtn.iperf:
|
||||||
if (!_checkClient(context, spi.id)) return;
|
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;
|
break;
|
||||||
case ServerFuncBtn.systemd:
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -174,7 +229,8 @@ void _onTapMoreBtns(
|
|||||||
void _gotoSSH(Spi spi, BuildContext context) async {
|
void _gotoSSH(Spi spi, BuildContext context) async {
|
||||||
// run built-in ssh on macOS due to incompatibility
|
// run built-in ssh on macOS due to incompatibility
|
||||||
if (isMobile || isMacOS) {
|
if (isMobile || isMacOS) {
|
||||||
AppRoutes.ssh(spi: spi).go(context);
|
final args = SshPageArgs(spi: spi);
|
||||||
|
SSHPage.route.go(context, args);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final extraArgs = <String>[];
|
final extraArgs = <String>[];
|
||||||
|
|||||||
148
pubspec.lock
148
pubspec.lock
@@ -22,6 +22,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.11.0"
|
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:
|
ansicolor:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -238,6 +254,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.2"
|
version: "2.3.2"
|
||||||
|
ci:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: ci
|
||||||
|
sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.0"
|
||||||
circle_chart:
|
circle_chart:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -247,6 +271,14 @@ packages:
|
|||||||
url: "https://github.com/lollipopkit/circle_chart"
|
url: "https://github.com/lollipopkit/circle_chart"
|
||||||
source: git
|
source: git
|
||||||
version: "0.0.3"
|
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:
|
clock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -328,6 +360,38 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.2"
|
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:
|
dart_style:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -394,7 +458,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.3"
|
version: "5.0.3"
|
||||||
equatable:
|
equatable:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: equatable
|
name: equatable
|
||||||
sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7"
|
sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7"
|
||||||
@@ -478,8 +542,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: "v1.0.269"
|
ref: "v1.0.281"
|
||||||
resolved-ref: "338eb9135ea24f1d936ec023511a22d6c409576d"
|
resolved-ref: cff32a02af3622926d10c209fe07abe64408675c
|
||||||
url: "https://github.com/lppcg/fl_lib"
|
url: "https://github.com/lppcg/fl_lib"
|
||||||
source: git
|
source: git
|
||||||
version: "0.0.1"
|
version: "0.0.1"
|
||||||
@@ -488,14 +552,6 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
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:
|
flutter_displaymode:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -638,7 +694,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "5.5.0"
|
version: "5.5.0"
|
||||||
flutter_riverpod:
|
flutter_riverpod:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_riverpod
|
name: flutter_riverpod
|
||||||
sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1"
|
sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1"
|
||||||
@@ -663,8 +719,16 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
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:
|
freezed_annotation:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: freezed_annotation
|
name: freezed_annotation
|
||||||
sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
|
sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
|
||||||
@@ -735,6 +799,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.1"
|
version: "2.0.1"
|
||||||
|
hotreloader:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: hotreloader
|
||||||
|
sha256: bc167a1163807b03bada490bfe2df25b0d744df359227880220a5cbd04e5734b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.3.0"
|
||||||
html:
|
html:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -983,6 +1055,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
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:
|
nested:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1200,6 +1280,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.2"
|
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:
|
riverpod:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1208,14 +1296,46 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.6.1"
|
version: "2.6.1"
|
||||||
riverpod_annotation:
|
riverpod_analyzer_utils:
|
||||||
dependency: transitive
|
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:
|
description:
|
||||||
name: riverpod_annotation
|
name: riverpod_annotation
|
||||||
sha256: e14b0bf45b71326654e2705d462f21b958f987087be850afd60578fcd502d1b8
|
sha256: e14b0bf45b71326654e2705d462f21b958f987087be850afd60578fcd502d1b8
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.6.1"
|
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:
|
screen_retriever:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
44
pubspec.yaml
44
pubspec.yaml
@@ -1,6 +1,6 @@
|
|||||||
name: server_box
|
name: server_box
|
||||||
description: server status & toolbox app.
|
description: server status & toolbox app.
|
||||||
publish_to: 'none'
|
publish_to: "none"
|
||||||
version: 1.0.1128+1128
|
version: 1.0.1128+1128
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
@@ -16,6 +16,7 @@ dependencies:
|
|||||||
easy_isolate: ^1.3.0
|
easy_isolate: ^1.3.0
|
||||||
intl: ^0.19.0
|
intl: ^0.19.0
|
||||||
highlight: ^0.7.0
|
highlight: ^0.7.0
|
||||||
|
equatable: ^2.0.7
|
||||||
flutter_highlight: ^0.7.0
|
flutter_highlight: ^0.7.0
|
||||||
code_text_field: ^1.1.0
|
code_text_field: ^1.1.0
|
||||||
shared_preferences: ^2.1.1
|
shared_preferences: ^2.1.1
|
||||||
@@ -31,7 +32,10 @@ dependencies:
|
|||||||
choice: ^2.3.2
|
choice: ^2.3.2
|
||||||
flutter_reorderable_grid_view: ^5.1.0
|
flutter_reorderable_grid_view: ^5.1.0
|
||||||
webdav_client_plus: ^1.0.2
|
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:
|
dartssh2:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/lollipopkit/dartssh2
|
url: https://github.com/lollipopkit/dartssh2
|
||||||
@@ -59,7 +63,7 @@ dependencies:
|
|||||||
fl_lib:
|
fl_lib:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/lppcg/fl_lib
|
url: https://github.com/lppcg/fl_lib
|
||||||
ref: v1.0.269
|
ref: v1.0.281
|
||||||
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
# webdav_client_plus:
|
# webdav_client_plus:
|
||||||
@@ -76,9 +80,13 @@ dependency_overrides:
|
|||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_native_splash: ^2.1.6
|
flutter_native_splash: ^2.1.6
|
||||||
hive_generator: ^2.0.0
|
hive_generator: ^2.0.0
|
||||||
build_runner: ^2.3.2
|
build_runner: ^2.4.15
|
||||||
flutter_lints: ^5.0.0
|
flutter_lints: ^5.0.0
|
||||||
json_serializable: ^6.8.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:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
fl_build:
|
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.
|
# size of the app. Only one parameter can be used, color and background_image cannot both be set.
|
||||||
color: "#ffffff"
|
color: "#ffffff"
|
||||||
#background_image: "assets/background.png"
|
#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 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.
|
# png file and should be sized for 4x pixel density.
|
||||||
image: assets/app_icon.png
|
image: assets/app_icon.png
|
||||||
|
|
||||||
# The color_dark, background_image_dark, and image_dark are parameters that set the background
|
# 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
|
# 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
|
# background_image_dark must be specified. color_dark and background_image_dark cannot both be
|
||||||
# set.
|
# set.
|
||||||
color_dark: "#121212"
|
color_dark: "#121212"
|
||||||
#background_image_dark: "assets/dark-background.png"
|
#background_image_dark: "assets/dark-background.png"
|
||||||
#image_dark: assets/splash-invert.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.
|
# platform.
|
||||||
#android: false
|
#android: false
|
||||||
#ios: 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
|
# The position of the splash image can be set with android_gravity, ios_content_mode, and
|
||||||
# web_image_mode parameters. All default to center.
|
# web_image_mode parameters. All default to center.
|
||||||
#
|
#
|
||||||
# android_gravity can be one of the following Android Gravity (see
|
# android_gravity can be one of the following Android Gravity (see
|
||||||
# https://developer.android.com/reference/android/view/Gravity): bottom, center,
|
# https://developer.android.com/reference/android/view/Gravity): bottom, center,
|
||||||
# center_horizontal, center_vertical, clip_horizontal, clip_vertical, end, fill, fill_horizontal,
|
# center_horizontal, center_vertical, clip_horizontal, clip_vertical, end, fill, fill_horizontal,
|
||||||
# fill_vertical, left, right, start, or top.
|
# fill_vertical, left, right, start, or top.
|
||||||
#android_gravity: center
|
#android_gravity: center
|
||||||
#
|
#
|
||||||
# ios_content_mode can be one of the following iOS UIView.ContentMode (see
|
# ios_content_mode can be one of the following iOS UIView.ContentMode (see
|
||||||
# https://developer.apple.com/documentation/uikit/uiview/contentmode): scaleToFill,
|
# https://developer.apple.com/documentation/uikit/uiview/contentmode): scaleToFill,
|
||||||
# scaleAspectFit, scaleAspectFill, center, top, bottom, left, right, topLeft, topRight,
|
# scaleAspectFit, scaleAspectFill, center, top, bottom, left, right, topLeft, topRight,
|
||||||
# bottomLeft, or bottomRight.
|
# bottomLeft, or bottomRight.
|
||||||
#ios_content_mode: center
|
#ios_content_mode: center
|
||||||
#
|
#
|
||||||
# web_image_mode can be one of the following modes: center, contain, stretch, and cover.
|
# web_image_mode can be one of the following modes: center, contain, stretch, and cover.
|
||||||
#web_image_mode: center
|
#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.
|
# has no notification bar. Defaults to false.
|
||||||
# NOTE: Unlike Android, iOS will not automatically show the notification bar when the app loads.
|
# 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:
|
# To show the notification bar, add the following code to your Flutter app:
|
||||||
# WidgetsFlutterBinding.ensureInitialized();
|
# WidgetsFlutterBinding.ensureInitialized();
|
||||||
# SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom, SystemUiOverlay.top]);
|
# SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom, SystemUiOverlay.top]);
|
||||||
#fullscreen: true
|
#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,
|
# with the info_plist_files parameter. Remove only the # characters in the three lines below,
|
||||||
# do not remove any spaces:
|
# do not remove any spaces:
|
||||||
info_plist_files:
|
info_plist_files:
|
||||||
- 'ios/Runner/Info-Debug.plist'
|
- "ios/Runner/Info-Debug.plist"
|
||||||
- 'ios/Runner/Info-Profile.plist'
|
- "ios/Runner/Info-Profile.plist"
|
||||||
- 'ios/Runner/Info-Release.plist'
|
- "ios/Runner/Info-Release.plist"
|
||||||
# To enable support for Android 12, set the following parameter to true. Defaults to false.
|
# To enable support for Android 12, set the following parameter to true. Defaults to false.
|
||||||
android12: true
|
android12: true
|
||||||
|
|||||||
Reference in New Issue
Block a user