file_model.dart 51 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753
  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'package:flutter/material.dart';
  4. import 'package:flutter_hbb/common.dart';
  5. import 'package:flutter_hbb/common/widgets/dialog.dart';
  6. import 'package:flutter_hbb/utils/event_loop.dart';
  7. import 'package:get/get.dart';
  8. import 'package:path/path.dart' as path;
  9. import 'package:flutter_hbb/web/dummy.dart'
  10. if (dart.library.html) 'package:flutter_hbb/web/web_unique.dart';
  11. import '../consts.dart';
  12. import 'model.dart';
  13. import 'platform_model.dart';
  14. enum SortBy {
  15. name,
  16. type,
  17. modified,
  18. size;
  19. @override
  20. String toString() {
  21. final str = this.name.toString();
  22. return "${str[0].toUpperCase()}${str.substring(1)}";
  23. }
  24. }
  25. class JobID {
  26. int _count = 0;
  27. int next() {
  28. _count++;
  29. return _count;
  30. }
  31. }
  32. typedef GetSessionID = SessionID Function();
  33. typedef GetDialogManager = OverlayDialogManager? Function();
  34. class FileModel {
  35. final WeakReference<FFI> parent;
  36. // late final String sessionId;
  37. late final FileFetcher fileFetcher;
  38. late final JobController jobController;
  39. late final FileController localController;
  40. late final FileController remoteController;
  41. late final GetSessionID getSessionID;
  42. late final GetDialogManager getDialogManager;
  43. SessionID get sessionId => getSessionID();
  44. late final FileDialogEventLoop evtLoop;
  45. FileModel(this.parent) {
  46. getSessionID = () => parent.target!.sessionId;
  47. getDialogManager = () => parent.target?.dialogManager;
  48. fileFetcher = FileFetcher(getSessionID);
  49. jobController = JobController(getSessionID, getDialogManager);
  50. localController = FileController(
  51. isLocal: true,
  52. getSessionID: getSessionID,
  53. rootState: parent,
  54. jobController: jobController,
  55. fileFetcher: fileFetcher,
  56. getOtherSideDirectoryData: () => remoteController.directoryData());
  57. remoteController = FileController(
  58. isLocal: false,
  59. getSessionID: getSessionID,
  60. rootState: parent,
  61. jobController: jobController,
  62. fileFetcher: fileFetcher,
  63. getOtherSideDirectoryData: () => localController.directoryData());
  64. evtLoop = FileDialogEventLoop();
  65. }
  66. Future<void> onReady() async {
  67. await evtLoop.onReady();
  68. if (!isWeb) await localController.onReady();
  69. await remoteController.onReady();
  70. }
  71. Future<void> close() async {
  72. await evtLoop.close();
  73. parent.target?.dialogManager.dismissAll();
  74. await localController.close();
  75. await remoteController.close();
  76. }
  77. Future<void> refreshAll() async {
  78. if (!isWeb) await localController.refresh();
  79. await remoteController.refresh();
  80. }
  81. void receiveFileDir(Map<String, dynamic> evt) {
  82. if (evt['is_local'] == "false") {
  83. // init remote home, the remote connection will send one dir event when established. TODO opt
  84. remoteController.initDirAndHome(evt);
  85. }
  86. fileFetcher.tryCompleteTask(evt['value'], evt['is_local']);
  87. }
  88. void receiveEmptyDirs(Map<String, dynamic> evt) {
  89. fileFetcher.tryCompleteEmptyDirsTask(evt['value'], evt['is_local']);
  90. }
  91. Future<void> postOverrideFileConfirm(Map<String, dynamic> evt) async {
  92. evtLoop.pushEvent(
  93. _FileDialogEvent(WeakReference(this), FileDialogType.overwrite, evt));
  94. }
  95. Future<void> overrideFileConfirm(Map<String, dynamic> evt,
  96. {bool? overrideConfirm, bool skip = false}) async {
  97. // If `skip == true`, it means to skip this file without showing dialog.
  98. // Because `resp` may be null after the user operation or the last remembered operation,
  99. // and we should distinguish them.
  100. final resp = overrideConfirm ??
  101. (!skip
  102. ? await showFileConfirmDialog(translate("Overwrite"),
  103. "${evt['read_path']}", true, evt['is_identical'] == "true")
  104. : null);
  105. final id = int.tryParse(evt['id']) ?? 0;
  106. if (false == resp) {
  107. final jobIndex = jobController.getJob(id);
  108. if (jobIndex != -1) {
  109. await jobController.cancelJob(id);
  110. final job = jobController.jobTable[jobIndex];
  111. job.state = JobState.done;
  112. jobController.jobTable.refresh();
  113. }
  114. } else {
  115. var need_override = false;
  116. if (resp == null) {
  117. // skip
  118. need_override = false;
  119. } else {
  120. // overwrite
  121. need_override = true;
  122. }
  123. // Update the loop config.
  124. if (fileConfirmCheckboxRemember) {
  125. evtLoop.setSkip(!need_override);
  126. }
  127. await bind.sessionSetConfirmOverrideFile(
  128. sessionId: sessionId,
  129. actId: id,
  130. fileNum: int.parse(evt['file_num']),
  131. needOverride: need_override,
  132. remember: fileConfirmCheckboxRemember,
  133. isUpload: evt['is_upload'] == "true");
  134. }
  135. // Update the loop config.
  136. if (fileConfirmCheckboxRemember) {
  137. evtLoop.setOverrideConfirm(resp);
  138. }
  139. }
  140. bool fileConfirmCheckboxRemember = false;
  141. Future<bool?> showFileConfirmDialog(
  142. String title, String content, bool showCheckbox, bool isIdentical) async {
  143. fileConfirmCheckboxRemember = false;
  144. return await parent.target?.dialogManager.show<bool?>(
  145. (setState, Function(bool? v) close, context) {
  146. cancel() => close(false);
  147. submit() => close(true);
  148. return CustomAlertDialog(
  149. title: Row(
  150. children: [
  151. const Icon(Icons.warning_rounded, color: Colors.red),
  152. Text(title).paddingOnly(
  153. left: 10,
  154. ),
  155. ],
  156. ),
  157. contentBoxConstraints:
  158. BoxConstraints(minHeight: 100, minWidth: 400, maxWidth: 400),
  159. content: Column(
  160. crossAxisAlignment: CrossAxisAlignment.start,
  161. mainAxisSize: MainAxisSize.min,
  162. children: [
  163. Text(translate("This file exists, skip or overwrite this file?"),
  164. style: const TextStyle(fontWeight: FontWeight.bold)),
  165. const SizedBox(height: 5),
  166. Text(content),
  167. Offstage(
  168. offstage: !isIdentical,
  169. child: Column(
  170. mainAxisSize: MainAxisSize.min,
  171. children: [
  172. const SizedBox(height: 12),
  173. Text(translate("identical_file_tip"),
  174. style: const TextStyle(fontWeight: FontWeight.w500))
  175. ],
  176. ),
  177. ),
  178. showCheckbox
  179. ? CheckboxListTile(
  180. contentPadding: const EdgeInsets.all(0),
  181. dense: true,
  182. controlAffinity: ListTileControlAffinity.leading,
  183. title: Text(
  184. translate("Do this for all conflicts"),
  185. ),
  186. value: fileConfirmCheckboxRemember,
  187. onChanged: (v) {
  188. if (v == null) return;
  189. setState(() => fileConfirmCheckboxRemember = v);
  190. },
  191. )
  192. : const SizedBox.shrink()
  193. ]),
  194. actions: [
  195. dialogButton(
  196. "Cancel",
  197. icon: Icon(Icons.close_rounded),
  198. onPressed: cancel,
  199. isOutline: true,
  200. ),
  201. dialogButton(
  202. "Skip",
  203. icon: Icon(Icons.navigate_next_rounded),
  204. onPressed: () => close(null),
  205. isOutline: true,
  206. ),
  207. dialogButton(
  208. "OK",
  209. icon: Icon(Icons.done_rounded),
  210. onPressed: submit,
  211. ),
  212. ],
  213. onSubmit: submit,
  214. onCancel: cancel,
  215. );
  216. }, useAnimation: false);
  217. }
  218. void onSelectedFiles(dynamic obj) {
  219. localController.selectedItems.clear();
  220. try {
  221. int handleIndex = int.parse(obj['handleIndex']);
  222. final file = jsonDecode(obj['file']);
  223. var entry = Entry.fromJson(file);
  224. entry.path = entry.name;
  225. final otherSideData = remoteController.directoryData();
  226. final toPath = otherSideData.directory.path;
  227. final isWindows = otherSideData.options.isWindows;
  228. final showHidden = otherSideData.options.showHidden;
  229. final jobID = jobController.addTransferJob(entry, false);
  230. webSendLocalFiles(
  231. handleIndex: handleIndex,
  232. actId: jobID,
  233. path: entry.path,
  234. to: PathUtil.join(toPath, entry.name, isWindows),
  235. fileNum: 0,
  236. includeHidden: showHidden,
  237. isRemote: false,
  238. );
  239. } catch (e) {
  240. debugPrint("Failed to decode onSelectedFiles: $e");
  241. }
  242. }
  243. void sendEmptyDirs(dynamic obj) {
  244. late final List<dynamic> emptyDirs;
  245. try {
  246. emptyDirs = jsonDecode(obj['dirs'] as String);
  247. } catch (e) {
  248. debugPrint("Failed to decode sendEmptyDirs: $e");
  249. }
  250. final otherSideData = remoteController.directoryData();
  251. final toPath = otherSideData.directory.path;
  252. final isPeerWindows = otherSideData.options.isWindows;
  253. final isLocalWindows = isWindows || isWebOnWindows;
  254. for (var dir in emptyDirs) {
  255. if (isLocalWindows != isPeerWindows) {
  256. dir = PathUtil.convert(dir, isLocalWindows, isPeerWindows);
  257. }
  258. var peerPath = PathUtil.join(toPath, dir, isPeerWindows);
  259. remoteController.createDirWithRemote(peerPath, true);
  260. }
  261. }
  262. }
  263. class DirectoryData {
  264. final DirectoryOptions options;
  265. final FileDirectory directory;
  266. DirectoryData(this.directory, this.options);
  267. }
  268. class FileController {
  269. final bool isLocal;
  270. final GetSessionID getSessionID;
  271. SessionID get sessionId => getSessionID();
  272. final FileFetcher fileFetcher;
  273. final options = DirectoryOptions().obs;
  274. final directory = FileDirectory().obs;
  275. final history = RxList<String>.empty(growable: true);
  276. final sortBy = SortBy.name.obs;
  277. var sortAscending = true;
  278. final JobController jobController;
  279. final WeakReference<FFI> rootState;
  280. final DirectoryData Function() getOtherSideDirectoryData;
  281. late final SelectedItems selectedItems = SelectedItems(isLocal: isLocal);
  282. FileController(
  283. {required this.isLocal,
  284. required this.getSessionID,
  285. required this.rootState,
  286. required this.jobController,
  287. required this.fileFetcher,
  288. required this.getOtherSideDirectoryData});
  289. String get homePath => options.value.home;
  290. void set homePath(String path) => options.value.home = path;
  291. OverlayDialogManager? get dialogManager => rootState.target?.dialogManager;
  292. String get shortPath {
  293. final dirPath = directory.value.path;
  294. if (dirPath.startsWith(homePath)) {
  295. var path = dirPath.replaceFirst(homePath, "");
  296. if (path.isEmpty) return "";
  297. if (path[0] == "/" || path[0] == "\\") {
  298. // remove more '/' or '\'
  299. path = path.replaceFirst(path[0], "");
  300. }
  301. return path;
  302. } else {
  303. return dirPath.replaceFirst(homePath, "");
  304. }
  305. }
  306. DirectoryData directoryData() {
  307. return DirectoryData(directory.value, options.value);
  308. }
  309. Future<void> onReady() async {
  310. if (isLocal) {
  311. options.value.home = await bind.mainGetHomeDir();
  312. }
  313. options.value.showHidden = (await bind.sessionGetPeerOption(
  314. sessionId: sessionId,
  315. name: isLocal ? "local_show_hidden" : "remote_show_hidden"))
  316. .isNotEmpty;
  317. options.value.isWindows = isLocal
  318. ? isWindows
  319. : rootState.target?.ffiModel.pi.platform == kPeerPlatformWindows;
  320. await Future.delayed(Duration(milliseconds: 100));
  321. final dir = (await bind.sessionGetPeerOption(
  322. sessionId: sessionId, name: isLocal ? "local_dir" : "remote_dir"));
  323. openDirectory(dir.isEmpty ? options.value.home : dir);
  324. await Future.delayed(Duration(seconds: 1));
  325. if (directory.value.path.isEmpty) {
  326. openDirectory(options.value.home);
  327. }
  328. }
  329. Future<void> close() async {
  330. // save config
  331. Map<String, String> msgMap = {};
  332. msgMap[isLocal ? "local_dir" : "remote_dir"] = directory.value.path;
  333. msgMap[isLocal ? "local_show_hidden" : "remote_show_hidden"] =
  334. options.value.showHidden ? "Y" : "";
  335. for (final msg in msgMap.entries) {
  336. await bind.sessionPeerOption(
  337. sessionId: sessionId, name: msg.key, value: msg.value);
  338. }
  339. directory.value.clear();
  340. options.value.clear();
  341. }
  342. void toggleShowHidden({bool? showHidden}) {
  343. options.value.showHidden = showHidden ?? !options.value.showHidden;
  344. refresh();
  345. }
  346. void changeSortStyle(SortBy sort, {bool? isLocal, bool ascending = true}) {
  347. sortBy.value = sort;
  348. sortAscending = ascending;
  349. directory.update((dir) {
  350. dir?.changeSortStyle(sort, ascending: ascending);
  351. });
  352. }
  353. Future<void> refresh() async {
  354. await openDirectory(directory.value.path);
  355. }
  356. Future<void> openDirectory(String path, {bool isBack = false}) async {
  357. if (path == ".") {
  358. refresh();
  359. return;
  360. }
  361. if (path == "..") {
  362. goToParentDirectory();
  363. return;
  364. }
  365. if (!isBack) {
  366. pushHistory();
  367. }
  368. final showHidden = options.value.showHidden;
  369. final isWindows = options.value.isWindows;
  370. // process /C:\ -> C:\ on Windows
  371. if (isWindows && path.length > 1 && path[0] == '/') {
  372. path = path.substring(1);
  373. if (path[path.length - 1] != '\\') {
  374. path = "$path\\";
  375. }
  376. }
  377. try {
  378. final fd = await fileFetcher.fetchDirectory(path, isLocal, showHidden);
  379. fd.format(isWindows, sort: sortBy.value);
  380. directory.value = fd;
  381. } catch (e) {
  382. debugPrint("Failed to openDirectory $path: $e");
  383. }
  384. }
  385. void pushHistory() {
  386. if (history.isNotEmpty && history.last == directory.value.path) {
  387. return;
  388. }
  389. history.add(directory.value.path);
  390. }
  391. void goToHomeDirectory() {
  392. if (isLocal) {
  393. openDirectory(homePath);
  394. return;
  395. }
  396. homePath = "";
  397. openDirectory(homePath);
  398. }
  399. void goBack() {
  400. if (history.isEmpty) return;
  401. final path = history.removeAt(history.length - 1);
  402. if (path.isEmpty) return;
  403. if (directory.value.path == path) {
  404. goBack();
  405. return;
  406. }
  407. openDirectory(path, isBack: true);
  408. }
  409. void goToParentDirectory() {
  410. final isWindows = options.value.isWindows;
  411. final dirPath = directory.value.path;
  412. var parent = PathUtil.dirname(dirPath, isWindows);
  413. // specially for C:\, D:\, goto '/'
  414. if (parent == dirPath && isWindows) {
  415. openDirectory('/');
  416. return;
  417. }
  418. openDirectory(parent);
  419. }
  420. // TODO deprecated this
  421. void initDirAndHome(Map<String, dynamic> evt) {
  422. try {
  423. final fd = FileDirectory.fromJson(jsonDecode(evt['value']));
  424. fd.format(options.value.isWindows, sort: sortBy.value);
  425. if (fd.id > 0) {
  426. final jobIndex = jobController.getJob(fd.id);
  427. if (jobIndex != -1) {
  428. final job = jobController.jobTable[jobIndex];
  429. var totalSize = 0;
  430. var fileCount = fd.entries.length;
  431. for (var element in fd.entries) {
  432. totalSize += element.size;
  433. }
  434. job.totalSize = totalSize;
  435. job.fileCount = fileCount;
  436. debugPrint("update receive details: ${fd.path}");
  437. jobController.jobTable.refresh();
  438. }
  439. } else if (options.value.home.isEmpty) {
  440. options.value.home = fd.path;
  441. debugPrint("init remote home: ${fd.path}");
  442. directory.value = fd;
  443. }
  444. } catch (e) {
  445. debugPrint("initDirAndHome err=$e");
  446. }
  447. }
  448. /// sendFiles from current side (FileController.isLocal) to other side (SelectedItems).
  449. Future<void> sendFiles(
  450. SelectedItems items, DirectoryData otherSideData) async {
  451. /// ignore wrong items side status
  452. if (items.isLocal != isLocal) {
  453. return;
  454. }
  455. // alias
  456. final isRemoteToLocal = !isLocal;
  457. final toPath = otherSideData.directory.path;
  458. final isWindows = otherSideData.options.isWindows;
  459. final showHidden = otherSideData.options.showHidden;
  460. for (var from in items.items) {
  461. final jobID = jobController.addTransferJob(from, isRemoteToLocal);
  462. bind.sessionSendFiles(
  463. sessionId: sessionId,
  464. actId: jobID,
  465. path: from.path,
  466. to: PathUtil.join(toPath, from.name, isWindows),
  467. fileNum: 0,
  468. includeHidden: showHidden,
  469. isRemote: isRemoteToLocal,
  470. isDir: from.isDirectory);
  471. debugPrint(
  472. "path: ${from.path}, toPath: $toPath, to: ${PathUtil.join(toPath, from.name, isWindows)}");
  473. }
  474. if (isWeb ||
  475. (!isLocal &&
  476. versionCmp(rootState.target!.ffiModel.pi.version, '1.3.3') < 0)) {
  477. return;
  478. }
  479. final List<Entry> entrys = items.items.toList();
  480. var isRemote = isLocal == true ? true : false;
  481. await Future.forEach(entrys, (Entry item) async {
  482. if (!item.isDirectory) {
  483. return;
  484. }
  485. final List<String> paths = [];
  486. final emptyDirs =
  487. await fileFetcher.readEmptyDirs(item.path, isLocal, showHidden);
  488. if (emptyDirs.isEmpty) {
  489. return;
  490. } else {
  491. for (var dir in emptyDirs) {
  492. paths.add(dir.path);
  493. }
  494. }
  495. final dirs = paths.map((path) {
  496. return PathUtil.getOtherSidePath(directory.value.path, path,
  497. options.value.isWindows, toPath, isWindows);
  498. });
  499. for (var dir in dirs) {
  500. createDirWithRemote(dir, isRemote);
  501. }
  502. });
  503. }
  504. bool _removeCheckboxRemember = false;
  505. Future<void> removeAction(SelectedItems items) async {
  506. _removeCheckboxRemember = false;
  507. if (items.isLocal != isLocal) {
  508. debugPrint("Failed to removeFile, wrong files");
  509. return;
  510. }
  511. final isWindows = options.value.isWindows;
  512. await Future.forEach(items.items, (Entry item) async {
  513. final jobID = JobController.jobID.next();
  514. var title = "";
  515. var content = "";
  516. late final List<Entry> entries;
  517. if (item.isFile) {
  518. title = translate("Are you sure you want to delete this file?");
  519. content = item.name;
  520. entries = [item];
  521. } else if (item.isDirectory) {
  522. title = translate("Not an empty directory");
  523. dialogManager?.showLoading(translate("Waiting"));
  524. final fd = await fileFetcher.fetchDirectoryRecursiveToRemove(
  525. jobID, item.path, items.isLocal, true);
  526. if (fd.path.isEmpty) {
  527. fd.path = item.path;
  528. }
  529. fd.format(isWindows);
  530. dialogManager?.dismissAll();
  531. if (fd.entries.isEmpty) {
  532. var deleteJobId = jobController.addDeleteDirJob(item, !isLocal, 0);
  533. final confirm = await showRemoveDialog(
  534. translate(
  535. "Are you sure you want to delete this empty directory?"),
  536. item.name,
  537. false);
  538. if (confirm == true) {
  539. sendRemoveEmptyDir(
  540. item.path,
  541. 0,
  542. deleteJobId,
  543. );
  544. } else {
  545. jobController.updateJobStatus(deleteJobId,
  546. error: "cancel", state: JobState.done);
  547. }
  548. return;
  549. }
  550. entries = fd.entries;
  551. } else {
  552. entries = [];
  553. }
  554. int deleteJobId;
  555. if (item.isDirectory) {
  556. deleteJobId =
  557. jobController.addDeleteDirJob(item, !isLocal, entries.length);
  558. } else {
  559. deleteJobId = jobController.addDeleteFileJob(item, !isLocal);
  560. }
  561. for (var i = 0; i < entries.length; i++) {
  562. final dirShow = item.isDirectory
  563. ? "${translate("Are you sure you want to delete the file of this directory?")}\n"
  564. : "";
  565. final count = entries.length > 1 ? "${i + 1}/${entries.length}" : "";
  566. content = "$dirShow\n\n${entries[i].path}".trim();
  567. final confirm = await showRemoveDialog(
  568. count.isEmpty ? title : "$title ($count)",
  569. content,
  570. item.isDirectory,
  571. );
  572. try {
  573. if (confirm == true) {
  574. sendRemoveFile(entries[i].path, i, deleteJobId);
  575. final res = await jobController.jobResultListener.start();
  576. // handle remove res;
  577. if (item.isDirectory &&
  578. res['file_num'] == (entries.length - 1).toString()) {
  579. sendRemoveEmptyDir(item.path, i, deleteJobId);
  580. }
  581. } else {
  582. jobController.updateJobStatus(deleteJobId,
  583. file_num: i, error: "cancel");
  584. }
  585. if (_removeCheckboxRemember) {
  586. if (confirm == true) {
  587. for (var j = i + 1; j < entries.length; j++) {
  588. sendRemoveFile(entries[j].path, j, deleteJobId);
  589. final res = await jobController.jobResultListener.start();
  590. if (item.isDirectory &&
  591. res['file_num'] == (entries.length - 1).toString()) {
  592. sendRemoveEmptyDir(item.path, i, deleteJobId);
  593. }
  594. }
  595. } else {
  596. jobController.updateJobStatus(deleteJobId,
  597. error: "cancel",
  598. file_num: entries.length,
  599. state: JobState.done);
  600. }
  601. break;
  602. }
  603. } catch (e) {
  604. print("remove error: $e");
  605. }
  606. }
  607. });
  608. refresh();
  609. }
  610. Future<bool?> showRemoveDialog(
  611. String title, String content, bool showCheckbox) async {
  612. return await dialogManager?.show<bool>(
  613. (setState, Function(bool v) close, context) {
  614. cancel() => close(false);
  615. submit() => close(true);
  616. return CustomAlertDialog(
  617. title: Row(
  618. mainAxisAlignment: MainAxisAlignment.center,
  619. children: [
  620. const Icon(Icons.warning_rounded, color: Colors.red),
  621. Expanded(
  622. child: Text(title).paddingOnly(
  623. left: 10,
  624. ),
  625. ),
  626. ],
  627. ),
  628. contentBoxConstraints:
  629. BoxConstraints(minHeight: 100, minWidth: 400, maxWidth: 400),
  630. content: Column(
  631. crossAxisAlignment: CrossAxisAlignment.start,
  632. children: [
  633. Text(content),
  634. Text(
  635. translate("This is irreversible!"),
  636. style: const TextStyle(
  637. fontWeight: FontWeight.bold,
  638. color: Colors.red,
  639. ),
  640. ).paddingOnly(top: 20),
  641. showCheckbox
  642. ? CheckboxListTile(
  643. contentPadding: const EdgeInsets.all(0),
  644. dense: true,
  645. controlAffinity: ListTileControlAffinity.leading,
  646. title: Text(
  647. translate("Do this for all conflicts"),
  648. ),
  649. value: _removeCheckboxRemember,
  650. onChanged: (v) {
  651. if (v == null) return;
  652. setState(() => _removeCheckboxRemember = v);
  653. },
  654. )
  655. : const SizedBox.shrink()
  656. ],
  657. ),
  658. actions: [
  659. dialogButton(
  660. "Cancel",
  661. icon: Icon(Icons.close_rounded),
  662. onPressed: cancel,
  663. isOutline: true,
  664. ),
  665. dialogButton(
  666. "OK",
  667. icon: Icon(Icons.done_rounded),
  668. onPressed: submit,
  669. ),
  670. ],
  671. onSubmit: submit,
  672. onCancel: cancel,
  673. );
  674. }, useAnimation: false);
  675. }
  676. void sendRemoveFile(String path, int fileNum, int actId) {
  677. bind.sessionRemoveFile(
  678. sessionId: sessionId,
  679. actId: actId,
  680. path: path,
  681. isRemote: !isLocal,
  682. fileNum: fileNum);
  683. }
  684. void sendRemoveEmptyDir(String path, int fileNum, int actId) {
  685. history.removeWhere((element) => element.contains(path));
  686. bind.sessionRemoveAllEmptyDirs(
  687. sessionId: sessionId, actId: actId, path: path, isRemote: !isLocal);
  688. }
  689. Future<void> createDirWithRemote(String path, bool isRemote) async {
  690. bind.sessionCreateDir(
  691. sessionId: sessionId,
  692. actId: JobController.jobID.next(),
  693. path: path,
  694. isRemote: isRemote);
  695. }
  696. Future<void> createDir(String path) async {
  697. await createDirWithRemote(path, !isLocal);
  698. }
  699. Future<void> renameAction(Entry item, bool isLocal) async {
  700. final textEditingController = TextEditingController(text: item.name);
  701. String? errorText;
  702. dialogManager?.show((setState, close, context) {
  703. textEditingController.addListener(() {
  704. if (errorText != null) {
  705. setState(() {
  706. errorText = null;
  707. });
  708. }
  709. });
  710. submit() async {
  711. final newName = textEditingController.text;
  712. if (newName.isEmpty || newName == item.name) {
  713. close();
  714. return;
  715. }
  716. if (directory.value.entries.any((e) => e.name == newName)) {
  717. setState(() {
  718. errorText = translate("Already exists");
  719. });
  720. return;
  721. }
  722. if (!PathUtil.validName(newName, options.value.isWindows)) {
  723. setState(() {
  724. if (item.isDirectory) {
  725. errorText = translate("Invalid folder name");
  726. } else {
  727. errorText = translate("Invalid file name");
  728. }
  729. });
  730. return;
  731. }
  732. await bind.sessionRenameFile(
  733. sessionId: sessionId,
  734. actId: JobController.jobID.next(),
  735. path: item.path,
  736. newName: newName,
  737. isRemote: !isLocal);
  738. close();
  739. }
  740. return CustomAlertDialog(
  741. content: Column(
  742. children: [
  743. DialogTextField(
  744. title: '${translate('Rename')} ${item.name}',
  745. controller: textEditingController,
  746. errorText: errorText,
  747. ),
  748. ],
  749. ),
  750. actions: [
  751. dialogButton(
  752. "Cancel",
  753. icon: Icon(Icons.close_rounded),
  754. onPressed: close,
  755. isOutline: true,
  756. ),
  757. dialogButton(
  758. "OK",
  759. icon: Icon(Icons.done_rounded),
  760. onPressed: submit,
  761. ),
  762. ],
  763. onSubmit: submit,
  764. onCancel: close,
  765. );
  766. });
  767. }
  768. }
  769. const _kOneWayFileTransferError = 'one-way-file-transfer-tip';
  770. class JobController {
  771. static final JobID jobID = JobID();
  772. final jobTable = List<JobProgress>.empty(growable: true).obs;
  773. final jobResultListener = JobResultListener<Map<String, dynamic>>();
  774. final GetSessionID getSessionID;
  775. final GetDialogManager getDialogManager;
  776. SessionID get sessionId => getSessionID();
  777. OverlayDialogManager? get alogManager => getDialogManager();
  778. int _lastTimeShowMsgbox = DateTime.now().millisecondsSinceEpoch;
  779. JobController(this.getSessionID, this.getDialogManager);
  780. int getJob(int id) {
  781. return jobTable.indexWhere((element) => element.id == id);
  782. }
  783. // return jobID
  784. int addTransferJob(Entry from, bool isRemoteToLocal) {
  785. final jobID = JobController.jobID.next();
  786. jobTable.add(JobProgress()
  787. ..type = JobType.transfer
  788. ..fileName = path.basename(from.path)
  789. ..jobName = from.path
  790. ..totalSize = from.size
  791. ..state = JobState.inProgress
  792. ..id = jobID
  793. ..isRemoteToLocal = isRemoteToLocal);
  794. return jobID;
  795. }
  796. int addDeleteFileJob(Entry file, bool isRemote) {
  797. final jobID = JobController.jobID.next();
  798. jobTable.add(JobProgress()
  799. ..type = JobType.deleteFile
  800. ..fileName = path.basename(file.path)
  801. ..jobName = file.path
  802. ..totalSize = file.size
  803. ..state = JobState.none
  804. ..id = jobID
  805. ..isRemoteToLocal = isRemote);
  806. return jobID;
  807. }
  808. int addDeleteDirJob(Entry file, bool isRemote, int fileCount) {
  809. final jobID = JobController.jobID.next();
  810. jobTable.add(JobProgress()
  811. ..type = JobType.deleteDir
  812. ..fileName = path.basename(file.path)
  813. ..jobName = file.path
  814. ..fileCount = fileCount
  815. ..totalSize = file.size
  816. ..state = JobState.none
  817. ..id = jobID
  818. ..isRemoteToLocal = isRemote);
  819. return jobID;
  820. }
  821. void tryUpdateJobProgress(Map<String, dynamic> evt) {
  822. try {
  823. int id = int.parse(evt['id']);
  824. // id = index + 1
  825. final jobIndex = getJob(id);
  826. if (jobIndex >= 0 && jobTable.length > jobIndex) {
  827. final job = jobTable[jobIndex];
  828. job.fileNum = int.parse(evt['file_num']);
  829. job.speed = double.parse(evt['speed']);
  830. job.finishedSize = int.parse(evt['finished_size']);
  831. job.recvJobRes = true;
  832. jobTable.refresh();
  833. }
  834. } catch (e) {
  835. debugPrint("Failed to tryUpdateJobProgress, evt: ${evt.toString()}");
  836. }
  837. }
  838. Future<bool> jobDone(Map<String, dynamic> evt) async {
  839. if (jobResultListener.isListening) {
  840. jobResultListener.complete(evt);
  841. // return;
  842. }
  843. int id = -1;
  844. int? fileNum = 0;
  845. double? speed = 0;
  846. try {
  847. id = int.parse(evt['id']);
  848. } catch (_) {}
  849. final jobIndex = getJob(id);
  850. if (jobIndex == -1) return true;
  851. final job = jobTable[jobIndex];
  852. job.recvJobRes = true;
  853. if (job.type == JobType.deleteFile) {
  854. job.state = JobState.done;
  855. } else if (job.type == JobType.deleteDir) {
  856. try {
  857. fileNum = int.tryParse(evt['file_num']);
  858. } catch (_) {}
  859. if (fileNum != null) {
  860. if (fileNum < job.fileNum) return true; // file_num can be 0 at last
  861. job.fileNum = fileNum;
  862. if (fileNum >= job.fileCount - 1) {
  863. job.state = JobState.done;
  864. }
  865. }
  866. } else {
  867. try {
  868. fileNum = int.tryParse(evt['file_num']);
  869. speed = double.tryParse(evt['speed']);
  870. } catch (_) {}
  871. if (fileNum != null) job.fileNum = fileNum;
  872. if (speed != null) job.speed = speed;
  873. job.state = JobState.done;
  874. }
  875. jobTable.refresh();
  876. if (job.type == JobType.deleteDir) {
  877. return job.state == JobState.done;
  878. } else {
  879. return true;
  880. }
  881. }
  882. void jobError(Map<String, dynamic> evt) {
  883. final err = evt['err'].toString();
  884. int jobIndex = getJob(int.parse(evt['id']));
  885. if (jobIndex != -1) {
  886. final job = jobTable[jobIndex];
  887. job.state = JobState.error;
  888. job.err = err;
  889. job.recvJobRes = true;
  890. if (job.type == JobType.transfer) {
  891. int? fileNum = int.tryParse(evt['file_num']);
  892. if (fileNum != null) job.fileNum = fileNum;
  893. if (err == "skipped") {
  894. job.state = JobState.done;
  895. job.finishedSize = job.totalSize;
  896. }
  897. } else if (job.type == JobType.deleteDir) {
  898. if (jobResultListener.isListening) {
  899. jobResultListener.complete(evt);
  900. }
  901. int? fileNum = int.tryParse(evt['file_num']);
  902. if (fileNum != null) job.fileNum = fileNum;
  903. } else if (job.type == JobType.deleteFile) {
  904. if (jobResultListener.isListening) {
  905. jobResultListener.complete(evt);
  906. }
  907. }
  908. jobTable.refresh();
  909. }
  910. if (err == _kOneWayFileTransferError) {
  911. if (DateTime.now().millisecondsSinceEpoch - _lastTimeShowMsgbox > 3000) {
  912. final dm = alogManager;
  913. if (dm != null) {
  914. _lastTimeShowMsgbox = DateTime.now().millisecondsSinceEpoch;
  915. msgBox(sessionId, 'custom-nocancel', 'Error', err, '', dm);
  916. }
  917. }
  918. }
  919. debugPrint("jobError $evt");
  920. }
  921. void updateJobStatus(int id,
  922. {int? file_num, String? error, JobState? state}) {
  923. final jobIndex = getJob(id);
  924. if (jobIndex < 0) return;
  925. final job = jobTable[jobIndex];
  926. job.recvJobRes = true;
  927. if (file_num != null) {
  928. job.fileNum = file_num;
  929. }
  930. if (error != null) {
  931. job.err = error;
  932. job.state = JobState.error;
  933. }
  934. if (state != null) {
  935. job.state = state;
  936. }
  937. if (job.type == JobType.deleteFile && error == null) {
  938. job.state = JobState.done;
  939. }
  940. jobTable.refresh();
  941. }
  942. Future<void> cancelJob(int id) async {
  943. await bind.sessionCancelJob(sessionId: sessionId, actId: id);
  944. }
  945. void loadLastJob(Map<String, dynamic> evt) {
  946. debugPrint("load last job: $evt");
  947. Map<String, dynamic> jobDetail = json.decode(evt['value']);
  948. // int id = int.parse(jobDetail['id']);
  949. String remote = jobDetail['remote'];
  950. String to = jobDetail['to'];
  951. bool showHidden = jobDetail['show_hidden'];
  952. int fileNum = jobDetail['file_num'];
  953. bool isRemote = jobDetail['is_remote'];
  954. final currJobId = JobController.jobID.next();
  955. String fileName = path.basename(isRemote ? remote : to);
  956. var jobProgress = JobProgress()
  957. ..type = JobType.transfer
  958. ..fileName = fileName
  959. ..jobName = isRemote ? remote : to
  960. ..id = currJobId
  961. ..isRemoteToLocal = isRemote
  962. ..fileNum = fileNum
  963. ..remote = remote
  964. ..to = to
  965. ..showHidden = showHidden
  966. ..state = JobState.paused;
  967. jobTable.add(jobProgress);
  968. bind.sessionAddJob(
  969. sessionId: sessionId,
  970. isRemote: isRemote,
  971. includeHidden: showHidden,
  972. actId: currJobId,
  973. path: isRemote ? remote : to,
  974. to: isRemote ? to : remote,
  975. fileNum: fileNum,
  976. );
  977. }
  978. void resumeJob(int jobId) {
  979. final jobIndex = getJob(jobId);
  980. if (jobIndex != -1) {
  981. final job = jobTable[jobIndex];
  982. bind.sessionResumeJob(
  983. sessionId: sessionId, actId: job.id, isRemote: job.isRemoteToLocal);
  984. job.state = JobState.inProgress;
  985. jobTable.refresh();
  986. } else {
  987. debugPrint("jobId $jobId is not exists");
  988. }
  989. }
  990. void updateFolderFiles(Map<String, dynamic> evt) {
  991. // ret: "{\"id\":1,\"num_entries\":12,\"total_size\":1264822.0}"
  992. Map<String, dynamic> info = json.decode(evt['info']);
  993. int id = info['id'];
  994. int num_entries = info['num_entries'];
  995. double total_size = info['total_size'];
  996. final jobIndex = getJob(id);
  997. if (jobIndex != -1) {
  998. final job = jobTable[jobIndex];
  999. job.fileCount = num_entries;
  1000. job.totalSize = total_size.toInt();
  1001. jobTable.refresh();
  1002. }
  1003. debugPrint("update folder files: $info");
  1004. }
  1005. }
  1006. class JobResultListener<T> {
  1007. Completer<T>? _completer;
  1008. Timer? _timer;
  1009. final int _timeoutSecond = 5;
  1010. bool get isListening => _completer != null;
  1011. clear() {
  1012. if (_completer != null) {
  1013. _timer?.cancel();
  1014. _timer = null;
  1015. _completer!.completeError("Cancel manually");
  1016. _completer = null;
  1017. return;
  1018. }
  1019. }
  1020. Future<T> start() {
  1021. if (_completer != null) return Future.error("Already start listen");
  1022. _completer = Completer();
  1023. _timer = Timer(Duration(seconds: _timeoutSecond), () {
  1024. if (!_completer!.isCompleted) {
  1025. _completer!.completeError("Time out");
  1026. }
  1027. _completer = null;
  1028. });
  1029. return _completer!.future;
  1030. }
  1031. complete(T res) {
  1032. if (_completer != null) {
  1033. _timer?.cancel();
  1034. _timer = null;
  1035. _completer!.complete(res);
  1036. _completer = null;
  1037. return;
  1038. }
  1039. }
  1040. }
  1041. class FileFetcher {
  1042. // Map<String,Completer<FileDirectory>> localTasks = {}; // now we only use read local dir sync
  1043. Map<String, Completer<FileDirectory>> remoteTasks = {};
  1044. Map<String, Completer<List<FileDirectory>>> remoteEmptyDirsTasks = {};
  1045. Map<int, Completer<FileDirectory>> readRecursiveTasks = {};
  1046. final GetSessionID getSessionID;
  1047. SessionID get sessionId => getSessionID();
  1048. FileFetcher(this.getSessionID);
  1049. Future<List<FileDirectory>> registerReadEmptyDirsTask(
  1050. bool isLocal, String path) {
  1051. // final jobs = isLocal?localJobs:remoteJobs; // maybe we will use read local dir async later
  1052. final tasks = remoteEmptyDirsTasks; // bypass now
  1053. if (tasks.containsKey(path)) {
  1054. throw "Failed to registerReadEmptyDirsTask, already have same read job";
  1055. }
  1056. final c = Completer<List<FileDirectory>>();
  1057. tasks[path] = c;
  1058. Timer(Duration(seconds: 2), () {
  1059. tasks.remove(path);
  1060. if (c.isCompleted) return;
  1061. c.completeError("Failed to read empty dirs, timeout");
  1062. });
  1063. return c.future;
  1064. }
  1065. Future<FileDirectory> registerReadTask(bool isLocal, String path) {
  1066. // final jobs = isLocal?localJobs:remoteJobs; // maybe we will use read local dir async later
  1067. final tasks = remoteTasks; // bypass now
  1068. if (tasks.containsKey(path)) {
  1069. throw "Failed to registerReadTask, already have same read job";
  1070. }
  1071. final c = Completer<FileDirectory>();
  1072. tasks[path] = c;
  1073. Timer(Duration(seconds: 2), () {
  1074. tasks.remove(path);
  1075. if (c.isCompleted) return;
  1076. c.completeError("Failed to read dir, timeout");
  1077. });
  1078. return c.future;
  1079. }
  1080. Future<FileDirectory> registerReadRecursiveTask(int actID) {
  1081. final tasks = readRecursiveTasks;
  1082. if (tasks.containsKey(actID)) {
  1083. throw "Failed to registerRemoveTask, already have same ReadRecursive job";
  1084. }
  1085. final c = Completer<FileDirectory>();
  1086. tasks[actID] = c;
  1087. Timer(Duration(seconds: 2), () {
  1088. tasks.remove(actID);
  1089. if (c.isCompleted) return;
  1090. c.completeError("Failed to read dir, timeout");
  1091. });
  1092. return c.future;
  1093. }
  1094. tryCompleteEmptyDirsTask(String? msg, String? isLocalStr) {
  1095. if (msg == null || isLocalStr == null) return;
  1096. late final Map<String, Completer<List<FileDirectory>>> tasks;
  1097. try {
  1098. final map = jsonDecode(msg);
  1099. final String path = map["path"];
  1100. final List<dynamic> fdJsons = map["empty_dirs"];
  1101. final List<FileDirectory> fds =
  1102. fdJsons.map((fdJson) => FileDirectory.fromJson(fdJson)).toList();
  1103. tasks = remoteEmptyDirsTasks;
  1104. final completer = tasks.remove(path);
  1105. completer?.complete(fds);
  1106. } catch (e) {
  1107. debugPrint("tryCompleteJob err: $e");
  1108. }
  1109. }
  1110. tryCompleteTask(String? msg, String? isLocalStr) {
  1111. if (msg == null || isLocalStr == null) return;
  1112. late final Map<Object, Completer<FileDirectory>> tasks;
  1113. try {
  1114. final fd = FileDirectory.fromJson(jsonDecode(msg));
  1115. if (fd.id > 0) {
  1116. // fd.id > 0 is result for read recursive
  1117. // to-do later,will be better if every fetch use ID,so that there will only one task map for read and recursive read
  1118. tasks = readRecursiveTasks;
  1119. final completer = tasks.remove(fd.id);
  1120. completer?.complete(fd);
  1121. } else if (fd.path.isNotEmpty) {
  1122. // result for normal read dir
  1123. // final jobs = isLocal?localJobs:remoteJobs; // maybe we will use read local dir async later
  1124. tasks = remoteTasks; // bypass now
  1125. final completer = tasks.remove(fd.path);
  1126. completer?.complete(fd);
  1127. }
  1128. } catch (e) {
  1129. debugPrint("tryCompleteJob err: $e");
  1130. }
  1131. }
  1132. Future<List<FileDirectory>> readEmptyDirs(
  1133. String path, bool isLocal, bool showHidden) async {
  1134. try {
  1135. if (isLocal) {
  1136. final res = await bind.sessionReadLocalEmptyDirsRecursiveSync(
  1137. sessionId: sessionId, path: path, includeHidden: showHidden);
  1138. final List<dynamic> fdJsons = jsonDecode(res);
  1139. final List<FileDirectory> fds =
  1140. fdJsons.map((fdJson) => FileDirectory.fromJson(fdJson)).toList();
  1141. return fds;
  1142. } else {
  1143. await bind.sessionReadRemoteEmptyDirsRecursiveSync(
  1144. sessionId: sessionId, path: path, includeHidden: showHidden);
  1145. return registerReadEmptyDirsTask(isLocal, path);
  1146. }
  1147. } catch (e) {
  1148. return Future.error(e);
  1149. }
  1150. }
  1151. Future<FileDirectory> fetchDirectory(
  1152. String path, bool isLocal, bool showHidden) async {
  1153. try {
  1154. if (isLocal) {
  1155. final res = await bind.sessionReadLocalDirSync(
  1156. sessionId: sessionId, path: path, showHidden: showHidden);
  1157. final fd = FileDirectory.fromJson(jsonDecode(res));
  1158. return fd;
  1159. } else {
  1160. await bind.sessionReadRemoteDir(
  1161. sessionId: sessionId, path: path, includeHidden: showHidden);
  1162. return registerReadTask(isLocal, path);
  1163. }
  1164. } catch (e) {
  1165. return Future.error(e);
  1166. }
  1167. }
  1168. Future<FileDirectory> fetchDirectoryRecursiveToRemove(
  1169. int actID, String path, bool isLocal, bool showHidden) async {
  1170. // TODO test Recursive is show hidden default?
  1171. try {
  1172. await bind.sessionReadDirToRemoveRecursive(
  1173. sessionId: sessionId,
  1174. actId: actID,
  1175. path: path,
  1176. isRemote: !isLocal,
  1177. showHidden: showHidden);
  1178. return registerReadRecursiveTask(actID);
  1179. } catch (e) {
  1180. return Future.error(e);
  1181. }
  1182. }
  1183. }
  1184. class FileDirectory {
  1185. List<Entry> entries = [];
  1186. int id = 0;
  1187. String path = "";
  1188. FileDirectory();
  1189. FileDirectory.fromJson(Map<String, dynamic> json) {
  1190. id = json['id'];
  1191. path = json['path'];
  1192. json['entries'].forEach((v) {
  1193. entries.add(Entry.fromJson(v));
  1194. });
  1195. }
  1196. // generate full path for every entry , init sort style if need.
  1197. format(bool isWindows, {SortBy? sort}) {
  1198. for (var entry in entries) {
  1199. entry.path = PathUtil.join(path, entry.name, isWindows);
  1200. }
  1201. if (sort != null) {
  1202. changeSortStyle(sort);
  1203. }
  1204. }
  1205. changeSortStyle(SortBy sort, {bool ascending = true}) {
  1206. entries = _sortList(entries, sort, ascending);
  1207. }
  1208. clear() {
  1209. entries = [];
  1210. id = 0;
  1211. path = "";
  1212. }
  1213. }
  1214. class Entry {
  1215. int entryType = 4;
  1216. int modifiedTime = 0;
  1217. String name = "";
  1218. String path = "";
  1219. int size = 0;
  1220. Entry();
  1221. Entry.fromJson(Map<String, dynamic> json) {
  1222. entryType = json['entry_type'];
  1223. modifiedTime = json['modified_time'];
  1224. name = json['name'];
  1225. size = json['size'];
  1226. }
  1227. bool get isFile => entryType > 3;
  1228. bool get isDirectory => entryType < 3;
  1229. bool get isDrive => entryType == 3;
  1230. DateTime lastModified() {
  1231. return DateTime.fromMillisecondsSinceEpoch(modifiedTime * 1000);
  1232. }
  1233. }
  1234. enum JobState { none, inProgress, done, error, paused }
  1235. extension JobStateDisplay on JobState {
  1236. String display() {
  1237. switch (this) {
  1238. case JobState.none:
  1239. return translate("Waiting");
  1240. case JobState.inProgress:
  1241. return translate("Transfer file");
  1242. case JobState.done:
  1243. return translate("Finished");
  1244. case JobState.error:
  1245. return translate("Error");
  1246. default:
  1247. return "";
  1248. }
  1249. }
  1250. }
  1251. enum JobType { none, transfer, deleteFile, deleteDir }
  1252. class JobProgress {
  1253. JobType type = JobType.none;
  1254. JobState state = JobState.none;
  1255. var recvJobRes = false;
  1256. var id = 0;
  1257. var fileNum = 0;
  1258. var speed = 0.0;
  1259. var finishedSize = 0;
  1260. var totalSize = 0;
  1261. var fileCount = 0;
  1262. // [isRemote == true] means [remote -> local]
  1263. // var isRemote = false;
  1264. // to-do use enum
  1265. var isRemoteToLocal = false;
  1266. var jobName = "";
  1267. var fileName = "";
  1268. var remote = "";
  1269. var to = "";
  1270. var showHidden = false;
  1271. var err = "";
  1272. int lastTransferredSize = 0;
  1273. clear() {
  1274. type = JobType.none;
  1275. state = JobState.none;
  1276. recvJobRes = false;
  1277. id = 0;
  1278. fileNum = 0;
  1279. speed = 0;
  1280. finishedSize = 0;
  1281. jobName = "";
  1282. fileName = "";
  1283. fileCount = 0;
  1284. remote = "";
  1285. to = "";
  1286. err = "";
  1287. }
  1288. String display() {
  1289. if (type == JobType.transfer) {
  1290. if (state == JobState.done && err == "skipped") {
  1291. return translate("Skipped");
  1292. }
  1293. } else if (type == JobType.deleteFile) {
  1294. if (err == "cancel") {
  1295. return translate("Cancel");
  1296. }
  1297. }
  1298. return state.display();
  1299. }
  1300. String getStatus() {
  1301. int handledFileCount = recvJobRes ? fileNum + 1 : fileNum;
  1302. if (handledFileCount >= fileCount) {
  1303. handledFileCount = fileCount;
  1304. }
  1305. if (state == JobState.done) {
  1306. handledFileCount = fileCount;
  1307. finishedSize = totalSize;
  1308. }
  1309. final filesStr = "$handledFileCount/$fileCount files";
  1310. final sizeStr = totalSize > 0 ? readableFileSize(totalSize.toDouble()) : "";
  1311. final sizePercentStr = totalSize > 0 && finishedSize > 0
  1312. ? "${readableFileSize(finishedSize.toDouble())} / ${readableFileSize(totalSize.toDouble())}"
  1313. : "";
  1314. if (type == JobType.deleteFile) {
  1315. return display();
  1316. } else if (type == JobType.deleteDir) {
  1317. var res = '';
  1318. if (state == JobState.done || state == JobState.error) {
  1319. res = display();
  1320. }
  1321. if (filesStr.isNotEmpty) {
  1322. if (res.isNotEmpty) {
  1323. res += " ";
  1324. }
  1325. res += filesStr;
  1326. }
  1327. if (sizeStr.isNotEmpty) {
  1328. if (res.isNotEmpty) {
  1329. res += ", ";
  1330. }
  1331. res += sizeStr;
  1332. }
  1333. return res;
  1334. } else if (type == JobType.transfer) {
  1335. var res = "";
  1336. if (state != JobState.inProgress && state != JobState.none) {
  1337. res += display();
  1338. }
  1339. if (filesStr.isNotEmpty) {
  1340. if (res.isNotEmpty) {
  1341. res += ", ";
  1342. }
  1343. res += filesStr;
  1344. }
  1345. if (sizeStr.isNotEmpty && state != JobState.inProgress) {
  1346. if (res.isNotEmpty) {
  1347. res += ", ";
  1348. }
  1349. res += sizeStr;
  1350. }
  1351. if (sizePercentStr.isNotEmpty && state == JobState.inProgress) {
  1352. if (res.isNotEmpty) {
  1353. res += ", ";
  1354. }
  1355. res += sizePercentStr;
  1356. }
  1357. return res;
  1358. }
  1359. return '';
  1360. }
  1361. }
  1362. class _PathStat {
  1363. final String path;
  1364. final DateTime dateTime;
  1365. _PathStat(this.path, this.dateTime);
  1366. }
  1367. class PathUtil {
  1368. static final windowsContext = path.Context(style: path.Style.windows);
  1369. static final posixContext = path.Context(style: path.Style.posix);
  1370. static String getOtherSidePath(String mainRootPath, String mainPath,
  1371. bool isMainWindows, String otherRootPath, bool isOtherWindows) {
  1372. final mainPathUtil = isMainWindows ? windowsContext : posixContext;
  1373. final relativePath = mainPathUtil.relative(mainPath, from: mainRootPath);
  1374. final names = mainPathUtil.split(relativePath);
  1375. final otherPathUtil = isOtherWindows ? windowsContext : posixContext;
  1376. String path = otherRootPath;
  1377. for (var name in names) {
  1378. path = otherPathUtil.join(path, name);
  1379. }
  1380. return path;
  1381. }
  1382. static String join(String path1, String path2, bool isWindows) {
  1383. final pathUtil = isWindows ? windowsContext : posixContext;
  1384. return pathUtil.join(path1, path2);
  1385. }
  1386. static List<String> split(String path, bool isWindows) {
  1387. final pathUtil = isWindows ? windowsContext : posixContext;
  1388. return pathUtil.split(path);
  1389. }
  1390. static String convert(String path, bool isMainWindows, bool isOtherWindows) {
  1391. final mainPathUtil = isMainWindows ? windowsContext : posixContext;
  1392. final otherPathUtil = isOtherWindows ? windowsContext : posixContext;
  1393. return otherPathUtil.joinAll(mainPathUtil.split(path));
  1394. }
  1395. static String dirname(String path, bool isWindows) {
  1396. final pathUtil = isWindows ? windowsContext : posixContext;
  1397. return pathUtil.dirname(path);
  1398. }
  1399. static bool validName(String name, bool isWindows) {
  1400. final unixFileNamePattern = RegExp(r'^[^/\0]+$');
  1401. final windowsFileNamePattern = RegExp(r'^[^<>:"/\\|?*]+$');
  1402. final reg = isWindows ? windowsFileNamePattern : unixFileNamePattern;
  1403. return reg.hasMatch(name);
  1404. }
  1405. }
  1406. class DirectoryOptions {
  1407. String home;
  1408. bool showHidden;
  1409. bool isWindows;
  1410. DirectoryOptions(
  1411. {this.home = "", this.showHidden = false, this.isWindows = false});
  1412. clear() {
  1413. home = "";
  1414. showHidden = false;
  1415. isWindows = false;
  1416. }
  1417. }
  1418. class SelectedItems {
  1419. final bool isLocal;
  1420. final items = RxList<Entry>.empty(growable: true);
  1421. SelectedItems({required this.isLocal});
  1422. void add(Entry e) {
  1423. if (e.isDrive) return;
  1424. if (!items.contains(e)) {
  1425. items.add(e);
  1426. }
  1427. }
  1428. void remove(Entry e) {
  1429. items.remove(e);
  1430. }
  1431. void clear() {
  1432. items.clear();
  1433. }
  1434. void selectAll(List<Entry> entries) {
  1435. items.clear();
  1436. items.addAll(entries);
  1437. }
  1438. static bool valid(RxList<Entry> items) {
  1439. if (items.isNotEmpty) {
  1440. // exclude DirDrive type
  1441. return items.any((item) => !item.isDrive);
  1442. }
  1443. return false;
  1444. }
  1445. }
  1446. // edited from [https://github.com/DevsOnFlutter/file_manager/blob/c1bf7f0225b15bcb86eba602c60acd5c4da90dd8/lib/file_manager.dart#L22]
  1447. List<Entry> _sortList(List<Entry> list, SortBy sortType, bool ascending) {
  1448. if (sortType == SortBy.name) {
  1449. // making list of only folders.
  1450. final dirs = list
  1451. .where((element) => element.isDirectory || element.isDrive)
  1452. .toList();
  1453. // sorting folder list by name.
  1454. dirs.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
  1455. // making list of only flies.
  1456. final files = list.where((element) => element.isFile).toList();
  1457. // sorting files list by name.
  1458. files.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
  1459. // first folders will go to list (if available) then files will go to list.
  1460. return ascending
  1461. ? [...dirs, ...files]
  1462. : [...dirs.reversed.toList(), ...files.reversed.toList()];
  1463. } else if (sortType == SortBy.modified) {
  1464. // making the list of Path & DateTime
  1465. List<_PathStat> pathStat = [];
  1466. for (Entry e in list) {
  1467. pathStat.add(_PathStat(e.name, e.lastModified()));
  1468. }
  1469. // sort _pathStat according to date
  1470. pathStat.sort((b, a) => a.dateTime.compareTo(b.dateTime));
  1471. // sorting [list] according to [_pathStat]
  1472. list.sort((a, b) => pathStat
  1473. .indexWhere((element) => element.path == a.name)
  1474. .compareTo(pathStat.indexWhere((element) => element.path == b.name)));
  1475. return ascending ? list : list.reversed.toList();
  1476. } else if (sortType == SortBy.type) {
  1477. // making list of only folders.
  1478. final dirs = list.where((element) => element.isDirectory).toList();
  1479. // sorting folders by name.
  1480. dirs.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
  1481. // making the list of files
  1482. final files = list.where((element) => element.isFile).toList();
  1483. // sorting files list by extension.
  1484. files.sort((a, b) => a.name
  1485. .toLowerCase()
  1486. .split('.')
  1487. .last
  1488. .compareTo(b.name.toLowerCase().split('.').last));
  1489. return ascending
  1490. ? [...dirs, ...files]
  1491. : [...dirs.reversed.toList(), ...files.reversed.toList()];
  1492. } else if (sortType == SortBy.size) {
  1493. // create list of path and size
  1494. Map<String, int> sizeMap = {};
  1495. for (Entry e in list) {
  1496. sizeMap[e.name] = e.size;
  1497. }
  1498. // making list of only folders.
  1499. final dirs = list.where((element) => element.isDirectory).toList();
  1500. // sorting folder list by name.
  1501. dirs.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
  1502. // making list of only flies.
  1503. final files = list.where((element) => element.isFile).toList();
  1504. // creating sorted list of [_sizeMapList] by size.
  1505. final List<MapEntry<String, int>> sizeMapList = sizeMap.entries.toList();
  1506. sizeMapList.sort((b, a) => a.value.compareTo(b.value));
  1507. // sort [list] according to [_sizeMapList]
  1508. files.sort((a, b) => sizeMapList
  1509. .indexWhere((element) => element.key == a.name)
  1510. .compareTo(sizeMapList.indexWhere((element) => element.key == b.name)));
  1511. return ascending
  1512. ? [...dirs, ...files]
  1513. : [...dirs.reversed.toList(), ...files.reversed.toList()];
  1514. }
  1515. return [];
  1516. }
  1517. /// Define a general queue which can accepts different dialog type.
  1518. ///
  1519. /// [Visibility]
  1520. /// The `_FileDialogType` and `_DialogEvent` are invisible for other models.
  1521. enum FileDialogType { overwrite, unknown }
  1522. class _FileDialogEvent extends BaseEvent<FileDialogType, Map<String, dynamic>> {
  1523. WeakReference<FileModel> fileModel;
  1524. bool? _overrideConfirm;
  1525. bool _skip = false;
  1526. _FileDialogEvent(this.fileModel, super.type, super.data);
  1527. void setOverrideConfirm(bool? confirm) {
  1528. _overrideConfirm = confirm;
  1529. }
  1530. void setSkip(bool skip) {
  1531. _skip = skip;
  1532. }
  1533. @override
  1534. EventCallback<Map<String, dynamic>>? findCallback(FileDialogType type) {
  1535. final model = fileModel.target;
  1536. if (model == null) {
  1537. return null;
  1538. }
  1539. switch (type) {
  1540. case FileDialogType.overwrite:
  1541. return (data) async {
  1542. return await model.overrideFileConfirm(data,
  1543. overrideConfirm: _overrideConfirm, skip: _skip);
  1544. };
  1545. default:
  1546. debugPrint("Unknown event type: $type with $data");
  1547. return null;
  1548. }
  1549. }
  1550. }
  1551. class FileDialogEventLoop
  1552. extends BaseEventLoop<FileDialogType, Map<String, dynamic>> {
  1553. bool? _overrideConfirm;
  1554. bool _skip = false;
  1555. @override
  1556. Future<void> onPreConsume(
  1557. BaseEvent<FileDialogType, Map<String, dynamic>> evt) async {
  1558. var event = evt as _FileDialogEvent;
  1559. event.setOverrideConfirm(_overrideConfirm);
  1560. event.setSkip(_skip);
  1561. debugPrint(
  1562. "FileDialogEventLoop: consuming<jobId: ${evt.data['id']} overrideConfirm: $_overrideConfirm, skip: $_skip>");
  1563. }
  1564. @override
  1565. Future<void> onEventsClear() {
  1566. _overrideConfirm = null;
  1567. _skip = false;
  1568. return super.onEventsClear();
  1569. }
  1570. void setOverrideConfirm(bool? confirm) {
  1571. _overrideConfirm = confirm;
  1572. }
  1573. void setSkip(bool skip) {
  1574. _skip = skip;
  1575. }
  1576. }