308 lines
14 KiB
Dart
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;
|
|
}
|
|
}
|