fix: cloud sync (#769)

This commit is contained in:
lollipopkit🏳️‍⚧️
2025-06-04 00:11:31 +08:00
committed by GitHub
parent 9547d92ac5
commit 0c1ada0067
70 changed files with 2348 additions and 1906 deletions

View File

@@ -6,7 +6,8 @@ import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/core/sync.dart';
import 'package:server_box/data/model/app/backup.dart';
import 'package:server_box/data/model/app/bak/backup2.dart';
import 'package:server_box/data/model/app/bak/utils.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/provider/snippet.dart';
@@ -27,14 +28,11 @@ class BackupPage extends StatefulWidget {
);
}
final class _BackupPageState extends State<BackupPage>
with AutomaticKeepAliveClientMixin {
final icloudLoading = false.vn;
final class _BackupPageState extends State<BackupPage> with AutomaticKeepAliveClientMixin {
final webdavLoading = false.vn;
@override
void dispose() {
icloudLoading.dispose();
webdavLoading.dispose();
super.dispose();
}
@@ -89,7 +87,7 @@ final class _BackupPageState extends State<BackupPage>
title: Text(libL10n.backup),
trailing: const Icon(Icons.save),
onTap: () async {
final path = await Backup.backup();
final path = await BackupV2.backup();
await Pfs.sharePaths(paths: [path]);
},
),
@@ -110,19 +108,15 @@ final class _BackupPageState extends State<BackupPage>
title: const Text('iCloud'),
trailing: StoreSwitch(
prop: PrefProps.icloudSync,
validator: (p0) {
validator: (p0) async {
if (p0 && PrefProps.webdavSync.get()) {
context.showSnackBar(l10n.autoBackupConflict);
return false;
}
return true;
},
callback: (val) async {
if (val) {
icloudLoading.value = true;
if (p0) {
await bakSync.sync(rs: icloud);
icloudLoading.value = false;
}
return true;
},
),
),
@@ -145,7 +139,11 @@ final class _BackupPageState extends State<BackupPage>
title: Text(libL10n.auto),
trailing: StoreSwitch(
prop: PrefProps.webdavSync,
validator: (p0) {
validator: (p0) async {
if (p0 && PrefProps.icloudSync.get()) {
context.showSnackBar(l10n.autoBackupConflict);
return false;
}
if (p0) {
final url = PrefProps.webdavUrl.get();
final user = PrefProps.webdavUser.get();
@@ -162,28 +160,20 @@ final class _BackupPageState extends State<BackupPage>
context.showSnackBar(l10n.webdavSettingEmpty);
return false;
}
}
if (PrefProps.icloudSync.get()) {
context.showSnackBar(l10n.autoBackupConflict);
return false;
}
return true;
},
callback: (val) async {
if (val) {
webdavLoading.value = true;
await bakSync.sync(rs: Webdav.shared);
webdavLoading.value = false;
}
return true;
},
),
),
ListTile(
title: Text(l10n.manual),
trailing: ListenableBuilder(
listenable: webdavLoading,
builder: (_, __) {
if (webdavLoading.value) return SizedLoading.small;
trailing: webdavLoading.listenVal(
(loading) {
if (loading) return SizedLoading.small;
return Row(
mainAxisSize: MainAxisSize.min,
@@ -217,7 +207,7 @@ final class _BackupPageState extends State<BackupPage>
title: Text(libL10n.backup),
trailing: const Icon(Icons.save),
onTap: () async {
final path = await Backup.backup();
final path = await BackupV2.backup();
Pfs.copy(await File(path).readAsString());
context.showSnackBar(libL10n.success);
},
@@ -310,22 +300,18 @@ final class _BackupPageState extends State<BackupPage>
try {
final (backup, err) = await context.showLoadingDialog(
fn: () => Computer.shared.start(Backup.fromJsonString, text.trim()),
fn: () => Computer.shared.start(MergeableUtils.fromJsonString, text.trim()),
);
if (err != null || backup == null) return;
if (backupFormatVersion != backup.version) {
context.showSnackBar(l10n.backupVersionNotMatch);
return;
}
await context.showRoundDialog(
title: libL10n.restore,
child: Text(libL10n.askContinue(
'${libL10n.restore} ${libL10n.backup}(${backup.date})',
'${libL10n.restore} ${libL10n.backup}(${backup.$2})',
)),
actions: Btn.ok(
onTap: () async {
await backup.merge(force: true);
await backup.$1.merge(force: true);
context.pop();
},
).toList,
@@ -350,7 +336,7 @@ final class _BackupPageState extends State<BackupPage>
await Webdav.shared.download(relativePath: fileName);
final dlFile = await File('${Paths.doc}/$fileName').readAsString();
final dlBak = await Computer.shared.start(Backup.fromJsonString, dlFile);
final dlBak = await Computer.shared.start(BackupV2.fromJsonString, dlFile);
await dlBak.merge(force: true);
} catch (e, s) {
context.showErrDialog(e, s, libL10n.restore);
@@ -365,7 +351,7 @@ final class _BackupPageState extends State<BackupPage>
final date = DateTime.now().ymdhms(ymdSep: '-', hmsSep: '-', sep: '-');
final bakName = '$date-${Miscs.bakFileName}';
try {
await Backup.backup(bakName);
await BackupV2.backup(bakName);
await Webdav.shared.upload(relativePath: bakName);
Loggers.app.info('Upload webdav backup success');
} catch (e, s) {
@@ -421,8 +407,7 @@ final class _BackupPageState extends State<BackupPage>
await Webdav.test(url_, user_, pwd_);
context.showSnackBar(libL10n.success);
Webdav.shared.client =
WebdavClient.basicAuth(url: url_, user: user_, pwd: pwd_);
Webdav.shared.client = WebdavClient.basicAuth(url: url_, user: user_, pwd: pwd_);
PrefProps.webdavUrl.set(url_);
PrefProps.webdavUser.set(user_);
PrefProps.webdavPwd.set(pwd_);
@@ -441,23 +426,18 @@ final class _BackupPageState extends State<BackupPage>
try {
final (backup, err) = await context.showLoadingDialog(
fn: () => Computer.shared.start(Backup.fromJsonString, text.trim()),
fn: () => Computer.shared.start(MergeableUtils.fromJsonString, text.trim()),
);
if (err != null || backup == null) return;
if (backupFormatVersion != backup.version) {
context.showSnackBar(l10n.backupVersionNotMatch);
return;
}
await context.showRoundDialog(
title: libL10n.restore,
child: Text(libL10n.askContinue(
'${libL10n.restore} ${libL10n.backup}(${backup.date})',
'${libL10n.restore} ${libL10n.backup}(${backup.$2})',
)),
actions: Btn.ok(
onTap: () async {
await backup.merge(force: true);
await backup.$1.merge(force: true);
context.pop();
},
).toList,

View File

@@ -7,29 +7,27 @@ extension on _ServerPageState {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ConstrainedBox(
constraints: BoxConstraints(maxWidth: _media.size.width / 2.3),
child: Hero(
tag: 'home_card_title_${s.spi.id}',
transitionOnUserGestures: true,
child: Material(
color: Colors.transparent,
child: Text(
s.spi.name,
style: UIs.text13Bold.copyWith(
color: context.isDark ? Colors.white : Colors.black,
LayoutBuilder(
builder: (_, cons) {
return ConstrainedBox(
constraints: BoxConstraints(maxWidth: cons.maxWidth / 2.3),
child: Hero(
tag: 'home_card_title_${s.spi.id}',
transitionOnUserGestures: true,
child: Material(
color: Colors.transparent,
child: Text(
s.spi.name,
style: UIs.text13Bold.copyWith(color: context.isDark ? Colors.white : Colors.black),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
),
const Icon(
Icons.keyboard_arrow_right,
size: 17,
color: Colors.grey,
);
},
),
const Icon(Icons.keyboard_arrow_right, size: 17, color: Colors.grey),
const Spacer(),
_buildTopRightText(s),
_buildTopRightWidget(s),
@@ -41,31 +39,31 @@ extension on _ServerPageState {
Widget _buildTopRightWidget(Server s) {
final (child, onTap) = switch (s.conn) {
ServerConn.connecting || ServerConn.loading || ServerConn.connected => (
SizedBox(
width: 19,
height: 19,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation(UIs.primaryColor),
),
SizedBox(
width: 19,
height: 19,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation(UIs.primaryColor),
),
null,
),
null,
),
ServerConn.failed => (
const Icon(Icons.refresh, size: 21, color: Colors.grey),
() {
TryLimiter.reset(s.spi.id);
ServerProvider.refresh(spi: s.spi);
},
),
const Icon(Icons.refresh, size: 21, color: Colors.grey),
() {
TryLimiter.reset(s.spi.id);
ServerProvider.refresh(spi: s.spi);
},
),
ServerConn.disconnected => (
const Icon(MingCute.link_3_line, size: 19, color: Colors.grey),
() => ServerProvider.refresh(spi: s.spi)
),
const Icon(MingCute.link_3_line, size: 19, color: Colors.grey),
() => ServerProvider.refresh(spi: s.spi),
),
ServerConn.finished => (
const Icon(MingCute.unlink_2_line, size: 17, color: Colors.grey),
() => ServerProvider.closeServer(id: s.spi.id),
),
const Icon(MingCute.unlink_2_line, size: 17, color: Colors.grey),
() => ServerProvider.closeServer(id: s.spi.id),
),
};
// Or the loading icon will be rescaled.
@@ -73,11 +71,7 @@ extension on _ServerPageState {
? child
: SizedBox(height: _ServerPageState._kCardHeightMin, width: 27, child: child);
if (onTap == null) return wrapped.paddingOnly(left: 10);
return InkWell(
borderRadius: BorderRadius.circular(7),
onTap: onTap,
child: wrapped,
).paddingOnly(left: 5);
return InkWell(borderRadius: BorderRadius.circular(7), onTap: onTap, child: wrapped).paddingOnly(left: 5);
}
Widget _buildTopRightText(Server s) {
@@ -94,7 +88,8 @@ extension on _ServerPageState {
}
void _showFailReason(ServerStatus ss) {
final md = '''
final md =
'''
${ss.err?.solution ?? l10n.unknown}
```sh
@@ -103,12 +98,7 @@ ${ss.err?.message ?? 'null'}
context.showRoundDialog(
title: libL10n.error,
child: SingleChildScrollView(child: SimpleMarkdown(data: md)),
actions: [
TextButton(
onPressed: () => Pfs.copy(md),
child: Text(libL10n.copy),
)
],
actions: [TextButton(onPressed: () => Pfs.copy(md), child: Text(libL10n.copy))],
);
}
@@ -156,13 +146,7 @@ ${ss.err?.message ?? 'null'}
);
}
Widget _buildIOData(
String up,
String down, {
void Function()? onTap,
Key? key,
int maxLines = 2
}) {
Widget _buildIOData(String up, String down, {void Function()? onTap, Key? key, int maxLines = 2}) {
final child = Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
@@ -181,7 +165,7 @@ ${ss.err?.message ?? 'null'}
textAlign: TextAlign.center,
textScaler: _textFactor,
maxLines: maxLines,
)
),
],
);
if (onTap == null) return child;

View File

@@ -28,9 +28,7 @@ extension on _ServerPageState {
Widget _buildLandscapeBody() {
return ServerProvider.serverOrder.listenVal((order) {
if (order.isEmpty) {
return Center(
child: Text(libL10n.empty, textAlign: TextAlign.center),
);
return Center(child: Text(libL10n.empty, textAlign: TextAlign.center));
}
return PageView.builder(
@@ -42,24 +40,18 @@ extension on _ServerPageState {
return srv.listenVal((srv) {
final title = _buildServerCardTitle(srv);
final List<Widget> children = [
title,
_buildNormalCard(srv.status, srv.spi),
];
final List<Widget> children = [title, _buildNormalCard(srv.status, srv.spi)];
return Padding(
padding: _media.padding,
child: ListenableBuilder(
listenable: _getCardNoti(id),
builder: (_, __) {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: children,
);
},
),
return ListenableBuilder(
listenable: _getCardNoti(id),
builder: (_, __) {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: children,
);
},
);
});
},

View File

@@ -37,18 +37,13 @@ class ServerPage extends StatefulWidget {
@override
State<ServerPage> createState() => _ServerPageState();
static const route = AppRouteNoArg(
page: ServerPage.new,
path: '/servers',
);
static const route = AppRouteNoArg(page: ServerPage.new, path: '/servers');
}
const _cardPad = 74.0;
const _cardPadSingle = 13.0;
class _ServerPageState extends State<ServerPage> with AutomaticKeepAliveClientMixin, AfterLayoutMixin {
late MediaQueryData _media;
late double _textFactorDouble;
double _offset = 1;
late TextScaler _textFactor;
@@ -80,7 +75,6 @@ class _ServerPageState extends State<ServerPage> with AutomaticKeepAliveClientMi
@override
void didChangeDependencies() {
super.didChangeDependencies();
_media = MediaQuery.of(context);
_updateOffset();
_updateTextScaler();
}
@@ -88,24 +82,22 @@ class _ServerPageState extends State<ServerPage> with AutomaticKeepAliveClientMi
@override
Widget build(BuildContext context) {
super.build(context);
return OrientationBuilder(builder: (_, orientation) {
if (orientation == Orientation.landscape) {
final useFullScreen = Stores.setting.fullScreen.fetch();
// Only enter landscape mode when the screen is wide enough and the
// full screen mode is enabled.
if (useFullScreen) return _buildLandscape();
}
return _buildPortrait();
});
return OrientationBuilder(
builder: (_, orientation) {
if (orientation == Orientation.landscape) {
final useFullScreen = Stores.setting.fullScreen.fetch();
// Only enter landscape mode when the screen is wide enough and the
// full screen mode is enabled.
if (useFullScreen) return _buildLandscape();
}
return _buildPortrait();
},
);
}
Widget _buildScaffold(Widget child) {
return Scaffold(
appBar: _TopBar(
tags: ServerProvider.tags,
onTagChanged: (p0) => _tag.value = p0,
initTag: _tag.value,
),
appBar: _TopBar(tags: ServerProvider.tags, onTagChanged: (p0) => _tag.value = p0, initTag: _tag.value),
body: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => _autoHideCtrl.show(),
@@ -134,27 +126,23 @@ 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;
// }
return ServerProvider.serverOrder.listenVal((order) {
return _tag.listenVal((val) {
final filtered = _filterServers(order);
final child = _buildScaffold(_buildBodySmall(filtered: filtered));
// if (isMobile) {
return child;
// }
// return SplitView(
// controller: _splitViewCtrl,
// leftWeight: 1,
// rightWeight: 1.3,
// initialRight: Center(child: CircularProgressIndicator()),
// leftBuilder: (_, __) => child,
// );
},
);
},
);
// return SplitView(
// controller: _splitViewCtrl,
// leftWeight: 1,
// rightWeight: 1.3,
// initialRight: Center(child: CircularProgressIndicator()),
// leftBuilder: (_, __) => child,
// );
});
});
}
Widget _buildBodySmall({
@@ -165,34 +153,38 @@ class _ServerPageState extends State<ServerPage> with AutomaticKeepAliveClientMi
return Center(child: Text(libL10n.empty, textAlign: TextAlign.center));
}
// Calculate number of columns based on available width
final columnsCount = math.max(1, (_media.size.width / UIs.columnWidth).floor());
return LayoutBuilder(
builder: (_, cons) {
// Calculate number of columns based on available width
final columnsCount = math.max(1, (cons.maxWidth / UIs.columnWidth).floor());
// Calculate number of rows needed
final rowCount = (filtered.length + columnsCount - 1) ~/ columnsCount;
return ListView.builder(
controller: _scrollController,
padding: padding,
itemCount: rowCount + 1, // +1 for the bottom space
itemBuilder: (_, rowIndex) {
// Bottom space
if (rowIndex == rowCount) return UIs.height77;
// Create a row of server cards
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: List.generate(columnsCount, (colIndex) {
final index = rowIndex * columnsCount + colIndex;
if (index >= filtered.length) return Expanded(child: Container());
final vnode = ServerProvider.pick(id: filtered[index]);
if (vnode == null) return Expanded(child: UIs.placeholder);
// Calculate which servers belong in this column
final serversInThisColumn = <String>[];
for (int i = colIndex; i < filtered.length; i += columnsCount) {
serversInThisColumn.add(filtered[i]);
}
final lens = serversInThisColumn.length;
return Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: vnode.listenVal(_buildEachServerCard),
child: ListView.builder(
controller: colIndex == 0 ? _scrollController : null,
padding: padding,
itemCount: lens + 1, // Add 1 for bottom spacing
itemBuilder: (context, index) {
// 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;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 0),
child: vnode.listenVal(_buildEachServerCard),
);
},
),
);
}),
@@ -227,13 +219,12 @@ class _ServerPageState extends State<ServerPage> with AutomaticKeepAliveClientMi
/// The child's width mat not equal to 1/4 of the screen width,
/// so we need to wrap it with a SizedBox.
Widget _wrapWithSizedbox(Widget child, double maxWidth, [bool circle = false]) {
return LayoutBuilder(builder: (_, cons) {
final width = (maxWidth - _cardPad) / 4;
return SizedBox(
width: width,
child: child,
);
});
return LayoutBuilder(
builder: (_, cons) {
final width = (maxWidth - _cardPad) / 4;
return SizedBox(width: width, child: child);
},
);
}
Widget _buildRealServerCard(Server srv) {
@@ -300,55 +291,52 @@ class _ServerPageState extends State<ServerPage> with AutomaticKeepAliveClientMi
icon: const Icon(Icons.edit, color: color),
text: libL10n.edit,
textStyle: textStyle,
)
),
];
return Padding(
padding: const EdgeInsets.only(top: 9),
child: LayoutBuilder(builder: (_, cons) {
final width = (cons.maxWidth - _cardPad) / children.length;
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: children.map((e) {
if (width == 0) return e;
return SizedBox(width: width, child: e);
}).toList(),
);
}),
child: LayoutBuilder(
builder: (_, cons) {
final width = (cons.maxWidth - _cardPad) / children.length;
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: children.map((e) {
if (width == 0) return e;
return SizedBox(width: width, child: e);
}).toList(),
);
},
),
);
}
Widget _buildNormalCard(ServerStatus ss, Spi spi) {
return LayoutBuilder(builder: (_, cons) {
final maxWidth = cons.maxWidth;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
UIs.height13,
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_wrapWithSizedbox(PercentCircle(percent: ss.cpu.usedPercent()), maxWidth, true),
_wrapWithSizedbox(
PercentCircle(percent: ss.mem.usedPercent * 100),
maxWidth,
true,
),
_wrapWithSizedbox(_buildNet(ss, spi.id), maxWidth),
_wrapWithSizedbox(_buildDisk(ss, spi.id), maxWidth),
],
),
UIs.height13,
if (Stores.setting.moveServerFuncs.fetch() &&
// Discussion #146
!Stores.setting.serverTabUseOldUI.fetch())
SizedBox(
height: 27,
child: ServerFuncBtns(spi: spi),
return LayoutBuilder(
builder: (_, cons) {
final maxWidth = cons.maxWidth;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
UIs.height13,
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_wrapWithSizedbox(PercentCircle(percent: ss.cpu.usedPercent()), maxWidth, true),
_wrapWithSizedbox(PercentCircle(percent: ss.mem.usedPercent * 100), maxWidth, true),
_wrapWithSizedbox(_buildNet(ss, spi.id), maxWidth),
_wrapWithSizedbox(_buildDisk(ss, spi.id), maxWidth),
],
),
],
);
});
UIs.height13,
if (Stores.setting.moveServerFuncs.fetch() &&
// Discussion #146
!Stores.setting.serverTabUseOldUI.fetch())
SizedBox(height: 27, child: ServerFuncBtns(spi: spi)),
],
);
},
);
}
@override

View File

@@ -164,7 +164,7 @@ extension _Utils on _ServerPageState {
void _updateOffset() {
if (!Stores.setting.fullScreenJitter.fetch()) return;
final x = _media.size.height * 0.03;
final x = MediaQuery.sizeOf(context).height * 0.03;
final r = math.Random().nextDouble();
final n = math.Random().nextBool() ? 1 : -1;
_offset = x * r * n;

View File

@@ -2,6 +2,7 @@ import 'dart:ui';
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.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';
@@ -35,17 +36,13 @@ class _ServerOrderPageState extends State<ServerOrderPage> {
final double scale = lerpDouble(1, 1.02, animValue)!;
return Transform.scale(
scale: scale,
// Create a Card based on the color and the content of the dragged one
// and set its elevation to the animated value.
child: Card(
elevation: elevation,
// color: cards[index].color,
// child: cards[index].child,
child: _buildCardTile(index),
child: child,
),
);
},
// child: child,
child: _buildCardTile(index),
);
}
@@ -57,28 +54,34 @@ class _ServerOrderPageState extends State<ServerOrderPage> {
}
return ReorderableListView.builder(
footer: const SizedBox(height: 77),
onReorder: (oldIndex, newIndex) => setState(() {
orders.value.move(
oldIndex,
newIndex,
property: Stores.setting.serverOrder,
);
orders.notify();
}),
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3),
onReorder: (oldIndex, newIndex) {
setState(() {
orders.value.move(
oldIndex,
newIndex,
property: Stores.setting.serverOrder,
);
});
},
padding: const EdgeInsets.all(8),
buildDefaultDragHandles: false,
itemBuilder: (_, idx) => _buildItem(idx),
itemBuilder: (_, idx) => _buildItem(idx, order[idx]),
itemCount: order.length,
proxyDecorator: _proxyDecorator,
);
});
}
Widget _buildItem(int index) {
Widget _buildItem(int index, String id) {
return ReorderableDelayedDragStartListener(
key: ValueKey('$index'),
key: ValueKey('server_item_$id'),
index: index,
child: CardX(child: _buildCardTile(index)),
child: Padding(
padding: const EdgeInsets.only(bottom: 8),
child: CardX(
child: _buildCardTile(index),
),
),
);
}
@@ -90,9 +93,14 @@ class _ServerOrderPageState extends State<ServerOrderPage> {
}
return ListTile(
title: Text(spi.name),
subtitle: Text(spi.id, style: UIs.textGrey),
title: Text(
spi.name,
style: const TextStyle(fontWeight: FontWeight.w500),
),
subtitle: Text(spi.oldId, style: UIs.textGrey),
leading: CircleAvatar(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
child: Text(spi.name[0]),
),
trailing: ReorderableDragStartListener(

View File

@@ -52,7 +52,7 @@ class ServerFuncBtns extends StatelessWidget {
if (btns.isEmpty) return UIs.placeholder;
return SizedBox(
height: 74,
height: 77,
child: ListView.builder(
itemCount: btns.length,
scrollDirection: Axis.horizontal,

View File

@@ -1,19 +1,19 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
final class RWX {
final class UnixPermOp {
final bool r;
final bool w;
final bool x;
const RWX({
const UnixPermOp({
required this.r,
required this.w,
required this.x,
});
RWX copyWith({bool? r, bool? w, bool? x}) {
return RWX(r: r ?? this.r, w: w ?? this.w, x: x ?? this.x);
UnixPermOp copyWith({bool? r, bool? w, bool? x}) {
return UnixPermOp(r: r ?? this.r, w: w ?? this.w, x: x ?? this.x);
}
int get value {
@@ -37,9 +37,9 @@ enum UnixPermScope {
}
final class UnixPerm {
final RWX user;
final RWX group;
final RWX other;
final UnixPermOp user;
final UnixPermOp group;
final UnixPermOp other;
const UnixPerm({
required this.user,
@@ -47,7 +47,7 @@ final class UnixPerm {
required this.other,
});
UnixPerm copyWith({RWX? user, RWX? group, RWX? other}) {
UnixPerm copyWith({UnixPermOp? user, UnixPermOp? group, UnixPermOp? other}) {
return UnixPerm(
user: user ?? this.user,
group: group ?? this.group,
@@ -55,7 +55,7 @@ final class UnixPerm {
);
}
UnixPerm copyWithScope(UnixPermScope scope, RWX rwx) {
UnixPerm copyWithScope(UnixPermScope scope, UnixPermOp rwx) {
switch (scope) {
case UnixPermScope.user:
return copyWith(user: rwx);
@@ -72,9 +72,9 @@ final class UnixPerm {
}
static UnixPerm get empty => const UnixPerm(
user: RWX(r: false, w: false, x: false),
group: RWX(r: false, w: false, x: false),
other: RWX(r: false, w: false, x: false),
user: UnixPermOp(r: false, w: false, x: false),
group: UnixPermOp(r: false, w: false, x: false),
other: UnixPermOp(r: false, w: false, x: false),
);
}
@@ -110,7 +110,7 @@ final class _UnixPermEditorState extends State<UnixPermEditor> {
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Text('Read'),
Text('Write'),
Text('Writ'), // Keep it short to fit UI
Text('Exec'),
],
).paddingOnly(left: 13),
@@ -122,7 +122,7 @@ final class _UnixPermEditorState extends State<UnixPermEditor> {
);
}
Widget _buildRow(UnixPermScope scope, RWX rwx) {
Widget _buildRow(UnixPermScope scope, UnixPermOp rwx) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [