FTP draft

This commit is contained in:
Radek Davidek 2026-04-21 18:32:45 +02:00
parent 676b3bbb3a
commit a810cfac02
11 changed files with 1752 additions and 118 deletions

View File

@ -116,5 +116,10 @@
<artifactId>flatlaf-extras</artifactId> <artifactId>flatlaf-extras</artifactId>
<version>3.5.1</version> <version>3.5.1</version>
</dependency> </dependency>
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>3.11.1</version>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -1018,4 +1018,66 @@ public class AppConfig {
public void setMaxCompareLines(int lines) { public void setMaxCompareLines(int lines) {
properties.setProperty("compare.maxLines", String.valueOf(lines)); properties.setProperty("compare.maxLines", String.valueOf(lines));
} }
// --- FTP Profiles persistence ---
public java.util.List<cz.kamma.kfmanager.model.FtpProfile> getFtpProfiles() {
java.util.List<cz.kamma.kfmanager.model.FtpProfile> list = new java.util.ArrayList<>();
int count = Integer.parseInt(properties.getProperty("ftp.profiles.count", "0"));
for (int i = 0; i < count; i++) {
String id = properties.getProperty("ftp.profile." + i + ".id");
String name = properties.getProperty("ftp.profile." + i + ".name");
String host = properties.getProperty("ftp.profile." + i + ".host");
String portStr = properties.getProperty("ftp.profile." + i + ".port", "21");
String username = properties.getProperty("ftp.profile." + i + ".username");
String password = properties.getProperty("ftp.profile." + i + ".password");
boolean passive = Boolean.parseBoolean(properties.getProperty("ftp.profile." + i + ".passiveMode", "true"));
if (name != null && host != null) {
cz.kamma.kfmanager.model.FtpProfile p = new cz.kamma.kfmanager.model.FtpProfile(
name, host, Integer.parseInt(portStr),
username != null ? username : "",
password != null ? password : "",
passive);
p.setId(id);
list.add(p);
}
}
return list;
}
public void saveFtpProfiles(java.util.List<cz.kamma.kfmanager.model.FtpProfile> profiles) {
// Remove old entries
int old = Integer.parseInt(properties.getProperty("ftp.profiles.count", "0"));
for (int i = 0; i < old; i++) {
properties.remove("ftp.profile." + i + ".id");
properties.remove("ftp.profile." + i + ".name");
properties.remove("ftp.profile." + i + ".host");
properties.remove("ftp.profile." + i + ".port");
properties.remove("ftp.profile." + i + ".username");
properties.remove("ftp.profile." + i + ".password");
properties.remove("ftp.profile." + i + ".passiveMode");
}
if (profiles == null) {
properties.setProperty("ftp.profiles.count", "0");
return;
}
properties.setProperty("ftp.profiles.count", String.valueOf(profiles.size()));
for (int i = 0; i < profiles.size(); i++) {
cz.kamma.kfmanager.model.FtpProfile p = profiles.get(i);
properties.setProperty("ftp.profile." + i + ".id", p.getId() != null ? p.getId() : "");
properties.setProperty("ftp.profile." + i + ".name", p.getName() != null ? p.getName() : "");
properties.setProperty("ftp.profile." + i + ".host", p.getHost() != null ? p.getHost() : "");
properties.setProperty("ftp.profile." + i + ".port", String.valueOf(p.getPort()));
properties.setProperty("ftp.profile." + i + ".username", p.getUsername() != null ? p.getUsername() : "");
properties.setProperty("ftp.profile." + i + ".password", p.getPassword() != null ? p.getPassword() : "");
properties.setProperty("ftp.profile." + i + ".passiveMode", String.valueOf(p.isPassiveMode()));
}
}
/** Generate a unique ID for a new FTP profile */
public String generateFtpProfileId() {
return java.util.UUID.randomUUID().toString();
}
} }

View File

