file_model.dart 47 KB

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