opt desktop file manager status list (#9117)
* Show delete file/dir log * Show full path rather than base file name * Show files count * Opt status card layout * Change selected color to accent Signed-off-by: 21pages <sunboeasy@gmail.com>
This commit is contained in:
parent
2a0fd55af7
commit
8745fcbb6a
@ -173,10 +173,25 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
/// transfer status list
|
||||
/// watch transfer status
|
||||
Widget statusList() {
|
||||
Widget getIcon(JobProgress job) {
|
||||
final color = Theme.of(context).tabBarTheme.labelColor;
|
||||
switch (job.type) {
|
||||
case JobType.deleteDir:
|
||||
case JobType.deleteFile:
|
||||
return Icon(Icons.delete_outline, color: color);
|
||||
default:
|
||||
return Transform.rotate(
|
||||
angle: job.isRemoteToLocal ? pi : 0,
|
||||
child: Icon(Icons.arrow_forward_ios, color: color),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
statusListView(List<JobProgress> jobs) => ListView.builder(
|
||||
controller: ScrollController(),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final item = jobs[index];
|
||||
final status = item.getStatus();
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 5),
|
||||
child: generateCard(
|
||||
@ -186,15 +201,8 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Transform.rotate(
|
||||
angle: item.isRemoteToLocal ? pi : 0,
|
||||
child: SvgPicture.asset("assets/arrow.svg",
|
||||
colorFilter: svgColor(
|
||||
Theme.of(context).tabBarTheme.labelColor)),
|
||||
).paddingOnly(left: 15),
|
||||
const SizedBox(
|
||||
width: 16.0,
|
||||
),
|
||||
getIcon(item)
|
||||
.marginSymmetric(horizontal: 10, vertical: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@ -204,44 +212,24 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
waitDuration: Duration(milliseconds: 500),
|
||||
message: item.jobName,
|
||||
child: Text(
|
||||
item.fileName,
|
||||
item.jobName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).paddingSymmetric(vertical: 10),
|
||||
),
|
||||
Text(
|
||||
'${translate("Total")} ${readableFileSize(item.totalSize.toDouble())}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: MyTheme.darkGray,
|
||||
),
|
||||
),
|
||||
Offstage(
|
||||
offstage: item.state != JobState.inProgress,
|
||||
child: Text(
|
||||
'${translate("Speed")} ${readableFileSize(item.speed)}/s',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: MyTheme.darkGray,
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
waitDuration: Duration(milliseconds: 500),
|
||||
message: status,
|
||||
child: Text(status,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: MyTheme.darkGray,
|
||||
)).marginOnly(top: 6),
|
||||
),
|
||||
Offstage(
|
||||
offstage: item.state == JobState.inProgress,
|
||||
child: Text(
|
||||
translate(
|
||||
item.display(),
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: MyTheme.darkGray,
|
||||
),
|
||||
),
|
||||
),
|
||||
Offstage(
|
||||
offstage: item.state != JobState.inProgress,
|
||||
offstage: item.type != JobType.transfer ||
|
||||
item.state != JobState.inProgress,
|
||||
child: LinearPercentIndicator(
|
||||
padding: EdgeInsets.only(right: 15),
|
||||
animateFromLastPercent: true,
|
||||
center: Text(
|
||||
'${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%',
|
||||
@ -251,7 +239,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
progressColor: MyTheme.accent,
|
||||
backgroundColor: Theme.of(context).hoverColor,
|
||||
lineHeight: kDesktopFileTransferRowHeight,
|
||||
).paddingSymmetric(vertical: 15),
|
||||
).paddingSymmetric(vertical: 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -276,7 +264,6 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
),
|
||||
MenuButton(
|
||||
tooltip: translate("Delete"),
|
||||
padding: EdgeInsets.only(right: 15),
|
||||
child: SvgPicture.asset(
|
||||
"assets/close.svg",
|
||||
colorFilter: svgColor(Colors.white),
|
||||
@ -289,11 +276,11 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
hoverColor: MyTheme.accent80,
|
||||
),
|
||||
],
|
||||
),
|
||||
).marginAll(12),
|
||||
],
|
||||
),
|
||||
],
|
||||
).paddingSymmetric(vertical: 10),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@ -1007,7 +994,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
child: Obx(() => Container(
|
||||
decoration: BoxDecoration(
|
||||
color: selectedItems.items.contains(entry)
|
||||
? Theme.of(context).hoverColor
|
||||
? MyTheme.button
|
||||
: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(5.0),
|
||||
@ -1050,6 +1037,11 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
),
|
||||
Expanded(
|
||||
child: Text(entry.name.nonBreaking,
|
||||
style: TextStyle(
|
||||
color: selectedItems.items
|
||||
.contains(entry)
|
||||
? Colors.white
|
||||
: null),
|
||||
overflow:
|
||||
TextOverflow.ellipsis))
|
||||
]),
|
||||
@ -1111,7 +1103,10 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: MyTheme.darkGray,
|
||||
color: selectedItems.items
|
||||
.contains(entry)
|
||||
? Colors.white70
|
||||
: MyTheme.darkGray,
|
||||
),
|
||||
)),
|
||||
),
|
||||
@ -1131,7 +1126,11 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
sizeStr,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 10, color: MyTheme.darkGray),
|
||||
fontSize: 10,
|
||||
color:
|
||||
selectedItems.items.contains(entry)
|
||||
? Colors.white70
|
||||
: MyTheme.darkGray),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -451,7 +451,7 @@ class FileController {
|
||||
final isWindows = otherSideData.options.isWindows;
|
||||
final showHidden = otherSideData.options.showHidden;
|
||||
for (var from in items.items) {
|
||||
final jobID = jobController.add(from, isRemoteToLocal);
|
||||
final jobID = jobController.addTransferJob(from, isRemoteToLocal);
|
||||
bind.sessionSendFiles(
|
||||
sessionId: sessionId,
|
||||
actId: jobID,
|
||||
@ -494,13 +494,21 @@ class FileController {
|
||||
fd.format(isWindows);
|
||||
dialogManager?.dismissAll();
|
||||
if (fd.entries.isEmpty) {
|
||||
var deleteJobId = jobController.addDeleteDirJob(item, !isLocal, 0);
|
||||
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);
|
||||
sendRemoveEmptyDir(
|
||||
item.path,
|
||||
0,
|
||||
deleteJobId,
|
||||
);
|
||||
} else {
|
||||
jobController.updateJobStatus(deleteJobId,
|
||||
error: "cancel", state: JobState.done);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@ -508,6 +516,13 @@ class FileController {
|
||||
} else {
|
||||
entries = [];
|
||||
}
|
||||
int deleteJobId;
|
||||
if (item.isDirectory) {
|
||||
deleteJobId =
|
||||
jobController.addDeleteDirJob(item, !isLocal, entries.length);
|
||||
} else {
|
||||
deleteJobId = jobController.addDeleteFileJob(item, !isLocal);
|
||||
}
|
||||
|
||||
for (var i = 0; i < entries.length; i++) {
|
||||
final dirShow = item.isDirectory
|
||||
@ -522,24 +537,32 @@ class FileController {
|
||||
);
|
||||
try {
|
||||
if (confirm == true) {
|
||||
sendRemoveFile(entries[i].path, i);
|
||||
sendRemoveFile(entries[i].path, i, deleteJobId);
|
||||
final res = await jobController.jobResultListener.start();
|
||||
// handle remove res;
|
||||
if (item.isDirectory &&
|
||||
res['file_num'] == (entries.length - 1).toString()) {
|
||||
sendRemoveEmptyDir(item.path, i);
|
||||
sendRemoveEmptyDir(item.path, i, deleteJobId);
|
||||
}
|
||||
} else {
|
||||
jobController.updateJobStatus(deleteJobId,
|
||||
file_num: i, error: "cancel");
|
||||
}
|
||||
if (_removeCheckboxRemember) {
|
||||
if (confirm == true) {
|
||||
for (var j = i + 1; j < entries.length; j++) {
|
||||
sendRemoveFile(entries[j].path, j);
|
||||
sendRemoveFile(entries[j].path, j, deleteJobId);
|
||||
final res = await jobController.jobResultListener.start();
|
||||
if (item.isDirectory &&
|
||||
res['file_num'] == (entries.length - 1).toString()) {
|
||||
sendRemoveEmptyDir(item.path, i);
|
||||
sendRemoveEmptyDir(item.path, i, deleteJobId);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
jobController.updateJobStatus(deleteJobId,
|
||||
error: "cancel",
|
||||
file_num: entries.length,
|
||||
state: JobState.done);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -618,22 +641,19 @@ class FileController {
|
||||
}, useAnimation: false);
|
||||
}
|
||||
|
||||
void sendRemoveFile(String path, int fileNum) {
|
||||
void sendRemoveFile(String path, int fileNum, int actId) {
|
||||
bind.sessionRemoveFile(
|
||||
sessionId: sessionId,
|
||||
actId: JobController.jobID.next(),
|
||||
actId: actId,
|
||||
path: path,
|
||||
isRemote: !isLocal,
|
||||
fileNum: fileNum);
|
||||
}
|
||||
|
||||
void sendRemoveEmptyDir(String path, int fileNum) {
|
||||
void sendRemoveEmptyDir(String path, int fileNum, int actId) {
|
||||
history.removeWhere((element) => element.contains(path));
|
||||
bind.sessionRemoveAllEmptyDirs(
|
||||
sessionId: sessionId,
|
||||
actId: JobController.jobID.next(),
|
||||
path: path,
|
||||
isRemote: !isLocal);
|
||||
sessionId: sessionId, actId: actId, path: path, isRemote: !isLocal);
|
||||
}
|
||||
|
||||
Future<void> createDir(String path) async {
|
||||
@ -729,14 +749,11 @@ class JobController {
|
||||
return jobTable.indexWhere((element) => element.id == id);
|
||||
}
|
||||
|
||||
// JobProgress? getJob(int id) {
|
||||
// return jobTable.firstWhere((element) => element.id == id);
|
||||
// }
|
||||
|
||||
// return jobID
|
||||
int add(Entry from, bool isRemoteToLocal) {
|
||||
int addTransferJob(Entry from, bool isRemoteToLocal) {
|
||||
final jobID = JobController.jobID.next();
|
||||
jobTable.add(JobProgress()
|
||||
..type = JobType.transfer
|
||||
..fileName = path.basename(from.path)
|
||||
..jobName = from.path
|
||||
..totalSize = from.size
|
||||
@ -746,6 +763,33 @@ class JobController {
|
||||
return jobID;
|
||||
}
|
||||
|
||||
int addDeleteFileJob(Entry file, bool isRemote) {
|
||||
final jobID = JobController.jobID.next();
|
||||
jobTable.add(JobProgress()
|
||||
..type = JobType.deleteFile
|
||||
..fileName = path.basename(file.path)
|
||||
..jobName = file.path
|
||||
..totalSize = file.size
|
||||
..state = JobState.none
|
||||
..id = jobID
|
||||
..isRemoteToLocal = isRemote);
|
||||
return jobID;
|
||||
}
|
||||
|
||||
int addDeleteDirJob(Entry file, bool isRemote, int fileCount) {
|
||||
final jobID = JobController.jobID.next();
|
||||
jobTable.add(JobProgress()
|
||||
..type = JobType.deleteDir
|
||||
..fileName = path.basename(file.path)
|
||||
..jobName = file.path
|
||||
..fileCount = fileCount
|
||||
..totalSize = file.size
|
||||
..state = JobState.none
|
||||
..id = jobID
|
||||
..isRemoteToLocal = isRemote);
|
||||
return jobID;
|
||||
}
|
||||
|
||||
void tryUpdateJobProgress(Map<String, dynamic> evt) {
|
||||
try {
|
||||
int id = int.parse(evt['id']);
|
||||
@ -756,6 +800,7 @@ class JobController {
|
||||
job.fileNum = int.parse(evt['file_num']);
|
||||
job.speed = double.parse(evt['speed']);
|
||||
job.finishedSize = int.parse(evt['finished_size']);
|
||||
job.recvJobRes = true;
|
||||
debugPrint("update job $id with $evt");
|
||||
jobTable.refresh();
|
||||
}
|
||||
@ -764,20 +809,48 @@ class JobController {
|
||||
}
|
||||
}
|
||||
|
||||
void jobDone(Map<String, dynamic> evt) async {
|
||||
Future<bool> jobDone(Map<String, dynamic> evt) async {
|
||||
if (jobResultListener.isListening) {
|
||||
jobResultListener.complete(evt);
|
||||
return;
|
||||
// return;
|
||||
}
|
||||
|
||||
int id = int.parse(evt['id']);
|
||||
int id = -1;
|
||||
int? fileNum = 0;
|
||||
double? speed = 0;
|
||||
try {
|
||||
id = int.parse(evt['id']);
|
||||
} catch (_) {}
|
||||
final jobIndex = getJob(id);
|
||||
if (jobIndex != -1) {
|
||||
final job = jobTable[jobIndex];
|
||||
job.finishedSize = job.totalSize;
|
||||
if (jobIndex == -1) return true;
|
||||
final job = jobTable[jobIndex];
|
||||
job.recvJobRes = true;
|
||||
if (job.type == JobType.deleteFile) {
|
||||
job.state = JobState.done;
|
||||
job.fileNum = int.parse(evt['file_num']);
|
||||
jobTable.refresh();
|
||||
} else if (job.type == JobType.deleteDir) {
|
||||
try {
|
||||
fileNum = int.tryParse(evt['file_num']);
|
||||
} catch (_) {}
|
||||
if (fileNum != null) {
|
||||
if (fileNum < job.fileNum) return true; // file_num can be 0 at last
|
||||
job.fileNum = fileNum;
|
||||
if (fileNum >= job.fileCount - 1) {
|
||||
job.state = JobState.done;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
fileNum = int.tryParse(evt['file_num']);
|
||||
speed = double.tryParse(evt['speed']);
|
||||
} catch (_) {}
|
||||
if (fileNum != null) job.fileNum = fileNum;
|
||||
if (speed != null) job.speed = speed;
|
||||
job.state = JobState.done;
|
||||
}
|
||||
jobTable.refresh();
|
||||
if (job.type == JobType.deleteDir) {
|
||||
return job.state == JobState.done;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -788,16 +861,52 @@ class JobController {
|
||||
final job = jobTable[jobIndex];
|
||||
job.state = JobState.error;
|
||||
job.err = err;
|
||||
job.fileNum = int.parse(evt['file_num']);
|
||||
if (err == "skipped") {
|
||||
job.state = JobState.done;
|
||||
job.finishedSize = job.totalSize;
|
||||
job.recvJobRes = true;
|
||||
if (job.type == JobType.transfer) {
|
||||
int? fileNum = int.tryParse(evt['file_num']);
|
||||
if (fileNum != null) job.fileNum = fileNum;
|
||||
if (err == "skipped") {
|
||||
job.state = JobState.done;
|
||||
job.finishedSize = job.totalSize;
|
||||
}
|
||||
} else if (job.type == JobType.deleteDir) {
|
||||
if (jobResultListener.isListening) {
|
||||
jobResultListener.complete(evt);
|
||||
}
|
||||
int? fileNum = int.tryParse(evt['file_num']);
|
||||
if (fileNum != null) job.fileNum = fileNum;
|
||||
} else if (job.type == JobType.deleteFile) {
|
||||
if (jobResultListener.isListening) {
|
||||
jobResultListener.complete(evt);
|
||||
}
|
||||
}
|
||||
jobTable.refresh();
|
||||
}
|
||||
debugPrint("jobError $evt");
|
||||
}
|
||||
|
||||
void updateJobStatus(int id,
|
||||
{int? file_num, String? error, JobState? state}) {
|
||||
final jobIndex = getJob(id);
|
||||
if (jobIndex < 0) return;
|
||||
final job = jobTable[jobIndex];
|
||||
job.recvJobRes = true;
|
||||
if (file_num != null) {
|
||||
job.fileNum = file_num;
|
||||
}
|
||||
if (error != null) {
|
||||
job.err = error;
|
||||
job.state = JobState.error;
|
||||
}
|
||||
if (state != null) {
|
||||
job.state = state;
|
||||
}
|
||||
if (job.type == JobType.deleteFile && error == null) {
|
||||
job.state = JobState.done;
|
||||
}
|
||||
jobTable.refresh();
|
||||
}
|
||||
|
||||
Future<void> cancelJob(int id) async {
|
||||
await bind.sessionCancelJob(sessionId: sessionId, actId: id);
|
||||
}
|
||||
@ -814,6 +923,7 @@ class JobController {
|
||||
final currJobId = JobController.jobID.next();
|
||||
String fileName = path.basename(isRemote ? remote : to);
|
||||
var jobProgress = JobProgress()
|
||||
..type = JobType.transfer
|
||||
..fileName = fileName
|
||||
..jobName = isRemote ? remote : to
|
||||
..id = currJobId
|
||||
@ -1088,8 +1198,12 @@ extension JobStateDisplay on JobState {
|
||||
}
|
||||
}
|
||||
|
||||
enum JobType { none, transfer, deleteFile, deleteDir }
|
||||
|
||||
class JobProgress {
|
||||
JobType type = JobType.none;
|
||||
JobState state = JobState.none;
|
||||
var recvJobRes = false;
|
||||
var id = 0;
|
||||
var fileNum = 0;
|
||||
var speed = 0.0;
|
||||
@ -1109,7 +1223,9 @@ class JobProgress {
|
||||
int lastTransferredSize = 0;
|
||||
|
||||
clear() {
|
||||
type = JobType.none;
|
||||
state = JobState.none;
|
||||
recvJobRes = false;
|
||||
id = 0;
|
||||
fileNum = 0;
|
||||
speed = 0;
|
||||
@ -1123,11 +1239,81 @@ class JobProgress {
|
||||
}
|
||||
|
||||
String display() {
|
||||
if (state == JobState.done && err == "skipped") {
|
||||
return translate("Skipped");
|
||||
if (type == JobType.transfer) {
|
||||
if (state == JobState.done && err == "skipped") {
|
||||
return translate("Skipped");
|
||||
}
|
||||
} else if (type == JobType.deleteFile) {
|
||||
if (err == "cancel") {
|
||||
return translate("Cancel");
|
||||
}
|
||||
}
|
||||
|
||||
return state.display();
|
||||
}
|
||||
|
||||
String getStatus() {
|
||||
int handledFileCount = recvJobRes ? fileNum + 1 : fileNum;
|
||||
if (handledFileCount >= fileCount) {
|
||||
handledFileCount = fileCount;
|
||||
}
|
||||
if (state == JobState.done) {
|
||||
handledFileCount = fileCount;
|
||||
finishedSize = totalSize;
|
||||
}
|
||||
final filesStr = "$handledFileCount/$fileCount files";
|
||||
final sizeStr = totalSize > 0 ? readableFileSize(totalSize.toDouble()) : "";
|
||||
final sizePercentStr = totalSize > 0 && finishedSize > 0
|
||||
? "${readableFileSize(finishedSize.toDouble())} / ${readableFileSize(totalSize.toDouble())}"
|
||||
: "";
|
||||
if (type == JobType.deleteFile) {
|
||||
return display();
|
||||
} else if (type == JobType.deleteDir) {
|
||||
var res = '';
|
||||
if (state == JobState.done || state == JobState.error) {
|
||||
res = display();
|
||||
}
|
||||
if (filesStr.isNotEmpty) {
|
||||
if (res.isNotEmpty) {
|
||||
res += " ";
|
||||
}
|
||||
res += filesStr;
|
||||
}
|
||||
|
||||
if (sizeStr.isNotEmpty) {
|
||||
if (res.isNotEmpty) {
|
||||
res += ", ";
|
||||
}
|
||||
res += sizeStr;
|
||||
}
|
||||
return res;
|
||||
} else if (type == JobType.transfer) {
|
||||
var res = "";
|
||||
if (state != JobState.inProgress && state != JobState.none) {
|
||||
res += display();
|
||||
}
|
||||
if (filesStr.isNotEmpty) {
|
||||
if (res.isNotEmpty) {
|
||||
res += ", ";
|
||||
}
|
||||
res += filesStr;
|
||||
}
|
||||
if (sizeStr.isNotEmpty && state != JobState.inProgress) {
|
||||
if (res.isNotEmpty) {
|
||||
res += ", ";
|
||||
}
|
||||
res += sizeStr;
|
||||
}
|
||||
if (sizePercentStr.isNotEmpty && state == JobState.inProgress) {
|
||||
if (res.isNotEmpty) {
|
||||
res += ", ";
|
||||
}
|
||||
res += sizePercentStr;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
class _PathStat {
|
||||
|
@ -304,8 +304,13 @@ class FfiModel with ChangeNotifier {
|
||||
} else if (name == 'job_progress') {
|
||||
parent.target?.fileModel.jobController.tryUpdateJobProgress(evt);
|
||||
} else if (name == 'job_done') {
|
||||
parent.target?.fileModel.jobController.jobDone(evt);
|
||||
parent.target?.fileModel.refreshAll();
|
||||
bool? refresh =
|
||||
await parent.target?.fileModel.jobController.jobDone(evt);
|
||||
if (refresh == true) {
|
||||
// many job done for delete directory
|
||||
// todo: refresh may not work when confirm delete local directory
|
||||
parent.target?.fileModel.refreshAll();
|
||||
}
|
||||
} else if (name == 'job_error') {
|
||||
parent.target?.fileModel.jobController.jobError(evt);
|
||||
} else if (name == 'override_file_confirm') {
|
||||
|
Loading…
x
Reference in New Issue
Block a user