rustdesk/lib/models/file_model.dart
2022-03-23 15:28:21 +08:00

751 lines
22 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:async';
import 'dart:convert';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/pages/file_manager_page.dart';
import 'package:path/path.dart' as p;
import 'package:flutter/material.dart';
import 'package:path/path.dart' as Path;
import 'model.dart';
enum SortBy { Name, Type, Modified, Size }
// enum FileType {
// Dir = 0,
// DirLink = 2,
// DirDrive = 3,
// File = 4,
// FileLink = 5,
// }
class FileModel extends ChangeNotifier {
var _isLocal = false;
var _selectMode = false;
var _localOption = DirectoryOption();
var _remoteOption = DirectoryOption();
/// 每一个选择的文件或文件夹占用一个 _jobIdfile_num是文件夹中的单独文件id
/// 如
/// 发送单独一个文件 file_num = 0;
/// 发送一个文件夹若文件夹下有3个文件 最后一个文件的 file_num = 2;
var _jobId = 0;
var _jobProgress = JobProgress(); // from rust update
bool get isLocal => _isLocal;
bool get selectMode => _selectMode;
JobProgress get jobProgress => _jobProgress;
JobState get jobState => _jobProgress.state;
SortBy _sortStyle = SortBy.Name;
SortBy get sortStyle => _sortStyle;
FileDirectory _currentLocalDir = FileDirectory();
FileDirectory get currentLocalDir => _currentLocalDir;
FileDirectory _currentRemoteDir = FileDirectory();
FileDirectory get currentRemoteDir => _currentRemoteDir;
FileDirectory get currentDir => _isLocal ? currentLocalDir : currentRemoteDir;
String get currentHome => _isLocal ? _localOption.home : _remoteOption.home;
String get currentShortPath {
if(currentDir.path.startsWith(currentHome)){
var path = currentDir.path.replaceFirst(currentHome, "");
if(path.length ==0 ) return "";
if(path[0] == "/" || path[0] == "\\") {
// remove more '/' or '\'
path = path.replaceFirst(path[0], "");
}
return path;
}else{
return currentDir.path.replaceFirst(currentHome, "");
}
}
bool get currentShowHidden =>
_isLocal ? _localOption.showHidden : _remoteOption.showHidden;
bool get currentIsWindows =>
_isLocal ? _localOption.isWindows : _remoteOption.isWindows;
final _fileFetcher = FileFetcher();
final _jobResultListener = JobResultListener<Map<String, dynamic>>();
toggleSelectMode() {
_selectMode = !_selectMode;
notifyListeners();
}
togglePage() {
_isLocal = !_isLocal;
notifyListeners();
}
toggleShowHidden({bool? showHidden, bool? local}) {
final isLocal = local ?? _isLocal;
if (isLocal) {
_localOption.showHidden = showHidden ?? !_localOption.showHidden;
} else {
_remoteOption.showHidden = showHidden ?? !_remoteOption.showHidden;
}
refresh();
}
tryUpdateJobProgress(Map<String, dynamic> evt) {
try {
int id = int.parse(evt['id']);
_jobProgress.id = id;
_jobProgress.fileNum = int.parse(evt['file_num']);
_jobProgress.speed = double.parse(evt['speed']);
_jobProgress.finishedSize = int.parse(evt['finished_size']);
notifyListeners();
} catch (e) {
debugPrint("Failed to tryUpdateJobProgress,evt:${evt.toString()}");
}
}
receiveFileDir(Map<String, dynamic> evt) {
_fileFetcher.tryCompleteTask(evt['value'], evt['is_local']);
}
// job 类型 复制结束 删除结束
jobDone(Map<String, dynamic> evt) {
if (_jobResultListener.isListening) {
_jobResultListener.complete(evt);
return;
}
_selectMode = false;
_jobProgress.state = JobState.done;
refresh();
}
jobError(Map<String, dynamic> evt) {
if (_jobResultListener.isListening) {
_jobResultListener.complete(evt);
return;
}
debugPrint("jobError $evt");
_selectMode = false;
_jobProgress.clear();
_jobProgress.state = JobState.error;
notifyListeners();
}
jobReset() {
_jobProgress.clear();
notifyListeners();
}
onReady() {
_localOption = DirectoryOption(
home: FFI.getByName("get_home_dir"),
showHidden: FFI.getByName("peer_option", "local_show_hidden") != "");
_remoteOption = DirectoryOption(
home: FFI.ffiModel.pi.homeDir,
showHidden: FFI.getByName("peer_option", "remote_show_hidden") != "",
isWindows: FFI.ffiModel.pi.platform == "Windows");
debugPrint("remote platform: ${FFI.ffiModel.pi.platform}");
final local = FFI.getByName("peer_option", "local_dir");
final remote = FFI.getByName("peer_option", "remote_dir");
openDirectory(local.isEmpty ? _localOption.home : local, isLocal: true);
openDirectory(remote.isEmpty ? _remoteOption.home : remote, isLocal: false);
Timer(Duration(seconds: 2), () {
if (_currentLocalDir.path.isEmpty) {
openDirectory(_localOption.home, isLocal: true);
}
if (_currentRemoteDir.path.isEmpty) {
openDirectory(_remoteOption.home, isLocal: false);
}
});
}
onClose() {
DialogManager.reset();
EasyLoading.dismiss();
// save config
Map<String, String> msg = Map();
msg["name"] = "local_dir";
msg["value"] = _currentLocalDir.path;
FFI.setByName('peer_option', jsonEncode(msg));
msg["name"] = "local_show_hidden";
msg["value"] = _localOption.showHidden ? "Y" : "";
FFI.setByName('peer_option', jsonEncode(msg));
msg["name"] = "remote_dir";
msg["value"] = _currentRemoteDir.path;
FFI.setByName('peer_option', jsonEncode(msg));
msg["name"] = "remote_show_hidden";
msg["value"] = _remoteOption.showHidden ? "Y" : "";
FFI.setByName('peer_option', jsonEncode(msg));
_currentLocalDir.clear();
_currentRemoteDir.clear();
}
refresh() {
openDirectory(currentDir.path);
}
openDirectory(String path, {bool? isLocal}) async {
isLocal = isLocal ?? _isLocal;
final showHidden =
isLocal ? _localOption.showHidden : _remoteOption.showHidden;
final isWindows =
isLocal ? _localOption.isWindows : _remoteOption.isWindows;
try {
final fd = await _fileFetcher.fetchDirectory(path, isLocal, showHidden);
fd.format(isWindows, sort: _sortStyle);
if (isLocal) {
_currentLocalDir = fd;
} else {
_currentRemoteDir = fd;
}
notifyListeners();
} catch (e) {
debugPrint("Failed to openDirectory :$e");
}
}
goHome() {
openDirectory(currentHome);
}
goToParentDirectory() {
openDirectory(currentDir.parent);
}
sendFiles(SelectedItems items) {
if (items.isLocal == null) {
debugPrint("Failed to sendFiles ,wrong path state");
return;
}
_jobProgress.state = JobState.inProgress;
final toPath =
items.isLocal! ? currentRemoteDir.path : currentLocalDir.path;
final isWindows =
items.isLocal! ? _localOption.isWindows : _remoteOption.isWindows;
final showHidden =
items.isLocal! ? _localOption.showHidden : _remoteOption.showHidden;
items.items.forEach((from) {
_jobId++;
final msg = {
"id": _jobId.toString(),
"path": from.path,
"to": PathUtil.join(toPath, from.name, isWindows),
"show_hidden": showHidden.toString(),
"is_remote": (!(items.isLocal!)).toString() // 指from的位置而不是to的位置
};
FFI.setByName("send_files", jsonEncode(msg));
});
}
bool removeCheckboxRemember = false;
removeAction(SelectedItems items) async {
removeCheckboxRemember = false;
if (items.isLocal == null) {
debugPrint("Failed to removeFile ,wrong path state");
return;
}
final isWindows =
items.isLocal! ? _localOption.isWindows : _remoteOption.isWindows;
await Future.forEach(items.items, (Entry item) async {
_jobId++;
var title = "";
var content = "";
late final List<Entry> entries;
if (item.isFile) {
title = translate("Are you sure you want to delete this file?");
content = "${item.name}";
entries = [item];
} else if (item.isDirectory) {
title = translate("Not a Empty Directory");
showLoading(translate("Waiting"));
final fd = await _fileFetcher.fetchDirectoryRecursive(
_jobId, item.path, items.isLocal!, true);
fd.format(isWindows);
EasyLoading.dismiss();
// 空文件夹
if (fd.entries.isEmpty) {
final confirm = await showRemoveDialog(translate("Are you sure you want to delete this empty directory?"), item.name, false);
if (confirm == true) {
sendRemoveEmptyDir(item.path, 0, items.isLocal!);
}
return;
}
entries = fd.entries;
} else {
entries = [];
}
for (var i = 0; i < entries.length; i++) {
final dirShow = item.isDirectory ? "${translate("Are you sure you want to delete the file of this directory?")}\n" : "";
final count =
entries.length > 1 ? "${i + 1}/${entries.length}" : "";
content = dirShow + "$count \n${entries[i].path}";
final confirm =
await showRemoveDialog(title, content, item.isDirectory);
try {
if (confirm == true) {
sendRemoveFile(entries[i].path, i, items.isLocal!);
final res = await _jobResultListener.start();
// handle remove res;
if (item.isDirectory &&
res['file_num'] == (entries.length - 1).toString()) {
sendRemoveEmptyDir(item.path, i, items.isLocal!);
}
}
if (removeCheckboxRemember) {
if (confirm == true) {
for (var j = i + 1; j < entries.length; j++) {
sendRemoveFile(entries[j].path, j, items.isLocal!);
final res = await _jobResultListener.start();
if (item.isDirectory &&
res['file_num'] == (entries.length - 1).toString()) {
sendRemoveEmptyDir(item.path, i, items.isLocal!);
}
}
}
break;
}
} catch (e) {}
}
});
refresh();
}
Future<bool?> showRemoveDialog(
String title, String content, bool showCheckbox) async {
return await DialogManager.show<bool>((setState, Function(bool v) close) =>
CustomAlertDialog(
title: Row(
children: [
Icon(Icons.warning, color: Colors.red),
SizedBox(width: 20),
Text(title)
],
),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(content),
SizedBox(height: 5),
Text(translate("This is irreversible!"),
style: TextStyle(fontWeight: FontWeight.bold)),
showCheckbox
? CheckboxListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
controlAffinity: ListTileControlAffinity.leading,
title: Text(
translate("Do this for all conflicts"),
),
value: removeCheckboxRemember,
onChanged: (v) {
if (v == null) return;
setState(() => removeCheckboxRemember = v);
},
)
: SizedBox.shrink()
]),
actions: [
TextButton(
style: flatButtonStyle,
onPressed: () => close(false),
child: Text(translate("Cancel"))),
TextButton(
style: flatButtonStyle,
onPressed: () => close(true),
child: Text(translate("OK"))),
]));
}
sendRemoveFile(String path, int fileNum, bool isLocal) {
final msg = {
"id": _jobId.toString(),
"path": path,
"file_num": fileNum.toString(),
"is_remote": (!(isLocal)).toString()
};
FFI.setByName("remove_file", jsonEncode(msg));
}
sendRemoveEmptyDir(String path, int fileNum, bool isLocal) {
final msg = {
"id": _jobId.toString(),
"path": path,
"is_remote": (!isLocal).toString()
};
FFI.setByName("remove_all_empty_dirs", jsonEncode(msg));
}
createDir(String path) {
_jobId++;
final msg = {
"id": _jobId.toString(),
"path": path,
"is_remote": (!isLocal).toString()
};
FFI.setByName("create_dir", jsonEncode(msg));
}
cancelJob(int id) {}
changeSortStyle(SortBy sort) {
_sortStyle = sort;
_currentLocalDir.changeSortStyle(sort);
_currentRemoteDir.changeSortStyle(sort);
notifyListeners();
}
}
class JobResultListener<T> {
Completer<T>? _completer;
Timer? _timer;
int _timeoutSecond = 5;
bool get isListening => _completer != null;
clear() {
if (_completer != null) {
_timer?.cancel();
_timer = null;
_completer!.completeError("Cancel manually");
_completer = null;
return;
}
}
Future<T> start() {
if (_completer != null) return Future.error("Already start listen");
_completer = Completer();
_timer = Timer(Duration(seconds: _timeoutSecond), () {
if (!_completer!.isCompleted) {
_completer!.completeError("Time out");
}
_completer = null;
});
return _completer!.future;
}
complete(T res) {
if (_completer != null) {
_timer?.cancel();
_timer = null;
_completer!.complete(res);
_completer = null;
return;
}
}
}
class FileFetcher {
// Map<String,Completer<FileDirectory>> localTasks = Map(); // now we only use read local dir sync
Map<String, Completer<FileDirectory>> remoteTasks = Map();
Map<int, Completer<FileDirectory>> readRecursiveTasks = Map();
Future<FileDirectory> registerReadTask(bool isLocal, String path) {
// final jobs = isLocal?localJobs:remoteJobs; // maybe we will use read local dir async later
final tasks = remoteTasks; // bypass now
if (tasks.containsKey(path)) {
throw "Failed to registerReadTask, already have same read job";
}
final c = Completer<FileDirectory>();
tasks[path] = c;
Timer(Duration(seconds: 2), () {
tasks.remove(path);
if (c.isCompleted) return; // 计时器加入map
c.completeError("Failed to read dir,timeout");
});
return c.future;
}
Future<FileDirectory> registerReadRecursiveTask(int id) {
final tasks = readRecursiveTasks;
if (tasks.containsKey(id)) {
throw "Failed to registerRemoveTask, already have same ReadRecursive job";
}
final c = Completer<FileDirectory>();
tasks[id] = c;
Timer(Duration(seconds: 2), () {
tasks.remove(id);
if (c.isCompleted) return; // 计时器加入map
c.completeError("Failed to read dir,timeout");
});
return c.future;
}
tryCompleteTask(String? msg, String? isLocalStr) {
if (msg == null || isLocalStr == null) return;
late final isLocal;
late final tasks;
if (isLocalStr == "true") {
isLocal = true;
} else if (isLocalStr == "false") {
isLocal = false;
} else {
return;
}
try {
final fd = FileDirectory.fromJson(jsonDecode(msg));
if (fd.id > 0) {
// fd.id > 0 is result for read recursive
// TODO later,will be better if every fetch use ID,so that there will only one task map for read and recursive read
tasks = readRecursiveTasks;
final completer = tasks.remove(fd.id);
completer?.complete(fd);
} else if (fd.path.isNotEmpty) {
// result for normal read dir
// final jobs = isLocal?localJobs:remoteJobs; // maybe we will use read local dir async later
tasks = remoteTasks; // bypass now
final completer = tasks.remove(fd.path);
completer?.complete(fd);
}
} catch (e) {
debugPrint("tryCompleteJob err :$e");
}
}
Future<FileDirectory> fetchDirectory(
String path, bool isLocal, bool showHidden) async {
try {
final msg = {"path": path, "show_hidden": showHidden.toString()};
if (isLocal) {
final res = FFI.getByName("read_local_dir_sync", jsonEncode(msg));
final fd = FileDirectory.fromJson(jsonDecode(res));
return fd;
} else {
FFI.setByName("read_remote_dir", jsonEncode(msg));
return registerReadTask(isLocal, path);
}
} catch (e) {
return Future.error(e);
}
}
Future<FileDirectory> fetchDirectoryRecursive(
int id, String path, bool isLocal, bool showHidden) async {
// TODO test Recursive is show hidden default?
try {
final msg = {
"id": id.toString(),
"path": path,
"show_hidden": showHidden.toString(),
"is_remote": (!isLocal).toString()
};
FFI.setByName("read_dir_recursive", jsonEncode(msg));
return registerReadRecursiveTask(id);
} catch (e) {
return Future.error(e);
}
}
}
class FileDirectory {
List<Entry> entries = [];
int id = 0;
String path = "";
String get parent => p.dirname(path);
FileDirectory();
FileDirectory.fromJson(Map<String, dynamic> json) {
id = json['id'];
path = json['path'];
json['entries'].forEach((v) {
entries.add(new Entry.fromJson(v));
});
}
// generate full path for every entry , init sort style if need.
format(bool isWindows, {SortBy? sort}) {
entries.forEach((entry) {
entry.path = PathUtil.join(path, entry.name, isWindows);
});
if (sort != null) {
changeSortStyle(sort);
}
}
changeSortStyle(SortBy sort) {
entries = _sortList(entries, sort);
}
clear() {
entries = [];
id = 0;
path = "";
}
}
class Entry {
int entryType = 4;
int modifiedTime = 0;
String name = "";
String path = "";
int size = 0;
Entry();
Entry.fromJson(Map<String, dynamic> json) {
entryType = json['entry_type'];
modifiedTime = json['modified_time'];
name = json['name'];
size = json['size'];
}
bool get isFile => entryType > 3;
bool get isDirectory => entryType <= 3;
DateTime lastModified() {
return DateTime.fromMillisecondsSinceEpoch(modifiedTime * 1000);
}
}
enum JobState { none, inProgress, done, error }
class JobProgress {
JobState state = JobState.none;
var id = 0;
var fileNum = 0;
var speed = 0.0;
var finishedSize = 0;
clear() {
state = JobState.none;
id = 0;
fileNum = 0;
speed = 0;
finishedSize = 0;
}
}
class _PathStat {
final String path;
final DateTime dateTime;
_PathStat(this.path, this.dateTime);
}
class PathUtil {
static final windowsContext = Path.Context(style: Path.Style.windows);
static final posixContext = Path.Context(style: Path.Style.posix);
static String join(String path1, String path2, bool isWindows) {
final pathUtil = isWindows ? windowsContext : posixContext;
return pathUtil.join(path1, path2);
}
static List<String> split(String path, bool isWindows) {
final pathUtil = isWindows ? windowsContext : posixContext;
return pathUtil.split(path);
}
}
class DirectoryOption {
String home;
bool showHidden;
bool isWindows;
DirectoryOption(
{this.home = "", this.showHidden = false, this.isWindows = false});
}
// code from file_manager pkg after edit
List<Entry> _sortList(List<Entry> list, SortBy sortType) {
if (sortType == SortBy.Name) {
// making list of only folders.
final dirs = list.where((element) => element.isDirectory).toList();
// sorting folder list by name.
dirs.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
// making list of only flies.
final files = list.where((element) => element.isFile).toList();
// sorting files list by name.
files.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
// first folders will go to list (if available) then files will go to list.
return [...dirs, ...files];
} else if (sortType == SortBy.Modified) {
// making the list of Path & DateTime
List<_PathStat> _pathStat = [];
for (Entry e in list) {
_pathStat.add(_PathStat(e.name, e.lastModified()));
}
// sort _pathStat according to date
_pathStat.sort((b, a) => a.dateTime.compareTo(b.dateTime));
// sorting [list] according to [_pathStat]
list.sort((a, b) => _pathStat
.indexWhere((element) => element.path == a.name)
.compareTo(_pathStat.indexWhere((element) => element.path == b.name)));
return list;
} else if (sortType == SortBy.Type) {
// making list of only folders.
final dirs = list.where((element) => element.isDirectory).toList();
// sorting folders by name.
dirs.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
// making the list of files
final files = list.where((element) => element.isFile).toList();
// sorting files list by extension.
files.sort((a, b) => a.name
.toLowerCase()
.split('.')
.last
.compareTo(b.name.toLowerCase().split('.').last));
return [...dirs, ...files];
} else if (sortType == SortBy.Size) {
// create list of path and size
Map<String, int> _sizeMap = {};
for (Entry e in list) {
_sizeMap[e.name] = e.size;
}
// making list of only folders.
final dirs = list.where((element) => element.isDirectory).toList();
// sorting folder list by name.
dirs.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
// making list of only flies.
final files = list.where((element) => element.isFile).toList();
// creating sorted list of [_sizeMapList] by size.
final List<MapEntry<String, int>> _sizeMapList = _sizeMap.entries.toList();
_sizeMapList.sort((b, a) => a.value.compareTo(b.value));
// sort [list] according to [_sizeMapList]
files.sort((a, b) => _sizeMapList
.indexWhere((element) => element.key == a.name)
.compareTo(
_sizeMapList.indexWhere((element) => element.key == b.name)));
return [...dirs, ...files];
}
return [];
}