mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2026-01-31 13:25:10 +01:00
fix: cloud sync (#769)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: [
|
||||
|
||||
Reference in New Issue
Block a user