diff --git a/README.md b/README.md index 335b901..b17feb5 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,51 @@ # KF File Manager -Dvoupanelový souborový manažer podobný Total Commander, vytvořený v Java 11. +A two-panel file manager similar to Total Commander, built with Java 11. -## Funkce +## Features - **Two panels** for browsing files and directories - **Copying** files and directories (F5) - **Moving** files and directories (F6) +- **Rename** (Shift+F6) - **Create directory** (F7) - **Delete** files and directories (F8) -- **Přejmenování** (Shift+F6) -- **Vyhledávání** souborů (Ctrl+F) -- **Přepínání** mezi panely (TAB) +- **Search** for files (Ctrl+F) +- **Sync Directories** - side-by-side folder comparison and synchronization +- **Toggle** between panels (TAB) - **Navigation** - double-click or Enter to open a directory -- **Zobrazení** velikosti souborů, data modifikace +- **View** file sizes, modification dates, and attributes -## Spuštění +## Running ```bash mvn clean compile mvn exec:java -Dexec.mainClass="cz.kamma.kfmanager.MainApp" ``` -Nebo vytvoření JAR souboru: +Or build a JAR file: ```bash mvn clean package java -jar target/kf-manager-1.0-SNAPSHOT.jar ``` -## Klávesové zkratky +## Keyboard Shortcuts -- **F5** - Kopírovat -- **F6** - Přesunout -- **Shift+F6** - Přejmenovat -- **F7** - Nový adresář -- **F8** - Smazat -- **TAB** - Přepnout mezi panely -- **Ctrl+F** - Vyhledat soubory -- **Enter** - Otevřít adresář -- **Backspace** - Nadřazený adresář +- **F3** - View file (internal viewer) +- **F4** - Edit file +- **F5** - Copy +- **F6** - Move +- **Shift+F6** - Rename +- **F7** - New directory +- **F8** - Delete +- **TAB** - Toggle between panels +- **Ctrl+F** - Search files +- **Ctrl+Y** - Sync directories +- **Enter** - Open directory +- **Backspace** - Parent directory -## Požadavky +## Requirements -- Java 11 nebo vyšší +- Java 11 or higher - Maven 3.6+ diff --git a/kf-manager.desktop b/kf-manager.desktop index 31c6115..41ef9ff 100755 --- a/kf-manager.desktop +++ b/kf-manager.desktop @@ -7,5 +7,5 @@ Exec=java -jar /home/kamma/projects/kf-manager/target/kf-manager-1.0-SNAPSHOT-ja Name=KF File Manager Icon=/home/kamma/projects/kf-manager/src/main/resources/icon.png StartupWMClass=cz-kamma-kfmanager-MainApp -Comment=Dvoupanelový souborový manažer pro Linux +Comment=Two-panel file manager for Linux Categories=Utility;System; diff --git a/src/main/java/cz/kamma/kfmanager/ui/DriveSelector.java b/src/main/java/cz/kamma/kfmanager/ui/DriveSelector.java index 1bd4c62..23cb347 100644 --- a/src/main/java/cz/kamma/kfmanager/ui/DriveSelector.java +++ b/src/main/java/cz/kamma/kfmanager/ui/DriveSelector.java @@ -24,7 +24,7 @@ public class DriveSelector extends JDialog { setLayout(new BorderLayout(10, 10)); ((JComponent) getContentPane()).setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); - // Získat seznam dostupných disků + // Get list of available drives File[] roots = File.listRoots(); List drives = new ArrayList<>(); @@ -32,7 +32,7 @@ public class DriveSelector extends JDialog { drives.add(new DriveInfo(root)); } - // Seznam disků + // List of drives JList driveList = new JList<>(drives.toArray(new DriveInfo[0])); driveList.setFont(new Font("Monospaced", Font.PLAIN, 14)); driveList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); @@ -53,7 +53,7 @@ public class DriveSelector extends JDialog { } }); - // Double-click pro výběr + // Double-click to select driveList.addMouseListener(new java.awt.event.MouseAdapter() { @Override public void mouseClicked(java.awt.event.MouseEvent e) { @@ -67,7 +67,7 @@ public class DriveSelector extends JDialog { } }); - // Enter pro výběr + // Enter to select driveList.addKeyListener(new java.awt.event.KeyAdapter() { @Override public void keyPressed(java.awt.event.KeyEvent e) { @@ -84,7 +84,7 @@ public class DriveSelector extends JDialog { JScrollPane scrollPane = new JScrollPane(driveList); add(scrollPane, BorderLayout.CENTER); - // Panel s tlačítky + // Panel with buttons JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); JButton okButton = new JButton("OK"); @@ -109,7 +109,7 @@ public class DriveSelector extends JDialog { add(buttonPanel, BorderLayout.SOUTH); - // Automaticky vybrat první disk + // Automatically select the first drive if (drives.size() > 0) { driveList.setSelectedIndex(0); } @@ -122,7 +122,7 @@ public class DriveSelector extends JDialog { } /** - * Pomocná třída pro informace o disku + * Helper class for drive information */ private static class DriveInfo { private final File root; diff --git a/src/main/java/cz/kamma/kfmanager/ui/FileEditor.java b/src/main/java/cz/kamma/kfmanager/ui/FileEditor.java index 3369235..a256469 100644 --- a/src/main/java/cz/kamma/kfmanager/ui/FileEditor.java +++ b/src/main/java/cz/kamma/kfmanager/ui/FileEditor.java @@ -61,7 +61,7 @@ public class FileEditor extends JDialog { } public FileEditor(Window parent, File file, String virtualPath, AppConfig config, boolean readOnly) { - super(parent, (readOnly ? "Prohlížeč - " : "Editor - ") + (virtualPath != null ? virtualPath.substring(virtualPath.lastIndexOf(File.separator) + 1) : file.getName()), ModalityType.MODELESS); + super(parent, (readOnly ? "Viewer - " : "Editor - ") + (virtualPath != null ? virtualPath.substring(virtualPath.lastIndexOf(File.separator) + 1) : file.getName()), ModalityType.MODELESS); this.file = file; this.virtualPath = virtualPath; this.config = config; @@ -101,10 +101,10 @@ public class FileEditor extends JDialog { searchField = new JTextField(20); searchField.addActionListener(e -> findNext()); - JButton nextBtn = new JButton("Dále"); + JButton nextBtn = new JButton("Next"); nextBtn.addActionListener(e -> findNext()); - JButton prevBtn = new JButton("Zpět"); + JButton prevBtn = new JButton("Back"); prevBtn.addActionListener(e -> findPrevious()); JButton closeBtn = new JButton("X"); @@ -113,7 +113,7 @@ public class FileEditor extends JDialog { searchStatusLabel = new JLabel(""); - searchPanel.add(new JLabel("Hledat:")); + searchPanel.add(new JLabel("Search:")); searchPanel.add(searchField); searchPanel.add(nextBtn); searchPanel.add(prevBtn); @@ -199,7 +199,7 @@ public class FileEditor extends JDialog { } catch (Exception ignore) {} searchStatusLabel.setText(""); } else { - searchStatusLabel.setText("Nenalezeno"); + searchStatusLabel.setText("Not found"); } } @@ -232,7 +232,7 @@ public class FileEditor extends JDialog { } catch (Exception ignore) {} searchStatusLabel.setText(""); } else { - searchStatusLabel.setText("Nenalezeno"); + searchStatusLabel.setText("Not found"); } } @@ -259,7 +259,7 @@ public class FileEditor extends JDialog { // Menu bar createMenuBar(); - // Textová oblast (editable nebo read-only) + // Text area (editable or read-only) textArea = new JTextArea(); textArea.setFont(config.getEditorFont()); textArea.setTabSize(4); @@ -271,7 +271,7 @@ public class FileEditor extends JDialog { undoManager.addEdit(e.getEdit()); }); - // Sledování změn (pouze pro editovatelný režim) + // Track changes (editable mode only) if (!readOnly) { textArea.getDocument().addDocumentListener(new javax.swing.event.DocumentListener() { public void insertUpdate(javax.swing.event.DocumentEvent e) { setModified(true); } @@ -295,10 +295,10 @@ public class FileEditor extends JDialog { // Apply appearance settings (colors, contrast caret etc) applyAppearance(); - // Kontextové menu pro clipboard + // Context menu for clipboard setupContextMenu(); - // Klávesové zkratky + // Keyboard shortcuts setupKeyBindings(); // Caret listener to update position and selection info @@ -314,11 +314,11 @@ public class FileEditor extends JDialog { private void setupContextMenu() { JPopupMenu popup = new JPopupMenu(); - JMenuItem undoItem = new JMenuItem("Zpět"); + JMenuItem undoItem = new JMenuItem("Undo"); undoItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Z, InputEvent.CTRL_DOWN_MASK)); undoItem.addActionListener(e -> { if (undoManager.canUndo()) undoManager.undo(); }); - JMenuItem redoItem = new JMenuItem("Znovu"); + JMenuItem redoItem = new JMenuItem("Redo"); redoItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Y, InputEvent.CTRL_DOWN_MASK)); redoItem.addActionListener(e -> { if (undoManager.canRedo()) undoManager.redo(); }); @@ -327,15 +327,15 @@ public class FileEditor extends JDialog { popup.addSeparator(); JMenuItem cutItem = new JMenuItem(new javax.swing.text.DefaultEditorKit.CutAction()); - cutItem.setText("Vyjmout"); + cutItem.setText("Cut"); cutItem.setMnemonic(KeyEvent.VK_X); JMenuItem copyItem = new JMenuItem(new javax.swing.text.DefaultEditorKit.CopyAction()); - copyItem.setText("Kopírovat"); + copyItem.setText("Copy"); copyItem.setMnemonic(KeyEvent.VK_C); JMenuItem pasteItem = new JMenuItem(new javax.swing.text.DefaultEditorKit.PasteAction()); - pasteItem.setText("Vložit"); + pasteItem.setText("Paste"); pasteItem.setMnemonic(KeyEvent.VK_V); popup.add(cutItem); @@ -343,7 +343,7 @@ public class FileEditor extends JDialog { popup.add(pasteItem); popup.addSeparator(); - JMenuItem selectAllItem = new JMenuItem("Vybrat vše"); + JMenuItem selectAllItem = new JMenuItem("Select All"); selectAllItem.setMnemonic(KeyEvent.VK_A); selectAllItem.addActionListener(e -> textArea.selectAll()); popup.add(selectAllItem); @@ -428,9 +428,9 @@ public class FileEditor extends JDialog { JMenu fileMenu = new JMenu("File"); fileMenu.setMnemonic(java.awt.event.KeyEvent.VK_S); - // Uložit - pouze v editovacím režimu + // Save - editable mode only if (!readOnly) { - JMenuItem saveItem = new JMenuItem("Uložit"); + JMenuItem saveItem = new JMenuItem("Save"); saveItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F2, 0)); saveItem.addActionListener(e -> saveFile()); fileMenu.add(saveItem); @@ -438,23 +438,23 @@ public class FileEditor extends JDialog { fileMenu.addSeparator(); } - JMenuItem closeItem = new JMenuItem("Zavřít"); + JMenuItem closeItem = new JMenuItem("Close"); closeItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0)); closeItem.addActionListener(e -> closeEditor()); fileMenu.add(closeItem); menuBar.add(fileMenu); - // Menu Edit + // Edit Menu JMenu editMenu = new JMenu("Edit"); editMenu.setMnemonic(java.awt.event.KeyEvent.VK_E); - JMenuItem undoItem = new JMenuItem("Zpět"); + JMenuItem undoItem = new JMenuItem("Undo"); undoItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Z, InputEvent.CTRL_DOWN_MASK)); undoItem.addActionListener(e -> { if (undoManager.canUndo()) undoManager.undo(); }); editMenu.add(undoItem); - JMenuItem redoItem = new JMenuItem("Znovu"); + JMenuItem redoItem = new JMenuItem("Redo"); redoItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Y, InputEvent.CTRL_DOWN_MASK)); redoItem.addActionListener(e -> { if (undoManager.canRedo()) undoManager.redo(); }); editMenu.add(redoItem); @@ -462,35 +462,35 @@ public class FileEditor extends JDialog { editMenu.addSeparator(); JMenuItem cutItem = new JMenuItem(new javax.swing.text.DefaultEditorKit.CutAction()); - cutItem.setText("Vyjmout"); + cutItem.setText("Cut"); cutItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_X, InputEvent.CTRL_DOWN_MASK)); editMenu.add(cutItem); JMenuItem copyItem = new JMenuItem(new javax.swing.text.DefaultEditorKit.CopyAction()); - copyItem.setText("Kopírovat"); + copyItem.setText("Copy"); copyItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_DOWN_MASK)); editMenu.add(copyItem); JMenuItem pasteItem = new JMenuItem(new javax.swing.text.DefaultEditorKit.PasteAction()); - pasteItem.setText("Vložit"); + pasteItem.setText("Paste"); pasteItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_V, InputEvent.CTRL_DOWN_MASK)); editMenu.add(pasteItem); editMenu.addSeparator(); - JMenuItem selectAllItem = new JMenuItem("Vybrat vše"); + JMenuItem selectAllItem = new JMenuItem("Select All"); selectAllItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_A, InputEvent.CTRL_DOWN_MASK)); selectAllItem.addActionListener(e -> textArea.selectAll()); editMenu.add(selectAllItem); editMenu.addSeparator(); - JMenuItem findItem = new JMenuItem("Hledat..."); + JMenuItem findItem = new JMenuItem("Search..."); findItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F, InputEvent.CTRL_DOWN_MASK)); findItem.addActionListener(e -> showSearchPanel(true)); editMenu.add(findItem); - JMenuItem findNextItem = new JMenuItem("Hledat další"); + JMenuItem findNextItem = new JMenuItem("Find Next"); findNextItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F3, 0)); findNextItem.addActionListener(e -> findNext()); editMenu.add(findNextItem); @@ -511,8 +511,8 @@ public class FileEditor extends JDialog { menuBar.add(editMenu); - // Menu Nastavení - JMenu settingsMenu = new JMenu("Nastavení"); + // Settings Menu + JMenu settingsMenu = new JMenu("Settings"); settingsMenu.setMnemonic(java.awt.event.KeyEvent.VK_N); JMenuItem fontItem = new JMenuItem("Font..."); @@ -556,14 +556,14 @@ public class FileEditor extends JDialog { private void setupKeyBindings() { JRootPane rootPane = getRootPane(); - // F2 - Uložit (pouze v editovacím režimu) + // F2 - Save (editable mode only) if (!readOnly) { rootPane.registerKeyboardAction(e -> saveFile(), KeyStroke.getKeyStroke(KeyEvent.VK_F2, 0), JComponent.WHEN_IN_FOCUSED_WINDOW); } - // ESC - Zavřít view + // ESC - Close view rootPane.registerKeyboardAction(e -> closeEditor(), KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW); @@ -626,7 +626,7 @@ public class FileEditor extends JDialog { if (undoManager.canRedo()) undoManager.redo(); }, KeyStroke.getKeyStroke(KeyEvent.VK_Y, InputEvent.CTRL_DOWN_MASK), JComponent.WHEN_IN_FOCUSED_WINDOW); - // Doprava/Doleva a Mezerník pro listování obrázků + // Right/Left arrow and Space for image cycling rootPane.registerKeyboardAction(e -> { if (isImageFile(file)) nextImage(); }, KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), JComponent.WHEN_IN_FOCUSED_WINDOW); @@ -644,7 +644,7 @@ public class FileEditor extends JDialog { if (undoManager.canRedo()) undoManager.redo(); }, KeyStroke.getKeyStroke(KeyEvent.VK_Z, InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK), JComponent.WHEN_IN_FOCUSED_WINDOW); - // Hledání + // Search rootPane.registerKeyboardAction(e -> { if (!isImageFile(file)) showSearchPanel(true); }, @@ -821,11 +821,11 @@ public class FileEditor extends JDialog { if (readOnly) return; if (!modified) { // No changes - inform the user briefly - JOptionPane.showMessageDialog(this, "Žádné změny k uložení.", "Info", JOptionPane.INFORMATION_MESSAGE); + JOptionPane.showMessageDialog(this, "No changes to save.", "Info", JOptionPane.INFORMATION_MESSAGE); return; } - int result = showSaveConfirmDialog("Soubor byl změněn. Uložit změny?", "Uložit změny"); + int result = showSaveConfirmDialog("File was modified. Save changes?", "Save changes"); if (result == JOptionPane.YES_OPTION) { saveFile(); @@ -863,7 +863,7 @@ public class FileEditor extends JDialog { updateStatus(); return; } catch (IOException e) { - JOptionPane.showMessageDialog(this, "Chyba při čtení z archivu: " + e.getMessage(), "Chyba", JOptionPane.ERROR_MESSAGE); + JOptionPane.showMessageDialog(this, "Error reading from archive: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); dispose(); return; } @@ -1078,7 +1078,7 @@ public class FileEditor extends JDialog { } private void updateTitle() { - setTitle((readOnly ? "Prohlížeč - " : "Editor - ") + file.getName() + (modified ? " *" : "")); + setTitle((readOnly ? "Viewer - " : "Editor - ") + file.getName() + (modified ? " *" : "")); } private void saveFile() { @@ -1088,14 +1088,14 @@ public class FileEditor extends JDialog { modified = false; updateTitle(); JOptionPane.showMessageDialog(this, - "Soubor uložen", - "Úspěch", + "File saved", + "Success", JOptionPane.INFORMATION_MESSAGE); updateStatus(); } catch (IOException e) { JOptionPane.showMessageDialog(this, - "Chyba při ukládání:\n" + e.getMessage(), - "Chyba", + "Error during saving:\n" + e.getMessage(), + "Error", JOptionPane.ERROR_MESSAGE); } } @@ -1113,7 +1113,7 @@ public class FileEditor extends JDialog { private void closeEditor() { saveWindowState(); if (!readOnly && modified) { - int result = showSaveConfirmDialog("Soubor byl změněn. Uložit změny?", "Neuložené změny"); + int result = showSaveConfirmDialog("File was modified. Save changes?", "Unsaved changes"); if (result == JOptionPane.YES_OPTION) { saveFile(); @@ -1121,7 +1121,7 @@ public class FileEditor extends JDialog { } else if (result == JOptionPane.NO_OPTION) { dispose(); } - // CANCEL_OPTION - nedělat nic + // CANCEL_OPTION - do nothing } else { dispose(); } @@ -1154,9 +1154,9 @@ public class FileEditor extends JDialog { dlg.add(new JLabel(message), BorderLayout.CENTER); JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); - JButton yesBtn = new JButton("Ano"); - JButton noBtn = new JButton("Ne"); - JButton cancelBtn = new JButton("Zrušit"); + JButton yesBtn = new JButton("Yes"); + JButton noBtn = new JButton("No"); + JButton cancelBtn = new JButton("Cancel"); final int[] result = {JOptionPane.CANCEL_OPTION}; diff --git a/src/main/java/cz/kamma/kfmanager/ui/FontChooserDialog.java b/src/main/java/cz/kamma/kfmanager/ui/FontChooserDialog.java index adc37d6..a2f124f 100644 --- a/src/main/java/cz/kamma/kfmanager/ui/FontChooserDialog.java +++ b/src/main/java/cz/kamma/kfmanager/ui/FontChooserDialog.java @@ -5,7 +5,7 @@ import javax.swing.*; import java.awt.*; /** - * Dialog pro výběr fontu + * Font selection dialog */ public class FontChooserDialog extends JDialog { private Font selectedFont; @@ -17,7 +17,7 @@ public class FontChooserDialog extends JDialog { private JTextArea previewArea; public FontChooserDialog(Window parent, Font initialFont) { - super(parent, "Výběr fontu", ModalityType.APPLICATION_MODAL); + super(parent, "Select Font", ModalityType.APPLICATION_MODAL); this.selectedFont = initialFont; initComponents(); @@ -31,10 +31,10 @@ public class FontChooserDialog extends JDialog { private void initComponents() { setLayout(new BorderLayout(10, 10)); - // Panel pro výběr (font, velikost, styl) + // Selection panel (font, size, style) JPanel selectionPanel = new JPanel(new GridLayout(1, 3, 10, 0)); - // Seznam fontů + // Font list JPanel fontPanel = new JPanel(new BorderLayout()); fontPanel.add(new JLabel("Font:"), BorderLayout.NORTH); @@ -51,9 +51,9 @@ public class FontChooserDialog extends JDialog { JScrollPane fontScroll = new JScrollPane(fontList); fontPanel.add(fontScroll, BorderLayout.CENTER); - // Seznam velikostí + // Size list JPanel sizePanel = new JPanel(new BorderLayout()); - sizePanel.add(new JLabel("Velikost:"), BorderLayout.NORTH); + sizePanel.add(new JLabel("Size:"), BorderLayout.NORTH); Integer[] sizes = {8, 9, 10, 11, 12, 13, 14, 16, 18, 20, 22, 24, 26, 28, 32, 36, 40, 48, 56, 64, 72}; sizeList = new JList<>(sizes); @@ -67,9 +67,9 @@ public class FontChooserDialog extends JDialog { JScrollPane sizeScroll = new JScrollPane(sizeList); sizePanel.add(sizeScroll, BorderLayout.CENTER); - // Seznam stylů (Plain, Bold, Italic, Bold Italic) + // Style list (Plain, Bold, Italic, Bold Italic) JPanel stylePanel = new JPanel(new BorderLayout()); - stylePanel.add(new JLabel("Styl:"), BorderLayout.NORTH); + stylePanel.add(new JLabel("Style:"), BorderLayout.NORTH); String[] styles = {"Plain", "Bold", "Italic", "Bold Italic"}; styleList = new JList<>(styles); @@ -100,7 +100,7 @@ public class FontChooserDialog extends JDialog { previewScroll.setPreferredSize(new Dimension(0, 120)); previewPanel.add(previewScroll, BorderLayout.CENTER); - // Tlačítka + // Buttons JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); JButton okButton = new JButton("OK"); @@ -109,7 +109,7 @@ public class FontChooserDialog extends JDialog { dispose(); }); - JButton cancelButton = new JButton("Zrušit"); + JButton cancelButton = new JButton("Cancel"); cancelButton.addActionListener(e -> { approved = false; dispose(); @@ -129,13 +129,13 @@ public class FontChooserDialog extends JDialog { } private void selectFont(Font font) { - // Najít a vybrat font + // Find and select font fontList.setSelectedValue(font.getName(), true); - // Najít a vybrat velikost + // Find and select size sizeList.setSelectedValue(font.getSize(), true); - // Najít a vybrat styl + // Find and select style int style = font.getStyle(); switch (style) { case Font.BOLD: diff --git a/src/main/java/cz/kamma/kfmanager/ui/MainWindow.java b/src/main/java/cz/kamma/kfmanager/ui/MainWindow.java index f1c3145..d041ab9 100644 --- a/src/main/java/cz/kamma/kfmanager/ui/MainWindow.java +++ b/src/main/java/cz/kamma/kfmanager/ui/MainWindow.java @@ -452,11 +452,18 @@ public class MainWindow extends JFrame { // Search button JButton btnSearch = new JButton("🔍"); - btnSearch.setToolTipText("Hledat soubory (Alt+F7)"); + btnSearch.setToolTipText("Search files (Alt+F7)"); btnSearch.setFocusable(false); btnSearch.addActionListener(e -> showSearchDialog()); toolBar.add(btnSearch); + // Sync button + JButton btnSync = new JButton("⚖"); + btnSync.setToolTipText("Compare directories"); + btnSync.setFocusable(false); + btnSync.addActionListener(e -> showSyncDialog()); + toolBar.add(btnSync); + toolBar.addSeparator(); // Load custom shortcuts from config @@ -1725,6 +1732,16 @@ public class MainWindow extends JFrame { dialog.setVisible(true); } + /** + * Show Sync Directories dialog + */ + private void showSyncDialog() { + File leftDir = leftPanel.getCurrentDirectory(); + File rightDir = rightPanel.getCurrentDirectory(); + SyncDirectoriesDialog dialog = new SyncDirectoriesDialog(this, leftDir, rightDir, config); + dialog.setVisible(true); + } + /** * Show wildcard select dialog */ diff --git a/src/main/java/cz/kamma/kfmanager/ui/SearchDialog.java b/src/main/java/cz/kamma/kfmanager/ui/SearchDialog.java index 62aff13..55edd0c 100644 --- a/src/main/java/cz/kamma/kfmanager/ui/SearchDialog.java +++ b/src/main/java/cz/kamma/kfmanager/ui/SearchDialog.java @@ -124,7 +124,7 @@ public class SearchDialog extends JDialog { gbc.gridx = 0; gbc.gridy = 0; - searchPanel.add(new JLabel("Hledat:"), gbc); + searchPanel.add(new JLabel("Search for:"), gbc); gbc.gridx = 1; gbc.weightx = 1.0; @@ -231,7 +231,7 @@ public class SearchDialog extends JDialog { }); JScrollPane scrollPane = new JScrollPane(resultsTable); - scrollPane.setBorder(BorderFactory.createTitledBorder("Výsledky")); + scrollPane.setBorder(BorderFactory.createTitledBorder("Results")); add(scrollPane, BorderLayout.CENTER); // Status bar with progress and message @@ -244,24 +244,24 @@ public class SearchDialog extends JDialog { statusPanel.add(statusLabel, BorderLayout.CENTER); statusPanel.add(statusProgressBar, BorderLayout.EAST); - // Panel s tlačítky + // Panel with buttons JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); - searchButton = new JButton("Hledat"); + searchButton = new JButton("Search"); searchButton.addActionListener(e -> performSearch()); - viewButton = new JButton("Zobrazit"); + viewButton = new JButton("View"); viewButton.setEnabled(false); viewButton.addActionListener(e -> viewSelectedFile()); - editButton = new JButton("Upravit"); + editButton = new JButton("Edit"); editButton.setEnabled(false); editButton.addActionListener(e -> editSelectedFile()); - cancelButton = new JButton("Zavřít"); + cancelButton = new JButton("Close"); cancelButton.addActionListener(e -> dispose()); - JButton openButton = new JButton("Otevřít umístění"); + JButton openButton = new JButton("Open location"); openButton.addActionListener(e -> openSelectedFile()); buttonPanel.add(searchButton); @@ -571,15 +571,15 @@ public class SearchDialog extends JDialog { statusLabel.setText("Done — found " + foundCount + " files"); if (tableModel.getRowCount() == 0) { JOptionPane.showMessageDialog(SearchDialog.this, - "Nebyly nalezeny žádné soubory", - "Výsledek", + "No files found", + "Result", JOptionPane.INFORMATION_MESSAGE); } } catch (Exception e) { statusLabel.setText("Error"); JOptionPane.showMessageDialog(SearchDialog.this, - "Chyba při hledání: " + e.getMessage(), - "Chyba", + "Error during search: " + e.getMessage(), + "Error", JOptionPane.ERROR_MESSAGE); } } @@ -589,7 +589,7 @@ public class SearchDialog extends JDialog { } /** - * Otevře umístění vybraného souboru v exploreru + * Open selected file location */ private void openSelectedFile() { int selectedRow = resultsTable.getSelectedRow(); @@ -622,8 +622,8 @@ public class SearchDialog extends JDialog { Desktop.getDesktop().open(item.getFile().getParentFile()); } catch (Exception e) { JOptionPane.showMessageDialog(this, - "Nepodařilo se otevřít umístění: " + e.getMessage(), - "Chyba", + "Could not open location: " + e.getMessage(), + "Error", JOptionPane.ERROR_MESSAGE); } } @@ -666,7 +666,7 @@ public class SearchDialog extends JDialog { }); viewer.setVisible(true); } catch (Exception e) { - JOptionPane.showMessageDialog(this, "Chyba při otevírání souboru: " + e.getMessage(), "Chyba", JOptionPane.ERROR_MESSAGE); + JOptionPane.showMessageDialog(this, "Error opening file: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); } } } @@ -682,7 +682,7 @@ public class SearchDialog extends JDialog { if (item != null && !item.isDirectory() && !"..".equals(item.getName())) { boolean inArchive = item.getPath() != null && !item.getPath().equals(item.getFile().getAbsolutePath()); if (inArchive) { - JOptionPane.showMessageDialog(this, "Editace souborů přímo v archivu není podporována. Použijte 'Otevřít umístění' pro přístup k archivu.", "Informace", JOptionPane.INFORMATION_MESSAGE); + JOptionPane.showMessageDialog(this, "Editing web files directly in an archive is not supported. Use 'Go to File' to access the archive.", "Information", JOptionPane.INFORMATION_MESSAGE); return; } try { @@ -710,18 +710,18 @@ public class SearchDialog extends JDialog { }); editor.setVisible(true); } catch (Exception e) { - JOptionPane.showMessageDialog(this, "Chyba při otevírání souboru: " + e.getMessage(), "Chyba", JOptionPane.ERROR_MESSAGE); + JOptionPane.showMessageDialog(this, "Error opening file: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); } } } } /** - * Model tabulky pro výsledky hledání + * Table model for search results */ private class ResultsTableModel extends AbstractTableModel { - private final String[] columnNames = {"Cesta", "Velikost", "Datum změny"}; + private final String[] columnNames = {"Path", "Size", "Modified Date"}; private List results = new ArrayList<>(); public void addResult(FileItem item) { diff --git a/src/main/java/cz/kamma/kfmanager/ui/SyncDirectoriesDialog.java b/src/main/java/cz/kamma/kfmanager/ui/SyncDirectoriesDialog.java new file mode 100644 index 0000000..35f5f6e --- /dev/null +++ b/src/main/java/cz/kamma/kfmanager/ui/SyncDirectoriesDialog.java @@ -0,0 +1,414 @@ +package cz.kamma.kfmanager.ui; + +import cz.kamma.kfmanager.config.AppConfig; +import cz.kamma.kfmanager.model.FileItem; +import cz.kamma.kfmanager.service.FileOperations; + +import javax.swing.*; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.DefaultTableCellRenderer; +import java.awt.*; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.io.File; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Collections; +import java.util.Map; +import java.util.HashMap; +import java.util.TreeMap; + +public class SyncDirectoriesDialog extends JDialog { + private JTextField leftPathField; + private JTextField rightPathField; + private JCheckBox subdirsCheckbox; + private JCheckBox byContentCheckbox; + private JCheckBox ignoreDateCheckbox; + private JToggleButton showLeftOnlyBtn; + private JToggleButton showEqualBtn; + private JToggleButton showDiffBtn; + private JToggleButton showRightOnlyBtn; + private List allEntries = new ArrayList<>(); + private JTable resultsTable; + private SyncTableModel tableModel; + private AppConfig config; + private JLabel statusLabel; + + public SyncDirectoriesDialog(Frame owner, File leftDir, File rightDir, AppConfig config) { + super(owner, "Synchronize Directories", true); + this.config = config; + + initComponents(leftDir, rightDir); + setSize(1000, 600); + setLocationRelativeTo(owner); + } + + private void initComponents(File leftDir, File rightDir) { + setLayout(new BorderLayout()); + + JPanel topPanel = new JPanel(); + topPanel.setLayout(new BoxLayout(topPanel, BoxLayout.Y_AXIS)); + topPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + + // Paths row + JPanel pathsPanel = new JPanel(new GridLayout(1, 2, 10, 0)); + + JPanel leftP = new JPanel(new BorderLayout(5, 0)); + leftPathField = new JTextField(leftDir != null ? leftDir.getAbsolutePath() : ""); + JButton browseLeft = new JButton("..."); + browseLeft.addActionListener(e -> browsePath(leftPathField)); + leftP.add(leftPathField, BorderLayout.CENTER); + leftP.add(browseLeft, BorderLayout.EAST); + + JPanel rightP = new JPanel(new BorderLayout(5, 0)); + rightPathField = new JTextField(rightDir != null ? rightDir.getAbsolutePath() : ""); + JButton browseRight = new JButton("..."); + browseRight.addActionListener(e -> browsePath(rightPathField)); + rightP.add(rightPathField, BorderLayout.CENTER); + rightP.add(browseRight, BorderLayout.EAST); + + pathsPanel.add(leftP); + pathsPanel.add(rightP); + topPanel.add(pathsPanel); + topPanel.add(Box.createVerticalStrut(10)); + + // Options toolbar + JPanel optionsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + JButton compareBtn = new JButton("Compare"); + compareBtn.addActionListener(e -> performComparison()); + optionsPanel.add(compareBtn); + + subdirsCheckbox = new JCheckBox("Include subdirectories"); + byContentCheckbox = new JCheckBox("By content"); + ignoreDateCheckbox = new JCheckBox("Ignore date"); + + optionsPanel.add(subdirsCheckbox); + optionsPanel.add(byContentCheckbox); + optionsPanel.add(ignoreDateCheckbox); + + optionsPanel.add(Box.createHorizontalStrut(20)); + optionsPanel.add(new JLabel("Show:")); + + showLeftOnlyBtn = new JToggleButton("➡"); + showEqualBtn = new JToggleButton("="); + showDiffBtn = new JToggleButton("≠"); + showRightOnlyBtn = new JToggleButton("⬅"); + + showLeftOnlyBtn.setSelected(true); + showEqualBtn.setSelected(true); + showDiffBtn.setSelected(true); + showRightOnlyBtn.setSelected(true); + + java.awt.event.ActionListener filterAl = e -> applyFilters(); + showLeftOnlyBtn.addActionListener(filterAl); + showEqualBtn.addActionListener(filterAl); + showDiffBtn.addActionListener(filterAl); + showRightOnlyBtn.addActionListener(filterAl); + + optionsPanel.add(showLeftOnlyBtn); + optionsPanel.add(showEqualBtn); + optionsPanel.add(showDiffBtn); + optionsPanel.add(showRightOnlyBtn); + + optionsPanel.add(Box.createHorizontalStrut(10)); + JButton duplicatesBtn = new JButton("duplicates"); + duplicatesBtn.addActionListener(e -> { + showEqualBtn.setSelected(true); + showDiffBtn.setSelected(true); + showLeftOnlyBtn.setSelected(false); + showRightOnlyBtn.setSelected(false); + applyFilters(); + }); + JButton singlesBtn = new JButton("singles"); + singlesBtn.addActionListener(e -> { + showEqualBtn.setSelected(false); + showDiffBtn.setSelected(false); + showLeftOnlyBtn.setSelected(true); + showRightOnlyBtn.setSelected(true); + applyFilters(); + }); + optionsPanel.add(duplicatesBtn); + optionsPanel.add(singlesBtn); + + topPanel.add(optionsPanel); + add(topPanel, BorderLayout.NORTH); + + // Table + tableModel = new SyncTableModel(); + resultsTable = new JTable(tableModel); + resultsTable.setShowGrid(false); + resultsTable.setIntercellSpacing(new Dimension(0, 0)); + resultsTable.setDefaultRenderer(Object.class, new SyncTableCellRenderer()); + + // Adjust column widths + resultsTable.getColumnModel().getColumn(3).setMaxWidth(40); // Icon column + + add(new JScrollPane(resultsTable), BorderLayout.CENTER); + + // Bottom panel + JPanel bottomPanel = new JPanel(new BorderLayout()); + statusLabel = new JLabel("Waiting for comparison..."); + statusLabel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + bottomPanel.add(statusLabel, BorderLayout.WEST); + + JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + JButton syncBtn = new JButton("Synchronize..."); + syncBtn.addActionListener(e -> performSync()); + JButton closeBtn = new JButton("Close"); + closeBtn.addActionListener(e -> dispose()); + btnPanel.add(syncBtn); + btnPanel.add(closeBtn); + bottomPanel.add(btnPanel, BorderLayout.EAST); + + add(bottomPanel, BorderLayout.SOUTH); + } + + private void browsePath(JTextField field) { + JFileChooser chooser = new JFileChooser(field.getText()); + chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) { + field.setText(chooser.getSelectedFile().getAbsolutePath()); + } + } + + private void performComparison() { + File leftDir = new File(leftPathField.getText()); + File rightDir = new File(rightPathField.getText()); + + if (!leftDir.isDirectory() || !rightDir.isDirectory()) { + JOptionPane.showMessageDialog(this, "Both paths must be directories.", "Error", JOptionPane.ERROR_MESSAGE); + return; + } + + statusLabel.setText("Comparing..."); + + new SwingWorker, Void>() { + @Override + protected List doInBackground() throws Exception { + return compareDirectories(leftDir, rightDir, "", subdirsCheckbox.isSelected()); + } + + @Override + protected void done() { + try { + allEntries = get(); + applyFilters(); + } catch (Exception e) { + e.printStackTrace(); + JOptionPane.showMessageDialog(SyncDirectoriesDialog.this, "Error during comparison: " + e.getMessage()); + statusLabel.setText("Error."); + } + } + }.execute(); + } + + private List compareDirectories(File leftRoot, File rightRoot, String relativePath, boolean recursive) { + Map map = new TreeMap<>(); + + File leftDir = relativePath.isEmpty() ? leftRoot : new File(leftRoot, relativePath); + File rightDir = relativePath.isEmpty() ? rightRoot : new File(rightRoot, relativePath); + + File[] leftFiles = leftDir.listFiles(); + File[] rightFiles = rightDir.listFiles(); + + if (leftFiles != null) { + for (File f : leftFiles) { + String name = relativePath.isEmpty() ? f.getName() : relativePath + File.separator + f.getName(); + SyncEntry entry = new SyncEntry(name); + entry.leftFile = f; + entry.leftSize = f.isDirectory() ? -1 : f.length(); + entry.leftModified = f.lastModified(); + entry.isDirectory = f.isDirectory(); + map.put(name, entry); + } + } + + if (rightFiles != null) { + for (File f : rightFiles) { + String name = relativePath.isEmpty() ? f.getName() : relativePath + File.separator + f.getName(); + SyncEntry entry = map.get(name); + if (entry == null) { + entry = new SyncEntry(name); + map.put(name, entry); + } + entry.rightFile = f; + entry.rightSize = f.isDirectory() ? -1 : f.length(); + entry.rightModified = f.lastModified(); + entry.isDirectory = entry.isDirectory || f.isDirectory(); + } + } + + List resultList = new ArrayList<>(); + List subdirsToProcess = new ArrayList<>(); + + for (SyncEntry entry : map.values()) { + if (entry.isDirectory) { + if (recursive) { + subdirsToProcess.add(entry.relativePath); + } + } else { + updateRelation(entry); + resultList.add(entry); + } + } + + if (recursive) { + for (String sub : subdirsToProcess) { + resultList.addAll(compareDirectories(leftRoot, rightRoot, sub, true)); + } + } + + return resultList; + } + + private void applyFilters() { + List filtered = new ArrayList<>(); + for (SyncEntry e : allEntries) { + if ("=".equals(e.relation) && showEqualBtn.isSelected()) filtered.add(e); + else if ("!=".equals(e.relation) && showDiffBtn.isSelected()) filtered.add(e); + else if ("->".equals(e.relation) && showLeftOnlyBtn.isSelected()) filtered.add(e); + else if ("<-".equals(e.relation) && showRightOnlyBtn.isSelected()) filtered.add(e); + } + tableModel.setResults(filtered); + statusLabel.setText(String.format("Showing %d of %d items.", filtered.size(), allEntries.size())); + } + + private void updateRelation(SyncEntry entry) { + if (entry.leftFile != null && entry.rightFile == null) { + entry.relation = "->"; + } else if (entry.leftFile == null && entry.rightFile != null) { + entry.relation = "<-"; + } else { + boolean sizeMatch = entry.leftSize == entry.rightSize; + boolean dateMatch = ignoreDateCheckbox.isSelected() || (entry.leftModified / 1000 == entry.rightModified / 1000); + + if (sizeMatch && dateMatch) { + if (byContentCheckbox.isSelected()) { + if (compareByContent(entry.leftFile, entry.rightFile)) { + entry.relation = "="; + } else { + entry.relation = "!="; + } + } else { + entry.relation = "="; + } + } else { + if (entry.leftModified > entry.rightModified) { + entry.relation = "->"; + } else if (entry.leftModified < entry.rightModified) { + entry.relation = "<-"; + } else { + entry.relation = "!="; + } + } + } + } + + private boolean compareByContent(File f1, File f2) { + if (f1.length() != f2.length()) return false; + try (java.io.InputStream i1 = new java.io.FileInputStream(f1); + java.io.InputStream i2 = new java.io.FileInputStream(f2)) { + byte[] b1 = new byte[8192]; + byte[] b2 = new byte[8192]; + int n; + while ((n = i1.read(b1)) != -1) { + if (i2.read(b2) != n) return false; + if (!java.util.Arrays.equals(b1, 0, n, b2, 0, n)) return false; + } + return true; + } catch (Exception e) { + return false; + } + } + + private void performSync() { + // Basic implementation of sync (copy missing/newer files) + // For now just show info, actual copy logic would use FileOperationQueue + JOptionPane.showMessageDialog(this, "Synchronization feature will be implemented soon. For now, only comparison is available.", "Information", JOptionPane.INFORMATION_MESSAGE); + } + + private static class SyncEntry { + String relativePath; + File leftFile; + File rightFile; + long leftSize; + long rightSize; + long leftModified; + long rightModified; + String relation = ""; // "=", "!=", "->", "<-" + boolean isDirectory; + + SyncEntry(String rel) { this.relativePath = rel; } + } + + private static class SyncTableModel extends AbstractTableModel { + private String[] columns = {"Name (L)", "Size", "Date", "", "Date", "Size", "Name (R)"}; + private List entries = new ArrayList<>(); + + public void setResults(List results) { + this.entries = results; + fireTableDataChanged(); + } + + @Override public int getRowCount() { return entries.size(); } + @Override public int getColumnCount() { return columns.length; } + @Override public String getColumnName(int col) { return columns[col]; } + + @Override + public Object getValueAt(int row, int col) { + SyncEntry e = entries.get(row); + switch(col) { + case 0: return e.leftFile != null ? e.relativePath : ""; + case 1: return e.leftFile != null ? formatSize(e.leftSize) : ""; + case 2: return e.leftFile != null ? formatDate(e.leftModified) : ""; + case 3: return e.relation; + case 4: return e.rightFile != null ? formatDate(e.rightModified) : ""; + case 5: return e.rightFile != null ? formatSize(e.rightSize) : ""; + case 6: return e.rightFile != null ? e.relativePath : ""; + } + return ""; + } + + private String formatSize(long size) { + if (size < 0) return ""; + return String.format("%,d", size); + } + + private String formatDate(long time) { + return new java.text.SimpleDateFormat("dd.MM.yy HH:mm:ss").format(new Date(time)); + } + } + + private static class SyncTableCellRenderer extends DefaultTableCellRenderer { + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSel, boolean hasFocus, int row, int col) { + Component c = super.getTableCellRendererComponent(table, value, isSel, hasFocus, row, col); + SyncTableModel model = (SyncTableModel) table.getModel(); + SyncEntry entry = model.entries.get(row); + + if (!isSel) { + if ("=".equals(entry.relation)) { + c.setForeground(Color.BLACK); + } else if ("!=".equals(entry.relation)) { + c.setForeground(Color.RED); + } else if ("->".equals(entry.relation)) { + c.setForeground(new Color(0, 100, 0)); // Dark green + } else if ("<-".equals(entry.relation)) { + c.setForeground(Color.BLUE); + } + } + + if (col == 3) { + setHorizontalAlignment(CENTER); + setFont(getFont().deriveFont(Font.BOLD)); + } else if (col == 1 || col == 5) { + setHorizontalAlignment(RIGHT); + } else { + setHorizontalAlignment(LEFT); + } + + return c; + } + } +} diff --git a/src/main/java/cz/kamma/kfmanager/ui/WildcardSelectDialog.java b/src/main/java/cz/kamma/kfmanager/ui/WildcardSelectDialog.java index 850f873..7f909b2 100644 --- a/src/main/java/cz/kamma/kfmanager/ui/WildcardSelectDialog.java +++ b/src/main/java/cz/kamma/kfmanager/ui/WildcardSelectDialog.java @@ -14,7 +14,7 @@ public class WildcardSelectDialog extends JDialog { private boolean confirmed = false; public WildcardSelectDialog(Frame parent) { - super(parent, "Vybrat podle masky", true); + super(parent, "Select by pattern", true); initComponents(); MainApp.applyReflectiveCaretColor(getContentPane()); @@ -31,7 +31,7 @@ public class WildcardSelectDialog extends JDialog { mainPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); // Label - JLabel label = new JLabel("Zadejte masku (např. *.txt, test*.*, file?.dat):"); + JLabel label = new JLabel("Enter pattern (e.g. *.txt, test*.*, file?.dat):"); mainPanel.add(label, BorderLayout.NORTH); // Text field for pattern @@ -50,7 +50,7 @@ public class WildcardSelectDialog extends JDialog { dispose(); }); - JButton cancelButton = new JButton("Zrušit"); + JButton cancelButton = new JButton("Cancel"); cancelButton.addActionListener(e -> { confirmed = false; dispose();