feat: 下载时若存在数据丢失风险则弹窗提示;服务端增加占用信息查看功能

This commit is contained in:
2026-07-19 00:21:57 +08:00
parent 0aacef79e5
commit d53d6deb2e
5 changed files with 123 additions and 14 deletions
+73 -9
View File
@@ -104,13 +104,13 @@ class _AppConfigDialogState extends State<AppConfigDialog> {
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
FilledButton( ElevatedButton(
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: Text('取消'), child: Text('取消'),
), ),
ElevatedButton( FilledButton(
onPressed: () { onPressed: () {
var client = clientCtl.text.trim(); var client = clientCtl.text.trim();
var server = serverCtl.text.trim(); var server = serverCtl.text.trim();
@@ -223,12 +223,10 @@ class _AddSyncItemDialogState extends State<AddSyncItemDialog> {
children: [ children: [
IconButton( IconButton(
onPressed: () { onPressed: () {
FilePicker.pickFiles( FilePicker.pickFile().then(
allowMultiple: false,
).then(
(res) { (res) {
if (res?.files.firstOrNull?.path?.isNotEmpty == true) { if (res?.xFile.path?.isNotEmpty == true) {
pathCtl.text = res!.files.first.path!; pathCtl.text = res!.xFile.path!;
} }
}, },
); );
@@ -253,13 +251,13 @@ class _AddSyncItemDialogState extends State<AddSyncItemDialog> {
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
FilledButton( ElevatedButton(
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: Text('取消'), child: Text('取消'),
), ),
ElevatedButton( FilledButton(
onPressed: () { onPressed: () {
var name = nameCtl.text.trim(); var name = nameCtl.text.trim();
var path = pathCtl.text.trim(); var path = pathCtl.text.trim();
@@ -284,3 +282,69 @@ class _AddSyncItemDialogState extends State<AddSyncItemDialog> {
); );
} }
} }
class UploadConfirmDialog extends StatelessWidget {
const UploadConfirmDialog({super.key});
static Future<bool?> show(BuildContext context, [SyncItemConfig? config]) {
return showDialog(
context: context,
builder: (context) => Dialog(
child: UploadConfirmDialog(),
),
);
}
@override
Widget build(BuildContext context) {
return Container(
width: 400,
height: 200,
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black26,
offset: Offset(4, 4),
spreadRadius: 8,
blurRadius: 16,
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'确认下载并覆盖本地数据?',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
SizedBox(height: 26),
Text(
'本地资源修改时间似乎晚于云端\n确认覆盖可能造成数据丢失!',
textAlign: TextAlign.center,
),
const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: () {
Navigator.of(context).pop(false);
},
child: Text('取消'),
),
FilledButton(
onPressed: () {
Navigator.of(context).pop(true);
},
child: Text('确认'),
),
],
),
],
),
);
}
}
+10 -2
View File
@@ -105,6 +105,7 @@ class _MyHomePageState extends State<MyHomePage> {
IconButton( IconButton(
onPressed: () { onPressed: () {
loadCloudArchives(); loadCloudArchives();
loadLocalStatus();
}, },
icon: Icon(Icons.refresh_outlined), icon: Icon(Icons.refresh_outlined),
), ),
@@ -210,9 +211,16 @@ class _MyHomePageState extends State<MyHomePage> {
return InkWell( return InkWell(
onTap: cloudModifiedTimeTs == null onTap: cloudModifiedTimeTs == null
? null ? null
: () { : () async {
if (syncItemConfig != null) { if (syncItemConfig != null) {
download(context, syncItemConfig, cloudArchives.sorted.first); if (cloudModifiedTimeTs != null &&
localModifiedTime != null &&
localModifiedTime.millisecondsSinceEpoch > cloudModifiedTimeTs) {
if (await UploadConfirmDialog.show(context) != true) return;
}
if (context.mounted) {
download(context, syncItemConfig, cloudArchives.sorted.first);
}
} }
}, },
onLongPress: cloudModifiedTimeTs == null onLongPress: cloudModifiedTimeTs == null
+2 -1
View File
@@ -4,6 +4,7 @@ import 'package:archive/archive_io.dart';
import 'package:cloud_archiver/config.dart'; import 'package:cloud_archiver/config.dart';
import 'package:cloud_archiver/state.dart'; import 'package:cloud_archiver/state.dart';
import 'package:filesize/filesize.dart'; import 'package:filesize/filesize.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
@@ -24,7 +25,7 @@ String totalSize() {
Future<bool> connect(String server, String user, String auth) async { Future<bool> connect(String server, String user, String auth) async {
var client = dav.newClient( var client = dav.newClient(
server, server,
debug: true, debug: kDebugMode,
user: user, user: user,
password: auth, password: auth,
); );
+1
View File
@@ -1,3 +1,4 @@
# https://dart.dev/tools/private-files # https://dart.dev/tools/private-files
# Created by `dart pub` # Created by `dart pub`
.dart_tool/ .dart_tool/
repo/
+37 -2
View File
@@ -2,6 +2,7 @@ import 'dart:io';
import 'package:args/args.dart'; import 'package:args/args.dart';
import 'package:file/local.dart'; import 'package:file/local.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart'; import 'package:shelf/shelf_io.dart';
import 'package:shelf_dav/shelf_dav.dart'; import 'package:shelf_dav/shelf_dav.dart';
@@ -24,6 +25,7 @@ void main(List<String> args) async {
if (!rootDir.existsSync()) { if (!rootDir.existsSync()) {
rootDir.createSync(recursive: true); rootDir.createSync(recursive: true);
} }
var user = argResults.option('user')!;
final config = DAVConfig( final config = DAVConfig(
root: rootDir, root: rootDir,
prefix: '/dav', prefix: '/dav',
@@ -33,7 +35,7 @@ void main(List<String> args) async {
readOnly: false, readOnly: false,
propertyStorageType: PropertyStorageType.file, propertyStorageType: PropertyStorageType.file,
authenticationProvider: BasicAuthenticationProvider.plaintext( authenticationProvider: BasicAuthenticationProvider.plaintext(
users: {argResults.option('user')!: argResults.option('auth')!}, users: {user: argResults.option('auth')!},
), ),
); );
@@ -42,6 +44,39 @@ void main(List<String> args) async {
final ip = InternetAddress.anyIPv4; final ip = InternetAddress.anyIPv4;
final port = int.parse(argResults.option('port') ?? '8800'); final port = int.parse(argResults.option('port') ?? '8800');
final server = await serve(dav.handler, ip, port); final server = await serve(
(request) {
if (request.method == 'GET' && request.url.path == 'info/$user') {
return getInfo(rootDir);
}
return dav.handler.call(request);
},
ip,
port,
);
print('WebDAV server listening on port ${server.port}'); print('WebDAV server listening on port ${server.port}');
} }
Response getInfo(Directory dir) {
var du = Process.runSync('du', [
'-h',
'--time',
dir.absolute.path,
]).stdout.toString();
var result = <String>[];
for (var line in du.split('\n')) {
var parts = line.split(dir.absolute.path);
if (parts.length == 2) {
if (parts.last.trim().isEmpty) {
result.insert(0, '${parts.first}总占用\n');
} else {
result.add('${parts.first}${Uri.decodeComponent(parts.last)}');
}
}
}
return Response.ok(
result.join('\n'),
headers: {'content-type': 'text/plain'},
);
}