FTP draft
This commit is contained in:
parent
676b3bbb3a
commit
a810cfac02
5
pom.xml
5
pom.xml
@ -116,5 +116,10 @@
|
||||
<artifactId>flatlaf-extras</artifactId>
|
||||
<version>3.5.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-net</groupId>
|
||||
<artifactId>commons-net</artifactId>
|
||||
<version>3.11.1</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@ -1018,4 +1018,66 @@ public class AppConfig {
|
||||
public void setMaxCompareLines(int 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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,10 +5,11 @@ import java.text.SimpleDateFormat;
|
||||
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 {
|
||||
|
||||
|
||||
private final File file;
|
||||
private final String name;
|
||||
private final long size;
|
||||
@ -17,6 +18,10 @@ public class FileItem {
|
||||
private boolean marked;
|
||||
private boolean recentlyChanged;
|
||||
private String displayPath;
|
||||
// FTP support
|
||||
private final boolean isFtp;
|
||||
private final String ftpPath;
|
||||
private final FtpProfile ftpProfile;
|
||||
|
||||
public FileItem(File file) {
|
||||
this(file, null);
|
||||
@ -29,7 +34,28 @@ public class FileItem {
|
||||
this.modified = new Date(file.lastModified());
|
||||
this.isDirectory = file.isDirectory();
|
||||
this.marked = false;
|
||||
this.recentlyChanged = false;
|
||||
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() {
|
||||
@ -64,6 +90,29 @@ public class FileItem {
|
||||
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.
|
||||
*/
|
||||
@ -74,21 +123,26 @@ public class FileItem {
|
||||
return isDirectory == other.isDirectory &&
|
||||
size == other.size &&
|
||||
(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() {
|
||||
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() {
|
||||
return marked;
|
||||
}
|
||||
|
||||
|
||||
public void setMarked(boolean marked) {
|
||||
this.marked = marked;
|
||||
}
|
||||
|
||||
|
||||
public void toggleMarked() {
|
||||
this.marked = !this.marked;
|
||||
}
|
||||
@ -100,7 +154,7 @@ public class FileItem {
|
||||
public void setRecentlyChanged(boolean recentlyChanged) {
|
||||
this.recentlyChanged = recentlyChanged;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Format file size into a human-readable string
|
||||
*/
|
||||
|
||||
71
src/main/java/cz/kamma/kfmanager/model/FtpProfile.java
Normal file
71
src/main/java/cz/kamma/kfmanager/model/FtpProfile.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -19,6 +19,7 @@ import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import cz.kamma.kfmanager.model.FileItem;
|
||||
import cz.kamma.kfmanager.model.FtpProfile;
|
||||
import net.lingala.zip4j.ZipFile;
|
||||
|
||||
/**
|
||||
@ -304,11 +305,15 @@ public class FileOperations {
|
||||
/**
|
||||
* 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);
|
||||
long totalItems = calculateTotalItems(cleanedItems);
|
||||
long[] currentItem = {0};
|
||||
|
||||
|
||||
for (FileItem item : cleanedItems) {
|
||||
if (callback != null && callback.isCancelled()) break;
|
||||
File file = item.getFile();
|
||||
@ -401,13 +406,156 @@ public class FileOperations {
|
||||
/**
|
||||
* 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);
|
||||
if (!file.renameTo(target)) {
|
||||
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
|
||||
*/
|
||||
|
||||
535
src/main/java/cz/kamma/kfmanager/service/FtpService.java
Normal file
535
src/main/java/cz/kamma/kfmanager/service/FtpService.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,8 @@ package cz.kamma.kfmanager.ui;
|
||||
|
||||
import cz.kamma.kfmanager.MainApp;
|
||||
import cz.kamma.kfmanager.model.FileItem;
|
||||
import cz.kamma.kfmanager.model.FtpProfile;
|
||||
import cz.kamma.kfmanager.service.FtpService;
|
||||
|
||||
import javax.swing.*;
|
||||
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
|
||||
*/
|
||||
@ -441,8 +505,13 @@ public class FilePanel extends JPanel {
|
||||
for (int i = 0; i < tabbedPane.getTabCount(); i++) {
|
||||
Component c = tabbedPane.getComponentAt(i);
|
||||
if (c instanceof FilePanelTab t) {
|
||||
File dir = t.getCurrentDirectory();
|
||||
paths.add(dir != null ? dir.getAbsolutePath() : System.getProperty("user.home"));
|
||||
if (t.isFtpTab() && t.getFtpProfile() != null) {
|
||||
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;
|
||||
@ -489,20 +558,35 @@ public class FilePanel extends JPanel {
|
||||
|
||||
for (int i = 0; i < paths.size(); i++) {
|
||||
String p = paths.get(i);
|
||||
ViewMode mode = ViewMode.FULL;
|
||||
if (viewModes != null && i < viewModes.size()) {
|
||||
try {
|
||||
mode = ViewMode.valueOf(viewModes.get(i));
|
||||
} catch (IllegalArgumentException ex) {
|
||||
mode = ViewMode.FULL;
|
||||
if (p != null && p.startsWith("ftp://")) {
|
||||
// FTP tab: look up matching profile
|
||||
FtpProfile profile = null;
|
||||
if (appConfig != null) {
|
||||
profile = FtpService.findProfileForUrl(p, appConfig);
|
||||
}
|
||||
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
|
||||
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 FilePanelTab currentTab = (FilePanelTab) tabbedPane.getComponentAt(i);
|
||||
final FilePanelTab currentTab = (FilePanelTab) tabbedPane.getComponentAt(tabIndex);
|
||||
SwingUtilities.invokeLater(() -> currentTab.selectItem(focusName, requestFocus));
|
||||
}
|
||||
}
|
||||
@ -515,6 +599,7 @@ public class FilePanel extends JPanel {
|
||||
|
||||
updatePathField();
|
||||
updateTabStyles();
|
||||
updateDriveComboVisibility();
|
||||
}
|
||||
|
||||
private void populateDrives() {
|
||||
@ -695,6 +780,25 @@ public class FilePanel extends JPanel {
|
||||
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
|
||||
*/
|
||||
|
||||
@ -2,9 +2,11 @@ package cz.kamma.kfmanager.ui;
|
||||
|
||||
import cz.kamma.kfmanager.MainApp;
|
||||
import cz.kamma.kfmanager.model.FileItem;
|
||||
import cz.kamma.kfmanager.model.FtpProfile;
|
||||
import cz.kamma.kfmanager.config.AppConfig;
|
||||
import cz.kamma.kfmanager.service.ClipboardService;
|
||||
import cz.kamma.kfmanager.service.FileOperations;
|
||||
import cz.kamma.kfmanager.service.FtpService;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.filechooser.FileSystemView;
|
||||
@ -93,15 +95,56 @@ public class FilePanelTab extends JPanel {
|
||||
private JTextField filterTextField;
|
||||
private JPanel filterPanel;
|
||||
private boolean searchModeActive = false;
|
||||
|
||||
// FTP support
|
||||
private boolean isFtpTab = false;
|
||||
private FtpProfile ftpProfile;
|
||||
private String ftpCurrentPath;
|
||||
|
||||
public FilePanelTab(String initialPath) {
|
||||
this(initialPath, true);
|
||||
}
|
||||
|
||||
|
||||
public FilePanelTab(String initialPath, boolean requestFocus) {
|
||||
this.currentDirectory = new File(initialPath);
|
||||
initComponents();
|
||||
loadDirectory(currentDirectory, true, requestFocus);
|
||||
if (initialPath != null && initialPath.startsWith("ftp://")) {
|
||||
this.isFtpTab = true;
|
||||
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). */
|
||||
@ -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.
|
||||
*/
|
||||
public void refresh(boolean requestFocus) {
|
||||
if (isFtpTab) {
|
||||
loadFtpDirectory(ftpCurrentPath, false, requestFocus);
|
||||
return;
|
||||
}
|
||||
|
||||
List<FileItem> newItems = createFileItemList(currentDirectory);
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
@ -1798,6 +1923,16 @@ public class FilePanelTab extends JPanel {
|
||||
}
|
||||
|
||||
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("..")) {
|
||||
navigateUp();
|
||||
@ -2321,7 +2456,19 @@ public class FilePanelTab extends JPanel {
|
||||
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("..")) {
|
||||
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
|
||||
// original archive's parent and select the archive file.
|
||||
try {
|
||||
@ -4338,20 +4491,32 @@ public class FilePanelTab extends JPanel {
|
||||
item = getItem(rowIndex);
|
||||
}
|
||||
|
||||
if (item == null) return;
|
||||
if (item == null) return;
|
||||
|
||||
// Perform rename using FileOperations and refresh the directory
|
||||
// Perform rename
|
||||
try {
|
||||
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) {}
|
||||
});
|
||||
if (item.isFtp()) {
|
||||
FtpService.renameOnFtp(item.getFtpProfile(), item.getFtpPath(), newName);
|
||||
String newFtpPath = FtpService.getParentPath(item.getFtpPath());
|
||||
loadFtpDirectory(newFtpPath, false, false);
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
try {
|
||||
FilePanelTab.this.selectItem(newName);
|
||||
FilePanelTab.this.getFileTable().requestFocusInWindow();
|
||||
} 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) {
|
||||
// show error to user
|
||||
try {
|
||||
|
||||
155
src/main/java/cz/kamma/kfmanager/ui/FtpProfileEditDialog.java
Normal file
155
src/main/java/cz/kamma/kfmanager/ui/FtpProfileEditDialog.java
Normal 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;
|
||||
}
|
||||
}
|
||||
151
src/main/java/cz/kamma/kfmanager/ui/FtpProfileManagerDialog.java
Normal file
151
src/main/java/cz/kamma/kfmanager/ui/FtpProfileManagerDialog.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3,14 +3,14 @@ package cz.kamma.kfmanager.ui;
|
||||
import cz.kamma.kfmanager.MainApp;
|
||||
import cz.kamma.kfmanager.config.AppConfig;
|
||||
import cz.kamma.kfmanager.model.FileItem;
|
||||
import cz.kamma.kfmanager.model.FtpProfile;
|
||||
import cz.kamma.kfmanager.service.ClipboardService;
|
||||
import cz.kamma.kfmanager.service.FileOperations;
|
||||
import cz.kamma.kfmanager.service.FileOperationQueue;
|
||||
import cz.kamma.kfmanager.service.FtpService;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.filechooser.FileSystemView;
|
||||
import javax.swing.event.AncestorListener;
|
||||
import javax.swing.event.AncestorEvent;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.datatransfer.DataFlavor;
|
||||
@ -986,14 +986,21 @@ public class MainWindow extends JFrame {
|
||||
refreshItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F5, InputEvent.CTRL_DOWN_MASK));
|
||||
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.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.addActionListener(e -> saveConfigAndExit());
|
||||
|
||||
|
||||
fileMenu.add(searchItem);
|
||||
fileMenu.add(selectAllItem);
|
||||
fileMenu.add(selectWildcardItem);
|
||||
@ -1001,6 +1008,9 @@ public class MainWindow extends JFrame {
|
||||
fileMenu.add(refreshItem);
|
||||
fileMenu.add(queueItem);
|
||||
fileMenu.addSeparator();
|
||||
fileMenu.add(ftpConnectItem);
|
||||
fileMenu.add(ftpProfileItem);
|
||||
fileMenu.addSeparator();
|
||||
fileMenu.add(exitItem);
|
||||
|
||||
// View menu
|
||||
@ -1349,11 +1359,16 @@ public class MainWindow extends JFrame {
|
||||
KeyStroke.getKeyStroke(KeyEvent.VK_T, InputEvent.CTRL_DOWN_MASK),
|
||||
JComponent.WHEN_IN_FOCUSED_WINDOW);
|
||||
|
||||
// Ctrl+W - Close current tab
|
||||
// Ctrl+W - Close current tab
|
||||
rootPane.registerKeyboardAction(e -> closeCurrentTabInActivePanel(),
|
||||
KeyStroke.getKeyStroke(KeyEvent.VK_W, InputEvent.CTRL_DOWN_MASK),
|
||||
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
|
||||
rootPane.registerKeyboardAction(e -> {
|
||||
if (activePanel != null && activePanel.getCurrentTab() != null) {
|
||||
@ -1749,7 +1764,7 @@ public class MainWindow extends JFrame {
|
||||
commandLine.requestFocusInWindow();
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Copy selected files to the opposite panel
|
||||
*/
|
||||
private void copyFiles() {
|
||||
@ -1762,37 +1777,73 @@ public class MainWindow extends JFrame {
|
||||
requestFocusInActivePanel();
|
||||
return;
|
||||
}
|
||||
|
||||
FilePanel targetPanel = (activePanel == leftPanel) ? rightPanel : leftPanel;
|
||||
File targetDir = targetPanel.getCurrentDirectory();
|
||||
FilePanel sourcePanel = activePanel;
|
||||
|
||||
int result = showConfirmWithBackground(
|
||||
"Copy %d items to:\n%s".formatted(selectedItems.size(), targetDir.getAbsolutePath()),
|
||||
FilePanel targetPanel = (activePanel == leftPanel) ? rightPanel : leftPanel;
|
||||
FilePanelTab targetTab = targetPanel.getCurrentTab();
|
||||
boolean targetIsFtp = targetTab != null && targetTab.isFtpTab();
|
||||
FilePanel sourcePanel = activePanel;
|
||||
FilePanelTab sourceTab = sourcePanel.getCurrentTab();
|
||||
boolean sourceIsFtp = sourceTab != null && sourceTab.isFtpTab();
|
||||
|
||||
if (targetIsFtp && targetTab.getFtpProfile() != null) {
|
||||
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");
|
||||
|
||||
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);
|
||||
|
||||
if (result == 0 || result == 1) {
|
||||
boolean background = (result == 1);
|
||||
if (background) {
|
||||
addOperationToQueue("Copy", "Copy %d items from FTP".formatted(selectedItems.size()),
|
||||
(cb) -> FileOperations.copyFromFtp(selectedItems, targetDir, cb),
|
||||
() -> sourcePanel.unselectAll(), targetPanel);
|
||||
} else {
|
||||
performFileOperation((cb) -> FileOperations.copyFromFtp(selectedItems, targetDir, cb),
|
||||
"Copy from FTP completed", false, true, () -> sourcePanel.unselectAll(), targetPanel);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (activePanel != null && activePanel.getFileTable() != null) {
|
||||
activePanel.getFileTable().requestFocusInWindow();
|
||||
File targetDir = targetPanel.getCurrentDirectory();
|
||||
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
|
||||
*/
|
||||
private void moveFiles() {
|
||||
@ -1805,31 +1856,83 @@ public class MainWindow extends JFrame {
|
||||
requestFocusInActivePanel();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
FilePanel targetPanel = (activePanel == leftPanel) ? rightPanel : leftPanel;
|
||||
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 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);
|
||||
FilePanelTab targetTab = targetPanel.getCurrentTab();
|
||||
boolean targetIsFtp = targetTab != null && targetTab.isFtpTab();
|
||||
boolean sourceIsFtp = !selectedItems.isEmpty() && selectedItems.get(0).isFtp();
|
||||
|
||||
if (targetIsFtp && targetTab.getFtpProfile() != null) {
|
||||
// Move to FTP (copy to FTP, then delete source)
|
||||
FtpProfile ftpProfile = targetTab.getFtpProfile();
|
||||
String targetPath = targetTab.getFtpCurrentPath();
|
||||
String msg = "Move %d items to:\nftp://%s:%d%s".formatted(
|
||||
selectedItems.size(), ftpProfile.getHost(), ftpProfile.getPort(), targetPath);
|
||||
int result = showConfirmWithBackground(msg, "Move");
|
||||
if (result == 0 || result == 1) {
|
||||
boolean background = (result == 1);
|
||||
if (background) {
|
||||
addOperationToQueue("Move", "Move %d items to FTP".formatted(selectedItems.size()),
|
||||
(cb) -> {
|
||||
FileOperations.copyToFtp(selectedItems, ftpProfile, targetPath, cb);
|
||||
if (sourceIsFtp) {
|
||||
FileOperations.deleteFromFtp(selectedItems, cb);
|
||||
} else {
|
||||
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 {
|
||||
if (activePanel != null && activePanel.getFileTable() != null) {
|
||||
activePanel.getFileTable().requestFocusInWindow();
|
||||
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 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();
|
||||
return;
|
||||
}
|
||||
FileItem item = selectedItems.getFirst();
|
||||
FileItem item = selectedItems.getFirst();
|
||||
String newName = JOptionPane.showInputDialog(this,
|
||||
"New name:",
|
||||
item.getName());
|
||||
if (newName != null && !newName.trim().isEmpty() && !newName.equals(item.getName())) {
|
||||
performFileOperation((callback) -> {
|
||||
FileOperations.rename(item.getFile(), newName.trim());
|
||||
FileOperations.rename(item, newName.trim());
|
||||
}, "Rename completed", false, activePanel);
|
||||
} else {
|
||||
if (activePanel != null && activePanel.getFileTable() != null) {
|
||||
@ -2120,10 +2223,11 @@ public class MainWindow extends JFrame {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Create a new directory
|
||||
*/
|
||||
private void createNewDirectory() {
|
||||
FilePanelTab currentTab = activePanel.getCurrentTab();
|
||||
String initialValue = "New directory";
|
||||
FileItem focusedItem = activePanel.getFocusedItem();
|
||||
if (focusedItem != null && !focusedItem.getName().equals("..")) {
|
||||
@ -2132,16 +2236,29 @@ public class MainWindow extends JFrame {
|
||||
String dirNameInput = JOptionPane.showInputDialog(this,
|
||||
"New directory name:",
|
||||
initialValue);
|
||||
|
||||
|
||||
if (dirNameInput != null && !dirNameInput.trim().isEmpty()) {
|
||||
final String dirName = dirNameInput.trim();
|
||||
performFileOperation((callback) -> {
|
||||
FileOperations.createDirectory(activePanel.getCurrentDirectory(), dirName);
|
||||
}, "Directory created", false, () -> {
|
||||
if (activePanel != null && activePanel.getCurrentTab() != null) {
|
||||
activePanel.getCurrentTab().selectItem(dirName);
|
||||
}
|
||||
}, activePanel);
|
||||
if (currentTab != null && currentTab.isFtpTab() && currentTab.getFtpProfile() != null) {
|
||||
final FtpProfile profile = currentTab.getFtpProfile();
|
||||
final String path = currentTab.getFtpCurrentPath();
|
||||
performFileOperation((callback) -> {
|
||||
FileOperations.createDirectoryOnFtp(profile, path, dirName);
|
||||
}, "Directory created", false, () -> {
|
||||
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 {
|
||||
if (activePanel != null && activePanel.getFileTable() != null) {
|
||||
activePanel.getFileTable().requestFocusInWindow();
|
||||
@ -2150,6 +2267,7 @@ public class MainWindow extends JFrame {
|
||||
}
|
||||
|
||||
private void createNewFile() {
|
||||
FilePanelTab currentTab = activePanel.getCurrentTab();
|
||||
String initialValue = "New file.txt";
|
||||
FileItem focusedItem = activePanel.getFocusedItem();
|
||||
if (focusedItem != null && !focusedItem.getName().equals("..")) {
|
||||
@ -2161,14 +2279,27 @@ public class MainWindow extends JFrame {
|
||||
|
||||
if (fileNameInput != null && !fileNameInput.trim().isEmpty()) {
|
||||
final String fileName = fileNameInput.trim();
|
||||
performFileOperation((callback) -> {
|
||||
FileOperations.createFile(activePanel.getCurrentDirectory(), fileName);
|
||||
}, "File created", false, () -> {
|
||||
if (activePanel != null && activePanel.getCurrentTab() != null) {
|
||||
activePanel.getCurrentTab().selectItem(fileName);
|
||||
editFile();
|
||||
}
|
||||
}, activePanel);
|
||||
if (currentTab != null && currentTab.isFtpTab() && currentTab.getFtpProfile() != null) {
|
||||
final FtpProfile profile = currentTab.getFtpProfile();
|
||||
final String path = currentTab.getFtpCurrentPath();
|
||||
performFileOperation((callback) -> {
|
||||
FileOperations.createFileOnFtp(profile, path, fileName);
|
||||
}, "File created", false, () -> {
|
||||
if (currentTab != null) {
|
||||
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 {
|
||||
if (activePanel != null && activePanel.getFileTable() != null) {
|
||||
activePanel.getFileTable().requestFocusInWindow();
|
||||
@ -3380,6 +3511,11 @@ public class MainWindow extends JFrame {
|
||||
if (autoRefreshTimer != null) {
|
||||
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();
|
||||
// Save window state
|
||||
config.saveWindowState(this);
|
||||
@ -3486,13 +3622,61 @@ public class MainWindow extends JFrame {
|
||||
}
|
||||
int interval = config.getAutoRefreshInterval();
|
||||
autoRefreshTimer = new Timer(interval, e -> {
|
||||
if (leftPanel != null && leftPanel.getCurrentDirectory() != null) {
|
||||
leftPanel.refresh(false);
|
||||
if (leftPanel != null) {
|
||||
FilePanelTab tab = leftPanel.getCurrentTab();
|
||||
if (tab != null && !tab.isFtpTab() && leftPanel.getCurrentDirectory() != null) {
|
||||
leftPanel.refresh(false);
|
||||
}
|
||||
}
|
||||
if (rightPanel != null && rightPanel.getCurrentDirectory() != null) {
|
||||
rightPanel.refresh(false);
|
||||
if (rightPanel != null) {
|
||||
FilePanelTab tab = rightPanel.getCurrentTab();
|
||||
if (tab != null && !tab.isFtpTab() && rightPanel.getCurrentDirectory() != null) {
|
||||
rightPanel.refresh(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user