diff --git a/CollectionManager/cliapp/src/main/java/cliapp/commands/cli/ExecuteCommand.java b/CollectionManager/cliapp/src/main/java/cliapp/commands/cli/ExecuteCommand.java index d454951..fb661f7 100644 --- a/CollectionManager/cliapp/src/main/java/cliapp/commands/cli/ExecuteCommand.java +++ b/CollectionManager/cliapp/src/main/java/cliapp/commands/cli/ExecuteCommand.java @@ -14,7 +14,6 @@ import cliapp.TextsManager; import cliapp.cliclient.CLIClient; -import cliapp.cliclient.UserInputPipeline; import cliapp.cliclient.exceptions.CommandNotFoundError; import cliapp.cliclient.exceptions.InlineParamsError; import commands.Command; @@ -160,6 +159,61 @@ public O askRequirement(Requirement requirement) throws Requirement } } + /** + * Prepared pipeline. + * + * Contains static requirements and dynamic requirements, + * but if dynamic requirements are not enough, it asks them from source + * pipeline. + */ + private static class PreparedPipeline implements RequirementsPipeline { + private final RequirementsPipeline sourcePipeline; + + private final Map staticRequirementsMap; + + private final List dynamicRequirements; + + private int askRequirementAttempts; + + public PreparedPipeline(RequirementsPipeline sourcePipeline, + Map staticRequirementsMap, + List dynamicRequirements, + int askRequirementAttempts) { + this.sourcePipeline = sourcePipeline; + this.staticRequirementsMap = staticRequirementsMap; + this.dynamicRequirements = dynamicRequirements; + this.askRequirementAttempts = askRequirementAttempts; + } + + /** + * Asks for the requirement and returns the value of it. + * + * @param requirement a {@link commands.requirements.Requirement} instance. + * + * @throws RequirementAskError if there's an error while asking the requirement. + * + * @return the value of the requirement. + */ + @Override + public O askRequirement(Requirement requirement) throws RequirementAskError { + try { + if (staticRequirementsMap.containsKey(requirement.getName())) { + // static requirements + return requirement.getValue((I) staticRequirementsMap.get(requirement.getName())); + } else { + // dynamic requirements + if (dynamicRequirements.isEmpty()) { + // if not enough dynamic requirements, ask them from source pipeline + return sourcePipeline.askRequirement(requirement); + } + return requirement.getValue((I) dynamicRequirements.remove(0)); + } + } catch (ValidationError e) { + throw new RequirementAskError(requirement.getName(), e); + } + } + } + /** * Extracts the list of static parameters from a given script line. * @@ -192,7 +246,8 @@ private List extractDynamicParams(String line) { * @param line the script line to execute with dynamic arguments * @throws ExecutionError if an error occurs while executing the command */ - private void executeWithDynamicParams(String line) throws ExecutionError { + private void executeWithDynamicParams(String line, RequirementsPipeline pipeline, OutputChannel output) + throws ExecutionError { // parse line for static params List params = extractStaticParams(line); String trigger = params.get(0); @@ -214,11 +269,10 @@ private void executeWithDynamicParams(String line) throws ExecutionError { // extract dynamic params List dynamicParams = extractDynamicParams(line); // init output channel and pipeline - OutputChannel output = System.out::println; - RequirementsPipeline pipeline = new DirectLoadPipeline(staticRequirementsMap, dynamicParams); + RequirementsPipeline dlPipeline = new DirectLoadPipeline(staticRequirementsMap, dynamicParams); // execute command output.putString("Execute: " + command.getName() + " ..."); - command.execute(pipeline, output); + command.execute(dlPipeline, output); } /** @@ -227,7 +281,8 @@ private void executeWithDynamicParams(String line) throws ExecutionError { * @param line the script line to execute with user input * @throws ExecutionError if an error occurs while executing the command */ - private void executeWithUserParams(String line) throws ExecutionError { + private void executeWithUserParams(String line, RequirementsPipeline pipeline, OutputChannel output) + throws ExecutionError { // parse line for static params List params = extractStaticParams(line); String trigger = params.get(0); @@ -246,21 +301,18 @@ private void executeWithUserParams(String line) throws ExecutionError { } catch (InlineParamsError e) { throw new ExecutionError(e.getMessage()); } - // init output channel and pipeline - OutputChannel output = System.out::println; - RequirementsPipeline pipeline = new UserInputPipeline( - staticRequirementsMap, - client.getAskRequirementAttempts(), - client.getUserInputSupplier()); + // execute command + RequirementsPipeline preparedPipeline = new PreparedPipeline(pipeline, staticRequirementsMap, List.of(), 3); output.putString("Execute: " + command.getName() + " ..."); - command.execute(pipeline, output); + command.execute(preparedPipeline, output); } /** * Parse one script line, resolve params type and execute command */ - private void executeScriptLine(String line) throws ExecutionError { + private void executeScriptLine(String line, RequirementsPipeline pipeline, OutputChannel output) + throws ExecutionError { if (line.startsWith("#")) { // comment return; @@ -269,10 +321,10 @@ private void executeScriptLine(String line) throws ExecutionError { return; } else if (line.matches("^.+\\{.+\\}$")) { // with direct loaded params - executeWithDynamicParams(line); + executeWithDynamicParams(line, pipeline, output); } else if (line.matches("^.+(\\{\\})?$")) { // with params from user - executeWithUserParams(line); + executeWithUserParams(line, pipeline, output); } else { // without dynamic params throw new ExecutionError(ts.t("ExecuteCommand.IncorrectLineFormat", line)); @@ -288,7 +340,8 @@ private void executeScriptLine(String line) throws ExecutionError { * @throws ExecutionError If an error occurs while executing any of the script * lines. */ - private void executeScript(Path scriptPath, String script) throws ExecutionError { + private void executeScript(Path scriptPath, String script, RequirementsPipeline pipeline, OutputChannel output) + throws ExecutionError { // update call counter Integer callCount = callCounter.getOrDefault(scriptPath, 0); callCounter.put(scriptPath, callCount + 1); @@ -303,7 +356,7 @@ private void executeScript(Path scriptPath, String script) throws ExecutionError // execute script for (String line : script.split(System.lineSeparator())) { try { - executeScriptLine(line); + executeScriptLine(line, pipeline, output); } catch (Exception e) { throw new ExecutionError( ts.t("ExecuteCommand.LineError", line, e.getMessage())); @@ -344,6 +397,6 @@ public void execute(RequirementsPipeline pipeline, OutputChannel output) throws } catch (IOException e) { throw new ExecutionError(e.getMessage(), e); } - executeScript(scriptPath, scriptContent); + executeScript(scriptPath, scriptContent, pipeline, output); } } diff --git a/CollectionManager/desktop/.gitignore b/CollectionManager/desktop/.gitignore new file mode 100644 index 0000000..989a3de --- /dev/null +++ b/CollectionManager/desktop/.gitignore @@ -0,0 +1,93 @@ +# File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig +# Created by https://www.toptal.com/developers/gitignore/api/windows,visualstudiocode,java,maven +# Edit at https://www.toptal.com/developers/gitignore?templates=windows,visualstudiocode,java,maven + +### Java ### +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +### 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 +# https://github.com/takari/maven-wrapper#usage-without-binary-jar +.mvn/wrapper/maven-wrapper.jar + +# Eclipse m2e generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +### VisualStudioCode ### +.vscode/* + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.toptal.com/developers/gitignore/api/windows,visualstudiocode,java,maven + +# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option) + diff --git a/CollectionManager/desktop/pom.xml b/CollectionManager/desktop/pom.xml new file mode 100644 index 0000000..6e48315 --- /dev/null +++ b/CollectionManager/desktop/pom.xml @@ -0,0 +1,162 @@ + + + 4.0.0 + + + collection-manager + mamsdeveloper.itmo + 1.0 + + + desktop + 1.0-SNAPSHOT + Desktop client + + + UTF-8 + UTF-8 + + + + + mamsdeveloper.itmo + commands + 1.0 + + + mamsdeveloper.itmo + adapter + 1.0 + + + mamsdeveloper.itmo + models + 1.0 + + + mamsdeveloper.itmo + humandeque + 1.0 + + + mamsdeveloper.itmo + auth + 0.2 + + + mamsdeveloper.itmo + collections-service + 2.2 + + + mamsdeveloper.itmo + textlocale + 4.0.1 + + + mamsdeveloper.itmo + cliapp + 2.6 + + + mamsdeveloper.itmo + commands + 1.0 + + + org.projectlombok + lombok + provided + + + com.formdev + flatlaf + 3.1.1 + + + com.github.jiconfont + jiconfont-swing + 1.0.0 + + + com.github.jiconfont + jiconfont-font_awesome + 4.7.0.1 + + + com.github.lgooddatepicker + LGoodDatePicker + 11.2.1 + + + + + + + src/main/resources + + + + + src/test/resources + + + + + + + maven-clean-plugin + 3.1.0 + + + maven-assembly-plugin + + + + desktop.App + + + + jar-with-dependencies + + + + + + maven-resources-plugin + 3.0.2 + + + maven-compiler-plugin + 3.8.0 + + + maven-surefire-plugin + 2.22.1 + + + maven-jar-plugin + 3.0.2 + + + maven-install-plugin + 2.5.2 + + + maven-deploy-plugin + 2.8.2 + + + + maven-site-plugin + 3.7.1 + + + maven-project-info-reports-plugin + 3.0.0 + + + + + diff --git a/CollectionManager/desktop/src/main/java/desktop/App.java b/CollectionManager/desktop/src/main/java/desktop/App.java new file mode 100644 index 0000000..90f5ff7 --- /dev/null +++ b/CollectionManager/desktop/src/main/java/desktop/App.java @@ -0,0 +1,99 @@ +package desktop; + +import java.awt.CardLayout; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; + +import com.formdev.flatlaf.themes.FlatMacDarkLaf; + +import desktop.lib.BasePage; +import desktop.pages.auth.AuthPage; +import desktop.pages.main.MainPage; +import jiconfont.icons.font_awesome.FontAwesome; +import jiconfont.swing.IconFontSwing; +import textlocale.TextLocale; +import textlocale.loader.common.ResourcesLoader; +import textlocale.text.TextPackage; + +public class App { + public static TextPackage texts; + + public static Context context = new Context(); + + private static JFrame frame; + + private static JPanel pagesPanel; + + private static CardLayout pagesLayout; + + private static Map pages = new HashMap<>(); + + public static void main(String[] args) { + initTexts(); + FlatMacDarkLaf.setup(); + IconFontSwing.register(FontAwesome.getIconFont()); + SwingUtilities.invokeLater(App::createAndShowGUI); + } + + public static void includePage(BasePage page) { + pages.put(page.getName(), page); + pagesPanel.add(page, page.getName()); + } + + public static void showPage(String name) { + var page = pages.get(name); + if (page == null) { + return; + } + + page.beforeShow(); + pagesLayout.show(pagesPanel, page.getName()); + page.afterShow(); + } + + private static void initTexts() { + try { + var codeSourceUrl = App.class.getProtectionDomain().getCodeSource().getLocation(); + var loader = new ResourcesLoader(codeSourceUrl, ".tl.json"); + texts = TextLocale.loadPackage("desktop", App.context::getLang, loader); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private static void createAndShowGUI() { + frame = new JFrame(texts.getText("texts.app.title")); + + URL imgURL = App.class.getResource("/desktop/icon.png"); + frame.setIconImage(frame.getToolkit().getImage(imgURL)); + + frame.setMinimumSize(context.getWindowSize()); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setExtendedState(JFrame.MAXIMIZED_BOTH); + + pagesLayout = new CardLayout(); + pagesPanel = new JPanel(pagesLayout); + frame.getContentPane().add(pagesPanel); + + initPages(); + + frame.setVisible(true); + + showPage(context.getRootPage()); + } + + private static void initPages() { + includePage(new AuthPage()); + includePage(new MainPage()); + } + + public static void setLang(String lang) { + context.setLang(lang); + frame.setTitle(texts.getText("texts.app.title")); + } +} diff --git a/CollectionManager/desktop/src/main/java/desktop/Context.java b/CollectionManager/desktop/src/main/java/desktop/Context.java new file mode 100644 index 0000000..b9e3a18 --- /dev/null +++ b/CollectionManager/desktop/src/main/java/desktop/Context.java @@ -0,0 +1,81 @@ +package desktop; + +import java.awt.Dimension; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.Random; + +import adapter.common.AuthAdapter; +import auth.AuthToken; +import desktop.lib.RemoteManager; +import desktop.pages.main.lang.LangItem; +import humandeque.manager.CollectionManager; +import lombok.Getter; +import lombok.Setter; +import models.Car; +import models.Coordinates; +import models.Human; +import models.Mood; + +public class Context { + @Getter + @Setter + private String rootPage = "auth"; + + @Getter + @Setter + private String username = "username"; + + @Getter + @Setter + private String lang = "en"; + + @Getter + @Setter + private Dimension windowSize = new Dimension(1600, 800); + + @Getter + @Setter + private List langs = List.of( + new LangItem("English", "en"), + new LangItem("Русский", "ru"), + new LangItem("日本語", "ja") + ); + + @Getter + @Setter + private List humans = generateRandomHumans(50); + + @Getter + private CollectionManager manager = null; + + private List generateRandomHumans(int count) { + Human[] humans = new Human[count]; + + Random random = new Random(); + + for (int i = 0; i < count; i++) { + Human human = Human.builder().id(i + 1L).name("Human" + (i + 1L)) + .coordinates(new Coordinates(random.nextFloat(), random.nextFloat())) + .creationDate(LocalDateTime.now()).realHero(random.nextBoolean()).hasToothpick(random.nextBoolean()) + .impactSpeed(random.nextDouble()).soundtrackName("Soundtrack" + (i + 1L)) + .minutesOfWaiting(random.nextFloat()).mood(Mood.values()[random.nextInt(Mood.values().length)]) + .car(new Car("Car" + (i + 1L))).build(); + + humans[i] = human; + } + + return List.of(humans); + } + + public void initCollectionManager(AuthToken token) { + var adapter = new AuthAdapter("127.0.0.1", 8000); + adapter.setToken(Optional.of(token)); + try { + manager = new RemoteManager(adapter); + } catch (Exception e) { + throw new RuntimeException(); + } + } +} diff --git a/CollectionManager/desktop/src/main/java/desktop/lib/BasePage.java b/CollectionManager/desktop/src/main/java/desktop/lib/BasePage.java new file mode 100644 index 0000000..8005fac --- /dev/null +++ b/CollectionManager/desktop/src/main/java/desktop/lib/BasePage.java @@ -0,0 +1,19 @@ +package desktop.lib; + +import javax.swing.JPanel; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public abstract class BasePage extends JPanel { + @Getter + private final String name; + + public void beforeShow() { + this.removeAll(); + }; + + public void afterShow() { + }; +} diff --git a/CollectionManager/desktop/src/main/java/desktop/lib/Config.java b/CollectionManager/desktop/src/main/java/desktop/lib/Config.java new file mode 100644 index 0000000..f295a2f --- /dev/null +++ b/CollectionManager/desktop/src/main/java/desktop/lib/Config.java @@ -0,0 +1,7 @@ +package desktop.lib; + + +public class Config { + public static String authServiceHost = "localhost"; + public static int authServicePort = 8003; +} diff --git a/CollectionManager/desktop/src/main/java/desktop/lib/RemoteManager.java b/CollectionManager/desktop/src/main/java/desktop/lib/RemoteManager.java new file mode 100644 index 0000000..7de53c1 --- /dev/null +++ b/CollectionManager/desktop/src/main/java/desktop/lib/RemoteManager.java @@ -0,0 +1,157 @@ +package desktop.lib; + +import java.io.Serializable; +import java.util.Map; + +import adapter.Adapter; +import adapter.exceptions.ReceiveResponseFailed; +import adapter.exceptions.SendRequestFailed; +import adapter.exceptions.SocketInitFailed; +import collections.service.api.StatusCodes; +import humandeque.HumanDeque; +import humandeque.TextResources.Manager.ExceptionsResources; +import humandeque.manager.CollectionManager; +import humandeque.manager.exceptions.CollectionLoadError; +import humandeque.manager.exceptions.CollectionSaveError; +import humandeque.manager.exceptions.ElementAlreadyExistsError; +import humandeque.manager.exceptions.ElementNotExistsError; +import humandeque.manager.exceptions.EmptyCollectionError; +import humandeque.manager.exceptions.ManipulationError; +import lombok.SneakyThrows; +import models.Human; +import server.responses.Response; + +/** + * That manager execute all manipulations on client and store collection in + * local csv file + */ +public class RemoteManager extends CollectionManager { + /** + * Adapter for send requests to collections service + */ + private Adapter serviceAdapter; + + /** + * Create new RemoteManager and load collection from collections service + * + * @param serviceAdapter Adapter for send requests to collections service + * @param userId user id + * @throws CollectionLoadError if collection can't be loaded + */ + public RemoteManager(Adapter serviceAdapter) throws CollectionLoadError, ManipulationError { + this.serviceAdapter = serviceAdapter; + load(); + } + + @Override + @SneakyThrows(ManipulationError.class) + public void add(Human element) throws ElementAlreadyExistsError, ManipulationError { + Map data = Map.of("human", element); + Response response = sendRequestOrFail("collections.add", data); + if (response.getCode() == StatusCodes.ELEMENT_ALREADY_EXISTS) { + throw new ElementAlreadyExistsError(element.getId()); + } else if (!response.getOk()) { + throw new ManipulationError(response.getMessage()); + } + this.collection.add(element); + } + + @Override + public void update(Human element) throws ElementNotExistsError, ManipulationError { + Map data = Map.of("human", element); + Response response = sendRequestOrFail("collections.update", data); + if (response.getCode() == StatusCodes.ELEMENT_NOT_EXISTS) { + throw new ElementNotExistsError(element.getId()); + } else if (!response.getOk()) { + throw new ManipulationError(response.getMessage()); + } + Human human = getElementById(element.getId()); + collection.remove(human); + collection.add(element); + } + + @Override + public void remove(long id) throws ElementNotExistsError, ManipulationError { + Map data = Map.of("id", id); + Response response = sendRequestOrFail("collections.remove", data); + if (response.getCode() == StatusCodes.ELEMENT_NOT_EXISTS) { + throw new ElementNotExistsError(id); + } else if (!response.getOk()) { + throw new ManipulationError(response.getMessage()); + } + Human human = getElementById(id); + collection.remove(human); + } + + @Override + public void clear() throws ManipulationError { + Map data = Map.of(); + Response response = sendRequestOrFail("collections.clear", data); + if (!response.getOk()) { + throw new ManipulationError(response.getMessage()); + } + collection.clear(); + } + + @Override + public void removeFirst() throws EmptyCollectionError, ManipulationError { + Map data = Map.of(); + Response response = sendRequestOrFail("collections.removeFirst", data); + if (!response.getOk()) { + throw new ManipulationError(response.getMessage()); + } + Human removedHuman = (Human) response.getData().get("human"); + collection.remove(removedHuman); + } + + @Override + public void removeLast() throws EmptyCollectionError, ManipulationError { + Map data = Map.of(); + Response response = sendRequestOrFail("collections.removeLast", data); + if (!response.getOk()) { + throw new ManipulationError(response.getMessage()); + } + Human removedHuman = (Human) response.getData().get("human"); + collection.remove(removedHuman); + } + + @Override + public void load() throws CollectionLoadError, ManipulationError { + Map data = Map.of(); + Response response = sendRequestOrFail("collections.get", data); + if (response.getOk()) { + HumanDeque loadedCollection = (HumanDeque) response.getData().get("collection"); + this.collection = loadedCollection; + } else { + throw new CollectionLoadError(ExceptionsResources.COLLECTION_LOAD_ERROR); + } + } + + @Override + public void save() throws CollectionSaveError, ManipulationError { + Map data = Map.of("collection", collection); + Response response = sendRequestOrFail("collections.save", data); + if (response.getCode() == StatusCodes.CANNOT_SAVE_COLLECTION) { + throw new CollectionSaveError(ExceptionsResources.COLLECTION_SAVE_ERROR); + } else if (!response.getOk()) { + throw new ManipulationError(response.getMessage()); + } + } + + /** + * Sends request to server and returns. + * If something goes wrong, throws ManipulationError. + * + * @param method name of method to call + * @param data data to send + * @return response from server + * @throws ManipulationError if something goes wrong + */ + private Response sendRequestOrFail(String method, Map data) throws ManipulationError { + try { + return serviceAdapter.triggerServer(method, data); + } catch (SocketInitFailed | SendRequestFailed | ReceiveResponseFailed e) { + throw new ManipulationError("Manipulation error"); + } + } +} diff --git a/CollectionManager/desktop/src/main/java/desktop/lib/TokenStore.java b/CollectionManager/desktop/src/main/java/desktop/lib/TokenStore.java new file mode 100644 index 0000000..bbbd760 --- /dev/null +++ b/CollectionManager/desktop/src/main/java/desktop/lib/TokenStore.java @@ -0,0 +1,47 @@ +package desktop.lib; + +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; + +import lombok.Cleanup; + +public class TokenStore { + private static Path getTokenPath() { + var userDirectory = System.getProperty("user.home"); + return Path.of(userDirectory, ".collection-manager", ".token"); + } + + public static void saveToken(String token) throws IOException { + var tokenPath = getTokenPath(); + var tokenFile = tokenPath.toFile(); + tokenFile.getParentFile().mkdirs(); + tokenFile.createNewFile(); + @Cleanup + var tokenWriter = new FileWriter(tokenFile); + tokenWriter.write(token); + } + + public static Optional getToken() throws IOException { + var tokenPath = getTokenPath(); + var tokenFile = tokenPath.toFile(); + if (!tokenFile.exists()) { + return Optional.empty(); + } + @Cleanup + var tokenReader = new java.io.FileReader(tokenFile); + var token = new StringBuilder(); + int c; + while ((c = tokenReader.read()) != -1) { + token.append((char) c); + } + return Optional.of(token.toString()); + } + + public static void deleteToken() throws IOException { + var tokenPath = getTokenPath(); + var tokenFile = tokenPath.toFile(); + tokenFile.delete(); + } +} diff --git a/CollectionManager/desktop/src/main/java/desktop/lib/components/CheckBox.java b/CollectionManager/desktop/src/main/java/desktop/lib/components/CheckBox.java new file mode 100644 index 0000000..65835d5 --- /dev/null +++ b/CollectionManager/desktop/src/main/java/desktop/lib/components/CheckBox.java @@ -0,0 +1,29 @@ +package desktop.lib.components; + +import javax.swing.JCheckBox; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import lombok.Getter; + +public class CheckBox extends JPanel { + private JLabel label; + + @Getter + private JCheckBox checkBox; + + public CheckBox(String text) { + init(text); + } + + private void init(String text) { + checkBox = new JCheckBox(); + add(checkBox); + label = new JLabel(text); + add(label); + } + + public Boolean isSelected() { + return checkBox.isSelected(); + } +} diff --git a/CollectionManager/desktop/src/main/java/desktop/lib/components/ComboBox.java b/CollectionManager/desktop/src/main/java/desktop/lib/components/ComboBox.java new file mode 100644 index 0000000..914df2b --- /dev/null +++ b/CollectionManager/desktop/src/main/java/desktop/lib/components/ComboBox.java @@ -0,0 +1,32 @@ +package desktop.lib.components; + +import java.awt.BorderLayout; + +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import lombok.Getter; + +public class ComboBox extends JPanel { + @Getter + private JComboBox comboBox; + + public ComboBox(String text, T[] items) { + init(text, items); + } + + private void init(String text, T[] items) { + setLayout(new BorderLayout(0, 5)); + + var label = new JLabel(text); + add(label, BorderLayout.NORTH); + + comboBox = new JComboBox<>(items); + add(comboBox, BorderLayout.CENTER); + } + + public T getSelectedItem() { + return (T) comboBox.getSelectedItem(); + } +} diff --git a/CollectionManager/desktop/src/main/java/desktop/lib/components/PasswordField.java b/CollectionManager/desktop/src/main/java/desktop/lib/components/PasswordField.java new file mode 100644 index 0000000..ec73414 --- /dev/null +++ b/CollectionManager/desktop/src/main/java/desktop/lib/components/PasswordField.java @@ -0,0 +1,33 @@ +package desktop.lib.components; + +import java.awt.BorderLayout; + +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JPasswordField; + +import lombok.Getter; + +public class PasswordField extends JPanel { + private JLabel label; + + @Getter + private JPasswordField passwordField; + + public PasswordField(String labelText, int columns) { + label = new JLabel(labelText); + passwordField = new JPasswordField(columns); + + this.setLayout(new BorderLayout(0, 5)); + this.add(label, BorderLayout.NORTH); + this.add(passwordField, BorderLayout.CENTER); + } + + public void setLabel(String text) { + label.setText(text); + } + + public String getText() { + return passwordField.getText(); + } +} diff --git a/CollectionManager/desktop/src/main/java/desktop/lib/components/Slider.java b/CollectionManager/desktop/src/main/java/desktop/lib/components/Slider.java new file mode 100644 index 0000000..1583d28 --- /dev/null +++ b/CollectionManager/desktop/src/main/java/desktop/lib/components/Slider.java @@ -0,0 +1,65 @@ +package desktop.lib.components; + +import java.awt.BorderLayout; + +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSlider; + +import lombok.Getter; + +public class Slider extends JPanel { + private JLabel label; + + @Getter + private JSlider slider; + + private JLabel valueLabel; + + private int from; + + private int to; + + private float ratio; + + @Getter + private float value; + + public Slider(String text, int from, int to) { + this.from = from; + this.to = to; + this.ratio = 1; + init(text); + } + + public Slider(String text, float from, float to, float ratio) { + this.from = (int) (from * ratio); + this.to = (int) (to * ratio); + this.ratio = ratio; + init(text); + } + + private void init(String text) { + setLayout(new BorderLayout(0, 5)); + + label = new JLabel(text); + add(label); + + // Slider and its value label + var sliderContainer = new JPanel(new BorderLayout()); + + // Slider + slider = new JSlider(from, to); + sliderContainer.add(slider); + slider.addChangeListener(e -> { + value = slider.getValue() / ratio; + valueLabel.setText(String.valueOf(value)); + }); + + // Value label + valueLabel = new JLabel(String.valueOf(slider.getValue() / ratio)); + sliderContainer.add(valueLabel, BorderLayout.LINE_END); + + add(sliderContainer, BorderLayout.PAGE_END); + } +} diff --git a/CollectionManager/desktop/src/main/java/desktop/lib/components/Spinner.java b/CollectionManager/desktop/src/main/java/desktop/lib/components/Spinner.java new file mode 100644 index 0000000..9fb1512 --- /dev/null +++ b/CollectionManager/desktop/src/main/java/desktop/lib/components/Spinner.java @@ -0,0 +1,54 @@ +package desktop.lib.components; + +import java.awt.BorderLayout; + +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSpinner; +import javax.swing.SpinnerNumberModel; + +import lombok.Getter; +import lombok.Setter; + +public class Spinner extends JPanel { + @Getter + private JSpinner spinner; + + @Setter + @Getter + private Double min = null; + + @Setter + @Getter + private Double max = null; + + @Setter + @Getter + private Double step = Double.valueOf(1); + + public Spinner(String text) { + init(text, Double.valueOf(0)); + } + + public Spinner(String text, Double value, Double min, Double max, Double step) { + this.min = min; + this.max = max; + this.step = step; + init(text, value); + } + + private void init(String text, Double value) { + setLayout(new BorderLayout(5, 0)); + + var label = new JLabel(text); + add(label, BorderLayout.WEST); + + spinner = new JSpinner(new SpinnerNumberModel(value, min, max, step)); + spinner.setValue(0); + add(spinner, BorderLayout.CENTER); + } + + public Double getValue() { + return Double.valueOf(spinner.getValue().toString()); + } +} diff --git a/CollectionManager/desktop/src/main/java/desktop/lib/components/TextField.java b/CollectionManager/desktop/src/main/java/desktop/lib/components/TextField.java new file mode 100644 index 0000000..be91470 --- /dev/null +++ b/CollectionManager/desktop/src/main/java/desktop/lib/components/TextField.java @@ -0,0 +1,33 @@ +package desktop.lib.components; + +import java.awt.BorderLayout; + +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextField; + +import lombok.Getter; + +public class TextField extends JPanel { + private JLabel label; + + @Getter + private JTextField textField; + + public TextField(String labelText, int columns) { + label = new JLabel(labelText); + textField = new JTextField(columns); + + this.setLayout(new BorderLayout(0, 5)); + this.add(label, BorderLayout.NORTH); + this.add(textField, BorderLayout.CENTER); + } + + public void setLabel(String text) { + label.setText(text); + } + + public String getText() { + return textField.getText(); + } +} diff --git a/CollectionManager/desktop/src/main/java/desktop/pages/auth/AuthPage.java b/CollectionManager/desktop/src/main/java/desktop/pages/auth/AuthPage.java new file mode 100644 index 0000000..85fef78 --- /dev/null +++ b/CollectionManager/desktop/src/main/java/desktop/pages/auth/AuthPage.java @@ -0,0 +1,48 @@ +package desktop.pages.auth; + +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; + +import javax.swing.JOptionPane; + +import adapter.Adapter; +import desktop.App; +import desktop.lib.BasePage; +import desktop.lib.Config; +import textlocale.text.TextSupplier; + +public class AuthPage extends BasePage { + private Adapter authAdapter; + + private TextSupplier ts = App.texts.getPackage("texts.auth")::getText; + + public AuthPage() { + super("auth"); + initAuthAdapter(); + + } + + @Override + public void beforeShow() { + super.beforeShow(); + + this.setLayout(new GridBagLayout()); + var gbc = new GridBagConstraints(); + + // Panel with login/register cards + var cardsPanel = new CardsPanel(authAdapter); + cardsPanel.showSignInCard(); + gbc.gridx = 0; + gbc.gridy = 0; + gbc.anchor = GridBagConstraints.CENTER; + this.add(cardsPanel, gbc); + } + + private void initAuthAdapter() { + try { + authAdapter = new Adapter(Config.authServiceHost, Config.authServicePort); + } catch (Exception e) { + JOptionPane.showMessageDialog(this, ts.t("messages.connectionError")); + } + } +} diff --git a/CollectionManager/desktop/src/main/java/desktop/pages/auth/CardsPanel.java b/CollectionManager/desktop/src/main/java/desktop/pages/auth/CardsPanel.java new file mode 100644 index 0000000..3ccc9ff --- /dev/null +++ b/CollectionManager/desktop/src/main/java/desktop/pages/auth/CardsPanel.java @@ -0,0 +1,43 @@ +package desktop.pages.auth; + +import java.awt.CardLayout; + +import javax.swing.BorderFactory; +import javax.swing.JPanel; + +import com.formdev.flatlaf.ui.FlatBorder; + +import adapter.Adapter; + +public class CardsPanel extends JPanel { + private final Adapter authAdapter; + + private CardLayout cardsLayout; + + public CardsPanel(Adapter authAdapter) { + super(); + this.authAdapter = authAdapter; + init(); + } + + private void init() { + this.setBorder( + BorderFactory.createCompoundBorder(new FlatBorder(), BorderFactory.createEmptyBorder(10, 10, 10, 10))); + + cardsLayout = new CardLayout(); + this.setLayout(cardsLayout); + + this.add(new SignInCard(authAdapter, this::showSignUpCard), "signIn"); + this.add(new SignUpCard(authAdapter, this::showSignInCard), "signUp"); + } + + public void showSignInCard() { + cardsLayout.show(this, "signIn"); + this.requestFocusInWindow(); + } + + public void showSignUpCard() { + cardsLayout.show(this, "signUp"); + this.requestFocusInWindow(); + } +} diff --git a/CollectionManager/desktop/src/main/java/desktop/pages/auth/SignInCard.java b/CollectionManager/desktop/src/main/java/desktop/pages/auth/SignInCard.java new file mode 100644 index 0000000..fce7bc9 --- /dev/null +++ b/CollectionManager/desktop/src/main/java/desktop/pages/auth/SignInCard.java @@ -0,0 +1,123 @@ +package desktop.pages.auth; + +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.io.Serializable; +import java.util.Map; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; + +import adapter.Adapter; +import adapter.exceptions.ReceiveResponseFailed; +import adapter.exceptions.SendRequestFailed; +import adapter.exceptions.SocketInitFailed; +import auth.AuthToken; +import authservice.api.StatusCodes; +import desktop.App; +import desktop.lib.TokenStore; +import desktop.lib.components.PasswordField; +import desktop.lib.components.TextField; +import server.responses.Response; +import textlocale.text.TextSupplier; + +public class SignInCard extends JPanel { + private TextSupplier ts = App.texts.getPackage("texts.auth")::getText; + + private TextField usernameField; + private PasswordField passwordField; + + private Adapter authAdapter; + private Runnable openSignUpCard; + + public SignInCard(Adapter authAdapter, Runnable openSignUp) { + super(new GridBagLayout()); + this.authAdapter = authAdapter; + this.openSignUpCard = openSignUp; + init(); + } + + private void init() { + var gbc = new GridBagConstraints(); + gbc.insets = new Insets(10, 10, 10, 10); + gbc.anchor = GridBagConstraints.LINE_START; + + // Title + var authTitle = new JLabel(ts.t("signInCard.title")); + authTitle.putClientProperty("FlatLaf.styleClass", "h2"); + authTitle.setHorizontalAlignment(JLabel.CENTER); + authTitle.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0)); + gbc.gridx = 0; + gbc.gridy = 0; + gbc.gridwidth = 2; + gbc.fill = GridBagConstraints.HORIZONTAL; + this.add(authTitle, gbc); + + // Username + usernameField = new TextField(ts.t("signInCard.username.title"), 25); + gbc.gridy++; + this.add(usernameField, gbc); + + // Password + passwordField = new PasswordField(ts.t("signInCard.password.title"), 25); + gbc.gridy++; + this.add(passwordField, gbc); + + // Sign in button + var loginButton = new JButton(ts.t("signInCard.signInButton")); + loginButton.addActionListener(this::signIn); + gbc.gridx = 0; + gbc.gridy++; + gbc.gridwidth = 2; + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.anchor = GridBagConstraints.CENTER; + this.add(loginButton, gbc); + + // Sign up button + var signUpButton = new JButton(ts.t("signInCard.openSignUpButton")); + signUpButton.setBackground(null); + signUpButton.setHorizontalAlignment(JLabel.RIGHT); + signUpButton.addActionListener(e -> openSignUpCard.run()); + gbc.gridy++; + gbc.fill = GridBagConstraints.HORIZONTAL; + this.add(signUpButton, gbc); + } + + private void signIn(ActionEvent event) { + var username = usernameField.getTextField().getText(); + char[] passwordChars = passwordField.getPasswordField().getPassword(); + var password = new String(passwordChars); + + Map data = Map.of("login", username, "password", password); + Response response; + try { + response = authAdapter.triggerServer("auth.login", data); + if (response.getCode() == 200) { + JOptionPane.showMessageDialog(this, ts.t("messages.signInSuccess")); + try { + var token = (AuthToken) response.getData().get("token"); + TokenStore.saveToken(token.getToken()); + App.context.setUsername(username); + App.context.initCollectionManager(token); + App.showPage("main"); + usernameField.getTextField().setText(""); + passwordField.getPasswordField().setText(""); + } catch (Exception e) { + JOptionPane.showMessageDialog(this, + ts.t("messages.signInError") + e.getMessage()); + } + } else if (response.getCode() == StatusCodes.INCORRECT_LOGIN_OR_PASSWORD) { + JOptionPane.showMessageDialog(this, ts.t("messages.invalidCredentials")); + } else { + JOptionPane.showMessageDialog(this, ts.t("messages.connectionError")); + } + } catch (SocketInitFailed | SendRequestFailed | ReceiveResponseFailed e) { + JOptionPane.showMessageDialog(this, ts.t("messages.connectionError")); + } + } +} diff --git a/CollectionManager/desktop/src/main/java/desktop/pages/auth/SignUpCard.java b/CollectionManager/desktop/src/main/java/desktop/pages/auth/SignUpCard.java new file mode 100644 index 0000000..6bbbe60 --- /dev/null +++ b/CollectionManager/desktop/src/main/java/desktop/pages/auth/SignUpCard.java @@ -0,0 +1,120 @@ +package desktop.pages.auth; + +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.io.Serializable; +import java.util.Map; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; + +import adapter.Adapter; +import adapter.exceptions.ReceiveResponseFailed; +import adapter.exceptions.SendRequestFailed; +import adapter.exceptions.SocketInitFailed; +import auth.AuthToken; +import authservice.api.StatusCodes; +import desktop.App; +import desktop.lib.TokenStore; +import desktop.lib.components.PasswordField; +import desktop.lib.components.TextField; +import server.responses.Response; +import textlocale.text.TextSupplier; + +public class SignUpCard extends JPanel { + private TextSupplier ts = App.texts.getPackage("texts.auth")::getText; + + private TextField usernameField; + private PasswordField passwordField; + + private Adapter authAdapter; + private Runnable openSignInCard; + + public SignUpCard(Adapter authAdapter, Runnable openSignInCard) { + super(new GridBagLayout()); + this.authAdapter = authAdapter; + this.openSignInCard = openSignInCard; + init(); + } + + private void init() { + var gbc = new GridBagConstraints(); + gbc.insets = new Insets(10, 10, 10, 10); + gbc.anchor = GridBagConstraints.LINE_START; + + // Title + var authTitle = new JLabel(ts.t("signUpCard.title")); + authTitle.putClientProperty("FlatLaf.styleClass", "h2"); + authTitle.setHorizontalAlignment(JLabel.CENTER); + authTitle.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0)); + gbc.gridx = 0; + gbc.gridy = 0; + gbc.gridwidth = 2; + gbc.fill = GridBagConstraints.HORIZONTAL; + this.add(authTitle, gbc); + + // Username + usernameField = new TextField(ts.t("signUpCard.username.title"), 25); + gbc.gridy++; + this.add(usernameField, gbc); + + // Password + passwordField = new PasswordField(ts.t("signUpCard.password.title"), 25); + gbc.gridy++; + this.add(passwordField, gbc); + + var loginButton = new JButton(ts.t("signUpCard.signUpButton")); + loginButton.addActionListener(this::signUp); + gbc.gridx = 0; + gbc.gridy++; + gbc.gridwidth = 2; + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.anchor = GridBagConstraints.CENTER; + this.add(loginButton, gbc); + + var signInButton = new JButton(ts.t("signUpCard.openSignInButton")); + signInButton.setBackground(null); + signInButton.setHorizontalAlignment(JLabel.RIGHT); + signInButton.addActionListener(e -> openSignInCard.run()); + gbc.gridy++; + gbc.fill = GridBagConstraints.HORIZONTAL; + this.add(signInButton, gbc); + } + + private void signUp(ActionEvent event) { + var username = usernameField.getTextField().getText(); + char[] passwordChars = passwordField.getPasswordField().getPassword(); + var password = new String(passwordChars); + + Map data = Map.of("login", username, "password", password); + Response response; + try { + response = authAdapter.triggerServer("auth.register", data); + if (response.getCode() == 200) { + JOptionPane.showMessageDialog(this, ts.t("messages.signUpSuccess")); + try { + var token = (AuthToken) response.getData().get("token"); + TokenStore.saveToken(token.getToken()); + App.context.setUsername(username); + App.context.initCollectionManager(token); + App.showPage("main"); + usernameField.getTextField().setText(""); + passwordField.getPasswordField().setText(""); + } catch (Exception e) { + JOptionPane.showMessageDialog(this, ts.t("messages.signUpError") + e.getMessage()); + } + } else if (response.getCode() == StatusCodes.LOGIN_ALREADY_EXISTS) { + JOptionPane.showMessageDialog(this, ts.t("messages.usernameTaken")); + } else { + JOptionPane.showMessageDialog(this, ts.t("messages.connectionError")); + } + } catch (SocketInitFailed | SendRequestFailed | ReceiveResponseFailed e) { + JOptionPane.showMessageDialog(this, ts.t("messages.connectionError")); + } + } +} diff --git a/CollectionManager/desktop/src/main/java/desktop/pages/main/ContentPanel.java b/CollectionManager/desktop/src/main/java/desktop/pages/main/ContentPanel.java new file mode 100644 index 0000000..56ba470 --- /dev/null +++ b/CollectionManager/desktop/src/main/java/desktop/pages/main/ContentPanel.java @@ -0,0 +1,38 @@ +package desktop.pages.main; + +import java.awt.CardLayout; + +import javax.swing.JPanel; + +import desktop.pages.main.table.TablePanel; +import desktop.pages.main.viz.VizPanel; + +public class ContentPanel extends JPanel { + private TablePanel table; + + private VizPanel viz; + + private CardLayout layout; + + public ContentPanel(TablePanel table, VizPanel viz) { + this.table = table; + this.viz = viz; + init(); + } + + private void init() { + layout = new CardLayout(); + setLayout(layout); + + add(table, "table"); + add(viz, "viz"); + } + + public void showTable() { + layout.show(this, "table"); + } + + public void showViz() { + layout.show(this, "viz"); + } +} diff --git a/CollectionManager/desktop/src/main/java/desktop/pages/main/ContentSwitch.java b/CollectionManager/desktop/src/main/java/desktop/pages/main/ContentSwitch.java new file mode 100644 index 0000000..8421e9f --- /dev/null +++ b/CollectionManager/desktop/src/main/java/desktop/pages/main/ContentSwitch.java @@ -0,0 +1,52 @@ +package desktop.pages.main; + +import java.awt.FlowLayout; + +import javax.swing.JButton; +import javax.swing.JPanel; + +import desktop.App; +import lombok.Setter; +import textlocale.text.TextSupplier; + +public class ContentSwitch extends JPanel { + private TextSupplier ts = App.texts + .getPackage("texts.main.topbar.contentSwitch")::getText; + + @Setter + private Runnable showTable; + + @Setter + private Runnable showViz; + + private JButton vizButton; + + private JButton tableButton; + + public ContentSwitch() { + init(); + } + + private void init() { + setLayout(new FlowLayout(FlowLayout.CENTER, 0, 0)); + + // Table button + tableButton = new JButton(ts.t("openTable")); + tableButton.addActionListener(e -> { + showTable.run(); + tableButton.setEnabled(false); + vizButton.setEnabled(true); + }); + tableButton.setEnabled(false); + add(tableButton); + + // Viz button + vizButton = new JButton(ts.t("openViz")); + vizButton.addActionListener(e -> { + showViz.run(); + vizButton.setEnabled(false); + tableButton.setEnabled(true); + }); + add(vizButton); + } +} diff --git a/CollectionManager/desktop/src/main/java/desktop/pages/main/MainPage.java b/CollectionManager/desktop/src/main/java/desktop/pages/main/MainPage.java new file mode 100644 index 0000000..e0ebf38 --- /dev/null +++ b/CollectionManager/desktop/src/main/java/desktop/pages/main/MainPage.java @@ -0,0 +1,92 @@ +package desktop.pages.main; + +import java.awt.BorderLayout; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.JOptionPane; + +import desktop.App; +import desktop.lib.BasePage; +import desktop.pages.main.table.TablePanel; +import desktop.pages.main.viz.HumanSprite; +import desktop.pages.main.viz.VizPanel; +import humandeque.HumanDeque; + +public class MainPage extends BasePage { + private HumanDeque collection; + + private TablePanel table; + + private VizPanel viz; + + private ContentPanel content; + + public MainPage() { + super("main"); + } + + public void beforeShow() { + super.beforeShow(); + + setLayout(new BorderLayout(0, 10)); + + // Top bar + var topBar = new TopBar(App.context.getUsername()); + add(topBar, BorderLayout.NORTH); + + // Collection table + table = new TablePanel( + List.copyOf(App.context.getHumans()), + this::updateCollection, + this::refreshCollection); + + // Humans visualization + viz = new VizPanel(List.copyOf(App.context.getHumans())); + viz.setOnSpriteClicked(this::onSpriteClicked); + + // Content + content = new ContentPanel(table, viz); + content.showTable(); + topBar.setOpenTable(content::showTable); + topBar.setOpenViz(content::showViz); + topBar.setLangSelectHandler((lang) -> { + App.setLang(lang); + this.setVisible(false); + this.beforeShow(); + this.setVisible(true); + this.requestFocus(); + }); + add(content); + + // Update collection data + refreshCollection(); + } + + private void updateCollection() { + try { + App.context.getManager().load(); + } catch (Exception e) { + + } + refreshCollection(); + } + + private void refreshCollection() { + collection = App.context.getManager().getCollection(); + table.setHumans(new ArrayList<>(collection)); + viz.setHumans(new ArrayList<>(collection)); + } + + private void onSpriteClicked(HumanSprite sprite) { + for (var human : collection) { + if (human.getId() == sprite.getId()) { + JOptionPane.showMessageDialog( + this, + human.toString(), + human.getName(), + JOptionPane.INFORMATION_MESSAGE); + } + } + } +} diff --git a/CollectionManager/desktop/src/main/java/desktop/pages/main/TopBar.java b/CollectionManager/desktop/src/main/java/desktop/pages/main/TopBar.java new file mode 100644 index 0000000..b539f38 --- /dev/null +++ b/CollectionManager/desktop/src/main/java/desktop/pages/main/TopBar.java @@ -0,0 +1,85 @@ +package desktop.pages.main; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.FlowLayout; +import java.util.function.Consumer; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSeparator; + +import desktop.App; +import desktop.lib.TokenStore; +import desktop.pages.main.lang.LangSelect; +import jiconfont.icons.font_awesome.FontAwesome; +import jiconfont.swing.IconFontSwing; + +public class TopBar extends JPanel { + private String username; + + private ContentSwitch contentSwitch; + + private LangSelect langSelect; + + public TopBar(String username) { + this.username = username; + init(); + } + + private void init() { + setLayout(new BorderLayout(10, 10)); + setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + + // Language select + langSelect = new LangSelect(App.context.getLangs(), App.context.getLang()); + add(langSelect, BorderLayout.WEST); + + // Content switcher + contentSwitch = new ContentSwitch(); + add(contentSwitch, BorderLayout.CENTER); + + // User container + var userContainer = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0)); + + // Username label + var usernameLabel = new JLabel(username); + usernameLabel.putClientProperty("FlatLaf.styleClass", "h3"); + userContainer.add(usernameLabel); + + // Logout button + var logoutIcon = IconFontSwing.buildIcon(FontAwesome.SIGN_OUT, 15, Color.WHITE); + var logoutButton = new JButton(logoutIcon); + logoutButton.addActionListener(e -> { + logout(); + }); + userContainer.add(logoutButton); + + add(userContainer, BorderLayout.EAST); + + // Bottom separator + add(new JSeparator(JSeparator.HORIZONTAL), BorderLayout.PAGE_END); + } + + private void logout() { + try { + TokenStore.deleteToken(); + } catch (Exception e) { + } + App.showPage("auth"); + } + + public void setOpenTable(Runnable r) { + contentSwitch.setShowTable(r); + } + + public void setOpenViz(Runnable r) { + contentSwitch.setShowViz(r); + } + + public void setLangSelectHandler(Consumer handler) { + langSelect.setLangSelectHandler(handler); + } +} diff --git a/CollectionManager/desktop/src/main/java/desktop/pages/main/lang/LangItem.java b/CollectionManager/desktop/src/main/java/desktop/pages/main/lang/LangItem.java new file mode 100644 index 0000000..5a4e8b9 --- /dev/null +++ b/CollectionManager/desktop/src/main/java/desktop/pages/main/lang/LangItem.java @@ -0,0 +1,38 @@ +package desktop.pages.main.lang; + +import java.awt.Image; +import java.net.URL; + +import javax.swing.Icon; +import javax.swing.ImageIcon; + +import lombok.Getter; + +public class LangItem { + @Getter + private Icon icon; + + @Getter + private String lang; + + @Getter + private String langCode; + + public LangItem(String lang, String langCode) { + this.lang = lang; + this.langCode = langCode; + this.icon = createImageIcon("/desktop/langs/" + langCode + ".png", lang, 16, 16); + } + + protected ImageIcon createImageIcon(String path, String description, int width, int height) { + URL imgURL = getClass().getResource(path); + if (imgURL != null) { + var icon = new ImageIcon(imgURL, description); + Image img = icon.getImage().getScaledInstance(width, height, Image.SCALE_SMOOTH); + return new ImageIcon(img, description); + } else { + System.err.println("Couldn't find file: " + path); + return null; + } + } +} diff --git a/CollectionManager/desktop/src/main/java/desktop/pages/main/lang/LangSelect.java b/CollectionManager/desktop/src/main/java/desktop/pages/main/lang/LangSelect.java new file mode 100644 index 0000000..2903404 --- /dev/null +++ b/CollectionManager/desktop/src/main/java/desktop/pages/main/lang/LangSelect.java @@ -0,0 +1,55 @@ +package desktop.pages.main.lang; + +import java.awt.Component; +import java.util.List; +import java.util.function.Consumer; + +import javax.swing.JComboBox; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.plaf.basic.BasicComboBoxRenderer; + +import lombok.Setter; + +public class LangSelect extends JPanel { + private List items; + + @Setter + private Consumer langSelectHandler; + + public LangSelect(List items, String selected) { + this.items = items; + init(selected); + } + + private void init(String selected) { + var langsComboBox = new JComboBox<>(items.toArray(new LangItem[0])); + langsComboBox.setRenderer(new LangItemRenderer()); + langsComboBox.addActionListener(e -> { + var item = (LangItem) langsComboBox.getSelectedItem(); + if (langSelectHandler != null) { + langSelectHandler.accept(item.getLangCode()); + } + }); + + var selectedItem = items.stream().filter(item -> item.getLangCode().equals(selected)).findFirst(); + selectedItem.ifPresent(item -> langsComboBox.setSelectedItem(item)); + + add(langsComboBox); + } + + class LangItemRenderer extends BasicComboBoxRenderer { + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { + super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + + if (value instanceof LangItem) { + LangItem langItem = (LangItem) value; + setText(langItem.getLang()); // Display the language name + setIcon(langItem.getIcon()); // Display the language icon + } + + return this; + } + } +} diff --git a/CollectionManager/desktop/src/main/java/desktop/pages/main/table/Add.java b/CollectionManager/desktop/src/main/java/desktop/pages/main/table/Add.java new file mode 100644 index 0000000..e1c463c --- /dev/null +++ b/CollectionManager/desktop/src/main/java/desktop/pages/main/table/Add.java @@ -0,0 +1,45 @@ +package desktop.pages.main.table; + +import java.awt.BorderLayout; +import java.util.function.Consumer; + +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import desktop.App; +import models.Human; +import textlocale.text.TextSupplier; + +public class Add extends JPanel { + private TextSupplier ts = App.texts + .getPackage("texts.main.table.toolbar.add")::getText; + + private JLabel label; + + private AddDialog dialog; + + public Add() { + init(); + } + + private void init() { + setLayout(new BorderLayout(0, 5)); + + // Dialog + dialog = new AddDialog(); + + // Label + label = new JLabel(ts.t("title")); + add(label, BorderLayout.NORTH); + + // Button + var addButton = new JButton(ts.t("addButton")); + addButton.addActionListener(e -> dialog.setVisible(true)); + add(addButton, BorderLayout.CENTER); + } + + public void setAddCallback(Consumer addCallback) { + dialog.setAddCallback(addCallback); + } +} diff --git a/CollectionManager/desktop/src/main/java/desktop/pages/main/table/AddDialog.java b/CollectionManager/desktop/src/main/java/desktop/pages/main/table/AddDialog.java new file mode 100644 index 0000000..88ad797 --- /dev/null +++ b/CollectionManager/desktop/src/main/java/desktop/pages/main/table/AddDialog.java @@ -0,0 +1,225 @@ +package desktop.pages.main.table; + +import java.awt.Color; +import java.awt.FlowLayout; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.util.function.Consumer; + +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; + +import desktop.App; +import desktop.lib.components.CheckBox; +import desktop.lib.components.ComboBox; +import desktop.lib.components.Slider; +import desktop.lib.components.Spinner; +import desktop.lib.components.TextField; +import humandeque.manager.exceptions.ElementAlreadyExistsError; +import humandeque.manager.exceptions.ManipulationError; +import lombok.Setter; +import models.Car; +import models.Coordinates; +import models.Human; +import models.Mood; +import textlocale.text.TextSupplier; + + +public class AddDialog extends JDialog { + private TextSupplier ts = App.texts + .getPackage("texts.main.table.toolbar.add.dialog")::getText; + + private JLabel errorLabel; + + @Setter + private Consumer addCallback; + + public AddDialog() { + init(); + } + + private void init() { + setTitle(ts.t("title")); + + setLayout(new GridBagLayout()); + + var gbc = new GridBagConstraints(); + gbc.insets = new Insets(10, 10, 10, 10); + gbc.anchor = GridBagConstraints.LINE_START; + gbc.fill = GridBagConstraints.BOTH; + gbc.weightx = 1; + + // Name field + var nameField = new TextField(ts.t("name"), 40); + gbc.gridwidth = 2; + gbc.gridx = 0; + gbc.gridy = 0; + add(nameField, gbc); + + // Real hero and toothpick row + gbc.gridwidth = 1; + gbc.gridy++; + + var realHeroContainer = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0)); + var realHeroCheckBox = new CheckBox(ts.t("realHero")); + realHeroContainer.add(realHeroCheckBox); + gbc.gridx = 0; + add(realHeroContainer, gbc); + + var toothpickContainer = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0)); + var toothpickCheckBox = new CheckBox(ts.t("toothpick")); + toothpickContainer.add(toothpickCheckBox); + gbc.gridx = 1; + add(toothpickContainer, gbc); + + gbc.gridx = 0; + + // Coordinates + var coordinatesLabel = new JLabel(ts.t("coordinates")); + gbc.gridwidth = 2; + gbc.gridy++; + add(coordinatesLabel, gbc); + + gbc.gridwidth = 1; + gbc.gridy++; + + // x + var xSpinner = new Spinner(ts.t("x"), 0D, null, null, 0.1D); + gbc.gridx = 0; + add(xSpinner, gbc); + + // y + var ySpinner = new Spinner(ts.t("y"), 0D, null, null, 0.1D); + gbc.gridx = 1; + add(ySpinner, gbc); + + gbc.gridx = 0; + + // Impact speed slider + var impactSpeedSlider = new Slider(ts.t("impactSpeed"), 0, 1, 100); + gbc.gridwidth = 2; + gbc.gridx = 0; + gbc.gridy++; + add(impactSpeedSlider, gbc); + + // Soundtrack field + var soundtrackNameField = new TextField(ts.t("soundtrack"), 40); + gbc.gridwidth = 2; + gbc.gridy++; + add(soundtrackNameField, gbc); + + // Waiting slider + var waitingSlider = new Slider(ts.t("waiting"), 0, 100); + gbc.gridwidth = 2; + gbc.gridy++; + add(waitingSlider, gbc); + + // Mood combo box + var moodComboBox = new ComboBox<>(ts.t("mood"), Mood.values()); + gbc.gridwidth = 2; + gbc.gridy++; + add(moodComboBox, gbc); + + // Car field + var carNameField = new TextField(ts.t("car"), 40); + gbc.gridwidth = 2; + gbc.gridy++; + add(carNameField, gbc); + + // Error label + errorLabel = new JLabel(" "); + errorLabel.setForeground(Color.RED); + gbc.gridy++; + add(errorLabel, gbc); + + // Buttons + gbc.gridwidth = 1; + + var cancelButton = new JButton(ts.t("cancelButton")); + cancelButton.addActionListener(e -> dispose()); + gbc.gridy++; + add(cancelButton, gbc); + + var addButton = new JButton(ts.t("addButton")); + addButton.addActionListener(e -> { + new Thread(() -> { + add(nameField.getText(), + xSpinner.getValue(), + ySpinner.getValue(), + realHeroCheckBox.isSelected(), + toothpickCheckBox.isSelected(), + impactSpeedSlider.getValue(), + soundtrackNameField.getText(), + waitingSlider.getValue(), + (Mood) moodComboBox.getSelectedItem(), + carNameField.getText()); + }).start(); + }); + gbc.gridx = 1; + add(addButton, gbc); + + pack(); + setResizable(false); + setLocationRelativeTo(null); + } + + private void add( + String name, + Double x, + Double y, + boolean realHero, + boolean toothpick, + Float impactSpeed, + String soundtrackName, + Float waiting, + Mood mood, + String carName) { + if (name.isEmpty()) { + errorLabel.setText(ts.t("errors.name")); + return; + } else if (soundtrackName.isEmpty()) { + errorLabel.setText(ts.t("errors.soundtrack")); + return; + } else if (carName.isEmpty()) { + errorLabel.setText(ts.t("errors.car")); + return; + } + + // Add human + var human = Human.builder() + .name(name) + .coordinates(new Coordinates(x.floatValue(), y.floatValue())) + .realHero(realHero) + .hasToothpick(toothpick) + .impactSpeed(impactSpeed.doubleValue()) + .soundtrackName(soundtrackName) + .minutesOfWaiting(waiting) + .mood(mood) + .car(new Car(carName)) + .build(); + + try { + App.context.getManager().add(human); + JOptionPane.showConfirmDialog( + null, + ts.t("success"), + ts.t("successTitle"), + JOptionPane.DEFAULT_OPTION, + JOptionPane.INFORMATION_MESSAGE); + addCallback.accept(human); + } catch (ManipulationError | ElementAlreadyExistsError e) { + JOptionPane.showConfirmDialog( + null, + ts.t("errors.add", e.getMessage()), + ts.t("errors.addTitle"), + JOptionPane.DEFAULT_OPTION, + JOptionPane.ERROR_MESSAGE); + } finally { + dispose(); + } + } +} diff --git a/CollectionManager/desktop/src/main/java/desktop/pages/main/table/Delete.java b/CollectionManager/desktop/src/main/java/desktop/pages/main/table/Delete.java new file mode 100644 index 0000000..ebcafa6 --- /dev/null +++ b/CollectionManager/desktop/src/main/java/desktop/pages/main/table/Delete.java @@ -0,0 +1,77 @@ +package desktop.pages.main.table; + +import java.awt.BorderLayout; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; + +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import desktop.App; +import humandeque.manager.exceptions.ElementNotExistsError; +import humandeque.manager.exceptions.ManipulationError; +import lombok.Setter; +import models.Human; +import textlocale.text.TextSupplier; + +public class Delete extends JPanel { + private TextSupplier ts = App.texts + .getPackage("texts.main.table.toolbar.delete")::getText; + + private JLabel label; + + private JButton deleteButton; + + private Set selectedHumans; + + @Setter + private Consumer> deleteCallback; + + public Delete() { + init(); + } + + private void init() { + setLayout(new BorderLayout(0, 5)); + + // Label + label = new JLabel(ts.t("noItemsSelected")); + add(label, BorderLayout.NORTH); + + // Button + deleteButton = new JButton(ts.t("deleteButton")); + deleteButton.addActionListener(e -> new Thread(this::delete).start()); + deleteButton.setEnabled(false); + add(deleteButton, BorderLayout.CENTER); + } + + public void notifySelection(Set selected) { + if (selected.isEmpty()) { + label.setText(ts.t("noItemsSelected")); + deleteButton.setEnabled(false); + } else { + label.setText(ts.t("itemsSelected", selected.size())); + deleteButton.setEnabled(true); + } + selectedHumans = selected; + } + + private void delete() { + var counter = 0; + label.setText(ts.t("deletingItems", counter, selectedHumans.size())); + for (Human human : selectedHumans) { + try { + App.context.getManager().remove(human.getId()); + } catch (ManipulationError | ElementNotExistsError e) { + e.printStackTrace(); + } + counter++; + label.setText(ts.t("deletingItems", counter, selectedHumans.size())); + } + deleteCallback.accept(List.copyOf(selectedHumans)); + label.setText(ts.t("noItemsSelected")); + deleteButton.setEnabled(false); + } +} diff --git a/CollectionManager/desktop/src/main/java/desktop/pages/main/table/Filter.java b/CollectionManager/desktop/src/main/java/desktop/pages/main/table/Filter.java new file mode 100644 index 0000000..5aad0f3 --- /dev/null +++ b/CollectionManager/desktop/src/main/java/desktop/pages/main/table/Filter.java @@ -0,0 +1,85 @@ +package desktop.pages.main.table; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.FlowLayout; +import java.util.function.BiConsumer; + +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import desktop.App; +import desktop.lib.components.TextField; +import jiconfont.icons.font_awesome.FontAwesome; +import jiconfont.swing.IconFontSwing; +import textlocale.text.TextSupplier; + +public class Filter extends JPanel { + private TextSupplier ts = App.texts.getPackage("texts.main.table.toolbar.filter")::getText; + + private JComboBox selectComboBox; + + private TextField filterField; + + private String[] columnNames; + + private BiConsumer filterCallback; + + public Filter(String[] columnNames, BiConsumer filterCallback) { + this.columnNames = columnNames; + this.filterCallback = filterCallback; + init(); + } + + private void init() { + setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); + + // Column select + var selectPanel = new JPanel(new BorderLayout(0, 5)); + var selectLabel = new JLabel(ts.t("columnSelect")); + selectPanel.add(selectLabel, BorderLayout.NORTH); + selectComboBox = new JComboBox<>(this.columnNames); + selectPanel.add(selectComboBox, BorderLayout.CENTER); + add(selectPanel); + add(Box.createHorizontalStrut(10)); + + // Filter field + filterField = new TextField(ts.t("searchRegex"), 25); + add(filterField); + add(Box.createHorizontalStrut(10)); + + // Filter button + var searchBtnPanel = new JPanel(new BorderLayout(0, 5)); + searchBtnPanel.add(new JLabel(" "), BorderLayout.NORTH); // dumb alignment + var searchIcon = IconFontSwing.buildIcon(FontAwesome.SEARCH, 15, Color.WHITE); + var searchButton = new JButton(searchIcon); + searchButton.addActionListener(e -> { + search(); + }); + searchBtnPanel.add(searchButton, BorderLayout.CENTER); + add(searchBtnPanel); + add(Box.createHorizontalStrut(10)); + + // Reset button + var resetBtnPanel = new JPanel(new BorderLayout(0, 5)); + resetBtnPanel.add(new JLabel(" "), BorderLayout.NORTH); // dumb alignment + var resetIcon = IconFontSwing.buildIcon(FontAwesome.TIMES, 15, Color.WHITE); + var resetButton = new JButton(resetIcon); + resetButton.addActionListener(e -> { + filterField.getTextField().setText(""); + search(); + }); + resetBtnPanel.add(resetButton, BorderLayout.CENTER); + add(resetBtnPanel); + } + + private void search() { + var selectedColumn = (String) selectComboBox.getSelectedItem(); + var filterText = filterField.getTextField().getText(); + filterCallback.accept(selectedColumn, filterText); + } +} diff --git a/CollectionManager/desktop/src/main/java/desktop/pages/main/table/Refresh.java b/CollectionManager/desktop/src/main/java/desktop/pages/main/table/Refresh.java new file mode 100644 index 0000000..6770c39 --- /dev/null +++ b/CollectionManager/desktop/src/main/java/desktop/pages/main/table/Refresh.java @@ -0,0 +1,44 @@ +package desktop.pages.main.table; + +import java.awt.BorderLayout; +import java.awt.Color; + +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import desktop.App; +import jiconfont.icons.font_awesome.FontAwesome; +import jiconfont.swing.IconFontSwing; +import lombok.Setter; +import textlocale.text.TextSupplier; + +public class Refresh extends JPanel { + private TextSupplier ts = App.texts + .getPackage("texts.main.table.toolbar.refresh")::getText; + + private JLabel label; + + @Setter + private Runnable refreshCallback; + + public Refresh() { + init(); + } + + private void init() { + setLayout(new BorderLayout(0, 5)); + + // Label + label = new JLabel(ts.t("title")); + add(label, BorderLayout.NORTH); + + // Button + var refreshIcon = IconFontSwing.buildIcon(FontAwesome.REFRESH, 15, Color.WHITE); + var refreshButton = new JButton(refreshIcon); + refreshButton.addActionListener(e -> new Thread(refreshCallback).start()); + add(refreshButton, BorderLayout.CENTER); + + setMaximumSize(getPreferredSize()); + } +} diff --git a/CollectionManager/desktop/src/main/java/desktop/pages/main/table/Save.java b/CollectionManager/desktop/src/main/java/desktop/pages/main/table/Save.java new file mode 100644 index 0000000..c88fe79 --- /dev/null +++ b/CollectionManager/desktop/src/main/java/desktop/pages/main/table/Save.java @@ -0,0 +1,77 @@ +package desktop.pages.main.table; + +import java.awt.BorderLayout; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; + +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import desktop.App; +import humandeque.manager.exceptions.ElementNotExistsError; +import humandeque.manager.exceptions.ManipulationError; +import lombok.Setter; +import models.Human; +import textlocale.text.TextSupplier; + +public class Save extends JPanel { + private TextSupplier ts = App.texts.getPackage("texts.main.table.toolbar.save")::getText; + + private JLabel label; + + private JButton saveButton; + + private Set updatedHumans; + + @Setter + private Consumer> saveCallback; + + public Save() { + init(); + } + + private void init() { + setLayout(new BorderLayout(0, 5)); + + // Label + label = new JLabel(ts.t("savedMessage")); + add(label, BorderLayout.NORTH); + + // Button + saveButton = new JButton(ts.t("saveButton")); + saveButton.setEnabled(false); + saveButton.addActionListener(e -> new Thread(this::save).start()); + add(saveButton, BorderLayout.CENTER); + } + + public void notifyUpdate(Set updatedHumans) { + if (updatedHumans.isEmpty()) { + label.setText(ts.t("savedMessage")); + saveButton.setEnabled(false); + } else { + label.setText(ts.t("updatedMessage")); + saveButton.setEnabled(true); + } + this.updatedHumans = updatedHumans; + } + + private void save() { + var counter = 0; + label.setText(ts.t("savingMessage", counter, updatedHumans.size())); + for (Human human : updatedHumans) { + try { + App.context.getManager().update(human); + } catch (ManipulationError | ElementNotExistsError e) { + e.printStackTrace(); + } + counter++; + label.setText(ts.t("savingMessage", counter, updatedHumans.size())); + } + saveCallback.accept(new ArrayList<>(updatedHumans)); + label.setText(ts.t("savedMessage")); + saveButton.setEnabled(false); + } +} diff --git a/CollectionManager/desktop/src/main/java/desktop/pages/main/table/Script.java b/CollectionManager/desktop/src/main/java/desktop/pages/main/table/Script.java new file mode 100644 index 0000000..c12e23f --- /dev/null +++ b/CollectionManager/desktop/src/main/java/desktop/pages/main/table/Script.java @@ -0,0 +1,94 @@ +package desktop.pages.main.table; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.io.File; + +import javax.swing.JButton; +import javax.swing.JFileChooser; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.filechooser.FileFilter; + +import desktop.App; +import jiconfont.icons.font_awesome.FontAwesome; +import jiconfont.swing.IconFontSwing; +import lombok.Setter; +import textlocale.text.TextSupplier; + +public class Script extends JPanel { + private TextSupplier ts = App.texts + .getPackage("texts.main.table.toolbar.script")::getText; + + private JLabel label; + + @Setter + private Runnable finishHandler; + + public Script() { + init(); + } + + private void init() { + setLayout(new BorderLayout(0, 5)); + + // Label + label = new JLabel(ts.t("title")); + add(label, BorderLayout.NORTH); + + // Button + var openScriptIcon = IconFontSwing.buildIcon(FontAwesome.FILE_TEXT, 15, + Color.WHITE); + var openScriptButton = new JButton(openScriptIcon); + openScriptButton.addActionListener((e) -> openScript()); + add(openScriptButton, BorderLayout.CENTER); + + setMaximumSize(getPreferredSize()); + } + + private void openScript() { + var fc = new JFileChooser(); + fc.setFileFilter(new NekoFileFilter()); + + int result = fc.showOpenDialog(null); + + if (result == JFileChooser.APPROVE_OPTION) { + var file = fc.getSelectedFile(); + var dialog = new ScriptDialog(file, this.finishHandler); + dialog.setVisible(true); + } + } + + // Create a FileFilter to filter specific file extensions + private class NekoFileFilter extends FileFilter { + @Override + public boolean accept(File file) { + if (file.isDirectory()) { + return true; // Allow directories to be displayed + } + + String extension = getExtension(file.getName()); + if (extension != null) { + // Specify the allowed file extensions here + return extension.equals("neko"); + } + + return false; + } + + @Override + public String getDescription() { + // Description for the file filter + return "Script files (*.neko)"; + } + + // Helper method to get the file extension + private static String getExtension(String fileName) { + int dotIndex = fileName.lastIndexOf('.'); + if (dotIndex > 0 && dotIndex < fileName.length() - 1) { + return fileName.substring(dotIndex + 1).toLowerCase(); + } + return null; + } + }; +} diff --git a/CollectionManager/desktop/src/main/java/desktop/pages/main/table/ScriptDialog.java b/CollectionManager/desktop/src/main/java/desktop/pages/main/table/ScriptDialog.java new file mode 100644 index 0000000..b344be8 --- /dev/null +++ b/CollectionManager/desktop/src/main/java/desktop/pages/main/table/ScriptDialog.java @@ -0,0 +1,184 @@ +package desktop.pages.main.table; + +import java.awt.BorderLayout; +import java.io.File; +import java.nio.file.Files; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; + +import cliapp.cliclient.CLIClient; +import cliapp.commands.cli.ExecuteCommand; +import cliapp.commands.cli.ExitCommand; +import cliapp.commands.cli.HelpCommand; +import cliapp.commands.cli.HistoryCommand; +import cliapp.commands.cli.SetFuzzyCommand; +import cliapp.commands.collection.AddElementCommand; +import cliapp.commands.collection.AverageOfImpactSpeedCommand; +import cliapp.commands.collection.ClearCommand; +import cliapp.commands.collection.FilterByImpactSpeed; +import cliapp.commands.collection.HeadCommand; +import cliapp.commands.collection.InfoCommand; +import cliapp.commands.collection.PrintSortedCommand; +import cliapp.commands.collection.RandomCommand; +import cliapp.commands.collection.RemoveByIdCommand; +import cliapp.commands.collection.RemoveFirstCommand; +import cliapp.commands.collection.RemoveLastCommand; +import cliapp.commands.collection.ShowCommand; +import cliapp.commands.collection.TailCommand; +import cliapp.commands.collection.UpdateElementCommand; +import commands.OutputChannel; +import commands.requirements.Requirement; +import commands.requirements.RequirementsPipeline; +import commands.requirements.exceptions.RequirementAskError; +import commands.requirements.exceptions.ValidationError; +import desktop.App; +import lombok.RequiredArgsConstructor; +import textlocale.text.TextSupplier; + +public class ScriptDialog extends JDialog { + private TextSupplier ts = App.texts + .getPackage("texts.main.table.toolbar.script.dialog")::getText; + + private File scriptFile; + + private JButton button; + + private JTextArea scriptField; + + private Runnable finishHandler; + + public ScriptDialog(File scriptFile, Runnable finishHandler) { + this.finishHandler = finishHandler; + this.scriptFile = scriptFile; + init(); + } + + private void init() { + setTitle(scriptFile.getName()); + + var contentPanel = new JPanel(new BorderLayout(10, 10)); + contentPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + + // Text area with script content + String scriptContent; + try { + scriptContent = Files.readString(scriptFile.toPath()); + } catch (Exception e) { + throw new RuntimeException(e); + } + + scriptField = new JTextArea(scriptContent); + scriptField.setLineWrap(true); + scriptField.setWrapStyleWord(true); + scriptField.setEditable(false); + + var scriptArea = new JScrollPane(scriptField); + contentPanel.add(scriptArea, BorderLayout.CENTER); + + // Run button + button = new JButton(ts.t("runButton")); + button.addActionListener((e) -> new Thread(this::runScript).start()); + contentPanel.add(button, BorderLayout.PAGE_END); + + setContentPane(contentPanel); + setSize(500, 500); + setLocationRelativeTo(null); + } + + private void runScript() { + var cli = createClient(); + var executor = new ExecuteCommand(cli); + + var pipe = new ScripPipeline(scriptFile); + var output = new ScriptOutput(); + + output.putString(ts.t("running")); + try { + executor.execute(pipe, output); + } catch (Exception e) { + output.putString(e.getMessage()); + e.printStackTrace(); + } + output.putString(ts.t("done")); + + button.setText(ts.t("closeButton")); + button.removeActionListener(button.getActionListeners()[0]); + button.addActionListener((e) -> dispose()); + + if (finishHandler != null) { + finishHandler.run(); + } + } + + private CLIClient createClient() { + var cli = new CLIClient(); + + var manager = App.context.getManager(); + cli.registerCommand("info", new InfoCommand(manager)); + cli.registerCommand("show", new ShowCommand(manager)); + cli.registerCommand("add", new AddElementCommand(manager)); + cli.registerCommand("update", new UpdateElementCommand(manager)); + cli.registerCommand("remove_first", new RemoveFirstCommand(manager)); + cli.registerCommand("remove_last", new RemoveLastCommand(manager)); + cli.registerCommand("remove_by_id", new RemoveByIdCommand(manager)); + cli.registerCommand("print_sorted", new PrintSortedCommand(manager)); + cli.registerCommand("random", new RandomCommand(manager)); + cli.registerCommand("clear", new ClearCommand(manager)); + cli.registerCommand("average_speed", new AverageOfImpactSpeedCommand(manager)); + cli.registerCommand("head", new HeadCommand(manager)); + cli.registerCommand("tail", new TailCommand(manager)); + cli.registerCommand("filter", new FilterByImpactSpeed(manager)); + cli.registerCommand("help", new HelpCommand(cli)); + cli.registerCommand("exit", new ExitCommand(cli)); + cli.registerCommand("history", new HistoryCommand(cli)); + cli.registerCommand("fuzzy", new SetFuzzyCommand(cli)); + cli.registerCommand("execute", new ExecuteCommand(cli)); + + return cli; + } + + @RequiredArgsConstructor + private class ScripPipeline implements RequirementsPipeline { + private final File scriptFile; + + @Override + public O askRequirement(Requirement requirement) + throws RequirementAskError { + try { + if (requirement.getName().equals("Script file")) { + return requirement.getValue((I) scriptFile.getAbsolutePath()); + } + + var userInput = JOptionPane.showInputDialog( + ScriptDialog.this, + requirement.getName(), + requirement.getDescription(), + JOptionPane.QUESTION_MESSAGE + ); + return requirement.getValue((I) userInput); + } catch (ValidationError e) { + throw new RequirementAskError(requirement.getName(), e); + } catch (ClassCastException e) { + throw new RequirementAskError(requirement.getName(), e); + } + } + } + + private class ScriptOutput implements OutputChannel { + public ScriptOutput() { + scriptField.setText(""); + } + + @Override + public void putString(String string) { + scriptField.append(System.lineSeparator()); + scriptField.append(string); + } + } +} diff --git a/CollectionManager/desktop/src/main/java/desktop/pages/main/table/Table.java b/CollectionManager/desktop/src/main/java/desktop/pages/main/table/Table.java new file mode 100644 index 0000000..58e4fbb --- /dev/null +++ b/CollectionManager/desktop/src/main/java/desktop/pages/main/table/Table.java @@ -0,0 +1,311 @@ +package desktop.pages.main.table; + +import java.awt.Color; +import java.awt.Component; +import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.regex.PatternSyntaxException; + +import javax.swing.DefaultCellEditor; +import javax.swing.JComboBox; +import javax.swing.JTable; +import javax.swing.RowFilter; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.event.TableModelEvent; +import javax.swing.event.TableModelListener; +import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.DefaultTableModel; +import javax.swing.table.TableCellRenderer; +import javax.swing.table.TableColumn; +import javax.swing.table.TableRowSorter; + +import com.github.lgooddatepicker.tableeditors.DateTimeTableEditor; + +import lombok.Setter; +import models.Car; +import models.Human; +import models.Mood; + +public class Table extends JTable { + private DefaultTableModel model; + + private TableRowSorter sorter; + + private List data; + + private Set updatedData = new HashSet<>(); + + @Setter + private Consumer> selectCallback; + + @Setter + private Consumer> updateCallback; + + public Table(String[] columnNames, List initialData) { + super(); + this.data = initialData; + init(columnNames, initialData); + } + + public void updateData(List data) { + this.data = data; + this.updatedData = new HashSet<>(); + + while (model.getRowCount() > 0) { + try { + model.removeRow(0); + } catch (Exception e) { + + } + } + + for (Human human : data) { + Object[] rowData = { + human.getId(), + human.getName(), + human.getCoordinates().getX(), + human.getCoordinates().getY(), + human.getCreationDate(), + human.getRealHero(), + human.getHasToothpick(), + human.getImpactSpeed(), + human.getSoundtrackName(), + human.getMinutesOfWaiting(), + human.getMood(), + human.getCar().getName() + }; + model.addRow(rowData); + } + fitColumnsToContent(); + } + + public void saveUpdatedHumans(List humans) { + humans.forEach(human -> { + data.removeIf(h -> h.getId() == human.getId()); + data.add(human); + }); + updateData(data); + } + + public void deleteSelectedHumans(List humans) { + humans.forEach(human -> { + data.removeIf(h -> h.getId() == human.getId()); + }); + updateData(data); + } + + public void addHuman(Human human) { + data.add(human); + updateData(data); + } + + public void filter(String column, String regex) { + RowFilter rf = null; + try { + rf = RowFilter.regexFilter(regex, model.findColumn(column)); + } catch (PatternSyntaxException e) { + return; + } + sorter.setRowFilter(rf); + } + + private void init(String[] columnNames, List initialData) { + setSelectionBackground(Color.decode("#666666")); + + model = new TableModel(columnNames, 0); + setModel(model); + + sorter = new TableRowSorter(model); + setRowSorter(sorter); + + // Use custom editor for datetime + setDefaultRenderer(LocalDateTime.class, new DateTimeTableEditor()); + setDefaultEditor(LocalDateTime.class, new DateTimeTableEditor()); + + // Show ComboBox for mood column + var moodsComboBox = new JComboBox(Mood.values()); + getColumnModel().getColumn(10) + .setCellEditor(new DefaultCellEditor(moodsComboBox)); + + // Show only name in car column + getColumnModel().getColumn(11).setCellRenderer(new DefaultTableCellRenderer() { + @Override + public void setValue(Object value) { + if (value instanceof String) { + setText((String) value); + } else if (value instanceof Car) { + setText(((Car) value).getName()); + } else { + setText(""); + } + } + }); + + // Set up selection handler + getSelectionModel().addListSelectionListener(new ListSelectionListener() { + public void valueChanged(ListSelectionEvent e) { + int[] selectedRows = getSelectedRows(); + Set selectedHumans = new HashSet<>(); + for (int selectedRow : selectedRows) { + int modelRow = convertRowIndexToModel(selectedRow); + selectedHumans.add(data.get(modelRow)); + } + selectCallback.accept(selectedHumans); + } + }); + + updateData(initialData); + } + + private Human getUpdatedHuman(Human originHuman, int changedColumn, Object value) { + var builder = originHuman.toBuilder(); + var coordinates = originHuman.getCoordinates(); + var car = originHuman.getCar(); + switch (changedColumn) { + case 0: + builder.id((Long) value); + break; + case 1: + builder.name((String) value); + break; + case 2: + coordinates.setX((Float) value); + builder.coordinates(coordinates); + break; + case 3: + coordinates.setX((Float) value); + builder.coordinates(coordinates); + break; + case 4: + builder.creationDate((LocalDateTime) value); + break; + case 5: + builder.realHero((Boolean) value); + break; + case 6: + builder.hasToothpick((Boolean) value); + break; + case 7: + builder.impactSpeed((Double) value); + break; + case 8: + builder.soundtrackName((String) value); + break; + case 9: + builder.minutesOfWaiting((Float) value); + break; + case 10: + builder.mood((Mood) value); + break; + case 11: + car.setName((String) value); + builder.car(car); + break; + default: + break; + } + return builder.build(); + } + + private void fitColumnsToContent() { + for (int columnIndex = 0; columnIndex < this.getColumnCount(); columnIndex++) { + TableColumn column = this.getColumnModel().getColumn(columnIndex); + int preferredWidth = 0; + + // Calculate the preferred width based on column content + for (int rowIndex = 0; rowIndex < this.getRowCount(); rowIndex++) { + TableCellRenderer cellRenderer = this.getCellRenderer(rowIndex, + columnIndex); + Component cellComponent = this.prepareRenderer(cellRenderer, rowIndex, + columnIndex); + preferredWidth = Math.max(preferredWidth, + cellComponent.getPreferredSize().width); + } + + column.setPreferredWidth(preferredWidth); + } + } + + class TableModel extends DefaultTableModel { + public TableModel(String[] columnNames, int rowCount) { + super(columnNames, rowCount); + this.addTableModelListener(new TableModelListener() { + @Override + public void tableChanged(TableModelEvent e) { + int row = e.getFirstRow(); + if (row < 0) { + return; + } + + var originalHuman = data.get(row); + var humanId = originalHuman.getId(); + + int column = e.getColumn(); + if (column < 0) { + return; + } + + Object value = null; + try { + value = model.getValueAt(row, column); + } catch (Exception ex) { + return; + } + + // Check if human in row was modified earlier + // and use modified version as original + var modifiedOriginal = updatedData.stream() + .filter(human -> human.getId() == humanId) + .findFirst(); + + if (modifiedOriginal.isPresent()) { + originalHuman = modifiedOriginal.get(); + } + + var updatedHuman = getUpdatedHuman(originalHuman, column, value); + updatedData.removeIf(human -> human.getId() == humanId); + if (!data.contains(updatedHuman)) { + updatedData.add(updatedHuman); + } + updateCallback.accept(updatedData); + } + }); + } + + @Override + public Class getColumnClass(int columnIndex) { + switch (columnIndex) { + case 0: // id + return Long.class; + case 1: // name + return String.class; + case 2: // x + return Float.class; + case 3: // y + return Float.class; + case 4: // create date + return LocalDateTime.class; + case 5: // real hero + return Boolean.class; + case 6: // has toothpick + return Boolean.class; + case 7: // impact speed + return Double.class; + case 8: // soundtrack name + return String.class; + case 9: // minutes of waiting + return Float.class; + case 10: // mood + return String.class; + case 11: // car + return String.class; + default: + return Object.class; + } + } + } +} diff --git a/CollectionManager/desktop/src/main/java/desktop/pages/main/table/TablePanel.java b/CollectionManager/desktop/src/main/java/desktop/pages/main/table/TablePanel.java new file mode 100644 index 0000000..5c2ba1a --- /dev/null +++ b/CollectionManager/desktop/src/main/java/desktop/pages/main/table/TablePanel.java @@ -0,0 +1,79 @@ +package desktop.pages.main.table; + +import java.awt.BorderLayout; +import java.util.List; + +import javax.swing.JPanel; +import javax.swing.JScrollPane; + +import desktop.App; +import models.Human; +import textlocale.text.TextSupplier; + +public class TablePanel extends JPanel { + private TextSupplier ts = App.texts.getPackage("texts.main.table")::getText; + + private Table table; + + private final String[] columnNames = initColumns(); + + private Runnable updateCallback; + + private Runnable refreshCallback; + + public TablePanel(List initialData, Runnable updateCallback, + Runnable refreshCallback) { + this.updateCallback = updateCallback; + this.refreshCallback = refreshCallback; + init(initialData); + } + + private void init(List data) { + setLayout(new BorderLayout(0, 10)); + + // Table + table = new Table(columnNames, data); + var scrollPane = new JScrollPane(table); + add(scrollPane, BorderLayout.CENTER); + + // Filter field + var toolBar = new ToolBar(columnNames, table::filter); + table.setUpdateCallback(toolBar::notifyUpdate); + toolBar.setSaveCallback(humans -> { + refreshCallback.run(); + }); + table.setSelectCallback(toolBar::notifySelection); + toolBar.setDeleteCallback(humans -> { + refreshCallback.run(); + }); + toolBar.setAddCallback(human -> { + refreshCallback.run(); + }); + toolBar.setRefreshCallback(updateCallback); + toolBar.setScriptFinishHandler(refreshCallback); + add(toolBar, BorderLayout.NORTH); + + table.updateData(data); + } + + public void setHumans(List humans) { + table.updateData(humans); + } + + private String[] initColumns() { + return new String[] { + ts.t("columns.id"), + ts.t("columns.name"), + ts.t("columns.x"), + ts.t("columns.y"), + ts.t("columns.creationDate"), + ts.t("columns.realHero"), + ts.t("columns.toothpick"), + ts.t("columns.impactSpeed"), + ts.t("columns.soundtrack"), + ts.t("columns.waiting"), + ts.t("columns.mood"), + ts.t("columns.car") + }; + } +} diff --git a/CollectionManager/desktop/src/main/java/desktop/pages/main/table/ToolBar.java b/CollectionManager/desktop/src/main/java/desktop/pages/main/table/ToolBar.java new file mode 100644 index 0000000..dde89ff --- /dev/null +++ b/CollectionManager/desktop/src/main/java/desktop/pages/main/table/ToolBar.java @@ -0,0 +1,107 @@ +package desktop.pages.main.table; + +import java.util.List; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JPanel; +import javax.swing.JSeparator; + +import models.Human; + +public class ToolBar extends JPanel { + private Filter filter; + + private Add add; + + private Delete delete; + + private Save save; + + private Refresh refresh; + + private Script script; + + public ToolBar(String[] columnNames, BiConsumer filterCallback) { + init(columnNames, filterCallback); + } + + private void init(String[] columnNames, BiConsumer filterCallback) { + setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); + + addSeparator(); + + // Filter + filter = new Filter(columnNames, filterCallback); + add(filter); + + addSeparator(); + + // Add button + add = new Add(); + add(add); + + addSeparator(); + + // Delete button + delete = new Delete(); + add(delete); + + addSeparator(); + + // Save button + save = new Save(); + add(save); + + addSeparator(); + + // Script button + script = new Script(); + add(script); + + addSeparator(); + + // Refresh button + refresh = new Refresh(); + add(refresh); + + addSeparator(); + } + + private void addSeparator() { + add(Box.createHorizontalStrut(10)); + add(new JSeparator(JSeparator.VERTICAL)); + add(Box.createHorizontalStrut(10)); + } + + public void notifyUpdate(Set updatedHumans) { + save.notifyUpdate(updatedHumans); + } + + public void notifySelection(Set selected) { + delete.notifySelection(selected); + } + + public void setSaveCallback(Consumer> saveCallback) { + save.setSaveCallback(saveCallback); + } + + public void setDeleteCallback(Consumer> deleteCallback) { + delete.setDeleteCallback(deleteCallback); + } + + public void setAddCallback(Consumer addCallback) { + add.setAddCallback(addCallback); + } + + public void setRefreshCallback(Runnable refreshCallback) { + refresh.setRefreshCallback(refreshCallback); + } + + public void setScriptFinishHandler(Runnable handler) { + script.setFinishHandler(handler); + } +} diff --git a/CollectionManager/desktop/src/main/java/desktop/pages/main/viz/HumanSprite.java b/CollectionManager/desktop/src/main/java/desktop/pages/main/viz/HumanSprite.java new file mode 100644 index 0000000..5b0ea5b --- /dev/null +++ b/CollectionManager/desktop/src/main/java/desktop/pages/main/viz/HumanSprite.java @@ -0,0 +1,76 @@ +package desktop.pages.main.viz; + +import java.util.Optional; +import java.util.Random; + +import lombok.Getter; +import lombok.Setter; + +public class HumanSprite { + @Getter + private long id; + + @Getter + private float x; + + @Getter + private float y; + + @Getter + private final float radius; + + @Getter @Setter + private double speed; + + @Getter + private double angle; + + + public HumanSprite(long id, float x, float y, float radius, double speed) { + this.id = id; + this.x = x; + this.y = y; + this.radius = radius; + this.speed = speed; + this.angle = new Random().nextDouble() * 2 * Math.PI; + } + + public void move() { + x += speed * Math.cos(angle); + y += speed * Math.sin(angle); + } + + public void bounceWalls(int width, int height) { + if (x < radius) { + x = radius; + angle = Math.PI - angle; + } else if (x > width - radius) { + x = width - radius; + angle = Math.PI - angle; + } + if (y < radius) { + y = radius; + angle = -angle; + } else if (y > height - radius) { + y = height - radius; + angle = -angle; + } + } + + public void bounceSprite(HumanSprite other) { + var collisionAngle = getCollisionAngel(other); + if (collisionAngle.isPresent()) { + angle = collisionAngle.get(); + } + } + + public Optional getCollisionAngel(HumanSprite other) { + float dx = x - other.x; + float dy = y - other.y; + float distance = (float) Math.sqrt(dx * dx + dy * dy); + if (distance < radius + other.radius) { + return Optional.of(Math.atan2(dy, dx)); + } + return Optional.empty(); + } +} diff --git a/CollectionManager/desktop/src/main/java/desktop/pages/main/viz/SpritesCanvas.java b/CollectionManager/desktop/src/main/java/desktop/pages/main/viz/SpritesCanvas.java new file mode 100644 index 0000000..042cdbd --- /dev/null +++ b/CollectionManager/desktop/src/main/java/desktop/pages/main/viz/SpritesCanvas.java @@ -0,0 +1,86 @@ +package desktop.pages.main.viz; + +import java.awt.event.ActionEvent; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import javax.swing.JPanel; +import javax.swing.Timer; + +import lombok.Getter; +import lombok.Setter; + +public class SpritesCanvas extends JPanel { + @Getter + @Setter + private List sprites = new ArrayList<>(); + + private Timer timer; + + @Setter + private Consumer onSpriteClicked; + + public SpritesCanvas() { + timer = new Timer(10, this::moveSprites); + timer.start(); + this.addMouseListener(new SpritesSelector()); + } + + @Override + protected void paintComponent(java.awt.Graphics g) { + super.paintComponent(g); + for (HumanSprite sprite : sprites) { + g.fillOval( + (int) sprite.getX() - (int) sprite.getRadius(), + (int) sprite.getY() - (int) sprite.getRadius(), + (int) sprite.getRadius() * 2, + (int) sprite.getRadius() * 2); + } + } + + private void moveSprites(ActionEvent e) { + for (HumanSprite sprite : sprites) { + sprite.move(); + sprite.bounceWalls(getWidth(), getHeight()); + } + for (int i = 0; i < sprites.size(); i++) { + for (int j = i + 1; j < sprites.size(); j++) { + sprites.get(i).bounceSprite(sprites.get(j)); + } + } + repaint(); + } + + private class SpritesSelector implements MouseListener { + @Override + public void mouseClicked(MouseEvent e) { + } + + @Override + public void mousePressed(MouseEvent e) { + var dummy = new HumanSprite(0, e.getX(), e.getY(), 10, 0); + for (HumanSprite sprite : sprites) { + if (sprite.getCollisionAngel(sprite).isPresent()) { + if (onSpriteClicked != null) { + onSpriteClicked.accept(sprite); + } + } + } + } + + @Override + public void mouseReleased(MouseEvent e) { + } + + @Override + public void mouseEntered(MouseEvent e) { + } + + @Override + public void mouseExited(MouseEvent e) { + } + } +} diff --git a/CollectionManager/desktop/src/main/java/desktop/pages/main/viz/VizPanel.java b/CollectionManager/desktop/src/main/java/desktop/pages/main/viz/VizPanel.java new file mode 100644 index 0000000..21722ed --- /dev/null +++ b/CollectionManager/desktop/src/main/java/desktop/pages/main/viz/VizPanel.java @@ -0,0 +1,45 @@ +package desktop.pages.main.viz; + +import java.util.List; +import java.util.function.Consumer; + +import javax.swing.BoxLayout; +import javax.swing.JPanel; + +import models.Human; + +public class VizPanel extends JPanel { + private SpritesCanvas spritesCanvas; + + private final float SPRITE_SIZE = 10; + + public VizPanel(List initialHumans) { + init(initialHumans); + } + + private void init(List initialHumans) { + setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); + spritesCanvas = new SpritesCanvas(); + add(spritesCanvas); + + setHumans(initialHumans); + } + + public void setHumans(List humans) { + List sprites = humans.stream() + .map(h -> new HumanSprite( + h.getId(), + h.getCoordinates().getX(), + h.getCoordinates().getY(), + h.getMinutesOfWaiting() == null ? SPRITE_SIZE : h.getMinutesOfWaiting(), + h.getImpactSpeed())) + .toList(); + + spritesCanvas.setSprites(sprites); + } + + public void setOnSpriteClicked(Consumer sprite) { + spritesCanvas.setOnSpriteClicked(sprite); + } +} + diff --git a/CollectionManager/desktop/src/main/resources/desktop/icon.png b/CollectionManager/desktop/src/main/resources/desktop/icon.png new file mode 100644 index 0000000..9e16102 Binary files /dev/null and b/CollectionManager/desktop/src/main/resources/desktop/icon.png differ diff --git a/CollectionManager/desktop/src/main/resources/desktop/langs/en.png b/CollectionManager/desktop/src/main/resources/desktop/langs/en.png new file mode 100644 index 0000000..de84aed Binary files /dev/null and b/CollectionManager/desktop/src/main/resources/desktop/langs/en.png differ diff --git a/CollectionManager/desktop/src/main/resources/desktop/langs/ja.png b/CollectionManager/desktop/src/main/resources/desktop/langs/ja.png new file mode 100644 index 0000000..82b982d Binary files /dev/null and b/CollectionManager/desktop/src/main/resources/desktop/langs/ja.png differ diff --git a/CollectionManager/desktop/src/main/resources/desktop/langs/ru.png b/CollectionManager/desktop/src/main/resources/desktop/langs/ru.png new file mode 100644 index 0000000..54a5485 Binary files /dev/null and b/CollectionManager/desktop/src/main/resources/desktop/langs/ru.png differ diff --git a/CollectionManager/desktop/src/main/resources/desktop/texts.tl.json b/CollectionManager/desktop/src/main/resources/desktop/texts.tl.json new file mode 100644 index 0000000..34eaec0 --- /dev/null +++ b/CollectionManager/desktop/src/main/resources/desktop/texts.tl.json @@ -0,0 +1,415 @@ +{ + "app": { + "title": { + "en": "Collections Manager", + "ru": "Менеджер Коллекций", + "ja": "コレクションマネージャー" + } + }, + "auth": { + "signInCard": { + "title": { + "en": "Sign In", + "ru": "Войти", + "ja": "サインイン" + }, + "username": { + "title": { + "en": "Username:", + "ru": "Имя пользователя:", + "ja": "ユーザー名:" + } + }, + "password": { + "title": { + "en": "Password:", + "ru": "Пароль:", + "ja": "パスワード:" + } + }, + "signInButton": { + "en": "Sign In", + "ru": "Войти", + "ja": "サインイン" + }, + "openSignUpButton": { + "en": "Do not have an account? Sign Up!", + "ru": "Нет аккаунта? Зарегистрироваться!", + "ja": "アカウントがありませんか? サインアップ!" + } + }, + "signUpCard": { + "title": { + "en": "Sign Up", + "ru": "Зарегистрироваться", + "ja": "サインアップ" + }, + "username": { + "title": { + "en": "Username:", + "ru": "Имя пользователя:", + "ja": "ユーザー名:" + } + }, + "password": { + "title": { + "en": "Password:", + "ru": "Пароль:", + "ja": "パスワード:" + } + }, + "signUpButton": { + "en": "Sign Up", + "ru": "Зарегистрироваться", + "ja": "サインアップ" + }, + "openSignInButton": { + "en": "Already have an account? Sign In!", + "ru": "Уже есть аккаунт? Войти!", + "ja": "すでにアカウントをお持ちですか? サインイン!" + } + }, + "messages": { + "signInSuccess": { + "en": "You have successfully signed in.", + "ru": "Вы успешно вошли.", + "ja": "サインインに成功しました。" + }, + "signInError": { + "en": "Cannot sign in. Please try again later.", + "ru": "Невозможно войти. Пожалуйста, попробуйте позже.", + "ja": "サインインできません。後でもう一度試してください。" + }, + "signUpSuccess": { + "en": "You have successfully signed up.", + "ru": "Вы успешно зарегистрировались.", + "ja": "サインアップに成功しました。" + }, + "signUpError": { + "en": "Cannot sign up. Please try again later.", + "ru": "Невозможно зарегистрироваться. Пожалуйста, попробуйте позже.", + "ja": "サインアップできません。後でもう一度試してください。" + }, + "connectionError": { + "en": "Cannot connect to the server. Please try again later.", + "ru": "Невозможно подключиться к серверу. Пожалуйста, попробуйте позже.", + "ja": "サーバーに接続できません。後でもう一度試してください。" + }, + "invalidCredentials": { + "en": "Invalid username or password.", + "ru": "Неверное имя пользователя или пароль.", + "ja": "無効なユーザー名またはパスワード。" + }, + "tokenSaveError": { + "en": "Cannot save token. Please try again later.", + "ru": "Не удается сохранить токен. Пожалуйста, попробуйте позже.", + "ja": "トークンを保存できません。後でもう一度試してください。" + }, + "usernameTaken": { + "en": "Username is already taken.", + "ru": "Имя пользователя уже занято.", + "ja": "ユーザー名はすでに使用されています。" + } + } + }, + "main": { + "topbar": { + "contentSwitch": { + "openTable": { + "en": "Data management", + "ru": "Управление данными", + "ja": "データ管理" + }, + "openViz": { + "en": "Visualization", + "ru": "Визуализация", + "ja": "可視化" + } + } + }, + "table": { + "columns": { + "id": { + "en": "ID", + "ru": "ID", + "ja": "ID" + }, + "name": { + "en": "Name", + "ru": "Имя", + "ja": "名前" + }, + "x": { + "en": "X", + "ru": "X", + "ja": "X" + }, + "y": { + "en": "Y", + "ru": "Y", + "ja": "Y" + }, + "creationDate": { + "en": "Create Time", + "ru": "Время создания", + "ja": "作成時間" + }, + "realHero": { + "en": "Real Hero", + "ru": "Настоящий герой", + "ja": "リアルヒーロー" + }, + "toothpick": { + "en": "Toothpick", + "ru": "Зубочистка", + "ja": "つまようじ" + }, + "impactSpeed": { + "en": "Impact Speed", + "ru": "Скорость удара", + "ja": "衝撃速度" + }, + "soundtrack": { + "en": "Soundtrack", + "ru": "Звуковая дорожка", + "ja": "サウンドトラック" + }, + "waiting": { + "en": "Minutes of waiting", + "ru": "Минуты ожидания", + "ja": "待機時間(分)" + }, + "mood": { + "en": "Mood", + "ru": "Настроение", + "ja": "気分" + }, + "car": { + "en": "Car", + "ru": "Автомобиль", + "ja": "車" + } + }, + "toolbar": { + "script": { + "title": { + "en": "Run script", + "ru": "Запустить скрипт", + "ja": "スクリプトを実行する" + }, + "dialog": { + "runButton": { + "en": "Run", + "ru": "Запустить", + "ja": "実行する" + }, + "closeButton": { + "en": "Close", + "ru": "Закрыть", + "ja": "閉じる" + }, + "running": { + "en": "Running...", + "ru": "Скрипт исполняется...", + "ja": "実行中..." + }, + "done": { + "en": "Script was executed.", + "ru": "Скрипт завершен.", + "ja": "スクリプトが実行されました。" + } + } + }, + "refresh": { + "title": { + "en": "Refresh", + "ru": "Обновить", + "ja": "リフレッシュ" + } + }, + "filter": { + "columnSelect": { + "en": "Select column", + "ru": "Выберите столбец", + "ja": "列を選択" + }, + "searchRegex": { + "en": "Search regex", + "ru": "Поиск по регулярному выражению", + "ja": "正規表現で検索" + } + }, + "save": { + "updatedMessage": { + "en": "Data was changed.", + "ru": "Данные были изменены.", + "ja": "データが変更されました。" + }, + "savedMessage": { + "en": "Data is actual.", + "ru": "Данные актуальны.", + "ja": "データは現在のものです。" + }, + "savingMessage": { + "en": "Updated %d of %d items.", + "ru": "Обновлено %d из %d элементов.", + "ja": "%d個のアイテムの更新" + }, + "saveButton": { + "en": "Save", + "ru": "Сохранить", + "ja": "保存" + } + }, + "delete": { + "noItemsSelected": { + "en": "No items selected.", + "ru": "Нет выбранных элементов.", + "ja": "アイテムが選択されていません。" + }, + "itemsSelected": { + "en": "Press to remove %d items.", + "ru": "Нажмите, чтобы удалить %d элементов.", + "ja": "%d個のアイテムを削除するには押してください。" + }, + "deletingItems": { + "en": "Deleted %d of %d items.", + "ru": "Удалено %d из %d элементов.", + "ja": "%d個のアイテムを削除しました。" + }, + "deleteButton": { + "en": "Delete", + "ru": "Удалить", + "ja": "削除" + } + }, + "add": { + "addButton": { + "en": "Add", + "ru": "Добавить", + "ja": "追加" + }, + "title": { + "en": "Click to add a new item.", + "ru": "Щелкните, чтобы добавить новый элемент.", + "ja": "新しいアイテムを追加するにはクリックしてください。" + }, + "dialog": { + "success": { + "en": "Item was added.", + "ru": "Элемент был добавлен.", + "ja": "アイテムが追加されました。" + }, + "successTitle": { + "en": "Success", + "ru": "Успех", + "ja": "成功" + }, + "error": { + "en": "Cannot add the item.", + "ru": "Не удается добавить элемент.", + "ja": "アイテムを追加できません。" + }, + "title": { + "en": "Add a new human", + "ru": "Добавить нового человека", + "ja": "新しい人物を追加" + }, + "errors": { + "name": { + "en": "Name is required.", + "ru": "Имя обязательно.", + "ja": "名前は必須です。" + }, + "soundtrack": { + "en": "Soundtrack is required.", + "ru": "Звуковая дорожка обязательна.", + "ja": "サウンドトラックは必須です。" + }, + "car": { + "en": "Car is required.", + "ru": "Автомобиль обязателен.", + "ja": "車は必須です。" + }, + "add": { + "en": "Cannot add the item: %s.", + "ru": "Невозможно добавить элемент: %s.", + "ja": "アイテムを追加できません:%s。" + }, + "addTitle": { + "en": "Error", + "ru": "Ошибка", + "ja": "エラー" + } + }, + "name": { + "en": "Name:", + "ru": "Имя:", + "ja": "名前:" + }, + "coordinates": { + "en": "Coordinates:", + "ru": "Координаты:", + "ja": "座標:" + }, + "x": { + "en": "X", + "ru": "X", + "ja": "X" + }, + "y": { + "en": "Y", + "ru": "Y", + "ja": "Y" + }, + "realHero": { + "en": "Real Hero", + "ru": "Настоящий герой", + "ja": "リアルヒーロー" + }, + "toothpick": { + "en": "Toothpick", + "ru": "Зубочистка", + "ja": "つまようじ" + }, + "impactSpeed": { + "en": "Impact Speed:", + "ru": "Скорость удара:", + "ja": "衝撃速度:" + }, + "soundtrack": { + "en": "Soundtrack:", + "ru": "Звуковая дорожка:", + "ja": "サウンドトラック:" + }, + "waiting": { + "en": "Minutes of waiting:", + "ru": "Минуты ожидания:", + "ja": "待機時間(分):" + }, + "mood": { + "en": "Mood:", + "ru": "Настроение:", + "ja": "気分:" + }, + "car": { + "en": "Car:", + "ru": "Автомобиль:", + "ja": "車:" + }, + "addButton": { + "en": "Add", + "ru": "Добавить", + "ja": "追加" + }, + "cancelButton": { + "en": "Cancel", + "ru": "Отмена", + "ja": "キャンセル" + } + } + } + } + } + } +} diff --git a/CollectionManager/pom.xml b/CollectionManager/pom.xml index 7a3c4a4..1db6bcf 100644 --- a/CollectionManager/pom.xml +++ b/CollectionManager/pom.xml @@ -1,83 +1,83 @@ - - 4.0.0 - - mamsdeveloper.itmo - collection-manager - 1.0 - pom - Applications for collection management - - - cliapp - collections-service - auth-service - shared - - - - UTF-8 - UTF-8 - - - - - jitpack.io - https://jitpack.io - - - - - - - junit - junit - 4.12 - test - - - org.projectlombok - lombok - 1.18.26 - provided - - - com.github.Mam-sDeveloper-ITMO - fqme - 2.0.1 - - - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 2.9.1 - - false - ./site - javadocs - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.5.1 - - 1.8 - 1.8 - - - org.projectlombok - lombok - 1.18.26 - - - - - - - \ No newline at end of file + + + 4.0.0 + mamsdeveloper.itmo + collection-manager + 1.0 + pom + Applications for collection management + + + cliapp + collections-service + auth-service + shared + desktop + + + + UTF-8 + UTF-8 + + + + + jitpack.io + https://jitpack.io + + + + + + + junit + junit + 4.12 + test + + + org.projectlombok + lombok + 1.18.26 + provided + + + com.github.Mam-sDeveloper-ITMO + fqme + 2.0.1 + + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.9.1 + + false + ./site + javadocs + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.5.1 + + 17 + 17 + + + org.projectlombok + lombok + 1.18.26 + + + + + + + diff --git a/CollectionManager/shared/models/src/main/java/models/Human.java b/CollectionManager/shared/models/src/main/java/models/Human.java index 5357ded..ff05b22 100644 --- a/CollectionManager/shared/models/src/main/java/models/Human.java +++ b/CollectionManager/shared/models/src/main/java/models/Human.java @@ -15,8 +15,8 @@ * @see Programming course * website */ -@Builder -@EqualsAndHashCode(of = { "id" }) +@Builder(toBuilder = true) +@EqualsAndHashCode public class Human implements Comparable, Serializable { /** diff --git a/CollectionManager/shared/textlocale/src/main/java/textlocale/loader/common/JarLoader.java b/CollectionManager/shared/textlocale/src/main/java/textlocale/loader/common/JarLoader.java index 443a6c1..712966d 100644 --- a/CollectionManager/shared/textlocale/src/main/java/textlocale/loader/common/JarLoader.java +++ b/CollectionManager/shared/textlocale/src/main/java/textlocale/loader/common/JarLoader.java @@ -81,6 +81,6 @@ private void processFile(String filePath, JarFile jarFile, JarEntry fileEntry, F private String readFileContent(JarFile jarFile, JarEntry entry) throws IOException { byte[] contentBytes = new byte[(int) entry.getSize()]; jarFile.getInputStream(entry).read(contentBytes); - return new String(contentBytes); + return new String(contentBytes, "UTF-8"); } } \ No newline at end of file