file_model.dart 52 KB

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