@ -5,7 +5,8 @@ import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
/** /**
* Model representing a file or directory for display in the table * Model representing a file or directory for display in the table.
* Supports both local files (java.io.File) and remote FTP files.
*/ */
public class FileItem { public class FileItem {
@ -17,6 +18,10 @@ public class FileItem {
private boolean marked; private boolean marked;
private boolean recentlyChanged; private boolean recentlyChanged;
private String displayPath; private String displayPath;
// FTP support
private final boolean isFtp;
private final String ftpPath;
private final FtpProfile ftpProfile;
public FileItem(File file) { public FileItem(File file) {
this(file, null); this(file, null);
@ -29,7 +34,28 @@ public class FileItem {
this.modified = new Date(file.lastModified()); this.modified = new Date(file.lastModified());
this.isDirectory = file.isDirectory(); this.isDirectory = file.isDirectory();
this.marked = false; this.marked = false;
this.recentlyChanged = false;
this.displayPath = displayPath; this.displayPath = displayPath;
this.isFtp = false;
this.ftpPath = null;
this.ftpProfile = null;
}
/**
* Create a FileItem for a remote FTP file/directory.
*/
public FileItem(String name, long size, Date modified, boolean isDirectory, String ftpPath, FtpProfile profile) {
this.file = null;
this.name = name;
this.size = isDirectory ? -1 : size;
this.modified = modified;
this.isDirectory = isDirectory;
this.marked = false;
this.recentlyChanged = false;
this.displayPath = null;
this.isFtp = true;
this.ftpPath = ftpPath;
this.ftpProfile = profile;
} }
public File getFile() { public File getFile() {
@ -64,6 +90,29 @@ public class FileItem {
return isDirectory; return isDirectory;
} }
/**
* Check if this item represents an FTP file/directory.
*/
public boolean isFtp() {
return isFtp;
}
/**
* Get the remote FTP path (e.g. "/remote/dir/file.txt").
* Returns null for local files.
*/
public String getFtpPath() {
return ftpPath;
}
/**
* Get the FTP profile for this item.
* Returns null for local files.
*/
public FtpProfile getFtpProfile() {
return ftpProfile;
}
/** /**
* Check if this item is the same as another item based on metadata. * Check if this item is the same as another item based on metadata.
*/ */
@ -74,11 +123,16 @@ public class FileItem {
return isDirectory == other.isDirectory && return isDirectory == other.isDirectory &&
size == other.size && size == other.size &&
(name != null ? name.equals(other.name) : other.name == null) && (name != null ? name.equals(other.name) : other.name == null) &&
(modified != null ? modified.getTime() == other.modified.getTime() : other.modified == null); (modified != null ? modified.getTime() == other.modified.getTime() : other.modified == null) &&
isFtp == other.isFtp &&
(isFtp ? (ftpPath != null ? ftpPath.equals(other.ftpPath) : other.ftpPath == null) : true);
} }
public String getPath() { public String getPath() {
return displayPath != null ? displayPath : file.getAbsolutePath(); if (isFtp && ftpProfile != null) {
return "ftp://" + ftpProfile.getHost() + ":" + ftpProfile.getPort() + ftpPath;
}
return displayPath != null ? displayPath : (file != null ? file.getAbsolutePath() : "");
} }
public boolean isMarked() { public boolean isMarked() {

View File

@ -0,0 +1,71 @@
package cz.kamma.kfmanager.model;
import java.io.Serializable;
import java.util.Objects;
/**
* FTP connection profile for storing server connection details
*/
public class FtpProfile implements Serializable {
private static final long serialVersionUID = 1L;
private String id;
private String name;
private String host;
private int port;
private String username;
private String password;
private boolean passiveMode;
public FtpProfile() {
this.passiveMode = true;
}
public FtpProfile(String name, String host, int port, String username, String password, boolean passiveMode) {
this.name = name;
this.host = host;
this.port = port;
this.username = username;
this.password = password;
this.passiveMode = passiveMode;
}
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getName() { return name != null ? name : ""; }
public void setName(String name) { this.name = name; }
public String getHost() { return host != null ? host : ""; }
public void setHost(String host) { this.host = host; }
public int getPort() { return port; }
public void setPort(int port) { this.port = port; }
public String getUsername() { return username != null ? username : ""; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password != null ? password : ""; }
public void setPassword(String password) { this.password = password; }
public boolean isPassiveMode() { return passiveMode; }
public void setPassiveMode(boolean passiveMode) { this.passiveMode = passiveMode; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
FtpProfile that = (FtpProfile) o;
return Objects.equals(id, that.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
@Override
public String toString() {
return name != null ? name : host;
}
}

View File

@ -19,6 +19,7 @@ import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream; import java.util.zip.ZipInputStream;
import cz.kamma.kfmanager.model.FileItem; import cz.kamma.kfmanager.model.FileItem;
import cz.kamma.kfmanager.model.FtpProfile;
import net.lingala.zip4j.ZipFile; import net.lingala.zip4j.ZipFile;
/** /**
@ -304,7 +305,11 @@ 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 {
if (!items.isEmpty() && items.get(0).isFtp()) {
deleteFromFtp(items, callback);
return;
}
List<FileItem> cleanedItems = cleanDuplicateItems(items); List<FileItem> cleanedItems = cleanDuplicateItems(items);
long totalItems = calculateTotalItems(cleanedItems); long totalItems = calculateTotalItems(cleanedItems);
long[] currentItem = {0}; long[] currentItem = {0};
@ -401,13 +406,156 @@ public class FileOperations {
/** /**
* Rename a file or directory * Rename a file or directory
*/ */
public static void rename(File file, String newName) throws IOException { public static void rename(File file, String newName) throws IOException {
File target = new File(file.getParentFile(), newName); File target = new File(file.getParentFile(), newName);
if (!file.renameTo(target)) { if (!file.renameTo(target)) {
throw new IOException("Failed to rename file"); throw new IOException("Failed to rename file");
} }
} }
/**
* Rename using FileItem (handles both local and FTP).
*/
public static void rename(FileItem item, String newName) throws IOException {
if (item.isFtp()) {
FtpService.renameOnFtp(item.getFtpProfile(), item.getFtpPath(), newName);
} else {
rename(item.getFile(), newName);
}
}
/**
* Create directory on FTP or local.
*/
public static void createDirectory(FileItem parentItem, String name) throws IOException {
if (parentItem.isFtp()) {
createDirectoryOnFtp(parentItem.getFtpProfile(),
parentItem.getFtpPath(), name);
} else {
createDirectory(parentItem.getFile(), name);
}
}
/**
* Create file on FTP or local.
*/
public static void createFile(FileItem parentItem, String name) throws IOException {
if (parentItem.isFtp()) {
String parentPath = parentItem.getFtpPath();
String fullPath = parentPath.equals("/") ? "/" + name : parentPath + "/" + name;
FtpService.createFileOnFtp(parentItem.getFtpProfile(), fullPath);
} else {
createFile(parentItem.getFile(), name);
}
}
/**
* Delete FTP items.
*/
public static void deleteFromFtp(List<FileItem> items, ProgressCallback callback) throws IOException {
for (FileItem item : items) {
if (callback != null && callback.isCancelled()) break;
FtpService.deleteOnFtp(item.getFtpProfile(), item.getFtpPath());
if (callback != null) {
callback.onFileProgress(item.getSize(), item.getSize());
}
}
}
/**
* Copy to FTP from local or FTP items.
*/
public static void copyToFtp(List<FileItem> items, FtpProfile profile, String remotePath, ProgressCallback callback) throws IOException {
for (FileItem item : items) {
if (callback != null && callback.isCancelled()) break;
if (item.isDirectory()) {
if (item.isFtp()) {
File tempDir = java.nio.file.Files.createTempDirectory("kf-ftp-copy").toFile();
copyFromFtp(java.util.Collections.singletonList(item), tempDir, callback);
uploadLocalToFtp(profile, tempDir, remotePath + "/" + item.getName(), callback);
deleteRecursively(tempDir);
} else {
uploadLocalToFtp(profile, item.getFile(), remotePath + "/" + item.getName(), callback);
}
} else {
if (item.isFtp()) {
File tempFile = java.nio.file.Files.createTempFile("kf-ftp-copy", ".tmp").toFile();
FtpService.downloadFile(item.getFtpProfile(), item.getFtpPath(), tempFile, callback);
FtpService.uploadFile(profile, tempFile, remotePath + "/" + item.getName(), callback);
tempFile.delete();
} else {
FtpService.uploadFile(profile, item.getFile(), remotePath + "/" + item.getName(), callback);
}
}
}
}
private static void uploadLocalToFtp(FtpProfile profile, File localSource, String remotePath, ProgressCallback callback) throws IOException {
if (localSource.isDirectory()) {
FtpService.uploadDirectory(profile, localSource, remotePath, callback);
} else {
FtpService.uploadFile(profile, localSource, remotePath + "/" + localSource.getName(), callback);
}
}
/**
* Copy from FTP to local directory.
*/
public static void copyFromFtp(List<FileItem> ftpItems, File localTarget, ProgressCallback callback) throws IOException {
long totalItems = ftpItems.size();
long[] currentItem = {0};
for (FileItem item : ftpItems) {
if (callback != null && callback.isCancelled()) break;
if (callback != null) {
callback.onProgress(currentItem[0], totalItems, item.getName());
}
if (item.isDirectory()) {
File targetDir = new File(localTarget, item.getName());
FtpService.downloadDirectory(item.getFtpProfile(), item.getFtpPath(), targetDir, callback);
} else {
File targetFile = new File(localTarget, item.getName());
FtpService.downloadFile(item.getFtpProfile(), item.getFtpPath(), targetFile, callback);
}
currentItem[0]++;
if (callback != null) {
callback.onProgress(currentItem[0], totalItems, item.getName());
}
}
}
private static void deleteRecursively(File file) {
if (file.isDirectory()) {
File[] children = file.listFiles();
if (children != null) {
for (File child : children) {
deleteRecursively(child);
}
}
}
file.delete();
}
/**
* Create directory on FTP.
*/
public static void createDirectoryOnFtp(FtpProfile profile, String parentPath, String name) throws IOException {
String fullPath = parentPath.equals("/") ? "/" + name : parentPath + "/" + name;
FtpService.createDirectoryOnFtp(profile, fullPath);
}
/**
* Create file on FTP.
*/
public static void createFileOnFtp(FtpProfile profile, String parentPath, String name) throws IOException {
String fullPath = parentPath.equals("/") ? "/" + name : parentPath + "/" + name;
FtpService.createFileOnFtp(profile, fullPath);
}
/** /**
* Create a new directory * Create a new directory
*/ */

View File

@ -0,0 +1,535 @@
package cz.kamma.kfmanager.service;
import cz.kamma.kfmanager.config.AppConfig;
import cz.kamma.kfmanager.model.FileItem;
import cz.kamma.kfmanager.model.FtpProfile;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPReply;
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* Service for FTP operations using Apache Commons Net
*/
public class FtpService {
private static final String FTP_PREFIX = "ftp://";
/** Log all FTP commands to stderr for debugging */
public static void log(String msg) {
System.err.println("[FTP] " + msg);
}
/**
* Connect and login to FTP server. Returns configured FTPClient.
* Caller is responsible for closing the client.
*/
public static FTPClient connect(FtpProfile profile) throws IOException {
FTPClient client = new FTPClient();
client.setRemoteVerificationEnabled(false);
client.setControlEncoding("UTF-8");
log("CMD: connect " + profile.getHost() + ":" + profile.getPort());
client.connect(profile.getHost(), profile.getPort());
int reply = client.getReplyCode();
log("RESP: " + reply + " " + client.getReplyString());
if (!FTPReply.isPositiveCompletion(reply)) {
client.disconnect();
throw new IOException("FTP connection rejected: " + client.getReplyString());
}
String user = (profile.getUsername() == null || profile.getUsername().isEmpty()) ? "anonymous" : profile.getUsername();
String pass = (profile.getPassword() == null) ? "" : profile.getPassword();
log("CMD: login " + user + "@" + profile.getHost());
boolean loggedIn = client.login(user, pass);
String loginReply = client.getReplyString();
log("RESP: " + (loggedIn ? "230 Login successful" : "550 Login failed") + " " + loginReply);
if (!loggedIn) {
client.disconnect();
throw new IOException("FTP login failed for user: " + user + ". Reply: " + loginReply);
}
// Set parameters AFTER login
client.setFileType(FTP.BINARY_FILE_TYPE);
if (profile.isPassiveMode()) {
log("CMD: enterLocalPassiveMode");
client.enterLocalPassiveMode();
}
return client;
}
/**
* List directory contents at the given remote path.
*/
public static List<FileItem> listDirectory(FtpProfile profile, String remotePath) throws IOException {
FTPClient client = null;
try {
client = connect(profile);
return listDirectory(client, profile, remotePath);
} finally {
if (client != null) {
try { client.logout(); client.disconnect(); } catch (IOException ignore) {}
}
}
}
/**
* List directory contents using an already-connected client.
*/
public static List<FileItem> listDirectory(FTPClient client, FtpProfile profile, String remotePath) throws IOException {
log("CMD: cwd " + remotePath);
boolean changed = client.changeWorkingDirectory(remotePath);
log("RESP: " + (changed ? "250 CWD successful" : "550 CWD failed") + " " + client.getReplyString());
if (!changed) {
throw new IOException("Cannot access directory: " + remotePath);
}
FTPFile[] entries = client.listFiles();
log("RESP: " + entries.length + " entries listed");
return parseFtpEntries(client, remotePath, profile, entries);
}
/**
* Parse FTPFile[] into FileItem list.
*/
public static List<FileItem> parseFtpEntries(FTPClient client, String remotePath, FtpProfile profile, FTPFile[] entries) {
List<FileItem> items = new ArrayList<>();
// Add ".." parent entry
String parentPath = getParentPath(remotePath);
if (!parentPath.equals(remotePath)) {
FileItem parent = new FileItem("..", -1, new Date(0), true, parentPath, profile);
items.add(parent);
}
for (FTPFile entry : entries) {
if (entry.getName().equals(".") || entry.getName().equals("..")) continue;
String entryPath = remotePath.endsWith("/") ? remotePath + entry.getName() : remotePath + "/" + entry.getName();
FileItem item = new FileItem(
entry.getName(),
entry.getSize(),
entry.getTimestamp() != null ? new Date(entry.getTimestamp().getTimeInMillis()) : new Date(0),
entry.isDirectory(),
entryPath,
profile
);
items.add(item);
}
// Sort: directories first, then by name case-insensitive
items.sort(Comparator.comparing((FileItem f) -> !f.isDirectory())
.thenComparing(FileItem::getName, String.CASE_INSENSITIVE_ORDER));
return items;
}
/**
* Download a single file from FTP to local filesystem.
*/
public static void downloadFile(FtpProfile profile, String remotePath, File localTarget, FileOperations.ProgressCallback callback) throws IOException {
FTPClient client = null;
try {
client = connect(profile);
client.setFileType(FTP.BINARY_FILE_TYPE);
log("CMD: retr " + remotePath);
long fileSize = 0;
try {
// Try to get size before downloading
FTPFile[] files = client.listFiles(remotePath);
if (files != null && files.length > 0) {
fileSize = files[0].getSize();
}
} catch (Exception ignore) {}
FileOutputStream fos = new FileOutputStream(localTarget);
boolean success = client.retrieveFile(remotePath, fos);
fos.close();
log("RESP: " + (success ? "226 Transfer complete" : "550 Transfer failed"));
log("RESP: " + (success ? "226 Transfer complete" : "550 Transfer failed"));
if (!success) {
localTarget.delete();
throw new IOException("Failed to download: " + remotePath);
}
if (callback != null) {
callback.onFileProgress(fileSize, fileSize);
}
} finally {
if (client != null) {
try { client.logout(); client.disconnect(); } catch (IOException ignore) {}
}
}
}
/**
* Upload a single local file to FTP.
*/
public static void uploadFile(FtpProfile profile, File localSource, String remotePath, FileOperations.ProgressCallback callback) throws IOException {
FTPClient client = null;
try {
client = connect(profile);
// Ensure remote parent directory exists
String parentDir = getParentPath(remotePath);
if (parentDir != null && !parentDir.equals("/") && !parentDir.isEmpty()) {
ensureDirectoryExists(client, profile, parentDir);
}
client.setFileType(FTP.BINARY_FILE_TYPE);
log("CMD: stor " + remotePath);
long fileSize = localSource.length();
FileInputStream fis = new FileInputStream(localSource);
boolean success = client.storeFile(remotePath, fis);
fis.close();
log("RESP: " + (success ? "226 Transfer complete" : "550 Transfer failed"));
if (!success) {
throw new IOException("Failed to upload: " + remotePath);
}
if (callback != null) {
callback.onFileProgress(fileSize, fileSize);
}
} finally {
if (client != null) {
try { client.logout(); client.disconnect(); } catch (IOException ignore) {}
}
}
}
/**
* Upload an entire local directory to FTP.
*/
public static void uploadDirectory(FtpProfile profile, File localDir, String remoteParentPath, FileOperations.ProgressCallback callback) throws IOException {
FTPClient client = null;
try {
client = connect(profile);
uploadDirectoryInternal(client, profile, localDir, localDir.getName(), remoteParentPath, callback);
} finally {
if (client != null) {
try { client.logout(); client.disconnect(); } catch (IOException ignore) {}
}
}
}
private static void uploadDirectoryInternal(FTPClient client, FtpProfile profile, File localDir, String dirName, String remoteParentPath, FileOperations.ProgressCallback callback) throws IOException {
String remoteDirPath = remoteParentPath.equals("/") ? "/" + dirName : remoteParentPath + "/" + dirName;
log("CMD: mkdir " + remoteDirPath);
client.makeDirectory(remoteDirPath);
log("RESP: " + client.getReplyString());
File[] files = localDir.listFiles();
if (files == null) return;
long totalSize = 0;
for (File f : files) {
if (f.isDirectory()) {
uploadDirectoryInternal(client, profile, f, f.getName(), remoteDirPath, callback);
} else {
totalSize += f.length();
}
}
long current = 0;
for (File f : files) {
if (f.isFile()) {
client.setFileType(FTP.BINARY_FILE_TYPE);
log("CMD: stor " + f.getName());
FileInputStream fis = new FileInputStream(f);
boolean success = client.storeFile(f.getName(), fis);
fis.close();
log("RESP: " + (success ? "226 Transfer complete" : "550 Transfer failed"));
if (!success) {
throw new IOException("Failed to upload: " + f.getName());
}
current += f.length();
if (callback != null) {
callback.onFileProgress(current, totalSize);
}
}
}
}
/**
* Download a single directory from FTP to local.
*/
public static void downloadDirectory(FtpProfile profile, String remoteDirPath, File localTarget, FileOperations.ProgressCallback callback) throws IOException {
FTPClient client = null;
try {
client = connect(profile);
downloadDirectoryInternal(client, profile, remoteDirPath, localTarget, callback);
} finally {
if (client != null) {
try { client.logout(); client.disconnect(); } catch (IOException ignore) {}
}
}
}
private static void downloadDirectoryInternal(FTPClient client, FtpProfile profile, String remoteDirPath, File localTarget, FileOperations.ProgressCallback callback) throws IOException {
localTarget.mkdirs();
log("CMD: cwd " + remoteDirPath);
client.changeWorkingDirectory(remoteDirPath);
log("RESP: " + client.getReplyString());
FTPFile[] entries = client.listFiles();
if (entries == null) return;
for (FTPFile entry : entries) {
if (entry.getName().equals(".") || entry.getName().equals("..")) continue;
String entryPath = remoteDirPath.equals("/") ? "/" + entry.getName() : remoteDirPath + "/" + entry.getName();
File localEntry = new File(localTarget, entry.getName());
if (entry.isDirectory()) {
downloadDirectoryInternal(client, profile, entryPath, localEntry, callback);
} else {
client.setFileType(FTP.BINARY_FILE_TYPE);
log("CMD: retr " + entry.getName());
FileOutputStream fos = new FileOutputStream(localEntry);
boolean success = client.retrieveFile(entry.getName(), fos);
fos.close();
log("RESP: " + (success ? "226 Transfer complete" : "550 Transfer failed"));
if (!success) {
throw new IOException("Failed to download: " + entry.getName());
}
if (callback != null) {
callback.onFileProgress(entry.getSize(), entry.getSize());
}
}
}
}
/**
* Delete a file or directory on FTP.
*/
public static void deleteOnFtp(FtpProfile profile, String remotePath) throws IOException {
FTPClient client = null;
try {
client = connect(profile);
removeRecursively(client, remotePath);
} finally {
if (client != null) {
try { client.logout(); client.disconnect(); } catch (IOException ignore) {}
}
}
}
private static void removeRecursively(FTPClient client, String remotePath) throws IOException {
log("CMD: cwd " + remotePath);
boolean inDir = client.changeWorkingDirectory(remotePath);
log("RESP: " + client.getReplyString());
if (inDir) {
// It's a directory, delete contents first
FTPFile[] entries = client.listFiles();
if (entries != null) {
for (FTPFile entry : entries) {
if (entry.getName().equals(".") || entry.getName().equals("..")) continue;
String entryPath = remotePath.equals("/") ? "/" + entry.getName() : remotePath + "/" + entry.getName();
if (entry.isDirectory()) {
removeRecursively(client, entryPath);
} else {
log("CMD: rm " + entry.getName());
client.deleteFile(entry.getName());
}
}
}
// Go back to parent and remove directory
String parent = getParentPath(remotePath);
if (!parent.equals(remotePath)) {
client.changeWorkingDirectory(parent);
}
}
log("CMD: rmd " + remotePath);
boolean removed = client.removeDirectory(remotePath);
log("RESP: " + (removed ? "250 Directory removed" : "550 Removal failed"));
if (!removed) {
// Try as file
log("CMD: rm " + remotePath);
boolean fileRemoved = client.deleteFile(remotePath);
log("RESP: " + (fileRemoved ? "250 File removed" : "550 File removal failed"));
if (!fileRemoved) {
throw new IOException("Failed to delete: " + remotePath);
}
}
}
/**
* Create directory on FTP.
*/
public static void createDirectoryOnFtp(FtpProfile profile, String remotePath) throws IOException {
FTPClient client = null;
try {
client = connect(profile);
ensureDirectoryExists(client, profile, remotePath);
} finally {
if (client != null) {
try { client.logout(); client.disconnect(); } catch (IOException ignore) {}
}
}
}
/**
* Ensure a directory exists on FTP, creating parent dirs as needed.
*/
public static void ensureDirectoryExists(FTPClient client, FtpProfile profile, String dirPath) throws IOException {
if (dirPath == null || dirPath.isEmpty() || dirPath.equals("/")) return;
// Create parent first
String parent = getParentPath(dirPath);
if (!parent.equals(dirPath) && !parent.isEmpty()) {
ensureDirectoryExists(client, profile, parent);
}
log("CMD: mkdir " + dirPath);
boolean created = client.makeDirectory(dirPath);
log("RESP: " + (created ? "257 Created" : "550 Already exists or error"));
}
/**
* Rename on FTP.
*/
public static void renameOnFtp(FtpProfile profile, String oldPath, String newName) throws IOException {
FTPClient client = null;
try {
client = connect(profile);
// Ensure parent directory exists
String parentDir = new File(oldPath).getParent();
if (parentDir != null && !parentDir.equals("/") && !parentDir.isEmpty()) {
ensureDirectoryExists(client, profile, parentDir);
}
// Rename using full old path and new full path
String newFullPath = parentDir.equals("/") ? "/" + newName : parentDir + "/" + newName;
log("CMD: renm " + oldPath + " -> " + newFullPath);
boolean success = client.rename(oldPath, newFullPath);
log("RESP: " + (success ? "250 Rename successful" : "550 Rename failed") + " " + client.getReplyString());
if (!success) {
throw new IOException("Failed to rename: " + oldPath + " to " + newName);
}
} finally {
if (client != null) {
try { client.logout(); client.disconnect(); } catch (IOException ignore) {}
}
}
}
/**
* Create an empty file on FTP.
*/
public static void createFileOnFtp(FtpProfile profile, String remotePath) throws IOException {
FTPClient client = null;
try {
client = connect(profile);
String parentDir = new File(remotePath).getParent();
if (parentDir != null && !parentDir.equals("/") && !parentDir.isEmpty()) {
ensureDirectoryExists(client, profile, parentDir);
}
String fileName = new File(remotePath).getName();
log("CMD: stor " + fileName + " (empty)");
boolean success = client.storeFile(fileName, new ByteArrayInputStream(new byte[0]));
log("RESP: " + (success ? "226 Transfer complete" : "550 Transfer failed"));
if (!success) {
throw new IOException("Failed to create file: " + fileName);
}
} finally {
if (client != null) {
try { client.logout(); client.disconnect(); } catch (IOException ignore) {}
}
}
}
/**
* Get the parent path of a remote FTP path.
*/
public static String getParentPath(String path) {
if (path == null || path.equals("/")) return "/";
String normalized = path.endsWith("/") ? path.substring(0, path.length() - 1) : path;
int lastSlash = normalized.lastIndexOf('/');
if (lastSlash <= 0) return "/";
return normalized.substring(0, lastSlash);
}
/**
* Get the current working directory from an FTP client.
*/
public static String getCurrentDirectory(FTPClient client) throws IOException {
log("CMD: pwd");
String pwd = client.printWorkingDirectory();
log("RESP: " + pwd);
return pwd;
}
/**
* Parse an FTP URL (ftp://host:port/path) and find matching profile from AppConfig.
*/
public static FtpProfile findProfileForUrl(String ftpUrl, AppConfig config) {
if (ftpUrl == null || !ftpUrl.startsWith(FTP_PREFIX)) return null;
String withoutProtocol = ftpUrl.substring(FTP_PREFIX.length());
int pathSep = withoutProtocol.indexOf('/');
String hostPart = pathSep >= 0 ? withoutProtocol.substring(0, pathSep) : withoutProtocol;
int portSep = hostPart.indexOf(':');
String host = portSep >= 0 ? hostPart.substring(0, portSep) : hostPart;
int port = portSep >= 0 ? Integer.parseInt(hostPart.substring(portSep + 1)) : 21;
if (config != null) {
List<FtpProfile> profiles = config.getFtpProfiles();
for (FtpProfile p : profiles) {
if (p.getHost().equalsIgnoreCase(host) && p.getPort() == port) {
return p;
}
}
}
// No matching profile found - create a minimal one
FtpProfile fallback = new FtpProfile();
fallback.setHost(host);
fallback.setPort(port);
return fallback;
}
/**
* Build an FTP URL from profile and remote path.
*/
public static String buildFtpUrl(FtpProfile profile, String remotePath) {
if (remotePath == null || remotePath.isEmpty()) remotePath = "/";
return FTP_PREFIX + profile.getHost() + ":" + profile.getPort() + remotePath;
}
private static String getNameFromPath(String path) {
if (path == null || path.equals("/") || path.isEmpty()) return "";
int lastSlash = path.lastIndexOf('/');
return path.substring(lastSlash + 1);
}
}

View File

@ -2,6 +2,8 @@ package cz.kamma.kfmanager.ui;
import cz.kamma.kfmanager.MainApp; import cz.kamma.kfmanager.MainApp;
import cz.kamma.kfmanager.model.FileItem; import cz.kamma.kfmanager.model.FileItem;
import cz.kamma.kfmanager.model.FtpProfile;
import cz.kamma.kfmanager.service.FtpService;
import javax.swing.*; import javax.swing.*;
import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuEvent;
@ -422,6 +424,68 @@ public class FilePanel extends JPanel {
}); });
} }
/**
* Add a new FTP tab.
*/
public void addFtpTab(String ftpPath, FtpProfile profile) {
FilePanelTab tab = new FilePanelTab(ftpPath, true);
if (appConfig != null) tab.setAppConfig(appConfig);
tab.setActive(this.active);
tab.setOnDirectoryChanged(() -> {
updateTabTitle(tab);
if (onDirectoryChangedAll != null) onDirectoryChangedAll.run();
});
tab.setOnSwitchPanelRequested(switchPanelCallback);
// Extract display name from FTP URL for tab title
String tabTitle = getFtpTabTitle(ftpPath);
tabbedPane.addTab(tabTitle, tab);
tabbedPane.setSelectedComponent(tab);
addMouseListenerToComponents(tab);
updateTabStyles();
updateDriveComboVisibility();
if (onTableCreated != null) {
onTableCreated.accept(tab.getFileTable());
}
SwingUtilities.invokeLater(() -> {
if (tab.getFileTable() != null) {
tab.getFileTable().requestFocusInWindow();
}
tab.ensureRenderers();
});
}
private String getFtpTabTitle(String ftpPath) {
if (ftpPath == null) return "FTP";
try {
String withoutProtocol = ftpPath.startsWith("ftp://") ? ftpPath.substring(6) : ftpPath;
int pathSep = withoutProtocol.indexOf('/');
String hostPart = pathSep >= 0 ? withoutProtocol.substring(0, pathSep) : withoutProtocol;
int portSep = hostPart.indexOf(':');
String host = portSep >= 0 ? hostPart.substring(0, portSep) : hostPart;
return "FTP: " + host;
} catch (Exception e) {
return "FTP";
}
}
private void updateDriveComboVisibility() {
FilePanelTab current = getCurrentTab();
if (current != null && current.isFtpTab()) {
driveCombo.setVisible(false);
driveInfoLabel.setVisible(false);
} else {
driveCombo.setVisible(true);
driveInfoLabel.setVisible(true);
}
revalidate();
repaint();
}
/** /**
* Provide AppConfig so tabs can persist/retrieve sort settings * Provide AppConfig so tabs can persist/retrieve sort settings
*/ */
@ -441,8 +505,13 @@ public class FilePanel extends JPanel {
for (int i = 0; i < tabbedPane.getTabCount(); i++) { for (int i = 0; i < tabbedPane.getTabCount(); i++) {
Component c = tabbedPane.getComponentAt(i); Component c = tabbedPane.getComponentAt(i);
if (c instanceof FilePanelTab t) { if (c instanceof FilePanelTab t) {
File dir = t.getCurrentDirectory(); if (t.isFtpTab() && t.getFtpProfile() != null) {
paths.add(dir != null ? dir.getAbsolutePath() : System.getProperty("user.home")); String ftpPath = FtpService.buildFtpUrl(t.getFtpProfile(), t.getFtpCurrentPath());
paths.add(ftpPath);
} else {
File dir = t.getCurrentDirectory();
paths.add(dir != null ? dir.getAbsolutePath() : System.getProperty("user.home"));
}
} }
} }
return paths; return paths;
@ -489,20 +558,35 @@ public class FilePanel extends JPanel {
for (int i = 0; i < paths.size(); i++) { for (int i = 0; i < paths.size(); i++) {
String p = paths.get(i); String p = paths.get(i);
ViewMode mode = ViewMode.FULL; if (p != null && p.startsWith("ftp://")) {
if (viewModes != null && i < viewModes.size()) { // FTP tab: look up matching profile
try { FtpProfile profile = null;
mode = ViewMode.valueOf(viewModes.get(i)); if (appConfig != null) {
} catch (IllegalArgumentException ex) { profile = FtpService.findProfileForUrl(p, appConfig);
mode = ViewMode.FULL;
} }
if (profile != null) {
addFtpTab(p, profile);
} else {
// Profile not found, skip this tab
continue;
}
} else {
ViewMode mode = ViewMode.FULL;
if (viewModes != null && i < viewModes.size()) {
try {
mode = ViewMode.valueOf(viewModes.get(i));
} catch (IllegalArgumentException ex) {
mode = ViewMode.FULL;
}
}
addNewTabWithMode(p, mode, false);
} }
addNewTabWithMode(p, mode, false);
// Restore focus to the specific item if provided // Restore focus to the specific item if provided
if (focusedItems != null && i < focusedItems.size() && focusedItems.get(i) != null) { int tabIndex = tabbedPane.getTabCount() - 1;
if (tabIndex >= 0 && focusedItems != null && i < focusedItems.size() && focusedItems.get(i) != null) {
final String focusName = focusedItems.get(i); final String focusName = focusedItems.get(i);
final FilePanelTab currentTab = (FilePanelTab) tabbedPane.getComponentAt(i); final FilePanelTab currentTab = (FilePanelTab) tabbedPane.getComponentAt(tabIndex);
SwingUtilities.invokeLater(() -> currentTab.selectItem(focusName, requestFocus)); SwingUtilities.invokeLater(() -> currentTab.selectItem(focusName, requestFocus));
} }
} }
@ -515,6 +599,7 @@ public class FilePanel extends JPanel {
updatePathField(); updatePathField();
updateTabStyles(); updateTabStyles();
updateDriveComboVisibility();
} }
private void populateDrives() { private void populateDrives() {
@ -695,6 +780,25 @@ public class FilePanel extends JPanel {
return new Color((rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF); return new Color((rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF);
} }
/**
* Remove all FTP tabs from the panel.
*/
public void closeAllFtpTabs() {
for (int i = tabbedPane.getTabCount() - 1; i >= 0; i--) {
Component comp = tabbedPane.getComponentAt(i);
if (comp instanceof FilePanelTab tab && tab.isFtpTab()) {
if (tabbedPane.getTabCount() > 1) {
tabbedPane.removeTabAt(i);
} else {
// If it's the last tab, reset it to home instead of removing
tab.loadDirectory(new File(System.getProperty("user.home")));
}
}
}
updatePathField();
updateTabStyles();
}
/** /**
* Remove the current tab * Remove the current tab
*/ */

View File

@ -2,9 +2,11 @@ package cz.kamma.kfmanager.ui;
import cz.kamma.kfmanager.MainApp; import cz.kamma.kfmanager.MainApp;
import cz.kamma.kfmanager.model.FileItem; import cz.kamma.kfmanager.model.FileItem;
import cz.kamma.kfmanager.model.FtpProfile;
import cz.kamma.kfmanager.config.AppConfig; import cz.kamma.kfmanager.config.AppConfig;
import cz.kamma.kfmanager.service.ClipboardService; import cz.kamma.kfmanager.service.ClipboardService;
import cz.kamma.kfmanager.service.FileOperations; import cz.kamma.kfmanager.service.FileOperations;
import cz.kamma.kfmanager.service.FtpService;
import javax.swing.*; import javax.swing.*;
import javax.swing.filechooser.FileSystemView; import javax.swing.filechooser.FileSystemView;
@ -93,15 +95,56 @@ public class FilePanelTab extends JPanel {
private JTextField filterTextField; private JTextField filterTextField;
private JPanel filterPanel; private JPanel filterPanel;
private boolean searchModeActive = false; private boolean searchModeActive = false;
// FTP support
private boolean isFtpTab = false;
private FtpProfile ftpProfile;
private String ftpCurrentPath;
public FilePanelTab(String initialPath) { public FilePanelTab(String initialPath) {
this(initialPath, true); this(initialPath, true);
} }
public FilePanelTab(String initialPath, boolean requestFocus) { public FilePanelTab(String initialPath, boolean requestFocus) {
this.currentDirectory = new File(initialPath); if (initialPath != null && initialPath.startsWith("ftp://")) {
initComponents(); this.isFtpTab = true;
loadDirectory(currentDirectory, true, requestFocus); this.ftpProfile = FtpService.findProfileForUrl(initialPath, null);
this.ftpCurrentPath = extractFtpPath(initialPath);
this.currentDirectory = new File(initialPath);
} else {
this.isFtpTab = false;
this.ftpProfile = null;
this.currentDirectory = new File(initialPath != null ? initialPath : System.getProperty("user.home"));
}
if (initialPath != null && initialPath.startsWith("ftp://")) {
initComponents();
loadFtpDirectory(ftpCurrentPath, true, requestFocus);
} else {
initComponents();
loadDirectory(currentDirectory, true, requestFocus);
}
}
private static String extractFtpPath(String url) {
if (url == null) return "/";
String withoutProtocol = url.startsWith("ftp://") ? url.substring(6) : url;
int slashIdx = withoutProtocol.indexOf('/');
return slashIdx >= 0 ? withoutProtocol.substring(slashIdx) : "/";
}
public boolean isFtpTab() {
return isFtpTab;
}
public FtpProfile getFtpProfile() {
return ftpProfile;
}
public String getFtpCurrentPath() {
return ftpCurrentPath;
}
public AppConfig getAppConfigRef() {
return persistedConfig;
} }
/** Start inline rename for currently selected item (if single selection). */ /** Start inline rename for currently selected item (if single selection). */
@ -1508,10 +1551,92 @@ public class FilePanelTab extends JPanel {
} }
} }
// --- FTP directory loading ---
/**
* Load an FTP directory into the table.
*/
public void loadFtpDirectory(final String remotePath, final boolean autoSelectFirst, final boolean requestFocus) {
SwingUtilities.invokeLater(() -> {
try {
if (ftpProfile == null) {
JOptionPane.showMessageDialog(FilePanelTab.this,
"No FTP profile configured for this tab.", "FTP Error", JOptionPane.ERROR_MESSAGE);
return;
}
List<FileItem> items = FtpService.listDirectory(ftpProfile, remotePath);
this.ftpCurrentPath = remotePath;
String ftpUrl = FtpService.buildFtpUrl(ftpProfile, remotePath);
this.currentDirectory = new File(ftpUrl);
// Save copy of all items for filtering
allItems.clear();
allItems.addAll(items);
// Apply filter if search mode is active
List<FileItem> displayItems = (searchModeActive && filterTextField != null)
? applyFilter(items, filterTextField.getText())
: items;
tableModel.setItems(displayItems);
if (sortColumn >= 0) {
sortItemsByColumn(sortColumn, sortAscending);
tableModel.fireTableDataChanged();
}
SwingUtilities.invokeLater(() -> {
if (viewMode == ViewMode.BRIEF) {
tableModel.calculateBriefLayout();
tableModel.fireTableStructureChanged();
} else {
tableModel.fireTableDataChanged();
}
updateColumnRenderers();
updateColumnWidths();
fileTable.revalidate();
fileTable.repaint();
if (autoSelectFirst && fileTable.getRowCount() > 0) {
fileTable.setRowSelectionInterval(0, 0);
fileTable.scrollRectToVisible(fileTable.getCellRect(0, 0, true));
}
if (requestFocus) {
try { fileTable.requestFocusInWindow(); } catch (Exception ignore) {}
}
updateStatus();
});
if (onDirectoryChanged != null) {
onDirectoryChanged.run();
}
} catch (IOException e) {
JOptionPane.showMessageDialog(FilePanelTab.this,
"Failed to list directory: " + e.getMessage(), "FTP Error", JOptionPane.ERROR_MESSAGE);
}
});
}
/**
* Navigate up one directory in FTP.
*/
public void navigateFtpUp() {
if (ftpCurrentPath == null || ftpCurrentPath.equals("/")) return;
String parentPath = FtpService.getParentPath(ftpCurrentPath);
loadFtpDirectory(parentPath, true, true);
}
/** /**
* Refresh the current directory while attempting to preserve selection and focus. * Refresh the current directory while attempting to preserve selection and focus.
*/ */
public void refresh(boolean requestFocus) { public void refresh(boolean requestFocus) {
if (isFtpTab) {
loadFtpDirectory(ftpCurrentPath, false, requestFocus);
return;
}
List<FileItem> newItems = createFileItemList(currentDirectory); List<FileItem> newItems = createFileItemList(currentDirectory);
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
@ -1799,6 +1924,16 @@ public class FilePanelTab extends JPanel {
if (item == null) return; if (item == null) return;
if (item.isFtp()) {
if (item.getName().equals("..")) {
navigateFtpUp();
} else if (item.isDirectory()) {
String entryPath = item.getFtpPath();
loadFtpDirectory(entryPath, true, true);
}
return;
}
if (item.getName().equals("..")) { if (item.getName().equals("..")) {
navigateUp(); navigateUp();
} else if (FileOperations.isArchiveFile(item.getFile())) { } else if (FileOperations.isArchiveFile(item.getFile())) {
@ -2321,7 +2456,19 @@ public class FilePanelTab extends JPanel {
item = tableModel.getItem(row); item = tableModel.getItem(row);
} }
if (item == null) return; if (item == null) return;
if (item.isFtp()) {
// FTP: handle navigation only (no archive support for FTP)
if (item.getName().equals("..")) {
navigateFtpUp();
} else if (item.isDirectory()) {
String entryPath = item.getFtpPath() + "/" + item.getName();
loadFtpDirectory(entryPath, true, true);
}
// FTP files: no action on double-click
return;
}
if (item.getName().equals("..")) { if (item.getName().equals("..")) {
navigateUp(); navigateUp();
@ -2737,7 +2884,13 @@ public class FilePanelTab extends JPanel {
}); });
} }
public void navigateUp() { public void navigateUp() {
// FTP: navigate up in remote path
if (isFtpTab && ftpProfile != null) {
navigateFtpUp();
return;
}
// If we're currently browsing an extracted archive root, navigate back to the // If we're currently browsing an extracted archive root, navigate back to the
// original archive's parent and select the archive file. // original archive's parent and select the archive file.
try { try {
@ -4338,20 +4491,32 @@ public class FilePanelTab extends JPanel {
item = getItem(rowIndex); item = getItem(rowIndex);
} }
if (item == null) return; if (item == null) return;
// Perform rename using FileOperations and refresh the directory // Perform rename
try { try {
cz.kamma.kfmanager.service.FileOperations.rename(item.getFile(), newName); if (item.isFtp()) {
// reload current directory to reflect updated names FtpService.renameOnFtp(item.getFtpProfile(), item.getFtpPath(), newName);
FilePanelTab.this.loadDirectory(FilePanelTab.this.getCurrentDirectory()); String newFtpPath = FtpService.getParentPath(item.getFtpPath());
// After reload, select the renamed item and focus the table loadFtpDirectory(newFtpPath, false, false);
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
try { try {
FilePanelTab.this.selectItem(newName); FilePanelTab.this.selectItem(newName);
FilePanelTab.this.getFileTable().requestFocusInWindow(); FilePanelTab.this.getFileTable().requestFocusInWindow();
} catch (Exception ignore) {} } catch (Exception ignore) {}
}); });
} else {
cz.kamma.kfmanager.service.FileOperations.rename(item.getFile(), newName);
// reload current directory to reflect updated names
FilePanelTab.this.loadDirectory(FilePanelTab.this.getCurrentDirectory());
// After reload, select the renamed item and focus the table
SwingUtilities.invokeLater(() -> {
try {
FilePanelTab.this.selectItem(newName);
FilePanelTab.this.getFileTable().requestFocusInWindow();
} catch (Exception ignore) {}
});
}
} catch (Exception ex) { } catch (Exception ex) {
// show error to user // show error to user
try { try {

View File

@ -0,0 +1,155 @@
package cz.kamma.kfmanager.ui;
import cz.kamma.kfmanager.config.AppConfig;
import cz.kamma.kfmanager.model.FtpProfile;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
/**
* Dialog for creating or editing a single FTP profile
*/
public class FtpProfileEditDialog extends JDialog {
private JTextField nameField;
private JTextField hostField;
private JSpinner portSpinner;
private JTextField usernameField;
private JPasswordField passwordField;
private JCheckBox passiveModeCheck;
private FtpProfile profile;
private boolean saved = false;
public FtpProfileEditDialog(Window parent, AppConfig config, FtpProfile existingProfile) {
super(parent, existingProfile == null ? "New FTP Profile" : "Edit FTP Profile", ModalityType.APPLICATION_MODAL);
this.profile = existingProfile != null ? existingProfile : new FtpProfile();
if (this.profile.getId() == null && existingProfile == null) {
this.profile.setId(config.generateFtpProfileId());
}
initComponents();
loadProfile();
pack();
setLocationRelativeTo(parent);
}
private void initComponents() {
setLayout(new BorderLayout(10, 10));
JPanel formPanel = new JPanel(new GridBagLayout());
formPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(4, 4, 4, 4);
gbc.anchor = GridBagConstraints.WEST;
// Name
gbc.gridx = 0; gbc.gridy = 0; gbc.weightx = 0;
formPanel.add(new JLabel("Name:"), gbc);
gbc.gridx = 1; gbc.weightx = 1; gbc.fill = GridBagConstraints.HORIZONTAL;
nameField = new JTextField(20);
formPanel.add(nameField, gbc);
// Host
gbc.gridx = 0; gbc.gridy = 1; gbc.weightx = 0;
formPanel.add(new JLabel("Host:"), gbc);
gbc.gridx = 1; gbc.weightx = 1; gbc.fill = GridBagConstraints.HORIZONTAL;
hostField = new JTextField(20);
formPanel.add(hostField, gbc);
// Port
gbc.gridx = 0; gbc.gridy = 2; gbc.weightx = 0;
formPanel.add(new JLabel("Port:"), gbc);
gbc.gridx = 1; gbc.weightx = 0; gbc.fill = GridBagConstraints.NONE;
SpinnerModel portModel = new javax.swing.SpinnerNumberModel(21, 1, 65535, 1);
portSpinner = new JSpinner(portModel);
portSpinner.setPreferredSize(new Dimension(80, portSpinner.getPreferredSize().height));
formPanel.add(portSpinner, gbc);
// Username
gbc.gridx = 0; gbc.gridy = 3; gbc.weightx = 0;
formPanel.add(new JLabel("Username:"), gbc);
gbc.gridx = 1; gbc.weightx = 1; gbc.fill = GridBagConstraints.HORIZONTAL;
usernameField = new JTextField(20);
formPanel.add(usernameField, gbc);
// Password
gbc.gridx = 0; gbc.gridy = 4; gbc.weightx = 0;
formPanel.add(new JLabel("Password:"), gbc);
gbc.gridx = 1; gbc.weightx = 1; gbc.fill = GridBagConstraints.HORIZONTAL;
passwordField = new JPasswordField(20);
formPanel.add(passwordField, gbc);
// Passive mode
gbc.gridx = 0; gbc.gridy = 5; gbc.gridwidth = 2; gbc.anchor = GridBagConstraints.WEST; gbc.weightx = 0;
passiveModeCheck = new JCheckBox("Passive mode");
passiveModeCheck.setSelected(true);
formPanel.add(passiveModeCheck, gbc);
add(formPanel, BorderLayout.CENTER);
// Buttons
JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
JButton okButton = new JButton("OK");
okButton.addActionListener(e -> saveProfile());
JButton cancelButton = new JButton("Cancel");
cancelButton.addActionListener(e -> dispose());
btnPanel.add(okButton);
btnPanel.add(cancelButton);
add(btnPanel, BorderLayout.SOUTH);
// Enter key to save, Escape key to cancel
getRootPane().setDefaultButton(okButton);
getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "cancel");
getRootPane().getActionMap().put("cancel", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) { dispose(); }
});
}
private void loadProfile() {
nameField.setText(profile.getName());
hostField.setText(profile.getHost());
portSpinner.setValue(profile.getPort());
usernameField.setText(profile.getUsername());
passwordField.setText(profile.getPassword());
passiveModeCheck.setSelected(profile.isPassiveMode());
}
private void saveProfile() {
String name = nameField.getText().trim();
String host = hostField.getText().trim();
if (name.isEmpty()) {
JOptionPane.showMessageDialog(this, "Name is required.", "Validation Error", JOptionPane.WARNING_MESSAGE);
nameField.requestFocusInWindow();
return;
}
if (host.isEmpty()) {
JOptionPane.showMessageDialog(this, "Host is required.", "Validation Error", JOptionPane.WARNING_MESSAGE);
hostField.requestFocusInWindow();
return;
}
profile.setName(name);
profile.setHost(host);
profile.setPort((Integer) portSpinner.getValue());
profile.setUsername(usernameField.getText().trim());
profile.setPassword(new String(passwordField.getPassword()));
profile.setPassiveMode(passiveModeCheck.isSelected());
saved = true;
dispose();
}
public boolean isSaved() {
return saved;
}
public FtpProfile getProfile() {
return profile;
}
}

View File

@ -0,0 +1,151 @@
package cz.kamma.kfmanager.ui;
import cz.kamma.kfmanager.config.AppConfig;
import cz.kamma.kfmanager.model.FtpProfile;
import javax.swing.*;
import javax.swing.table.DefaultTableModel;
import java.awt.*;
import java.util.List;
import java.util.function.Consumer;
/**
* Dialog for managing FTP profiles (CRUD operations)
*/
public class FtpProfileManagerDialog extends JDialog {
private JTable profileTable;
private DefaultTableModel tableModel;
private AppConfig config;
private Consumer<FtpProfile> onConnectRequested;
public FtpProfileManagerDialog(Frame parent, AppConfig config) {
this(parent, config, null);
}
public FtpProfileManagerDialog(Frame parent, AppConfig config, Consumer<FtpProfile> onConnectRequested) {
super(parent, "FTP Profiles", ModalityType.APPLICATION_MODAL);
this.config = config;
this.onConnectRequested = onConnectRequested;
initComponents();
loadProfiles();
pack();
setLocationRelativeTo(parent);
}
private void initComponents() {
setLayout(new BorderLayout(10, 10));
// Table
String[] columns = {"Name", "Host", "Port", "Username"};
tableModel = new DefaultTableModel(columns, 0) {
@Override
public boolean isCellEditable(int row, int column) {
return false;
}
};
profileTable = new JTable(tableModel);
profileTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
profileTable.setRowHeight(24);
JScrollPane scrollPane = new JScrollPane(profileTable);
add(scrollPane, BorderLayout.CENTER);
// Buttons
JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 0));
JButton newButton = new JButton("New");
newButton.addActionListener(e -> createProfile());
JButton editButton = new JButton("Edit");
editButton.addActionListener(e -> editProfile());
JButton deleteButton = new JButton("Delete");
deleteButton.addActionListener(e -> deleteProfile());
JButton connectButton = new JButton("Connect");
connectButton.setToolTipText("Connect to selected profile in current panel");
connectButton.addActionListener(e -> connectToProfile());
JButton closeButton = new JButton("Close");
closeButton.addActionListener(e -> dispose());
btnPanel.add(newButton);
btnPanel.add(editButton);
btnPanel.add(deleteButton);
btnPanel.add(connectButton);
btnPanel.add(closeButton);
add(btnPanel, BorderLayout.SOUTH);
}
private void loadProfiles() {
tableModel.setRowCount(0);
List<FtpProfile> profiles = config.getFtpProfiles();
for (FtpProfile p : profiles) {
tableModel.addRow(new Object[]{
p.getName(), p.getHost(), p.getPort(), p.getUsername()
});
}
}
private void createProfile() {
FtpProfileEditDialog dialog = new FtpProfileEditDialog(this, config, null);
dialog.setVisible(true);
if (dialog.isSaved()) {
List<FtpProfile> profiles = config.getFtpProfiles();
profiles.add(dialog.getProfile());
config.saveFtpProfiles(profiles);
loadProfiles();
}
}
private void editProfile() {
int row = profileTable.getSelectedRow();
if (row < 0) {
JOptionPane.showMessageDialog(this, "Select a profile to edit.", "No Selection", JOptionPane.INFORMATION_MESSAGE);
return;
}
List<FtpProfile> profiles = config.getFtpProfiles();
FtpProfile profile = profiles.get(row);
FtpProfileEditDialog dialog = new FtpProfileEditDialog(this, config, profile);
dialog.setVisible(true);
if (dialog.isSaved()) {
profiles.set(row, dialog.getProfile());
config.saveFtpProfiles(profiles);
loadProfiles();
}
}
private void deleteProfile() {
int row = profileTable.getSelectedRow();
if (row < 0) {
JOptionPane.showMessageDialog(this, "Select a profile to delete.", "No Selection", JOptionPane.INFORMATION_MESSAGE);
return;
}
int result = JOptionPane.showConfirmDialog(this,
"Delete profile \"" + profileTable.getValueAt(row, 0) + "\"?",
"Confirm Delete", JOptionPane.YES_NO_OPTION);
if (result != JOptionPane.YES_OPTION) return;
List<FtpProfile> profiles = config.getFtpProfiles();
profiles.remove(row);
config.saveFtpProfiles(profiles);
loadProfiles();
}
private void connectToProfile() {
int row = profileTable.getSelectedRow();
if (row < 0) {
JOptionPane.showMessageDialog(this, "Select a profile to connect.", "No Selection", JOptionPane.INFORMATION_MESSAGE);
return;
}
List<FtpProfile> profiles = config.getFtpProfiles();
FtpProfile profile = profiles.get(row);
dispose();
if (onConnectRequested != null) {
onConnectRequested.accept(profile);
}
}
}

View File

@ -3,14 +3,14 @@ package cz.kamma.kfmanager.ui;
import cz.kamma.kfmanager.MainApp; 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.model.FtpProfile;
import cz.kamma.kfmanager.service.ClipboardService; import cz.kamma.kfmanager.service.ClipboardService;
import cz.kamma.kfmanager.service.FileOperations; import cz.kamma.kfmanager.service.FileOperations;
import cz.kamma.kfmanager.service.FileOperationQueue; import cz.kamma.kfmanager.service.FileOperationQueue;
import cz.kamma.kfmanager.service.FtpService;
import javax.swing.*; import javax.swing.*;
import javax.swing.filechooser.FileSystemView; import javax.swing.filechooser.FileSystemView;
import javax.swing.event.AncestorListener;
import javax.swing.event.AncestorEvent;
import java.awt.*; import java.awt.*;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.DataFlavor;
@ -986,11 +986,18 @@ 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..."); JMenuItem queueItem = new JMenuItem("Operations Queue...");
queueItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q, InputEvent.ALT_DOWN_MASK)); queueItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q, InputEvent.ALT_DOWN_MASK));
queueItem.addActionListener(e -> OperationQueueDialog.showQueue(this)); queueItem.addActionListener(e -> OperationQueueDialog.showQueue(this));
JMenuItem exitItem = new JMenuItem("Exit"); JMenuItem ftpConnectItem = new JMenuItem("Connect to FTP...");
ftpConnectItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F, InputEvent.CTRL_DOWN_MASK));
ftpConnectItem.addActionListener(e -> showFtpConnectDialog());
JMenuItem ftpProfileItem = new JMenuItem("FTP Profiles...");
ftpProfileItem.addActionListener(e -> showFtpProfileManagerDialog());
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());
@ -1001,6 +1008,9 @@ public class MainWindow extends JFrame {
fileMenu.add(refreshItem); fileMenu.add(refreshItem);
fileMenu.add(queueItem); fileMenu.add(queueItem);
fileMenu.addSeparator(); fileMenu.addSeparator();
fileMenu.add(ftpConnectItem);
fileMenu.add(ftpProfileItem);
fileMenu.addSeparator();
fileMenu.add(exitItem); fileMenu.add(exitItem);
// View menu // View menu
@ -1349,11 +1359,16 @@ public class MainWindow extends JFrame {
KeyStroke.getKeyStroke(KeyEvent.VK_T, InputEvent.CTRL_DOWN_MASK), KeyStroke.getKeyStroke(KeyEvent.VK_T, InputEvent.CTRL_DOWN_MASK),
JComponent.WHEN_IN_FOCUSED_WINDOW); JComponent.WHEN_IN_FOCUSED_WINDOW);
// Ctrl+W - Close current tab // Ctrl+W - Close current tab
rootPane.registerKeyboardAction(e -> closeCurrentTabInActivePanel(), rootPane.registerKeyboardAction(e -> closeCurrentTabInActivePanel(),
KeyStroke.getKeyStroke(KeyEvent.VK_W, InputEvent.CTRL_DOWN_MASK), KeyStroke.getKeyStroke(KeyEvent.VK_W, InputEvent.CTRL_DOWN_MASK),
JComponent.WHEN_IN_FOCUSED_WINDOW); JComponent.WHEN_IN_FOCUSED_WINDOW);
// Ctrl+F - Connect to FTP
rootPane.registerKeyboardAction(e -> showFtpConnectDialog(),
KeyStroke.getKeyStroke(KeyEvent.VK_F, InputEvent.CTRL_DOWN_MASK),
JComponent.WHEN_IN_FOCUSED_WINDOW);
// Ctrl+` - Home directory // Ctrl+` - Home directory
rootPane.registerKeyboardAction(e -> { rootPane.registerKeyboardAction(e -> {
if (activePanel != null && activePanel.getCurrentTab() != null) { if (activePanel != null && activePanel.getCurrentTab() != null) {
@ -1749,7 +1764,7 @@ public class MainWindow extends JFrame {
commandLine.requestFocusInWindow(); commandLine.requestFocusInWindow();
} }
/** /**
* Copy selected files to the opposite panel * Copy selected files to the opposite panel
*/ */
private void copyFiles() { private void copyFiles() {
@ -1764,35 +1779,71 @@ public class MainWindow extends JFrame {
} }
FilePanel targetPanel = (activePanel == leftPanel) ? rightPanel : leftPanel; FilePanel targetPanel = (activePanel == leftPanel) ? rightPanel : leftPanel;
File targetDir = targetPanel.getCurrentDirectory(); FilePanelTab targetTab = targetPanel.getCurrentTab();
boolean targetIsFtp = targetTab != null && targetTab.isFtpTab();
FilePanel sourcePanel = activePanel; FilePanel sourcePanel = activePanel;
FilePanelTab sourceTab = sourcePanel.getCurrentTab();
boolean sourceIsFtp = sourceTab != null && sourceTab.isFtpTab();
int result = showConfirmWithBackground( if (targetIsFtp && targetTab.getFtpProfile() != null) {
"Copy %d items to:\n%s".formatted(selectedItems.size(), targetDir.getAbsolutePath()), FtpProfile ftpProfile = targetTab.getFtpProfile();
String targetPath = targetTab.getFtpCurrentPath();
String msg = "Copy %d items to:\nftp://%s:%d%s".formatted(
selectedItems.size(), ftpProfile.getHost(), ftpProfile.getPort(), targetPath);
int result = showConfirmWithBackground(msg, "Copy");
if (result == 0 || result == 1) {
boolean background = (result == 1);
if (background) {
addOperationToQueue("Copy", "Copy %d items to FTP".formatted(selectedItems.size()),
(cb) -> FileOperations.copyToFtp(selectedItems, ftpProfile, targetPath, cb),
() -> sourcePanel.unselectAll(), targetPanel);
} else {
performFileOperation((cb) -> FileOperations.copyToFtp(selectedItems, ftpProfile, targetPath, cb),
"Copy completed", false, true, () -> sourcePanel.unselectAll(), targetPanel);
}
}
} else if (sourceIsFtp) {
File targetDir = targetPanel.getCurrentDirectory();
int result = showConfirmWithBackground(
"Copy %d items from FTP to:\n%s".formatted(selectedItems.size(), targetDir.getAbsolutePath()),
"Copy"); "Copy");
if (result == 0 || result == 1) { if (result == 0 || result == 1) {
boolean background = (result == 1); boolean background = (result == 1);
if (background) { if (background) {
addOperationToQueue("Copy", "Copy %d items to %s".formatted(selectedItems.size(), targetDir.getName()), addOperationToQueue("Copy", "Copy %d items from FTP".formatted(selectedItems.size()),
(cb) -> { (cb) -> FileOperations.copyFromFtp(selectedItems, targetDir, cb),
FileOperations.copy(selectedItems, targetDir, cb); () -> sourcePanel.unselectAll(), targetPanel);
syncTargetArchiveIfNeeded(targetPanel, targetDir, cb); } else {
}, () -> sourcePanel.unselectAll(), targetPanel); performFileOperation((cb) -> FileOperations.copyFromFtp(selectedItems, targetDir, cb),
} else { "Copy from FTP completed", false, true, () -> sourcePanel.unselectAll(), targetPanel);
performFileOperation((callback) -> { }
FileOperations.copy(selectedItems, targetDir, callback);
syncTargetArchiveIfNeeded(targetPanel, targetDir, callback);
}, "Copy completed", false, true, () -> sourcePanel.unselectAll(), targetPanel);
} }
} else { } else {
if (activePanel != null && activePanel.getFileTable() != null) { File targetDir = targetPanel.getCurrentDirectory();
activePanel.getFileTable().requestFocusInWindow(); int result = showConfirmWithBackground(
"Copy %d items to:\n%s".formatted(selectedItems.size(), targetDir.getAbsolutePath()),
"Copy");
if (result == 0 || result == 1) {
boolean background = (result == 1);
if (background) {
addOperationToQueue("Copy", "Copy %d items to %s".formatted(selectedItems.size(), targetDir.getName()),
(cb) -> {
FileOperations.copy(selectedItems, targetDir, cb);
syncTargetArchiveIfNeeded(targetPanel, targetDir, cb);
}, () -> sourcePanel.unselectAll(), targetPanel);
} else {
performFileOperation((callback) -> {
FileOperations.copy(selectedItems, targetDir, callback);
syncTargetArchiveIfNeeded(targetPanel, targetDir, callback);
}, "Copy completed", false, true, () -> sourcePanel.unselectAll(), targetPanel);
}
} }
} }
} }
/** /**
* Move selected files to the opposite panel * Move selected files to the opposite panel
*/ */
private void moveFiles() { private void moveFiles() {
@ -1807,29 +1858,81 @@ public class MainWindow extends JFrame {
} }
FilePanel targetPanel = (activePanel == leftPanel) ? rightPanel : leftPanel; FilePanel targetPanel = (activePanel == leftPanel) ? rightPanel : leftPanel;
File targetDir = targetPanel.getCurrentDirectory(); FilePanelTab targetTab = targetPanel.getCurrentTab();
boolean targetIsFtp = targetTab != null && targetTab.isFtpTab();
boolean sourceIsFtp = !selectedItems.isEmpty() && selectedItems.get(0).isFtp();
int result = showConfirmWithBackground( if (targetIsFtp && targetTab.getFtpProfile() != null) {
"Move %d items to:\n%s".formatted(selectedItems.size(), targetDir.getAbsolutePath()), // Move to FTP (copy to FTP, then delete source)
"Move"); FtpProfile ftpProfile = targetTab.getFtpProfile();
String targetPath = targetTab.getFtpCurrentPath();
if (result == 0 || result == 1) { String msg = "Move %d items to:\nftp://%s:%d%s".formatted(
boolean background = (result == 1); selectedItems.size(), ftpProfile.getHost(), ftpProfile.getPort(), targetPath);
if (background) { int result = showConfirmWithBackground(msg, "Move");
addOperationToQueue("Move", "Move %d items to %s".formatted(selectedItems.size(), targetDir.getName()), if (result == 0 || result == 1) {
(cb) -> { boolean background = (result == 1);
FileOperations.move(selectedItems, targetDir, cb); if (background) {
syncTargetArchiveIfNeeded(targetPanel, targetDir, cb); addOperationToQueue("Move", "Move %d items to FTP".formatted(selectedItems.size()),
}, activePanel, targetPanel); (cb) -> {
} else { FileOperations.copyToFtp(selectedItems, ftpProfile, targetPath, cb);
performFileOperation((callback) -> { if (sourceIsFtp) {
FileOperations.move(selectedItems, targetDir, callback); FileOperations.deleteFromFtp(selectedItems, cb);
syncTargetArchiveIfNeeded(targetPanel, targetDir, callback); } else {
}, "Move completed", false, true, activePanel, targetPanel); FileOperations.delete(selectedItems, cb);
}
}, activePanel, targetPanel);
} else {
performFileOperation((cb) -> {
FileOperations.copyToFtp(selectedItems, ftpProfile, targetPath, cb);
if (sourceIsFtp) {
FileOperations.deleteFromFtp(selectedItems, cb);
} else {
FileOperations.delete(selectedItems, cb);
}
}, "Move completed", false, true, activePanel, targetPanel);
}
}
} else if (sourceIsFtp) {
// Move from FTP to local (copy from FTP, then delete source)
File targetDir = targetPanel.getCurrentDirectory();
int result = showConfirmWithBackground(
"Move %d items to:\n%s".formatted(selectedItems.size(), targetDir.getAbsolutePath()),
"Move");
if (result == 0 || result == 1) {
boolean background = (result == 1);
if (background) {
addOperationToQueue("Move", "Move %d items from FTP".formatted(selectedItems.size()),
(cb) -> {
FileOperations.copyFromFtp(selectedItems, targetDir, cb);
FileOperations.deleteFromFtp(selectedItems, cb);
}, activePanel, targetPanel);
} else {
performFileOperation((cb) -> {
FileOperations.copyFromFtp(selectedItems, targetDir, cb);
FileOperations.deleteFromFtp(selectedItems, cb);
}, "Move completed", false, true, activePanel, targetPanel);
}
} }
} else { } else {
if (activePanel != null && activePanel.getFileTable() != null) { File targetDir = targetPanel.getCurrentDirectory();
activePanel.getFileTable().requestFocusInWindow(); int result = showConfirmWithBackground(
"Move %d items to:\n%s".formatted(selectedItems.size(), targetDir.getAbsolutePath()),
"Move");
if (result == 0 || result == 1) {
boolean background = (result == 1);
if (background) {
addOperationToQueue("Move", "Move %d items to %s".formatted(selectedItems.size(), targetDir.getName()),
(cb) -> {
FileOperations.move(selectedItems, targetDir, cb);
syncTargetArchiveIfNeeded(targetPanel, targetDir, cb);
}, activePanel, targetPanel);
} else {
performFileOperation((callback) -> {
FileOperations.move(selectedItems, targetDir, callback);
syncTargetArchiveIfNeeded(targetPanel, targetDir, callback);
}, "Move completed", false, true, activePanel, targetPanel);
}
} }
} }
} }
@ -2103,13 +2206,13 @@ public class MainWindow extends JFrame {
requestFocusInActivePanel(); requestFocusInActivePanel();
return; return;
} }
FileItem item = selectedItems.getFirst(); FileItem item = selectedItems.getFirst();
String newName = JOptionPane.showInputDialog(this, String newName = JOptionPane.showInputDialog(this,
"New name:", "New name:",
item.getName()); item.getName());
if (newName != null && !newName.trim().isEmpty() && !newName.equals(item.getName())) { if (newName != null && !newName.trim().isEmpty() && !newName.equals(item.getName())) {
performFileOperation((callback) -> { performFileOperation((callback) -> {
FileOperations.rename(item.getFile(), newName.trim()); FileOperations.rename(item, newName.trim());
}, "Rename completed", false, activePanel); }, "Rename completed", false, activePanel);
} else { } else {
if (activePanel != null && activePanel.getFileTable() != null) { if (activePanel != null && activePanel.getFileTable() != null) {
@ -2120,10 +2223,11 @@ public class MainWindow extends JFrame {
} }
} }
/** /**
* Create a new directory * Create a new directory
*/ */
private void createNewDirectory() { private void createNewDirectory() {
FilePanelTab currentTab = activePanel.getCurrentTab();
String initialValue = "New directory"; String initialValue = "New directory";
FileItem focusedItem = activePanel.getFocusedItem(); FileItem focusedItem = activePanel.getFocusedItem();
if (focusedItem != null && !focusedItem.getName().equals("..")) { if (focusedItem != null && !focusedItem.getName().equals("..")) {
@ -2135,13 +2239,26 @@ public class MainWindow extends JFrame {
if (dirNameInput != null && !dirNameInput.trim().isEmpty()) { if (dirNameInput != null && !dirNameInput.trim().isEmpty()) {
final String dirName = dirNameInput.trim(); final String dirName = dirNameInput.trim();
performFileOperation((callback) -> { if (currentTab != null && currentTab.isFtpTab() && currentTab.getFtpProfile() != null) {
FileOperations.createDirectory(activePanel.getCurrentDirectory(), dirName); final FtpProfile profile = currentTab.getFtpProfile();
}, "Directory created", false, () -> { final String path = currentTab.getFtpCurrentPath();
if (activePanel != null && activePanel.getCurrentTab() != null) { performFileOperation((callback) -> {
activePanel.getCurrentTab().selectItem(dirName); FileOperations.createDirectoryOnFtp(profile, path, dirName);
} }, "Directory created", false, () -> {
}, activePanel); if (currentTab != null) {
currentTab.loadFtpDirectory(path, false, false);
SwingUtilities.invokeLater(() -> currentTab.selectItem(dirName));
}
}, activePanel);
} else {
performFileOperation((callback) -> {
FileOperations.createDirectory(activePanel.getCurrentDirectory(), dirName);
}, "Directory created", false, () -> {
if (activePanel != null && activePanel.getCurrentTab() != null) {
activePanel.getCurrentTab().selectItem(dirName);
}
}, activePanel);
}
} else { } else {
if (activePanel != null && activePanel.getFileTable() != null) { if (activePanel != null && activePanel.getFileTable() != null) {
activePanel.getFileTable().requestFocusInWindow(); activePanel.getFileTable().requestFocusInWindow();
@ -2150,6 +2267,7 @@ public class MainWindow extends JFrame {
} }
private void createNewFile() { private void createNewFile() {
FilePanelTab currentTab = activePanel.getCurrentTab();
String initialValue = "New file.txt"; String initialValue = "New file.txt";
FileItem focusedItem = activePanel.getFocusedItem(); FileItem focusedItem = activePanel.getFocusedItem();
if (focusedItem != null && !focusedItem.getName().equals("..")) { if (focusedItem != null && !focusedItem.getName().equals("..")) {
@ -2161,14 +2279,27 @@ public class MainWindow extends JFrame {
if (fileNameInput != null && !fileNameInput.trim().isEmpty()) { if (fileNameInput != null && !fileNameInput.trim().isEmpty()) {
final String fileName = fileNameInput.trim(); final String fileName = fileNameInput.trim();
performFileOperation((callback) -> { if (currentTab != null && currentTab.isFtpTab() && currentTab.getFtpProfile() != null) {
FileOperations.createFile(activePanel.getCurrentDirectory(), fileName); final FtpProfile profile = currentTab.getFtpProfile();
}, "File created", false, () -> { final String path = currentTab.getFtpCurrentPath();
if (activePanel != null && activePanel.getCurrentTab() != null) { performFileOperation((callback) -> {
activePanel.getCurrentTab().selectItem(fileName); FileOperations.createFileOnFtp(profile, path, fileName);
editFile(); }, "File created", false, () -> {
} if (currentTab != null) {
}, activePanel); currentTab.loadFtpDirectory(path, false, false);
SwingUtilities.invokeLater(() -> currentTab.selectItem(fileName));
}
}, activePanel);
} else {
performFileOperation((callback) -> {
FileOperations.createFile(activePanel.getCurrentDirectory(), fileName);
}, "File created", false, () -> {
if (activePanel != null && activePanel.getCurrentTab() != null) {
activePanel.getCurrentTab().selectItem(fileName);
editFile();
}
}, activePanel);
}
} else { } else {
if (activePanel != null && activePanel.getFileTable() != null) { if (activePanel != null && activePanel.getFileTable() != null) {
activePanel.getFileTable().requestFocusInWindow(); activePanel.getFileTable().requestFocusInWindow();
@ -3380,6 +3511,11 @@ public class MainWindow extends JFrame {
if (autoRefreshTimer != null) { if (autoRefreshTimer != null) {
autoRefreshTimer.stop(); autoRefreshTimer.stop();
} }
// Close all FTP tabs before saving configuration to prevent them from being restored
if (leftPanel != null) leftPanel.closeAllFtpTabs();
if (rightPanel != null) rightPanel.closeAllFtpTabs();
persistDividerPosition(); persistDividerPosition();
// Save window state // Save window state
config.saveWindowState(this); config.saveWindowState(this);
@ -3486,13 +3622,61 @@ public class MainWindow extends JFrame {
} }
int interval = config.getAutoRefreshInterval(); int interval = config.getAutoRefreshInterval();
autoRefreshTimer = new Timer(interval, e -> { autoRefreshTimer = new Timer(interval, e -> {
if (leftPanel != null && leftPanel.getCurrentDirectory() != null) { if (leftPanel != null) {
leftPanel.refresh(false); FilePanelTab tab = leftPanel.getCurrentTab();
if (tab != null && !tab.isFtpTab() && leftPanel.getCurrentDirectory() != null) {
leftPanel.refresh(false);
}
} }
if (rightPanel != null && rightPanel.getCurrentDirectory() != null) { if (rightPanel != null) {
rightPanel.refresh(false); FilePanelTab tab = rightPanel.getCurrentTab();
if (tab != null && !tab.isFtpTab() && rightPanel.getCurrentDirectory() != null) {
rightPanel.refresh(false);
}
} }
}); });
autoRefreshTimer.start(); autoRefreshTimer.start();
} }
// --- FTP support ---
private void showFtpConnectDialog() {
List<FtpProfile> profiles = config.getFtpProfiles();
if (profiles.isEmpty()) {
JOptionPane.showMessageDialog(this,
"No FTP profiles configured.\nGo to File -> FTP Profiles to add one.",
"FTP Connection", JOptionPane.INFORMATION_MESSAGE);
return;
}
String[] names = profiles.stream().map(FtpProfile::getName).toArray(String[]::new);
String selectedName = (String) JOptionPane.showInputDialog(
this, "Select FTP profile to connect:", "Connect to FTP",
JOptionPane.QUESTION_MESSAGE, null, names, names[0]);
if (selectedName != null) {
profiles.stream()
.filter(p -> p.getName().equals(selectedName))
.findFirst()
.ifPresent(this::connectToFtp);
}
}
private void connectToFtp(FtpProfile profile) {
try {
// Test connection by listing root
FtpService.listDirectory(profile, "/");
String ftpPath = FtpService.buildFtpUrl(profile, "/");
if (activePanel != null) {
activePanel.addFtpTab(ftpPath, profile);
}
} catch (IOException e) {
JOptionPane.showMessageDialog(this,
"Failed to connect to FTP server: " + e.getMessage(),
"FTP Error", JOptionPane.ERROR_MESSAGE);
}
}
private void showFtpProfileManagerDialog() {
FtpProfileManagerDialog dialog = new FtpProfileManagerDialog(this, config, this::connectToFtp);
dialog.setVisible(true);
}
} }