mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2026-01-31 13:25:10 +01:00
migrate: riverpod + freezed (#870)
This commit is contained in:
@@ -4,6 +4,7 @@ import 'dart:io';
|
||||
import 'package:computer/computer.dart';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:icons_plus/icons_plus.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/core/sync.dart';
|
||||
@@ -17,16 +18,16 @@ import 'package:server_box/data/res/misc.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
import 'package:webdav_client_plus/webdav_client_plus.dart';
|
||||
|
||||
class BackupPage extends StatefulWidget {
|
||||
class BackupPage extends ConsumerStatefulWidget {
|
||||
const BackupPage({super.key});
|
||||
|
||||
@override
|
||||
State<BackupPage> createState() => _BackupPageState();
|
||||
ConsumerState<BackupPage> createState() => _BackupPageState();
|
||||
|
||||
static const route = AppRouteNoArg(page: BackupPage.new, path: '/backup');
|
||||
}
|
||||
|
||||
final class _BackupPageState extends State<BackupPage> with AutomaticKeepAliveClientMixin {
|
||||
final class _BackupPageState extends ConsumerState<BackupPage> with AutomaticKeepAliveClientMixin {
|
||||
final webdavLoading = false.vn;
|
||||
final gistLoading = false.vn;
|
||||
|
||||
@@ -401,8 +402,9 @@ final class _BackupPageState extends State<BackupPage> with AutomaticKeepAliveCl
|
||||
child: SingleChildScrollView(child: Text(libL10n.askContinue('${libL10n.import} [$snippetNames]'))),
|
||||
actions: Btn.ok(
|
||||
onTap: () {
|
||||
final notifier = ref.read(snippetNotifierProvider.notifier);
|
||||
for (final snippet in snippets) {
|
||||
SnippetProvider.add(snippet);
|
||||
notifier.add(snippet);
|
||||
}
|
||||
context.pop();
|
||||
context.pop();
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
part of 'container.dart';
|
||||
|
||||
extension on _ContainerPageState {
|
||||
/// The notifier for the container state.
|
||||
ContainerNotifier get _containerNotifier => ref.read(_provider.notifier);
|
||||
|
||||
/// Watch the current state of the container.
|
||||
ContainerState get _containerState => ref.watch(_provider);
|
||||
|
||||
Future<void> _showAddFAB() async {
|
||||
final imageCtrl = TextEditingController();
|
||||
final nameCtrl = TextEditingController();
|
||||
@@ -79,7 +85,7 @@ extension on _ContainerPageState {
|
||||
onPressed: () async {
|
||||
context.pop();
|
||||
|
||||
final (result, err) = await context.showLoadingDialog(fn: () => _container.run(cmd));
|
||||
final (result, err) = await context.showLoadingDialog(fn: () => _containerNotifier.run(cmd));
|
||||
if (err != null || result != null) {
|
||||
final e = result?.message ?? err?.toString();
|
||||
context.showRoundDialog(title: libL10n.error, child: Text(e.toString()));
|
||||
@@ -111,7 +117,7 @@ extension on _ContainerPageState {
|
||||
void _onSaveDockerHost(String val) {
|
||||
context.pop();
|
||||
Stores.container.put(widget.args.spi.id, val.trim());
|
||||
_container.refresh();
|
||||
_containerNotifier.refresh();
|
||||
}
|
||||
|
||||
void _showImageRmDialog(ContainerImg e) {
|
||||
@@ -121,7 +127,7 @@ extension on _ContainerPageState {
|
||||
actions: Btn.ok(
|
||||
onTap: () async {
|
||||
context.pop();
|
||||
final result = await _container.run('rmi ${e.id} -f');
|
||||
final result = await _containerNotifier.run('rmi ${e.id} -f');
|
||||
if (result != null) {
|
||||
context.showSnackBar(result.message ?? 'null');
|
||||
}
|
||||
@@ -163,7 +169,9 @@ extension on _ContainerPageState {
|
||||
onTap: () async {
|
||||
context.pop();
|
||||
|
||||
final (result, err) = await context.showLoadingDialog(fn: () => _container.delete(id, force));
|
||||
final (result, err) = await context.showLoadingDialog(
|
||||
fn: () => _containerNotifier.delete(id, force),
|
||||
);
|
||||
if (err != null || result != null) {
|
||||
final e = result?.message ?? err?.toString();
|
||||
context.showRoundDialog(title: libL10n.error, child: Text(e ?? 'null'));
|
||||
@@ -173,21 +181,21 @@ extension on _ContainerPageState {
|
||||
);
|
||||
break;
|
||||
case ContainerMenu.start:
|
||||
final (result, err) = await context.showLoadingDialog(fn: () => _container.start(id));
|
||||
final (result, err) = await context.showLoadingDialog(fn: () => _containerNotifier.start(id));
|
||||
if (err != null || result != null) {
|
||||
final e = result?.message ?? err?.toString();
|
||||
context.showRoundDialog(title: libL10n.error, child: Text(e ?? 'null'));
|
||||
}
|
||||
break;
|
||||
case ContainerMenu.stop:
|
||||
final (result, err) = await context.showLoadingDialog(fn: () => _container.stop(id));
|
||||
final (result, err) = await context.showLoadingDialog(fn: () => _containerNotifier.stop(id));
|
||||
if (err != null || result != null) {
|
||||
final e = result?.message ?? err?.toString();
|
||||
context.showRoundDialog(title: libL10n.error, child: Text(e ?? 'null'));
|
||||
}
|
||||
break;
|
||||
case ContainerMenu.restart:
|
||||
final (result, err) = await context.showLoadingDialog(fn: () => _container.restart(id));
|
||||
final (result, err) = await context.showLoadingDialog(fn: () => _containerNotifier.restart(id));
|
||||
if (err != null || result != null) {
|
||||
final e = result?.message ?? err?.toString();
|
||||
context.showRoundDialog(title: libL10n.error, child: Text(e ?? 'null'));
|
||||
@@ -197,7 +205,7 @@ extension on _ContainerPageState {
|
||||
final args = SshPageArgs(
|
||||
spi: widget.args.spi,
|
||||
initCmd:
|
||||
'${switch (_container.type) {
|
||||
'${switch (_containerState.type) {
|
||||
ContainerType.podman => 'podman',
|
||||
ContainerType.docker => 'docker',
|
||||
}} logs -f --tail 100 ${dItem.id}',
|
||||
@@ -208,7 +216,7 @@ extension on _ContainerPageState {
|
||||
final args = SshPageArgs(
|
||||
spi: widget.args.spi,
|
||||
initCmd:
|
||||
'${switch (_container.type) {
|
||||
'${switch (_containerState.type) {
|
||||
ContainerType.podman => 'podman',
|
||||
ContainerType.docker => 'docker',
|
||||
}} exec -it ${dItem.id} sh',
|
||||
@@ -222,7 +230,7 @@ extension on _ContainerPageState {
|
||||
if (Stores.setting.containerAutoRefresh.fetch()) {
|
||||
Timer.periodic(Duration(seconds: Stores.setting.serverStatusUpdateInterval.fetch()), (timer) {
|
||||
if (mounted) {
|
||||
_container.refresh(isAuto: true);
|
||||
_containerNotifier.refresh(isAuto: true);
|
||||
} else {
|
||||
timer.cancel();
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import 'dart:async';
|
||||
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:icons_plus/icons_plus.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/core/route.dart';
|
||||
import 'package:server_box/data/model/app/error.dart';
|
||||
@@ -12,81 +12,79 @@ import 'package:server_box/data/model/app/menu/container.dart';
|
||||
import 'package:server_box/data/model/container/image.dart';
|
||||
import 'package:server_box/data/model/container/ps.dart';
|
||||
import 'package:server_box/data/model/container/type.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/server.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
import 'package:server_box/view/page/ssh/page/page.dart';
|
||||
|
||||
part 'actions.dart';
|
||||
part 'types.dart';
|
||||
|
||||
class ContainerPage extends StatefulWidget {
|
||||
class ContainerPage extends ConsumerStatefulWidget {
|
||||
final SpiRequiredArgs args;
|
||||
const ContainerPage({required this.args, super.key});
|
||||
|
||||
@override
|
||||
State<ContainerPage> createState() => _ContainerPageState();
|
||||
ConsumerState<ContainerPage> createState() => _ContainerPageState();
|
||||
|
||||
static const route = AppRouteArg(page: ContainerPage.new, path: '/container');
|
||||
}
|
||||
|
||||
class _ContainerPageState extends State<ContainerPage> {
|
||||
class _ContainerPageState extends ConsumerState<ContainerPage> {
|
||||
final _textController = TextEditingController();
|
||||
late final _container = ContainerProvider(
|
||||
client: widget.args.spi.server?.value.client,
|
||||
userName: widget.args.spi.user,
|
||||
hostId: widget.args.spi.id,
|
||||
context: context,
|
||||
);
|
||||
late final ContainerNotifierProvider _provider;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_textController.dispose();
|
||||
_container.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final serverState = ref.read(individualServerNotifierProvider(widget.args.spi.id));
|
||||
_provider = containerNotifierProvider(
|
||||
serverState.client,
|
||||
widget.args.spi.user,
|
||||
widget.args.spi.id,
|
||||
context,
|
||||
);
|
||||
_initAutoRefresh();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProvider(
|
||||
create: (_) => _container,
|
||||
builder: (_, _) => Consumer<ContainerProvider>(
|
||||
builder: (_, _, _) {
|
||||
return Scaffold(
|
||||
appBar: _buildAppBar,
|
||||
body: SafeArea(child: _buildMain),
|
||||
floatingActionButton: _container.error == null ? _buildFAB : null,
|
||||
);
|
||||
},
|
||||
),
|
||||
final err = ref.watch(_provider.select((p) => p.error));
|
||||
|
||||
return Scaffold(
|
||||
appBar: _buildAppBar(),
|
||||
body: SafeArea(child: _buildMain()),
|
||||
floatingActionButton: err == null ? _buildFAB() : null,
|
||||
);
|
||||
}
|
||||
|
||||
CustomAppBar get _buildAppBar {
|
||||
CustomAppBar _buildAppBar() {
|
||||
return CustomAppBar(
|
||||
centerTitle: true,
|
||||
title: TwoLineText(up: l10n.container, down: widget.args.spi.name),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () => context.showLoadingDialog(fn: () => _container.refresh()),
|
||||
onPressed: () => context.showLoadingDialog(fn: () => _containerNotifier.refresh()),
|
||||
icon: const Icon(Icons.refresh),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget get _buildFAB {
|
||||
Widget _buildFAB() {
|
||||
return FloatingActionButton(onPressed: () async => await _showAddFAB(), child: const Icon(Icons.add));
|
||||
}
|
||||
|
||||
Widget get _buildMain {
|
||||
if (_container.error != null && _container.items == null) {
|
||||
Widget _buildMain() {
|
||||
final containerState = _containerState;
|
||||
|
||||
if (containerState.error != null && containerState.items == null) {
|
||||
return SizedBox.expand(
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -95,7 +93,7 @@ class _ContainerPageState extends State<ContainerPage> {
|
||||
UIs.height13,
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 23),
|
||||
child: Text(_container.error.toString()),
|
||||
child: Text(containerState.error.toString()),
|
||||
),
|
||||
const Spacer(),
|
||||
UIs.height13,
|
||||
@@ -104,27 +102,27 @@ class _ContainerPageState extends State<ContainerPage> {
|
||||
).paddingSymmetric(horizontal: 13),
|
||||
);
|
||||
}
|
||||
if (_container.items == null || _container.images == null) {
|
||||
if (containerState.items == null || containerState.images == null) {
|
||||
return UIs.centerLoading;
|
||||
}
|
||||
|
||||
return AutoMultiList(
|
||||
children: <Widget>[
|
||||
_buildLoading(),
|
||||
_buildVersion(),
|
||||
_buildPs(),
|
||||
_buildImage(),
|
||||
_buildEmptyStateMessage(),
|
||||
_buildLoading(containerState),
|
||||
_buildVersion(containerState),
|
||||
_buildPs(containerState),
|
||||
_buildImage(containerState),
|
||||
_buildEmptyStateMessage(containerState),
|
||||
_buildPruneBtns,
|
||||
_buildSettingsBtns,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyStateMessage() {
|
||||
final emptyImgs = _container.images?.isEmpty ?? true;
|
||||
final emptyPs = _container.items?.isEmpty ?? true;
|
||||
if (emptyPs && emptyImgs && _container.runLog == null) {
|
||||
Widget _buildEmptyStateMessage(ContainerState containerState) {
|
||||
final emptyImgs = containerState.images?.isEmpty ?? true;
|
||||
final emptyPs = containerState.items?.isEmpty ?? true;
|
||||
if (emptyPs && emptyImgs && containerState.runLog == null) {
|
||||
return CardX(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(17, 17, 17, 7),
|
||||
@@ -135,13 +133,13 @@ class _ContainerPageState extends State<ContainerPage> {
|
||||
return UIs.placeholder;
|
||||
}
|
||||
|
||||
Widget _buildImage() {
|
||||
Widget _buildImage(ContainerState containerState) {
|
||||
return ExpandTile(
|
||||
leading: const Icon(MingCute.clapperboard_line),
|
||||
title: Text(l10n.imagesList),
|
||||
subtitle: Text(l10n.dockerImagesFmt(_container.images!.length), style: UIs.textGrey),
|
||||
initiallyExpanded: (_container.images?.length ?? 0) <= 3,
|
||||
children: _container.images?.map(_buildImageItem).toList() ?? [],
|
||||
subtitle: Text(l10n.dockerImagesFmt(containerState.images?.length ?? 'null'), style: UIs.textGrey),
|
||||
initiallyExpanded: (containerState.images?.length ?? 0) <= 3,
|
||||
children: containerState.images?.map(_buildImageItem).toList() ?? [],
|
||||
).cardx;
|
||||
}
|
||||
|
||||
@@ -161,34 +159,34 @@ class _ContainerPageState extends State<ContainerPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoading() {
|
||||
if (_container.runLog == null) return UIs.placeholder;
|
||||
Widget _buildLoading(ContainerState containerState) {
|
||||
if (containerState.runLog == null) return UIs.placeholder;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(17),
|
||||
child: Column(
|
||||
children: [
|
||||
const Center(child: CircularProgressIndicator()),
|
||||
UIs.height13,
|
||||
Text(_container.runLog ?? '...'),
|
||||
Text(containerState.runLog ?? '...'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVersion() {
|
||||
Widget _buildVersion(ContainerState containerState) {
|
||||
return CardX(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(17),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [Text(_container.type.name.capitalize), Text(_container.version ?? l10n.unknown)],
|
||||
children: [Text(containerState.type.name.capitalize), Text(containerState.version ?? l10n.unknown)],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPs() {
|
||||
final items = _container.items;
|
||||
Widget _buildPs(ContainerState containerState) {
|
||||
final items = containerState.items;
|
||||
if (items == null) return UIs.placeholder;
|
||||
final running = items.where((e) => e.running).length;
|
||||
final stopped = items.length - running;
|
||||
@@ -309,16 +307,17 @@ class _ContainerPageState extends State<ContainerPage> {
|
||||
|
||||
Widget _buildPruneBtn(_PruneTypes type) {
|
||||
final title = type.name.capitalize;
|
||||
final containerNotifier = _containerNotifier;
|
||||
return ListTile(
|
||||
onTap: () async {
|
||||
await _showPruneDialog(
|
||||
title: title,
|
||||
message: type.tip,
|
||||
onConfirm: switch (type) {
|
||||
_PruneTypes.images => _container.pruneImages,
|
||||
_PruneTypes.containers => _container.pruneContainers,
|
||||
_PruneTypes.volumes => _container.pruneVolumes,
|
||||
_PruneTypes.system => _container.pruneSystem,
|
||||
_PruneTypes.images => containerNotifier.pruneImages,
|
||||
_PruneTypes.containers => containerNotifier.pruneContainers,
|
||||
_PruneTypes.volumes => containerNotifier.pruneVolumes,
|
||||
_PruneTypes.system => containerNotifier.pruneSystem,
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -330,22 +329,26 @@ class _ContainerPageState extends State<ContainerPage> {
|
||||
Widget get _buildSettingsBtns {
|
||||
final len = _SettingsMenuItems.values.length;
|
||||
if (len == 0) return UIs.placeholder;
|
||||
final containerState = _containerState;
|
||||
|
||||
return ExpandTile(
|
||||
leading: const Icon(Icons.settings),
|
||||
title: Text(libL10n.setting),
|
||||
initiallyExpanded: _container.error != null,
|
||||
children: _SettingsMenuItems.values.map(_buildSettingTile).toList(),
|
||||
initiallyExpanded: containerState.error != null,
|
||||
children: _SettingsMenuItems.values.map((item) => _buildSettingTile(item, containerState)).toList(),
|
||||
).cardx;
|
||||
}
|
||||
|
||||
Widget _buildSettingTile(_SettingsMenuItems item) {
|
||||
Widget _buildSettingTile(_SettingsMenuItems item, ContainerState containerState) {
|
||||
final String title;
|
||||
switch (item) {
|
||||
case _SettingsMenuItems.editDockerHost:
|
||||
title = '${libL10n.edit} DOCKER_HOST';
|
||||
break;
|
||||
case _SettingsMenuItems.switchProvider:
|
||||
title = _container.type == ContainerType.podman ? l10n.switchTo('Docker') : l10n.switchTo('Podman');
|
||||
title = containerState.type == ContainerType.podman
|
||||
? l10n.switchTo('Docker')
|
||||
: l10n.switchTo('Podman');
|
||||
break;
|
||||
}
|
||||
return ListTile(
|
||||
@@ -355,9 +358,11 @@ class _ContainerPageState extends State<ContainerPage> {
|
||||
_showEditHostDialog();
|
||||
break;
|
||||
case _SettingsMenuItems.switchProvider:
|
||||
_container.setType(
|
||||
_container.type == ContainerType.docker ? ContainerType.podman : ContainerType.docker,
|
||||
);
|
||||
ref
|
||||
.read(_provider.notifier)
|
||||
.setType(
|
||||
containerState.type == ContainerType.docker ? ContainerType.podman : ContainerType.docker,
|
||||
);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:responsive_framework/responsive_framework.dart';
|
||||
import 'package:server_box/core/chan.dart';
|
||||
import 'package:server_box/core/sync.dart';
|
||||
import 'package:server_box/data/model/app/tab.dart';
|
||||
import 'package:server_box/data/provider/server.dart';
|
||||
import 'package:server_box/data/res/build_data.dart';
|
||||
@@ -10,16 +12,16 @@ import 'package:server_box/data/res/url.dart';
|
||||
import 'package:server_box/view/page/setting/entry.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
class HomePage extends ConsumerStatefulWidget {
|
||||
const HomePage({super.key});
|
||||
|
||||
@override
|
||||
State<HomePage> createState() => _HomePageState();
|
||||
ConsumerState<HomePage> createState() => _HomePageState();
|
||||
|
||||
static const route = AppRouteNoArg(page: HomePage.new, path: '/');
|
||||
}
|
||||
|
||||
class _HomePageState extends State<HomePage>
|
||||
class _HomePageState extends ConsumerState<HomePage>
|
||||
with AutomaticKeepAliveClientMixin, AfterLayoutMixin, WidgetsBindingObserver {
|
||||
late final PageController _pageController;
|
||||
|
||||
@@ -29,11 +31,14 @@ class _HomePageState extends State<HomePage>
|
||||
bool _shouldAuth = false;
|
||||
DateTime? _pausedTime;
|
||||
|
||||
late final _notifier = ref.read(serverNotifierProvider.notifier);
|
||||
late final _provider = ref.read(serverNotifierProvider);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
ServerProvider.closeServer();
|
||||
Future(() => _notifier.closeServer());
|
||||
_pageController.dispose();
|
||||
WakelockPlus.disable();
|
||||
|
||||
@@ -76,8 +81,9 @@ class _HomePageState extends State<HomePage>
|
||||
_goAuth();
|
||||
}
|
||||
}
|
||||
if (!ServerProvider.isAutoRefreshOn) {
|
||||
ServerProvider.startAutoRefresh();
|
||||
final serverNotifier = _notifier;
|
||||
if (_provider.autoRefreshTimer == null) {
|
||||
serverNotifier.startAutoRefresh();
|
||||
}
|
||||
MethodChans.updateHomeWidget();
|
||||
break;
|
||||
@@ -92,7 +98,7 @@ class _HomePageState extends State<HomePage>
|
||||
// }
|
||||
} else {
|
||||
//Pros.server.setDisconnected();
|
||||
ServerProvider.stopAutoRefresh();
|
||||
_notifier.stopAutoRefresh();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@@ -194,7 +200,9 @@ class _HomePageState extends State<HomePage>
|
||||
AppUpdateIface.doUpdate(build: BuildData.build, url: Urls.updateCfg, context: context);
|
||||
}
|
||||
MethodChans.updateHomeWidget();
|
||||
await ServerProvider.refresh();
|
||||
await _notifier.refresh();
|
||||
|
||||
bakSync.sync(milliDelay: 1000);
|
||||
}
|
||||
|
||||
// Future<void> _reqNotiPerm() async {
|
||||
@@ -202,7 +210,6 @@ class _HomePageState extends State<HomePage>
|
||||
// final suc = await PermUtils.request(Permission.notification);
|
||||
// if (!suc) {
|
||||
// final noNotiPerm = Stores.setting.noNotiPerm;
|
||||
// if (noNotiPerm.fetch()) return;
|
||||
// context.showRoundDialog(
|
||||
// title: l10n.error,
|
||||
// child: Text(l10n.noNotiPerm),
|
||||
@@ -212,6 +219,7 @@ class _HomePageState extends State<HomePage>
|
||||
// noNotiPerm.put(true);
|
||||
// context.pop();
|
||||
// },
|
||||
// if (noNotiPerm.fetch()) return;
|
||||
// child: Text(l10n.ok),
|
||||
// ),
|
||||
// ],
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/data/model/server/ping_result.dart';
|
||||
import 'package:server_box/data/provider/server.dart';
|
||||
@@ -9,16 +10,16 @@ import 'package:server_box/data/provider/server.dart';
|
||||
/// Only permit ipv4 / ipv6 / domain chars
|
||||
final targetReg = RegExp(r'[a-zA-Z0-9\.-_:]+');
|
||||
|
||||
class PingPage extends StatefulWidget {
|
||||
class PingPage extends ConsumerStatefulWidget {
|
||||
const PingPage({super.key});
|
||||
|
||||
@override
|
||||
State<PingPage> createState() => _PingPageState();
|
||||
ConsumerState<PingPage> createState() => _PingPageState();
|
||||
|
||||
static const route = AppRouteNoArg(page: PingPage.new, path: '/ping');
|
||||
}
|
||||
|
||||
class _PingPageState extends State<PingPage> with AutomaticKeepAliveClientMixin {
|
||||
class _PingPageState extends ConsumerState<PingPage> with AutomaticKeepAliveClientMixin {
|
||||
late TextEditingController _textEditingController;
|
||||
final _results = ValueNotifier(<PingResult>[]);
|
||||
bool get isInit => _results.value.isEmpty;
|
||||
@@ -129,7 +130,7 @@ class _PingPageState extends State<PingPage> with AutomaticKeepAliveClientMixin
|
||||
return;
|
||||
}
|
||||
|
||||
if (ServerProvider.serverOrder.value.isEmpty) {
|
||||
if (ref.read(serverNotifierProvider).serverOrder.isEmpty) {
|
||||
context.showSnackBar(l10n.pingNoServer);
|
||||
return;
|
||||
}
|
||||
@@ -141,13 +142,13 @@ class _PingPageState extends State<PingPage> with AutomaticKeepAliveClientMixin
|
||||
}
|
||||
|
||||
await Future.wait(
|
||||
ServerProvider.servers.values.map((v) async {
|
||||
final e = v.value;
|
||||
if (e.client == null) {
|
||||
ref.read(serverNotifierProvider).servers.values.map((spi) async {
|
||||
final serverState = ref.read(individualServerNotifierProvider(spi.id));
|
||||
if (serverState.client == null) {
|
||||
return;
|
||||
}
|
||||
final result = await e.client!.run('ping -c 3 $target').string;
|
||||
_results.value.add(PingResult.parse(e.spi.name, result));
|
||||
final result = await serverState.client!.run('ping -c 3 $target').string;
|
||||
_results.value.add(PingResult.parse(spi.name, result));
|
||||
// [ValueNotifier] only notify when value is changed
|
||||
// But we just add a element to list without changing the list itself
|
||||
// So we need to notify manually
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:computer/computer.dart';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/core/utils/server.dart';
|
||||
import 'package:server_box/data/model/server/private_key_info.dart';
|
||||
@@ -17,17 +18,17 @@ final class PrivateKeyEditPageArgs {
|
||||
const PrivateKeyEditPageArgs({this.pki});
|
||||
}
|
||||
|
||||
class PrivateKeyEditPage extends StatefulWidget {
|
||||
class PrivateKeyEditPage extends ConsumerStatefulWidget {
|
||||
final PrivateKeyEditPageArgs? args;
|
||||
const PrivateKeyEditPage({super.key, this.args});
|
||||
|
||||
@override
|
||||
State<PrivateKeyEditPage> createState() => _PrivateKeyEditPageState();
|
||||
ConsumerState<PrivateKeyEditPage> createState() => _PrivateKeyEditPageState();
|
||||
|
||||
static const route = AppRoute(page: PrivateKeyEditPage.new, path: '/private_key/edit');
|
||||
}
|
||||
|
||||
class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
|
||||
class _PrivateKeyEditPageState extends ConsumerState<PrivateKeyEditPage> {
|
||||
final _nameController = TextEditingController();
|
||||
final _keyController = TextEditingController();
|
||||
final _pwdController = TextEditingController();
|
||||
@@ -39,6 +40,8 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
|
||||
|
||||
final _loading = ValueNotifier<Widget?>(null);
|
||||
|
||||
late final _notifier = ref.read(privateKeyNotifierProvider.notifier);
|
||||
|
||||
PrivateKeyInfo? get pki => widget.args?.pki;
|
||||
|
||||
@override
|
||||
@@ -94,7 +97,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
|
||||
child: Text(libL10n.askContinue('${libL10n.delete} ${l10n.privateKey}(${pki.id})')),
|
||||
actions: Btn.ok(
|
||||
onTap: () {
|
||||
PrivateKeyProvider.delete(pki);
|
||||
_notifier.delete(pki);
|
||||
context.pop();
|
||||
context.pop();
|
||||
},
|
||||
@@ -196,9 +199,9 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
|
||||
final pki = PrivateKeyInfo(id: name, key: decrypted);
|
||||
final originPki = this.pki;
|
||||
if (originPki != null) {
|
||||
PrivateKeyProvider.update(originPki, pki);
|
||||
_notifier.update(originPki, pki);
|
||||
} else {
|
||||
PrivateKeyProvider.add(pki);
|
||||
_notifier.add(pki);
|
||||
}
|
||||
} catch (e) {
|
||||
context.showSnackBar(e.toString());
|
||||
|
||||
@@ -3,22 +3,23 @@ import 'dart:io';
|
||||
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:server_box/core/extension/context/locale.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/res/store.dart';
|
||||
import 'package:server_box/view/page/private_key/edit.dart';
|
||||
|
||||
class PrivateKeysListPage extends StatefulWidget {
|
||||
class PrivateKeysListPage extends ConsumerStatefulWidget {
|
||||
const PrivateKeysListPage({super.key});
|
||||
|
||||
@override
|
||||
State<PrivateKeysListPage> createState() => _PrivateKeyListState();
|
||||
ConsumerState<PrivateKeysListPage> createState() => _PrivateKeyListState();
|
||||
|
||||
static const route = AppRouteNoArg(page: PrivateKeysListPage.new, path: '/private_key');
|
||||
}
|
||||
|
||||
class _PrivateKeyListState extends State<PrivateKeysListPage> with AfterLayoutMixin {
|
||||
class _PrivateKeyListState extends ConsumerState<PrivateKeysListPage> with AfterLayoutMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -31,14 +32,15 @@ class _PrivateKeyListState extends State<PrivateKeysListPage> with AfterLayoutMi
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
return PrivateKeyProvider.pkis.listenVal((pkis) {
|
||||
if (pkis.isEmpty) {
|
||||
return Center(child: Text(libL10n.empty));
|
||||
}
|
||||
final privateKeyState = ref.watch(privateKeyNotifierProvider);
|
||||
final pkis = privateKeyState.keys;
|
||||
|
||||
if (pkis.isEmpty) {
|
||||
return Center(child: Text(libL10n.empty));
|
||||
}
|
||||
|
||||
final children = pkis.map(_buildKeyItem).toList();
|
||||
return AutoMultiList(children: children);
|
||||
});
|
||||
final children = pkis.map(_buildKeyItem).toList();
|
||||
return AutoMultiList(children: children);
|
||||
}
|
||||
|
||||
Widget _buildKeyItem(PrivateKeyInfo item) {
|
||||
|
||||
@@ -3,25 +3,26 @@ import 'dart:async';
|
||||
import 'package:dartssh2/dartssh2.dart';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/core/route.dart';
|
||||
import 'package:server_box/data/model/app/scripts/shell_func.dart';
|
||||
import 'package:server_box/data/model/server/proc.dart';
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
import 'package:server_box/data/provider/server.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
|
||||
class ProcessPage extends StatefulWidget {
|
||||
class ProcessPage extends ConsumerStatefulWidget {
|
||||
final SpiRequiredArgs args;
|
||||
|
||||
const ProcessPage({super.key, required this.args});
|
||||
|
||||
@override
|
||||
State<ProcessPage> createState() => _ProcessPageState();
|
||||
ConsumerState<ProcessPage> createState() => _ProcessPageState();
|
||||
|
||||
static const route = AppRouteArg(page: ProcessPage.new, path: '/process');
|
||||
}
|
||||
|
||||
class _ProcessPageState extends State<ProcessPage> {
|
||||
class _ProcessPageState extends ConsumerState<ProcessPage> {
|
||||
late Timer _timer;
|
||||
late MediaQueryData _media;
|
||||
|
||||
@@ -36,6 +37,8 @@ class _ProcessPageState extends State<ProcessPage> {
|
||||
ProcSortMode _procSortMode = ProcSortMode.cpu;
|
||||
List<ProcSortMode> _sortModes = List.from(ProcSortMode.values);
|
||||
|
||||
late final _provider = individualServerNotifierProvider(widget.args.spi.id);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
@@ -45,7 +48,8 @@ class _ProcessPageState extends State<ProcessPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_client = widget.args.spi.server?.value.client;
|
||||
final serverState = ref.read(_provider);
|
||||
_client = serverState.client;
|
||||
final duration = Duration(seconds: Stores.setting.serverStatusUpdateInterval.fetch());
|
||||
_timer = Timer.periodic(duration, (_) => _refresh());
|
||||
}
|
||||
@@ -58,9 +62,10 @@ class _ProcessPageState extends State<ProcessPage> {
|
||||
|
||||
Future<void> _refresh() async {
|
||||
if (mounted) {
|
||||
final systemType = widget.args.spi.server?.value.status.system;
|
||||
final serverState = ref.read(_provider);
|
||||
final systemType = serverState.status.system;
|
||||
final result = await _client
|
||||
?.run(ShellFunc.process.exec(widget.args.spi.id, systemType: systemType))
|
||||
?.run(ShellFunc.process.exec(widget.args.spi.id, systemType: systemType, customDir: null))
|
||||
.string;
|
||||
if (result == null || result.isEmpty) {
|
||||
context.showSnackBar(libL10n.empty);
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/data/model/server/pve.dart';
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
@@ -15,29 +16,30 @@ final class PvePageArgs {
|
||||
const PvePageArgs({required this.spi});
|
||||
}
|
||||
|
||||
final class PvePage extends StatefulWidget {
|
||||
final class PvePage extends ConsumerStatefulWidget {
|
||||
final PvePageArgs args;
|
||||
|
||||
const PvePage({super.key, required this.args});
|
||||
|
||||
@override
|
||||
State<PvePage> createState() => _PvePageState();
|
||||
ConsumerState<PvePage> createState() => _PvePageState();
|
||||
|
||||
static const route = AppRouteArg<void, PvePageArgs>(page: PvePage.new, path: '/pve');
|
||||
}
|
||||
|
||||
const _kHorziPadding = 11.0;
|
||||
|
||||
final class _PvePageState extends State<PvePage> {
|
||||
late final _pve = PveProvider(spi: widget.args.spi);
|
||||
final class _PvePageState extends ConsumerState<PvePage> {
|
||||
late MediaQueryData _media;
|
||||
Timer? _timer;
|
||||
|
||||
late final _provider = pveNotifierProvider(widget.args.spi);
|
||||
late final _notifier = ref.read(_provider.notifier);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_timer?.cancel();
|
||||
_pve.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -55,43 +57,34 @@ final class _PvePageState extends State<PvePage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final pveState = ref.watch(_provider);
|
||||
|
||||
// If there is an error, stop the timer
|
||||
if (pveState.error != null) {
|
||||
_timer?.cancel();
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: CustomAppBar(
|
||||
title: TwoLineText(up: 'PVE', down: widget.args.spi.name),
|
||||
actions: [
|
||||
ValBuilder(
|
||||
listenable: _pve.err,
|
||||
builder: (val) => val == null
|
||||
? UIs.placeholder
|
||||
: Btn.icon(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onTap: () {
|
||||
_pve.err.value = null;
|
||||
_pve.list();
|
||||
_initRefreshTimer();
|
||||
},
|
||||
),
|
||||
),
|
||||
pveState.error == null
|
||||
? UIs.placeholder
|
||||
: Btn.icon(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onTap: () {
|
||||
_notifier.list();
|
||||
_initRefreshTimer();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ValBuilder(
|
||||
listenable: _pve.err,
|
||||
builder: (val) {
|
||||
if (val != null) {
|
||||
_timer?.cancel();
|
||||
return Padding(
|
||||
body: pveState.error != null
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(13),
|
||||
child: Center(child: Text(val)),
|
||||
);
|
||||
}
|
||||
return ValBuilder(
|
||||
listenable: _pve.data,
|
||||
builder: (val) {
|
||||
return _buildBody(val);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
child: Center(child: Text(pveState.error.toString())),
|
||||
)
|
||||
: _buildBody(pveState.data),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -342,7 +335,7 @@ final class _PvePageState extends State<PvePage> {
|
||||
if (!item.available) {
|
||||
return Btn.icon(
|
||||
icon: const Icon(Icons.play_arrow, color: Colors.grey),
|
||||
onTap: () => _onCtrl(_pve.start, l10n.start, item),
|
||||
onTap: () => _onCtrl(l10n.start, item, () => _notifier.start(item.node, item.id)),
|
||||
);
|
||||
}
|
||||
return Row(
|
||||
@@ -350,17 +343,17 @@ final class _PvePageState extends State<PvePage> {
|
||||
Btn.icon(
|
||||
icon: const Icon(Icons.stop, color: Colors.grey, size: 20),
|
||||
padding: pad,
|
||||
onTap: () => _onCtrl(_pve.stop, l10n.stop, item),
|
||||
onTap: () => _onCtrl(l10n.stop, item, () => _notifier.stop(item.node, item.id)),
|
||||
),
|
||||
Btn.icon(
|
||||
icon: const Icon(Icons.refresh, color: Colors.grey, size: 20),
|
||||
padding: pad,
|
||||
onTap: () => _onCtrl(_pve.reboot, l10n.reboot, item),
|
||||
onTap: () => _onCtrl(l10n.reboot, item, () => _notifier.reboot(item.node, item.id)),
|
||||
),
|
||||
Btn.icon(
|
||||
icon: const Icon(Icons.power_off, color: Colors.grey, size: 20),
|
||||
padding: pad,
|
||||
onTap: () => _onCtrl(_pve.shutdown, l10n.shutdown, item),
|
||||
onTap: () => _onCtrl(l10n.shutdown, item, () => _notifier.shutdown(item.node, item.id)),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -368,7 +361,7 @@ final class _PvePageState extends State<PvePage> {
|
||||
}
|
||||
|
||||
extension on _PvePageState {
|
||||
void _onCtrl(PveCtrlFunc func, String action, PveCtrlIface item) async {
|
||||
void _onCtrl(String action, PveCtrlIface item, Future<bool> Function() func) async {
|
||||
final sure = await context.showRoundDialog<bool>(
|
||||
title: libL10n.attention,
|
||||
child: Text(libL10n.askContinue('$action ${item.id}')),
|
||||
@@ -376,7 +369,7 @@ extension on _PvePageState {
|
||||
);
|
||||
if (sure != true) return;
|
||||
|
||||
final (suc, err) = await context.showLoadingDialog(fn: () => func(item.node, item.id));
|
||||
final (suc, err) = await context.showLoadingDialog(fn: func);
|
||||
if (suc == true) {
|
||||
context.showSnackBar(libL10n.success);
|
||||
} else {
|
||||
@@ -384,28 +377,40 @@ extension on _PvePageState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Add PveNode if [PveProvider.onlyOneNode] is false
|
||||
/// Add PveNode if only one node exists
|
||||
String _wrapNodeName(PveCtrlIface item) {
|
||||
if (_pve.onlyOneNode) {
|
||||
final pveState = ref.read(_provider);
|
||||
if (pveState.data?.onlyOneNode ?? false) {
|
||||
return item.name;
|
||||
}
|
||||
return '${item.node} / ${item.name}';
|
||||
}
|
||||
|
||||
void _initRefreshTimer() {
|
||||
_timer?.cancel();
|
||||
_timer = Timer.periodic(Duration(seconds: Stores.setting.serverStatusUpdateInterval.fetch()), (_) {
|
||||
if (mounted) {
|
||||
_pve.list();
|
||||
_notifier.list();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _afterInit() async {
|
||||
await _pve.connected.future;
|
||||
if (_pve.release != null && _pve.release!.compareTo('8.0') < 0) {
|
||||
if (mounted) {
|
||||
context.showSnackBar(l10n.pveVersionLow);
|
||||
// Wait for the PVE state to be connected
|
||||
while (mounted) {
|
||||
final pveState = ref.read(_provider);
|
||||
if (pveState.isConnected) {
|
||||
if (pveState.release != null && pveState.release!.compareTo('8.0') < 0) {
|
||||
if (mounted) {
|
||||
context.showSnackBar(l10n.pveVersionLow);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (pveState.error != null) {
|
||||
break; // Skip if there is an error
|
||||
}
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@ import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:icons_plus/icons_plus.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/core/extension/server.dart';
|
||||
import 'package:server_box/core/route.dart';
|
||||
import 'package:server_box/data/model/app/scripts/cmd_types.dart';
|
||||
import 'package:server_box/data/model/app/server_detail_card.dart';
|
||||
@@ -16,28 +18,28 @@ import 'package:server_box/data/model/server/disk_smart.dart';
|
||||
import 'package:server_box/data/model/server/net_speed.dart';
|
||||
import 'package:server_box/data/model/server/nvdia.dart';
|
||||
import 'package:server_box/data/model/server/sensors.dart';
|
||||
import 'package:server_box/data/model/server/server.dart';
|
||||
import 'package:server_box/data/model/server/server.dart' as server_model;
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
import 'package:server_box/data/model/server/system.dart';
|
||||
import 'package:server_box/data/provider/server.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
import 'package:server_box/view/page/pve.dart';
|
||||
import 'package:server_box/view/page/server/edit.dart';
|
||||
import 'package:server_box/view/page/server/logo.dart';
|
||||
import 'package:server_box/view/widget/server_func_btns.dart';
|
||||
|
||||
part 'misc.dart';
|
||||
|
||||
class ServerDetailPage extends StatefulWidget {
|
||||
class ServerDetailPage extends ConsumerStatefulWidget {
|
||||
final SpiRequiredArgs args;
|
||||
const ServerDetailPage({super.key, required this.args});
|
||||
|
||||
@override
|
||||
State<ServerDetailPage> createState() => _ServerDetailPageState();
|
||||
ConsumerState<ServerDetailPage> createState() => _ServerDetailPageState();
|
||||
|
||||
static const route = AppRouteArg(page: ServerDetailPage.new, path: '/servers/detail');
|
||||
}
|
||||
|
||||
class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerProviderStateMixin {
|
||||
class _ServerDetailPageState extends ConsumerState<ServerDetailPage> with SingleTickerProviderStateMixin {
|
||||
late final _cardBuildMap = Map.fromIterables(ServerDetailCards.names, [
|
||||
_buildAbout,
|
||||
_buildCPUView,
|
||||
@@ -84,17 +86,17 @@ class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerPr
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = widget.args.spi.server;
|
||||
if (s == null) {
|
||||
final serverState = ref.watch(individualServerNotifierProvider(widget.args.spi.id));
|
||||
if (serverState.client == null) {
|
||||
return Scaffold(
|
||||
appBar: CustomAppBar(),
|
||||
body: Center(child: Text(libL10n.empty)),
|
||||
);
|
||||
}
|
||||
return s.listenVal(_buildMainPage);
|
||||
return _buildMainPage(serverState);
|
||||
}
|
||||
|
||||
Widget _buildMainPage(Server si) {
|
||||
Widget _buildMainPage(ServerState si) {
|
||||
final buildFuncs = !Stores.setting.moveServerFuncs.fetch();
|
||||
final logo = _buildLogo(si);
|
||||
final children = <Widget>[if (logo != null) logo, if (buildFuncs) ServerFuncBtns(spi: si.spi)];
|
||||
@@ -111,7 +113,7 @@ class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerPr
|
||||
);
|
||||
}
|
||||
|
||||
CustomAppBar _buildAppBar(Server si) {
|
||||
CustomAppBar _buildAppBar(ServerState si) {
|
||||
return CustomAppBar(
|
||||
title: Text(
|
||||
si.spi.name,
|
||||
@@ -132,7 +134,7 @@ class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerPr
|
||||
);
|
||||
}
|
||||
|
||||
Widget? _buildLogo(Server si) {
|
||||
Widget? _buildLogo(ServerState si) {
|
||||
final logoUrl = si.getLogoUrl(context);
|
||||
|
||||
return Padding(
|
||||
@@ -153,7 +155,7 @@ class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerPr
|
||||
);
|
||||
}
|
||||
|
||||
Widget? _buildAbout(Server si) {
|
||||
Widget? _buildAbout(ServerState si) {
|
||||
final ss = si.status;
|
||||
return ExpandTile(
|
||||
key: ValueKey(ss.more.hashCode), // Use hashCode to avoid perf issue
|
||||
@@ -178,7 +180,7 @@ class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerPr
|
||||
).cardx;
|
||||
}
|
||||
|
||||
Widget? _buildCPUView(Server si) {
|
||||
Widget? _buildCPUView(ServerState si) {
|
||||
final ss = si.status;
|
||||
final percent = ss.cpu.usedPercent(coreIdx: 0).toInt();
|
||||
final details = [
|
||||
@@ -305,7 +307,7 @@ class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerPr
|
||||
return children;
|
||||
}
|
||||
|
||||
Widget _buildCPUChart(ServerStatus ss) {
|
||||
Widget _buildCPUChart(server_model.ServerStatus ss) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 13),
|
||||
child: LayoutBuilder(
|
||||
@@ -335,7 +337,7 @@ class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerPr
|
||||
);
|
||||
}
|
||||
|
||||
Widget? _buildMemView(Server si) {
|
||||
Widget? _buildMemView(ServerState si) {
|
||||
final ss = si.status;
|
||||
final free = ss.mem.free / ss.mem.total * 100;
|
||||
final avail = ss.mem.availPercent * 100;
|
||||
@@ -376,7 +378,7 @@ class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerPr
|
||||
).cardx;
|
||||
}
|
||||
|
||||
Widget? _buildSwapView(Server si) {
|
||||
Widget? _buildSwapView(ServerState si) {
|
||||
final ss = si.status;
|
||||
if (ss.swap.total == 0) return null;
|
||||
|
||||
@@ -408,7 +410,7 @@ class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerPr
|
||||
).cardx;
|
||||
}
|
||||
|
||||
Widget? _buildGpuView(Server si) {
|
||||
Widget? _buildGpuView(ServerState si) {
|
||||
final ss = si.status;
|
||||
final hasNvidia = ss.nvidia != null && ss.nvidia!.isNotEmpty;
|
||||
final hasAmd = ss.amd != null && ss.amd!.isNotEmpty;
|
||||
@@ -532,7 +534,7 @@ class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerPr
|
||||
);
|
||||
}
|
||||
|
||||
Widget? _buildDiskView(Server si) {
|
||||
Widget? _buildDiskView(ServerState si) {
|
||||
final ss = si.status;
|
||||
final children = <Widget>[];
|
||||
|
||||
@@ -553,7 +555,7 @@ class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerPr
|
||||
).cardx;
|
||||
}
|
||||
|
||||
Widget _buildDiskItemWithHierarchy(Disk disk, ServerStatus ss, int depth) {
|
||||
Widget _buildDiskItemWithHierarchy(Disk disk, server_model.ServerStatus ss, int depth) {
|
||||
// Create a list to hold this disk and its children
|
||||
final items = <Widget>[];
|
||||
|
||||
@@ -570,7 +572,7 @@ class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerPr
|
||||
return Column(children: items);
|
||||
}
|
||||
|
||||
Widget _buildDiskItem(Disk disk, ServerStatus ss, int depth) {
|
||||
Widget _buildDiskItem(Disk disk, server_model.ServerStatus ss, int depth) {
|
||||
final (read, write) = ss.diskIO.getSpeed(disk.path);
|
||||
final text = () {
|
||||
final use = '${l10n.used} ${disk.used.kb2Str} / ${disk.size.kb2Str}';
|
||||
@@ -625,7 +627,7 @@ class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerPr
|
||||
);
|
||||
}
|
||||
|
||||
Widget? _buildDiskSmart(Server si) {
|
||||
Widget? _buildDiskSmart(ServerState si) {
|
||||
final smarts = si.status.diskSmart;
|
||||
if (smarts.isEmpty) return null;
|
||||
return CardX(
|
||||
@@ -770,7 +772,7 @@ class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerPr
|
||||
);
|
||||
}
|
||||
|
||||
Widget? _buildNetView(Server si) {
|
||||
Widget? _buildNetView(ServerState si) {
|
||||
final ss = si.status;
|
||||
final ns = ss.netSpeed;
|
||||
final children = <Widget>[];
|
||||
@@ -847,7 +849,7 @@ class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerPr
|
||||
);
|
||||
}
|
||||
|
||||
Widget? _buildTemperature(Server si) {
|
||||
Widget? _buildTemperature(ServerState si) {
|
||||
final ss = si.status;
|
||||
if (ss.temps.isEmpty) return null;
|
||||
|
||||
@@ -879,7 +881,7 @@ class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerPr
|
||||
);
|
||||
}
|
||||
|
||||
Widget? _buildBatteries(Server si) {
|
||||
Widget? _buildBatteries(ServerState si) {
|
||||
final ss = si.status;
|
||||
if (ss.batteries.isEmpty) return null;
|
||||
|
||||
@@ -914,7 +916,7 @@ class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerPr
|
||||
);
|
||||
}
|
||||
|
||||
Widget? _buildSensors(Server si) {
|
||||
Widget? _buildSensors(ServerState si) {
|
||||
final ss = si.status;
|
||||
if (ss.sensors.isEmpty) return UIs.placeholder;
|
||||
return CardX(
|
||||
@@ -967,7 +969,7 @@ class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerPr
|
||||
);
|
||||
}
|
||||
|
||||
Widget? _buildPve(Server si) {
|
||||
Widget? _buildPve(ServerState si) {
|
||||
final addr = si.spi.custom?.pveAddr;
|
||||
if (addr == null || addr.isEmpty) return null;
|
||||
return CardX(
|
||||
@@ -980,7 +982,7 @@ class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerPr
|
||||
);
|
||||
}
|
||||
|
||||
Widget? _buildCustomCmd(Server si) {
|
||||
Widget? _buildCustomCmd(ServerState si) {
|
||||
final ss = si.status;
|
||||
if (ss.customCmds.isEmpty) return null;
|
||||
return CardX(
|
||||
|
||||
@@ -3,12 +3,12 @@ import 'dart:convert';
|
||||
import 'package:choice/choice.dart';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:icons_plus/icons_plus.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/core/route.dart';
|
||||
import 'package:server_box/data/model/app/scripts/cmd_types.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_private_info.dart';
|
||||
import 'package:server_box/data/model/server/system.dart';
|
||||
import 'package:server_box/data/model/server/wol_cfg.dart';
|
||||
@@ -17,7 +17,7 @@ import 'package:server_box/data/provider/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 ConsumerStatefulWidget {
|
||||
final SpiRequiredArgs? args;
|
||||
|
||||
const ServerEditPage({super.key, this.args});
|
||||
@@ -25,10 +25,10 @@ class ServerEditPage extends StatefulWidget {
|
||||
static const route = AppRoute<bool, SpiRequiredArgs>(page: ServerEditPage.new, path: '/servers/edit');
|
||||
|
||||
@override
|
||||
State<ServerEditPage> createState() => _ServerEditPageState();
|
||||
ConsumerState<ServerEditPage> createState() => _ServerEditPageState();
|
||||
}
|
||||
|
||||
class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||
class _ServerEditPageState extends ConsumerState<ServerEditPage> with AfterLayoutMixin {
|
||||
late final spi = widget.args?.spi;
|
||||
final _nameController = TextEditingController();
|
||||
final _ipController = TextEditingController();
|
||||
@@ -167,7 +167,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||
hint: 'root',
|
||||
suggestion: false,
|
||||
),
|
||||
TagTile(tags: _tags, allTags: ServerProvider.tags.value).cardx,
|
||||
TagTile(tags: _tags, allTags: ref.watch(serverNotifierProvider).tags).cardx,
|
||||
ListTile(
|
||||
title: Text(l10n.autoConnect),
|
||||
trailing: _autoConnect.listenVal(
|
||||
@@ -227,12 +227,14 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||
}
|
||||
|
||||
Widget _buildKeyAuth() {
|
||||
return PrivateKeyProvider.pkis.listenVal((pkis) {
|
||||
final tiles = List<Widget>.generate(pkis.length, (index) {
|
||||
final e = pkis[index];
|
||||
return ListTile(
|
||||
contentPadding: const EdgeInsets.only(left: 10, right: 15),
|
||||
leading: Radio<int>(value: index),
|
||||
final privateKeyState = ref.watch(privateKeyNotifierProvider);
|
||||
final pkis = privateKeyState.keys;
|
||||
|
||||
final tiles = List<Widget>.generate(pkis.length, (index) {
|
||||
final e = pkis[index];
|
||||
return ListTile(
|
||||
contentPadding: const EdgeInsets.only(left: 10, right: 15),
|
||||
leading: Radio<int>(value: index),
|
||||
title: Text(e.id, textAlign: TextAlign.start),
|
||||
subtitle: Text(e.type ?? l10n.unknown, textAlign: TextAlign.start, style: UIs.textGrey),
|
||||
trailing: Btn.icon(
|
||||
@@ -254,7 +256,6 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||
onChanged: (val) => _keyIdx.value = val,
|
||||
child: _keyIdx.listenVal((_) => Column(children: tiles)).cardx,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildEnvs() {
|
||||
@@ -485,27 +486,26 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||
|
||||
Widget _buildJumpServer() {
|
||||
const padding = EdgeInsets.only(left: 13, right: 13, bottom: 7);
|
||||
final srvs = ServerProvider.servers.values
|
||||
.map((e) => e.value)
|
||||
.where((e) => e.spi.jumpId == null)
|
||||
.where((e) => e.spi.id != spi?.id)
|
||||
final srvs = ref.watch(serverNotifierProvider).servers.values
|
||||
.where((e) => e.jumpId == null)
|
||||
.where((e) => e.id != spi?.id)
|
||||
.toList();
|
||||
final choice = _jumpServer.listenVal((val) {
|
||||
final srv = srvs.firstWhereOrNull((e) => e.id == _jumpServer.value);
|
||||
return Choice<Server>(
|
||||
return Choice<Spi>(
|
||||
multiple: false,
|
||||
clearable: true,
|
||||
value: srv != null ? [srv] : [],
|
||||
builder: (state, _) => Wrap(
|
||||
children: List<Widget>.generate(srvs.length, (index) {
|
||||
final item = srvs[index];
|
||||
return ChoiceChipX<Server>(
|
||||
label: item.spi.name,
|
||||
return ChoiceChipX<Spi>(
|
||||
label: item.name,
|
||||
state: state,
|
||||
value: item,
|
||||
onSelected: (srv, on) {
|
||||
if (on) {
|
||||
_jumpServer.value = srv.spi.id;
|
||||
_jumpServer.value = srv.id;
|
||||
} else {
|
||||
_jumpServer.value = null;
|
||||
}
|
||||
@@ -569,7 +569,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||
actions: Btn.ok(
|
||||
onTap: () async {
|
||||
context.pop();
|
||||
ServerProvider.delServer(spi!.id);
|
||||
ref.read(serverNotifierProvider.notifier).delServer(spi!.id);
|
||||
context.pop(true);
|
||||
},
|
||||
red: true,
|
||||
@@ -705,7 +705,7 @@ extension on _ServerEditPageState {
|
||||
port: int.parse(_portController.text),
|
||||
user: _usernameController.text,
|
||||
pwd: _passwordController.text.selfNotEmptyOrNull,
|
||||
keyId: _keyIdx.value != null ? PrivateKeyProvider.pkis.value.elementAt(_keyIdx.value!).id : null,
|
||||
keyId: _keyIdx.value != null ? ref.read(privateKeyNotifierProvider).keys.elementAt(_keyIdx.value!).id : null,
|
||||
tags: _tags.value.isEmpty ? null : _tags.value.toList(),
|
||||
alterUrl: _altUrlController.text.selfNotEmptyOrNull,
|
||||
autoConnect: _autoConnect.value,
|
||||
@@ -724,9 +724,9 @@ extension on _ServerEditPageState {
|
||||
context.showSnackBar('${l10n.sameIdServerExist}: ${spi.id}');
|
||||
return;
|
||||
}
|
||||
ServerProvider.addServer(spi);
|
||||
ref.read(serverNotifierProvider.notifier).addServer(spi);
|
||||
} else {
|
||||
ServerProvider.updateServer(this.spi!, spi);
|
||||
ref.read(serverNotifierProvider.notifier).updateServer(this.spi!, spi);
|
||||
}
|
||||
|
||||
context.pop();
|
||||
@@ -740,7 +740,7 @@ extension on _ServerEditPageState {
|
||||
if (spi.keyId == null) {
|
||||
_passwordController.text = spi.pwd ?? '';
|
||||
} else {
|
||||
_keyIdx.value = PrivateKeyProvider.pkis.value.indexWhere((e) => e.id == spi.keyId);
|
||||
_keyIdx.value = ref.read(privateKeyNotifierProvider).keys.indexWhere((e) => e.id == spi.keyId);
|
||||
}
|
||||
|
||||
/// List in dart is passed by pointer, so you need to copy it here
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:server_box/data/model/app/scripts/cmd_types.dart';
|
||||
import 'package:server_box/data/model/server/dist.dart';
|
||||
import 'package:server_box/data/model/server/server.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
|
||||
extension LogoExt on Server {
|
||||
String? getLogoUrl(BuildContext context) {
|
||||
var logoUrl = spi.custom?.logoUrl ?? Stores.setting.serverLogoUrl.fetch().selfNotEmptyOrNull;
|
||||
if (logoUrl == null) {
|
||||
return null;
|
||||
}
|
||||
final dist = status.more[StatusCmdType.sys]?.dist;
|
||||
if (dist != null) {
|
||||
logoUrl = logoUrl.replaceFirst('{DIST}', dist.name);
|
||||
}
|
||||
logoUrl = logoUrl.replaceFirst('{BRIGHT}', context.isDark ? 'dark' : 'light');
|
||||
return logoUrl;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
part of 'tab.dart';
|
||||
|
||||
extension on _ServerPageState {
|
||||
Widget _buildServerCardTitle(Server s) {
|
||||
Widget _buildServerCardTitle(ServerState s) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 7, right: 13),
|
||||
child: Row(
|
||||
@@ -17,12 +17,12 @@ extension on _ServerPageState {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTopRightWidget(Server s) {
|
||||
Widget _buildTopRightWidget(ServerState s) {
|
||||
final (child, onTap) = switch (s.conn) {
|
||||
ServerConn.connecting || ServerConn.loading || ServerConn.connected => (
|
||||
SizedBox.square(
|
||||
dimension: _ServerPageState._kCardHeightMin,
|
||||
child: SizedLoading(_ServerPageState._kCardHeightMin, strokeWidth: 3, padding: 3),
|
||||
child: SizedLoading(_ServerPageState._kCardHeightMin, padding: 3),
|
||||
),
|
||||
null,
|
||||
),
|
||||
@@ -30,16 +30,16 @@ extension on _ServerPageState {
|
||||
const Icon(Icons.refresh, size: 21, color: Colors.grey),
|
||||
() {
|
||||
TryLimiter.reset(s.spi.id);
|
||||
ServerProvider.refresh(spi: s.spi);
|
||||
ref.read(serverNotifierProvider.notifier).refresh(spi: s.spi);
|
||||
},
|
||||
),
|
||||
ServerConn.disconnected => (
|
||||
const Icon(MingCute.link_3_line, size: 19, color: Colors.grey),
|
||||
() => ServerProvider.refresh(spi: s.spi),
|
||||
() => ref.read(serverNotifierProvider.notifier).refresh(spi: s.spi),
|
||||
),
|
||||
ServerConn.finished => (
|
||||
const Icon(MingCute.unlink_2_line, size: 17, color: Colors.grey),
|
||||
() => ServerProvider.closeServer(id: s.spi.id),
|
||||
() => ref.read(serverNotifierProvider.notifier).closeServer(id: s.spi.id),
|
||||
),
|
||||
};
|
||||
|
||||
@@ -51,7 +51,7 @@ extension on _ServerPageState {
|
||||
return InkWell(borderRadius: BorderRadius.circular(7), onTap: onTap, child: wrapped).paddingOnly(left: 5);
|
||||
}
|
||||
|
||||
Widget _buildTopRightText(Server s) {
|
||||
Widget _buildTopRightText(ServerState s) {
|
||||
final hasErr = s.status.err != null;
|
||||
final str = s._getTopRightStr(s.spi);
|
||||
if (str == null) return UIs.placeholder;
|
||||
@@ -106,7 +106,7 @@ ${ss.err?.message ?? 'null'}
|
||||
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 device = ref.watch(serverNotifierProvider).servers[id]?.custom?.netDev;
|
||||
final (a, b) = type.build(ss, dev: device);
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 377),
|
||||
|
||||
@@ -26,33 +26,31 @@ extension on _ServerPageState {
|
||||
}
|
||||
|
||||
Widget _buildLandscapeBody() {
|
||||
return ServerProvider.serverOrder.listenVal((order) {
|
||||
if (order.isEmpty) {
|
||||
return Center(child: Text(libL10n.empty, textAlign: TextAlign.center));
|
||||
}
|
||||
final serverState = ref.watch(serverNotifierProvider);
|
||||
final order = serverState.serverOrder;
|
||||
|
||||
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 PageView.builder(
|
||||
itemCount: order.length,
|
||||
itemBuilder: (_, idx) {
|
||||
final id = order[idx];
|
||||
final srv = ref.watch(individualServerNotifierProvider(id));
|
||||
|
||||
return srv.listenVal((srv) {
|
||||
final title = _buildServerCardTitle(srv);
|
||||
final List<Widget> children = [title, _buildNormalCard(srv.status, srv.spi)];
|
||||
final title = _buildServerCardTitle(srv);
|
||||
final List<Widget> children = [title, _buildNormalCard(srv.status, srv.spi)];
|
||||
|
||||
return _getCardNoti(id).listenVal((_) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: children,
|
||||
);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
return _getCardNoti(id).listenVal((_) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: children,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'dart:math' as math;
|
||||
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:icons_plus/icons_plus.dart';
|
||||
import 'package:responsive_framework/responsive_framework.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
@@ -31,11 +32,11 @@ part 'landscape.dart';
|
||||
part 'top_bar.dart';
|
||||
part 'utils.dart';
|
||||
|
||||
class ServerPage extends StatefulWidget {
|
||||
class ServerPage extends ConsumerStatefulWidget {
|
||||
const ServerPage({super.key});
|
||||
|
||||
@override
|
||||
State<ServerPage> createState() => _ServerPageState();
|
||||
ConsumerState<ServerPage> createState() => _ServerPageState();
|
||||
|
||||
static const route = AppRouteNoArg(page: ServerPage.new, path: '/servers');
|
||||
}
|
||||
@@ -43,12 +44,14 @@ class ServerPage extends StatefulWidget {
|
||||
const _cardPad = 74.0;
|
||||
const _cardPadSingle = 13.0;
|
||||
|
||||
class _ServerPageState extends State<ServerPage> with AutomaticKeepAliveClientMixin, AfterLayoutMixin {
|
||||
class _ServerPageState extends ConsumerState<ServerPage>
|
||||
with AutomaticKeepAliveClientMixin, AfterLayoutMixin {
|
||||
late double _textFactorDouble;
|
||||
double _offset = 1;
|
||||
late TextScaler _textFactor;
|
||||
|
||||
final _cardsStatus = <String, _CardNotifier>{};
|
||||
late final ValueNotifier<Set<String>> _tags;
|
||||
|
||||
Timer? _timer;
|
||||
|
||||
@@ -64,11 +67,13 @@ class _ServerPageState extends State<ServerPage> with AutomaticKeepAliveClientMi
|
||||
_scrollController.dispose();
|
||||
_autoHideCtrl.dispose();
|
||||
_tag.dispose();
|
||||
_tags.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tags = ValueNotifier(ref.read(serverNotifierProvider).tags);
|
||||
_startAvoidJitterTimer();
|
||||
}
|
||||
|
||||
@@ -78,9 +83,14 @@ class _ServerPageState extends State<ServerPage> with AutomaticKeepAliveClientMi
|
||||
_updateOffset();
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
// Listen to provider changes and update the ValueNotifier
|
||||
ref.listen(serverNotifierProvider, (previous, next) {
|
||||
_tags.value = next.tags;
|
||||
});
|
||||
return OrientationBuilder(
|
||||
builder: (_, orientation) {
|
||||
if (orientation == Orientation.landscape) {
|
||||
@@ -96,7 +106,7 @@ class _ServerPageState extends State<ServerPage> with AutomaticKeepAliveClientMi
|
||||
|
||||
Widget _buildScaffold(Widget child) {
|
||||
return Scaffold(
|
||||
appBar: _TopBar(tags: ServerProvider.tags, onTagChanged: (p0) => _tag.value = p0, initTag: _tag.value),
|
||||
appBar: _TopBar(tags: _tags, onTagChanged: (p0) => _tag.value = p0, initTag: _tag.value),
|
||||
body: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: _autoHideCtrl.show,
|
||||
@@ -122,22 +132,21 @@ class _ServerPageState extends State<ServerPage> with AutomaticKeepAliveClientMi
|
||||
|
||||
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;
|
||||
// }
|
||||
final serverState = ref.watch(serverNotifierProvider);
|
||||
return _tag.listenVal((val) {
|
||||
final filtered = _filterServers(serverState.serverOrder);
|
||||
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,
|
||||
// );
|
||||
});
|
||||
// return SplitView(
|
||||
// controller: _splitViewCtrl,
|
||||
// leftWeight: 1,
|
||||
// rightWeight: 1.3,
|
||||
// initialRight: Center(child: CircularProgressIndicator()),
|
||||
// leftBuilder: (_, __) => child,
|
||||
// );
|
||||
});
|
||||
}
|
||||
|
||||
@@ -173,10 +182,9 @@ class _ServerPageState extends State<ServerPage> with AutomaticKeepAliveClientMi
|
||||
// Last item is just spacing
|
||||
if (index == lens) return SizedBox(height: 77);
|
||||
|
||||
final vnode = ServerProvider.pick(id: serversInThisColumn[index]);
|
||||
if (vnode == null) return UIs.placeholder;
|
||||
final individualState = ref.watch(individualServerNotifierProvider(serversInThisColumn[index]));
|
||||
|
||||
return vnode.listenVal(_buildEachServerCard);
|
||||
return _buildEachServerCard(individualState);
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -186,9 +194,7 @@ class _ServerPageState extends State<ServerPage> with AutomaticKeepAliveClientMi
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEachServerCard(Server? srv) {
|
||||
if (srv == null) return UIs.placeholder;
|
||||
|
||||
Widget _buildEachServerCard(ServerState srv) {
|
||||
return CardX(
|
||||
key: Key(srv.spi.id + _tag.value),
|
||||
child: InkWell(
|
||||
@@ -218,7 +224,7 @@ class _ServerPageState extends State<ServerPage> with AutomaticKeepAliveClientMi
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRealServerCard(Server srv) {
|
||||
Widget _buildRealServerCard(ServerState srv) {
|
||||
final id = srv.spi.id;
|
||||
final cardStatus = _getCardNoti(id);
|
||||
final title = _buildServerCardTitle(srv);
|
||||
@@ -255,7 +261,7 @@ class _ServerPageState extends State<ServerPage> with AutomaticKeepAliveClientMi
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildFlippedCard(Server srv) {
|
||||
Widget _buildFlippedCard(ServerState srv) {
|
||||
const color = Colors.grey;
|
||||
const textStyle = TextStyle(fontSize: 13, color: color);
|
||||
final children = [
|
||||
@@ -332,8 +338,8 @@ class _ServerPageState extends State<ServerPage> with AutomaticKeepAliveClientMi
|
||||
|
||||
@override
|
||||
Future<void> afterFirstLayout(BuildContext context) async {
|
||||
ServerProvider.refresh();
|
||||
ServerProvider.startAutoRefresh();
|
||||
ref.read(serverNotifierProvider.notifier).refresh();
|
||||
ref.read(serverNotifierProvider.notifier).startAutoRefresh();
|
||||
}
|
||||
|
||||
static const _kCardHeightMin = 23.0;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
part of 'tab.dart';
|
||||
|
||||
extension _Actions on _ServerPageState {
|
||||
void _onTapCard(Server srv) {
|
||||
void _onTapCard(ServerState srv) {
|
||||
if (srv.canViewDetails) {
|
||||
// _splitViewCtrl.replace(ServerDetailPage(
|
||||
// key: ValueKey(srv.spi.id),
|
||||
@@ -19,7 +19,7 @@ extension _Actions on _ServerPageState {
|
||||
}
|
||||
}
|
||||
|
||||
void _onLongPressCard(Server srv) {
|
||||
void _onLongPressCard(ServerState srv) {
|
||||
if (srv.conn == ServerConn.finished) {
|
||||
final id = srv.spi.id;
|
||||
final cardStatus = _getCardNoti(id);
|
||||
@@ -42,7 +42,7 @@ extension _Actions on _ServerPageState {
|
||||
}
|
||||
|
||||
extension _Operation on _ServerPageState {
|
||||
void _onTapSuspend(Server srv) {
|
||||
void _onTapSuspend(ServerState srv) {
|
||||
_askFor(
|
||||
func: () async {
|
||||
if (Stores.setting.showSuspendTip.fetch()) {
|
||||
@@ -50,7 +50,7 @@ extension _Operation on _ServerPageState {
|
||||
Stores.setting.showSuspendTip.put(false);
|
||||
}
|
||||
srv.client?.execWithPwd(
|
||||
ShellFunc.suspend.exec(srv.spi.id, systemType: srv.status.system),
|
||||
ShellFunc.suspend.exec(srv.spi.id, systemType: srv.status.system, customDir: null),
|
||||
context: context,
|
||||
id: srv.id,
|
||||
);
|
||||
@@ -60,10 +60,10 @@ extension _Operation on _ServerPageState {
|
||||
);
|
||||
}
|
||||
|
||||
void _onTapShutdown(Server srv) {
|
||||
void _onTapShutdown(ServerState srv) {
|
||||
_askFor(
|
||||
func: () => srv.client?.execWithPwd(
|
||||
ShellFunc.shutdown.exec(srv.spi.id, systemType: srv.status.system),
|
||||
ShellFunc.shutdown.exec(srv.spi.id, systemType: srv.status.system, customDir: null),
|
||||
context: context,
|
||||
id: srv.id,
|
||||
),
|
||||
@@ -72,10 +72,10 @@ extension _Operation on _ServerPageState {
|
||||
);
|
||||
}
|
||||
|
||||
void _onTapReboot(Server srv) {
|
||||
void _onTapReboot(ServerState srv) {
|
||||
_askFor(
|
||||
func: () => srv.client?.execWithPwd(
|
||||
ShellFunc.reboot.exec(srv.spi.id, systemType: srv.status.system),
|
||||
ShellFunc.reboot.exec(srv.spi.id, systemType: srv.status.system, customDir: null),
|
||||
context: context,
|
||||
id: srv.id,
|
||||
),
|
||||
@@ -84,7 +84,7 @@ extension _Operation on _ServerPageState {
|
||||
);
|
||||
}
|
||||
|
||||
void _onTapEdit(Server srv) {
|
||||
void _onTapEdit(ServerState srv) {
|
||||
if (srv.canViewDetails) {
|
||||
ServerDetailPage.route.go(context, SpiRequiredArgs(srv.spi));
|
||||
} else {
|
||||
@@ -98,7 +98,7 @@ extension _Utils on _ServerPageState {
|
||||
final tag = _tag.value;
|
||||
if (tag == TagSwitcher.kDefaultTag) return order;
|
||||
return order.where((e) {
|
||||
final tags = ServerProvider.pick(id: e)?.value.spi.tags;
|
||||
final tags = ref.read(serverNotifierProvider).servers[e]?.tags;
|
||||
if (tags == null) return false;
|
||||
return tags.contains(tag);
|
||||
}).toList();
|
||||
@@ -160,7 +160,7 @@ extension _Utils on _ServerPageState {
|
||||
}
|
||||
}
|
||||
|
||||
extension _ServerX on Server {
|
||||
extension _ServerX on ServerState {
|
||||
String? _getTopRightStr(Spi spi) {
|
||||
if (status.err != null) {
|
||||
return l10n.viewErr;
|
||||
|
||||
@@ -46,7 +46,7 @@ extension _Server on _AppSettingsPageState {
|
||||
onTap: () async {
|
||||
final keys = Stores.server.keys();
|
||||
final names = Map.fromEntries(
|
||||
keys.map((e) => MapEntry(e, ServerProvider.pick(id: e)?.value.spi.name ?? e)),
|
||||
keys.map((e) => MapEntry(e, ref.read(serverNotifierProvider).servers[e]?.name ?? e)),
|
||||
);
|
||||
final deleteKeys = await context.showPickDialog<String>(
|
||||
clearable: true,
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'dart:io';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_highlight/theme_map.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:icons_plus/icons_plus.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/data/model/app/net_view.dart';
|
||||
@@ -35,16 +36,16 @@ part 'entries/ssh.dart';
|
||||
|
||||
const _kIconSize = 23.0;
|
||||
|
||||
class SettingsPage extends StatefulWidget {
|
||||
class SettingsPage extends ConsumerStatefulWidget {
|
||||
const SettingsPage({super.key});
|
||||
|
||||
static const route = AppRouteNoArg(page: SettingsPage.new, path: '/settings');
|
||||
|
||||
@override
|
||||
State<SettingsPage> createState() => _SettingsPageState();
|
||||
ConsumerState<SettingsPage> createState() => _SettingsPageState();
|
||||
}
|
||||
|
||||
class _SettingsPageState extends State<SettingsPage> with SingleTickerProviderStateMixin {
|
||||
class _SettingsPageState extends ConsumerState<SettingsPage> with SingleTickerProviderStateMixin {
|
||||
late final _tabCtrl = TabController(length: SettingsTabs.values.length, vsync: this);
|
||||
|
||||
@override
|
||||
@@ -98,14 +99,14 @@ class _SettingsPageState extends State<SettingsPage> with SingleTickerProviderSt
|
||||
}
|
||||
}
|
||||
|
||||
final class AppSettingsPage extends StatefulWidget {
|
||||
final class AppSettingsPage extends ConsumerStatefulWidget {
|
||||
const AppSettingsPage({super.key});
|
||||
|
||||
@override
|
||||
State<AppSettingsPage> createState() => _AppSettingsPageState();
|
||||
ConsumerState<AppSettingsPage> createState() => _AppSettingsPageState();
|
||||
}
|
||||
|
||||
final class _AppSettingsPageState extends State<AppSettingsPage> {
|
||||
final class _AppSettingsPageState extends ConsumerState<AppSettingsPage> {
|
||||
final _setting = Stores.setting;
|
||||
|
||||
late final _sshOpacityCtrl = TextEditingController(text: _setting.sshBgOpacity.fetch().toString());
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
import 'dart:ui';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
import 'package:server_box/data/provider/server.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
|
||||
class ServerOrderPage extends StatefulWidget {
|
||||
class ServerOrderPage extends ConsumerStatefulWidget {
|
||||
const ServerOrderPage({super.key});
|
||||
|
||||
@override
|
||||
State<ServerOrderPage> createState() => _ServerOrderPageState();
|
||||
ConsumerState<ServerOrderPage> createState() => _ServerOrderPageState();
|
||||
|
||||
static const route = AppRouteNoArg(page: ServerOrderPage.new, path: '/settings/order/server');
|
||||
}
|
||||
|
||||
class _ServerOrderPageState extends State<ServerOrderPage> {
|
||||
class _ServerOrderPageState extends ConsumerState<ServerOrderPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -41,25 +42,27 @@ class _ServerOrderPageState extends State<ServerOrderPage> {
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
final orders = ServerProvider.serverOrder;
|
||||
return orders.listenVal((order) {
|
||||
if (order.isEmpty) {
|
||||
return Center(child: Text(libL10n.empty));
|
||||
}
|
||||
return ReorderableListView.builder(
|
||||
footer: const SizedBox(height: 77),
|
||||
onReorder: (oldIndex, newIndex) {
|
||||
setState(() {
|
||||
orders.value.move(oldIndex, newIndex, property: Stores.setting.serverOrder);
|
||||
});
|
||||
},
|
||||
padding: const EdgeInsets.all(8),
|
||||
buildDefaultDragHandles: false,
|
||||
itemBuilder: (_, idx) => _buildItem(idx, order[idx]),
|
||||
itemCount: order.length,
|
||||
proxyDecorator: _proxyDecorator,
|
||||
);
|
||||
});
|
||||
final serverState = ref.watch(serverNotifierProvider);
|
||||
final order = serverState.serverOrder;
|
||||
|
||||
if (order.isEmpty) {
|
||||
return Center(child: Text(libL10n.empty));
|
||||
}
|
||||
return ReorderableListView.builder(
|
||||
footer: const SizedBox(height: 77),
|
||||
onReorder: (oldIndex, newIndex) {
|
||||
setState(() {
|
||||
final newOrder = List<String>.from(order);
|
||||
newOrder.move(oldIndex, newIndex);
|
||||
Stores.setting.serverOrder.put(newOrder);
|
||||
});
|
||||
},
|
||||
padding: const EdgeInsets.all(8),
|
||||
buildDefaultDragHandles: false,
|
||||
itemBuilder: (_, idx) => _buildItem(idx, order[idx]),
|
||||
itemCount: order.length,
|
||||
proxyDecorator: _proxyDecorator,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildItem(int index, String id) {
|
||||
@@ -74,8 +77,10 @@ class _ServerOrderPageState extends State<ServerOrderPage> {
|
||||
}
|
||||
|
||||
Widget _buildCardTile(int index) {
|
||||
final id = ServerProvider.serverOrder.value[index];
|
||||
final spi = ServerProvider.pick(id: id)?.value.spi;
|
||||
final serverState = ref.watch(serverNotifierProvider);
|
||||
final order = serverState.serverOrder;
|
||||
final id = order[index];
|
||||
final spi = serverState.servers[id];
|
||||
if (spi == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/data/model/server/snippet.dart';
|
||||
import 'package:server_box/data/provider/server.dart';
|
||||
@@ -11,18 +12,18 @@ final class SnippetEditPageArgs {
|
||||
const SnippetEditPageArgs({this.snippet});
|
||||
}
|
||||
|
||||
class SnippetEditPage extends StatefulWidget {
|
||||
class SnippetEditPage extends ConsumerStatefulWidget {
|
||||
final SnippetEditPageArgs? args;
|
||||
|
||||
const SnippetEditPage({super.key, this.args});
|
||||
|
||||
@override
|
||||
State<SnippetEditPage> createState() => _SnippetEditPageState();
|
||||
ConsumerState<SnippetEditPage> createState() => _SnippetEditPageState();
|
||||
|
||||
static const route = AppRoute(page: SnippetEditPage.new, path: '/snippets/edit');
|
||||
}
|
||||
|
||||
class _SnippetEditPageState extends State<SnippetEditPage> with AfterLayoutMixin {
|
||||
class _SnippetEditPageState extends ConsumerState<SnippetEditPage> with AfterLayoutMixin {
|
||||
final _nameController = TextEditingController();
|
||||
final _scriptController = TextEditingController();
|
||||
final _noteController = TextEditingController();
|
||||
@@ -61,7 +62,7 @@ class _SnippetEditPageState extends State<SnippetEditPage> with AfterLayoutMixin
|
||||
child: Text(libL10n.askContinue('${libL10n.delete} ${l10n.snippet}(${snippet.name})')),
|
||||
actions: Btn.ok(
|
||||
onTap: () {
|
||||
SnippetProvider.del(snippet);
|
||||
ref.read(snippetNotifierProvider.notifier).del(snippet);
|
||||
context.pop();
|
||||
context.pop();
|
||||
},
|
||||
@@ -95,10 +96,11 @@ class _SnippetEditPageState extends State<SnippetEditPage> with AfterLayoutMixin
|
||||
autoRunOn: _autoRunOn.value.isEmpty ? null : _autoRunOn.value,
|
||||
);
|
||||
final oldSnippet = widget.args?.snippet;
|
||||
final notifier = ref.read(snippetNotifierProvider.notifier);
|
||||
if (oldSnippet != null) {
|
||||
SnippetProvider.update(oldSnippet, snippet);
|
||||
notifier.update(oldSnippet, snippet);
|
||||
} else {
|
||||
SnippetProvider.add(snippet);
|
||||
notifier.add(snippet);
|
||||
}
|
||||
context.pop();
|
||||
},
|
||||
@@ -126,7 +128,12 @@ class _SnippetEditPageState extends State<SnippetEditPage> with AfterLayoutMixin
|
||||
icon: Icons.note,
|
||||
suggestion: true,
|
||||
),
|
||||
TagTile(tags: _tags, allTags: SnippetProvider.tags.value).cardx,
|
||||
Consumer(
|
||||
builder: (_, ref, _) {
|
||||
final tags = ref.watch(snippetNotifierProvider.select((p) => p.tags));
|
||||
return TagTile(tags: _tags, allTags: tags).cardx;
|
||||
},
|
||||
),
|
||||
Input(
|
||||
controller: _scriptController,
|
||||
node: _scriptNode,
|
||||
@@ -150,7 +157,7 @@ class _SnippetEditPageState extends State<SnippetEditPage> with AfterLayoutMixin
|
||||
builder: (vals) {
|
||||
final subtitle = vals.isEmpty
|
||||
? null
|
||||
: vals.map((e) => ServerProvider.pick(id: e)?.value.spi.name ?? e).join(', ');
|
||||
: vals.map((e) => ref.read(serverNotifierProvider).servers[e]?.name ?? e).join(', ');
|
||||
return ListTile(
|
||||
leading: const Padding(
|
||||
padding: EdgeInsets.only(left: 5),
|
||||
@@ -162,11 +169,11 @@ class _SnippetEditPageState extends State<SnippetEditPage> with AfterLayoutMixin
|
||||
? null
|
||||
: Text(subtitle, maxLines: 1, style: UIs.textGrey, overflow: TextOverflow.ellipsis),
|
||||
onTap: () async {
|
||||
vals.removeWhere((e) => !ServerProvider.serverOrder.value.contains(e));
|
||||
vals.removeWhere((e) => !ref.read(serverNotifierProvider).serverOrder.contains(e));
|
||||
final serverIds = await context.showPickDialog(
|
||||
title: l10n.autoRun,
|
||||
items: ServerProvider.serverOrder.value,
|
||||
display: (e) => ServerProvider.pick(id: e)?.value.spi.name ?? e,
|
||||
items: ref.read(serverNotifierProvider).serverOrder,
|
||||
display: (e) => ref.read(serverNotifierProvider).servers[e]?.name ?? e,
|
||||
initial: vals,
|
||||
clearable: true,
|
||||
);
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:server_box/data/model/server/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 ConsumerStatefulWidget {
|
||||
const SnippetListPage({super.key});
|
||||
|
||||
@override
|
||||
State<SnippetListPage> createState() => _SnippetListPageState();
|
||||
ConsumerState<SnippetListPage> createState() => _SnippetListPageState();
|
||||
|
||||
static const route = AppRouteNoArg(page: SnippetListPage.new, path: '/snippets');
|
||||
}
|
||||
|
||||
class _SnippetListPageState extends State<SnippetListPage> with AutomaticKeepAliveClientMixin {
|
||||
class _SnippetListPageState extends ConsumerState<SnippetListPage> with AutomaticKeepAliveClientMixin {
|
||||
final _tag = ''.vn;
|
||||
final _splitViewCtrl = SplitViewController();
|
||||
|
||||
@@ -35,12 +36,14 @@ class _SnippetListPageState extends State<SnippetListPage> with AutomaticKeepAli
|
||||
|
||||
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;
|
||||
// }
|
||||
final snippetState = ref.watch(snippetNotifierProvider);
|
||||
final snippets = snippetState.snippets;
|
||||
|
||||
return _tag.listenVal((tag) {
|
||||
final child = _buildScaffold(snippets, tag);
|
||||
// if (isMobile) {
|
||||
return child;
|
||||
// }
|
||||
|
||||
// return SplitView(
|
||||
// controller: _splitViewCtrl,
|
||||
@@ -49,14 +52,14 @@ class _SnippetListPageState extends State<SnippetListPage> with AutomaticKeepAli
|
||||
// initialRight: Center(child: Text(libL10n.empty)),
|
||||
// leftBuilder: (_, __) => child,
|
||||
// );
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildScaffold(List<Snippet> snippets, String tag) {
|
||||
final snippetState = ref.watch(snippetNotifierProvider);
|
||||
return Scaffold(
|
||||
appBar: TagSwitcher(
|
||||
tags: SnippetProvider.tags,
|
||||
tags: snippetState.tags.vn,
|
||||
onTagChanged: (tag) => _tag.value = tag,
|
||||
initTag: _tag.value,
|
||||
),
|
||||
|
||||
@@ -66,7 +66,8 @@ extension _Init on SSHPageState {
|
||||
// Mark status connected for notifications / live activities
|
||||
TermSessionManager.updateStatus(_sessionId, TermSessionStatus.connected);
|
||||
|
||||
for (final snippet in SnippetProvider.snippets.value) {
|
||||
final snippets = ref.read(snippetNotifierProvider.select((p) => p.snippets));
|
||||
for (final snippet in snippets) {
|
||||
if (snippet.autoRunOn?.contains(widget.args.spi.id) == true) {
|
||||
snippet.runInTerm(_terminal, widget.args.spi);
|
||||
}
|
||||
|
||||
@@ -7,9 +7,8 @@ import 'package:dartssh2/dartssh2.dart';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:icons_plus/icons_plus.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:server_box/core/chan.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/core/utils/server.dart';
|
||||
@@ -17,6 +16,7 @@ import 'package:server_box/core/utils/ssh_auth.dart';
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
import 'package:server_box/data/model/server/snippet.dart';
|
||||
import 'package:server_box/data/model/ssh/virtual_key.dart';
|
||||
import 'package:server_box/data/provider/server.dart';
|
||||
import 'package:server_box/data/provider/snippet.dart';
|
||||
import 'package:server_box/data/provider/virtual_keyboard.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
@@ -52,23 +52,22 @@ final class SshPageArgs {
|
||||
});
|
||||
}
|
||||
|
||||
class SSHPage extends StatefulWidget {
|
||||
class SSHPage extends ConsumerStatefulWidget {
|
||||
final SshPageArgs args;
|
||||
|
||||
const SSHPage({super.key, required this.args});
|
||||
|
||||
@override
|
||||
State<SSHPage> createState() => SSHPageState();
|
||||
ConsumerState<SSHPage> createState() => SSHPageState();
|
||||
|
||||
static const route = AppRouteArg<void, SshPageArgs>(page: SSHPage.new, path: '/ssh/page');
|
||||
}
|
||||
|
||||
const _horizonPadding = 7.0;
|
||||
|
||||
class SSHPageState extends State<SSHPage>
|
||||
class SSHPageState extends ConsumerState<SSHPage>
|
||||
with AutomaticKeepAliveClientMixin, AfterLayoutMixin, TickerProviderStateMixin {
|
||||
final _keyboard = VirtKeyProvider();
|
||||
late final _terminal = Terminal(inputHandler: _keyboard);
|
||||
late final _terminal = Terminal();
|
||||
late final TerminalController _terminalController = TerminalController(vsync: this);
|
||||
final List<List<VirtKey>> _virtKeysList = [];
|
||||
late final _termKey = widget.args.terminalKey ?? GlobalKey<TerminalViewState>();
|
||||
@@ -81,7 +80,7 @@ class SSHPageState extends State<SSHPage>
|
||||
|
||||
bool _isDark = false;
|
||||
Timer? _virtKeyLongPressTimer;
|
||||
late SSHClient? _client = widget.args.spi.server?.value.client;
|
||||
SSHClient? _client;
|
||||
SSHSession? _session;
|
||||
Timer? _discontinuityTimer;
|
||||
|
||||
@@ -117,6 +116,10 @@ class SSHPageState extends State<SSHPage>
|
||||
_initStoredCfg();
|
||||
_initVirtKeys();
|
||||
_setupDiscontinuityTimer();
|
||||
|
||||
// Initialize client from provider
|
||||
final serverState = ref.read(individualServerNotifierProvider(widget.args.spi.id));
|
||||
_client = serverState.client;
|
||||
|
||||
if (++_sshConnCount == 1) {
|
||||
WakelockPlus.enable();
|
||||
@@ -262,19 +265,22 @@ class SSHPageState extends State<SSHPage>
|
||||
child: Container(
|
||||
color: _terminalTheme.background,
|
||||
height: _virtKeysHeight + _media.padding.bottom,
|
||||
child: ChangeNotifierProvider(
|
||||
create: (_) => _keyboard,
|
||||
builder: (_, _) => Consumer<VirtKeyProvider>(
|
||||
builder: (_, _, _) {
|
||||
return _buildVirtualKey();
|
||||
},
|
||||
),
|
||||
child: Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final virtKeyState = ref.watch(virtKeyboardProvider);
|
||||
final virtKeyNotifier = ref.read(virtKeyboardProvider.notifier);
|
||||
|
||||
// Set the terminal input handler
|
||||
_terminal.inputHandler = virtKeyNotifier;
|
||||
|
||||
return _buildVirtualKey(virtKeyState, virtKeyNotifier);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVirtualKey() {
|
||||
Widget _buildVirtualKey(VirtKeyState virtKeyState, VirtKeyboard virtKeyNotifier) {
|
||||
final count = _horizonVirtKeys ? _virtKeysList.length : _virtKeysList.firstOrNull?.length ?? 0;
|
||||
if (count == 0) return UIs.placeholder;
|
||||
return LayoutBuilder(
|
||||
@@ -286,30 +292,30 @@ class SSHPageState extends State<SSHPage>
|
||||
child: Row(
|
||||
children: _virtKeysList
|
||||
.expand((e) => e)
|
||||
.map((e) => _buildVirtKeyItem(e, virtKeyWidth))
|
||||
.map((e) => _buildVirtKeyItem(e, virtKeyWidth, virtKeyState, virtKeyNotifier))
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
final rows = _virtKeysList
|
||||
.map((e) => Row(children: e.map((e) => _buildVirtKeyItem(e, virtKeyWidth)).toList()))
|
||||
.map((e) => Row(children: e.map((e) => _buildVirtKeyItem(e, virtKeyWidth, virtKeyState, virtKeyNotifier)).toList()))
|
||||
.toList();
|
||||
return Column(mainAxisSize: MainAxisSize.min, children: rows);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVirtKeyItem(VirtKey item, double virtKeyWidth) {
|
||||
Widget _buildVirtKeyItem(VirtKey item, double virtKeyWidth, VirtKeyState virtKeyState, VirtKeyboard virtKeyNotifier) {
|
||||
var selected = false;
|
||||
switch (item.key) {
|
||||
case TerminalKey.control:
|
||||
selected = _keyboard.ctrl;
|
||||
selected = virtKeyState.ctrl;
|
||||
break;
|
||||
case TerminalKey.alt:
|
||||
selected = _keyboard.alt;
|
||||
selected = virtKeyState.alt;
|
||||
break;
|
||||
case TerminalKey.shift:
|
||||
selected = _keyboard.shift;
|
||||
selected = virtKeyState.shift;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -326,12 +332,12 @@ class SSHPageState extends State<SSHPage>
|
||||
);
|
||||
|
||||
return InkWell(
|
||||
onTap: () => _doVirtualKey(item),
|
||||
onTap: () => _doVirtualKey(item, virtKeyNotifier),
|
||||
onTapDown: (details) {
|
||||
if (item.canLongPress) {
|
||||
_virtKeyLongPressTimer = Timer.periodic(
|
||||
const Duration(milliseconds: 137),
|
||||
(_) => _doVirtualKey(item),
|
||||
(_) => _doVirtualKey(item, virtKeyNotifier),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
part of 'page.dart';
|
||||
|
||||
extension _VirtKey on SSHPageState {
|
||||
void _doVirtualKey(VirtKey item) {
|
||||
void _doVirtualKey(VirtKey item, VirtKeyboard virtKeyNotifier) {
|
||||
if (item.func != null) {
|
||||
HapticFeedback.mediumImpact();
|
||||
_doVirtualKeyFunc(item.func!);
|
||||
@@ -9,7 +9,7 @@ extension _VirtKey on SSHPageState {
|
||||
}
|
||||
if (item.key != null) {
|
||||
HapticFeedback.mediumImpact();
|
||||
_doVirtualKeyInput(item.key!);
|
||||
_doVirtualKeyInput(item.key!, virtKeyNotifier);
|
||||
}
|
||||
final inputRaw = item.inputRaw;
|
||||
if (inputRaw != null) {
|
||||
@@ -18,16 +18,16 @@ extension _VirtKey on SSHPageState {
|
||||
}
|
||||
}
|
||||
|
||||
void _doVirtualKeyInput(TerminalKey key) {
|
||||
void _doVirtualKeyInput(TerminalKey key, VirtKeyboard virtKeyNotifier) {
|
||||
switch (key) {
|
||||
case TerminalKey.control:
|
||||
_keyboard.ctrl = !_keyboard.ctrl;
|
||||
virtKeyNotifier.setCtrl(!virtKeyNotifier.ctrl);
|
||||
break;
|
||||
case TerminalKey.alt:
|
||||
_keyboard.alt = !_keyboard.alt;
|
||||
virtKeyNotifier.setAlt(!virtKeyNotifier.alt);
|
||||
break;
|
||||
case TerminalKey.shift:
|
||||
_keyboard.shift = !_keyboard.shift;
|
||||
virtKeyNotifier.setShift(!virtKeyNotifier.shift);
|
||||
break;
|
||||
default:
|
||||
_terminal.keyInput(key);
|
||||
@@ -52,14 +52,15 @@ extension _VirtKey on SSHPageState {
|
||||
}
|
||||
break;
|
||||
case VirtualKeyFunc.snippet:
|
||||
final snippetState = ref.read(snippetNotifierProvider);
|
||||
final snippets = await context.showPickWithTagDialog<Snippet>(
|
||||
title: l10n.snippet,
|
||||
tags: SnippetProvider.tags,
|
||||
tags: snippetState.tags.vn,
|
||||
itemsBuilder: (e) {
|
||||
if (e == TagSwitcher.kDefaultTag) {
|
||||
return SnippetProvider.snippets.value;
|
||||
return snippetState.snippets;
|
||||
}
|
||||
return SnippetProvider.snippets.value
|
||||
return snippetState.snippets
|
||||
.where((element) => element.tags?.contains(e) ?? false)
|
||||
.toList();
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:math';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:icons_plus/icons_plus.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
@@ -10,18 +11,18 @@ import 'package:server_box/data/provider/server.dart';
|
||||
import 'package:server_box/view/page/server/edit.dart';
|
||||
import 'package:server_box/view/page/ssh/page/page.dart';
|
||||
|
||||
class SSHTabPage extends StatefulWidget {
|
||||
class SSHTabPage extends ConsumerStatefulWidget {
|
||||
const SSHTabPage({super.key});
|
||||
|
||||
@override
|
||||
State<SSHTabPage> createState() => _SSHTabPageState();
|
||||
ConsumerState<SSHTabPage> createState() => _SSHTabPageState();
|
||||
|
||||
static const route = AppRouteNoArg(page: SSHTabPage.new, path: '/ssh');
|
||||
}
|
||||
|
||||
typedef _TabMap = Map<String, ({Widget page, FocusNode? focus})>;
|
||||
|
||||
class _SSHTabPageState extends State<SSHTabPage>
|
||||
class _SSHTabPageState extends ConsumerState<SSHTabPage>
|
||||
with TickerProviderStateMixin, AutomaticKeepAliveClientMixin {
|
||||
late final _TabMap _tabMap = {libL10n.add: (page: _AddPage(onTapInitCard: _onTapInitCard), focus: null)};
|
||||
final _pageCtrl = PageController();
|
||||
@@ -236,7 +237,7 @@ final class _TabBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _AddPage extends StatelessWidget {
|
||||
class _AddPage extends ConsumerWidget {
|
||||
const _AddPage({required this.onTapInitCard});
|
||||
|
||||
final void Function(Spi spi) onTapInitCard;
|
||||
@@ -244,11 +245,12 @@ class _AddPage extends StatelessWidget {
|
||||
Widget get _placeholder => const Expanded(child: UIs.placeholder);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
const viewPadding = 7.0;
|
||||
final viewWidth = context.mediaQuery.size.width - 2 * viewPadding;
|
||||
final viewWidth = context.windowSize.width - 2 * viewPadding;
|
||||
|
||||
final itemCount = ServerProvider.servers.length;
|
||||
final serverState = ref.watch(serverNotifierProvider);
|
||||
final itemCount = serverState.servers.length;
|
||||
const itemPadding = 1.0;
|
||||
const itemWidth = 150.0;
|
||||
const itemHeight = 50.0;
|
||||
@@ -257,53 +259,53 @@ class _AddPage extends StatelessWidget {
|
||||
final crossCount = max(viewWidth ~/ (visualCrossCount * itemPadding + itemWidth), 1);
|
||||
final mainCount = itemCount ~/ crossCount + 1;
|
||||
|
||||
return ServerProvider.serverOrder.listenVal((order) {
|
||||
if (order.isEmpty) {
|
||||
return Center(child: Text(libL10n.empty, textAlign: TextAlign.center));
|
||||
}
|
||||
final order = serverState.serverOrder;
|
||||
|
||||
if (order.isEmpty) {
|
||||
return Center(child: Text(libL10n.empty, textAlign: TextAlign.center));
|
||||
}
|
||||
|
||||
// Custom grid
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(viewPadding),
|
||||
children: List.generate(
|
||||
mainCount,
|
||||
(rowIndex) => Row(
|
||||
children: List.generate(crossCount, (columnIndex) {
|
||||
final idx = rowIndex * crossCount + columnIndex;
|
||||
final id = order.elementAtOrNull(idx);
|
||||
final spi = ServerProvider.pick(id: id)?.value.spi;
|
||||
if (spi == null) return _placeholder;
|
||||
// Custom grid
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(viewPadding),
|
||||
children: List.generate(
|
||||
mainCount,
|
||||
(rowIndex) => Row(
|
||||
children: List.generate(crossCount, (columnIndex) {
|
||||
final idx = rowIndex * crossCount + columnIndex;
|
||||
final id = order.elementAtOrNull(idx);
|
||||
final spi = serverState.servers[id];
|
||||
if (spi == null) return _placeholder;
|
||||
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(itemPadding),
|
||||
child: InkWell(
|
||||
onTap: () => onTapInitCard(spi),
|
||||
child: Container(
|
||||
height: itemHeight,
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.only(left: 17, right: 7),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
spi.name,
|
||||
style: UIs.text18,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(itemPadding),
|
||||
child: InkWell(
|
||||
onTap: () => onTapInitCard(spi),
|
||||
child: Container(
|
||||
height: itemHeight,
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.only(left: 17, right: 7),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
spi.name,
|
||||
style: UIs.text18,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const Icon(Icons.chevron_right),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(Icons.chevron_right),
|
||||
],
|
||||
),
|
||||
).cardx,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
).cardx,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:io';
|
||||
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/data/model/app/path_with_prefix.dart';
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
@@ -19,7 +20,7 @@ final class LocalFilePageArgs {
|
||||
const LocalFilePageArgs({this.isPickFile, this.initDir});
|
||||
}
|
||||
|
||||
class LocalFilePage extends StatefulWidget {
|
||||
class LocalFilePage extends ConsumerStatefulWidget {
|
||||
final LocalFilePageArgs? args;
|
||||
|
||||
const LocalFilePage({super.key, this.args});
|
||||
@@ -27,10 +28,10 @@ class LocalFilePage extends StatefulWidget {
|
||||
static const route = AppRoute<String, LocalFilePageArgs>(page: LocalFilePage.new, path: '/files/local');
|
||||
|
||||
@override
|
||||
State<LocalFilePage> createState() => _LocalFilePageState();
|
||||
ConsumerState<LocalFilePage> createState() => _LocalFilePageState();
|
||||
}
|
||||
|
||||
class _LocalFilePageState extends State<LocalFilePage> with AutomaticKeepAliveClientMixin {
|
||||
class _LocalFilePageState extends ConsumerState<LocalFilePage> with AutomaticKeepAliveClientMixin {
|
||||
late final _path = LocalPath(widget.args?.initDir ?? Paths.file);
|
||||
final _sortType = _SortType.name.vn;
|
||||
bool get isPickFile => widget.args?.isPickFile ?? false;
|
||||
@@ -358,10 +359,7 @@ extension _OnTapFile on _LocalFilePageState {
|
||||
|
||||
final spi = await context.showPickSingleDialog<Spi>(
|
||||
title: libL10n.select,
|
||||
items: ServerProvider.serverOrder.value
|
||||
.map((e) => ServerProvider.pick(id: e)?.value.spi)
|
||||
.whereType<Spi>()
|
||||
.toList(),
|
||||
items: ref.read(serverNotifierProvider).servers.values.toList(),
|
||||
display: (e) => e.name,
|
||||
);
|
||||
if (spi == null) return;
|
||||
@@ -372,7 +370,7 @@ extension _OnTapFile on _LocalFilePageState {
|
||||
return;
|
||||
}
|
||||
|
||||
SftpProvider.add(SftpReq(spi, '$remotePath/$fileName', file.absolute.path, SftpReqType.upload));
|
||||
ref.read(sftpNotifierProvider.notifier).add(SftpReq(spi, '$remotePath/$fileName', file.absolute.path, SftpReqType.upload));
|
||||
context.showSnackBar(l10n.added2List);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'dart:io';
|
||||
import 'package:dartssh2/dartssh2.dart';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:icons_plus/icons_plus.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/core/extension/sftpfile.dart';
|
||||
@@ -11,6 +12,7 @@ import 'package:server_box/core/utils/comparator.dart';
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
import 'package:server_box/data/model/sftp/browser_status.dart';
|
||||
import 'package:server_box/data/model/sftp/worker.dart';
|
||||
import 'package:server_box/data/provider/server.dart';
|
||||
import 'package:server_box/data/provider/sftp.dart';
|
||||
import 'package:server_box/data/res/misc.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
@@ -29,21 +31,29 @@ final class SftpPageArgs {
|
||||
const SftpPageArgs({required this.spi, this.isSelect = false, this.initPath});
|
||||
}
|
||||
|
||||
class SftpPage extends StatefulWidget {
|
||||
class SftpPage extends ConsumerStatefulWidget {
|
||||
final SftpPageArgs args;
|
||||
|
||||
const SftpPage({super.key, required this.args});
|
||||
|
||||
@override
|
||||
State<SftpPage> createState() => _SftpPageState();
|
||||
ConsumerState<SftpPage> createState() => _SftpPageState();
|
||||
|
||||
static const route = AppRouteArg<String, SftpPageArgs>(page: SftpPage.new, path: '/sftp');
|
||||
}
|
||||
|
||||
class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
||||
late final _status = SftpBrowserStatus(_client);
|
||||
late final _client = widget.args.spi.server!.value.client!;
|
||||
class _SftpPageState extends ConsumerState<SftpPage> with AfterLayoutMixin {
|
||||
late final SftpBrowserStatus _status;
|
||||
late final SSHClient _client;
|
||||
final _sortOption = _SortOption().vn;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final serverState = ref.read(individualServerNotifierProvider(widget.args.spi.id));
|
||||
_client = serverState.client!;
|
||||
_status = SftpBrowserStatus(_client);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -280,7 +290,7 @@ extension _Actions on _SftpPageState {
|
||||
final localPath = _getLocalPath(remotePath);
|
||||
final completer = Completer();
|
||||
final req = SftpReq(widget.args.spi, remotePath, localPath, SftpReqType.download);
|
||||
SftpProvider.add(req, completer: completer);
|
||||
ref.read(sftpNotifierProvider.notifier).add(req, completer: completer);
|
||||
final (suc, err) = await context.showLoadingDialog(fn: () => completer.future);
|
||||
if (suc == null || err != null) return;
|
||||
|
||||
@@ -289,7 +299,9 @@ extension _Actions on _SftpPageState {
|
||||
args: EditorPageArgs(
|
||||
path: localPath,
|
||||
onSave: (_) {
|
||||
SftpProvider.add(SftpReq(req.spi, remotePath, localPath, SftpReqType.upload));
|
||||
ref
|
||||
.read(sftpNotifierProvider.notifier)
|
||||
.add(SftpReq(req.spi, remotePath, localPath, SftpReqType.upload));
|
||||
context.showSnackBar(l10n.added2List);
|
||||
},
|
||||
closeAfterSave: SettingStore.instance.closeAfterSave.fetch(),
|
||||
@@ -310,9 +322,9 @@ extension _Actions on _SftpPageState {
|
||||
context.pop();
|
||||
final remotePath = _getRemotePath(name);
|
||||
|
||||
SftpProvider.add(
|
||||
SftpReq(widget.args.spi, remotePath, _getLocalPath(remotePath), SftpReqType.download),
|
||||
);
|
||||
ref
|
||||
.read(sftpNotifierProvider.notifier)
|
||||
.add(SftpReq(widget.args.spi, remotePath, _getLocalPath(remotePath), SftpReqType.download));
|
||||
|
||||
context.pop();
|
||||
},
|
||||
@@ -640,7 +652,9 @@ extension _Actions on _SftpPageState {
|
||||
final fileName = path.split(Platform.pathSeparator).lastOrNull;
|
||||
final remotePath = '$remoteDir/$fileName';
|
||||
Loggers.app.info('SFTP upload local: $path, remote: $remotePath');
|
||||
SftpProvider.add(SftpReq(widget.args.spi, remotePath, path, SftpReqType.upload));
|
||||
ref
|
||||
.read(sftpNotifierProvider.notifier)
|
||||
.add(SftpReq(widget.args.spi, remotePath, path, SftpReqType.upload));
|
||||
},
|
||||
icon: const Icon(Icons.upload_file),
|
||||
);
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/data/model/sftp/worker.dart';
|
||||
import 'package:server_box/data/provider/sftp.dart';
|
||||
import 'package:server_box/view/page/storage/local.dart';
|
||||
|
||||
class SftpMissionPage extends StatefulWidget {
|
||||
class SftpMissionPage extends ConsumerStatefulWidget {
|
||||
const SftpMissionPage({super.key});
|
||||
|
||||
@override
|
||||
State<SftpMissionPage> createState() => _SftpMissionPageState();
|
||||
ConsumerState<SftpMissionPage> createState() => _SftpMissionPageState();
|
||||
|
||||
static const route = AppRouteNoArg(page: SftpMissionPage.new, path: '/sftp/mission');
|
||||
}
|
||||
|
||||
class _SftpMissionPageState extends State<SftpMissionPage> {
|
||||
class _SftpMissionPageState extends ConsumerState<SftpMissionPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -24,18 +25,17 @@ class _SftpMissionPageState extends State<SftpMissionPage> {
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
return SftpProvider.status.listenVal((status) {
|
||||
if (status.isEmpty) {
|
||||
return Center(child: Text(libL10n.empty));
|
||||
}
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(11),
|
||||
itemCount: status.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildItem(status[index]);
|
||||
},
|
||||
);
|
||||
});
|
||||
final status = ref.watch(sftpNotifierProvider.select((pro) => pro.requests));
|
||||
if (status.isEmpty) {
|
||||
return Center(child: Text(libL10n.empty));
|
||||
}
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(11),
|
||||
itemCount: status.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildItem(status[index]);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildItem(SftpReqStatus status) {
|
||||
@@ -143,7 +143,7 @@ class _SftpMissionPageState extends State<SftpMissionPage> {
|
||||
child: Text(libL10n.askContinue('${libL10n.delete} ${l10n.mission}($name)')),
|
||||
actions: Btn.ok(
|
||||
onTap: () {
|
||||
SftpProvider.cancel(id);
|
||||
ref.read(sftpNotifierProvider.notifier).cancel(id);
|
||||
context.pop();
|
||||
},
|
||||
).toList,
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.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/systemd.dart';
|
||||
import 'package:server_box/data/provider/systemd.dart';
|
||||
import 'package:server_box/view/page/ssh/page/page.dart';
|
||||
|
||||
final class SystemdPage extends StatefulWidget {
|
||||
final class SystemdPage extends ConsumerStatefulWidget {
|
||||
final SpiRequiredArgs args;
|
||||
|
||||
const SystemdPage({super.key, required this.args});
|
||||
@@ -14,44 +15,39 @@ final class SystemdPage extends StatefulWidget {
|
||||
static const route = AppRouteArg<void, SpiRequiredArgs>(page: SystemdPage.new, path: '/systemd');
|
||||
|
||||
@override
|
||||
State<SystemdPage> createState() => _SystemdPageState();
|
||||
ConsumerState<SystemdPage> createState() => _SystemdPageState();
|
||||
}
|
||||
|
||||
final class _SystemdPageState extends State<SystemdPage> {
|
||||
late final _pro = SystemdProvider.init(widget.args.spi);
|
||||
final class _SystemdPageState extends ConsumerState<SystemdPage> {
|
||||
late final _pro = systemdNotifierProvider(widget.args.spi);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_pro.dispose();
|
||||
}
|
||||
late final _notifier = ref.read(_pro.notifier);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: CustomAppBar(
|
||||
title: const Text('Systemd'),
|
||||
actions: isDesktop ? [Btn.icon(icon: const Icon(Icons.refresh), onTap: _pro.getUnits)] : null,
|
||||
actions: isDesktop ? [Btn.icon(icon: const Icon(Icons.refresh), onTap: _notifier.getUnits)] : null,
|
||||
),
|
||||
body: RefreshIndicator(onRefresh: _pro.getUnits, child: _buildBody()),
|
||||
body: RefreshIndicator(onRefresh: _notifier.getUnits, child: _buildBody()),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
final isBusy = ref.watch(_pro.select((pro) => pro.isBusy));
|
||||
return CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
SliverToBoxAdapter(
|
||||
child: Column(
|
||||
children: [
|
||||
_buildScopeFilterChips(),
|
||||
_pro.isBusy.listenVal(
|
||||
(isBusy) => AnimatedContainer(
|
||||
duration: Durations.medium1,
|
||||
curve: Curves.fastEaseInToSlowEaseOut,
|
||||
height: isBusy ? SizedLoading.medium.size : 0,
|
||||
width: isBusy ? SizedLoading.medium.size : 0,
|
||||
child: isBusy ? SizedLoading.medium : const SizedBox.shrink(),
|
||||
),
|
||||
AnimatedContainer(
|
||||
duration: Durations.medium1,
|
||||
curve: Curves.fastEaseInToSlowEaseOut,
|
||||
height: isBusy ? SizedLoading.medium.size : 0,
|
||||
width: isBusy ? SizedLoading.medium.size : 0,
|
||||
child: isBusy ? SizedLoading.medium : const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -62,43 +58,40 @@ final class _SystemdPageState extends State<SystemdPage> {
|
||||
}
|
||||
|
||||
Widget _buildScopeFilterChips() {
|
||||
return _pro.scopeFilter.listenVal((currentFilter) {
|
||||
return Wrap(
|
||||
spacing: 8,
|
||||
children: SystemdScopeFilter.values.map((filter) {
|
||||
final isSelected = filter == currentFilter;
|
||||
return FilterChip(
|
||||
selected: isSelected,
|
||||
label: Text(filter.displayName),
|
||||
onSelected: (_) => _pro.scopeFilter.value = filter,
|
||||
);
|
||||
}).toList(),
|
||||
).paddingSymmetric(horizontal: 13, vertical: 8);
|
||||
});
|
||||
final currentFilter = ref.watch(_pro.select((p) => p.scopeFilter));
|
||||
return Wrap(
|
||||
spacing: 8,
|
||||
children: SystemdScopeFilter.values.map((filter) {
|
||||
final isSelected = filter == currentFilter;
|
||||
return FilterChip(
|
||||
selected: isSelected,
|
||||
label: Text(filter.displayName),
|
||||
onSelected: (_) => _notifier.setScopeFilter(filter),
|
||||
);
|
||||
}).toList(),
|
||||
).paddingSymmetric(horizontal: 13, vertical: 8);
|
||||
}
|
||||
|
||||
Widget _buildUnitList() {
|
||||
return _pro.units.listenVal((allUnits) {
|
||||
return _pro.scopeFilter.listenVal((filter) {
|
||||
final filteredUnits = _pro.filteredUnits;
|
||||
if (filteredUnits.isEmpty) {
|
||||
return SliverToBoxAdapter(child: CenterGreyTitle(libL10n.empty).paddingSymmetric(horizontal: 13));
|
||||
}
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
final unit = filteredUnits[index];
|
||||
return ListTile(
|
||||
leading: _buildScopeTag(unit.scope),
|
||||
title: unit.description != null ? TipText(unit.name, unit.description!) : Text(unit.name),
|
||||
subtitle: Wrap(
|
||||
children: [_buildStateTag(unit.state), _buildTypeTag(unit.type)],
|
||||
).paddingOnly(top: 7),
|
||||
trailing: _buildUnitFuncs(unit),
|
||||
).cardx.paddingSymmetric(horizontal: 13);
|
||||
}, childCount: filteredUnits.length),
|
||||
);
|
||||
});
|
||||
});
|
||||
ref.watch(_pro.select((p) => p.units));
|
||||
ref.watch(_pro.select((p) => p.scopeFilter));
|
||||
final filteredUnits = _notifier.filteredUnits;
|
||||
if (filteredUnits.isEmpty) {
|
||||
return SliverToBoxAdapter(child: CenterGreyTitle(libL10n.empty).paddingSymmetric(horizontal: 13));
|
||||
}
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
final unit = filteredUnits[index];
|
||||
return ListTile(
|
||||
leading: _buildScopeTag(unit.scope),
|
||||
title: unit.description != null ? TipText(unit.name, unit.description!) : Text(unit.name),
|
||||
subtitle: Wrap(
|
||||
children: [_buildStateTag(unit.state), _buildTypeTag(unit.type)],
|
||||
).paddingOnly(top: 7),
|
||||
trailing: _buildUnitFuncs(unit),
|
||||
).cardx.paddingSymmetric(horizontal: 13);
|
||||
}, childCount: filteredUnits.length),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUnitFuncs(SystemdUnit unit) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:io';
|
||||
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/core/route.dart';
|
||||
import 'package:server_box/core/utils/server.dart';
|
||||
@@ -19,17 +20,17 @@ import 'package:server_box/view/page/ssh/page/page.dart';
|
||||
import 'package:server_box/view/page/storage/sftp.dart';
|
||||
import 'package:server_box/view/page/systemd.dart';
|
||||
|
||||
class ServerFuncBtnsTopRight extends StatelessWidget {
|
||||
class ServerFuncBtnsTopRight extends ConsumerWidget {
|
||||
final Spi spi;
|
||||
|
||||
const ServerFuncBtnsTopRight({super.key, required this.spi});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return PopupMenu<ServerFuncBtn>(
|
||||
items: ServerFuncBtn.values.map((e) => PopMenu.build(e, e.icon, e.toStr)).toList(),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
onSelected: (val) => _onTapMoreBtns(val, spi, context),
|
||||
onSelected: (val) => _onTapMoreBtns(val, spi, context, ref),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -52,18 +53,18 @@ class ServerFuncBtns extends StatelessWidget {
|
||||
padding: EdgeInsets.symmetric(horizontal: 13),
|
||||
itemBuilder: (context, index) {
|
||||
final value = btns[index];
|
||||
final item = _buildItem(context, value);
|
||||
final item = Consumer(builder: (_, ref, _) => _buildItem(context, value, ref));
|
||||
return item.paddingSymmetric(horizontal: 7);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildItem(BuildContext context, ServerFuncBtn e) {
|
||||
Widget _buildItem(BuildContext context, ServerFuncBtn e, WidgetRef ref) {
|
||||
final move = Stores.setting.moveServerFuncs.fetch();
|
||||
if (move) {
|
||||
return IconButton(
|
||||
onPressed: () => _onTapMoreBtns(e, spi, context),
|
||||
onPressed: () => _onTapMoreBtns(e, spi, context, ref),
|
||||
padding: EdgeInsets.zero,
|
||||
tooltip: e.toStr,
|
||||
icon: Icon(e.icon, size: 15),
|
||||
@@ -76,7 +77,7 @@ class ServerFuncBtns extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () => _onTapMoreBtns(e, spi, context),
|
||||
onPressed: () => _onTapMoreBtns(e, spi, context, ref),
|
||||
padding: EdgeInsets.zero,
|
||||
icon: Icon(e.icon, size: 17),
|
||||
),
|
||||
@@ -101,14 +102,14 @@ class ServerFuncBtns extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
void _onTapMoreBtns(ServerFuncBtn value, Spi spi, BuildContext context) async {
|
||||
void _onTapMoreBtns(ServerFuncBtn value, Spi spi, BuildContext context, WidgetRef ref) async {
|
||||
// final isMobile = ResponsiveBreakpoints.of(context).isMobile;
|
||||
switch (value) {
|
||||
// case ServerFuncBtn.pkg:
|
||||
// _onPkg(context, spi);
|
||||
// break;
|
||||
case ServerFuncBtn.sftp:
|
||||
if (!_checkClient(context, spi.id)) return;
|
||||
if (!_checkClient(context, spi.id, ref)) return;
|
||||
final args = SftpPageArgs(spi: spi);
|
||||
// if (isMobile) {
|
||||
SftpPage.route.go(context, args);
|
||||
@@ -120,18 +121,19 @@ void _onTapMoreBtns(ServerFuncBtn value, Spi spi, BuildContext context) async {
|
||||
|
||||
break;
|
||||
case ServerFuncBtn.snippet:
|
||||
if (SnippetProvider.snippets.value.isEmpty) {
|
||||
final snippetState = ref.read(snippetNotifierProvider);
|
||||
if (snippetState.snippets.isEmpty) {
|
||||
context.showSnackBar(libL10n.empty);
|
||||
return;
|
||||
}
|
||||
final snippets = await context.showPickWithTagDialog<Snippet>(
|
||||
title: l10n.snippet,
|
||||
tags: SnippetProvider.tags,
|
||||
tags: snippetState.tags.vn,
|
||||
itemsBuilder: (e) {
|
||||
if (e == TagSwitcher.kDefaultTag) {
|
||||
return SnippetProvider.snippets.value;
|
||||
return snippetState.snippets;
|
||||
}
|
||||
return SnippetProvider.snippets.value
|
||||
return snippetState.snippets
|
||||
.where((element) => element.tags?.contains(e) ?? false)
|
||||
.toList();
|
||||
},
|
||||
@@ -147,7 +149,7 @@ void _onTapMoreBtns(ServerFuncBtn value, Spi spi, BuildContext context) async {
|
||||
actions: [CountDownBtn(onTap: () => context.pop(true), text: l10n.run, afterColor: Colors.red)],
|
||||
);
|
||||
if (sure != true) return;
|
||||
if (!_checkClient(context, spi.id)) return;
|
||||
if (!_checkClient(context, spi.id, ref)) return;
|
||||
final args = SshPageArgs(spi: spi, initSnippet: snippet);
|
||||
// if (isMobile) {
|
||||
SSHPage.route.go(context, args);
|
||||
@@ -158,7 +160,7 @@ void _onTapMoreBtns(ServerFuncBtn value, Spi spi, BuildContext context) async {
|
||||
// }
|
||||
break;
|
||||
case ServerFuncBtn.container:
|
||||
if (!_checkClient(context, spi.id)) return;
|
||||
if (!_checkClient(context, spi.id, ref)) return;
|
||||
final args = SpiRequiredArgs(spi);
|
||||
// if (isMobile) {
|
||||
ContainerPage.route.go(context, args);
|
||||
@@ -169,7 +171,7 @@ void _onTapMoreBtns(ServerFuncBtn value, Spi spi, BuildContext context) async {
|
||||
// }
|
||||
break;
|
||||
case ServerFuncBtn.process:
|
||||
if (!_checkClient(context, spi.id)) return;
|
||||
if (!_checkClient(context, spi.id, ref)) return;
|
||||
final args = SpiRequiredArgs(spi);
|
||||
// if (isMobile) {
|
||||
ProcessPage.route.go(context, args);
|
||||
@@ -183,7 +185,7 @@ void _onTapMoreBtns(ServerFuncBtn value, Spi spi, BuildContext context) async {
|
||||
_gotoSSH(spi, context);
|
||||
break;
|
||||
case ServerFuncBtn.iperf:
|
||||
if (!_checkClient(context, spi.id)) return;
|
||||
if (!_checkClient(context, spi.id, ref)) return;
|
||||
final args = SpiRequiredArgs(spi);
|
||||
// if (isMobile) {
|
||||
IPerfPage.route.go(context, args);
|
||||
@@ -194,7 +196,7 @@ void _onTapMoreBtns(ServerFuncBtn value, Spi spi, BuildContext context) async {
|
||||
// }
|
||||
break;
|
||||
case ServerFuncBtn.systemd:
|
||||
if (!_checkClient(context, spi.id)) return;
|
||||
if (!_checkClient(context, spi.id, ref)) return;
|
||||
final args = SpiRequiredArgs(spi);
|
||||
// if (isMobile) {
|
||||
SystemdPage.route.go(context, args);
|
||||
@@ -270,9 +272,9 @@ void _gotoSSH(Spi spi, BuildContext context) async {
|
||||
}
|
||||
}
|
||||
|
||||
bool _checkClient(BuildContext context, String id) {
|
||||
final server = ServerProvider.pick(id: id)?.value;
|
||||
if (server == null || server.client == null) {
|
||||
bool _checkClient(BuildContext context, String id, WidgetRef ref) {
|
||||
final serverState = ref.read(individualServerNotifierProvider(id));
|
||||
if (serverState.client == null) {
|
||||
context.showSnackBar(l10n.waitConnection);
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user