progress dialog added

This commit is contained in:
rdavidek 2026-01-14 21:55:15 +01:00
parent 2e2f4bd4f5
commit 69afa0c738
4 changed files with 307 additions and 84 deletions

View File

@ -23,25 +23,83 @@ public class FileOperations {
throw new IOException("Target directory does not exist");
}
int current = 0;
int total = items.size();
long totalSize = calculateTotalSize(items);
final long[] currentCopied = {0};
for (FileItem item : items) {
current++;
if (callback != null && callback.isCancelled()) break;
File source = item.getFile();
File target = new File(targetDirectory, source.getName());
if (callback != null) {
callback.onProgress(current, total, source.getName());
}
if (source.isDirectory()) {
copyDirectory(source.toPath(), target.toPath());
copyDirectory(source.toPath(), target.toPath(), totalSize, currentCopied, callback);
} else {
copyFile(source.toPath(), target.toPath());
copyFileWithProgress(source.toPath(), target.toPath(), totalSize, currentCopied, callback);
}
}
}
private static long calculateTotalSize(List<FileItem> items) {
long total = 0;
for (FileItem item : items) {
total += calculateSize(item.getFile().toPath());
}
return total;
}
private static long calculateSize(Path path) {
if (!Files.exists(path)) return 0;
if (!Files.isDirectory(path)) {
try { return Files.size(path); } catch (IOException e) { return 0; }
}
final long[] size = {0};
try {
Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
size[0] += attrs.size();
return FileVisitResult.CONTINUE;
}
});
} catch (IOException ignore) {}
return size[0];
}
private static void copyFileWithProgress(Path source, Path target, long totalSize, long[] totalCopied, ProgressCallback callback) throws IOException {
try (InputStream in = Files.newInputStream(source);
OutputStream out = Files.newOutputStream(target)) {
byte[] buffer = new byte[8192];
int length;
while ((length = in.read(buffer)) > 0) {
if (callback != null && callback.isCancelled()) return;
out.write(buffer, 0, length);
totalCopied[0] += length;
if (callback != null) {
callback.onProgress(totalCopied[0], totalSize, source.getFileName().toString());
}
}
}
Files.setLastModifiedTime(target, Files.getLastModifiedTime(source));
}
private static void copyDirectory(Path source, Path target, long totalSize, final long[] totalCopied, ProgressCallback callback) throws IOException {
Files.walkFileTree(source, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
Path targetDir = target.resolve(source.relativize(dir));
Files.createDirectories(targetDir);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (callback != null && callback.isCancelled()) return FileVisitResult.TERMINATE;
Path targetFile = target.resolve(source.relativize(file));
copyFileWithProgress(file, targetFile, totalSize, totalCopied, callback);
return FileVisitResult.CONTINUE;
}
});
}
/**
* Move files/directories to target directory
@ -55,6 +113,7 @@ public class FileOperations {
int total = items.size();
for (FileItem item : items) {
if (callback != null && callback.isCancelled()) break;
current++;
File source = item.getFile();
File target = new File(targetDirectory, source.getName());
@ -75,6 +134,7 @@ public class FileOperations {
int total = items.size();
for (FileItem item : items) {
if (callback != null && callback.isCancelled()) break;
current++;
File file = item.getFile();
@ -108,34 +168,6 @@ public class FileOperations {
}
}
/**
* Copy a file
*/
private static void copyFile(Path source, Path target) throws IOException {
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES);
}
/**
* Copy directory recursively
*/
private static void copyDirectory(Path source, Path target) throws IOException {
Files.walkFileTree(source, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
Path targetDir = target.resolve(source.relativize(dir));
Files.createDirectories(targetDir);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Path targetFile = target.resolve(source.relativize(file));
copyFile(file, targetFile);
return FileVisitResult.CONTINUE;
}
});
}
/**
* Delete directory recursively
*/
@ -243,8 +275,8 @@ public class FileOperations {
*/
public static void zip(List<FileItem> items, File targetZipFile, ProgressCallback callback) throws IOException {
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(targetZipFile))) {
int current = 0;
int total = items.size();
long current = 0;
long total = items.size();
for (FileItem item : items) {
current++;
@ -304,7 +336,7 @@ public class FileOperations {
File newFile = new File(targetDirectory, entry.getName());
if (callback != null) {
callback.onProgress(0, 0, entry.getName());
callback.onProgress(0L, 0L, entry.getName());
}
if (entry.isDirectory()) {
@ -338,7 +370,8 @@ public class FileOperations {
* Callback pro progress operací
*/
public interface ProgressCallback {
void onProgress(int current, int total, String currentFile);
void onProgress(long current, long total, String currentFile);
default boolean isCancelled() { return false; }
}
/**

View File

@ -955,15 +955,35 @@ public class FilePanelTab extends JPanel {
JOptionPane.YES_NO_OPTION,
JOptionPane.WARNING_MESSAGE);
if (res == JOptionPane.YES_OPTION) {
try {
java.util.List<FileItem> toDelete = new java.util.ArrayList<>();
toDelete.add(item);
com.kfmanager.service.FileOperations.delete(toDelete, null);
// reload current directory
loadDirectory(getCurrentDirectory());
} catch (Exception ex) {
try { JOptionPane.showMessageDialog(FilePanelTab.this, "Delete failed: " + ex.getMessage()); } catch (Exception ignore) {}
}
java.util.List<FileItem> toDelete = new java.util.ArrayList<>();
toDelete.add(item);
Window parentWindow = SwingUtilities.getWindowAncestor(FilePanelTab.this);
ProgressDialog progressDialog = new ProgressDialog(parentWindow instanceof Frame ? (Frame)parentWindow : null, "Deleting");
new Thread(() -> {
try {
com.kfmanager.service.FileOperations.delete(toDelete, new com.kfmanager.service.FileOperations.ProgressCallback() {
@Override
public void onProgress(long current, long total, String currentFile) {
progressDialog.updateProgress(current, total, currentFile);
}
@Override
public boolean isCancelled() {
return progressDialog.isCancelled();
}
});
SwingUtilities.invokeLater(() -> {
progressDialog.dispose();
loadDirectory(getCurrentDirectory());
});
} catch (Exception ex) {
SwingUtilities.invokeLater(() -> {
progressDialog.dispose();
JOptionPane.showMessageDialog(FilePanelTab.this, "Delete failed: " + ex.getMessage());
});
}
}).start();
progressDialog.setVisible(true);
}
});
menu.add(deleteItem);

View File

@ -652,9 +652,9 @@ public class MainWindow extends JFrame {
JOptionPane.OK_CANCEL_OPTION);
if (result == JOptionPane.OK_OPTION) {
performFileOperation(() -> {
FileOperations.copy(selectedItems, targetDir, null);
}, "Kopírování dokončeno", targetPanel);
performFileOperation((callback) -> {
FileOperations.copy(selectedItems, targetDir, callback);
}, "Kopírování dokončeno", true, targetPanel);
}
}
@ -680,9 +680,9 @@ public class MainWindow extends JFrame {
JOptionPane.OK_CANCEL_OPTION);
if (result == JOptionPane.OK_OPTION) {
performFileOperation(() -> {
FileOperations.move(selectedItems, targetDir, null);
}, "Přesouvání dokončeno", activePanel, targetPanel);
performFileOperation((callback) -> {
FileOperations.move(selectedItems, targetDir, callback);
}, "Přesouvání dokončeno", false, activePanel, targetPanel);
}
}
@ -718,9 +718,9 @@ public class MainWindow extends JFrame {
JOptionPane.WARNING_MESSAGE);
if (result == JOptionPane.YES_OPTION) {
performFileOperation(() -> {
FileOperations.delete(selectedItems, null);
}, "Mazání dokončeno", activePanel);
performFileOperation((callback) -> {
FileOperations.delete(selectedItems, callback);
}, "Mazání dokončeno", false, activePanel);
// After deletion and refresh, restore selection: stay on same row if possible,
// otherwise move selection one row up.
@ -793,9 +793,9 @@ public class MainWindow extends JFrame {
}
final File finalTargetZip = targetZip;
performFileOperation(() -> {
FileOperations.zip(selectedItems, finalTargetZip, null);
}, "Zabaleno do " + zipName, targetPanel);
performFileOperation((callback) -> {
FileOperations.zip(selectedItems, finalTargetZip, callback);
}, "Zabaleno do " + zipName, false, targetPanel);
}
/**
@ -829,9 +829,9 @@ public class MainWindow extends JFrame {
JOptionPane.OK_CANCEL_OPTION);
if (result == JOptionPane.OK_OPTION) {
performFileOperation(() -> {
FileOperations.unzip(zipFile, targetDir, null);
}, "Rozbaleno do " + targetDir.getName(), targetPanel);
performFileOperation((callback) -> {
FileOperations.unzip(zipFile, targetDir, callback);
}, "Rozbaleno do " + targetDir.getName(), false, targetPanel);
}
}
@ -858,9 +858,9 @@ public class MainWindow extends JFrame {
"New name:",
item.getName());
if (newName != null && !newName.trim().isEmpty() && !newName.equals(item.getName())) {
performFileOperation(() -> {
performFileOperation((callback) -> {
FileOperations.rename(item.getFile(), newName.trim());
}, "Přejmenování dokončeno", activePanel);
}, "Přejmenování dokončeno", false, activePanel);
}
}
}
@ -875,9 +875,9 @@ public class MainWindow extends JFrame {
"New directory");
if (dirName != null && !dirName.trim().isEmpty()) {
performFileOperation(() -> {
performFileOperation((callback) -> {
FileOperations.createDirectory(activePanel.getCurrentDirectory(), dirName.trim());
}, "Directory created", activePanel);
}, "Directory created", false, activePanel);
}
}
@ -1066,21 +1066,50 @@ public class MainWindow extends JFrame {
/**
* Execute file operation with error handling
*/
private void performFileOperation(FileOperation operation, String successMessage, FilePanel... panelsToRefresh) {
try {
operation.execute();
for (FilePanel panel : panelsToRefresh) {
if (panel.getCurrentDirectory() != null) {
panel.loadDirectory(panel.getCurrentDirectory());
}
private void performFileOperation(FileOperation operation, String successMessage, boolean showBytes, FilePanel... panelsToRefresh) {
ProgressDialog progressDialog = new ProgressDialog(this, "Operační systém");
progressDialog.setDisplayAsBytes(showBytes);
FileOperations.ProgressCallback callback = new FileOperations.ProgressCallback() {
@Override
public void onProgress(long current, long total, String currentFile) {
progressDialog.updateProgress(current, total, currentFile);
}
// Info okna o úspěchu zrušena - operace proběhne tiše
} catch (Exception e) {
JOptionPane.showMessageDialog(this,
"Chyba: " + e.getMessage(),
"Chyba",
JOptionPane.ERROR_MESSAGE);
}
@Override
public boolean isCancelled() {
return progressDialog.isCancelled();
}
};
// Run operation in a background thread
new Thread(() -> {
try {
operation.execute(callback);
SwingUtilities.invokeLater(() -> {
progressDialog.dispose();
for (FilePanel panel : panelsToRefresh) {
if (panel.getCurrentDirectory() != null) {
panel.loadDirectory(panel.getCurrentDirectory());
}
}
if (callback.isCancelled()) {
JOptionPane.showMessageDialog(MainWindow.this, "Operace byla přerušena uživatelem.");
}
});
} catch (Exception e) {
SwingUtilities.invokeLater(() -> {
progressDialog.dispose();
JOptionPane.showMessageDialog(MainWindow.this,
"Chyba: " + e.getMessage(),
"Chyba",
JOptionPane.ERROR_MESSAGE);
});
}
}).start();
progressDialog.setVisible(true);
}
/**
@ -1142,6 +1171,6 @@ public class MainWindow extends JFrame {
@FunctionalInterface
private interface FileOperation {
void execute() throws Exception;
void execute(FileOperations.ProgressCallback callback) throws Exception;
}
}

View File

@ -0,0 +1,141 @@
package com.kfmanager.ui;
import javax.swing.*;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
public class ProgressDialog extends JDialog {
private final JProgressBar progressBar;
private final JLabel statusLabel;
private final JLabel speedLabel;
private final JButton pauseButton;
private final JButton cancelButton;
private volatile boolean cancelled = false;
private volatile boolean paused = false;
private long startTime = -1;
public ProgressDialog(Frame owner, String title) {
super(owner, title, true);
setLayout(new BorderLayout(10, 10));
((JPanel)getContentPane()).setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15));
JPanel infoPanel = new JPanel(new GridLayout(2, 1, 5, 5));
statusLabel = new JLabel("Starting...");
speedLabel = new JLabel("Speed: 0 B/s");
infoPanel.add(statusLabel);
infoPanel.add(speedLabel);
add(infoPanel, BorderLayout.NORTH);
progressBar = new JProgressBar(0, 100);
progressBar.setStringPainted(true);
progressBar.setPreferredSize(new Dimension(400, 25));
add(progressBar, BorderLayout.CENTER);
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
pauseButton = new JButton("Pause");
cancelButton = new JButton("Cancel");
pauseButton.addActionListener(e -> {
paused = !paused;
pauseButton.setText(paused ? "Resume" : "Pause");
if (!paused) {
synchronized(this) {
this.notifyAll();
}
}
});
cancelButton.addActionListener(e -> {
cancelled = true;
paused = false;
cancelButton.setEnabled(false);
statusLabel.setText("Cancelling...");
synchronized(this) {
this.notifyAll();
}
});
buttonPanel.add(pauseButton);
buttonPanel.add(cancelButton);
add(buttonPanel, BorderLayout.SOUTH);
setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
cancelled = true;
}
});
pack();
setMinimumSize(new Dimension(450, getHeight()));
setLocationRelativeTo(owner);
}
private boolean displayAsBytes = false;
public void setDisplayAsBytes(boolean displayAsBytes) {
this.displayAsBytes = displayAsBytes;
}
public void updateProgress(long current, long total, String fileName) {
if (startTime == -1) {
startTime = System.currentTimeMillis();
}
SwingUtilities.invokeLater(() -> {
if (total > 0) {
progressBar.setIndeterminate(false);
int percent = (int) ((double) current / total * 100);
progressBar.setValue(percent);
if (displayAsBytes) {
progressBar.setString(formatSize(current) + " / " + formatSize(total) + " (" + percent + "%)");
long elapsed = System.currentTimeMillis() - startTime;
if (elapsed > 0) {
long bytesPerSec = (long) (current / (elapsed / 1000.0));
speedLabel.setText("Speed: " + formatSize(bytesPerSec) + "/s");
}
} else {
progressBar.setString(current + " / " + total + " (" + percent + "%)");
speedLabel.setText("");
}
} else {
progressBar.setIndeterminate(true);
speedLabel.setText("");
}
statusLabel.setText(fileName);
});
checkState();
}
private String formatSize(long bytes) {
if (bytes < 1024) return bytes + " B";
int exp = (int) (Math.log(bytes) / Math.log(1024));
char pre = "KMGTPE".charAt(exp - 1);
return String.format("%.1f %cB", bytes / Math.pow(1024, exp), pre);
}
private void checkState() {
if (paused && !cancelled) {
synchronized(this) {
while (paused && !cancelled) {
try {
this.wait(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
}
}
public boolean isCancelled() {
return cancelled;
}
}