mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 23:34:24 +01:00
374 lines
12 KiB
Dart
374 lines
12 KiB
Dart
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:server_box/core/extension/context/locale.dart';
|
|
import 'package:server_box/core/route.dart';
|
|
import 'package:server_box/data/model/app/error.dart';
|
|
import 'package:server_box/data/model/app/menu/base.dart';
|
|
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/provider/container.dart';
|
|
import 'package:server_box/data/provider/server/single.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 ConsumerStatefulWidget {
|
|
final SpiRequiredArgs args;
|
|
const ContainerPage({required this.args, super.key});
|
|
|
|
@override
|
|
ConsumerState<ContainerPage> createState() => _ContainerPageState();
|
|
|
|
static const route = AppRouteArg(page: ContainerPage.new, path: '/container');
|
|
}
|
|
|
|
class _ContainerPageState extends ConsumerState<ContainerPage> {
|
|
final _textController = TextEditingController();
|
|
late final ContainerNotifierProvider _provider;
|
|
|
|
@override
|
|
void dispose() {
|
|
super.dispose();
|
|
_textController.dispose();
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
final serverState = ref.read(serverProvider(widget.args.spi.id));
|
|
_provider = containerProvider(
|
|
serverState.client,
|
|
widget.args.spi.user,
|
|
widget.args.spi.id,
|
|
context,
|
|
);
|
|
_initAutoRefresh();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final err = ref.watch(_provider.select((p) => p.error));
|
|
|
|
return Scaffold(
|
|
appBar: _buildAppBar(),
|
|
body: SafeArea(child: _buildMain()),
|
|
floatingActionButton: err == null ? _buildFAB() : null,
|
|
);
|
|
}
|
|
|
|
CustomAppBar _buildAppBar() {
|
|
return CustomAppBar(
|
|
centerTitle: true,
|
|
title: TwoLineText(up: l10n.container, down: widget.args.spi.name),
|
|
actions: [
|
|
IconButton(
|
|
onPressed: () => context.showLoadingDialog(fn: () => _containerNotifier.refresh()),
|
|
icon: const Icon(Icons.refresh),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildFAB() {
|
|
return FloatingActionButton(onPressed: () async => await _showAddFAB(), child: const Icon(Icons.add));
|
|
}
|
|
|
|
Widget _buildMain() {
|
|
final containerState = _containerState;
|
|
|
|
if (containerState.error != null && containerState.items == null) {
|
|
return SizedBox.expand(
|
|
child: Column(
|
|
children: [
|
|
const Spacer(),
|
|
const Icon(Icons.error, size: 37),
|
|
UIs.height13,
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 23),
|
|
child: Text(containerState.error.toString()),
|
|
),
|
|
const Spacer(),
|
|
UIs.height13,
|
|
_buildSettingsBtns,
|
|
],
|
|
).paddingSymmetric(horizontal: 13),
|
|
);
|
|
}
|
|
if (containerState.items == null || containerState.images == null) {
|
|
return UIs.centerLoading;
|
|
}
|
|
|
|
return AutoMultiList(
|
|
children: <Widget>[
|
|
_buildLoading(containerState),
|
|
_buildVersion(containerState),
|
|
_buildPs(containerState),
|
|
_buildImage(containerState),
|
|
_buildEmptyStateMessage(containerState),
|
|
_buildPruneBtns,
|
|
_buildSettingsBtns,
|
|
],
|
|
);
|
|
}
|
|
|
|
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),
|
|
child: SimpleMarkdown(data: l10n.dockerEmptyRunningItems),
|
|
),
|
|
);
|
|
}
|
|
return UIs.placeholder;
|
|
}
|
|
|
|
Widget _buildImage(ContainerState containerState) {
|
|
return ExpandTile(
|
|
leading: const Icon(MingCute.clapperboard_line),
|
|
title: Text(l10n.imagesList),
|
|
subtitle: Text(l10n.dockerImagesFmt(containerState.images?.length ?? 'null'), style: UIs.textGrey),
|
|
initiallyExpanded: (containerState.images?.length ?? 0) <= 3,
|
|
children: containerState.images?.map(_buildImageItem).toList() ?? [],
|
|
).cardx;
|
|
}
|
|
|
|
Widget _buildImageItem(ContainerImg e) {
|
|
final repoSplited = e.repository?.split('/');
|
|
final title = repoSplited?.lastOrNull ?? e.repository;
|
|
repoSplited?.removeLast();
|
|
final reg = repoSplited?.join('/');
|
|
return ListTile(
|
|
title: Text(title ?? l10n.unknown, style: UIs.text15),
|
|
subtitle: Text('${reg ?? ''} - ${e.tag} - ${e.sizeMB}', style: UIs.text13Grey),
|
|
trailing: Btn.icon(
|
|
padding: EdgeInsets.zero,
|
|
icon: const Icon(Icons.delete),
|
|
onTap: () => _showImageRmDialog(e),
|
|
),
|
|
);
|
|
}
|
|
|
|
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(containerState.runLog ?? '...'),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildVersion(ContainerState containerState) {
|
|
return CardX(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(17),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [Text(containerState.type.name.capitalize), Text(containerState.version ?? l10n.unknown)],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildPs(ContainerState containerState) {
|
|
final items = containerState.items;
|
|
if (items == null) return UIs.placeholder;
|
|
final running = items.where((e) => e.status.isRunning).length;
|
|
final stopped = items.length - running;
|
|
final subtitle = stopped > 0
|
|
? l10n.dockerStatusRunningAndStoppedFmt(running, stopped)
|
|
: l10n.dockerStatusRunningFmt(running);
|
|
return ExpandTile(
|
|
leading: const Icon(OctIcons.container, size: 22),
|
|
title: Text(l10n.container),
|
|
subtitle: Text(subtitle, style: UIs.textGrey),
|
|
initiallyExpanded: items.length < 7,
|
|
children: items.map(_buildPsItem).toList(),
|
|
).cardx;
|
|
}
|
|
|
|
Widget _buildPsItem(ContainerPs item) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 11),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(item.name ?? l10n.unknown, style: UIs.text15),
|
|
const SizedBox(height: 3),
|
|
_buildMoreBtn(item),
|
|
],
|
|
),
|
|
Text(
|
|
'${item.image ?? l10n.unknown} - ${switch (item) {
|
|
final PodmanPs ps => ps.status.displayName,
|
|
final DockerPs ps => ps.state ?? ps.status.displayName,
|
|
}}',
|
|
style: UIs.text13Grey,
|
|
),
|
|
_buildPsItemStats(item),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildPsItemStats(ContainerPs item) {
|
|
if (item.cpu == null || item.mem == null) return UIs.placeholder;
|
|
return LayoutBuilder(
|
|
builder: (_, cons) {
|
|
final width = cons.maxWidth / 2 - 41;
|
|
return Column(
|
|
children: [
|
|
UIs.height13,
|
|
Row(
|
|
children: [
|
|
_buildPsItemStatsItem('CPU', item.cpu, Icons.memory, width: width),
|
|
UIs.width13,
|
|
_buildPsItemStatsItem('Net', item.net, Icons.network_cell, width: width),
|
|
],
|
|
),
|
|
Row(
|
|
children: [
|
|
_buildPsItemStatsItem('Mem', item.mem, Icons.settings_input_component, width: width),
|
|
UIs.width13,
|
|
_buildPsItemStatsItem('Disk', item.disk, Icons.storage, width: width),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildPsItemStatsItem(String title, String? value, IconData icon, {required double width}) {
|
|
return SizedBox(
|
|
width: width,
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(icon, size: 12, color: Colors.grey),
|
|
UIs.width7,
|
|
Text(value ?? l10n.unknown, style: UIs.text11Grey),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildMoreBtn(ContainerPs dItem) {
|
|
return PopupMenu(
|
|
items: ContainerMenu.items(dItem.status.isRunning).map((e) => PopMenu.build(e, e.icon, e.toStr)).toList(),
|
|
onSelected: (item) => _onTapMoreBtn(item, dItem),
|
|
);
|
|
}
|
|
|
|
String _buildAddCmd(String image, String name, String args) {
|
|
var suffix = '';
|
|
if (args.isEmpty) {
|
|
suffix = image;
|
|
} else {
|
|
suffix = '$args $image';
|
|
}
|
|
if (name.isEmpty) {
|
|
return 'run -itd $suffix';
|
|
}
|
|
return 'run -itd --name $name $suffix';
|
|
}
|
|
|
|
Widget get _buildPruneBtns {
|
|
final len = _PruneTypes.values.length;
|
|
if (len == 0) return UIs.placeholder;
|
|
return ExpandTile(
|
|
leading: const Icon(Icons.delete),
|
|
title: Text(l10n.prune),
|
|
children: _PruneTypes.values.map(_buildPruneBtn).toList(),
|
|
).cardx;
|
|
}
|
|
|
|
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 => containerNotifier.pruneImages,
|
|
_PruneTypes.containers => containerNotifier.pruneContainers,
|
|
_PruneTypes.volumes => containerNotifier.pruneVolumes,
|
|
_PruneTypes.system => containerNotifier.pruneSystem,
|
|
},
|
|
);
|
|
},
|
|
title: Text(title),
|
|
trailing: const Icon(Icons.keyboard_arrow_right),
|
|
);
|
|
}
|
|
|
|
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: containerState.error != null,
|
|
children: _SettingsMenuItems.values.map((item) => _buildSettingTile(item, containerState)).toList(),
|
|
).cardx;
|
|
}
|
|
|
|
Widget _buildSettingTile(_SettingsMenuItems item, ContainerState containerState) {
|
|
final String title;
|
|
switch (item) {
|
|
case _SettingsMenuItems.editDockerHost:
|
|
title = '${libL10n.edit} DOCKER_HOST';
|
|
break;
|
|
case _SettingsMenuItems.switchProvider:
|
|
title = containerState.type == ContainerType.podman
|
|
? l10n.switchTo('Docker')
|
|
: l10n.switchTo('Podman');
|
|
break;
|
|
}
|
|
return ListTile(
|
|
onTap: () {
|
|
switch (item) {
|
|
case _SettingsMenuItems.editDockerHost:
|
|
_showEditHostDialog();
|
|
break;
|
|
case _SettingsMenuItems.switchProvider:
|
|
ref
|
|
.read(_provider.notifier)
|
|
.setType(
|
|
containerState.type == ContainerType.docker ? ContainerType.podman : ContainerType.docker,
|
|
);
|
|
break;
|
|
}
|
|
},
|
|
title: Text(title),
|
|
trailing: const Icon(Icons.keyboard_arrow_right),
|
|
);
|
|
}
|
|
}
|