Files
cloud_archiver/client/lib/main.dart
T

308 lines
14 KiB
Dart

import 'package:cloud_archiver/config.dart';
import 'package:cloud_archiver/dialog.dart';
import 'package:cloud_archiver/state.dart';
import 'package:cloud_archiver/utils.dart';
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:path/path.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:webdav_client/webdav_client.dart' as dav;
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await AppConfig.init();
if (appConfig?.server.isNotEmpty == true) {
connect(appConfig!.server, appConfig!.user, appConfig!.passwd).then(
(success) {
if (success) {
loadCloudArchives();
}
},
);
}
if (appConfig?.syncItems.isNotEmpty == true) {
loadLocalStatus();
}
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Cloud Archiver',
theme: ThemeData(colorScheme: .fromSeed(seedColor: Colors.deepPurple)),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
void initState() {
checkAndRequestStoragePermission();
super.initState();
}
Future<void> checkAndRequestStoragePermission() async {
// 1. 检查是否已经拥有“所有文件访问权限”
var status = await Permission.manageExternalStorage.status;
if (!status.isGranted) {
print("未获得管理外部存储权限,尝试申请...");
// 2. 首次申请,或者已经被拒绝过
// 在 Android 11+ 上,这行代码会自动尝试跳转到系统的“所有文件访问权限”设置列表
var requestStatus = await Permission.manageExternalStorage.request();
// 3. 如果系统未能自动跳转(某些国产ROM限制),则强制通过插件打开系统设置
if (!requestStatus.isGranted) {
print("自动跳转失败,提示用户手动去设置开启");
await openAppSettings();
}
} else {
print("已获得所有文件访问权限,可以放心执行 listSync()");
}
}
@override
Widget build(BuildContext context) {
var width = MediaQuery.sizeOf(context).width;
int rows = (width ~/ 800) + 1;
double itemWidth = width / rows;
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
leading: IconButton(
onPressed: () {
AddSyncItemDialog.show(context);
},
icon: Icon(Icons.add),
),
title: ValueListenableBuilder(
valueListenable: davStatus,
builder: (context, value, child) => Center(
child: switch (value) {
DavStatus.connected => Text('总占用: ${totalSize()}'),
DavStatus.loading => Text('加载中...'),
DavStatus.notConnected => Text('未连接服务器'),
},
),
),
actions: [
IconButton(
onPressed: () {
loadCloudArchives();
loadLocalStatus();
},
icon: Icon(Icons.refresh_outlined),
),
IconButton(
onPressed: () {
AppConfigDialog.show(context);
},
icon: Icon(Icons.settings),
),
],
),
body: ValueListenableBuilder(
valueListenable: localSyncItems,
builder: (context, localSyncItems, child) {
return ValueListenableBuilder(
valueListenable: cloudArchives,
builder: (context, cloudArchives, child) {
var itemList = genItemList(localSyncItems, {...cloudArchives});
return GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: rows,
childAspectRatio: itemWidth / 80,
),
itemBuilder: (context, index) {
var (name, syncItemConfig, localModifiedTime, cloudArchives) = itemList[index];
var cloudModifiedTimeTs = cloudArchives.map((archive) => archive.mTime?.millisecondsSinceEpoch ?? 0).maxOrNull;
return GestureDetector(
onLongPress: () {
AddSyncItemDialog.show(context, syncItemConfig ?? SyncItemConfig(name: name, path: ''));
},
child: Container(
margin: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
padding: EdgeInsets.only(left: 10, right: 10, top: 6),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black26,
offset: Offset(1, 1),
spreadRadius: 2,
blurRadius: 5,
),
],
),
height: 80,
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Tooltip(
message: syncItemConfig?.path ?? '',
child: Text(
name,
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, height: 1),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.videogame_asset_outlined, size: 18),
SizedBox(width: 4),
Flexible(child: Text(localModifiedTime?.strHm ?? '未同步')),
],
),
),
Flexible(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.cloud_outlined, size: 18),
SizedBox(width: 4),
Flexible(
child: Text(
cloudModifiedTimeTs == null ? '未上传' : DateTime.fromMillisecondsSinceEpoch(cloudModifiedTimeTs).strHm,
),
),
],
),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
InkWell(
onTap: localModifiedTime == null
? null
: () {
if (syncItemConfig != null) {
upload(context, syncItemConfig, cloudModifiedTimeTs == null);
}
},
child: Icon(Icons.upload_rounded, size: 22),
),
SizedBox(width: 10),
Builder(
builder: (ctx) {
return InkWell(
onTap: cloudModifiedTimeTs == null
? null
: () async {
if (syncItemConfig != null) {
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
? null
: () {
var btn = (ctx.findRenderObject() as RenderBox);
var offset = btn.localToGlobal(Offset(btn.size.width, btn.size.height));
showMenu<dav.File>(
context: ctx,
position: RelativeRect.fromLTRB(offset.dx - 300, offset.dy, width - offset.dx, offset.dy + 200),
items: cloudArchives.sorted.map(
(file) {
var parts = basenameWithoutExtension(file.name ?? '').split('-');
parts.removeLast();
var client = parts.join('-');
return PopupMenuItem<dav.File>(
value: file,
onTap: () {
if (syncItemConfig != null) {
download(context, syncItemConfig, file);
}
},
child: SizedBox(
width: 300,
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
client,
overflow: TextOverflow.ellipsis,
),
),
Text((file.mTime ?? file.cTime ?? DateTime(0)).strHms),
],
),
),
);
},
).toList(),
);
},
child: Icon(Icons.download_rounded, size: 22),
);
},
),
],
),
],
),
],
),
),
);
},
itemCount: itemList.length,
);
},
);
},
),
);
}
List<(String, SyncItemConfig?, DateTime?, List<dav.File>)> genItemList(
Map<SyncItemConfig, DateTime?> localSyncItems,
Map<String, List<dav.File>> cloudArchives,
) {
var result = <(String, SyncItemConfig?, DateTime?, List<dav.File>)>[];
var items = localSyncItems.entries.toList()..sort((a, b) => (a.value?.millisecondsSinceEpoch ?? 0).compareTo(b.value?.millisecondsSinceEpoch ?? 0));
for (var item in items) {
var archive = cloudArchives.remove(item.key.name);
result.add((item.key.name, item.key, item.value, archive ?? []));
}
for (var archive in cloudArchives.entries) {
result.add((archive.key, null, null, archive.value));
}
return result;
}
}