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(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
FilledButton(
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text('取消'),
),
ElevatedButton(
FilledButton(
onPressed: () {
var client = clientCtl.text.trim();
var server = serverCtl.text.trim();
@@ -223,12 +223,10 @@ class _AddSyncItemDialogState extends State<AddSyncItemDialog> {
children: [
IconButton(
onPressed: () {
FilePicker.pickFiles(
allowMultiple: false,
).then(
FilePicker.pickFile().then(
(res) {
if (res?.files.firstOrNull?.path?.isNotEmpty == true) {
pathCtl.text = res!.files.first.path!;
if (res?.xFile.path?.isNotEmpty == true) {
pathCtl.text = res!.xFile.path!;
}
},
);
@@ -253,13 +251,13 @@ class _AddSyncItemDialogState extends State<AddSyncItemDialog> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
FilledButton(
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text('取消'),
),
ElevatedButton(
FilledButton(
onPressed: () {
var name = nameCtl.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(
onPressed: () {
loadCloudArchives();
loadLocalStatus();
},
icon: Icon(Icons.refresh_outlined),
),
@@ -210,9 +211,16 @@ class _MyHomePageState extends State<MyHomePage> {
return InkWell(
onTap: cloudModifiedTimeTs == null
? null
: () {
: () async {
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
+2 -1
View File
@@ -4,6 +4,7 @@ import 'package:archive/archive_io.dart';
import 'package:cloud_archiver/config.dart';
import 'package:cloud_archiver/state.dart';
import 'package:filesize/filesize.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
@@ -24,7 +25,7 @@ String totalSize() {
Future<bool> connect(String server, String user, String auth) async {
var client = dav.newClient(
server,
debug: true,
debug: kDebugMode,
user: user,
password: auth,
);
+1
View File
@@ -1,3 +1,4 @@
# https://dart.dev/tools/private-files
# Created by `dart pub`
.dart_tool/
repo/
+37 -2
View File
@@ -2,6 +2,7 @@ import 'dart:io';
import 'package:args/args.dart';
import 'package:file/local.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart';
import 'package:shelf_dav/shelf_dav.dart';
@@ -24,6 +25,7 @@ void main(List<String> args) async {
if (!rootDir.existsSync()) {
rootDir.createSync(recursive: true);
}
var user = argResults.option('user')!;
final config = DAVConfig(
root: rootDir,
prefix: '/dav',
@@ -33,7 +35,7 @@ void main(List<String> args) async {
readOnly: false,
propertyStorageType: PropertyStorageType.file,
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 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}');
}
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'},
);
}