diff --git a/client/lib/dialog.dart b/client/lib/dialog.dart index 6486063..b6fc7b5 100644 --- a/client/lib/dialog.dart +++ b/client/lib/dialog.dart @@ -104,13 +104,13 @@ class _AppConfigDialogState extends State { 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 { 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 { 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 { ); } } + +class UploadConfirmDialog extends StatelessWidget { + const UploadConfirmDialog({super.key}); + + static Future 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('确认'), + ), + ], + ), + ], + ), + ); + } +} diff --git a/client/lib/main.dart b/client/lib/main.dart index f9d6d11..690ea95 100644 --- a/client/lib/main.dart +++ b/client/lib/main.dart @@ -105,6 +105,7 @@ class _MyHomePageState extends State { IconButton( onPressed: () { loadCloudArchives(); + loadLocalStatus(); }, icon: Icon(Icons.refresh_outlined), ), @@ -210,9 +211,16 @@ class _MyHomePageState extends State { 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 diff --git a/client/lib/utils.dart b/client/lib/utils.dart index a007b10..f7da7dd 100644 --- a/client/lib/utils.dart +++ b/client/lib/utils.dart @@ -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 connect(String server, String user, String auth) async { var client = dav.newClient( server, - debug: true, + debug: kDebugMode, user: user, password: auth, ); diff --git a/server/.gitignore b/server/.gitignore index e4ce6a1..0325afb 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -1,3 +1,4 @@ # https://dart.dev/tools/private-files # Created by `dart pub` .dart_tool/ +repo/ diff --git a/server/bin/server.dart b/server/bin/server.dart index dd91087..8c20c86 100644 --- a/server/bin/server.dart +++ b/server/bin/server.dart @@ -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 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 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 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 = []; + 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'}, + ); +}