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 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 createState() => _MyHomePageState(); } class _MyHomePageState extends State { @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 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(syncItemConfig, cloudModifiedTimeTs == null); } }, child: Icon(Icons.upload_rounded, size: 22), ), SizedBox(width: 10), Builder( builder: (ctx) { return InkWell( onTap: 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( 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( 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(), ); }, child: Icon(Icons.download_rounded, size: 22), ); }, ), ], ), ], ), ], ), ), ); }, itemCount: itemList.length, ); }, ); }, ), ); } List<(String, SyncItemConfig?, DateTime?, List)> genItemList( Map localSyncItems, Map> cloudArchives, ) { var result = <(String, SyncItemConfig?, DateTime?, List)>[]; 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; } }