mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 07:14:28 +01:00
@@ -29,7 +29,7 @@ Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/rel
|
|||||||
|
|
||||||
|
|
||||||
## 🔖 Feature
|
## 🔖 Feature
|
||||||
- `Status chart` (CPU, Sensors, GPU...), `SSH` Term, `SFTP`, `Docker & Pkg & Process`...
|
- `Status chart` (CPU, Sensors, GPU...), `SSH` Term, `SFTP`, `Docker & Process`...
|
||||||
- Platform specific: `Bio auth`、`Msg push`、`Home widget`、`watchOS App`...
|
- Platform specific: `Bio auth`、`Msg push`、`Home widget`、`watchOS App`...
|
||||||
- English, 简体中文; Deutsch [@its-tom](https://github.com/its-tom), 繁體中文 [@kalashnikov](https://github.com/kalashnikov), Indonesian [@azkadev](https://github.com/azkadev), Français [@FrancXPT](https://github.com/FrancXPT), Dutch [@QazCetelic](https://github.com/QazCetelic); Español, Русский язык, Português, 日本語 (Generated by GPT)
|
- English, 简体中文; Deutsch [@its-tom](https://github.com/its-tom), 繁體中文 [@kalashnikov](https://github.com/kalashnikov), Indonesian [@azkadev](https://github.com/azkadev), Français [@FrancXPT](https://github.com/FrancXPT), Dutch [@QazCetelic](https://github.com/QazCetelic); Español, Русский язык, Português, 日本語 (Generated by GPT)
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/rel
|
|||||||
|
|
||||||
|
|
||||||
## 🔖 特点
|
## 🔖 特点
|
||||||
- `状态图表`(CPU、传感器、GPU 等), `SSH` 终端, `SFTP`, `Docker & 包 & 进程` 管理器...
|
- `状态图表`(CPU、传感器、GPU 等), `SSH` 终端, `SFTP`, `Docker & 进程` 管理...
|
||||||
- 特殊支持:`生物认证`、`推送`、`桌面小部件`、`watchOS App`、`跟随系统颜色`...
|
- 特殊支持:`生物认证`、`推送`、`桌面小部件`、`watchOS App`、`跟随系统颜色`...
|
||||||
- 本地化
|
- 本地化
|
||||||
- English, 简体中文
|
- English, 简体中文
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ enum ServerFuncBtn {
|
|||||||
container,
|
container,
|
||||||
@HiveField(3)
|
@HiveField(3)
|
||||||
process,
|
process,
|
||||||
@HiveField(4)
|
//@HiveField(4)
|
||||||
pkg,
|
//pkg,
|
||||||
@HiveField(5)
|
@HiveField(5)
|
||||||
snippet,
|
snippet,
|
||||||
@HiveField(6)
|
@HiveField(6)
|
||||||
@@ -30,14 +30,14 @@ enum ServerFuncBtn {
|
|||||||
sftp,
|
sftp,
|
||||||
container,
|
container,
|
||||||
process,
|
process,
|
||||||
pkg,
|
//pkg,
|
||||||
snippet,
|
snippet,
|
||||||
].map((e) => e.index).toList();
|
].map((e) => e.index).toList();
|
||||||
|
|
||||||
IconData get icon => switch (this) {
|
IconData get icon => switch (this) {
|
||||||
sftp => Icons.insert_drive_file,
|
sftp => Icons.insert_drive_file,
|
||||||
snippet => Icons.code,
|
snippet => Icons.code,
|
||||||
pkg => Icons.system_security_update,
|
//pkg => Icons.system_security_update,
|
||||||
container => FontAwesome.docker_brand,
|
container => FontAwesome.docker_brand,
|
||||||
process => Icons.list_alt_outlined,
|
process => Icons.list_alt_outlined,
|
||||||
terminal => Icons.terminal,
|
terminal => Icons.terminal,
|
||||||
@@ -47,7 +47,7 @@ enum ServerFuncBtn {
|
|||||||
String get toStr => switch (this) {
|
String get toStr => switch (this) {
|
||||||
sftp => 'SFTP',
|
sftp => 'SFTP',
|
||||||
snippet => l10n.snippet,
|
snippet => l10n.snippet,
|
||||||
pkg => l10n.pkg,
|
//pkg => l10n.pkg,
|
||||||
container => l10n.container,
|
container => l10n.container,
|
||||||
process => l10n.process,
|
process => l10n.process,
|
||||||
terminal => l10n.terminal,
|
terminal => l10n.terminal,
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ class ServerFuncBtnAdapter extends TypeAdapter<ServerFuncBtn> {
|
|||||||
return ServerFuncBtn.container;
|
return ServerFuncBtn.container;
|
||||||
case 3:
|
case 3:
|
||||||
return ServerFuncBtn.process;
|
return ServerFuncBtn.process;
|
||||||
case 4:
|
|
||||||
return ServerFuncBtn.pkg;
|
|
||||||
case 5:
|
case 5:
|
||||||
return ServerFuncBtn.snippet;
|
return ServerFuncBtn.snippet;
|
||||||
case 6:
|
case 6:
|
||||||
@@ -47,9 +45,6 @@ class ServerFuncBtnAdapter extends TypeAdapter<ServerFuncBtn> {
|
|||||||
case ServerFuncBtn.process:
|
case ServerFuncBtn.process:
|
||||||
writer.writeByte(3);
|
writer.writeByte(3);
|
||||||
break;
|
break;
|
||||||
case ServerFuncBtn.pkg:
|
|
||||||
writer.writeByte(4);
|
|
||||||
break;
|
|
||||||
case ServerFuncBtn.snippet:
|
case ServerFuncBtn.snippet:
|
||||||
writer.writeByte(5);
|
writer.writeByte(5);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -170,15 +170,6 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
|||||||
hint: 'root',
|
hint: 'root',
|
||||||
suggestion: false,
|
suggestion: false,
|
||||||
),
|
),
|
||||||
Input(
|
|
||||||
controller: _altUrlController,
|
|
||||||
type: TextInputType.url,
|
|
||||||
node: _alterUrlFocus,
|
|
||||||
label: l10n.fallbackSshDest,
|
|
||||||
icon: MingCute.link_line,
|
|
||||||
hint: 'user@ip:port',
|
|
||||||
suggestion: false,
|
|
||||||
),
|
|
||||||
TagEditor(
|
TagEditor(
|
||||||
tags: _tags,
|
tags: _tags,
|
||||||
onChanged: (p0) => _tags = p0,
|
onChanged: (p0) => _tags = p0,
|
||||||
@@ -328,19 +319,16 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
|||||||
return ExpandTile(
|
return ExpandTile(
|
||||||
title: Text(l10n.more),
|
title: Text(l10n.more),
|
||||||
children: [
|
children: [
|
||||||
const Text('Logo', style: UIs.text13Grey),
|
|
||||||
UIs.height7,
|
UIs.height7,
|
||||||
Input(
|
Input(
|
||||||
controller: _logoUrlCtrl,
|
controller: _logoUrlCtrl,
|
||||||
type: TextInputType.url,
|
type: TextInputType.url,
|
||||||
icon: Icons.image,
|
icon: Icons.image,
|
||||||
label: 'URL',
|
label: 'Logo URL',
|
||||||
hint: 'https://example.com/logo.png',
|
hint: 'https://example.com/logo.png',
|
||||||
suggestion: false,
|
suggestion: false,
|
||||||
),
|
),
|
||||||
UIs.height7,
|
_buildAltUrl(),
|
||||||
Text(l10n.envVars, style: UIs.text13Grey),
|
|
||||||
UIs.height7,
|
|
||||||
_buildEnvs(),
|
_buildEnvs(),
|
||||||
UIs.height7,
|
UIs.height7,
|
||||||
..._buildPVEs(),
|
..._buildPVEs(),
|
||||||
@@ -363,6 +351,18 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildAltUrl() {
|
||||||
|
return Input(
|
||||||
|
controller: _altUrlController,
|
||||||
|
type: TextInputType.url,
|
||||||
|
node: _alterUrlFocus,
|
||||||
|
label: l10n.fallbackSshDest,
|
||||||
|
icon: MingCute.link_line,
|
||||||
|
hint: 'user@ip:port',
|
||||||
|
suggestion: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
List<Widget> _buildPVEs() {
|
List<Widget> _buildPVEs() {
|
||||||
const addr = 'https://127.0.0.1:8006';
|
const addr = 'https://127.0.0.1:8006';
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ class ServerPage extends StatefulWidget {
|
|||||||
State<ServerPage> createState() => _ServerPageState();
|
State<ServerPage> createState() => _ServerPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const _cardPad = 74.0;
|
||||||
|
const _cardPadSingle = 13.0;
|
||||||
|
|
||||||
class _ServerPageState extends State<ServerPage>
|
class _ServerPageState extends State<ServerPage>
|
||||||
with AutomaticKeepAliveClientMixin, AfterLayoutMixin {
|
with AutomaticKeepAliveClientMixin, AfterLayoutMixin {
|
||||||
late MediaQueryData _media;
|
late MediaQueryData _media;
|
||||||
@@ -97,7 +100,7 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
),
|
),
|
||||||
floatingActionButton: AutoHide(
|
floatingActionButton: AutoHide(
|
||||||
key: _autoHideKey,
|
key: _autoHideKey,
|
||||||
direction: AxisDirection.down,
|
direction: AxisDirection.right,
|
||||||
offset: 75,
|
offset: 75,
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
child: FloatingActionButton(
|
child: FloatingActionButton(
|
||||||
@@ -289,8 +292,12 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding:
|
padding: const EdgeInsets.only(
|
||||||
const EdgeInsets.only(left: 13, right: 3, top: 13, bottom: 13),
|
left: _cardPadSingle,
|
||||||
|
right: 3,
|
||||||
|
top: _cardPadSingle,
|
||||||
|
bottom: _cardPadSingle,
|
||||||
|
),
|
||||||
child: _buildRealServerCard(srv),
|
child: _buildRealServerCard(srv),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -300,7 +307,7 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
/// The child's width mat not equal to 1/4 of the screen width,
|
/// The child's width mat not equal to 1/4 of the screen width,
|
||||||
/// so we need to wrap it with a SizedBox.
|
/// so we need to wrap it with a SizedBox.
|
||||||
Widget _wrapWithSizedbox(Widget child, [bool circle = false]) {
|
Widget _wrapWithSizedbox(Widget child, [bool circle = false]) {
|
||||||
var width = (_media.size.width - 74) / (circle ? 4 : 4.3);
|
var width = (_media.size.width - _cardPad) / (circle ? 4 : 4.3);
|
||||||
if (_useDoubleColumn) width /= 2;
|
if (_useDoubleColumn) width /= 2;
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: width,
|
width: width,
|
||||||
@@ -341,11 +348,7 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _buildFlippedCard(Server srv) {
|
List<Widget> _buildFlippedCard(Server srv) {
|
||||||
return [
|
final children = [
|
||||||
UIs.height13,
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
||||||
children: [
|
|
||||||
IconTextBtn(
|
IconTextBtn(
|
||||||
onPressed: () => _askFor(
|
onPressed: () => _askFor(
|
||||||
func: () async {
|
func: () async {
|
||||||
@@ -399,8 +402,18 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
icon: Icons.edit,
|
icon: Icons.edit,
|
||||||
text: l10n.edit,
|
text: l10n.edit,
|
||||||
)
|
)
|
||||||
],
|
];
|
||||||
)
|
|
||||||
|
final width = (_media.size.width - _cardPad) / children.length;
|
||||||
|
return [
|
||||||
|
UIs.height13,
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
children: children.map((e) {
|
||||||
|
if (width == 0) return e;
|
||||||
|
return SizedBox(width: width, child: e);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class _ServerDetailOrderPageState extends State<ServerFuncBtnsOrderPage> {
|
|||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
children: [
|
children: [
|
||||||
WidgetSpan(child: Icon(funcBtn.icon)),
|
WidgetSpan(child: Icon(funcBtn.icon)),
|
||||||
const WidgetSpan(child: UIs.width7),
|
const WidgetSpan(child: UIs.width13),
|
||||||
TextSpan(text: funcBtn.toStr, style: UIs.textGrey),
|
TextSpan(text: funcBtn.toStr, style: UIs.textGrey),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -3,19 +3,14 @@ import 'dart:io';
|
|||||||
import 'package:fl_lib/fl_lib.dart';
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:server_box/core/extension/context/locale.dart';
|
import 'package:server_box/core/extension/context/locale.dart';
|
||||||
import 'package:server_box/core/extension/ssh_client.dart';
|
|
||||||
import 'package:server_box/data/model/app/menu/base.dart';
|
import 'package:server_box/data/model/app/menu/base.dart';
|
||||||
import 'package:server_box/data/model/app/menu/server_func.dart';
|
import 'package:server_box/data/model/app/menu/server_func.dart';
|
||||||
import 'package:server_box/data/model/app/shell_func.dart';
|
|
||||||
import 'package:server_box/data/model/pkg/manager.dart';
|
|
||||||
import 'package:server_box/data/model/server/dist.dart';
|
|
||||||
import 'package:server_box/data/model/server/snippet.dart';
|
import 'package:server_box/data/model/server/snippet.dart';
|
||||||
import 'package:server_box/data/res/provider.dart';
|
import 'package:server_box/data/res/provider.dart';
|
||||||
import 'package:server_box/data/res/store.dart';
|
import 'package:server_box/data/res/store.dart';
|
||||||
|
|
||||||
import '../../core/route.dart';
|
import '../../core/route.dart';
|
||||||
import '../../core/utils/server.dart';
|
import '../../core/utils/server.dart';
|
||||||
import '../../data/model/pkg/upgrade_info.dart';
|
|
||||||
import '../../data/model/server/server_private_info.dart';
|
import '../../data/model/server/server_private_info.dart';
|
||||||
|
|
||||||
class ServerFuncBtnsTopRight extends StatelessWidget {
|
class ServerFuncBtnsTopRight extends StatelessWidget {
|
||||||
@@ -98,9 +93,9 @@ void _onTapMoreBtns(
|
|||||||
BuildContext context,
|
BuildContext context,
|
||||||
) async {
|
) async {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case ServerFuncBtn.pkg:
|
// case ServerFuncBtn.pkg:
|
||||||
_onPkg(context, spi);
|
// _onPkg(context, spi);
|
||||||
break;
|
// break;
|
||||||
case ServerFuncBtn.sftp:
|
case ServerFuncBtn.sftp:
|
||||||
AppRoutes.sftp(spi: spi).checkGo(
|
AppRoutes.sftp(spi: spi).checkGo(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -225,86 +220,86 @@ bool _checkClient(BuildContext context, String id) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onPkg(BuildContext context, ServerPrivateInfo spi) async {
|
// Future<void> _onPkg(BuildContext context, ServerPrivateInfo spi) async {
|
||||||
final server = spi.server;
|
// final server = spi.server;
|
||||||
final client = server?.client;
|
// final client = server?.client;
|
||||||
if (server == null || client == null) {
|
// if (server == null || client == null) {
|
||||||
context.showSnackBar(l10n.noClient);
|
// context.showSnackBar(l10n.noClient);
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
final sys = server.status.more[StatusCmdType.sys];
|
// final sys = server.status.more[StatusCmdType.sys];
|
||||||
if (sys == null) {
|
// if (sys == null) {
|
||||||
context.showSnackBar(l10n.noResult);
|
// context.showSnackBar(l10n.noResult);
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
final pkg = PkgManager.fromDist(sys.dist);
|
// final pkg = PkgManager.fromDist(sys.dist);
|
||||||
if (pkg == null) {
|
// if (pkg == null) {
|
||||||
context.showSnackBar('Unsupported dist: $sys');
|
// context.showSnackBar('Unsupported dist: $sys');
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Update pkg list
|
// // Update pkg list
|
||||||
final suc = await context.showLoadingDialog(
|
// final suc = await context.showLoadingDialog(
|
||||||
fn: () async {
|
// fn: () async {
|
||||||
final updateCmd = pkg.update;
|
// final updateCmd = pkg.update;
|
||||||
if (updateCmd != null) {
|
// if (updateCmd != null) {
|
||||||
await client.execWithPwd(
|
// await client.execWithPwd(
|
||||||
updateCmd,
|
// updateCmd,
|
||||||
context: context,
|
// context: context,
|
||||||
id: spi.id,
|
// id: spi.id,
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
},
|
// },
|
||||||
barrierDismiss: true,
|
// barrierDismiss: true,
|
||||||
);
|
// );
|
||||||
if (suc != true) return;
|
// if (suc != true) return;
|
||||||
|
|
||||||
final listCmd = pkg.listUpdate;
|
// final listCmd = pkg.listUpdate;
|
||||||
if (listCmd == null) {
|
// if (listCmd == null) {
|
||||||
context.showSnackBar('Unsupported dist: $sys');
|
// context.showSnackBar('Unsupported dist: $sys');
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Get upgrade list
|
// // Get upgrade list
|
||||||
final result = await context.showLoadingDialog(
|
// final result = await context.showLoadingDialog(
|
||||||
fn: () => client.run(listCmd).string,
|
// fn: () => client.run(listCmd).string,
|
||||||
);
|
// );
|
||||||
if (result == null || result.isEmpty) {
|
// if (result == null || result.isEmpty) {
|
||||||
context.showSnackBar(l10n.noResult);
|
// context.showSnackBar(l10n.noResult);
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
final list = pkg.updateListRemoveUnused(result.split('\n'));
|
// final list = pkg.updateListRemoveUnused(result.split('\n'));
|
||||||
final upgradeable = list.map((e) => UpgradePkgInfo(e, pkg)).toList();
|
// final upgradeable = list.map((e) => UpgradePkgInfo(e, pkg)).toList();
|
||||||
if (upgradeable.isEmpty) {
|
// if (upgradeable.isEmpty) {
|
||||||
context.showSnackBar(l10n.noUpdateAvailable);
|
// context.showSnackBar(l10n.noUpdateAvailable);
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
final args = upgradeable.map((e) => e.package).join(' ');
|
// final args = upgradeable.map((e) => e.package).join(' ');
|
||||||
final isSU = server.spi.user == 'root';
|
// final isSU = server.spi.user == 'root';
|
||||||
final upgradeCmd = isSU ? pkg.upgrade(args) : 'sudo ${pkg.upgrade(args)}';
|
// final upgradeCmd = isSU ? pkg.upgrade(args) : 'sudo ${pkg.upgrade(args)}';
|
||||||
|
|
||||||
// Confirm upgrade
|
// // Confirm upgrade
|
||||||
final gotoUpgrade = await context.showRoundDialog<bool>(
|
// final gotoUpgrade = await context.showRoundDialog<bool>(
|
||||||
title: l10n.attention,
|
// title: l10n.attention,
|
||||||
child: SingleChildScrollView(
|
// child: SingleChildScrollView(
|
||||||
child: Text(
|
// child: Text(
|
||||||
'${l10n.pkgUpgradeTip}\n${l10n.foundNUpdate(upgradeable.length)}\n\n$upgradeCmd'),
|
// '${l10n.pkgUpgradeTip}\n${l10n.foundNUpdate(upgradeable.length)}\n\n$upgradeCmd'),
|
||||||
),
|
// ),
|
||||||
actions: [
|
// actions: [
|
||||||
CountDownBtn(
|
// CountDownBtn(
|
||||||
onTap: () => context.pop(true),
|
// onTap: () => context.pop(true),
|
||||||
text: l10n.update,
|
// text: l10n.update,
|
||||||
afterColor: Colors.red,
|
// afterColor: Colors.red,
|
||||||
),
|
// ),
|
||||||
],
|
// ],
|
||||||
);
|
// );
|
||||||
|
|
||||||
if (gotoUpgrade != true) return;
|
// if (gotoUpgrade != true) return;
|
||||||
|
|
||||||
AppRoutes.ssh(spi: spi, initCmd: upgradeCmd).checkGo(
|
// AppRoutes.ssh(spi: spi, initCmd: upgradeCmd).checkGo(
|
||||||
context: context,
|
// context: context,
|
||||||
check: () => _checkClient(context, spi.id),
|
// check: () => _checkClient(context, spi.id),
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|||||||
Reference in New Issue
Block a user