update file: add show hidden, add remember last path, add breadcrumb action
This commit is contained in:
parent
281acf7474
commit
27b80f034c
@ -4,6 +4,12 @@ import 'package:flutter_easyloading/flutter_easyloading.dart';
|
|||||||
|
|
||||||
final globalKey = GlobalKey<NavigatorState>();
|
final globalKey = GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
|
var isAndroid = false;
|
||||||
|
var isIOS = false;
|
||||||
|
var isWeb = false;
|
||||||
|
var isDesktop = false;
|
||||||
|
var version = "";
|
||||||
|
|
||||||
typedef F = String Function(String);
|
typedef F = String Function(String);
|
||||||
typedef FMethod = String Function(String, dynamic);
|
typedef FMethod = String Function(String, dynamic);
|
||||||
|
|
||||||
@ -172,8 +178,18 @@ Color str2color(String str, [alpha = 0xFF]) {
|
|||||||
return Color((hash & 0xFF7FFF) | (alpha << 24));
|
return Color((hash & 0xFF7FFF) | (alpha << 24));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isAndroid = false;
|
const K = 1024;
|
||||||
bool isIOS = false;
|
const M = K * K;
|
||||||
bool isWeb = false;
|
const G = M * K;
|
||||||
bool isDesktop = false;
|
|
||||||
var version = "";
|
String readableFileSize(double size) {
|
||||||
|
if (size < K) {
|
||||||
|
return size.toString() + " B";
|
||||||
|
} else if (size < M) {
|
||||||
|
return (size / K).toStringAsFixed(2) + " KB";
|
||||||
|
} else if (size < G) {
|
||||||
|
return (size / M).toStringAsFixed(2) + " MB";
|
||||||
|
} else {
|
||||||
|
return (size / G).toStringAsFixed(2) + " GB";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -19,15 +19,13 @@ enum SortBy { name, type, date, size }
|
|||||||
// FileLink = 5,
|
// FileLink = 5,
|
||||||
// }
|
// }
|
||||||
|
|
||||||
class RemoveCompleter {}
|
|
||||||
|
|
||||||
typedef OnJobStateChange = void Function(JobState state, JobProgress jp);
|
|
||||||
|
|
||||||
class FileModel extends ChangeNotifier {
|
class FileModel extends ChangeNotifier {
|
||||||
// TODO 添加 dispose 退出页面的时候清理数据以及尚未完成的任务和对话框
|
|
||||||
var _isLocal = false;
|
var _isLocal = false;
|
||||||
var _selectMode = false;
|
var _selectMode = false;
|
||||||
|
|
||||||
|
var _localOption = DirectoryOption();
|
||||||
|
var _remoteOption = DirectoryOption();
|
||||||
|
|
||||||
/// 每一个选择的文件或文件夹占用一个 _jobId,file_num是文件夹中的单独文件id
|
/// 每一个选择的文件或文件夹占用一个 _jobId,file_num是文件夹中的单独文件id
|
||||||
/// 如
|
/// 如
|
||||||
/// 发送单独一个文件 file_num = 0;
|
/// 发送单独一个文件 file_num = 0;
|
||||||
@ -58,6 +56,28 @@ class FileModel extends ChangeNotifier {
|
|||||||
|
|
||||||
FileDirectory get currentDir => _isLocal ? currentLocalDir : 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 _fileFetcher = FileFetcher();
|
||||||
|
|
||||||
final _jobResultListener = JobResultListener<Map<String, dynamic>>();
|
final _jobResultListener = JobResultListener<Map<String, dynamic>>();
|
||||||
@ -72,6 +92,16 @@ class FileModel extends ChangeNotifier {
|
|||||||
notifyListeners();
|
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) {
|
tryUpdateJobProgress(Map<String, dynamic> evt) {
|
||||||
try {
|
try {
|
||||||
int id = int.parse(evt['id']);
|
int id = int.parse(evt['id']);
|
||||||
@ -79,7 +109,6 @@ class FileModel extends ChangeNotifier {
|
|||||||
_jobProgress.fileNum = int.parse(evt['file_num']);
|
_jobProgress.fileNum = int.parse(evt['file_num']);
|
||||||
_jobProgress.speed = double.parse(evt['speed']);
|
_jobProgress.speed = double.parse(evt['speed']);
|
||||||
_jobProgress.finishedSize = int.parse(evt['finished_size']);
|
_jobProgress.finishedSize = int.parse(evt['finished_size']);
|
||||||
debugPrint("_jobProgress update:${_jobProgress.toString()}");
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Failed to tryUpdateJobProgress,evt:${evt.toString()}");
|
debugPrint("Failed to tryUpdateJobProgress,evt:${evt.toString()}");
|
||||||
@ -107,7 +136,7 @@ class FileModel extends ChangeNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
debugPrint("jobError $evt");
|
||||||
_selectMode = false;
|
_selectMode = false;
|
||||||
_jobProgress.clear();
|
_jobProgress.clear();
|
||||||
_jobProgress.state = JobState.error;
|
_jobProgress.state = JobState.error;
|
||||||
@ -120,19 +149,69 @@ class FileModel extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onReady() {
|
onReady() {
|
||||||
openDirectory(FFI.getByName("get_home_dir"), isLocal: true);
|
_localOption = DirectoryOption(
|
||||||
openDirectory(FFI.ffiModel.pi.homeDir, isLocal: false);
|
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() {
|
refresh() {
|
||||||
openDirectory(_isLocal ? _currentLocalDir.path : _currentRemoteDir.path);
|
openDirectory(currentDir.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
openDirectory(String path, {bool? isLocal}) async {
|
openDirectory(String path, {bool? isLocal}) async {
|
||||||
isLocal = isLocal ?? _isLocal;
|
isLocal = isLocal ?? _isLocal;
|
||||||
|
final showHidden =
|
||||||
|
isLocal ? _localOption.showHidden : _remoteOption.showHidden;
|
||||||
|
final isWindows =
|
||||||
|
isLocal ? _localOption.isWindows : _remoteOption.isWindows;
|
||||||
try {
|
try {
|
||||||
final fd = await _fileFetcher.fetchDirectory(path, isLocal);
|
final fd = await _fileFetcher.fetchDirectory(path, isLocal, showHidden);
|
||||||
fd.changeSortStyle(_sortStyle);
|
fd.format(isWindows, sort: _sortStyle);
|
||||||
if (isLocal) {
|
if (isLocal) {
|
||||||
_currentLocalDir = fd;
|
_currentLocalDir = fd;
|
||||||
} else {
|
} else {
|
||||||
@ -144,9 +223,12 @@ class FileModel extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
goHome() {
|
||||||
|
openDirectory(currentHome);
|
||||||
|
}
|
||||||
|
|
||||||
goToParentDirectory() {
|
goToParentDirectory() {
|
||||||
final fd = _isLocal ? _currentLocalDir : _currentRemoteDir;
|
openDirectory(currentDir.parent);
|
||||||
openDirectory(fd.parent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sendFiles(SelectedItems items) {
|
sendFiles(SelectedItems items) {
|
||||||
@ -157,13 +239,17 @@ class FileModel extends ChangeNotifier {
|
|||||||
_jobProgress.state = JobState.inProgress;
|
_jobProgress.state = JobState.inProgress;
|
||||||
final toPath =
|
final toPath =
|
||||||
items.isLocal! ? currentRemoteDir.path : currentLocalDir.path;
|
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) {
|
items.items.forEach((from) {
|
||||||
_jobId++;
|
_jobId++;
|
||||||
final msg = {
|
final msg = {
|
||||||
"id": _jobId.toString(),
|
"id": _jobId.toString(),
|
||||||
"path": from.path,
|
"path": from.path,
|
||||||
"to": Path.join(toPath, from.name),
|
"to": PathUtil.join(toPath, from.name, isWindows),
|
||||||
"show_hidden": "false", // TODO showHidden
|
"show_hidden": showHidden.toString(),
|
||||||
"is_remote": (!(items.isLocal!)).toString() // 指from的位置而不是to的位置
|
"is_remote": (!(items.isLocal!)).toString() // 指from的位置而不是to的位置
|
||||||
};
|
};
|
||||||
FFI.setByName("send_files", jsonEncode(msg));
|
FFI.setByName("send_files", jsonEncode(msg));
|
||||||
@ -178,6 +264,8 @@ class FileModel extends ChangeNotifier {
|
|||||||
debugPrint("Failed to removeFile ,wrong path state");
|
debugPrint("Failed to removeFile ,wrong path state");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
final isWindows =
|
||||||
|
items.isLocal! ? _localOption.isWindows : _remoteOption.isWindows;
|
||||||
await Future.forEach(items.items, (Entry item) async {
|
await Future.forEach(items.items, (Entry item) async {
|
||||||
_jobId++;
|
_jobId++;
|
||||||
var title = "";
|
var title = "";
|
||||||
@ -191,35 +279,33 @@ class FileModel extends ChangeNotifier {
|
|||||||
title = "这不是一个空文件夹";
|
title = "这不是一个空文件夹";
|
||||||
showLoading("正在读取...");
|
showLoading("正在读取...");
|
||||||
final fd = await _fileFetcher.fetchDirectoryRecursive(
|
final fd = await _fileFetcher.fetchDirectoryRecursive(
|
||||||
_jobId, item.path, items.isLocal!);
|
_jobId, item.path, items.isLocal!, true);
|
||||||
|
fd.format(isWindows);
|
||||||
EasyLoading.dismiss();
|
EasyLoading.dismiss();
|
||||||
// 空文件夹
|
// 空文件夹
|
||||||
if(fd.entries.isEmpty){
|
if (fd.entries.isEmpty) {
|
||||||
final confirm = await showRemoveDialog("是否删除空文件夹",item.name,false);
|
final confirm = await showRemoveDialog("是否删除空文件夹", item.name, false);
|
||||||
if(confirm == true){
|
if (confirm == true) {
|
||||||
sendRemoveEmptyDir(item.path, 0, items.isLocal!);
|
sendRemoveEmptyDir(item.path, 0, items.isLocal!);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
debugPrint("removeDirAllIntent res:${fd.id}");
|
|
||||||
entries = fd.entries;
|
entries = fd.entries;
|
||||||
} else {
|
} else {
|
||||||
debugPrint("none : ${item.toString()}");
|
|
||||||
entries = [];
|
entries = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var i = 0; i < entries.length; i++) {
|
for (var i = 0; i < entries.length; i++) {
|
||||||
final dirShow = item.isDirectory?"是否删除文件夹下的文件?\n":"";
|
final dirShow = item.isDirectory ? "是否删除文件夹下的文件?\n" : "";
|
||||||
final count = entries.length>1?"第 ${i + 1}/${entries.length} 项":"";
|
final count =
|
||||||
|
entries.length > 1 ? "第 ${i + 1}/${entries.length} 项" : "";
|
||||||
content = dirShow + "$count \n${entries[i].path}";
|
content = dirShow + "$count \n${entries[i].path}";
|
||||||
final confirm = await showRemoveDialog(title,content,item.isDirectory);
|
final confirm =
|
||||||
debugPrint("已选择:$confirm");
|
await showRemoveDialog(title, content, item.isDirectory);
|
||||||
try {
|
try {
|
||||||
if (confirm == true) {
|
if (confirm == true) {
|
||||||
sendRemoveFile(entries[i].path, i, items.isLocal!);
|
sendRemoveFile(entries[i].path, i, items.isLocal!);
|
||||||
final res = await _jobResultListener.start();
|
final res = await _jobResultListener.start();
|
||||||
debugPrint("remove got res ${res.toString()}");
|
|
||||||
// handle remove res;
|
// handle remove res;
|
||||||
if (item.isDirectory &&
|
if (item.isDirectory &&
|
||||||
res['file_num'] == (entries.length - 1).toString()) {
|
res['file_num'] == (entries.length - 1).toString()) {
|
||||||
@ -231,7 +317,6 @@ class FileModel extends ChangeNotifier {
|
|||||||
for (var j = i + 1; j < entries.length; j++) {
|
for (var j = i + 1; j < entries.length; j++) {
|
||||||
sendRemoveFile(entries[j].path, j, items.isLocal!);
|
sendRemoveFile(entries[j].path, j, items.isLocal!);
|
||||||
final res = await _jobResultListener.start();
|
final res = await _jobResultListener.start();
|
||||||
debugPrint("remove got res ${res.toString()}");
|
|
||||||
if (item.isDirectory &&
|
if (item.isDirectory &&
|
||||||
res['file_num'] == (entries.length - 1).toString()) {
|
res['file_num'] == (entries.length - 1).toString()) {
|
||||||
sendRemoveEmptyDir(item.path, i, items.isLocal!);
|
sendRemoveEmptyDir(item.path, i, items.isLocal!);
|
||||||
@ -246,9 +331,10 @@ class FileModel extends ChangeNotifier {
|
|||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool?> showRemoveDialog(String title,String content,bool showCheckbox) async {
|
Future<bool?> showRemoveDialog(
|
||||||
return await DialogManager.show<bool>(
|
String title, String content, bool showCheckbox) async {
|
||||||
(setState, Function(bool v) close) => CustomAlertDialog(
|
return await DialogManager.show<bool>((setState, Function(bool v) close) =>
|
||||||
|
CustomAlertDialog(
|
||||||
title: Row(
|
title: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.warning, color: Colors.red),
|
Icon(Icons.warning, color: Colors.red),
|
||||||
@ -262,9 +348,10 @@ class FileModel extends ChangeNotifier {
|
|||||||
children: [
|
children: [
|
||||||
Text(content),
|
Text(content),
|
||||||
SizedBox(height: 5),
|
SizedBox(height: 5),
|
||||||
Text("此操作不可逆!",style: TextStyle(fontWeight: FontWeight.bold)),
|
Text("此操作不可逆!",
|
||||||
showCheckbox?
|
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
CheckboxListTile(
|
showCheckbox
|
||||||
|
? CheckboxListTile(
|
||||||
contentPadding: const EdgeInsets.all(0),
|
contentPadding: const EdgeInsets.all(0),
|
||||||
dense: true,
|
dense: true,
|
||||||
controlAffinity: ListTileControlAffinity.leading,
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
@ -276,15 +363,18 @@ class FileModel extends ChangeNotifier {
|
|||||||
if (v == null) return;
|
if (v == null) return;
|
||||||
setState(() => removeCheckboxRemember = v);
|
setState(() => removeCheckboxRemember = v);
|
||||||
},
|
},
|
||||||
):SizedBox.shrink()
|
)
|
||||||
|
: SizedBox.shrink()
|
||||||
]),
|
]),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
style: flatButtonStyle,
|
style: flatButtonStyle,
|
||||||
onPressed: () => close(true), child: Text("Yes")),
|
onPressed: () => close(true),
|
||||||
|
child: Text("Yes")),
|
||||||
TextButton(
|
TextButton(
|
||||||
style: flatButtonStyle,
|
style: flatButtonStyle,
|
||||||
onPressed: () => close(false), child: Text("No"))
|
onPressed: () => close(false),
|
||||||
|
child: Text("No"))
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -308,18 +398,16 @@ class FileModel extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createDir(String path) {
|
createDir(String path) {
|
||||||
_jobId ++;
|
_jobId++;
|
||||||
final msg = {
|
final msg = {
|
||||||
"id": _jobId.toString(),
|
"id": _jobId.toString(),
|
||||||
"path": path,
|
"path": path,
|
||||||
"is_remote": (!isLocal).toString()
|
"is_remote": (!isLocal).toString()
|
||||||
};
|
};
|
||||||
FFI.setByName("create_dir",jsonEncode(msg));
|
FFI.setByName("create_dir", jsonEncode(msg));
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelJob(int id){
|
cancelJob(int id) {}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
changeSortStyle(SortBy sort) {
|
changeSortStyle(SortBy sort) {
|
||||||
_sortStyle = sort;
|
_sortStyle = sort;
|
||||||
@ -327,11 +415,6 @@ class FileModel extends ChangeNotifier {
|
|||||||
_currentRemoteDir.changeSortStyle(sort);
|
_currentRemoteDir.changeSortStyle(sort);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void clear() {
|
|
||||||
_currentLocalDir.clear();
|
|
||||||
_currentRemoteDir.clear();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class JobResultListener<T> {
|
class JobResultListener<T> {
|
||||||
@ -413,7 +496,6 @@ class FileFetcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tryCompleteTask(String? msg, String? isLocalStr) {
|
tryCompleteTask(String? msg, String? isLocalStr) {
|
||||||
debugPrint("tryCompleteTask : $msg");
|
|
||||||
if (msg == null || isLocalStr == null) return;
|
if (msg == null || isLocalStr == null) return;
|
||||||
late final isLocal;
|
late final isLocal;
|
||||||
late final tasks;
|
late final tasks;
|
||||||
@ -444,15 +526,16 @@ class FileFetcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<FileDirectory> fetchDirectory(String path, bool isLocal) async {
|
Future<FileDirectory> fetchDirectory(
|
||||||
debugPrint("fetch :$path");
|
String path, bool isLocal, bool showHidden) async {
|
||||||
try {
|
try {
|
||||||
|
final msg = {"path": path, "show_hidden": showHidden.toString()};
|
||||||
if (isLocal) {
|
if (isLocal) {
|
||||||
final res = FFI.getByName("read_dir", path);
|
final res = FFI.getByName("read_local_dir_sync", jsonEncode(msg));
|
||||||
final fd = FileDirectory.fromJson(jsonDecode(res));
|
final fd = FileDirectory.fromJson(jsonDecode(res));
|
||||||
return fd;
|
return fd;
|
||||||
} else {
|
} else {
|
||||||
FFI.setByName("read_remote_dir", path);
|
FFI.setByName("read_remote_dir", jsonEncode(msg));
|
||||||
return registerReadTask(isLocal, path);
|
return registerReadTask(isLocal, path);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -461,12 +544,13 @@ class FileFetcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<FileDirectory> fetchDirectoryRecursive(
|
Future<FileDirectory> fetchDirectoryRecursive(
|
||||||
int id, String path, bool isLocal) async {
|
int id, String path, bool isLocal, bool showHidden) async {
|
||||||
debugPrint("fetchDirectoryRecursive id:$id , path:$path");
|
// TODO test Recursive is show hidden default?
|
||||||
try {
|
try {
|
||||||
final msg = {
|
final msg = {
|
||||||
"id": id.toString(),
|
"id": id.toString(),
|
||||||
"path": path,
|
"path": path,
|
||||||
|
"show_hidden": showHidden.toString(),
|
||||||
"is_remote": (!isLocal).toString()
|
"is_remote": (!isLocal).toString()
|
||||||
};
|
};
|
||||||
FFI.setByName("read_dir_recursive", jsonEncode(msg));
|
FFI.setByName("read_dir_recursive", jsonEncode(msg));
|
||||||
@ -486,27 +570,22 @@ class FileDirectory {
|
|||||||
|
|
||||||
FileDirectory();
|
FileDirectory();
|
||||||
|
|
||||||
FileDirectory.fromJsonWithSort(Map<String, dynamic> json, SortBy sort) {
|
|
||||||
id = json['id'];
|
|
||||||
path = json['path'];
|
|
||||||
if (json['entries'] != null) {
|
|
||||||
entries = <Entry>[];
|
|
||||||
json['entries'].forEach((v) {
|
|
||||||
entries.add(new Entry.fromJsonWithPath(v, path));
|
|
||||||
});
|
|
||||||
entries = _sortList(entries, sort);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileDirectory.fromJson(Map<String, dynamic> json) {
|
FileDirectory.fromJson(Map<String, dynamic> json) {
|
||||||
id = json['id'];
|
id = json['id'];
|
||||||
path = json['path'];
|
path = json['path'];
|
||||||
if (json['entries'] != null) {
|
|
||||||
entries = <Entry>[];
|
|
||||||
json['entries'].forEach((v) {
|
json['entries'].forEach((v) {
|
||||||
entries.add(new Entry.fromJsonWithPath(v, path));
|
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) {
|
changeSortStyle(SortBy sort) {
|
||||||
@ -529,12 +608,11 @@ class Entry {
|
|||||||
|
|
||||||
Entry();
|
Entry();
|
||||||
|
|
||||||
Entry.fromJsonWithPath(Map<String, dynamic> json, String parent) {
|
Entry.fromJson(Map<String, dynamic> json) {
|
||||||
entryType = json['entry_type'];
|
entryType = json['entry_type'];
|
||||||
modifiedTime = json['modified_time'];
|
modifiedTime = json['modified_time'];
|
||||||
name = json['name'];
|
name = json['name'];
|
||||||
size = json['size'];
|
size = json['size'];
|
||||||
path = Path.join(parent, name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get isFile => entryType > 3;
|
bool get isFile => entryType > 3;
|
||||||
@ -571,6 +649,30 @@ class _PathStat {
|
|||||||
_PathStat(this.path, this.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
|
// code from file_manager pkg after edit
|
||||||
List<Entry> _sortList(List<Entry> list, SortBy sortType) {
|
List<Entry> _sortList(List<Entry> list, SortBy sortType) {
|
||||||
if (sortType == SortBy.name) {
|
if (sortType == SortBy.name) {
|
||||||
|
@ -4,7 +4,6 @@ import 'package:flutter_easyloading/flutter_easyloading.dart';
|
|||||||
import 'package:flutter_hbb/models/file_model.dart';
|
import 'package:flutter_hbb/models/file_model.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:flutter_breadcrumb/flutter_breadcrumb.dart';
|
import 'package:flutter_breadcrumb/flutter_breadcrumb.dart';
|
||||||
import 'package:path/path.dart' as Path;
|
|
||||||
|
|
||||||
import '../common.dart';
|
import '../common.dart';
|
||||||
import '../models/model.dart';
|
import '../models/model.dart';
|
||||||
@ -38,7 +37,7 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
model.clear();
|
model.onClose();
|
||||||
_interval?.cancel();
|
_interval?.cancel();
|
||||||
FFI.close();
|
FFI.close();
|
||||||
EasyLoading.dismiss();
|
EasyLoading.dismiss();
|
||||||
@ -97,28 +96,16 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
|||||||
itemCount: entries.length + 1,
|
itemCount: entries.length + 1,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
if (index >= entries.length) {
|
if (index >= entries.length) {
|
||||||
// 添加尾部信息 文件统计信息等
|
|
||||||
// 添加快速返回上部
|
|
||||||
// 使用 bottomSheet 提示以选择的文件数量 点击后展开查看更多
|
|
||||||
return listTail();
|
return listTail();
|
||||||
}
|
}
|
||||||
var selected = false;
|
var selected = false;
|
||||||
if (model.selectMode) {
|
if (model.selectMode) {
|
||||||
selected = _selectedItems.contains(entries[index]);
|
selected = _selectedItems.contains(entries[index]);
|
||||||
}
|
}
|
||||||
var sizeStr = "";
|
|
||||||
if (entries[index].isFile) {
|
final sizeStr = entries[index].isFile
|
||||||
final size = entries[index].size;
|
? readableFileSize(entries[index].size.toDouble())
|
||||||
if (size < 1024) {
|
: "";
|
||||||
sizeStr += size.toString() + "B";
|
|
||||||
} else if (size < 1024 * 1024) {
|
|
||||||
sizeStr += (size / 1024).toStringAsFixed(2) + "kB";
|
|
||||||
} else if (size < 1024 * 1024 * 1024) {
|
|
||||||
sizeStr += (size / 1024 / 1024).toStringAsFixed(2) + "MB";
|
|
||||||
} else if (size < 1024 * 1024 * 1024 * 1024) {
|
|
||||||
sizeStr += (size / 1024 / 1024 / 1024).toStringAsFixed(2) + "GB";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Card(
|
return Card(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: Icon(
|
leading: Icon(
|
||||||
@ -248,8 +235,21 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: BreadCrumb(
|
child: BreadCrumb(
|
||||||
items: getPathBreadCrumbItems(() => debugPrint("pressed home"),
|
items: getPathBreadCrumbItems(() => model.goHome(), (list) {
|
||||||
(e) => debugPrint("pressed url:$e")),
|
var path = "";
|
||||||
|
if (model.currentHome.startsWith(list[0])) {
|
||||||
|
// absolute path
|
||||||
|
for (var item in list) {
|
||||||
|
path = PathUtil.join(path, item, model.currentIsWindows);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
path += model.currentHome;
|
||||||
|
for (var item in list) {
|
||||||
|
path = PathUtil.join(path, item, model.currentIsWindows);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
model.openDirectory(path);
|
||||||
|
}),
|
||||||
divider: Icon(Icons.chevron_right),
|
divider: Icon(Icons.chevron_right),
|
||||||
overflow: ScrollableOverflow(controller: _breadCrumbScroller),
|
overflow: ScrollableOverflow(controller: _breadCrumbScroller),
|
||||||
)),
|
)),
|
||||||
@ -261,8 +261,8 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
|||||||
itemBuilder: (context) {
|
itemBuilder: (context) {
|
||||||
return SortBy.values
|
return SortBy.values
|
||||||
.map((e) => PopupMenuItem(
|
.map((e) => PopupMenuItem(
|
||||||
child:
|
child: Text(
|
||||||
Text(translate(e.toString().split(".").last)),
|
e.toString().split(".").last.toUpperCase()),
|
||||||
value: e,
|
value: e,
|
||||||
))
|
))
|
||||||
.toList();
|
.toList();
|
||||||
@ -274,24 +274,45 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
|||||||
return [
|
return [
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [Icon(Icons.refresh), Text("刷新")],
|
children: [
|
||||||
|
Icon(Icons.refresh),
|
||||||
|
SizedBox(width: 5),
|
||||||
|
Text("刷新")
|
||||||
|
],
|
||||||
),
|
),
|
||||||
value: "refresh",
|
value: "refresh",
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [Icon(Icons.check), Text("多选")],
|
children: [
|
||||||
|
Icon(Icons.check),
|
||||||
|
SizedBox(width: 5),
|
||||||
|
Text("多选")
|
||||||
|
],
|
||||||
),
|
),
|
||||||
value: "select",
|
value: "select",
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.folder),
|
Icon(Icons.folder_outlined),
|
||||||
|
SizedBox(width: 5),
|
||||||
Text(translate("Create Folder"))
|
Text(translate("Create Folder"))
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
value: "folder",
|
value: "folder",
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(model.currentShowHidden
|
||||||
|
? Icons.check_box_outlined
|
||||||
|
: Icons.check_box_outline_blank),
|
||||||
|
SizedBox(width: 5),
|
||||||
|
Text(translate("Toggle Hidden"))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
value: "hidden",
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
@ -322,7 +343,10 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
|||||||
style: flatButtonStyle,
|
style: flatButtonStyle,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (name.value.text.isNotEmpty) {
|
if (name.value.text.isNotEmpty) {
|
||||||
model.createDir(Path.join(model.currentDir.path,name.value.text));
|
model.createDir(PathUtil.join(
|
||||||
|
model.currentDir.path,
|
||||||
|
name.value.text,
|
||||||
|
model.currentIsWindows));
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -332,6 +356,8 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
|||||||
onPressed: () => close(false),
|
onPressed: () => close(false),
|
||||||
child: Text(translate("Cancel")))
|
child: Text(translate("Cancel")))
|
||||||
]));
|
]));
|
||||||
|
} else if (v == "hidden") {
|
||||||
|
model.toggleShowHidden();
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
@ -349,7 +375,20 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget listTail() {
|
Widget listTail() {
|
||||||
return SizedBox(height: 100);
|
return Container(
|
||||||
|
height: 100,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.all(2),
|
||||||
|
child: Text(
|
||||||
|
"总计: ${model.currentDir.entries.length}个项目",
|
||||||
|
style: TextStyle(color: MyTheme.darkGray),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget? bottomSheet() {
|
Widget? bottomSheet() {
|
||||||
@ -402,8 +441,7 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
|||||||
return BottomSheetBody(
|
return BottomSheetBody(
|
||||||
leading: CircularProgressIndicator(),
|
leading: CircularProgressIndicator(),
|
||||||
title: "正在发送文件...",
|
title: "正在发送文件...",
|
||||||
text:
|
text: "速度: ${readableFileSize(model.jobProgress.speed)}/s",
|
||||||
"速度: ${(model.jobProgress.speed / 1024).toStringAsFixed(2)} kb/s",
|
|
||||||
onCanceled: null,
|
onCanceled: null,
|
||||||
);
|
);
|
||||||
case JobState.done:
|
case JobState.done:
|
||||||
@ -427,10 +465,9 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<BreadCrumbItem> getPathBreadCrumbItems(
|
List<BreadCrumbItem> getPathBreadCrumbItems(
|
||||||
void Function() onHome, void Function(String) onPressed) {
|
void Function() onHome, void Function(List<String>) onPressed) {
|
||||||
final path = model.currentDir.path;
|
final path = model.currentShortPath;
|
||||||
final list = Path.split(path);
|
final list = PathUtil.split(path, model.currentIsWindows);
|
||||||
list.remove('/');
|
|
||||||
final breadCrumbList = [
|
final breadCrumbList = [
|
||||||
BreadCrumbItem(
|
BreadCrumbItem(
|
||||||
content: IconButton(
|
content: IconButton(
|
||||||
@ -438,12 +475,12 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
|||||||
onPressed: onHome,
|
onPressed: onHome,
|
||||||
))
|
))
|
||||||
];
|
];
|
||||||
breadCrumbList.addAll(list.map((e) => BreadCrumbItem(
|
breadCrumbList.addAll(list.asMap().entries.map((e) => BreadCrumbItem(
|
||||||
content: TextButton(
|
content: TextButton(
|
||||||
child: Text(e),
|
child: Text(e.value),
|
||||||
style:
|
style:
|
||||||
ButtonStyle(minimumSize: MaterialStateProperty.all(Size(0, 0))),
|
ButtonStyle(minimumSize: MaterialStateProperty.all(Size(0, 0))),
|
||||||
onPressed: () => onPressed(e)))));
|
onPressed: () => onPressed(list.sublist(0, e.key + 1))))));
|
||||||
return breadCrumbList;
|
return breadCrumbList;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user