initial commit
This commit is contained in:
commit
46f8ceccbc
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
# Maven
|
||||
target/
|
||||
pom.xml.tag
|
||||
pom.xml.releaseBackup
|
||||
pom.xml.versionsBackup
|
||||
pom.xml.next
|
||||
release.properties
|
||||
dependency-reduced-pom.xml
|
||||
buildNumber.properties
|
||||
.mvn/timing.properties
|
||||
.mvn/wrapper/maven-wrapper.jar
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
*.iml
|
||||
*.iws
|
||||
*.ipr
|
||||
.vscode/
|
||||
.classpath
|
||||
.project
|
||||
.settings/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
8
.vscode/settings.json
vendored
Normal file
8
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"java.compile.nullAnalysis.mode": "automatic",
|
||||
"java.configuration.updateBuildConfiguration": "automatic",
|
||||
"chat.tools.terminal.autoApprove": {
|
||||
"/home/kamma/java/apache-maven-3.9.11/bin/mvn": true,
|
||||
"/home/kamma/java/jdk-21.0.9+10/bin/java": true
|
||||
}
|
||||
}
|
||||
58
pom.xml
Normal file
58
pom.xml
Normal file
@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.jkeepass</groupId>
|
||||
<artifactId>jkeepass</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>2.10.1</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
<configuration>
|
||||
<source>17</source>
|
||||
<target>17</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.5.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<transformers>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<mainClass>cz.kamma.jkeepass.KeepassApp</mainClass>
|
||||
</transformer>
|
||||
</transformers>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
216
src/main/java/cz/kamma/jkeepass/CustomDatabaseFormat.java
Normal file
216
src/main/java/cz/kamma/jkeepass/CustomDatabaseFormat.java
Normal file
@ -0,0 +1,216 @@
|
||||
package cz.kamma.jkeepass;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import cz.kamma.jkeepass.model.Database;
|
||||
import cz.kamma.jkeepass.model.Entry;
|
||||
import cz.kamma.jkeepass.model.Group;
|
||||
|
||||
/**
|
||||
* Handler for custom JKP format (encrypted JSON database)
|
||||
*/
|
||||
public class CustomDatabaseFormat {
|
||||
|
||||
private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
|
||||
private static final Gson gson = new GsonBuilder().create();
|
||||
|
||||
/**
|
||||
* Internal JSON structure for serialization
|
||||
*/
|
||||
static class JkpDatabase {
|
||||
public int version = 1;
|
||||
public String salt; // Base64 encoded
|
||||
public String iv; // Base64 encoded
|
||||
public String encryptedData; // Base64 encoded
|
||||
}
|
||||
|
||||
static class JkpGroup {
|
||||
public String name;
|
||||
public List<JkpGroup> groups = new ArrayList<>();
|
||||
public List<JkpEntry> entries = new ArrayList<>();
|
||||
}
|
||||
|
||||
static class JkpEntry {
|
||||
public String title;
|
||||
public String username;
|
||||
public String password;
|
||||
public String url;
|
||||
public String notes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save database to .jkp file (encrypted JSON)
|
||||
*/
|
||||
public static void saveDatabase(File file, Database database, String password) throws Exception {
|
||||
// Serialize database to JSON
|
||||
JkpGroup rootGroup = serializeGroup(database.getRootGroup());
|
||||
String jsonData = gson.toJson(rootGroup);
|
||||
|
||||
// Generate salt and derive key
|
||||
byte[] salt = generateSalt();
|
||||
SecretKey key = deriveKey(password.toCharArray(), salt);
|
||||
|
||||
// Encrypt data
|
||||
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
|
||||
javax.crypto.spec.IvParameterSpec iv = new javax.crypto.spec.IvParameterSpec(new byte[16]);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key, iv);
|
||||
byte[] encryptedBytes = cipher.doFinal(jsonData.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// Create JKP structure
|
||||
JkpDatabase jkpDb = new JkpDatabase();
|
||||
jkpDb.salt = Base64.getEncoder().encodeToString(salt);
|
||||
jkpDb.iv = Base64.getEncoder().encodeToString(iv.getIV());
|
||||
jkpDb.encryptedData = Base64.getEncoder().encodeToString(encryptedBytes);
|
||||
|
||||
// Write to file
|
||||
String jkpJson = gson.toJson(jkpDb);
|
||||
Files.write(Path.of(file.getAbsolutePath()), jkpJson.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
/**
|
||||
* Load JKP file and convert to Database object
|
||||
* Creates an empty database structure and populates it with JKP data
|
||||
*/
|
||||
public static Database convertJkpToDatabase(File file, String password) throws Exception {
|
||||
// Load the JKP structure
|
||||
JkpGroup jkpData = loadDatabaseStructure(file, password);
|
||||
|
||||
// Create a new empty database to hold the JKP data
|
||||
Database database = new Database();
|
||||
|
||||
// Clear template data and populate with JKP data
|
||||
Group rootGroup = database.getRootGroup();
|
||||
rootGroup.setName(jkpData.name != null ? jkpData.name : "Root");
|
||||
|
||||
// Clear existing entries and groups
|
||||
for (Object entryObj : new java.util.ArrayList<>(rootGroup.getEntries())) {
|
||||
rootGroup.removeEntry((Entry) entryObj);
|
||||
}
|
||||
for (Object groupObj : new java.util.ArrayList<>(rootGroup.getGroups())) {
|
||||
rootGroup.removeGroup((Group) groupObj);
|
||||
}
|
||||
|
||||
// Populate with JKP data
|
||||
deserializeGroup(rootGroup, jkpData, database);
|
||||
|
||||
return database;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load database from .jkp file
|
||||
*/
|
||||
public static JkpGroup loadDatabaseStructure(File file, String password) throws Exception {
|
||||
// Read file
|
||||
String jkpJson = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8);
|
||||
JkpDatabase jkpDb = gson.fromJson(jkpJson, JkpDatabase.class);
|
||||
|
||||
if (jkpDb == null || jkpDb.encryptedData == null) {
|
||||
throw new IOException("Invalid JKP file format");
|
||||
}
|
||||
|
||||
// Decode salt and IV
|
||||
byte[] salt = Base64.getDecoder().decode(jkpDb.salt);
|
||||
byte[] ivBytes = Base64.getDecoder().decode(jkpDb.iv);
|
||||
byte[] encryptedBytes = Base64.getDecoder().decode(jkpDb.encryptedData);
|
||||
|
||||
// Derive key
|
||||
SecretKey key = deriveKey(password.toCharArray(), salt);
|
||||
|
||||
// Decrypt data
|
||||
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
|
||||
javax.crypto.spec.IvParameterSpec ivSpec = new javax.crypto.spec.IvParameterSpec(ivBytes);
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
|
||||
byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
|
||||
String jsonData = new String(decryptedBytes, StandardCharsets.UTF_8);
|
||||
|
||||
// Deserialize from JSON
|
||||
return gson.fromJson(jsonData, JkpGroup.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize KeePass Group to JKP Group
|
||||
*/
|
||||
private static JkpGroup serializeGroup(Group group) {
|
||||
JkpGroup jkpGroup = new JkpGroup();
|
||||
jkpGroup.name = group.getName();
|
||||
|
||||
// Serialize subgroups
|
||||
for (Object subGroupObj : group.getGroups()) {
|
||||
Group subGroup = (Group) subGroupObj;
|
||||
jkpGroup.groups.add(serializeGroup(subGroup));
|
||||
}
|
||||
|
||||
// Serialize entries
|
||||
for (Object entryObj : group.getEntries()) {
|
||||
Entry entry = (Entry) entryObj;
|
||||
JkpEntry jkpEntry = new JkpEntry();
|
||||
jkpEntry.title = entry.getTitle() != null ? entry.getTitle() : "";
|
||||
jkpEntry.username = entry.getUsername() != null ? entry.getUsername() : "";
|
||||
jkpEntry.password = entry.getPassword() != null ? entry.getPassword() : "";
|
||||
jkpEntry.url = entry.getUrl() != null ? entry.getUrl() : "";
|
||||
jkpEntry.notes = entry.getNotes() != null ? entry.getNotes() : "";
|
||||
jkpGroup.entries.add(jkpEntry);
|
||||
}
|
||||
|
||||
return jkpGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize JKP Group and populate KeePass Database
|
||||
*/
|
||||
public static void deserializeGroup(Group targetGroup, JkpGroup source, Database database) {
|
||||
// Add subgroups
|
||||
for (JkpGroup srcGroup : source.groups) {
|
||||
Group newGroup = database.newGroup(srcGroup.name);
|
||||
targetGroup.addGroup(newGroup);
|
||||
deserializeGroup(newGroup, srcGroup, database);
|
||||
}
|
||||
|
||||
// Add entries
|
||||
for (JkpEntry srcEntry : source.entries) {
|
||||
Entry newEntry = targetGroup.addEntry(database.newEntry());
|
||||
newEntry.setTitle(srcEntry.title != null ? srcEntry.title : "");
|
||||
newEntry.setUsername(srcEntry.username != null ? srcEntry.username : "");
|
||||
newEntry.setPassword(srcEntry.password != null ? srcEntry.password : "");
|
||||
newEntry.setUrl(srcEntry.url != null ? srcEntry.url : "");
|
||||
newEntry.setNotes(srcEntry.notes != null ? srcEntry.notes : "");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive encryption key from password
|
||||
*/
|
||||
private static SecretKey deriveKey(char[] password, byte[] salt) throws Exception {
|
||||
String passwordStr = new String(password);
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
md.update(salt);
|
||||
byte[] keyBytes = md.digest(passwordStr.getBytes(StandardCharsets.UTF_8));
|
||||
// Ensure exactly 32 bytes (256 bits) for AES-256
|
||||
return new SecretKeySpec(keyBytes, 0, 32, "AES");
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate random salt
|
||||
*/
|
||||
private static byte[] generateSalt() {
|
||||
SecureRandom random = new SecureRandom();
|
||||
byte[] salt = new byte[16];
|
||||
random.nextBytes(salt);
|
||||
return salt;
|
||||
}
|
||||
}
|
||||
985
src/main/java/cz/kamma/jkeepass/KeepassApp.java
Normal file
985
src/main/java/cz/kamma/jkeepass/KeepassApp.java
Normal file
@ -0,0 +1,985 @@
|
||||
package cz.kamma.jkeepass;
|
||||
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.FlowLayout;
|
||||
import java.awt.Frame;
|
||||
import java.awt.GridBagConstraints;
|
||||
import java.awt.GridBagLayout;
|
||||
import java.awt.Image;
|
||||
import java.awt.Insets;
|
||||
import java.awt.MenuItem;
|
||||
import java.awt.Point;
|
||||
import java.awt.PopupMenu;
|
||||
import java.awt.SystemTray;
|
||||
import java.awt.Toolkit;
|
||||
import java.awt.TrayIcon;
|
||||
import java.awt.datatransfer.StringSelection;
|
||||
import java.awt.event.ComponentAdapter;
|
||||
import java.awt.event.ComponentEvent;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.io.File;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.prefs.Preferences;
|
||||
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JCheckBox;
|
||||
import javax.swing.JCheckBoxMenuItem;
|
||||
import javax.swing.JDialog;
|
||||
import javax.swing.JFileChooser;
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JMenu;
|
||||
import javax.swing.JMenuBar;
|
||||
import javax.swing.JMenuItem;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JPasswordField;
|
||||
import javax.swing.JPopupMenu;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.JSplitPane;
|
||||
import javax.swing.JTable;
|
||||
import javax.swing.JTextArea;
|
||||
import javax.swing.JTextField;
|
||||
import javax.swing.JTree;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.Timer;
|
||||
import javax.swing.UIManager;
|
||||
import javax.swing.event.AncestorEvent;
|
||||
import javax.swing.event.AncestorListener;
|
||||
import javax.swing.table.DefaultTableModel;
|
||||
import javax.swing.tree.DefaultMutableTreeNode;
|
||||
import javax.swing.tree.DefaultTreeModel;
|
||||
import javax.swing.tree.TreePath;
|
||||
|
||||
import cz.kamma.jkeepass.model.Database;
|
||||
import cz.kamma.jkeepass.model.Entry;
|
||||
import cz.kamma.jkeepass.model.Group;
|
||||
|
||||
public class KeepassApp extends JFrame {
|
||||
private JTable entryTable;
|
||||
private DefaultTableModel tableModel;
|
||||
private JTextArea notesArea;
|
||||
private JLabel statusLabel;
|
||||
private JMenu recentMenu;
|
||||
private JTree groupTree;
|
||||
private DefaultTreeModel treeModel;
|
||||
private JSplitPane mainSplitPane;
|
||||
private JSplitPane rightSplitPane;
|
||||
private Database database;
|
||||
private Group currentGroup;
|
||||
private List<Entry> currentEntries = new ArrayList<>();
|
||||
private final Preferences prefs = Preferences.userNodeForPackage(KeepassApp.class);
|
||||
private static final String PREF_RECENT_LIST = "recent_database_list";
|
||||
private static final String PREF_WINDOW_X = "window_x";
|
||||
private static final String PREF_WINDOW_Y = "window_y";
|
||||
private static final String PREF_WINDOW_WIDTH = "window_width";
|
||||
private static final String PREF_WINDOW_HEIGHT = "window_height";
|
||||
private static final String PREF_MAIN_DIVIDER = "main_divider_location";
|
||||
private static final String PREF_RIGHT_DIVIDER = "right_divider_location";
|
||||
private static final int MAX_RECENT_FILES = 5;
|
||||
private File currentFile;
|
||||
private String currentPassword;
|
||||
|
||||
private TrayIcon trayIcon;
|
||||
|
||||
public KeepassApp() {
|
||||
// Set properties before anything else
|
||||
System.setProperty("sun.awt.enableExtraMouseButtons", "true");
|
||||
// Force GTK 2 or 3 for better X11 integration in Cinnamon
|
||||
System.setProperty("jdk.gtk.version", "3");
|
||||
// Ensure we don't use the old way of drawing that can conflict with XApp
|
||||
System.setProperty("sun.java2d.xrender", "true");
|
||||
|
||||
setTitle("JKeepass");
|
||||
setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
|
||||
|
||||
// Load and apply saved window position and size
|
||||
int savedX = prefs.getInt(PREF_WINDOW_X, -1);
|
||||
int savedY = prefs.getInt(PREF_WINDOW_Y, -1);
|
||||
int savedWidth = prefs.getInt(PREF_WINDOW_WIDTH, 800);
|
||||
int savedHeight = prefs.getInt(PREF_WINDOW_HEIGHT, 500);
|
||||
|
||||
if (savedX != -1 && savedY != -1) {
|
||||
setLocation(savedX, savedY);
|
||||
setSize(savedWidth, savedHeight);
|
||||
} else {
|
||||
setSize(800, 500);
|
||||
setLocationRelativeTo(null);
|
||||
}
|
||||
|
||||
addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosing(WindowEvent e) {
|
||||
saveWindowState();
|
||||
saveComponentState();
|
||||
if (SystemTray.isSupported() && trayIcon != null) {
|
||||
setVisible(false);
|
||||
} else {
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
addComponentListener(new ComponentAdapter() {
|
||||
@Override
|
||||
public void componentMoved(ComponentEvent e) {
|
||||
saveWindowState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void componentResized(ComponentEvent e) {
|
||||
saveWindowState();
|
||||
}
|
||||
});
|
||||
|
||||
loadAppIcon();
|
||||
initMenuBar();
|
||||
initComponents();
|
||||
|
||||
// Small delay to let OS/Window Manager settle before checking tray
|
||||
Timer trayTimer = new Timer(1000, e -> initSystemTray());
|
||||
trayTimer.setRepeats(false);
|
||||
trayTimer.start();
|
||||
}
|
||||
|
||||
private void loadAppIcon() {
|
||||
try {
|
||||
java.net.URL iconUrl = getClass().getResource("/icon.png");
|
||||
if (iconUrl != null) {
|
||||
Image img = Toolkit.getDefaultToolkit().getImage(iconUrl);
|
||||
setIconImage(img);
|
||||
System.out.println("Icon loaded successfully via Toolkit.");
|
||||
} else {
|
||||
System.err.println("Icon resource not found at /icon.png");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("Could not load app icon: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void initSystemTray() {
|
||||
try {
|
||||
if (!SystemTray.isSupported()) {
|
||||
return;
|
||||
}
|
||||
SystemTray tray = SystemTray.getSystemTray();
|
||||
|
||||
java.net.URL iconUrl = getClass().getResource("/icon.png");
|
||||
if (iconUrl == null) return;
|
||||
|
||||
Image image;
|
||||
try {
|
||||
// Read image directly into a BufferedImage for better compatibility with XApp applet
|
||||
image = javax.imageio.ImageIO.read(iconUrl);
|
||||
} catch (Exception e) {
|
||||
image = Toolkit.getDefaultToolkit().getImage(iconUrl);
|
||||
}
|
||||
|
||||
PopupMenu popup = new PopupMenu();
|
||||
MenuItem showItem = new MenuItem("Show JKeepass");
|
||||
MenuItem lockItem = new MenuItem("Lock Database");
|
||||
MenuItem exitItem = new MenuItem("Exit");
|
||||
|
||||
showItem.addActionListener(e -> {
|
||||
setVisible(true);
|
||||
setExtendedState(JFrame.NORMAL);
|
||||
toFront();
|
||||
});
|
||||
lockItem.addActionListener(e -> lockDatabase());
|
||||
exitItem.addActionListener(e -> System.exit(0));
|
||||
|
||||
popup.add(showItem);
|
||||
popup.add(lockItem);
|
||||
popup.addSeparator();
|
||||
popup.add(exitItem);
|
||||
|
||||
trayIcon = new TrayIcon(image, "JKeepass", popup);
|
||||
trayIcon.setToolTip("JKeepass - Password Manager");
|
||||
trayIcon.setImageAutoSize(true);
|
||||
|
||||
trayIcon.addActionListener(e -> {
|
||||
setVisible(true);
|
||||
setExtendedState(JFrame.NORMAL);
|
||||
toFront();
|
||||
});
|
||||
|
||||
tray.add(trayIcon);
|
||||
if (statusLabel != null) {
|
||||
statusLabel.setText(" Ready (SysTray active)");
|
||||
}
|
||||
setupWindowListeners();
|
||||
} catch (Exception e) {
|
||||
// Silently fail if SystemTray is not available
|
||||
trayIcon = null;
|
||||
}
|
||||
}
|
||||
|
||||
private JFrame createRestoreFrame() {
|
||||
JFrame restoreFrame = new JFrame("JKeepass - Click to Restore");
|
||||
restoreFrame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
|
||||
restoreFrame.setAlwaysOnTop(true);
|
||||
restoreFrame.setUndecorated(true);
|
||||
restoreFrame.setOpacity(0.85f);
|
||||
restoreFrame.setSize(160, 60);
|
||||
restoreFrame.setLocation(20, 20);
|
||||
restoreFrame.setType(JFrame.Type.UTILITY);
|
||||
|
||||
JButton restoreBtn = new JButton("Restore JKeepass");
|
||||
restoreBtn.setFont(restoreBtn.getFont().deriveFont(12f));
|
||||
restoreBtn.addActionListener(e -> {
|
||||
setVisible(true);
|
||||
setExtendedState(JFrame.NORMAL);
|
||||
toFront();
|
||||
requestFocus();
|
||||
restoreFrame.dispose();
|
||||
});
|
||||
|
||||
restoreFrame.add(restoreBtn);
|
||||
return restoreFrame;
|
||||
}
|
||||
|
||||
private void minimizeToCustomTray() {
|
||||
if (SystemTray.isSupported() && trayIcon != null) {
|
||||
setVisible(false);
|
||||
} else {
|
||||
// Linux Mint doesn't support SystemTray - use floating restore button
|
||||
lockDatabase();
|
||||
JFrame restoreFrame = createRestoreFrame();
|
||||
restoreFrame.setVisible(true);
|
||||
setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void setupWindowListeners() {
|
||||
for (java.awt.event.WindowListener wl : getWindowListeners()) {
|
||||
if (!(wl instanceof WindowAdapter) || wl.getClass().getName().contains("KeepassApp")) {
|
||||
removeWindowListener(wl);
|
||||
}
|
||||
}
|
||||
addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosing(WindowEvent e) {
|
||||
if (trayIcon != null) {
|
||||
setVisible(false);
|
||||
} else {
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void windowIconified(WindowEvent e) {
|
||||
if (trayIcon != null) {
|
||||
setVisible(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void initMenuBar() {
|
||||
JMenuBar menuBar = new JMenuBar();
|
||||
|
||||
// File Menu
|
||||
JMenu fileMenu = new JMenu("File");
|
||||
JMenuItem openItem = new JMenuItem("Open Database...");
|
||||
recentMenu = new JMenu("Open Recent");
|
||||
JMenuItem saveItem = new JMenuItem("Save");
|
||||
JMenuItem minimizeItem = new JMenuItem(SystemTray.isSupported() ? "Minimize to Tray" : "Minimize & Lock");
|
||||
JMenuItem lockItem = new JMenuItem("Lock Database");
|
||||
JMenuItem exitItem = new JMenuItem("Exit");
|
||||
|
||||
openItem.addActionListener(e -> openDatabase(null));
|
||||
saveItem.addActionListener(e -> saveDatabase());
|
||||
minimizeItem.addActionListener(e -> minimizeToCustomTray());
|
||||
lockItem.addActionListener(e -> lockDatabase());
|
||||
exitItem.addActionListener(e -> System.exit(0));
|
||||
|
||||
fileMenu.add(openItem);
|
||||
fileMenu.add(recentMenu);
|
||||
fileMenu.add(saveItem);
|
||||
fileMenu.addSeparator();
|
||||
fileMenu.add(lockItem);
|
||||
fileMenu.add(minimizeItem);
|
||||
fileMenu.add(exitItem);
|
||||
|
||||
updateRecentMenu();
|
||||
|
||||
// Entry Menu
|
||||
JMenu entryMenu = new JMenu("Entry");
|
||||
JMenuItem addItem = new JMenuItem("Add Entry...");
|
||||
JMenuItem editItem = new JMenuItem("Edit Entry...");
|
||||
JMenuItem delItem = new JMenuItem("Delete Entry");
|
||||
|
||||
addItem.addActionListener(e -> addEntry());
|
||||
editItem.addActionListener(e -> editEntry());
|
||||
delItem.addActionListener(e -> deleteEntry());
|
||||
|
||||
entryMenu.add(addItem);
|
||||
entryMenu.add(editItem);
|
||||
entryMenu.add(delItem);
|
||||
|
||||
// Edit Menu
|
||||
JMenu editMenu = new JMenu("Edit");
|
||||
JMenuItem copyUserItem = new JMenuItem("Copy Username");
|
||||
JMenuItem copyPassItem = new JMenuItem("Copy Password");
|
||||
|
||||
copyUserItem.addActionListener(e -> copyToClipboard(true));
|
||||
copyPassItem.addActionListener(e -> copyToClipboard(false));
|
||||
|
||||
editMenu.add(copyUserItem);
|
||||
editMenu.add(copyPassItem);
|
||||
|
||||
// View Menu
|
||||
JMenu viewMenu = new JMenu("View");
|
||||
JCheckBoxMenuItem onTopItem = new JCheckBoxMenuItem("Always on Top");
|
||||
onTopItem.addActionListener(e -> setAlwaysOnTop(onTopItem.isSelected()));
|
||||
viewMenu.add(onTopItem);
|
||||
|
||||
menuBar.add(fileMenu);
|
||||
menuBar.add(entryMenu);
|
||||
menuBar.add(editMenu);
|
||||
menuBar.add(viewMenu);
|
||||
setJMenuBar(menuBar);
|
||||
}
|
||||
|
||||
private void initComponents() {
|
||||
// Tree setup
|
||||
DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode("No database loaded");
|
||||
treeModel = new DefaultTreeModel(rootNode);
|
||||
groupTree = new JTree(treeModel);
|
||||
groupTree.addTreeSelectionListener(e -> {
|
||||
DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) groupTree.getLastSelectedPathComponent();
|
||||
if (selectedNode != null && selectedNode.getUserObject() instanceof GroupWrapper) {
|
||||
displayEntriesInGroup(((GroupWrapper) selectedNode.getUserObject()).getGroup());
|
||||
}
|
||||
});
|
||||
groupTree.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mousePressed(MouseEvent e) {
|
||||
showTreePopupMenu(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
showTreePopupMenu(e);
|
||||
}
|
||||
|
||||
private void showTreePopupMenu(MouseEvent e) {
|
||||
if (e.isPopupTrigger()) {
|
||||
TreePath path = groupTree.getPathForLocation(e.getX(), e.getY());
|
||||
if (path != null) {
|
||||
groupTree.setSelectionPath(path);
|
||||
DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) path.getLastPathComponent();
|
||||
if (selectedNode.getUserObject() instanceof GroupWrapper) {
|
||||
Group group = ((GroupWrapper) selectedNode.getUserObject()).getGroup();
|
||||
JPopupMenu popup = new JPopupMenu();
|
||||
JMenuItem addGroupItem = new JMenuItem("Add Category...");
|
||||
addGroupItem.addActionListener(al -> addCategory(group));
|
||||
popup.add(addGroupItem);
|
||||
|
||||
// Add Delete Category option (not for root group)
|
||||
if (selectedNode.getParent() != null) {
|
||||
popup.addSeparator();
|
||||
JMenuItem deleteGroupItem = new JMenuItem("Delete Category...");
|
||||
deleteGroupItem.addActionListener(al -> deleteCategory(group, selectedNode));
|
||||
popup.add(deleteGroupItem);
|
||||
}
|
||||
|
||||
popup.show(e.getComponent(), e.getX(), e.getY());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
JScrollPane treeScrollPane = new JScrollPane(groupTree);
|
||||
|
||||
// Notes area setup
|
||||
notesArea = new JTextArea(5, 20);
|
||||
notesArea.setEditable(false);
|
||||
notesArea.setLineWrap(true);
|
||||
notesArea.setWrapStyleWord(true);
|
||||
JScrollPane notesScrollPane = new JScrollPane(notesArea);
|
||||
notesScrollPane.setBorder(BorderFactory.createTitledBorder("Notes"));
|
||||
|
||||
// Table setup
|
||||
String[] columnNames = {"Title", "Username", "Password", "URL", "Notes"};
|
||||
tableModel = new DefaultTableModel(columnNames, 0) {
|
||||
@Override
|
||||
public boolean isCellEditable(int row, int column) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
entryTable = new JTable(tableModel);
|
||||
entryTable.setAutoCreateRowSorter(true);
|
||||
entryTable.getSelectionModel().addListSelectionListener(e -> {
|
||||
if (!e.getValueIsAdjusting()) {
|
||||
int selectedRow = entryTable.getSelectedRow();
|
||||
if (selectedRow >= 0) {
|
||||
int modelRow = entryTable.convertRowIndexToModel(selectedRow);
|
||||
notesArea.setText(currentEntries.get(modelRow).getNotes());
|
||||
} else {
|
||||
notesArea.setText("");
|
||||
}
|
||||
}
|
||||
});
|
||||
entryTable.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
if (e.getClickCount() == 2) {
|
||||
int row = entryTable.rowAtPoint(e.getPoint());
|
||||
int col = entryTable.columnAtPoint(e.getPoint());
|
||||
if (row >= 0 && col >= 0) {
|
||||
int modelRow = entryTable.convertRowIndexToModel(row);
|
||||
Entry entry = currentEntries.get(modelRow);
|
||||
String content = switch (col) {
|
||||
case 0 -> entry.getTitle();
|
||||
case 1 -> entry.getUsername();
|
||||
case 2 -> entry.getPassword();
|
||||
case 3 -> entry.getUrl();
|
||||
case 4 -> entry.getNotes();
|
||||
default -> "";
|
||||
};
|
||||
copyTextToClipboard(content, entryTable.getColumnName(col));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mousePressed(MouseEvent e) {
|
||||
showPopupMenu(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
showPopupMenu(e);
|
||||
}
|
||||
|
||||
private void showPopupMenu(MouseEvent e) {
|
||||
if (e.isPopupTrigger()) {
|
||||
int row = entryTable.rowAtPoint(e.getPoint());
|
||||
if (row >= 0) {
|
||||
entryTable.setRowSelectionInterval(row, row);
|
||||
}
|
||||
|
||||
JPopupMenu popup = new JPopupMenu();
|
||||
|
||||
JMenuItem addPop = new JMenuItem("Add Entry");
|
||||
JMenuItem editPop = new JMenuItem("Edit");
|
||||
JMenuItem copyPop = new JMenuItem("Copy Username");
|
||||
JMenuItem copyPassPop = new JMenuItem("Copy Password");
|
||||
JMenuItem delPop = new JMenuItem("Delete");
|
||||
|
||||
addPop.addActionListener(al -> addEntry());
|
||||
editPop.addActionListener(al -> editEntry());
|
||||
copyPop.addActionListener(al -> copyToClipboard(true));
|
||||
copyPassPop.addActionListener(al -> copyToClipboard(false));
|
||||
delPop.addActionListener(al -> deleteEntry());
|
||||
|
||||
popup.add(addPop);
|
||||
popup.add(editPop);
|
||||
popup.addSeparator();
|
||||
popup.add(copyPop);
|
||||
popup.add(copyPassPop);
|
||||
popup.addSeparator();
|
||||
popup.add(delPop);
|
||||
|
||||
popup.show(e.getComponent(), e.getX(), e.getY());
|
||||
}
|
||||
}
|
||||
});
|
||||
JScrollPane tableScrollPane = new JScrollPane(entryTable);
|
||||
|
||||
// Right side: Table and Notes
|
||||
rightSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, tableScrollPane, notesScrollPane);
|
||||
int rightDivider = prefs.getInt(PREF_RIGHT_DIVIDER, 250);
|
||||
rightSplitPane.setDividerLocation(rightDivider);
|
||||
rightSplitPane.addPropertyChangeListener(JSplitPane.DIVIDER_LOCATION_PROPERTY, e -> saveComponentState());
|
||||
|
||||
// Main Split pane
|
||||
mainSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, treeScrollPane, rightSplitPane);
|
||||
int mainDivider = prefs.getInt(PREF_MAIN_DIVIDER, 200);
|
||||
mainSplitPane.setDividerLocation(mainDivider);
|
||||
mainSplitPane.addPropertyChangeListener(JSplitPane.DIVIDER_LOCATION_PROPERTY, e -> saveComponentState());
|
||||
add(mainSplitPane, BorderLayout.CENTER);
|
||||
|
||||
// Status bar setup
|
||||
statusLabel = new JLabel(" Ready");
|
||||
statusLabel.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 5));
|
||||
|
||||
JButton lockBtn = new JButton("Lock");
|
||||
lockBtn.setFocusable(false);
|
||||
lockBtn.setMargin(new Insets(0, 5, 0, 5));
|
||||
lockBtn.addActionListener(e -> lockDatabase());
|
||||
|
||||
JPanel statusBar = new JPanel(new BorderLayout());
|
||||
statusBar.setPreferredSize(new Dimension(getWidth(), 30));
|
||||
statusBar.setBorder(BorderFactory.createEtchedBorder());
|
||||
statusBar.add(statusLabel, BorderLayout.WEST);
|
||||
statusBar.add(lockBtn, BorderLayout.EAST);
|
||||
add(statusBar, BorderLayout.PAGE_END);
|
||||
|
||||
// Tray diagnostic
|
||||
if (!SystemTray.isSupported()) {
|
||||
statusLabel.setText(" Ready (SysTray not supported - use Lock button)");
|
||||
} else if (trayIcon == null) {
|
||||
statusLabel.setText(" Ready (SysTray icon failed)");
|
||||
} else {
|
||||
statusLabel.setText(" Ready (SysTray active)");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save current window position, size, and component layout to preferences
|
||||
*/
|
||||
private void saveWindowState() {
|
||||
Point location = getLocation();
|
||||
Dimension size = getSize();
|
||||
prefs.putInt(PREF_WINDOW_X, location.x);
|
||||
prefs.putInt(PREF_WINDOW_Y, location.y);
|
||||
prefs.putInt(PREF_WINDOW_WIDTH, size.width);
|
||||
prefs.putInt(PREF_WINDOW_HEIGHT, size.height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save component layout (divider positions) to preferences
|
||||
*/
|
||||
private void saveComponentState() {
|
||||
if (mainSplitPane != null) {
|
||||
prefs.putInt(PREF_MAIN_DIVIDER, mainSplitPane.getDividerLocation());
|
||||
}
|
||||
if (rightSplitPane != null) {
|
||||
prefs.putInt(PREF_RIGHT_DIVIDER, rightSplitPane.getDividerLocation());
|
||||
}
|
||||
}
|
||||
|
||||
private void updateRecentMenu() {
|
||||
recentMenu.removeAll();
|
||||
String recentFiles = prefs.get(PREF_RECENT_LIST, "");
|
||||
if (recentFiles.isEmpty()) {
|
||||
JMenuItem noneItem = new JMenuItem("No recent files");
|
||||
noneItem.setEnabled(false);
|
||||
recentMenu.add(noneItem);
|
||||
} else {
|
||||
String[] paths = recentFiles.split(";");
|
||||
for (String path : paths) {
|
||||
if (path.isEmpty()) continue;
|
||||
JMenuItem item = new JMenuItem(path);
|
||||
item.addActionListener(e -> {
|
||||
File file = new File(path);
|
||||
if (file.exists()) {
|
||||
openDatabase(file);
|
||||
} else {
|
||||
JOptionPane.showMessageDialog(this, "File not found: " + path, "Error", JOptionPane.ERROR_MESSAGE);
|
||||
removeRecentFile(path);
|
||||
}
|
||||
});
|
||||
recentMenu.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void saveRecentFile(String path) {
|
||||
String recentFiles = prefs.get(PREF_RECENT_LIST, "");
|
||||
List<String> list = new ArrayList<>(List.of(recentFiles.split(";")));
|
||||
list.remove(""); // Clean up
|
||||
list.remove(path); // Avoid duplicates
|
||||
list.add(0, path); // Add to top
|
||||
if (list.size() > MAX_RECENT_FILES) {
|
||||
list = list.subList(0, MAX_RECENT_FILES);
|
||||
}
|
||||
prefs.put(PREF_RECENT_LIST, String.join(";", list));
|
||||
updateRecentMenu();
|
||||
}
|
||||
|
||||
private void removeRecentFile(String path) {
|
||||
String recentFiles = prefs.get(PREF_RECENT_LIST, "");
|
||||
List<String> list = new ArrayList<>(List.of(recentFiles.split(";")));
|
||||
list.remove(path);
|
||||
prefs.put(PREF_RECENT_LIST, String.join(";", list));
|
||||
updateRecentMenu();
|
||||
}
|
||||
|
||||
private void openDatabase(File selectedFile) {
|
||||
if (selectedFile == null) {
|
||||
JFileChooser fileChooser = new JFileChooser();
|
||||
fileChooser.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter("JKeepass Database (*.jkp)", "jkp"));
|
||||
if (fileChooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
|
||||
selectedFile = fileChooser.getSelectedFile();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
JPasswordField pf = new JPasswordField();
|
||||
pf.addAncestorListener(new AncestorListener() {
|
||||
@Override
|
||||
public void ancestorAdded(AncestorEvent event) {
|
||||
SwingUtilities.invokeLater(pf::requestFocusInWindow);
|
||||
}
|
||||
@Override public void ancestorRemoved(AncestorEvent event) {}
|
||||
@Override public void ancestorMoved(AncestorEvent event) {}
|
||||
});
|
||||
int okCxl = JOptionPane.showConfirmDialog(this, pf, "Enter Master Password:", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
|
||||
|
||||
if (okCxl == JOptionPane.OK_OPTION) {
|
||||
String password = new String(pf.getPassword());
|
||||
try {
|
||||
// Load JKP format
|
||||
this.database = CustomDatabaseFormat.convertJkpToDatabase(selectedFile, password);
|
||||
this.currentFile = selectedFile;
|
||||
this.currentPassword = password;
|
||||
updateTree(this.database.getRootGroup());
|
||||
saveRecentFile(selectedFile.getAbsolutePath());
|
||||
statusLabel.setText(" Database loaded: " + selectedFile.getName());
|
||||
} catch (Exception ex) {
|
||||
JOptionPane.showMessageDialog(this, "Error loading database: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void saveDatabase() {
|
||||
if (database == null || currentFile == null || currentPassword == null) {
|
||||
JOptionPane.showMessageDialog(this, "No database open to save.", "Warning", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Save as custom JKP format
|
||||
CustomDatabaseFormat.saveDatabase(currentFile, database, currentPassword);
|
||||
statusLabel.setText(" Database saved successfully.");
|
||||
} catch (Exception ex) {
|
||||
JOptionPane.showMessageDialog(this, "Error saving database: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void lockDatabase() {
|
||||
if (database == null) return;
|
||||
|
||||
// Clear sensitive data
|
||||
database = null;
|
||||
currentGroup = null;
|
||||
currentEntries.clear();
|
||||
tableModel.setRowCount(0);
|
||||
notesArea.setText("");
|
||||
treeModel.setRoot(new DefaultMutableTreeNode("Database locked"));
|
||||
|
||||
statusLabel.setText(" Database locked.");
|
||||
|
||||
// Re-open password prompt
|
||||
if (currentFile != null) {
|
||||
File toOpen = currentFile;
|
||||
// Clear current file/pass so we don't think it's still open
|
||||
currentPassword = null;
|
||||
currentFile = null;
|
||||
openDatabase(toOpen);
|
||||
}
|
||||
}
|
||||
|
||||
private void addEntry() {
|
||||
if (currentGroup == null) {
|
||||
JOptionPane.showMessageDialog(this, "Please select a folder first.", "Warning", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
EntryDialog dialog = new EntryDialog(this, "Add Entry", null);
|
||||
if (dialog.showDialog()) {
|
||||
Entry entry = currentGroup.addEntry(database.newEntry());
|
||||
updateEntryFromDialog(entry, dialog);
|
||||
displayEntriesInGroup(currentGroup);
|
||||
statusLabel.setText(" Entry added. Remember to Save.");
|
||||
}
|
||||
}
|
||||
|
||||
private void editEntry() {
|
||||
int selectedRow = entryTable.getSelectedRow();
|
||||
if (selectedRow >= 0) {
|
||||
int modelRow = entryTable.convertRowIndexToModel(selectedRow);
|
||||
Entry entry = currentEntries.get(modelRow);
|
||||
EntryDialog dialog = new EntryDialog(this, "Edit Entry", entry);
|
||||
if (dialog.showDialog()) {
|
||||
updateEntryFromDialog(entry, dialog);
|
||||
displayEntriesInGroup(currentGroup);
|
||||
statusLabel.setText(" Entry updated. Remember to Save.");
|
||||
}
|
||||
} else {
|
||||
JOptionPane.showMessageDialog(this, "Please select an entry to edit.", "Warning", JOptionPane.WARNING_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteEntry() {
|
||||
int selectedRow = entryTable.getSelectedRow();
|
||||
if (selectedRow >= 0) {
|
||||
int confirm = JOptionPane.showConfirmDialog(this, "Are you sure you want to delete this entry?", "Confirm Delete", JOptionPane.YES_NO_OPTION);
|
||||
if (confirm == JOptionPane.YES_OPTION) {
|
||||
int modelRow = entryTable.convertRowIndexToModel(selectedRow);
|
||||
Entry entry = currentEntries.get(modelRow);
|
||||
currentGroup.removeEntry(entry);
|
||||
displayEntriesInGroup(currentGroup);
|
||||
statusLabel.setText(" Entry deleted. Remember to Save.");
|
||||
}
|
||||
} else {
|
||||
JOptionPane.showMessageDialog(this, "Please select an entry to delete.", "Warning", JOptionPane.WARNING_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateEntryFromDialog(Entry entry, EntryDialog dialog) {
|
||||
entry.setTitle(dialog.getTitleText());
|
||||
entry.setUsername(dialog.getUsernameText());
|
||||
entry.setPassword(dialog.getPasswordText());
|
||||
entry.setUrl(dialog.getUrlText());
|
||||
entry.setNotes(dialog.getNotesText());
|
||||
}
|
||||
|
||||
private void addCategory(Group parentGroup) {
|
||||
String name = JOptionPane.showInputDialog(this, "Enter name for the new category:", "Add Category", JOptionPane.QUESTION_MESSAGE);
|
||||
if (name != null && !name.trim().isEmpty()) {
|
||||
parentGroup.addGroup(database.newGroup(name.trim()));
|
||||
updateTree(database.getRootGroup());
|
||||
statusLabel.setText(" Category added. Remember to Save.");
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteCategory(Group group, DefaultMutableTreeNode node) {
|
||||
// Get parent group from parent node
|
||||
DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode) node.getParent();
|
||||
if (parentNode == null || !(parentNode.getUserObject() instanceof GroupWrapper)) {
|
||||
JOptionPane.showMessageDialog(this, "Cannot delete root category.", "Error", JOptionPane.ERROR_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if category has subgroups or entries
|
||||
int subGroupCount = group.getGroups().size();
|
||||
int entryCount = group.getEntries().size();
|
||||
|
||||
String message = "Delete category \"" + group.getName() + "\"?";
|
||||
if (subGroupCount > 0) {
|
||||
message += "\nThis category has " + subGroupCount + " subcategory/categories.";
|
||||
}
|
||||
if (entryCount > 0) {
|
||||
message += "\nThis category has " + entryCount + " entry/entries.";
|
||||
}
|
||||
if (subGroupCount > 0 || entryCount > 0) {
|
||||
message += "\n\nWarning: All contents will be deleted!";
|
||||
}
|
||||
message += "\n\nThis action cannot be undone!";
|
||||
|
||||
int result = JOptionPane.showConfirmDialog(this, message, "Delete Category",
|
||||
JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
|
||||
|
||||
if (result == JOptionPane.YES_OPTION) {
|
||||
Group parentGroup = ((GroupWrapper) parentNode.getUserObject()).getGroup();
|
||||
parentGroup.removeGroup(group);
|
||||
updateTree(database.getRootGroup());
|
||||
statusLabel.setText(" Category deleted. Remember to Save.");
|
||||
}
|
||||
}
|
||||
|
||||
private void displayEntriesInGroup(Group group) {
|
||||
this.currentGroup = group;
|
||||
tableModel.setRowCount(0);
|
||||
currentEntries.clear();
|
||||
for (Object entryObj : group.getEntries()) {
|
||||
Entry entry = (Entry) entryObj;
|
||||
currentEntries.add(entry);
|
||||
tableModel.addRow(new Object[]{
|
||||
entry.getTitle(),
|
||||
entry.getUsername(),
|
||||
"********",
|
||||
entry.getUrl(),
|
||||
entry.getNotes()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dialog for adding/editing entries
|
||||
*/
|
||||
private static class EntryDialog extends JDialog {
|
||||
private final JTextField titleField = new JTextField(20);
|
||||
private final JTextField userField = new JTextField(20);
|
||||
private final JPasswordField passField = new JPasswordField(20);
|
||||
private final JTextField urlField = new JTextField(20);
|
||||
private final JTextArea notesArea = new JTextArea(5, 20);
|
||||
private boolean confirmed = false;
|
||||
|
||||
public EntryDialog(Frame owner, String title, Entry entry) {
|
||||
super(owner, title, true);
|
||||
setLayout(new BorderLayout());
|
||||
|
||||
titleField.addAncestorListener(new AncestorListener() {
|
||||
@Override
|
||||
public void ancestorAdded(AncestorEvent event) {
|
||||
SwingUtilities.invokeLater(titleField::requestFocusInWindow);
|
||||
}
|
||||
@Override public void ancestorRemoved(AncestorEvent event) {}
|
||||
@Override public void ancestorMoved(AncestorEvent event) {}
|
||||
});
|
||||
|
||||
JPanel panel = new JPanel(new GridBagLayout());
|
||||
GridBagConstraints gbc = new GridBagConstraints();
|
||||
gbc.insets = new Insets(5, 5, 5, 5);
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
|
||||
gbc.gridx = 0; gbc.gridy = 0;
|
||||
panel.add(new JLabel(" Title:"), gbc);
|
||||
gbc.gridx = 1; gbc.gridwidth = 2;
|
||||
panel.add(titleField, gbc);
|
||||
|
||||
gbc.gridx = 0; gbc.gridy = 1; gbc.gridwidth = 1;
|
||||
panel.add(new JLabel(" Username:"), gbc);
|
||||
gbc.gridx = 1; gbc.gridwidth = 2;
|
||||
panel.add(userField, gbc);
|
||||
|
||||
gbc.gridx = 0; gbc.gridy = 2; gbc.gridwidth = 1;
|
||||
panel.add(new JLabel(" Password:"), gbc);
|
||||
gbc.gridx = 1;
|
||||
panel.add(passField, gbc);
|
||||
|
||||
JPanel passBtnOptionPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
|
||||
JCheckBox showPassCheck = new JCheckBox("Show");
|
||||
showPassCheck.addActionListener(e -> {
|
||||
if (showPassCheck.isSelected()) {
|
||||
passField.setEchoChar((char) 0);
|
||||
} else {
|
||||
passField.setEchoChar('•');
|
||||
}
|
||||
});
|
||||
JButton genBtn = new JButton("Generate");
|
||||
genBtn.addActionListener(e -> passField.setText(generateRandomPassword(16)));
|
||||
passBtnOptionPanel.add(showPassCheck);
|
||||
passBtnOptionPanel.add(genBtn);
|
||||
gbc.gridx = 2;
|
||||
panel.add(passBtnOptionPanel, gbc);
|
||||
|
||||
gbc.gridx = 0; gbc.gridy = 3; gbc.gridwidth = 1;
|
||||
panel.add(new JLabel(" URL:"), gbc);
|
||||
gbc.gridx = 1; gbc.gridwidth = 2;
|
||||
panel.add(urlField, gbc);
|
||||
|
||||
if (entry != null) {
|
||||
titleField.setText(entry.getTitle());
|
||||
userField.setText(entry.getUsername());
|
||||
passField.setText(entry.getPassword());
|
||||
urlField.setText(entry.getUrl());
|
||||
notesArea.setText(entry.getNotes());
|
||||
}
|
||||
|
||||
add(panel, BorderLayout.NORTH);
|
||||
JScrollPane scrollPane = new JScrollPane(notesArea);
|
||||
scrollPane.setBorder(BorderFactory.createTitledBorder("Notes"));
|
||||
add(scrollPane, BorderLayout.CENTER);
|
||||
|
||||
JPanel btnPanel = new JPanel();
|
||||
JButton okBtn = new JButton("OK");
|
||||
JButton cancelBtn = new JButton("Cancel");
|
||||
okBtn.addActionListener(e -> { confirmed = true; setVisible(false); });
|
||||
cancelBtn.addActionListener(e -> setVisible(false));
|
||||
btnPanel.add(okBtn);
|
||||
btnPanel.add(cancelBtn);
|
||||
add(btnPanel, BorderLayout.SOUTH);
|
||||
|
||||
pack();
|
||||
setLocationRelativeTo(owner);
|
||||
}
|
||||
|
||||
public boolean showDialog() {
|
||||
setVisible(true);
|
||||
return confirmed;
|
||||
}
|
||||
|
||||
public String getTitleText() { return titleField.getText(); }
|
||||
public String getUsernameText() { return userField.getText(); }
|
||||
public String getPasswordText() { return new String(passField.getPassword()); }
|
||||
public String getUrlText() { return urlField.getText(); }
|
||||
public String getNotesText() { return notesArea.getText(); }
|
||||
|
||||
private String generateRandomPassword(int length) {
|
||||
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+";
|
||||
SecureRandom random = new SecureRandom();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < length; i++) {
|
||||
sb.append(chars.charAt(random.nextInt(chars.length())));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateTree(Group rootGroup) {
|
||||
DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode(new GroupWrapper(rootGroup));
|
||||
populateTreeRecursive(rootNode, rootGroup);
|
||||
treeModel.setRoot(rootNode);
|
||||
// Automatically select root and display its entries
|
||||
groupTree.setSelectionRow(0);
|
||||
displayEntriesInGroup(rootGroup);
|
||||
}
|
||||
|
||||
private void populateTreeRecursive(DefaultMutableTreeNode node, Group group) {
|
||||
for (Object subGroupObj : group.getGroups()) {
|
||||
Group subGroup = (Group) subGroupObj;
|
||||
DefaultMutableTreeNode childNode = new DefaultMutableTreeNode(new GroupWrapper(subGroup));
|
||||
node.add(childNode);
|
||||
populateTreeRecursive(childNode, subGroup);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class to display group name in JTree while keeping reference to Group object.
|
||||
*/
|
||||
private static class GroupWrapper {
|
||||
private final Group group;
|
||||
|
||||
public GroupWrapper(Group group) {
|
||||
this.group = group;
|
||||
}
|
||||
|
||||
public Group getGroup() {
|
||||
return group;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return group.getName();
|
||||
}
|
||||
}
|
||||
|
||||
private void copyToClipboard(boolean isUsername) {
|
||||
int selectedRow = entryTable.getSelectedRow();
|
||||
if (selectedRow >= 0) {
|
||||
int modelRow = entryTable.convertRowIndexToModel(selectedRow);
|
||||
Entry entry = currentEntries.get(modelRow);
|
||||
String text = isUsername ? entry.getUsername() : entry.getPassword();
|
||||
String label = isUsername ? "Username" : "Password";
|
||||
copyTextToClipboard(text, label);
|
||||
} else {
|
||||
JOptionPane.showMessageDialog(this, "Please select an entry first.");
|
||||
}
|
||||
}
|
||||
|
||||
private void copyTextToClipboard(String text, String label) {
|
||||
if (text != null) {
|
||||
StringSelection selection = new StringSelection(text);
|
||||
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(selection, selection);
|
||||
statusLabel.setText(" " + label + " copied to clipboard.");
|
||||
Timer timer = new Timer(3000, e -> statusLabel.setText(" Ready"));
|
||||
timer.setRepeats(false);
|
||||
timer.start();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static void main(String[] args) {
|
||||
try {
|
||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
new KeepassApp().setVisible(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
24
src/main/java/cz/kamma/jkeepass/model/Database.java
Normal file
24
src/main/java/cz/kamma/jkeepass/model/Database.java
Normal file
@ -0,0 +1,24 @@
|
||||
package cz.kamma.jkeepass.model;
|
||||
|
||||
/**
|
||||
* Custom Database implementation (replaces KeePassJava2 dependency)
|
||||
*/
|
||||
public class Database {
|
||||
private Group rootGroup;
|
||||
|
||||
public Database() {
|
||||
this.rootGroup = new Group("Root");
|
||||
}
|
||||
|
||||
public Group getRootGroup() {
|
||||
return rootGroup;
|
||||
}
|
||||
|
||||
public Entry newEntry() {
|
||||
return new Entry();
|
||||
}
|
||||
|
||||
public Group newGroup(String name) {
|
||||
return new Group(name);
|
||||
}
|
||||
}
|
||||
52
src/main/java/cz/kamma/jkeepass/model/Entry.java
Normal file
52
src/main/java/cz/kamma/jkeepass/model/Entry.java
Normal file
@ -0,0 +1,52 @@
|
||||
package cz.kamma.jkeepass.model;
|
||||
|
||||
/**
|
||||
* Custom Entry implementation (replaces KeePassJava2 dependency)
|
||||
*/
|
||||
public class Entry {
|
||||
private String title = "";
|
||||
private String username = "";
|
||||
private String password = "";
|
||||
private String url = "";
|
||||
private String notes = "";
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title != null ? title : "";
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username != null ? username : "";
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password != null ? password : "";
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url != null ? url : "";
|
||||
}
|
||||
|
||||
public String getNotes() {
|
||||
return notes;
|
||||
}
|
||||
|
||||
public void setNotes(String notes) {
|
||||
this.notes = notes != null ? notes : "";
|
||||
}
|
||||
}
|
||||
51
src/main/java/cz/kamma/jkeepass/model/Group.java
Normal file
51
src/main/java/cz/kamma/jkeepass/model/Group.java
Normal file
@ -0,0 +1,51 @@
|
||||
package cz.kamma.jkeepass.model;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Custom Group implementation (replaces KeePassJava2 dependency)
|
||||
*/
|
||||
public class Group {
|
||||
private String name;
|
||||
private List<Group> groups = new ArrayList<>();
|
||||
private List<Entry> entries = new ArrayList<>();
|
||||
|
||||
public Group(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Collection<Group> getGroups() {
|
||||
return new ArrayList<>(groups);
|
||||
}
|
||||
|
||||
public Collection<Entry> getEntries() {
|
||||
return new ArrayList<>(entries);
|
||||
}
|
||||
|
||||
public void addGroup(Group group) {
|
||||
groups.add(group);
|
||||
}
|
||||
|
||||
public void removeGroup(Group group) {
|
||||
groups.remove(group);
|
||||
}
|
||||
|
||||
public void removeEntry(Entry entry) {
|
||||
entries.remove(entry);
|
||||
}
|
||||
|
||||
public Entry addEntry(Entry entry) {
|
||||
entries.add(entry);
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
BIN
src/main/resources/icon.png
Normal file
BIN
src/main/resources/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.6 KiB |
Loading…
x
Reference in New Issue
Block a user