operation queue, symbolic link support
This commit is contained in:
parent
0950eba98a
commit
68fe5ca0c6
162
src/main/java/cz/kamma/kfmanager/service/FileOperationQueue.java
Normal file
162
src/main/java/cz/kamma/kfmanager/service/FileOperationQueue.java
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
package cz.kamma.kfmanager.service;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
public class FileOperationQueue {
|
||||||
|
public enum OperationStatus {
|
||||||
|
QUEUED, RUNNING, COMPLETED, FAILED, CANCELLED
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class QueuedTask {
|
||||||
|
private final String title;
|
||||||
|
private final String description;
|
||||||
|
private final OperationExecutor executor;
|
||||||
|
private volatile OperationStatus status = OperationStatus.QUEUED;
|
||||||
|
private volatile long currentProgress = 0;
|
||||||
|
private volatile long totalProgress = 0;
|
||||||
|
private volatile String currentFile = "";
|
||||||
|
private volatile String errorMessage = "";
|
||||||
|
|
||||||
|
public QueuedTask(String title, String description, OperationExecutor executor) {
|
||||||
|
this.title = title;
|
||||||
|
this.description = description;
|
||||||
|
this.executor = executor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() { return title; }
|
||||||
|
public String getDescription() { return description; }
|
||||||
|
public OperationStatus getStatus() { return status; }
|
||||||
|
public long getCurrentProgress() { return currentProgress; }
|
||||||
|
public long getTotalProgress() { return totalProgress; }
|
||||||
|
public String getCurrentFile() { return currentFile; }
|
||||||
|
public String getErrorMessage() { return errorMessage; }
|
||||||
|
|
||||||
|
public void cancel() {
|
||||||
|
if (status == OperationStatus.QUEUED || status == OperationStatus.RUNNING) {
|
||||||
|
status = OperationStatus.CANCELLED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface OperationExecutor {
|
||||||
|
void execute(FileOperations.ProgressCallback callback) throws Exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FileOperationQueue instance;
|
||||||
|
private final List<QueuedTask> tasks = new CopyOnWriteArrayList<>();
|
||||||
|
private final List<Consumer<List<QueuedTask>>> listeners = new ArrayList<>();
|
||||||
|
private boolean isRunning = false;
|
||||||
|
|
||||||
|
private FileOperationQueue() {}
|
||||||
|
|
||||||
|
public static synchronized FileOperationQueue getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = new FileOperationQueue();
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addTask(QueuedTask task) {
|
||||||
|
tasks.add(task);
|
||||||
|
notifyListeners();
|
||||||
|
startNextTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addListener(Consumer<List<QueuedTask>> listener) {
|
||||||
|
listeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeListener(Consumer<List<QueuedTask>> listener) {
|
||||||
|
listeners.remove(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyListeners() {
|
||||||
|
List<QueuedTask> currentTasks = new ArrayList<>(tasks);
|
||||||
|
for (Consumer<List<QueuedTask>> listener : listeners) {
|
||||||
|
listener.accept(currentTasks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void startNextTask() {
|
||||||
|
if (isRunning) return;
|
||||||
|
|
||||||
|
QueuedTask nextTask = null;
|
||||||
|
for (QueuedTask task : tasks) {
|
||||||
|
if (task.status == OperationStatus.QUEUED) {
|
||||||
|
nextTask = task;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextTask != null) {
|
||||||
|
isRunning = true;
|
||||||
|
final QueuedTask taskToRun = nextTask;
|
||||||
|
new Thread(() -> {
|
||||||
|
runTask(taskToRun);
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runTask(QueuedTask task) {
|
||||||
|
task.status = OperationStatus.RUNNING;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
try {
|
||||||
|
task.executor.execute(new FileOperations.ProgressCallback() {
|
||||||
|
@Override
|
||||||
|
public void onProgress(long current, long total, String currentFile) {
|
||||||
|
task.currentProgress = current;
|
||||||
|
task.totalProgress = total;
|
||||||
|
task.currentFile = currentFile;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isCancelled() {
|
||||||
|
return task.status == OperationStatus.CANCELLED;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FileOperations.OverwriteResponse confirmOverwrite(File file) {
|
||||||
|
// In background queue, we might want a default or auto-rename?
|
||||||
|
// For now, let's assume YES or handle it in the executor if possible.
|
||||||
|
// This is tricky for a background queue.
|
||||||
|
return FileOperations.OverwriteResponse.YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FileOperations.ErrorResponse onError(File file, Exception e) {
|
||||||
|
// For background queue, maybe skip on error?
|
||||||
|
return FileOperations.ErrorResponse.SKIP;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (task.status != OperationStatus.CANCELLED) {
|
||||||
|
task.status = OperationStatus.COMPLETED;
|
||||||
|
task.currentProgress = task.totalProgress; // Force 100% on completion
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
task.status = OperationStatus.FAILED;
|
||||||
|
task.errorMessage = e.getMessage();
|
||||||
|
task.currentProgress = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
isRunning = false;
|
||||||
|
startNextTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<QueuedTask> getTasks() {
|
||||||
|
return new ArrayList<>(tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearCompleted() {
|
||||||
|
tasks.removeIf(t -> t.status == OperationStatus.COMPLETED || t.status == OperationStatus.FAILED || t.status == OperationStatus.CANCELLED);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@ import cz.kamma.kfmanager.model.FileItem;
|
|||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.nio.file.*;
|
import java.nio.file.*;
|
||||||
import java.nio.file.attribute.BasicFileAttributes;
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
@ -23,15 +24,21 @@ public class FileOperations {
|
|||||||
throw new IOException("Target directory does not exist");
|
throw new IOException("Target directory does not exist");
|
||||||
}
|
}
|
||||||
|
|
||||||
long totalSize = calculateTotalSize(items);
|
List<FileItem> cleanedItems = cleanDuplicateItems(items);
|
||||||
final long[] currentCopied = {0};
|
long totalItems = calculateTotalItems(cleanedItems);
|
||||||
|
final long[] currentItem = {0};
|
||||||
final OverwriteResponse[] globalResponse = {null};
|
final OverwriteResponse[] globalResponse = {null};
|
||||||
|
|
||||||
for (FileItem item : items) {
|
for (FileItem item : cleanedItems) {
|
||||||
if (callback != null && callback.isCancelled()) break;
|
if (callback != null && callback.isCancelled()) break;
|
||||||
File source = item.getFile();
|
File source = item.getFile();
|
||||||
File target = new File(targetDirectory, source.getName());
|
File target = new File(targetDirectory, source.getName());
|
||||||
|
|
||||||
|
// If target is redicrected to subfolder of source, skip to avoid infinite loop
|
||||||
|
if (isSubfolder(source, targetDirectory)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// If target is the same as source (copying to the same directory), rename target
|
// If target is the same as source (copying to the same directory), rename target
|
||||||
if (source.getAbsolutePath().equals(target.getAbsolutePath())) {
|
if (source.getAbsolutePath().equals(target.getAbsolutePath())) {
|
||||||
target = new File(targetDirectory, "copy-of-" + source.getName());
|
target = new File(targetDirectory, "copy-of-" + source.getName());
|
||||||
@ -47,14 +54,23 @@ public class FileOperations {
|
|||||||
if (res == OverwriteResponse.NO) continue;
|
if (res == OverwriteResponse.NO) continue;
|
||||||
if (res == OverwriteResponse.YES_TO_ALL) globalResponse[0] = OverwriteResponse.YES_TO_ALL;
|
if (res == OverwriteResponse.YES_TO_ALL) globalResponse[0] = OverwriteResponse.YES_TO_ALL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle directory vs file clash
|
||||||
|
if (target.isDirectory() && !source.isDirectory()) {
|
||||||
|
deleteDirectoryInternal(target.toPath());
|
||||||
|
} else if (!target.isDirectory() && source.isDirectory()) {
|
||||||
|
Files.delete(target.toPath());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
if (source.isDirectory()) {
|
if (Files.isSymbolicLink(source.toPath())) {
|
||||||
copyDirectory(source.toPath(), target.toPath(), totalSize, currentCopied, callback, globalResponse);
|
copySymlink(source.toPath(), target.toPath(), totalItems, currentItem, callback);
|
||||||
|
} else if (source.isDirectory()) {
|
||||||
|
copyDirectory(source.toPath(), target.toPath(), totalItems, currentItem, callback, globalResponse);
|
||||||
} else {
|
} else {
|
||||||
copyFileWithProgress(source.toPath(), target.toPath(), totalSize, currentCopied, callback);
|
copyFileWithProgress(source.toPath(), target.toPath(), totalItems, currentItem, callback);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@ -71,34 +87,37 @@ public class FileOperations {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static long calculateTotalSize(List<FileItem> items) {
|
private static long calculateTotalItems(List<FileItem> items) {
|
||||||
long total = 0;
|
long total = 0;
|
||||||
for (FileItem item : items) {
|
for (FileItem item : items) {
|
||||||
total += calculateSize(item.getFile().toPath());
|
total += countItems(item.getFile().toPath());
|
||||||
}
|
}
|
||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static long calculateSize(Path path) {
|
private static long countItems(Path path) {
|
||||||
if (!Files.exists(path)) return 0;
|
if (!Files.exists(path, LinkOption.NOFOLLOW_LINKS)) return 0;
|
||||||
if (!Files.isDirectory(path)) {
|
if (Files.isSymbolicLink(path)) return 1;
|
||||||
try { return Files.size(path); } catch (IOException e) { return 0; }
|
if (!Files.isDirectory(path)) return 1;
|
||||||
}
|
final long[] count = {1}; // Start with 1 for the directory itself
|
||||||
final long[] size = {0};
|
|
||||||
try {
|
try {
|
||||||
Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
|
Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
|
||||||
|
@Override
|
||||||
|
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
|
||||||
|
if (!dir.equals(path)) count[0]++;
|
||||||
|
return FileVisitResult.CONTINUE;
|
||||||
|
}
|
||||||
@Override
|
@Override
|
||||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
|
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
|
||||||
size[0] += attrs.size();
|
count[0]++;
|
||||||
return FileVisitResult.CONTINUE;
|
return FileVisitResult.CONTINUE;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (IOException ignore) {}
|
} catch (IOException ignore) {}
|
||||||
return size[0];
|
return count[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void copyFileWithProgress(Path source, Path target, long totalSize, long[] totalCopied, ProgressCallback callback) throws IOException {
|
private static void copyFileWithProgress(Path source, Path target, long totalItems, long[] currentItem, ProgressCallback callback) throws IOException {
|
||||||
long initialTotalCopied = totalCopied[0];
|
|
||||||
try (InputStream in = Files.newInputStream(source);
|
try (InputStream in = Files.newInputStream(source);
|
||||||
OutputStream out = Files.newOutputStream(target)) {
|
OutputStream out = Files.newOutputStream(target)) {
|
||||||
byte[] buffer = new byte[24576];
|
byte[] buffer = new byte[24576];
|
||||||
@ -106,26 +125,42 @@ public class FileOperations {
|
|||||||
while ((length = in.read(buffer)) > 0) {
|
while ((length = in.read(buffer)) > 0) {
|
||||||
if (callback != null && callback.isCancelled()) return;
|
if (callback != null && callback.isCancelled()) return;
|
||||||
out.write(buffer, 0, length);
|
out.write(buffer, 0, length);
|
||||||
totalCopied[0] += length;
|
}
|
||||||
|
}
|
||||||
|
currentItem[0]++;
|
||||||
if (callback != null) {
|
if (callback != null) {
|
||||||
callback.onProgress(totalCopied[0], totalSize, source.getFileName().toString());
|
callback.onProgress(currentItem[0], totalItems, source.getFileName().toString());
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
totalCopied[0] = initialTotalCopied;
|
|
||||||
throw e;
|
|
||||||
}
|
}
|
||||||
Files.setLastModifiedTime(target, Files.getLastModifiedTime(source));
|
Files.setLastModifiedTime(target, Files.getLastModifiedTime(source));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void copyDirectory(Path source, Path target, long totalSize, final long[] totalCopied, ProgressCallback callback, final OverwriteResponse[] globalResponse) throws IOException {
|
private static void copySymlink(Path source, Path target, long totalItems, long[] currentItem, ProgressCallback callback) throws IOException {
|
||||||
|
Path linkTarget = Files.readSymbolicLink(source);
|
||||||
|
Files.deleteIfExists(target);
|
||||||
|
Files.createSymbolicLink(target, linkTarget);
|
||||||
|
currentItem[0]++;
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onProgress(currentItem[0], totalItems, source.getFileName().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void copyDirectory(Path source, Path target, long totalItems, final long[] currentItem, ProgressCallback callback, final OverwriteResponse[] globalResponse) throws IOException {
|
||||||
Files.walkFileTree(source, new SimpleFileVisitor<Path>() {
|
Files.walkFileTree(source, new SimpleFileVisitor<Path>() {
|
||||||
@Override
|
@Override
|
||||||
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
|
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
|
||||||
Path targetDir = target.resolve(source.relativize(dir));
|
Path targetDir = target.resolve(source.relativize(dir));
|
||||||
|
if (Files.exists(targetDir) && !Files.isDirectory(targetDir)) {
|
||||||
|
Files.delete(targetDir);
|
||||||
|
}
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
Files.createDirectories(targetDir);
|
Files.createDirectories(targetDir);
|
||||||
|
if (!dir.equals(source)) {
|
||||||
|
currentItem[0]++;
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onProgress(currentItem[0], totalItems, dir.getFileName().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
return FileVisitResult.CONTINUE;
|
return FileVisitResult.CONTINUE;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
if (callback != null) {
|
if (callback != null) {
|
||||||
@ -156,11 +191,19 @@ public class FileOperations {
|
|||||||
if (res == OverwriteResponse.NO) return FileVisitResult.CONTINUE;
|
if (res == OverwriteResponse.NO) return FileVisitResult.CONTINUE;
|
||||||
if (res == OverwriteResponse.YES_TO_ALL) globalResponse[0] = OverwriteResponse.YES_TO_ALL;
|
if (res == OverwriteResponse.YES_TO_ALL) globalResponse[0] = OverwriteResponse.YES_TO_ALL;
|
||||||
}
|
}
|
||||||
|
// If we are here, we are overwriting. If target is a directory, delete it.
|
||||||
|
if (Files.isDirectory(targetFile)) {
|
||||||
|
deleteDirectoryInternal(targetFile);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
copyFileWithProgress(file, targetFile, totalSize, totalCopied, callback);
|
if (Files.isSymbolicLink(file)) {
|
||||||
|
copySymlink(file, targetFile, totalItems, currentItem, callback);
|
||||||
|
} else {
|
||||||
|
copyFileWithProgress(file, targetFile, totalItems, currentItem, callback);
|
||||||
|
}
|
||||||
return FileVisitResult.CONTINUE;
|
return FileVisitResult.CONTINUE;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
if (callback != null) {
|
if (callback != null) {
|
||||||
@ -176,6 +219,25 @@ public class FileOperations {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void deleteDirectoryInternal(Path path) throws IOException {
|
||||||
|
if (Files.isSymbolicLink(path)) {
|
||||||
|
Files.delete(path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
|
||||||
|
@Override
|
||||||
|
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||||
|
Files.delete(file);
|
||||||
|
return FileVisitResult.CONTINUE;
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
|
||||||
|
Files.delete(dir);
|
||||||
|
return FileVisitResult.CONTINUE;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move files/directories to target directory
|
* Move files/directories to target directory
|
||||||
*/
|
*/
|
||||||
@ -184,17 +246,22 @@ public class FileOperations {
|
|||||||
throw new IOException("Target directory does not exist");
|
throw new IOException("Target directory does not exist");
|
||||||
}
|
}
|
||||||
|
|
||||||
int current = 0;
|
List<FileItem> cleanedItems = cleanDuplicateItems(items);
|
||||||
int total = items.size();
|
long totalItems = calculateTotalItems(cleanedItems);
|
||||||
|
long[] currentItem = {0};
|
||||||
final OverwriteResponse[] globalResponse = {null};
|
final OverwriteResponse[] globalResponse = {null};
|
||||||
|
|
||||||
for (FileItem item : items) {
|
for (FileItem item : cleanedItems) {
|
||||||
if (callback != null && callback.isCancelled()) break;
|
if (callback != null && callback.isCancelled()) break;
|
||||||
current++;
|
|
||||||
File source = item.getFile();
|
File source = item.getFile();
|
||||||
File target = new File(targetDirectory, source.getName());
|
File target = new File(targetDirectory, source.getName());
|
||||||
|
|
||||||
if (target.exists() && !source.getAbsolutePath().equals(target.getAbsolutePath())) {
|
if (isSubfolder(source, targetDirectory)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use NOFOLLOW_LINKS for target existence check
|
||||||
|
if (Files.exists(target.toPath(), LinkOption.NOFOLLOW_LINKS) && !source.getAbsolutePath().equals(target.getAbsolutePath())) {
|
||||||
if (globalResponse[0] == OverwriteResponse.NO_TO_ALL) continue;
|
if (globalResponse[0] == OverwriteResponse.NO_TO_ALL) continue;
|
||||||
if (globalResponse[0] != OverwriteResponse.YES_TO_ALL) {
|
if (globalResponse[0] != OverwriteResponse.YES_TO_ALL) {
|
||||||
OverwriteResponse res = callback.confirmOverwrite(target);
|
OverwriteResponse res = callback.confirmOverwrite(target);
|
||||||
@ -206,10 +273,22 @@ public class FileOperations {
|
|||||||
if (res == OverwriteResponse.NO) continue;
|
if (res == OverwriteResponse.NO) continue;
|
||||||
if (res == OverwriteResponse.YES_TO_ALL) globalResponse[0] = OverwriteResponse.YES_TO_ALL;
|
if (res == OverwriteResponse.YES_TO_ALL) globalResponse[0] = OverwriteResponse.YES_TO_ALL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle directory vs file clash
|
||||||
|
if (target.isDirectory() && !source.isDirectory()) {
|
||||||
|
deleteDirectoryInternal(target.toPath());
|
||||||
|
} else if (!target.isDirectory() && source.isDirectory()) {
|
||||||
|
Files.delete(target.toPath());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (callback != null) {
|
if (callback != null) {
|
||||||
callback.onProgress(current, total, source.getName());
|
// For move, we report the start of moving the item.
|
||||||
|
// Note: if it's a directory, this counts as multiple items in totalItems,
|
||||||
|
// but Files.move will do it in one go. We increment currentItem by the actual count.
|
||||||
|
long itemCount = countItems(source.toPath());
|
||||||
|
currentItem[0] += itemCount;
|
||||||
|
callback.onProgress(currentItem[0], totalItems, source.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
@ -234,23 +313,29 @@ public class FileOperations {
|
|||||||
* Delete files/directories
|
* Delete files/directories
|
||||||
*/
|
*/
|
||||||
public static void delete(List<FileItem> items, ProgressCallback callback) throws IOException {
|
public static void delete(List<FileItem> items, ProgressCallback callback) throws IOException {
|
||||||
int current = 0;
|
List<FileItem> cleanedItems = cleanDuplicateItems(items);
|
||||||
int total = items.size();
|
long totalItems = calculateTotalItems(cleanedItems);
|
||||||
|
long[] currentItem = {0};
|
||||||
|
|
||||||
for (FileItem item : items) {
|
for (FileItem item : cleanedItems) {
|
||||||
if (callback != null && callback.isCancelled()) break;
|
if (callback != null && callback.isCancelled()) break;
|
||||||
current++;
|
|
||||||
File file = item.getFile();
|
File file = item.getFile();
|
||||||
|
|
||||||
if (callback != null) {
|
|
||||||
callback.onProgress(current, total, file.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
if (file.isDirectory()) {
|
if (Files.isSymbolicLink(file.toPath())) {
|
||||||
deleteDirectory(file.toPath(), callback);
|
currentItem[0]++;
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onProgress(currentItem[0], totalItems, file.getName());
|
||||||
|
}
|
||||||
|
Files.delete(file.toPath());
|
||||||
|
} else if (file.isDirectory()) {
|
||||||
|
deleteDirectory(file.toPath(), totalItems, currentItem, callback);
|
||||||
} else {
|
} else {
|
||||||
|
currentItem[0]++;
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onProgress(currentItem[0], totalItems, file.getName());
|
||||||
|
}
|
||||||
Files.delete(file.toPath());
|
Files.delete(file.toPath());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -268,6 +353,47 @@ public class FileOperations {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static List<FileItem> cleanDuplicateItems(List<FileItem> items) {
|
||||||
|
if (items == null || items.size() <= 1) return items;
|
||||||
|
|
||||||
|
List<FileItem> sorted = new ArrayList<>(items);
|
||||||
|
sorted.sort((a, b) -> a.getFile().getAbsolutePath().compareTo(b.getFile().getAbsolutePath()));
|
||||||
|
|
||||||
|
List<FileItem> cleaned = new ArrayList<>();
|
||||||
|
for (int i = 0; i < sorted.size(); i++) {
|
||||||
|
FileItem current = sorted.get(i);
|
||||||
|
boolean isDuplicate = false;
|
||||||
|
for (int j = 0; j < i; j++) {
|
||||||
|
File parent = sorted.get(j).getFile();
|
||||||
|
if (isSubfolder(parent, current.getFile())) {
|
||||||
|
isDuplicate = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isDuplicate) {
|
||||||
|
cleaned.add(current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isSubfolder(File parent, File child) {
|
||||||
|
if (parent == null || child == null) return false;
|
||||||
|
try {
|
||||||
|
String parentPath = parent.getCanonicalPath();
|
||||||
|
String childPath = child.getCanonicalPath();
|
||||||
|
|
||||||
|
if (parentPath.equals(childPath)) return true;
|
||||||
|
|
||||||
|
if (!parentPath.endsWith(File.separator)) {
|
||||||
|
parentPath += File.separator;
|
||||||
|
}
|
||||||
|
return childPath.startsWith(parentPath);
|
||||||
|
} catch (IOException e) {
|
||||||
|
return child.getAbsolutePath().startsWith(parent.getAbsolutePath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rename a file or directory
|
* Rename a file or directory
|
||||||
*/
|
*/
|
||||||
@ -289,12 +415,24 @@ public class FileOperations {
|
|||||||
/**
|
/**
|
||||||
* Delete directory recursively
|
* Delete directory recursively
|
||||||
*/
|
*/
|
||||||
private static void deleteDirectory(Path directory, ProgressCallback callback) throws IOException {
|
private static void deleteDirectory(Path directory, long totalItems, long[] currentItem, ProgressCallback callback) throws IOException {
|
||||||
|
if (Files.isSymbolicLink(directory)) {
|
||||||
|
currentItem[0]++;
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onProgress(currentItem[0], totalItems, directory.getFileName().toString());
|
||||||
|
}
|
||||||
|
Files.delete(directory);
|
||||||
|
return;
|
||||||
|
}
|
||||||
Files.walkFileTree(directory, new SimpleFileVisitor<Path>() {
|
Files.walkFileTree(directory, new SimpleFileVisitor<Path>() {
|
||||||
@Override
|
@Override
|
||||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
|
currentItem[0]++;
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onProgress(currentItem[0], totalItems, file.getFileName().toString());
|
||||||
|
}
|
||||||
Files.delete(file);
|
Files.delete(file);
|
||||||
return FileVisitResult.CONTINUE;
|
return FileVisitResult.CONTINUE;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@ -314,6 +452,10 @@ public class FileOperations {
|
|||||||
if (exc != null) throw exc;
|
if (exc != null) throw exc;
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
|
currentItem[0]++;
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onProgress(currentItem[0], totalItems, dir.getFileName().toString());
|
||||||
|
}
|
||||||
Files.delete(dir);
|
Files.delete(dir);
|
||||||
return FileVisitResult.CONTINUE;
|
return FileVisitResult.CONTINUE;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@ -568,25 +710,28 @@ public class FileOperations {
|
|||||||
* Zip files/directories into a target zip file
|
* Zip files/directories into a target zip file
|
||||||
*/
|
*/
|
||||||
public static void zip(List<FileItem> items, File targetZipFile, ProgressCallback callback) throws IOException {
|
public static void zip(List<FileItem> items, File targetZipFile, ProgressCallback callback) throws IOException {
|
||||||
|
List<FileItem> cleanedItems = cleanDuplicateItems(items);
|
||||||
|
long totalItems = calculateTotalItems(cleanedItems);
|
||||||
|
long[] currentItem = {0};
|
||||||
|
|
||||||
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(targetZipFile))) {
|
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(targetZipFile))) {
|
||||||
long current = 0;
|
for (FileItem item : cleanedItems) {
|
||||||
long total = items.size();
|
if (callback != null && callback.isCancelled()) break;
|
||||||
|
addToZip(item.getFile(), item.getName(), zos, totalItems, currentItem, callback);
|
||||||
for (FileItem item : items) {
|
|
||||||
current++;
|
|
||||||
File source = item.getFile();
|
|
||||||
if (callback != null) {
|
|
||||||
callback.onProgress(current, total, source.getName());
|
|
||||||
}
|
|
||||||
addToZip(source, source.getName(), zos);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void addToZip(File fileToZip, String fileName, ZipOutputStream zos) throws IOException {
|
private static void addToZip(File fileToZip, String fileName, ZipOutputStream zos, long totalItems, long[] currentItem, ProgressCallback callback) throws IOException {
|
||||||
if (fileToZip.isHidden()) {
|
if (fileToZip.isHidden()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentItem[0]++;
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onProgress(currentItem[0], totalItems, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
if (fileToZip.isDirectory()) {
|
if (fileToZip.isDirectory()) {
|
||||||
if (fileName.endsWith("/")) {
|
if (fileName.endsWith("/")) {
|
||||||
zos.putNextEntry(new ZipEntry(fileName));
|
zos.putNextEntry(new ZipEntry(fileName));
|
||||||
@ -598,7 +743,7 @@ public class FileOperations {
|
|||||||
File[] children = fileToZip.listFiles();
|
File[] children = fileToZip.listFiles();
|
||||||
if (children != null) {
|
if (children != null) {
|
||||||
for (File childFile : children) {
|
for (File childFile : children) {
|
||||||
addToZip(childFile, fileName + "/" + childFile.getName(), zos);
|
addToZip(childFile, fileName + "/" + childFile.getName(), zos, totalItems, currentItem, callback);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@ -623,14 +768,22 @@ public class FileOperations {
|
|||||||
Files.createDirectories(targetDirectory.toPath());
|
Files.createDirectories(targetDirectory.toPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
long totalItems = 0;
|
||||||
|
try (ZipFile zf = new ZipFile(zipFile)) {
|
||||||
|
totalItems = zf.size();
|
||||||
|
} catch (IOException e) {
|
||||||
|
// fallback if ZipFile fails
|
||||||
|
}
|
||||||
|
|
||||||
|
long currentItem = 0;
|
||||||
try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile))) {
|
try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile))) {
|
||||||
ZipEntry entry;
|
ZipEntry entry;
|
||||||
// First pass or estimated count could be done, but keep it simple for now
|
|
||||||
while ((entry = zis.getNextEntry()) != null) {
|
while ((entry = zis.getNextEntry()) != null) {
|
||||||
|
currentItem++;
|
||||||
File newFile = new File(targetDirectory, entry.getName());
|
File newFile = new File(targetDirectory, entry.getName());
|
||||||
|
|
||||||
if (callback != null) {
|
if (callback != null) {
|
||||||
callback.onProgress(0L, 0L, entry.getName());
|
callback.onProgress(currentItem, totalItems, entry.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.isDirectory()) {
|
if (entry.isDirectory()) {
|
||||||
@ -644,6 +797,10 @@ public class FileOperations {
|
|||||||
throw new IOException("Failed to create directory " + parent);
|
throw new IOException("Failed to create directory " + parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (newFile.exists() && newFile.isDirectory()) {
|
||||||
|
deleteDirectoryInternal(newFile.toPath());
|
||||||
|
}
|
||||||
|
|
||||||
// write file content
|
// write file content
|
||||||
try (FileOutputStream fos = new FileOutputStream(newFile)) {
|
try (FileOutputStream fos = new FileOutputStream(newFile)) {
|
||||||
byte[] buffer = new byte[24576];
|
byte[] buffer = new byte[24576];
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import cz.kamma.kfmanager.MainApp;
|
|||||||
import cz.kamma.kfmanager.config.AppConfig;
|
import cz.kamma.kfmanager.config.AppConfig;
|
||||||
import cz.kamma.kfmanager.model.FileItem;
|
import cz.kamma.kfmanager.model.FileItem;
|
||||||
import cz.kamma.kfmanager.service.FileOperations;
|
import cz.kamma.kfmanager.service.FileOperations;
|
||||||
|
import cz.kamma.kfmanager.service.FileOperationQueue;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
@ -657,6 +658,10 @@ public class MainWindow extends JFrame {
|
|||||||
refreshItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F5, InputEvent.CTRL_DOWN_MASK));
|
refreshItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F5, InputEvent.CTRL_DOWN_MASK));
|
||||||
refreshItem.addActionListener(e -> refreshPanels());
|
refreshItem.addActionListener(e -> refreshPanels());
|
||||||
|
|
||||||
|
JMenuItem queueItem = new JMenuItem("Operations Queue...");
|
||||||
|
queueItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q, InputEvent.CTRL_DOWN_MASK));
|
||||||
|
queueItem.addActionListener(e -> OperationQueueDialog.showQueue(this));
|
||||||
|
|
||||||
JMenuItem exitItem = new JMenuItem("Exit");
|
JMenuItem exitItem = new JMenuItem("Exit");
|
||||||
exitItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F10, 0));
|
exitItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F10, 0));
|
||||||
exitItem.addActionListener(e -> saveConfigAndExit());
|
exitItem.addActionListener(e -> saveConfigAndExit());
|
||||||
@ -664,6 +669,7 @@ public class MainWindow extends JFrame {
|
|||||||
fileMenu.add(searchItem);
|
fileMenu.add(searchItem);
|
||||||
fileMenu.add(selectWildcardItem);
|
fileMenu.add(selectWildcardItem);
|
||||||
fileMenu.add(refreshItem);
|
fileMenu.add(refreshItem);
|
||||||
|
fileMenu.add(queueItem);
|
||||||
fileMenu.addSeparator();
|
fileMenu.addSeparator();
|
||||||
fileMenu.add(exitItem);
|
fileMenu.add(exitItem);
|
||||||
|
|
||||||
@ -1272,10 +1278,15 @@ public class MainWindow extends JFrame {
|
|||||||
"Copy");
|
"Copy");
|
||||||
|
|
||||||
if (result == 0 || result == 1) {
|
if (result == 0 || result == 1) {
|
||||||
boolean modal = (result == 0);
|
boolean background = (result == 1);
|
||||||
|
if (background) {
|
||||||
|
addOperationToQueue("Copy", String.format("Copy %d items to %s", selectedItems.size(), targetDir.getName()),
|
||||||
|
(cb) -> FileOperations.copy(selectedItems, targetDir, cb), targetPanel);
|
||||||
|
} else {
|
||||||
performFileOperation((callback) -> {
|
performFileOperation((callback) -> {
|
||||||
FileOperations.copy(selectedItems, targetDir, callback);
|
FileOperations.copy(selectedItems, targetDir, callback);
|
||||||
}, "Copy completed", true, modal, targetPanel);
|
}, "Copy completed", false, true, targetPanel);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (activePanel != null && activePanel.getFileTable() != null) {
|
if (activePanel != null && activePanel.getFileTable() != null) {
|
||||||
activePanel.getFileTable().requestFocusInWindow();
|
activePanel.getFileTable().requestFocusInWindow();
|
||||||
@ -1305,10 +1316,15 @@ public class MainWindow extends JFrame {
|
|||||||
"Move");
|
"Move");
|
||||||
|
|
||||||
if (result == 0 || result == 1) {
|
if (result == 0 || result == 1) {
|
||||||
boolean modal = (result == 0);
|
boolean background = (result == 1);
|
||||||
|
if (background) {
|
||||||
|
addOperationToQueue("Move", String.format("Move %d items to %s", selectedItems.size(), targetDir.getName()),
|
||||||
|
(cb) -> FileOperations.move(selectedItems, targetDir, cb), activePanel, targetPanel);
|
||||||
|
} else {
|
||||||
performFileOperation((callback) -> {
|
performFileOperation((callback) -> {
|
||||||
FileOperations.move(selectedItems, targetDir, callback);
|
FileOperations.move(selectedItems, targetDir, callback);
|
||||||
}, "Move completed", false, modal, activePanel, targetPanel);
|
}, "Move completed", false, true, activePanel, targetPanel);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (activePanel != null && activePanel.getFileTable() != null) {
|
if (activePanel != null && activePanel.getFileTable() != null) {
|
||||||
activePanel.getFileTable().requestFocusInWindow();
|
activePanel.getFileTable().requestFocusInWindow();
|
||||||
@ -1364,16 +1380,17 @@ public class MainWindow extends JFrame {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int result = JOptionPane.showConfirmDialog(this,
|
int result = showConfirmWithBackground(message.toString(), "Delete");
|
||||||
message.toString(),
|
|
||||||
"Delete",
|
|
||||||
JOptionPane.YES_NO_OPTION,
|
|
||||||
JOptionPane.WARNING_MESSAGE);
|
|
||||||
|
|
||||||
if (result == JOptionPane.YES_OPTION) {
|
if (result == 0 || result == 1) {
|
||||||
|
boolean background = (result == 1);
|
||||||
|
if (background) {
|
||||||
|
addOperationToQueue("Delete", String.format("Delete %d items", selectedItems.size()),
|
||||||
|
(cb) -> FileOperations.delete(selectedItems, cb), activePanel);
|
||||||
|
} else {
|
||||||
performFileOperation((callback) -> {
|
performFileOperation((callback) -> {
|
||||||
FileOperations.delete(selectedItems, callback);
|
FileOperations.delete(selectedItems, callback);
|
||||||
}, "Delete completed", false, activePanel);
|
}, "Delete completed", false, true, activePanel);
|
||||||
|
|
||||||
// After deletion and refresh, restore selection: move focus to the nearest higher item.
|
// After deletion and refresh, restore selection: move focus to the nearest higher item.
|
||||||
SwingUtilities.invokeLater(() -> {
|
SwingUtilities.invokeLater(() -> {
|
||||||
@ -1386,10 +1403,9 @@ public class MainWindow extends JFrame {
|
|||||||
}
|
}
|
||||||
} catch (Exception ignore) {}
|
} catch (Exception ignore) {}
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
if (activePanel != null && activePanel.getFileTable() != null) {
|
|
||||||
activePanel.getFileTable().requestFocusInWindow();
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
requestFocusInActivePanel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1453,9 +1469,23 @@ public class MainWindow extends JFrame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final File finalTargetZip = targetZip;
|
final File finalTargetZip = targetZip;
|
||||||
|
int result = showConfirmWithBackground(
|
||||||
|
String.format("Zip %d items to:\n%s", selectedItems.size(), targetZip.getAbsolutePath()),
|
||||||
|
"Zip");
|
||||||
|
|
||||||
|
if (result == 0 || result == 1) {
|
||||||
|
boolean background = (result == 1);
|
||||||
|
if (background) {
|
||||||
|
addOperationToQueue("Zip", String.format("Zip %d items to %s", selectedItems.size(), finalTargetZip.getName()),
|
||||||
|
(cb) -> FileOperations.zip(selectedItems, finalTargetZip, cb), targetPanel);
|
||||||
|
} else {
|
||||||
performFileOperation((callback) -> {
|
performFileOperation((callback) -> {
|
||||||
FileOperations.zip(selectedItems, finalTargetZip, callback);
|
FileOperations.zip(selectedItems, finalTargetZip, callback);
|
||||||
}, "Zipped into " + zipName, false, targetPanel, targetPanel);
|
}, "Zipped into " + zipName, false, true, targetPanel);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
requestFocusInActivePanel();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1485,15 +1515,20 @@ public class MainWindow extends JFrame {
|
|||||||
FilePanel targetPanel = (activePanel == leftPanel) ? rightPanel : leftPanel;
|
FilePanel targetPanel = (activePanel == leftPanel) ? rightPanel : leftPanel;
|
||||||
File targetDir = targetPanel.getCurrentDirectory();
|
File targetDir = targetPanel.getCurrentDirectory();
|
||||||
|
|
||||||
int result = JOptionPane.showConfirmDialog(this,
|
int result = showConfirmWithBackground(
|
||||||
String.format("Unzip %s to:\n%s", zipFile.getName(), targetDir.getAbsolutePath()),
|
String.format("Unzip %s to:\n%s", zipFile.getName(), targetDir.getAbsolutePath()),
|
||||||
"Unzip",
|
"Unzip");
|
||||||
JOptionPane.OK_CANCEL_OPTION);
|
|
||||||
|
|
||||||
if (result == JOptionPane.OK_OPTION) {
|
if (result == 0 || result == 1) {
|
||||||
|
boolean background = (result == 1);
|
||||||
|
if (background) {
|
||||||
|
addOperationToQueue("Unzip", String.format("Unzip %s to %s", zipFile.getName(), targetDir.getName()),
|
||||||
|
(cb) -> FileOperations.unzip(zipFile, targetDir, cb), targetPanel);
|
||||||
|
} else {
|
||||||
performFileOperation((callback) -> {
|
performFileOperation((callback) -> {
|
||||||
FileOperations.unzip(zipFile, targetDir, callback);
|
FileOperations.unzip(zipFile, targetDir, callback);
|
||||||
}, "Unzipped into " + targetDir.getName(), false, targetPanel);
|
}, "Unzipped into " + targetDir.getName(), false, true, targetPanel);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (activePanel != null && activePanel.getFileTable() != null) {
|
if (activePanel != null && activePanel.getFileTable() != null) {
|
||||||
activePanel.getFileTable().requestFocusInWindow();
|
activePanel.getFileTable().requestFocusInWindow();
|
||||||
@ -2200,6 +2235,23 @@ public class MainWindow extends JFrame {
|
|||||||
void execute(FileOperations.ProgressCallback callback) throws Exception;
|
void execute(FileOperations.ProgressCallback callback) throws Exception;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void addOperationToQueue(String title, String description, FileOperation operation, FilePanel... panelsToRefresh) {
|
||||||
|
FileOperationQueue.QueuedTask task = new FileOperationQueue.QueuedTask(title, description, (callback) -> {
|
||||||
|
operation.execute(callback);
|
||||||
|
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
for (FilePanel panel : panelsToRefresh) {
|
||||||
|
if (panel.getCurrentDirectory() != null) {
|
||||||
|
panel.loadDirectory(panel.getCurrentDirectory(), false, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
FileOperationQueue.getInstance().addTask(task);
|
||||||
|
OperationQueueDialog.showQueue(this);
|
||||||
|
}
|
||||||
|
|
||||||
private void requestFocusInActivePanel() {
|
private void requestFocusInActivePanel() {
|
||||||
if (activePanel != null && activePanel.getFileTable() != null) {
|
if (activePanel != null && activePanel.getFileTable() != null) {
|
||||||
activePanel.getFileTable().requestFocusInWindow();
|
activePanel.getFileTable().requestFocusInWindow();
|
||||||
|
|||||||
128
src/main/java/cz/kamma/kfmanager/ui/OperationQueueDialog.java
Normal file
128
src/main/java/cz/kamma/kfmanager/ui/OperationQueueDialog.java
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
package cz.kamma.kfmanager.ui;
|
||||||
|
|
||||||
|
import cz.kamma.kfmanager.service.FileOperationQueue;
|
||||||
|
import cz.kamma.kfmanager.service.FileOperationQueue.QueuedTask;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import javax.swing.table.AbstractTableModel;
|
||||||
|
import javax.swing.table.DefaultTableCellRenderer;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class OperationQueueDialog extends JDialog {
|
||||||
|
private final JTable table;
|
||||||
|
private final TaskTableModel model;
|
||||||
|
private static OperationQueueDialog instance;
|
||||||
|
|
||||||
|
public static synchronized void showQueue(Frame owner) {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = new OperationQueueDialog(owner);
|
||||||
|
}
|
||||||
|
instance.setVisible(true);
|
||||||
|
instance.toFront();
|
||||||
|
}
|
||||||
|
|
||||||
|
private OperationQueueDialog(Frame owner) {
|
||||||
|
super(owner, "File Operation Queue", false);
|
||||||
|
|
||||||
|
setLayout(new BorderLayout(10, 10));
|
||||||
|
setSize(600, 400);
|
||||||
|
setLocationRelativeTo(owner);
|
||||||
|
|
||||||
|
model = new TaskTableModel();
|
||||||
|
table = new JTable(model);
|
||||||
|
table.setRowHeight(30);
|
||||||
|
|
||||||
|
table.getColumnModel().getColumn(2).setCellRenderer(new ProgressRenderer());
|
||||||
|
|
||||||
|
JScrollPane scrollPane = new JScrollPane(table);
|
||||||
|
add(scrollPane, BorderLayout.CENTER);
|
||||||
|
|
||||||
|
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||||
|
JButton closeButton = new JButton("Close");
|
||||||
|
closeButton.addActionListener(e -> setVisible(false));
|
||||||
|
|
||||||
|
JButton clearButton = new JButton("Clear Completed");
|
||||||
|
clearButton.addActionListener(e -> FileOperationQueue.getInstance().clearCompleted());
|
||||||
|
|
||||||
|
buttonPanel.add(clearButton);
|
||||||
|
buttonPanel.add(closeButton);
|
||||||
|
add(buttonPanel, BorderLayout.SOUTH);
|
||||||
|
|
||||||
|
FileOperationQueue.getInstance().addListener(tasks -> {
|
||||||
|
SwingUtilities.invokeLater(() -> model.setTasks(tasks));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial task load
|
||||||
|
model.setTasks(FileOperationQueue.getInstance().getTasks());
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TaskTableModel extends AbstractTableModel {
|
||||||
|
private List<QueuedTask> tasks = new ArrayList<>();
|
||||||
|
private final String[] columns = {"Operation", "Status", "Progress", "Active File"};
|
||||||
|
|
||||||
|
public void setTasks(List<QueuedTask> tasks) {
|
||||||
|
this.tasks = tasks;
|
||||||
|
fireTableDataChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getRowCount() { return tasks.size(); }
|
||||||
|
@Override
|
||||||
|
public int getColumnCount() { return columns.length; }
|
||||||
|
@Override
|
||||||
|
public String getColumnName(int column) { return columns[column]; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getValueAt(int rowIndex, int columnIndex) {
|
||||||
|
QueuedTask task = tasks.get(rowIndex);
|
||||||
|
switch (columnIndex) {
|
||||||
|
case 0: return task.getTitle();
|
||||||
|
case 1: return task.getStatus().toString();
|
||||||
|
case 2: return task; // For progress renderer
|
||||||
|
case 3: return task.getCurrentFile();
|
||||||
|
default: return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ProgressRenderer extends DefaultTableCellRenderer {
|
||||||
|
private final JProgressBar progressBar = new JProgressBar(0, 100);
|
||||||
|
|
||||||
|
public ProgressRenderer() {
|
||||||
|
progressBar.setStringPainted(true);
|
||||||
|
progressBar.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
|
||||||
|
if (value instanceof QueuedTask) {
|
||||||
|
QueuedTask task = (QueuedTask) value;
|
||||||
|
if (task.getStatus() == FileOperationQueue.OperationStatus.FAILED) {
|
||||||
|
progressBar.setIndeterminate(false);
|
||||||
|
progressBar.setValue(0);
|
||||||
|
progressBar.setString("Error");
|
||||||
|
} else if (task.getStatus() == FileOperationQueue.OperationStatus.COMPLETED) {
|
||||||
|
progressBar.setIndeterminate(false);
|
||||||
|
progressBar.setValue(100);
|
||||||
|
progressBar.setString("100%");
|
||||||
|
} else if (task.getTotalProgress() > 0) {
|
||||||
|
progressBar.setIndeterminate(false);
|
||||||
|
int percent = (int) Math.min(100, ((double) task.getCurrentProgress() / task.getTotalProgress() * 100));
|
||||||
|
progressBar.setValue(percent);
|
||||||
|
progressBar.setString(task.getCurrentProgress() + " / " + task.getTotalProgress() + " (" + percent + "%)");
|
||||||
|
} else if (task.getStatus() == FileOperationQueue.OperationStatus.RUNNING) {
|
||||||
|
progressBar.setIndeterminate(true);
|
||||||
|
progressBar.setString("Processing...");
|
||||||
|
} else {
|
||||||
|
progressBar.setIndeterminate(false);
|
||||||
|
progressBar.setValue(0);
|
||||||
|
progressBar.setString("");
|
||||||
|
}
|
||||||
|
return progressBar;
|
||||||
|
}
|
||||||
|
return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -97,10 +97,10 @@ public class ProgressDialog extends JDialog {
|
|||||||
SwingUtilities.invokeLater(() -> {
|
SwingUtilities.invokeLater(() -> {
|
||||||
if (total > 0) {
|
if (total > 0) {
|
||||||
progressBar.setIndeterminate(false);
|
progressBar.setIndeterminate(false);
|
||||||
int percent = (int) ((double) current / total * 100);
|
int percent = (int) Math.min(100, ((double) current / total * 100));
|
||||||
progressBar.setValue(percent);
|
progressBar.setValue(percent);
|
||||||
if (displayAsBytes) {
|
if (displayAsBytes) {
|
||||||
progressBar.setString(formatSize(current) + " / " + formatSize(total) + " (" + percent + "%)");
|
progressBar.setString(formatSize(Math.min(current, total)) + " / " + formatSize(total) + " (" + percent + "%)");
|
||||||
|
|
||||||
long elapsed = System.currentTimeMillis() - startTime;
|
long elapsed = System.currentTimeMillis() - startTime;
|
||||||
if (elapsed > 0) {
|
if (elapsed > 0) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user