first commit.
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cloud_archiver/utils.dart';
|
||||
import 'dart:convert' show json;
|
||||
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:path/path.dart' show join;
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
late File _configFile;
|
||||
|
||||
AppConfig? appConfig;
|
||||
|
||||
class AppConfig {
|
||||
final String client;
|
||||
final String server;
|
||||
final String user;
|
||||
final String passwd;
|
||||
|
||||
final List<SyncItemConfig> syncItems;
|
||||
|
||||
static Future<void> init() async {
|
||||
var directory = await getApplicationSupportDirectory();
|
||||
_configFile = File(join(directory.path, 'config.json'));
|
||||
if (_configFile.existsSync()) {
|
||||
try {
|
||||
appConfig = AppConfig.fromJson(json.decode(_configFile.readAsStringSync()));
|
||||
} catch (e) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void updateConfig({
|
||||
required String client,
|
||||
required String server,
|
||||
required String user,
|
||||
required String passwd,
|
||||
}) {
|
||||
appConfig = AppConfig(
|
||||
client: client,
|
||||
server: server,
|
||||
user: user,
|
||||
passwd: passwd,
|
||||
syncItems: appConfig?.syncItems ?? [],
|
||||
);
|
||||
_save();
|
||||
}
|
||||
|
||||
static void addSyncItem(SyncItemConfig item) {
|
||||
var items = (appConfig?.syncItems ?? []).where((ele) => ele.uuid != item.uuid);
|
||||
|
||||
appConfig = AppConfig(
|
||||
client: appConfig?.client ?? '',
|
||||
server: appConfig?.server ?? '',
|
||||
user: appConfig?.user ?? '',
|
||||
passwd: appConfig?.passwd ?? '',
|
||||
syncItems: [
|
||||
item,
|
||||
...items,
|
||||
],
|
||||
);
|
||||
_save();
|
||||
loadLocalStatus();
|
||||
}
|
||||
|
||||
static void removeSyncItem(SyncItemConfig item) {
|
||||
var items = (appConfig?.syncItems ?? []).where((ele) => ele.uuid != item.uuid);
|
||||
|
||||
appConfig = AppConfig(
|
||||
client: appConfig?.client ?? '',
|
||||
server: appConfig?.server ?? '',
|
||||
user: appConfig?.user ?? '',
|
||||
passwd: appConfig?.passwd ?? '',
|
||||
syncItems: items.toList(),
|
||||
);
|
||||
_save();
|
||||
loadLocalStatus();
|
||||
}
|
||||
|
||||
AppConfig({
|
||||
required this.client,
|
||||
required this.server,
|
||||
required this.user,
|
||||
required this.passwd,
|
||||
required this.syncItems,
|
||||
});
|
||||
|
||||
factory AppConfig.fromJson(Map<String, dynamic> json) => AppConfig(
|
||||
client: json['client'],
|
||||
server: json['server'],
|
||||
user: json['user'],
|
||||
passwd: json['passwd'],
|
||||
syncItems: (json['syncItems'] as List? ?? []).map((ele) => SyncItemConfig.fromJson(ele)).toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'client': client,
|
||||
'server': server,
|
||||
'user': user,
|
||||
'passwd': passwd,
|
||||
'syncItems': syncItems,
|
||||
};
|
||||
|
||||
static void _save() {
|
||||
_configFile.writeAsStringSync(json.encode(appConfig));
|
||||
}
|
||||
}
|
||||
|
||||
class SyncItemConfig {
|
||||
final String uuid;
|
||||
final String name;
|
||||
final String path;
|
||||
|
||||
SyncItemConfig({required this.name, required this.path, String? uuid}) : uuid = uuid ?? Uuid().v4();
|
||||
|
||||
factory SyncItemConfig.fromJson(Map<String, dynamic> json) => SyncItemConfig(
|
||||
uuid: json['uuid'],
|
||||
name: json['name'],
|
||||
path: json['path'],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'uuid': uuid,
|
||||
'name': name,
|
||||
'path': path,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
import 'package:cloud_archiver/config.dart';
|
||||
import 'package:cloud_archiver/utils.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppConfigDialog extends StatefulWidget {
|
||||
const AppConfigDialog({super.key});
|
||||
|
||||
static void show(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => Dialog(
|
||||
child: AppConfigDialog(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
State<AppConfigDialog> createState() => _AppConfigDialogState();
|
||||
}
|
||||
|
||||
class _AppConfigDialogState extends State<AppConfigDialog> {
|
||||
final TextEditingController clientCtl = TextEditingController();
|
||||
final TextEditingController serverCtl = TextEditingController();
|
||||
final TextEditingController userCtl = TextEditingController();
|
||||
final TextEditingController passwdCtl = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (appConfig != null) {
|
||||
clientCtl.text = appConfig!.client;
|
||||
serverCtl.text = appConfig!.server;
|
||||
userCtl.text = appConfig!.user;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 400,
|
||||
height: 300,
|
||||
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(
|
||||
'配置DAV服务',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(width: 70, child: Text('标识名: ')),
|
||||
Flexible(
|
||||
child: TextField(
|
||||
controller: clientCtl,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(width: 70, child: Text('服务器: ')),
|
||||
Flexible(
|
||||
child: TextField(
|
||||
controller: serverCtl,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(width: 70, child: Text('用户名: ')),
|
||||
Flexible(
|
||||
child: TextField(
|
||||
controller: userCtl,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(width: 70, child: Text('密码: ')),
|
||||
Flexible(
|
||||
child: TextField(
|
||||
controller: passwdCtl,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text('取消'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
var client = clientCtl.text.trim();
|
||||
var server = serverCtl.text.trim();
|
||||
var user = userCtl.text.trim();
|
||||
var passwd = passwdCtl.text.trim();
|
||||
connect(server, user, passwd).then(
|
||||
(connected) {
|
||||
if (connected) {
|
||||
AppConfig.updateConfig(
|
||||
client: client,
|
||||
server: server,
|
||||
user: user,
|
||||
passwd: passwd,
|
||||
);
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Text('确认'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AddSyncItemDialog extends StatefulWidget {
|
||||
final SyncItemConfig? editItemConfig;
|
||||
|
||||
const AddSyncItemDialog({super.key, this.editItemConfig});
|
||||
|
||||
static void show(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => Dialog(
|
||||
child: AddSyncItemDialog(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
State<AddSyncItemDialog> createState() => _AddSyncItemDialogState();
|
||||
}
|
||||
|
||||
class _AddSyncItemDialogState extends State<AddSyncItemDialog> {
|
||||
final TextEditingController nameCtl = TextEditingController();
|
||||
final TextEditingController pathCtl = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 400,
|
||||
height: 300,
|
||||
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(
|
||||
widget.editItemConfig == null ? '添加同步项' : '修改同步项',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(width: 80, child: Text('项目名称: ')),
|
||||
Flexible(
|
||||
child: TextField(
|
||||
controller: nameCtl,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(width: 80, child: Text('项目路径: ')),
|
||||
Flexible(
|
||||
child: TextField(
|
||||
controller: pathCtl,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
FilePicker.pickFiles(
|
||||
allowMultiple: false,
|
||||
).then(
|
||||
(res) {
|
||||
if (res?.files.firstOrNull?.path?.isNotEmpty == true) {
|
||||
pathCtl.text = res!.files.first.path!;
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
icon: Icon(Icons.file_open_rounded),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
FilePicker.getDirectoryPath().then(
|
||||
(path) {
|
||||
if (path?.isNotEmpty == true) {
|
||||
pathCtl.text = path!;
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
icon: Icon(Icons.folder_open_rounded),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text('取消'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
var name = nameCtl.text.trim();
|
||||
var path = pathCtl.text.trim();
|
||||
|
||||
if (name.isNotEmpty && path.isNotEmpty) {
|
||||
AppConfig.addSyncItem(
|
||||
SyncItemConfig(
|
||||
name: name,
|
||||
path: path,
|
||||
uuid: widget.editItemConfig?.uuid,
|
||||
),
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
child: Text('确认'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
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: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
|
||||
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();
|
||||
},
|
||||
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 Container(
|
||||
margin: EdgeInsets.all(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: [
|
||||
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: [
|
||||
IconButton(
|
||||
onPressed: localModifiedTime == null
|
||||
? null
|
||||
: () {
|
||||
if (syncItemConfig != null) {
|
||||
upload(syncItemConfig, cloudModifiedTimeTs == null);
|
||||
}
|
||||
},
|
||||
icon: Icon(Icons.upload_rounded, size: 22),
|
||||
),
|
||||
Builder(
|
||||
builder: (ctx) {
|
||||
return IconButton(
|
||||
onPressed: cloudModifiedTimeTs == null
|
||||
? null
|
||||
: () {
|
||||
if (syncItemConfig != null) {
|
||||
download(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(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(),
|
||||
);
|
||||
},
|
||||
icon: 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'package:cloud_archiver/config.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:webdav_client/webdav_client.dart' as dav;
|
||||
|
||||
enum DavStatus {
|
||||
notConnected,
|
||||
connected,
|
||||
loading,
|
||||
}
|
||||
|
||||
extension CloudArchivesSortExt on List<dav.File> {
|
||||
List<dav.File> get sorted => sortedByCompare(
|
||||
(file) => file.name ?? '',
|
||||
(a, b) => b.split('-').last.compareTo(a.split('-').last),
|
||||
);
|
||||
}
|
||||
|
||||
final ValueNotifier<DavStatus> davStatus = ValueNotifier(DavStatus.notConnected);
|
||||
|
||||
final ValueNotifier<Map<String, List<dav.File>>> cloudArchives = ValueNotifier({});
|
||||
|
||||
final ValueNotifier<Map<SyncItemConfig, DateTime?>> localSyncItems = ValueNotifier({});
|
||||
@@ -0,0 +1,174 @@
|
||||
import 'dart:io';
|
||||
|
||||
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:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:webdav_client/webdav_client.dart' as dav;
|
||||
|
||||
late dav.Client _client;
|
||||
|
||||
String totalSize() {
|
||||
if (cloudArchives.value.isEmpty) return '0 MB';
|
||||
return filesize(
|
||||
cloudArchives.value.values
|
||||
.map((archives) => archives.map((archive) => archive.size ?? 0).reduce((total, size) => total + size))
|
||||
.reduce((total, size) => total + size),
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> connect(String server, String user, String auth) async {
|
||||
var client = dav.newClient(
|
||||
server,
|
||||
debug: true,
|
||||
user: user,
|
||||
password: auth,
|
||||
);
|
||||
client.setHeaders({'accept-charset': 'utf-8'});
|
||||
|
||||
client.setConnectTimeout(5 * 1000);
|
||||
|
||||
// Set send data timeout time in milliseconds.
|
||||
client.setSendTimeout(60 * 1000);
|
||||
|
||||
// Set transfer data time in milliseconds.
|
||||
client.setReceiveTimeout(60 * 1000);
|
||||
|
||||
try {
|
||||
davStatus.value = DavStatus.loading;
|
||||
await client.ping();
|
||||
davStatus.value = DavStatus.connected;
|
||||
_client = client;
|
||||
return true;
|
||||
} catch (e) {
|
||||
print('$e');
|
||||
davStatus.value = DavStatus.notConnected;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void loadLocalStatus() {
|
||||
if (appConfig?.syncItems.isNotEmpty != true) return;
|
||||
var result = <SyncItemConfig, DateTime?>{};
|
||||
for (var syncItem in appConfig!.syncItems) {
|
||||
var file = File(syncItem.path);
|
||||
var stat = file.statSync();
|
||||
switch (stat.type) {
|
||||
case FileSystemEntityType.file:
|
||||
result[syncItem] = stat.modified;
|
||||
case FileSystemEntityType.directory:
|
||||
result[syncItem] = dirModifiedTime(syncItem.path);
|
||||
default:
|
||||
result[syncItem] = null;
|
||||
}
|
||||
}
|
||||
|
||||
localSyncItems.value = {
|
||||
...localSyncItems.value,
|
||||
...result,
|
||||
};
|
||||
}
|
||||
|
||||
DateTime dirModifiedTime(String dir) {
|
||||
var directory = Directory(dir);
|
||||
var modified = directory.statSync().modified;
|
||||
for (var entry in Directory(dir).listSync(recursive: true)) {
|
||||
var stat = entry.statSync();
|
||||
if (stat.type == FileSystemEntityType.file) {
|
||||
if (stat.modified.isAfter(modified)) {
|
||||
modified = stat.modified;
|
||||
}
|
||||
}
|
||||
}
|
||||
return modified;
|
||||
}
|
||||
|
||||
void loadCloudArchives() async {
|
||||
if (davStatus.value != DavStatus.connected) return;
|
||||
davStatus.value = DavStatus.loading;
|
||||
List<dav.File> list = [];
|
||||
try {
|
||||
list = await _client.readDir('/');
|
||||
} catch (e) {
|
||||
print(e);
|
||||
davStatus.value = DavStatus.notConnected;
|
||||
return;
|
||||
}
|
||||
var result = <String, List<dav.File>>{};
|
||||
for (var item in list) {
|
||||
if (item.isDir == true && item.path?.isNotEmpty == true) {
|
||||
var files = await _client.readDir(item.path!);
|
||||
result[basename(item.path!)] = files;
|
||||
}
|
||||
}
|
||||
|
||||
cloudArchives.value = result;
|
||||
davStatus.value = DavStatus.connected;
|
||||
}
|
||||
|
||||
Future<void> upload(SyncItemConfig item, bool needCreateDir) async {
|
||||
if (needCreateDir) {
|
||||
await _client.mkdir(item.name);
|
||||
}
|
||||
var file = File(item.path);
|
||||
|
||||
var cacheDir = await getApplicationCacheDirectory();
|
||||
|
||||
var zipFileEncoder = ZipFileEncoder();
|
||||
var zipFileName = '${appConfig?.client ?? 'DEBUG'}-${DateTime.now().strFileName}.zip';
|
||||
var zipTempFilePath = join(cacheDir.path, zipFileName);
|
||||
zipFileEncoder.create(zipTempFilePath);
|
||||
if (file.statSync().type == FileSystemEntityType.directory) {
|
||||
await zipFileEncoder.addDirectory(Directory(item.path));
|
||||
} else {
|
||||
await zipFileEncoder.addFile(file);
|
||||
}
|
||||
|
||||
await zipFileEncoder.close();
|
||||
|
||||
await _client.writeFromFile(zipTempFilePath, join(item.name, zipFileName));
|
||||
|
||||
File(zipTempFilePath).delete();
|
||||
|
||||
loadCloudArchives();
|
||||
}
|
||||
|
||||
Future<void> download(SyncItemConfig item, dav.File cloudArchive) async {
|
||||
var cacheDir = await getApplicationCacheDirectory();
|
||||
var zipPath = join(cacheDir.path, '${item.name}_${cloudArchive.name}');
|
||||
await _client.read2File(cloudArchive.path!, zipPath);
|
||||
await extractFileToDisk(zipPath, cacheDir.path);
|
||||
File(zipPath).delete();
|
||||
|
||||
var temp = File(join(cacheDir.path, basename(item.path)));
|
||||
|
||||
switch (temp.statSync().type) {
|
||||
case FileSystemEntityType.directory:
|
||||
var directory = Directory(item.path);
|
||||
if (directory.existsSync()) {
|
||||
directory.deleteSync(recursive: true);
|
||||
}
|
||||
Directory(temp.path);
|
||||
Directory(temp.path).renameSync(item.path);
|
||||
case FileSystemEntityType.file:
|
||||
var file = File(item.path);
|
||||
if (file.existsSync()) {
|
||||
file.deleteSync();
|
||||
}
|
||||
temp.renameSync(item.path);
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
extension DateTimeExt on DateTime {
|
||||
String get strHm => '$year/${padZero(month)}/${padZero(day)} ${padZero(hour)}:${padZero(minute)}';
|
||||
|
||||
String get strHms => '$strHm:${padZero(second)}';
|
||||
|
||||
String get strFileName => '${year}_${padZero(month)}_${padZero(day)}_${padZero(hour)}_${padZero(minute)}_${padZero(second)}';
|
||||
|
||||
String padZero(int num, [fix = 2]) => num.toString().padLeft(fix, '0');
|
||||
}
|
||||
Reference in New Issue
Block a user