migrate: riverpod + freezed (#870)

This commit is contained in:
lollipopkit🏳️‍⚧️
2025-08-31 00:55:54 +08:00
committed by GitHub
parent 9cb705f8dd
commit 53a7c0d8ff
67 changed files with 5012 additions and 1328 deletions

View File

@@ -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();

View File

@@ -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();
}

View File

@@ -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;
}
},

View File

@@ -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),
// ),
// ],

View File

@@ -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

View File

@@ -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());

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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));
}
}
}

View File

@@ -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(

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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),

View File

@@ -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,
);
});
},
);
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,

View File

@@ -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());

View File

@@ -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();
}

View File

@@ -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,
);

View 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/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,
),

View File

@@ -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);
}

View File

@@ -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),
);
}
},

View File

@@ -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();
},

View File

@@ -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,
),
);
}),
),
);
});
),
);
}
}

View File

@@ -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);
}
}

View File

@@ -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),
);

View 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,

View File

@@ -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) {

View File

@@ -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;
}