diff --git a/pom.xml b/pom.xml index af690af3..193012dc 100644 --- a/pom.xml +++ b/pom.xml @@ -42,7 +42,7 @@ me.piitex.engine ren-engine - 1.0.4-SNAPSHOT + 1.0.6-SNAPSHOT org.junit.jupiter diff --git a/src/main/java/me/piitex/app/App.java b/src/main/java/me/piitex/app/App.java index 3cc4b58d..73cad8fa 100644 --- a/src/main/java/me/piitex/app/App.java +++ b/src/main/java/me/piitex/app/App.java @@ -74,12 +74,13 @@ public void preInitialization() { setupDirectories(); settings = new ServerSettings(); + appSettings = new AppSettings(); long currentPid = ProcessHandle.current().pid(); if (settings.getInfoFile().hasKey("main-pid")) { String pid = settings.getInfoFile().get("main-pid"); if (ProcessUtil.isProcessRunning(Long.parseLong(pid))) { - logger.error("Process already running!"); + logger.error("Process already running! '{}'", pid); error = true; Platform.runLater(() -> { buildErrorWindow("Process is already running!").render(); @@ -101,7 +102,6 @@ public void preInitialization() { App.logger.info("Finished pre-initialization."); loading = false; }); - appSettings = new AppSettings(); } @Override @@ -160,7 +160,7 @@ public void initialization(Stage initialStage) { // This is because the pathing for the image doesn't change but the image gets replaced by the new image. ImageLoader.useCache = false; - window = new WindowBuilder("Chat App").setIcon(new ImageLoader(new File(App.getAppDirectory(), "logo.png"))).setScale(false).setDimensions(setWidth, setHeight).build(); + window = new WindowBuilder("Chat App").setIcon(new ImageLoader(new File(App.getAppDirectory(), "logo.png"))).setScale((appSettings.isWindowScaling()) && !mobile).setAntiAliasing(false).setDimensions(setWidth, setHeight).build(); // Initialize global positions. Needed for the rendering process. Positions.initialize(); @@ -326,12 +326,22 @@ public void performUpdates() { downloader.startDownload(dataFileUrl, currentData); } - logger.info("Model list updated..."); + logger.info("Model list updated."); downloader.shutdown(); } catch (IOException e) { App.logger.error("Failed to fetch download size."); } +// TODO: Automatically update llamacpp +// try { +// GitHubUtil gitHubUtil = new GitHubUtil("https://api.github.com/repos/ggerganov/llama.cpp/"); +// FileDownloader llamaDownloader = gitHubUtil.downloadAsset(gitHubUtil.getReleaseAsset(gitHubUtil.getLatestReleaseID(), "llama-[a-zA-Z0-9]+-bin-win-cuda-12\\.4-x64\\.zip").getInt("id"), new File("output/download.zip")); +// +// } catch (IOException e) { +// throw new RuntimeException(e); +// } + + } public AppSettings getAppSettings() { @@ -373,7 +383,7 @@ public Map getUserTemplates() { public Window buildErrorWindow(String message) { if (window != null) { - window.close(); + window.close(true); } Application.setUserAgentStylesheet(new PrimerDark().getUserAgentStylesheet()); @@ -400,6 +410,8 @@ public Window buildErrorWindow(String message) { App.logger.info("Forcefully killing old process."); ProcessUtil.terminateProcess(Long.parseLong(settings.getInfoFile().get("main-pid"))); } + + appSettings.getInfoFile().set("main-pid", ""); }); window.getStage().setOnCloseRequest(event -> { diff --git a/src/main/java/me/piitex/app/backend/Character.java b/src/main/java/me/piitex/app/backend/Character.java index 6cec8579..a861a437 100644 --- a/src/main/java/me/piitex/app/backend/Character.java +++ b/src/main/java/me/piitex/app/backend/Character.java @@ -4,7 +4,7 @@ import me.piitex.app.App; import me.piitex.app.configuration.ModelSettings; import me.piitex.app.views.chats.ChatView; -import me.piitex.engine.LimitedHashMap; +import me.piitex.engine.maps.LimitedHashMap; import me.piitex.os.configurations.InfoFile; import java.io.File; diff --git a/src/main/java/me/piitex/app/backend/Chat.java b/src/main/java/me/piitex/app/backend/Chat.java index 976c0f8e..65390df8 100644 --- a/src/main/java/me/piitex/app/backend/Chat.java +++ b/src/main/java/me/piitex/app/backend/Chat.java @@ -1,7 +1,6 @@ package me.piitex.app.backend; import me.piitex.app.App; -import me.piitex.app.views.chats.ChatView; import me.piitex.os.configurations.FileCrypter; import javax.crypto.IllegalBlockSizeException; @@ -17,7 +16,6 @@ public class Chat { private Response response; private final LinkedList messages = new LinkedList<>(); private final boolean dev = false; - private ChatView cachedView; public Chat(File file) { @@ -240,12 +238,4 @@ public Response getResponse() { public void setResponse(Response response) { this.response = response; } - - public ChatView getCachedView() { - return cachedView; - } - - public void setCachedView(ChatView cachedView) { - this.cachedView = cachedView; - } } diff --git a/src/main/java/me/piitex/app/backend/server/DeviceProcess.java b/src/main/java/me/piitex/app/backend/server/DeviceProcess.java index 1ee3d5c4..b156b3c4 100644 --- a/src/main/java/me/piitex/app/backend/server/DeviceProcess.java +++ b/src/main/java/me/piitex/app/backend/server/DeviceProcess.java @@ -7,6 +7,7 @@ import java.io.IOException; import java.nio.file.Files; import java.util.LinkedList; +import java.util.List; public class DeviceProcess { private final Process process; @@ -37,12 +38,11 @@ public DeviceProcess(String backend) throws IOException { } public void handleOutput() { - App.logger.info("Handling input..."); try { LinkedList lines = new LinkedList<>(Files.readAllLines(new File(App.getDataDirectory(), "devices.txt").toPath())); lines.removeFirst(); App.getInstance().getSettings().setDevices(lines); - + App.logger.info("Devices: {}", List.of(lines)); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/src/main/java/me/piitex/app/configuration/AppSettings.java b/src/main/java/me/piitex/app/configuration/AppSettings.java index 32025325..51779792 100644 --- a/src/main/java/me/piitex/app/configuration/AppSettings.java +++ b/src/main/java/me/piitex/app/configuration/AppSettings.java @@ -16,6 +16,7 @@ public class AppSettings { private String textColor; private String quoteColor; private String astrixColor; + private boolean windowScaling = false; private final InfoFile infoFile; @@ -55,6 +56,9 @@ public AppSettings() { } else { this.astrixColor = Color.DODGERBLUE.toString(); } + if (infoFile.hasKey("window-scaling")) { + this.windowScaling = infoFile.getBoolean("window-scaling"); + } } public int getWidth() { @@ -142,10 +146,19 @@ public void setAstrixColor(String astrixColor) { infoFile.set("astrix-color", astrixColor); } + public boolean isWindowScaling() { + return windowScaling; + } + + public void setWindowScaling(boolean windowScaling) { + this.windowScaling = windowScaling; + infoFile.set("window-scaling", windowScaling); + } + /* - Utility functions for getting theme coloring. - Needed for RichTextFX components and BBCode - */ + Utility functions for getting theme coloring. + Needed for RichTextFX components and BBCode + */ public Theme getStyleTheme(String name) { if (name.equalsIgnoreCase("primer light")) { return new PrimerLight(); diff --git a/src/main/java/me/piitex/app/utils/CharacterCardImporter.java b/src/main/java/me/piitex/app/utils/CharacterCardImporter.java index 909f4303..ae859108 100644 --- a/src/main/java/me/piitex/app/utils/CharacterCardImporter.java +++ b/src/main/java/me/piitex/app/utils/CharacterCardImporter.java @@ -201,6 +201,22 @@ public static String getChatScenario(JSONObject metaData) throws JSONException { return ""; } + public static String getUserDisplay(JSONObject metaData) { + JSONObject characterJson = getCharacterJson(metaData); + if (characterJson.has("userDisplay")) { + return characterJson.getString("userDisplay"); + } + return null; + } + + public static String getUserPersona(JSONObject metaData) { + JSONObject characterJson = getCharacterJson(metaData); + if (characterJson.has("userPersona")) { + return characterJson.getString("userPersona"); + } + return null; + } + public static Map getLoreItems(JSONObject metaData) throws JSONException { JSONObject characterJson = getCharacterJson(metaData); Map map = new HashMap<>(); diff --git a/src/main/java/me/piitex/app/utils/ImageCardExporter.java b/src/main/java/me/piitex/app/utils/ImageCardExporter.java new file mode 100644 index 00000000..224fa06e --- /dev/null +++ b/src/main/java/me/piitex/app/utils/ImageCardExporter.java @@ -0,0 +1,183 @@ +package me.piitex.app.utils; + +import me.piitex.app.App; +import me.piitex.app.backend.Character; +import me.piitex.app.backend.User; +import org.json.JSONArray; +import org.json.JSONObject; + +import javax.imageio.*; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataNode; +import javax.imageio.stream.ImageOutputStream; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.Base64; +import java.util.Iterator; + +public class ImageCardExporter { + + public static void exportCharacter(Character character, File output) throws IOException { + + Files.copy(new File(character.getIconPath()).toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING); + + BufferedImage image = ImageIO.read(output); + + Iterator writers = ImageIO.getImageWritersByFormatName("png"); + if (!writers.hasNext()) { + App.logger.error("Could not find png writer for exporter.", new RuntimeException()); + return; + } + ImageWriter writer = writers.next(); + + // Prepare ImageWriteParam and IIOMetadata + ImageWriteParam writeParam = writer.getDefaultWriteParam(); + ImageTypeSpecifier typeSpecifier = ImageTypeSpecifier.createFromRenderedImage(image); + IIOMetadata metadata = writer.getDefaultImageMetadata(typeSpecifier, writeParam); + String nativeFormat = metadata.getNativeMetadataFormatName(); + + // Create the native metadata tree + IIOMetadataNode root = (IIOMetadataNode) metadata.getAsTree(nativeFormat); + IIOMetadataNode textNode = null; + + + // Check if a tEXt node already exists + for (int i = 0; i < root.getChildNodes().getLength(); i++) { + if (root.getChildNodes().item(i).getNodeName().equals("tEXt")) { + textNode = (IIOMetadataNode) root.getChildNodes().item(i); + break; + } + } + + // If not, create a new one and append it to the root + if (textNode == null) { + textNode = new IIOMetadataNode("tEXt"); + root.appendChild(textNode); + } + + IIOMetadataNode textEntry = new IIOMetadataNode("tEXtEntry"); + textEntry.setAttribute("keyword", "chara"); + textEntry.setAttribute("value", Base64.getEncoder().encodeToString(toJson(character).toString().getBytes(StandardCharsets.UTF_8))); + textNode.appendChild(textEntry); + + + metadata.setFromTree(nativeFormat, root); + + try (ImageOutputStream stream = ImageIO.createImageOutputStream(output)) { + writer.setOutput(stream); + writer.write(new IIOImage(image, null, metadata)); + } finally { + writer.dispose(); + } + } + + public static void exportUser(User user, File output) throws IOException { + + Files.copy(new File(user.getIconPath()).toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING); + + BufferedImage image = ImageIO.read(output); + + Iterator writers = ImageIO.getImageWritersByFormatName("png"); + if (!writers.hasNext()) { + App.logger.error("Could not find png writer for exporter.", new RuntimeException()); + return; + } + ImageWriter writer = writers.next(); + + // Prepare ImageWriteParam and IIOMetadata + ImageWriteParam writeParam = writer.getDefaultWriteParam(); + ImageTypeSpecifier typeSpecifier = ImageTypeSpecifier.createFromRenderedImage(image); + IIOMetadata metadata = writer.getDefaultImageMetadata(typeSpecifier, writeParam); + String nativeFormat = metadata.getNativeMetadataFormatName(); + + // Create the native metadata tree + IIOMetadataNode root = (IIOMetadataNode) metadata.getAsTree(nativeFormat); + IIOMetadataNode textNode = null; + + + // Check if a tEXt node already exists + for (int i = 0; i < root.getChildNodes().getLength(); i++) { + if (root.getChildNodes().item(i).getNodeName().equals("tEXt")) { + textNode = (IIOMetadataNode) root.getChildNodes().item(i); + break; + } + } + + // If not, create a new one and append it to the root + if (textNode == null) { + textNode = new IIOMetadataNode("tEXt"); + root.appendChild(textNode); + } + + IIOMetadataNode textEntry = new IIOMetadataNode("tEXtEntry"); + textEntry.setAttribute("keyword", "user"); + textEntry.setAttribute("value", Base64.getEncoder().encodeToString(toJson(user).toString().getBytes(StandardCharsets.UTF_8))); + textNode.appendChild(textEntry); + + + metadata.setFromTree(nativeFormat, root); + + try (ImageOutputStream stream = ImageIO.createImageOutputStream(output)) { + writer.setOutput(stream); + writer.write(new IIOImage(image, null, metadata)); + } finally { + writer.dispose(); + } + } + + public static JSONObject toJson(Character character) { + JSONObject root = new JSONObject(); + + JSONObject characterRoot = new JSONObject(); + characterRoot.put("basePrompt", App.getInstance().getSettings().getGlobalModel().getSettings().getModelInstructions()); + characterRoot.put("customDialogue", ""); + characterRoot.put("firstMessage", character.getFirstMessage()); + characterRoot.put("scenario", character.getChatScenario()); + characterRoot.put("aiDisplayName", character.getDisplayName()); + characterRoot.put("aiName", character.getId()); + characterRoot.put("aiPersona", character.getPersona()); + + JSONArray loreRoot = new JSONArray(); + + for (String loreId : character.getLorebook().keySet()) { + String loreValue = character.getLorebook().get(loreId); + JSONObject loreEntry = new JSONObject(); + loreEntry.put("id", loreId); + loreEntry.put("key", loreId); + loreEntry.put("value", loreValue); + loreRoot.put(loreEntry); + } + characterRoot.put("loreItems", loreRoot); + root.put("character", characterRoot); + + return root; + } + + public static JSONObject toJson(User user) { + JSONObject root = new JSONObject(); + + JSONObject characterRoot = new JSONObject(); + characterRoot.put("userDisplay", user.getDisplayName()); + characterRoot.put("userPersona", user.getPersona()); + + JSONArray loreRoot = new JSONArray(); + + for (String loreId : user.getLorebook().keySet()) { + String loreValue = user.getLorebook().get(loreId); + JSONObject loreEntry = new JSONObject(); + loreEntry.put("id", loreId); + loreEntry.put("key", loreId); + loreEntry.put("value", loreValue); + loreRoot.put(loreEntry); + } + characterRoot.put("loreItems", loreRoot); + root.put("user", characterRoot); + + return root; + } + +} diff --git a/src/main/java/me/piitex/app/utils/UserCardImporter.java b/src/main/java/me/piitex/app/utils/UserCardImporter.java new file mode 100644 index 00000000..1f06ee36 --- /dev/null +++ b/src/main/java/me/piitex/app/utils/UserCardImporter.java @@ -0,0 +1,94 @@ +package me.piitex.app.utils; + +import com.drew.imaging.ImageMetadataReader; +import com.drew.imaging.ImageProcessingException; +import com.drew.metadata.Directory; +import com.drew.metadata.Metadata; +import com.drew.metadata.Tag; +import me.piitex.app.App; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.IOException; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +public class UserCardImporter { + public static JSONObject getImageMetaData(File file) throws ImageProcessingException, IOException { + App.logger.info("Gathering card data..."); + Metadata metadata = ImageMetadataReader.readMetadata(file); + JSONObject toReturn = null; + + + for (Directory directory : metadata.getDirectories()) { + for (Tag tag : directory.getTags()) { + // Slightly easier with Silly Tavern. They all start with Chara: for the metadata. + if (tag.getDescription().startsWith("user")) { + // Remove the beginning "chara: " so it follows the json scheme. + String data = tag.getDescription().replace("user: ", ""); + try { + data = new String(Base64.getDecoder().decode(data)); + } catch (IllegalArgumentException ignored) { + // Thrown if the user entry cannot be decoded into base64. + // This happens if there is multiple user entries which is possible. + // Go to next entry to loop. + continue; + } + try { + toReturn = new JSONObject(data); + return toReturn; + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + } + } + return null; + } + + private static JSONObject getUserJson(JSONObject metaData) throws JSONException { + if (metaData.has("user")) { + return metaData.getJSONObject("user"); + } + // Can process other structures + + return metaData; + } + + public static String getUserDisplay(JSONObject metaData) { + JSONObject userJson = getUserJson(metaData); + if (userJson.has("userDisplay")) { + return userJson.getString("userDisplay"); + } + return null; + } + + public static String getUserPersona(JSONObject metaData) { + JSONObject userJson = getUserJson(metaData); + if (userJson.has("userPersona")) { + return userJson.getString("userPersona"); + } + return null; + } + + public static Map getLoreItems(JSONObject metaData) throws JSONException { + JSONObject userJson = getUserJson(metaData); + Map map = new HashMap<>(); + if (userJson.has("loreItems")) { + + JSONArray array = userJson.getJSONArray("loreItems"); + for (int i = 0; i < array.length(); i++) { + JSONObject object = array.getJSONObject(i); + String key = object.getString("key"); + String value = object.getString("value"); + map.put(key, value); + } + + return map; + } + return map; + } +} diff --git a/src/main/java/me/piitex/app/views/characters/CharacterEditView.java b/src/main/java/me/piitex/app/views/characters/CharacterEditView.java index 5544bd78..7b24d46e 100644 --- a/src/main/java/me/piitex/app/views/characters/CharacterEditView.java +++ b/src/main/java/me/piitex/app/views/characters/CharacterEditView.java @@ -340,9 +340,11 @@ public HorizontalLayout buildSubmitBox() { characterSpecificUser.setPersona(userPersona); if (userIconPath != null && userIconPath.exists()) { - File output = new File(currentCharacterInstance.getUserDirectory(), userIconPath.getName()); - Files.copy(userIconPath.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING); - characterSpecificUser.setIconPath(output.getAbsolutePath()); + File output = new File(character.getUserDirectory(), userIconPath.getName()); + if (!userIconPath.toPath().equals(output.toPath())) { + Files.copy(userIconPath.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING); + characterSpecificUser.setIconPath(output.getAbsolutePath()); + } } character.setUser(characterSpecificUser); diff --git a/src/main/java/me/piitex/app/views/characters/CharactersView.java b/src/main/java/me/piitex/app/views/characters/CharactersView.java index c8635f93..951c5764 100644 --- a/src/main/java/me/piitex/app/views/characters/CharactersView.java +++ b/src/main/java/me/piitex/app/views/characters/CharactersView.java @@ -19,6 +19,7 @@ import me.piitex.engine.layouts.FlowLayout; import me.piitex.engine.layouts.HorizontalLayout; import me.piitex.engine.layouts.VerticalLayout; +import me.piitex.engine.loaders.ImageLoader; import me.piitex.engine.overlays.*; import org.apache.commons.io.FileUtils; import org.kordamp.ikonli.javafx.FontIcon; @@ -255,7 +256,9 @@ private void deleteCharacter(FlowLayout base, CardContainer card, Character char // Add a buffer to ensure image resources are disposed. App.getThreadPoolManager().submitSchedule(() -> { try { - App.logger.info("Deleting: {}", character.getId()); + App.logger.info("Removing image from cache '{}'", character.getIconPath()); + ImageLoader.imageCache.remove(character.getIconPath()); // Clear image from cache. + App.logger.info("Deleting Character: {}", character.getId()); FileUtils.deleteDirectory(character.getCharacterDirectory()); } catch (IOException e) { App.logger.error("Could not delete directory!", e); diff --git a/src/main/java/me/piitex/app/views/characters/tabs/CharacterTab.java b/src/main/java/me/piitex/app/views/characters/tabs/CharacterTab.java index 30f49c99..71dd3744 100644 --- a/src/main/java/me/piitex/app/views/characters/tabs/CharacterTab.java +++ b/src/main/java/me/piitex/app/views/characters/tabs/CharacterTab.java @@ -12,6 +12,7 @@ import me.piitex.app.App; import me.piitex.app.backend.User; import me.piitex.app.configuration.AppSettings; +import me.piitex.app.utils.ImageCardExporter; import me.piitex.app.utils.CharacterCardImporter; import me.piitex.app.views.characters.CharacterEditView; import me.piitex.engine.containers.CardContainer; @@ -85,7 +86,6 @@ private void buildCharacterTabContent(@Nullable Character character, boolean dup charDescription.addStyle(Styles.TEXT_ON_EMPHASIS); rootLayout.addElement(charDescription); - //rootLayout.addElement(buildExampleDialogue()); this.addElement(parentView.buildSubmitBox()); @@ -177,9 +177,11 @@ private VerticalLayout buildCharacterInput(@Nullable Character character, boolea importCard.setWidth(200); importCard.setHeight(50); - FileChooserOverlay fileSelector = new FileChooserOverlay(App.window, importCard); - root.addElement(fileSelector); - fileSelector.onFileSelect(event -> { + FileChooserOverlay importSelector = new FileChooserOverlay(App.window, importCard); + importSelector.setText("Import character card."); + importSelector.setFileExtensions(new String[]{"*.png"}); + root.addElement(importSelector); + importSelector.onFileSelect(event -> { File file = event.getDirectory(); try { JSONObject metadata = CharacterCardImporter.getImageMetaData(file); @@ -194,6 +196,15 @@ private VerticalLayout buildCharacterInput(@Nullable Character character, boolea parentView.getChatTabInstance().getFirstMessageInput().setCurrentText(CharacterCardImporter.getFirstMessage(metadata)); parentView.getChatTabInstance().getChatScenarioInput().setCurrentText(CharacterCardImporter.getChatScenario(metadata)); + String userDisplay = CharacterCardImporter.getUserDisplay(metadata); + if (userDisplay != null) { + parentView.getUserTabInstance().getUserDisplayNameInput().setCurrentText(userDisplay); + } + String userPersona = CharacterCardImporter.getUserPersona(metadata); + if (userPersona != null) { + parentView.getUserTabInstance().getUserDescription().setCurrentText(userPersona); + } + parentView.setCharacterIconPath(file); parentView.getInfoFile().set("icon-path", file.getAbsolutePath()); image.setImage(new ImageLoader(file)); @@ -211,6 +222,34 @@ private VerticalLayout buildCharacterInput(@Nullable Character character, boolea } }); + ButtonOverlay exportCard = new ButtonBuilder("import").setText("Export Character Card").build(); + if (character == null) { + exportCard.setEnabled(false); + } + exportCard.addStyle(Styles.ACCENT); + exportCard.addStyle(Styles.BUTTON_OUTLINED); + exportCard.setWidth(200); + exportCard.setHeight(50); + + FileChooserOverlay exportSelector = new FileChooserOverlay(App.window, exportCard); + exportSelector.setText("Export character card as."); + exportSelector.setFileExtensions(new String[]{"*.png"}); + root.addElement(exportSelector); + exportSelector.onFileSelect(event -> { + File file = event.getDirectory(); + try { + // Character cannot be null as the button is disabled if it is. + ImageCardExporter.exportCharacter(character, file); + } catch (IOException e) { + App.logger.error("Error importing character card: ", e); + Platform.runLater(() -> { + MessageOverlay errorOverlay = new MessageOverlay(0, 0, 500, 50, "Import Failed", "Could not import character card: " + e.getMessage()); + errorOverlay.addStyle(Styles.DANGER); + errorOverlay.addStyle(Styles.BG_DEFAULT); + App.window.renderPopup(errorOverlay, 650, 870, 500, 50, false, null); + }); + } + }); return root; } diff --git a/src/main/java/me/piitex/app/views/characters/tabs/UserTab.java b/src/main/java/me/piitex/app/views/characters/tabs/UserTab.java index 669b8e2c..880eff1e 100644 --- a/src/main/java/me/piitex/app/views/characters/tabs/UserTab.java +++ b/src/main/java/me/piitex/app/views/characters/tabs/UserTab.java @@ -1,11 +1,15 @@ package me.piitex.app.views.characters.tabs; import atlantafx.base.theme.Styles; +import com.drew.imaging.ImageProcessingException; +import javafx.application.Platform; import javafx.geometry.Pos; import javafx.stage.FileChooser; import me.piitex.app.App; import me.piitex.app.backend.User; import me.piitex.app.configuration.AppSettings; +import me.piitex.app.utils.ImageCardExporter; +import me.piitex.app.utils.UserCardImporter; import me.piitex.os.configurations.InfoFile; import me.piitex.app.views.characters.CharacterEditView; import me.piitex.engine.containers.CardContainer; @@ -15,8 +19,10 @@ import me.piitex.engine.layouts.VerticalLayout; import me.piitex.engine.loaders.ImageLoader; import me.piitex.engine.overlays.*; +import org.json.JSONObject; import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -78,10 +84,9 @@ private void buildUserTabContent() { userDescription.addStyle(Styles.BG_DEFAULT); userDescription.addStyle(appSettings.getChatTextSize()); userDescription.addStyle(Styles.TEXT_ON_EMPHASIS); - rootLayout.addElement(userDescription); - this.addElement(parentView.buildSubmitBox()); + addElement(parentView.buildSubmitBox()); } private CardContainer buildUserDisplay() { @@ -186,11 +191,87 @@ private VerticalLayout buildUserInput() { parentView.setUserIconPath(new File(template.getIconPath())); image.setImage(new ImageLoader(parentView.getUserIconPath())); } + + if (parentView.getUser() != null) { + parentView.getUser().getLorebook().keySet().forEach(s -> parentView.getLoreBookTabInstance().getItems().remove(s)); + } + + parentView.getLoreBookTabInstance().getItems().putAll(template.getLorebook()); + parentView.getLoreBookTabInstance().buildLorebookTabContent(); } parentView.updateInfoData(); }); + ButtonOverlay importCard = new ButtonBuilder("import").setText("Import User Card").build(); + importCard.addStyle(Styles.ACCENT); + importCard.addStyle(Styles.BUTTON_OUTLINED); + importCard.setWidth(200); + importCard.setHeight(50); + + FileChooserOverlay fileSelector = new FileChooserOverlay(App.window, importCard); + fileSelector.setText("Import user card."); + fileSelector.setFileExtensions(new String[]{"*.png"}); + root.addElement(fileSelector); + fileSelector.onFileSelect(event -> { + File file = event.getDirectory(); + try { + JSONObject metadata = UserCardImporter.getImageMetaData(file); + userDisplayNameInput.setCurrentText(UserCardImporter.getUserDisplay(metadata)); + userDescription.setCurrentText(UserCardImporter.getUserPersona(metadata)); + + if (parentView.getUser() != null) { + parentView.getUser().getLorebook().keySet().forEach(s -> parentView.getLoreBookTabInstance().getItems().remove(s)); + } + parentView.getLoreBookTabInstance().getItems().putAll(UserCardImporter.getLoreItems(metadata)); + parentView.getLoreBookTabInstance().buildLorebookTabContent(); + + parentView.setUserIconPath(file); + parentView.getInfoFile().set("icon-path-user", file.getAbsolutePath()); + image.setImage(new ImageLoader(file)); + + parentView.updateInfoData(); + + } catch (ImageProcessingException | IOException e) { + App.logger.error("Error importing user card: ", e); + Platform.runLater(() -> { + MessageOverlay errorOverlay = new MessageOverlay(0, 0, 500, 50, "Import Failed", "Could not import user card: " + e.getMessage()); + errorOverlay.addStyle(Styles.DANGER); + errorOverlay.addStyle(Styles.BG_DEFAULT); + App.window.renderPopup(errorOverlay, 650, 870, 500, 50, false, null); + }); + } + }); + + ButtonOverlay exportCard = new ButtonBuilder("export").setText("Export User Card").build(); + if (parentView.getUser() == null) { + exportCard.setEnabled(false); + } + exportCard.addStyle(Styles.ACCENT); + exportCard.addStyle(Styles.BUTTON_OUTLINED); + exportCard.setWidth(200); + exportCard.setHeight(50); + + FileChooserOverlay exportSelector = new FileChooserOverlay(App.window, exportCard); + exportSelector.setText("Export user card as."); + exportSelector.setFileExtensions(new String[]{"*.png"}); + root.addElement(exportSelector); + exportSelector.onFileSelect(event -> { + File file = event.getDirectory(); + try { + // User cannot be null as the button is disabled if it is. + ImageCardExporter.exportUser(parentView.getUser(), file); + } catch (IOException e) { + App.logger.error("Error importing character card: ", e); + Platform.runLater(() -> { + MessageOverlay errorOverlay = new MessageOverlay(0, 0, 500, 50, "Import Failed", "Could not import user card: " + e.getMessage()); + errorOverlay.addStyle(Styles.DANGER); + errorOverlay.addStyle(Styles.BG_DEFAULT); + App.window.renderPopup(errorOverlay, 650, 870, 500, 50, false, null); + }); + } + }); + return root; } diff --git a/src/main/java/me/piitex/app/views/chats/ChatView.java b/src/main/java/me/piitex/app/views/chats/ChatView.java index b2b64af9..68fe0e9d 100644 --- a/src/main/java/me/piitex/app/views/chats/ChatView.java +++ b/src/main/java/me/piitex/app/views/chats/ChatView.java @@ -199,7 +199,7 @@ public ChoiceBoxOverlay buildSelection() { ChoiceBox choiceBox = selection.getChoiceBox(); choiceBox.getSelectionModel().clearSelection(); App.window.clearContainers(); - ChatView cachedView = character.getChatViewCachedNodes().get(chat); + ChatView cachedView = character.getChatViewCachedNodes().get(next); if (next != null && cachedView != null) { App.logger.info("Using cached selection view..."); App.window.addContainer(cachedView); @@ -207,6 +207,7 @@ public ChoiceBoxOverlay buildSelection() { App.window.clearContainers(); App.window.addContainer(new ChatView(character, next, true)); } + character.setLastChat(next); }); return selection; diff --git a/src/main/java/me/piitex/app/views/chats/components/ChatBoxCard.java b/src/main/java/me/piitex/app/views/chats/components/ChatBoxCard.java index 212dbbbb..3d7b4a3c 100644 --- a/src/main/java/me/piitex/app/views/chats/components/ChatBoxCard.java +++ b/src/main/java/me/piitex/app/views/chats/components/ChatBoxCard.java @@ -45,6 +45,7 @@ public void buildCard() { displayName = character.getUser().getDisplayName(); } else { iconPath = (character != null && character.getIconPath() != null && !character.getIconPath().isEmpty() ? character.getIconPath() : new File(App.getAppDirectory(), "icons/character.png").getAbsolutePath()); + assert character != null; // Character cannot be null at this stage. If the character is null, the error will be caught and thrown before this call. displayName = character.getDisplayName(); } diff --git a/src/main/java/me/piitex/app/views/settings/SettingsView.java b/src/main/java/me/piitex/app/views/settings/SettingsView.java index 1215f330..cebefb09 100644 --- a/src/main/java/me/piitex/app/views/settings/SettingsView.java +++ b/src/main/java/me/piitex/app/views/settings/SettingsView.java @@ -50,6 +50,7 @@ public void build() { scrollContainer.setHorizontalScroll(false); root.addElement(scrollContainer); + layout.addElement(buildWindowScaling()); layout.addElement(buildResolution()); layout.addElement(buildGlobalChatSize()); layout.addElement(buildChatSize()); @@ -59,6 +60,28 @@ public void build() { layout.addElement(buildAstrixColor()); } + public TileContainer buildWindowScaling() { + TileContainer tileContainer = new TileContainer(0, 0, appSettings.getWidth() - 300, 120); + tileContainer.setMaxSize(appSettings.getWidth() - 300, 120); + tileContainer.addStyle(Styles.BORDER_DEFAULT); + tileContainer.addStyle(Styles.BG_DEFAULT); + tileContainer.addStyle(appSettings.getGlobalTextSize()); + + tileContainer.setTitle("Window Scaling"); + tileContainer.setDescription("Enable/Disable window scaling. Can cause text to become blurry or stretched."); + + ToggleSwitchOverlay toggleSwitchOverlay = new ToggleSwitchOverlay(appSettings.isWindowScaling()); + toggleSwitchOverlay.onToggle(event -> { + App.window.clear(); + App.window.close(false); + appSettings.setWindowScaling(event.getNewValue()); + App.getInstance().initialization(App.window.getStage()); + }); + tileContainer.setAction(toggleSwitchOverlay); + + return tileContainer; + } + public TileContainer buildResolution() { TileContainer tileContainer = new TileContainer(0, 0, appSettings.getWidth() - 300, 120); tileContainer.setMaxSize(appSettings.getWidth() - 300, 120); @@ -144,6 +167,11 @@ public TileContainer buildChatSize() { item = Styles.TEXT; } appSettings.setChatTextSize(item); + + App.getInstance().getCharacters().values().forEach(character -> character.getChatViewCachedNodes().clear()); + App.window.clear(); + App.window.close(false); + App.getInstance().initialization(App.window.getStage()); }); tileContainer.setAction(selection); @@ -195,11 +223,10 @@ public TileContainer buildGlobalChatSize() { appSettings.setGlobalTextSize(item); // Refresh view to reflect changes. - container.getElements().clear(); - build(); - Pane pane = (Pane) container.getNode(); - pane.getChildren().clear(); - pane.getChildren().addAll(container.build()); + App.getInstance().getCharacters().values().forEach(character -> character.getChatViewCachedNodes().clear()); + App.window.clear(); + App.window.close(false); + App.getInstance().initialization(App.window.getStage()); }); diff --git a/src/main/java/me/piitex/app/views/users/UserEditView.java b/src/main/java/me/piitex/app/views/users/UserEditView.java index 93692352..62e805e1 100644 --- a/src/main/java/me/piitex/app/views/users/UserEditView.java +++ b/src/main/java/me/piitex/app/views/users/UserEditView.java @@ -18,7 +18,6 @@ import me.piitex.engine.overlays.ButtonBuilder; import me.piitex.engine.overlays.ButtonOverlay; import me.piitex.engine.overlays.MessageOverlay; -import me.piitex.os.configurations.InfoFile; import java.io.File; import java.io.IOException; @@ -38,6 +37,7 @@ public class UserEditView extends EmptyContainer { private TabsContainer tabsContainer; private UserTab userTab; + private UserLoreBookTab userLoreBookTab; private static final AppSettings appSettings = App.getInstance().getAppSettings(); @@ -59,6 +59,8 @@ public UserEditView(User user) { } public void init() { + addStyle(Styles.BG_INSET); + HorizontalLayout root = new HorizontalLayout(getWidth(), getHeight()); root.setMaxSize(root.getWidth(), root.getHeight()); addElement(root); @@ -76,7 +78,7 @@ public void init() { userTab = new UserTab("User", this); tabsContainer.addTab(userTab); - UserLoreBookTab userLoreBookTab = new UserLoreBookTab("Lorebook", this); + userLoreBookTab = new UserLoreBookTab("Lorebook", this); tabsContainer.addTab(userLoreBookTab); } @@ -171,6 +173,14 @@ public boolean validate() { } } + public UserTab getUserTab() { + return userTab; + } + + public UserLoreBookTab getUserLoreBookTab() { + return userLoreBookTab; + } + public User getUser() { return user; } diff --git a/src/main/java/me/piitex/app/views/users/UsersView.java b/src/main/java/me/piitex/app/views/users/UsersView.java index c9e79dae..afdd568d 100644 --- a/src/main/java/me/piitex/app/views/users/UsersView.java +++ b/src/main/java/me/piitex/app/views/users/UsersView.java @@ -2,7 +2,6 @@ import atlantafx.base.theme.Styles; import javafx.application.Platform; -import javafx.geometry.Orientation; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.control.ContextMenu; @@ -34,7 +33,7 @@ public class UsersView extends EmptyContainer { public UsersView() { super(appSettings.getWidth(), appSettings.getHeight()); - + addStyle(Styles.BG_INSET); HorizontalLayout root = new HorizontalLayout(getWidth(), getHeight()); root.setMaxSize(root.getWidth(), root.getHeight()); addElement(root); @@ -50,7 +49,8 @@ public UsersView() { } public void init() { - VerticalLayout header = new VerticalLayout(appSettings.getWidth() - 200, 200); + VerticalLayout header = new VerticalLayout(appSettings.getWidth() - 200, 100); + header.addStyle(Styles.BG_DEFAULT); header.setMaxSize(header.getWidth(), header.getHeight()); header.setAlignment(Pos.CENTER); mainPage.addElement(header); @@ -63,7 +63,6 @@ public void init() { App.window.addContainer(new UserEditView()); }); header.addElement(newUser); - header.addElement(new SeparatorOverlay(Orientation.HORIZONTAL)); buildFlowLayout(); } @@ -75,6 +74,7 @@ public void buildFlowLayout() { base.setMaxSize(base.getWidth(), base.getHeight()); FlowLayout flowLayout = new FlowLayout(appSettings.getWidth() - 200, 0); + flowLayout.setX(10); flowLayout.setMaxSize(flowLayout.getWidth(), flowLayout.getHeight()); mainPage.addElement(flowLayout); flowLayout.addStyle(Styles.BORDER_DEFAULT); diff --git a/src/main/java/me/piitex/app/views/users/tabs/UserTab.java b/src/main/java/me/piitex/app/views/users/tabs/UserTab.java index 94322570..1bb5838a 100644 --- a/src/main/java/me/piitex/app/views/users/tabs/UserTab.java +++ b/src/main/java/me/piitex/app/views/users/tabs/UserTab.java @@ -1,10 +1,13 @@ package me.piitex.app.views.users.tabs; import atlantafx.base.theme.Styles; +import com.drew.imaging.ImageProcessingException; +import javafx.application.Platform; import javafx.geometry.Pos; import javafx.stage.FileChooser; import me.piitex.app.App; import me.piitex.app.configuration.AppSettings; +import me.piitex.app.utils.UserCardImporter; import me.piitex.app.views.users.UserEditView; import me.piitex.engine.containers.CardContainer; import me.piitex.engine.containers.ScrollContainer; @@ -13,8 +16,10 @@ import me.piitex.engine.layouts.VerticalLayout; import me.piitex.engine.loaders.ImageLoader; import me.piitex.engine.overlays.*; +import org.json.JSONObject; import java.io.File; +import java.io.IOException; public class UserTab extends Tab { private final UserEditView userEditView; @@ -151,6 +156,41 @@ private VerticalLayout buildUserInput() { }); root.addElement(userDisplayNameInput); + ButtonOverlay importCard = new ButtonBuilder("import").setText("Import Character Card").build(); + if (userEditView.getUser() != null) { + importCard.setEnabled(false); + } + importCard.addStyle(Styles.ACCENT); + importCard.addStyle(Styles.BUTTON_OUTLINED); + importCard.setWidth(200); + importCard.setHeight(50); + + FileChooserOverlay fileSelector = new FileChooserOverlay(App.window, importCard); + root.addElement(fileSelector); + fileSelector.onFileSelect(event -> { + File file = event.getDirectory(); + try { + JSONObject metadata = UserCardImporter.getImageMetaData(file); + userDisplayNameInput.setCurrentText(UserCardImporter.getUserDisplay(metadata)); + userDescription.setCurrentText(UserCardImporter.getUserPersona(metadata)); + + userEditView.getLoreBook().clear(); + userEditView.getLoreBook().putAll(UserCardImporter.getLoreItems(metadata)); + userEditView.getUserLoreBookTab().buildLorebookTabContent(); + + userEditView.setUserIconPath(file); + image.setImage(new ImageLoader(file)); + } catch (ImageProcessingException | IOException e) { + App.logger.error("Error importing character card: ", e); + Platform.runLater(() -> { + MessageOverlay errorOverlay = new MessageOverlay(0, 0, 500, 50, "Import Failed", "Could not import character card: " + e.getMessage()); + errorOverlay.addStyle(Styles.DANGER); + errorOverlay.addStyle(Styles.BG_DEFAULT); + App.window.renderPopup(errorOverlay, 650, 870, 500, 50, false, null); + }); + } + }); + return root; }