diff --git a/src/main/java/com/kfmanager/service/FileOperations.java b/src/main/java/com/kfmanager/service/FileOperations.java index 7a0a200..af22f8f 100644 --- a/src/main/java/com/kfmanager/service/FileOperations.java +++ b/src/main/java/com/kfmanager/service/FileOperations.java @@ -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 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() { + @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() { + @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() { - @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 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; } } /** diff --git a/src/main/java/com/kfmanager/ui/FilePanelTab.java b/src/main/java/com/kfmanager/ui/FilePanelTab.java index 7932c2a..c95ecb5 100644 --- a/src/main/java/com/kfmanager/ui/FilePanelTab.java +++ b/src/main/java/com/kfmanager/ui/FilePanelTab.java @@ -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 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 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); diff --git a/src/main/java/com/kfmanager/ui/MainWindow.java b/src/main/java/com/kfmanager/ui/MainWindow.java index 0d7839d..dcf4162 100644 --- a/src/main/java/com/kfmanager/ui/MainWindow.java +++ b/src/main/java/com/kfmanager/ui/MainWindow.java @@ -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; } } diff --git a/src/main/java/com/kfmanager/ui/ProgressDialog.java b/src/main/java/com/kfmanager/ui/ProgressDialog.java new file mode 100644 index 0000000..3a4c420 --- /dev/null +++ b/src/main/java/com/kfmanager/ui/ProgressDialog.java @@ -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; + } +}