diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..273fff0
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,11 @@
+# To get started with Dependabot version updates, you'll need to specify which
+# package ecosystems to update and where the package manifests are located.
+# Please see the documentation for all configuration options:
+# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
+
+version: 2
+updates:
+ - package-ecosystem: "maven" # See documentation for possible values
+ directory: "/" # Location of package manifests
+ schedule:
+ interval: "weekly"
diff --git a/.github/ic.png b/.github/ic.png
new file mode 100644
index 0000000..dc7f147
Binary files /dev/null and b/.github/ic.png differ
diff --git a/.github/im1.png b/.github/im1.png
new file mode 100644
index 0000000..48298c7
Binary files /dev/null and b/.github/im1.png differ
diff --git a/.github/im2.png b/.github/im2.png
new file mode 100644
index 0000000..f99978a
Binary files /dev/null and b/.github/im2.png differ
diff --git a/.github/im3.png b/.github/im3.png
new file mode 100644
index 0000000..632eb88
Binary files /dev/null and b/.github/im3.png differ
diff --git a/.github/im4.png b/.github/im4.png
new file mode 100644
index 0000000..5fee082
Binary files /dev/null and b/.github/im4.png differ
diff --git a/.github/mvvm.png b/.github/mvvm.png
new file mode 100644
index 0000000..8480136
Binary files /dev/null and b/.github/mvvm.png differ
diff --git a/content/Capture.PNG b/.github/version1.png
similarity index 100%
rename from content/Capture.PNG
rename to .github/version1.png
diff --git a/content/2m.png b/.github/version2.png
similarity index 100%
rename from content/2m.png
rename to .github/version2.png
diff --git a/content/1.png b/.github/version3.png
similarity index 100%
rename from content/1.png
rename to .github/version3.png
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..5555625
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,31 @@
+name: build
+
+on:
+ push:
+ branches:
+ - "master"
+ paths:
+ - 'src/**'
+ - '.github/**'
+ - 'pom.xml'
+ pull_request:
+ branches:
+ - "master"
+ paths:
+ - 'src/**'
+ - '.github/**'
+ - 'pom.xml'
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v3
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+ - name: Build with Maven
+ run: mvn -B package --file pom.xml
\ No newline at end of file
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..2aa2215
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,36 @@
+name: Create Release
+
+on:
+ push:
+ tags:
+ - 'v*.*.*'
+ branches:
+ - "master"
+
+jobs:
+ release:
+ if: github.event_name == 'push' && contains(github.ref, 'refs/heads/master') && startsWith(github.ref, 'refs/tags/v')
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v3
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+ - name: Build with Maven
+ run: mvn -B package --file pom.xml
+
+ - name: Extract version
+ run: echo "VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)" >> $GITHUB_ENV
+
+ - name: Create GitHub Release
+ uses: softprops/action-gh-release@v2
+ with:
+ files: |
+ target/jfxcrypto-${{ env.VERSION }}.jar
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index f63a24e..e23809d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,33 @@
-.idea
+# Java
+.mtj.tmp/
+*.class
+*.jar
+*.war
+*.ear
+*.nar
+hs_err_pid*
+replay_pid*
+
+# Intellij Idea
+out/
+.idea/
+.idea_modules/
*.iml
-out
+*.ipr
+*.iws
+
+# Maven
+target/
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+pom.xml.bak
+release.properties
+dependency-reduced-pom.xml
+buildNumber.properties
+.mvn/timing.properties
+.mvn/wrapper/maven-wrapper.jar
+
+# OS X
+.DS_Store
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..b3f953d
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,6 @@
+Please try to create bug reports that are:
+
+- Reproducible. Include steps to reproduce the problem.
+- Specific. Include as much detail as possible: which version, what environment, etc.
+- Unique. Do not duplicate existing opened issues.
+- Scoped to a Single Bug. One bug per report.
diff --git a/LICENSE b/LICENSE
index 3a7cce9..f49a4e1 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,21 +1,201 @@
-MIT License
-
-Copyright (c) 2019 MasterFlomaster1
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
\ No newline at end of file
diff --git a/README.md b/README.md
index 0d3bc9f..36a6eed 100644
--- a/README.md
+++ b/README.md
@@ -1,93 +1,81 @@
This method uses traditional IO operations, which may be less efficient compared to NIO. Ensure that the
+ * {@code Cipher} instance is properly initialized for decryption before calling this method.
+ *
+ * @param cipher The {@link Cipher} instance initialized for decryption.
+ * @param target The path to the source file to decrypt.
+ * @param destination The path to the destination file where decrypted data will be written.
+ */
+ public static void encrypt(Cipher cipher, Path target, Path destination) {
+ try (FileInputStream fis = new FileInputStream(target.toString());
+ FileOutputStream fos = new FileOutputStream(destination.toString());
+ CipherOutputStream cos = new CipherOutputStream(fos, cipher)) {
+
+ byte[] buffer = new byte[1024];
+ int bytesRead;
+
+ while ((bytesRead = fis.read(buffer)) != -1) {
+ cos.write(buffer, 0, bytesRead);
+ }
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Decrypts the content of a file and writes the decrypted data to a destination file using traditional IO.
+ *
+ *
This method uses traditional IO operations, which may be less efficient compared to NIO. Ensure that the
+ * {@code Cipher} instance is properly initialized for decryption before calling this method.
+ *
+ * @param cipher The {@link Cipher} instance initialized for decryption.
+ * @param target The path to the source file to decrypt.
+ * @param destination The path to the destination file where decrypted data will be written.
+ */
+ public static void decrypt(Cipher cipher, Path target, Path destination) {
+ try (FileInputStream fis = new FileInputStream(target.toString());
+ FileOutputStream fos = new FileOutputStream(destination.toString());
+ CipherInputStream cis = new CipherInputStream(fis, cipher)) {
+
+ byte[] buffer = new byte[1024];
+ int bytesRead;
+
+ while ((bytesRead = cis.read(buffer)) != -1) {
+ fos.write(buffer, 0, bytesRead);
+ }
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Encrypts or decrypts the content of a file and writes the result to a destination file using NIO boosting
+ * operation speed.
+ *
+ * @param cipher The Cipher instance already initialized for encryption or decryption.
+ * @param target The path to the source file to process.
+ * @param destination The path to the destination file where processed data will be written.
+ */
+ public static void nioEncryptAndDecrypt(Cipher cipher, Path target, Path destination) {
+ try (FileChannel sourceChannel = FileChannel.open(target, StandardOpenOption.READ);
+ FileChannel destChannel = FileChannel.open(destination,
+ StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {
+
+ ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
+ ByteBuffer encryptedBuffer;
+
+ while (sourceChannel.read(buffer) != -1) {
+ buffer.flip();
+ encryptedBuffer = ByteBuffer.wrap(cipher.update(buffer.array(), buffer.position(), buffer.remaining()));
+ destChannel.write(encryptedBuffer);
+ buffer.clear();
+ }
+
+ encryptedBuffer = ByteBuffer.wrap(cipher.doFinal());
+ destChannel.write(encryptedBuffer);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/crypto/KeyPairPersistence.java b/src/main/java/dev/masterflomaster1/jfxc/crypto/KeyPairPersistence.java
new file mode 100644
index 0000000..4d8edd2
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/crypto/KeyPairPersistence.java
@@ -0,0 +1,14 @@
+package dev.masterflomaster1.jfxc.crypto;
+
+import java.nio.file.Path;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+
+public interface KeyPairPersistence extends KeyPersistence {
+
+ void exportPublicKey(Path target, PublicKey publicKey);
+ void exportPrivateKey(Path target, PrivateKey privateKey);
+ PublicKey importPublicKey(Path target, String keyGenAlgorithm);
+ PrivateKey importPrivateKey(Path target, String keyGenAlgorithm);
+
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/crypto/KeyPersistence.java b/src/main/java/dev/masterflomaster1/jfxc/crypto/KeyPersistence.java
new file mode 100644
index 0000000..db1854b
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/crypto/KeyPersistence.java
@@ -0,0 +1,4 @@
+package dev.masterflomaster1.jfxc.crypto;
+
+public interface KeyPersistence {
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/crypto/MacImpl.java b/src/main/java/dev/masterflomaster1/jfxc/crypto/MacImpl.java
new file mode 100644
index 0000000..c6ab89b
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/crypto/MacImpl.java
@@ -0,0 +1,24 @@
+package dev.masterflomaster1.jfxc.crypto;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+
+public final class MacImpl {
+
+ private MacImpl() { }
+
+ public static byte[] hmac(String algorithm, byte[] key, byte[] value) {
+ try {
+ Mac mac = Mac.getInstance(algorithm, "BC");
+ SecretKeySpec keySpec = new SecretKeySpec(key, algorithm);
+ mac.init(keySpec);
+ return mac.doFinal(value);
+ } catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchProviderException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/crypto/PbeImpl.java b/src/main/java/dev/masterflomaster1/jfxc/crypto/PbeImpl.java
new file mode 100644
index 0000000..7afd890
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/crypto/PbeImpl.java
@@ -0,0 +1,28 @@
+package dev.masterflomaster1.jfxc.crypto;
+
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.KeySpec;
+import java.util.concurrent.CompletableFuture;
+
+public final class PbeImpl {
+
+ private PbeImpl() { }
+
+ public static CompletableFuture asyncHash(String algorithm, char[] password, byte[] salt, int iter, int kLen) {
+ return CompletableFuture.supplyAsync(() -> {
+ KeySpec spec = new PBEKeySpec(password, salt, iter, kLen);
+
+ try {
+ SecretKeyFactory factory = SecretKeyFactory.getInstance(algorithm, "BC");
+ return factory.generateSecret(spec).getEncoded();
+ } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidKeySpecException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/crypto/PemKeyPairPersistence.java b/src/main/java/dev/masterflomaster1/jfxc/crypto/PemKeyPairPersistence.java
new file mode 100644
index 0000000..d167a13
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/crypto/PemKeyPairPersistence.java
@@ -0,0 +1,85 @@
+package dev.masterflomaster1.jfxc.crypto;
+
+import org.bouncycastle.util.io.pem.PemObject;
+import org.bouncycastle.util.io.pem.PemReader;
+import org.bouncycastle.util.io.pem.PemWriter;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+
+public class PemKeyPairPersistence implements KeyPairPersistence {
+
+ @Override
+ public void exportPublicKey(Path target, PublicKey publicKey) {
+ PemObject pemObject = new PemObject("PUBLIC KEY", publicKey.getEncoded());
+
+ try (StringWriter stringWriter = new StringWriter(); PemWriter pemWriter = new PemWriter(stringWriter)) {
+ pemWriter.writeObject(pemObject);
+ pemWriter.flush();
+ Files.writeString(target, stringWriter.toString());
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void exportPrivateKey(Path target, PrivateKey privateKey) {
+ PemObject pemObject = new PemObject("PRIVATE KEY", privateKey.getEncoded());
+
+ try (StringWriter stringWriter = new StringWriter(); PemWriter pemWriter = new PemWriter(stringWriter)) {
+ pemWriter.writeObject(pemObject);
+ pemWriter.flush();
+ Files.writeString(target, stringWriter.toString());
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public PublicKey importPublicKey(Path target, String keyGenAlgorithm) {
+ try {
+ String pemContent = Files.readString(target, StandardCharsets.UTF_8);
+
+ try (PemReader pemReader = new PemReader(new StringReader(pemContent))) {
+ PemObject pemObject = pemReader.readPemObject();
+ byte[] keyBytes = pemObject.getContent();
+ X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
+ KeyFactory keyFactory = KeyFactory.getInstance(keyGenAlgorithm, "BC");
+ return keyFactory.generatePublic(spec);
+ }
+
+ } catch (IOException | NoSuchAlgorithmException | NoSuchProviderException | InvalidKeySpecException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public PrivateKey importPrivateKey(Path target, String keyGenAlgorithm) {
+ try {
+ String pemContent = Files.readString(target, StandardCharsets.UTF_8);
+
+ try (PemReader pemReader = new PemReader(new StringReader(pemContent))) {
+ PemObject pemObject = pemReader.readPemObject();
+ byte[] keyBytes = pemObject.getContent();
+ PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
+ KeyFactory keyFactory = KeyFactory.getInstance(keyGenAlgorithm, "BC");
+ return keyFactory.generatePrivate(spec);
+ }
+
+ } catch (IOException | NoSuchAlgorithmException | NoSuchProviderException | InvalidKeySpecException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/crypto/SecurityUtils.java b/src/main/java/dev/masterflomaster1/jfxc/crypto/SecurityUtils.java
new file mode 100644
index 0000000..3ae96a6
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/crypto/SecurityUtils.java
@@ -0,0 +1,191 @@
+package dev.masterflomaster1.jfxc.crypto;
+
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+
+import java.security.Provider;
+import java.security.SecureRandom;
+import java.security.Security;
+import java.util.List;
+import java.util.TreeSet;
+import java.util.concurrent.ExecutionException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public final class SecurityUtils {
+
+ private static final SecureRandom SECURE_RANDOM = new SecureRandom();
+
+ private static final TreeSet DIGESTS = new TreeSet<>();
+ private static final TreeSet HMACS = new TreeSet<>();
+ private static final TreeSet PBKDFS = new TreeSet<>();
+ private static final TreeSet BLOCK_CIPHERS = new TreeSet<>();
+ private static final TreeSet STREAM_CIPHERS = new TreeSet<>();
+ private static final TreeSet ASYMMETRIC_CIPHERS = new TreeSet<>();
+ private static final TreeSet HYBRID_ASYMMETRIC_CIPHERS = new TreeSet<>();
+ private static final Pattern OID_PATTERN = Pattern.compile("^(OID\\.)?(\\d+\\.)+\\d+$");
+
+ private static final PemKeyPairPersistence pemKeyPairPersistence = new PemKeyPairPersistence();
+
+ private SecurityUtils() { }
+
+ public static TreeSet getDigests() {
+ return DIGESTS;
+ }
+
+ public static TreeSet getHmacs() {
+ return HMACS;
+ }
+
+ public static TreeSet getPbkdfs() {
+ return PBKDFS;
+ }
+
+ public static TreeSet getBlockCiphers() {
+ return BLOCK_CIPHERS;
+ }
+
+ public static TreeSet getStreamCiphers() {
+ return STREAM_CIPHERS;
+ }
+
+ public static TreeSet getAsymmetricCiphers() {
+ return ASYMMETRIC_CIPHERS;
+ }
+
+ public static TreeSet getHybridAsymmetricCiphers() {
+ return HYBRID_ASYMMETRIC_CIPHERS;
+ }
+
+ public static PemKeyPairPersistence getPemKeyPairPersistence() {
+ return pemKeyPairPersistence;
+ }
+
+ public static void init() {
+ // Setup Unlimited Strength Jurisdiction Policy
+ Security.setProperty("crypto.policy", "unlimited");
+ Security.addProvider(new BouncyCastleProvider());
+ Provider p = Security.getProvider("BC");
+
+ p.getServices().stream()
+ .filter(s -> !SecurityUtils.isOID(s.getAlgorithm()))
+ .forEach(s -> {
+ String type = s.getType();
+ String algorithm = s.getAlgorithm();
+
+ if ("SecretKeyFactory".equals(type) && algorithm.startsWith("PBKDF2")) {
+ PBKDFS.add(algorithm);
+ } else if ("MessageDigest".equals(type)) {
+ DIGESTS.add(algorithm);
+ } else if ("Mac".equals(type) && algorithm.startsWith("HMAC")) {
+ HMACS.add(algorithm);
+ }
+ });
+
+
+ BLOCK_CIPHERS.addAll(List.of(
+ "AES",
+ "BLOWFISH",
+ "CAMELLIA",
+ "CAST5",
+ "CAST6",
+ "DES",
+ "DESEDE",
+ "DSTU7624",
+ "GOST28147",
+ "GOST3412-2015",
+ "IDEA",
+ "NOEKEON",
+ "RC2",
+ "RC5",
+ "RC6",
+ "RIJNDAEL",
+ "SEED",
+ "SHACAL-2",
+ "SKIPJACK",
+ "SM4",
+ "Serpent",
+ "TEA",
+ "Threefish-1024",
+ "Threefish-256",
+ "Threefish-512",
+ "Tnepres",
+ "Twofish",
+ "XTEA"
+ ));
+
+ STREAM_CIPHERS.addAll(List.of(
+ "ARC4",
+ "CHACHA",
+ "CHACHA20-POLY1305",
+ "CHACHA7539",
+ "Grain128",
+ "Grainv1",
+ "HC128",
+ "HC256",
+ "SALSA20",
+ "XSALSA20",
+ "ZUC-128",
+ "ZUC-256",
+ "VMPC",
+ "VMPC-KSA3"
+ ));
+
+ // Algorithms excluded: NTRU, ELGAMAL/PKCS1, RSA/1, RSA/2, RSA/ISO9796-1, RSA/OAEP, RSA/PKCS1, RSA/RAW
+
+ ASYMMETRIC_CIPHERS.addAll(List.of(
+ "DHIES",
+ "ECIES",
+ "ECIESwithSHA1",
+ "ECIESwithSHA256",
+ "ECIESwithSHA384",
+ "ECIESwithSHA512",
+ "ELGAMAL",
+ "IES",
+ "RSA"
+ ));
+
+ HYBRID_ASYMMETRIC_CIPHERS.addAll(List.of(
+ "DHIESWITHDESEDE-CBC",
+ "DHIESwithAES-CBC",
+ "ECIESwithAES-CBC",
+ "ECIESwithDESEDE-CBC",
+ "ECIESwithSHA1andAES-CBC",
+ "ECIESwithSHA1andDESEDE-CBC",
+ "ECIESwithSHA256andAES-CBC",
+ "ECIESwithSHA256andDESEDE-CBC",
+ "ECIESwithSHA384andAES-CBC",
+ "ECIESwithSHA384andDESEDE-CBC",
+ "ECIESwithSHA512andAES-CBC",
+ "ECIESwithSHA512andDESEDE-CBC",
+ "IESWITHDESEDE-CBC",
+ "IESwithAES-CBC"
+ ));
+ }
+
+ private static boolean isOID(String input) {
+ Matcher matcher = OID_PATTERN.matcher(input);
+ return matcher.matches();
+ }
+
+ public static byte[] generatePasswordBasedKey(char[] password, int keySize, byte[] salt) {
+ var f = PbeImpl.asyncHash("PBKDF2", password, salt, 10000, keySize);
+
+ try {
+ return f.get();
+ } catch (InterruptedException | ExecutionException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static byte[] generateIV(int lengthBits) {
+ byte[] iv = new byte[lengthBits / 8];
+ SECURE_RANDOM.nextBytes(iv);
+ return iv;
+ }
+
+ public static byte[] generateSalt() {
+ byte[] salt = new byte[16];
+ SECURE_RANDOM.nextBytes(salt);
+ return salt;
+ }
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/crypto/StreamCipherImpl.java b/src/main/java/dev/masterflomaster1/jfxc/crypto/StreamCipherImpl.java
new file mode 100644
index 0000000..489eef2
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/crypto/StreamCipherImpl.java
@@ -0,0 +1,111 @@
+package dev.masterflomaster1.jfxc.crypto;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.file.Path;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.util.List;
+import java.util.Optional;
+
+public final class StreamCipherImpl {
+
+ private StreamCipherImpl() { }
+
+ public static byte[] encrypt(String algorithm, byte[] iv, byte[] inputData, byte[] key) {
+ try {
+ SecretKey secretKey = new SecretKeySpec(key, algorithm);
+ Cipher cipher = Cipher.getInstance(algorithm, "BC");
+
+ if (getCorrespondingIvLengthBits(algorithm).isPresent())
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));
+ else
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey);
+
+ return cipher.doFinal(inputData);
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException | NoSuchProviderException | InvalidKeyException |
+ IllegalBlockSizeException | BadPaddingException | InvalidAlgorithmParameterException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static byte[] decrypt(String algorithm, byte[] iv, byte[] inputData, byte[] key) {
+ try {
+ SecretKey secretKey = new SecretKeySpec(key, algorithm);
+ Cipher cipher = Cipher.getInstance(algorithm, "BC");
+
+ if (getCorrespondingIvLengthBits(algorithm).isPresent())
+ cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));
+ else
+ cipher.init(Cipher.DECRYPT_MODE, secretKey);
+
+ return cipher.doFinal(inputData);
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException | NoSuchProviderException | InvalidKeyException |
+ IllegalBlockSizeException | BadPaddingException | InvalidAlgorithmParameterException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static void nioEncrypt(Path target, Path destination, String algorithm, byte[] iv, byte[] key) {
+ try {
+ SecretKey secretKey = new SecretKeySpec(key, algorithm);
+ Cipher cipher = Cipher.getInstance(algorithm, "BC");
+
+ if (getCorrespondingIvLengthBits(algorithm).isPresent())
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));
+ else
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey);
+
+ FileOperations.nioEncryptAndDecrypt(cipher, target, destination);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static void nioDecrypt(Path target, Path destination, String algorithm, byte[] iv, byte[] key) {
+ try {
+ SecretKey secretKey = new SecretKeySpec(key, algorithm);
+ Cipher cipher = Cipher.getInstance(algorithm, "BC");
+
+ if (getCorrespondingIvLengthBits(algorithm).isPresent())
+ cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));
+ else
+ cipher.init(Cipher.DECRYPT_MODE, secretKey);
+
+ FileOperations.nioEncryptAndDecrypt(cipher, target, destination);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static List getCorrespondingKeyLengths(String algorithm) {
+ return switch (algorithm) {
+ case "ARC4", "Grain128", "HC128", "ZUC-128", "VMPC", "VMPC-KSA3" -> List.of(128);
+ case "CHACHA", "CHACHA7539", "CHACHA20-POLY1305", "HC256", "ZUC-256", "XSALSA20" -> List.of(256);
+ case "SALSA20" -> List.of(128, 256);
+ case "Grainv1" -> List.of(80);
+ default -> throw new RuntimeException("Unsupported algorithm: " + algorithm);
+ };
+ }
+
+ public static Optional> getCorrespondingIvLengthBits(String algorithm) {
+ return switch (algorithm) {
+ case "ARC4" -> Optional.empty();
+ case "CHACHA", "Grainv1", "SALSA20" -> Optional.of(List.of(64));
+ case "CHACHA7539", "CHACHA20-POLY1305", "Grain128" -> Optional.of(List.of(96));
+ case "HC128", "ZUC-128" -> Optional.of(List.of(128));
+ case "HC256" -> Optional.of(List.of(256));
+ case "XSALSA20" -> Optional.of(List.of(192));
+ case "ZUC-256" -> Optional.of(List.of(200));
+ case "VMPC", "VMPC-KSA3" -> Optional.of(List.of(8, 16, 32, 64, 128, 256, 512));
+ default -> throw new RuntimeException("Unsupported algorithm: " + algorithm);
+ };
+ }
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/crypto/UnkeyedCryptoHash.java b/src/main/java/dev/masterflomaster1/jfxc/crypto/UnkeyedCryptoHash.java
new file mode 100644
index 0000000..92cdc66
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/crypto/UnkeyedCryptoHash.java
@@ -0,0 +1,81 @@
+package dev.masterflomaster1.jfxc.crypto;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.AsynchronousFileChannel;
+import java.nio.channels.CompletionHandler;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.util.concurrent.CompletableFuture;
+
+public final class UnkeyedCryptoHash {
+
+ private UnkeyedCryptoHash() { }
+
+ public static byte[] hash(String algorithm, byte[] value) {
+ try {
+ MessageDigest md = MessageDigest.getInstance(algorithm, "BC");
+ md.update(value);
+ return md.digest();
+ } catch (NoSuchAlgorithmException | NoSuchProviderException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static CompletableFuture asyncHash(String algorithm, String filePath) {
+ CompletableFuture future = new CompletableFuture<>();
+
+ try {
+ Path path = Paths.get(filePath);
+ AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);
+ MessageDigest digest = MessageDigest.getInstance(algorithm, "BC");
+
+ ByteBuffer buffer = ByteBuffer.allocate(1024);
+ long[] position = {0};
+
+ CompletionHandler handler = new CompletionHandler<>() {
+ @Override
+ public void completed(Integer result, ByteBuffer attachment) {
+ if (result == -1) {
+ byte[] hashBytes = digest.digest();
+ future.complete(hashBytes);
+ try {
+ fileChannel.close();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return;
+ }
+
+ buffer.flip();
+ digest.update(buffer);
+ buffer.clear();
+
+ position[0] += result;
+ fileChannel.read(buffer, position[0], buffer, this);
+ }
+
+ @Override
+ public void failed(Throwable exc, ByteBuffer attachment) {
+ future.completeExceptionally(exc);
+ try {
+ fileChannel.close();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ };
+
+ fileChannel.read(buffer, 0, buffer, handler);
+ } catch (IOException | NoSuchProviderException | NoSuchAlgorithmException e) {
+ future.completeExceptionally(e);
+ }
+
+ return future;
+ }
+
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/crypto/classic/ADFGVXImpl.java b/src/main/java/dev/masterflomaster1/jfxc/crypto/classic/ADFGVXImpl.java
new file mode 100644
index 0000000..94d9e0c
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/crypto/classic/ADFGVXImpl.java
@@ -0,0 +1,130 @@
+package dev.masterflomaster1.jfxc.crypto.classic;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Cipher from WW1, which substitutes and transposes
+ *
+ * @see ADFGVX cipher
+ * @see Online encoder
+ */
+public final class ADFGVXImpl {
+
+ private static final char[] ADFGVX = {'A', 'D', 'F', 'G', 'V', 'X'};
+ private static final char[][] TABLE = {
+ {'A', 'B', 'C', 'D', 'E', 'F'},
+ {'G', 'H', 'I', 'J', 'K', 'L'},
+ {'M', 'N', 'O', 'P', 'Q', 'R'},
+ {'S', 'T', 'U', 'V', 'W', 'X'},
+ {'Y', 'Z', '0', '1', '2', '3'},
+ {'4', '5', '6', '7', '8', '9'}
+ };
+
+ private static final Map CHAR_TO_PAIR = new HashMap<>();
+ private static final Map PAIR_TO_CHAR = new HashMap<>();
+
+ static {
+ // Create the map for character to pair of ADFGVX
+ for (int row = 0; row < 6; row++) {
+ for (int col = 0; col < 6; col++) {
+ CHAR_TO_PAIR.put(TABLE[row][col], "" + ADFGVX[row] + ADFGVX[col]);
+ PAIR_TO_CHAR.put("" + ADFGVX[row] + ADFGVX[col], TABLE[row][col]);
+ }
+ }
+ }
+
+ private ADFGVXImpl() { }
+
+ public static String encrypt(String plaintext, String keyword) {
+ // Remove spaces and convert to uppercase
+ plaintext = plaintext.replaceAll("\\s+", "").toUpperCase();
+
+ // Step 1: Replace each letter in plaintext with corresponding ADFGVX pair
+ StringBuilder intermediate = new StringBuilder();
+ for (char ch : plaintext.toCharArray()) {
+ if (CHAR_TO_PAIR.containsKey(ch)) {
+ intermediate.append(CHAR_TO_PAIR.get(ch));
+ }
+ }
+
+ // Step 2: Arrange the intermediate text into columns based on the keyword
+ int numCols = keyword.length();
+ int numRows = (int) Math.ceil((double) intermediate.length() / numCols);
+ char[][] table = new char[numRows][numCols];
+ for (char[] row : table) {
+ Arrays.fill(row, ' '); // Fill with space to indicate empty cells
+ }
+ for (int i = 0; i < intermediate.length(); i++) {
+ table[i / numCols][i % numCols] = intermediate.charAt(i);
+ }
+
+ // Step 3: Read columns in the order of the sorted keyword
+ char[] sortedKeyword = keyword.toCharArray();
+ Arrays.sort(sortedKeyword);
+
+ StringBuilder ciphertext = new StringBuilder();
+ for (char keyChar : sortedKeyword) {
+ int col = keyword.indexOf(keyChar);
+ for (int row = 0; row < numRows; row++) {
+ if (table[row][col] != ' ') { // Only add non-empty cells
+ ciphertext.append(table[row][col]);
+ }
+ }
+ }
+
+ return ciphertext.toString();
+ }
+
+ public static String decrypt(String ciphertext, String keyword) {
+ int numCols = keyword.length();
+ int numRows = (int) Math.ceil((double) ciphertext.length() / numCols);
+
+ // Step 1: Determine the number of characters in each column
+ int[] colLengths = new int[numCols];
+ Arrays.fill(colLengths, numRows);
+ int numExtra = (numCols * numRows) - ciphertext.length();
+ for (int i = numCols - numExtra; i < numCols; i++) {
+ colLengths[i]--;
+ }
+
+ // Step 2: Arrange the ciphertext into columns based on the sorted keyword
+ char[] sortedKeyword = keyword.toCharArray();
+ Arrays.sort(sortedKeyword);
+
+ char[][] table = new char[numRows][numCols];
+ int index = 0;
+ for (char keyChar : sortedKeyword) {
+ int col = keyword.indexOf(keyChar);
+ for (int row = 0; row < colLengths[col]; row++) {
+ if (index < ciphertext.length()) {
+ table[row][col] = ciphertext.charAt(index++);
+ }
+ }
+ }
+
+ // Step 3: Read the table row-wise to get the intermediate text
+ StringBuilder intermediate = new StringBuilder();
+ for (int row = 0; row < numRows; row++) {
+ for (int col = 0; col < numCols; col++) {
+ if (table[row][col] != ' ') {
+ intermediate.append(table[row][col]);
+ }
+ }
+ }
+
+ // Step 4: Replace each ADFGVX pair with the corresponding letter
+ StringBuilder plaintext = new StringBuilder();
+ for (int i = 0; i < intermediate.length(); i += 2) {
+ String pair = intermediate.substring(i, i + 2);
+ if (PAIR_TO_CHAR.containsKey(pair)) {
+ plaintext.append(PAIR_TO_CHAR.get(pair));
+ }
+ }
+
+ return plaintext.toString();
+ }
+
+
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/crypto/classic/AffineCipherImpl.java b/src/main/java/dev/masterflomaster1/jfxc/crypto/classic/AffineCipherImpl.java
new file mode 100644
index 0000000..d276485
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/crypto/classic/AffineCipherImpl.java
@@ -0,0 +1,61 @@
+package dev.masterflomaster1.jfxc.crypto.classic;
+
+import java.util.List;
+
+/**
+ * @see Affine cipher
+ */
+public final class AffineCipherImpl {
+
+ private static final int ALPHABET_SIZE = 26;
+
+ public static final List SLOPE = List.of(1, 3, 5, 7, 9, 11, 15, 17, 19, 21, 23, 25);
+ public static final List INTERCEPT = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25);
+
+ private AffineCipherImpl() { }
+
+ public static String encrypt(String plaintext, int a, int b) {
+ StringBuilder ciphertext = new StringBuilder();
+
+ for (char ch : plaintext.toUpperCase().toCharArray()) {
+ if (Character.isLetter(ch)) {
+ int x = ch - 'A';
+ char encryptedChar = (char) (((a * x + b) % ALPHABET_SIZE) + 'A');
+ ciphertext.append(encryptedChar);
+ } else {
+ ciphertext.append(ch);
+ }
+ }
+
+ return ciphertext.toString();
+ }
+
+ public static String decrypt(String ciphertext, int a, int b) {
+ StringBuilder plaintext = new StringBuilder();
+ int aInverse = modInverse(a, ALPHABET_SIZE);
+
+ for (char ch : ciphertext.toUpperCase().toCharArray()) {
+ if (Character.isLetter(ch)) {
+ int y = ch - 'A';
+ char decryptedChar = (char) ((aInverse * (y - b + ALPHABET_SIZE) % ALPHABET_SIZE) + 'A');
+ plaintext.append(decryptedChar);
+ } else {
+ plaintext.append(ch);
+ }
+ }
+
+ return plaintext.toString();
+ }
+
+ // Function to find modular inverse of a under modulo m
+ private static int modInverse(int a, int m) {
+ a = a % m;
+ for (int x = 1; x < m; x++) {
+ if ((a * x) % m == 1) {
+ return x;
+ }
+ }
+ throw new IllegalArgumentException("Multiplicative inverse for the given 'a' does not exist.");
+ }
+
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/crypto/classic/AtbashCipherImpl.java b/src/main/java/dev/masterflomaster1/jfxc/crypto/classic/AtbashCipherImpl.java
new file mode 100644
index 0000000..daf5c3a
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/crypto/classic/AtbashCipherImpl.java
@@ -0,0 +1,33 @@
+package dev.masterflomaster1.jfxc.crypto.classic;
+
+/**
+ * Atbash
+ */
+public final class AtbashCipherImpl {
+
+ private static final String ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+
+ private AtbashCipherImpl() { }
+
+ public static String encrypt(String plaintext) {
+ plaintext = plaintext.toUpperCase();
+ StringBuilder ciphertext = new StringBuilder();
+
+ for (char ch : plaintext.toCharArray()) {
+ if (Character.isLetter(ch)) {
+ int index = ALPHABET.indexOf(ch);
+ char encryptedChar = ALPHABET.charAt(ALPHABET.length() - 1 - index);
+ ciphertext.append(encryptedChar);
+ } else {
+ ciphertext.append(ch);
+ }
+ }
+
+ return ciphertext.toString();
+ }
+
+ public static String decrypt(String ciphertext) {
+ return encrypt(ciphertext);
+ }
+
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/crypto/classic/CaesarCipherImpl.java b/src/main/java/dev/masterflomaster1/jfxc/crypto/classic/CaesarCipherImpl.java
new file mode 100644
index 0000000..c60499e
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/crypto/classic/CaesarCipherImpl.java
@@ -0,0 +1,30 @@
+package dev.masterflomaster1.jfxc.crypto.classic;
+
+/**
+ * Caesar cipher
+ */
+public final class CaesarCipherImpl {
+
+ private CaesarCipherImpl() { }
+
+ public static String encrypt(String text, int shift) {
+ StringBuilder encrypted = new StringBuilder();
+
+ for (char c : text.toCharArray()) {
+ if (Character.isLetter(c)) {
+ char base = Character.isUpperCase(c) ? 'A' : 'a';
+ int newChar = (c - base + shift) % 26 + base;
+ encrypted.append((char) newChar);
+ } else {
+ encrypted.append(c);
+ }
+ }
+
+ return encrypted.toString();
+ }
+
+ public static String decrypt(String text, int shift) {
+ return encrypt(text, 26 - shift % 26);
+ }
+
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/crypto/classic/HillCipherImpl.java b/src/main/java/dev/masterflomaster1/jfxc/crypto/classic/HillCipherImpl.java
new file mode 100644
index 0000000..d49f36b
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/crypto/classic/HillCipherImpl.java
@@ -0,0 +1,138 @@
+package dev.masterflomaster1.jfxc.crypto.classic;
+
+/**
+ * Hill cipher
+ */
+public final class HillCipherImpl {
+
+ private static final int ALPHABET_SIZE = 26;
+
+ private HillCipherImpl() { }
+
+ public static String encrypt(String plaintext, int[][] key) {
+ plaintext = plaintext.toUpperCase().replaceAll("[^A-Z]", "");
+ StringBuilder ciphertext = new StringBuilder();
+
+ int blockSize = key.length;
+
+ // Pad plaintext if needed
+ while (plaintext.length() % blockSize != 0) {
+ plaintext += 'X';
+ }
+
+ // Encrypt each block
+ for (int i = 0; i < plaintext.length(); i += blockSize) {
+ String block = plaintext.substring(i, i + blockSize);
+ int[] blockVector = new int[blockSize];
+ for (int j = 0; j < blockSize; j++) {
+ blockVector[j] = block.charAt(j) - 'A';
+ }
+ int[] encryptedBlockVector = multiplyMatrixVector(key, blockVector);
+ for (int value : encryptedBlockVector) {
+ char encryptedChar = (char) ('A' + value % ALPHABET_SIZE);
+ ciphertext.append(encryptedChar);
+ }
+ }
+
+ return ciphertext.toString();
+ }
+
+ public static String decrypt(String ciphertext, int[][] key) {
+ int determinant = getDeterminant(key);
+ int inverseDeterminant = modInverse(determinant, ALPHABET_SIZE);
+ int[][] adjugate = getAdjugate(key);
+
+ int[][] inverseKey = multiplyMatrixScalar(adjugate, inverseDeterminant);
+ for (int i = 0; i < inverseKey.length; i++) {
+ for (int j = 0; j < inverseKey[i].length; j++) {
+ inverseKey[i][j] = (inverseKey[i][j] % ALPHABET_SIZE + ALPHABET_SIZE) % ALPHABET_SIZE; // Ensure positive values
+ }
+ }
+
+ return encrypt(ciphertext, inverseKey);
+ }
+
+ private static int[] multiplyMatrixVector(int[][] matrix, int[] vector) {
+ int[] result = new int[matrix.length];
+ for (int i = 0; i < matrix.length; i++) {
+ int sum = 0;
+ for (int j = 0; j < matrix[i].length; j++) {
+ sum += matrix[i][j] * vector[j];
+ }
+ result[i] = sum;
+ }
+ return result;
+ }
+
+ private static int[][] multiplyMatrixScalar(int[][] matrix, int scalar) {
+ int[][] result = new int[matrix.length][matrix[0].length];
+ for (int i = 0; i < matrix.length; i++) {
+ for (int j = 0; j < matrix[i].length; j++) {
+ result[i][j] = matrix[i][j] * scalar;
+ }
+ }
+ return result;
+ }
+
+ private static int getDeterminant(int[][] matrix) {
+ if (matrix.length != matrix[0].length) {
+ throw new IllegalArgumentException("Matrix is not square.");
+ }
+ if (matrix.length == 1) {
+ return matrix[0][0];
+ }
+ if (matrix.length == 2) {
+ return matrix[0][0] * matrix[1][1] - matrix[0][1] * matrix[1][0];
+ }
+ int determinant = 0;
+ for (int i = 0; i < matrix.length; i++) {
+ determinant += matrix[0][i] * getCofactor(matrix, 0, i);
+ }
+ return determinant;
+ }
+
+ private static int getCofactor(int[][] matrix, int row, int col) {
+ return (int) Math.pow(-1, row + col) * getMinor(matrix, row, col);
+ }
+
+ private static int getMinor(int[][] matrix, int row, int col) {
+ int[][] minor = new int[matrix.length - 1][matrix.length - 1];
+ int minorRow = 0;
+ for (int i = 0; i < matrix.length; i++) {
+ if (i == row) {
+ continue;
+ }
+ int minorCol = 0;
+ for (int j = 0; j < matrix[i].length; j++) {
+ if (j == col) {
+ continue;
+ }
+ minor[minorRow][minorCol] = matrix[i][j];
+ minorCol++;
+ }
+ minorRow++;
+ }
+ return getDeterminant(minor);
+ }
+
+ private static int[][] getAdjugate(int[][] matrix) {
+ int[][] adjugate = new int[matrix.length][matrix.length];
+ for (int i = 0; i < matrix.length; i++) {
+ for (int j = 0; j < matrix[i].length; j++) {
+ adjugate[j][i] = getCofactor(matrix, i, j);
+ }
+ }
+ return adjugate;
+ }
+
+ private static int modInverse(int a, int m) {
+ a = a % m;
+ for (int x = 1; x < m; x++) {
+ if ((a * x) % m == 1) {
+ return x;
+ }
+ }
+ return 1;
+ }
+
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/crypto/classic/PlayfairCipherImpl.java b/src/main/java/dev/masterflomaster1/jfxc/crypto/classic/PlayfairCipherImpl.java
new file mode 100644
index 0000000..4b81f41
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/crypto/classic/PlayfairCipherImpl.java
@@ -0,0 +1,156 @@
+package dev.masterflomaster1.jfxc.crypto.classic;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * @see Playfair cipher
+ */
+public final class PlayfairCipherImpl {
+
+ private static final char[][] PLAYFAIR_TABLE = new char[5][5];
+
+ private PlayfairCipherImpl() { }
+
+ public static String encrypt(String plaintext, String keyword) {
+ buildPlayfairTable(keyword);
+ plaintext = preprocessText(plaintext);
+
+ StringBuilder ciphertext = new StringBuilder();
+ for (int i = 0; i < plaintext.length(); i += 2) {
+ char first = plaintext.charAt(i);
+ char second = plaintext.charAt(i + 1);
+ int[] firstPos = findPosition(first);
+ int[] secondPos = findPosition(second);
+
+ int row1 = firstPos[0];
+ int col1 = firstPos[1];
+ int row2 = secondPos[0];
+ int col2 = secondPos[1];
+
+ if (row1 == row2) {
+ col1 = (col1 + 1) % 5;
+ col2 = (col2 + 1) % 5;
+ } else if (col1 == col2) {
+ row1 = (row1 + 1) % 5;
+ row2 = (row2 + 1) % 5;
+ } else {
+ int temp = col1;
+ col1 = col2;
+ col2 = temp;
+ }
+
+ ciphertext.append(PLAYFAIR_TABLE[row1][col1]);
+ ciphertext.append(PLAYFAIR_TABLE[row2][col2]);
+ }
+
+ return ciphertext.toString();
+ }
+
+ public static String decrypt(String ciphertext, String keyword) {
+ buildPlayfairTable(keyword);
+
+ StringBuilder plaintext = new StringBuilder();
+ for (int i = 0; i < ciphertext.length(); i += 2) {
+ char first = ciphertext.charAt(i);
+ char second = ciphertext.charAt(i + 1);
+ int[] firstPos = findPosition(first);
+ int[] secondPos = findPosition(second);
+
+ int row1 = firstPos[0];
+ int col1 = firstPos[1];
+ int row2 = secondPos[0];
+ int col2 = secondPos[1];
+
+ if (row1 == row2) {
+ col1 = (col1 - 1 + 5) % 5;
+ col2 = (col2 - 1 + 5) % 5;
+ } else if (col1 == col2) {
+ row1 = (row1 - 1 + 5) % 5;
+ row2 = (row2 - 1 + 5) % 5;
+ } else {
+ int temp = col1;
+ col1 = col2;
+ col2 = temp;
+ }
+
+ plaintext.append(PLAYFAIR_TABLE[row1][col1]);
+ plaintext.append(PLAYFAIR_TABLE[row2][col2]);
+ }
+
+ return plaintext.toString();
+ }
+
+ private static void buildPlayfairTable(String keyword) {
+ // Prepare keyword
+ keyword = keyword.toUpperCase().replaceAll("[^A-Z]", "");
+ keyword = keyword.replace("J", "I");
+
+ // Build key table
+ Set usedChars = new HashSet<>();
+ StringBuilder keyBuilder = new StringBuilder();
+
+ for (char ch : keyword.toCharArray()) {
+ if (!usedChars.contains(ch)) {
+ keyBuilder.append(ch);
+ usedChars.add(ch);
+ }
+ }
+
+ for (char ch = 'A'; ch <= 'Z'; ch++) {
+ if (ch != 'J' && !usedChars.contains(ch)) {
+ keyBuilder.append(ch);
+ usedChars.add(ch);
+ }
+ }
+
+ keyword = keyBuilder.toString();
+
+ // Fill the key table into the 5x5 matrix
+ int index = 0;
+ for (int i = 0; i < 5; i++) {
+ for (int j = 0; j < 5; j++) {
+ PLAYFAIR_TABLE[i][j] = keyword.charAt(index);
+ index++;
+ }
+ }
+ }
+
+ private static String preprocessText(String text) {
+ // Replace J with I
+ text = text.toUpperCase().replaceAll("[^A-Z]", "");
+ text = text.replace("J", "I");
+
+ // Insert X between repeated letters and add X if the length is odd
+ StringBuilder processedText = new StringBuilder();
+ for (int i = 0; i < text.length() - 1; i += 2) {
+ char first = text.charAt(i);
+ char second = text.charAt(i + 1);
+ processedText.append(first);
+ if (first == second) {
+ processedText.append('X');
+ } else {
+ processedText.append(second);
+ }
+ }
+ if (processedText.length() % 2 != 0) {
+ processedText.append('X');
+ }
+ return processedText.toString();
+ }
+
+ private static int[] findPosition(char ch) {
+ int[] pos = new int[2];
+ for (int i = 0; i < 5; i++) {
+ for (int j = 0; j < 5; j++) {
+ if (PLAYFAIR_TABLE[i][j] == ch) {
+ pos[0] = i;
+ pos[1] = j;
+ return pos;
+ }
+ }
+ }
+ return pos;
+ }
+
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/crypto/classic/VigenereCipherImpl.java b/src/main/java/dev/masterflomaster1/jfxc/crypto/classic/VigenereCipherImpl.java
new file mode 100644
index 0000000..a7105a7
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/crypto/classic/VigenereCipherImpl.java
@@ -0,0 +1,52 @@
+package dev.masterflomaster1.jfxc.crypto.classic;
+
+/**
+ * @see Vigenère cipher
+ */
+public final class VigenereCipherImpl {
+
+ private static final int ALPHABET_SIZE = 26;
+
+ private VigenereCipherImpl() { }
+
+ public static String encrypt(String plaintext, String keyword) {
+ StringBuilder ciphertext = new StringBuilder();
+ plaintext = plaintext.toUpperCase();
+ keyword = keyword.toUpperCase();
+
+ int keywordIndex = 0;
+ for (char ch : plaintext.toCharArray()) {
+ if (Character.isLetter(ch)) {
+ int shift = keyword.charAt(keywordIndex) - 'A';
+ char encryptedChar = (char) ((ch + shift - 'A') % ALPHABET_SIZE + 'A');
+ ciphertext.append(encryptedChar);
+ keywordIndex = (keywordIndex + 1) % keyword.length();
+ } else {
+ ciphertext.append(ch);
+ }
+ }
+
+ return ciphertext.toString();
+ }
+
+ public static String decrypt(String ciphertext, String keyword) {
+ StringBuilder plaintext = new StringBuilder();
+ ciphertext = ciphertext.toUpperCase();
+ keyword = keyword.toUpperCase();
+
+ int keywordIndex = 0;
+ for (char ch : ciphertext.toCharArray()) {
+ if (Character.isLetter(ch)) {
+ int shift = keyword.charAt(keywordIndex) - 'A';
+ char decryptedChar = (char) ((ch - shift - 'A' + ALPHABET_SIZE) % ALPHABET_SIZE + 'A');
+ plaintext.append(decryptedChar);
+ keywordIndex = (keywordIndex + 1) % keyword.length();
+ } else {
+ plaintext.append(ch);
+ }
+ }
+
+ return plaintext.toString();
+ }
+
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/crypto/enigma/Enigma.java b/src/main/java/dev/masterflomaster1/jfxc/crypto/enigma/Enigma.java
new file mode 100644
index 0000000..3941f26
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/crypto/enigma/Enigma.java
@@ -0,0 +1,86 @@
+package dev.masterflomaster1.jfxc.crypto.enigma;
+
+public class Enigma {
+
+ public Rotor leftRotor;
+ public Rotor middleRotor;
+ public Rotor rightRotor;
+
+ public Reflector reflector;
+
+ public Plugboard plugboard;
+
+ public Enigma(String[] rotors, String reflector, int[] rotorPositions, int[] ringSettings, String plugboardConnections) {
+ this.leftRotor = Rotor.create(rotors[0], rotorPositions[0], ringSettings[0]);
+ this.middleRotor = Rotor.create(rotors[1], rotorPositions[1], ringSettings[1]);
+ this.rightRotor = Rotor.create(rotors[2], rotorPositions[2], ringSettings[2]);
+ this.reflector = Reflector.create(reflector);
+ this.plugboard = new Plugboard(plugboardConnections);
+ }
+
+ public Enigma(EnigmaKey key) {
+ this(key.rotors, "B", key.indicators, key.rings, key.plugboard);
+ }
+
+ public void rotate() {
+ // If middle rotor notch - double-stepping
+ if (middleRotor.isAtNotch()) {
+ middleRotor.turnover();
+ leftRotor.turnover();
+ } else if (rightRotor.isAtNotch()) {
+ // If left-rotor notch
+ middleRotor.turnover();
+ }
+
+ // Increment right-most rotor
+ rightRotor.turnover();
+ }
+
+ public int encrypt(int c) {
+ rotate();
+
+ // Plugboard in
+ c = this.plugboard.forward(c);
+
+ // Right to left
+ int c1 = rightRotor.forward(c);
+ int c2 = middleRotor.forward(c1);
+ int c3 = leftRotor.forward(c2);
+
+ // Reflector
+ int c4 = reflector.forward(c3);
+
+ // Left to right
+ int c5 = leftRotor.backward(c4);
+ int c6 = middleRotor.backward(c5);
+ int c7 = rightRotor.backward(c6);
+
+ // Plugboard out
+ c7 = plugboard.forward(c7);
+
+ return c7;
+ }
+
+ public char encrypt(char c) {
+ return (char) (this.encrypt(c - 65) + 65);
+ }
+
+ public char[] encrypt(char[] input) {
+ char[] output = new char[input.length];
+ for (int i = 0; i < input.length; i++) {
+ output[i] = this.encrypt(input[i]);
+ }
+ return output;
+ }
+
+ public char[] encrypt(String input) {
+ input = input.toUpperCase().replace(" ", "");
+
+ char[] output = new char[input.length()];
+ for (int i = 0; i < input.length(); i++) {
+ output[i] = this.encrypt(input.charAt(i));
+ }
+ return output;
+ }
+
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/crypto/enigma/EnigmaKey.java b/src/main/java/dev/masterflomaster1/jfxc/crypto/enigma/EnigmaKey.java
new file mode 100644
index 0000000..4630ffb
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/crypto/enigma/EnigmaKey.java
@@ -0,0 +1,10 @@
+package dev.masterflomaster1.jfxc.crypto.enigma;
+
+public class EnigmaKey {
+
+ public String[] rotors;
+ public int[] indicators;
+ public int[] rings;
+ public String plugboard;
+
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/crypto/enigma/Plugboard.java b/src/main/java/dev/masterflomaster1/jfxc/crypto/enigma/Plugboard.java
new file mode 100644
index 0000000..469f0fe
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/crypto/enigma/Plugboard.java
@@ -0,0 +1,81 @@
+package dev.masterflomaster1.jfxc.crypto.enigma;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public class Plugboard {
+
+ private int[] wiring;
+
+ public Plugboard(String connections) {
+ this.wiring = decodePlugboard(connections);
+ }
+
+ public int forward(int c) {
+ return this.wiring[c];
+ }
+
+ private static int[] identityPlugboard() {
+ int[] mapping = new int[26];
+ for (int i = 0; i < 26; i++) {
+ mapping[i] = i;
+ }
+ return mapping;
+ }
+
+ public static Set getUnpluggedCharacters(String plugboard) {
+ Set unpluggedCharacters = new HashSet<>();
+ for (int i = 0; i < 26; i++) {
+ unpluggedCharacters.add(i);
+ }
+
+ if (plugboard.isEmpty()) {
+ return unpluggedCharacters;
+ }
+
+ String[] pairings = plugboard.split("[^a-zA-Z]");
+
+ // Validate and create mapping
+ for (String pair : pairings) {
+ int c1 = pair.charAt(0) - 65;
+ int c2 = pair.charAt(1) - 65;
+
+ unpluggedCharacters.remove(c1);
+ unpluggedCharacters.remove(c2);
+ }
+
+ return unpluggedCharacters;
+ }
+
+ public static int[] decodePlugboard(String plugboard) {
+ if (plugboard == null || plugboard.isEmpty()) {
+ return identityPlugboard();
+ }
+
+ String[] pairings = plugboard.split("[^a-zA-Z]");
+ Set pluggedCharacters = new HashSet<>();
+ int[] mapping = identityPlugboard();
+
+ // Validate and create mapping
+ for (String pair : pairings) {
+ if (pair.length() != 2)
+ return identityPlugboard();
+
+ int c1 = pair.charAt(0) - 65;
+ int c2 = pair.charAt(1) - 65;
+
+ if (pluggedCharacters.contains(c1) || pluggedCharacters.contains(c2)) {
+ return identityPlugboard();
+ }
+
+ pluggedCharacters.add(c1);
+ pluggedCharacters.add(c2);
+
+ mapping[c1] = c2;
+ mapping[c2] = c1;
+ }
+
+ return mapping;
+ }
+
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/crypto/enigma/Reflector.java b/src/main/java/dev/masterflomaster1/jfxc/crypto/enigma/Reflector.java
new file mode 100644
index 0000000..c2bea65
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/crypto/enigma/Reflector.java
@@ -0,0 +1,32 @@
+package dev.masterflomaster1.jfxc.crypto.enigma;
+
+public class Reflector {
+
+ protected int[] forwardWiring;
+
+ public Reflector(String encoding) {
+ this.forwardWiring = decodeWiring(encoding);
+ }
+
+ public static Reflector create(String name) {
+ return switch (name) {
+ case "B" -> new Reflector("YRUHQSLDPXNGOKMIEBFZCWVJAT");
+ case "C" -> new Reflector("FVPJIAOYEDRZXWGCTKUQSBNMHL");
+ default -> new Reflector("ZYXWVUTSRQPONMLKJIHGFEDCBA");
+ };
+ }
+
+ protected static int[] decodeWiring(String encoding) {
+ char[] charWiring = encoding.toCharArray();
+ int[] wiring = new int[charWiring.length];
+ for (int i = 0; i < charWiring.length; i++) {
+ wiring[i] = charWiring[i] - 65;
+ }
+ return wiring;
+ }
+
+ public int forward(int c) {
+ return this.forwardWiring[c];
+ }
+
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/crypto/enigma/Rotor.java b/src/main/java/dev/masterflomaster1/jfxc/crypto/enigma/Rotor.java
new file mode 100644
index 0000000..6c39e5b
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/crypto/enigma/Rotor.java
@@ -0,0 +1,98 @@
+package dev.masterflomaster1.jfxc.crypto.enigma;
+
+public class Rotor {
+
+ protected String name;
+ protected int[] forwardWiring;
+ protected int[] backwardWiring;
+
+ protected int rotorPosition;
+ protected int notchPosition;
+ protected int ringSetting;
+
+ public Rotor(String name, String encoding, int rotorPosition, int notchPosition, int ringSetting) {
+ this.name = name;
+ this.forwardWiring = decodeWiring(encoding);
+ this.backwardWiring = inverseWiring(this.forwardWiring);
+ this.rotorPosition = rotorPosition;
+ this.notchPosition = notchPosition;
+ this.ringSetting = ringSetting;
+ }
+
+ public static Rotor create(String name, int rotorPosition, int ringSetting) {
+ return switch (name) {
+ case "I" -> new Rotor("I", "EKMFLGDQVZNTOWYHXUSPAIBRCJ", rotorPosition, 16, ringSetting);
+ case "II" -> new Rotor("II", "AJDKSIRUXBLHWTMCQGZNPYFVOE", rotorPosition, 4, ringSetting);
+ case "III" -> new Rotor("III", "BDFHJLCPRTXVZNYEIWGAKMUSQO", rotorPosition, 21, ringSetting);
+ case "IV" -> new Rotor("IV", "ESOVPZJAYQUIRHXLNFTGKDCMWB", rotorPosition, 9, ringSetting);
+ case "V" -> new Rotor("V", "VZBRGITYUPSDNHLXAWMJQOFECK", rotorPosition, 25, ringSetting);
+ case "VI" -> new Rotor("VI", "JPGVOUMFYQBENHZRDKASXLICTW", rotorPosition, 0, ringSetting) {
+ @Override
+ public boolean isAtNotch() {
+ return this.rotorPosition == 12 || this.rotorPosition == 25;
+ }
+ };
+ case "VII" -> new Rotor("VII", "NZJHGRCXMYSWBOUFAIVLPEKQDT", rotorPosition, 0, ringSetting) {
+ @Override
+ public boolean isAtNotch() {
+ return this.rotorPosition == 12 || this.rotorPosition == 25;
+ }
+ };
+ case "VIII" -> new Rotor("VIII", "FKQHTLXOCBJSPDZRAMEWNIUYGV", rotorPosition, 0, ringSetting) {
+ @Override
+ public boolean isAtNotch() {
+ return this.rotorPosition == 12 || this.rotorPosition == 25;
+ }
+ };
+ default -> new Rotor("Identity", "ABCDEFGHIJKLMNOPQRSTUVWXYZ", rotorPosition, 0, ringSetting);
+ };
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public int getPosition() {
+ return rotorPosition;
+ }
+
+ protected static int[] decodeWiring(String encoding) {
+ char[] charWiring = encoding.toCharArray();
+ int[] wiring = new int[charWiring.length];
+ for (int i = 0; i < charWiring.length; i++) {
+ wiring[i] = charWiring[i] - 65;
+ }
+ return wiring;
+ }
+
+ protected static int[] inverseWiring(int[] wiring) {
+ int[] inverse = new int[wiring.length];
+ for (int i = 0; i < wiring.length; i++) {
+ int forward = wiring[i];
+ inverse[forward] = i;
+ }
+ return inverse;
+ }
+
+ protected static int encipher(int k, int pos, int ring, int[] mapping) {
+ int shift = pos - ring;
+ return (mapping[(k + shift + 26) % 26] - shift + 26) % 26;
+ }
+
+ public int forward(int c) {
+ return encipher(c, this.rotorPosition, this.ringSetting, this.forwardWiring);
+ }
+
+ public int backward(int c) {
+ return encipher(c, this.rotorPosition, this.ringSetting, this.backwardWiring);
+ }
+
+ public boolean isAtNotch() {
+ return this.notchPosition == this.rotorPosition;
+ }
+
+ public void turnover() {
+ this.rotorPosition = (this.rotorPosition + 1) % 26;
+ }
+
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/crypto/passwords/HaveIBeenPwnedApiClient.java b/src/main/java/dev/masterflomaster1/jfxc/crypto/passwords/HaveIBeenPwnedApiClient.java
new file mode 100644
index 0000000..70b6a57
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/crypto/passwords/HaveIBeenPwnedApiClient.java
@@ -0,0 +1,48 @@
+package dev.masterflomaster1.jfxc.crypto.passwords;
+
+import dev.masterflomaster1.jfxc.crypto.UnkeyedCryptoHash;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.HexFormat;
+import java.util.Optional;
+
+public final class HaveIBeenPwnedApiClient {
+
+ private static final String RANGE_URL = "https://api.pwnedpasswords.com/range/";
+
+ private HaveIBeenPwnedApiClient() { }
+
+ public static Optional passwordRange(byte[] password) throws IOException, InterruptedException {
+ var hashed = HexFormat.of().formatHex(UnkeyedCryptoHash.hash("SHA-1", password)).toUpperCase();
+
+ String section1 = hashed.substring(0, 5);
+ String section2 = hashed.substring(5);
+
+ HttpClient client = HttpClient.newHttpClient();
+
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(RANGE_URL + section1))
+ .GET()
+ .build();
+
+ HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
+
+ String content = response.body();
+ String[] hashes = content.split("\n");
+
+ for (String line : hashes) {
+ String[] parts = line.split(":");
+
+ if (parts[0].equals(section2)) {
+ return Optional.of(Integer.parseInt(parts[1].trim()));
+ }
+ }
+
+ return Optional.empty();
+ }
+
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/crypto/passwords/PasswordEvaluator.java b/src/main/java/dev/masterflomaster1/jfxc/crypto/passwords/PasswordEvaluator.java
new file mode 100644
index 0000000..ddbbbde
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/crypto/passwords/PasswordEvaluator.java
@@ -0,0 +1,8 @@
+package dev.masterflomaster1.jfxc.crypto.passwords;
+
+public interface PasswordEvaluator {
+
+ int getStrengthScore(String password);
+ PasswordEvaluatorFeedback getStrengthReport(String password);
+
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/crypto/passwords/PasswordEvaluatorFeedback.java b/src/main/java/dev/masterflomaster1/jfxc/crypto/passwords/PasswordEvaluatorFeedback.java
new file mode 100644
index 0000000..2375fb6
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/crypto/passwords/PasswordEvaluatorFeedback.java
@@ -0,0 +1,7 @@
+package dev.masterflomaster1.jfxc.crypto.passwords;
+
+public interface PasswordEvaluatorFeedback {
+
+ String getCombined();
+
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/crypto/passwords/PasswordEvaluatorService.java b/src/main/java/dev/masterflomaster1/jfxc/crypto/passwords/PasswordEvaluatorService.java
new file mode 100644
index 0000000..85d4781
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/crypto/passwords/PasswordEvaluatorService.java
@@ -0,0 +1,22 @@
+package dev.masterflomaster1.jfxc.crypto.passwords;
+
+public final class PasswordEvaluatorService {
+
+ private static PasswordEvaluatorService instance;
+ private final ZxcvbnPasswordEvaluator checker = new ZxcvbnPasswordEvaluator();
+
+ private PasswordEvaluatorService() {
+ }
+
+ public static PasswordEvaluatorService of() {
+ if (instance == null)
+ instance = new PasswordEvaluatorService();
+
+ return instance;
+ }
+
+ public ZxcvbnPasswordEvaluator getZxcvbn() {
+ return checker;
+ }
+
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/crypto/passwords/ZxcvbnPasswordEvaluator.java b/src/main/java/dev/masterflomaster1/jfxc/crypto/passwords/ZxcvbnPasswordEvaluator.java
new file mode 100644
index 0000000..44d1af8
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/crypto/passwords/ZxcvbnPasswordEvaluator.java
@@ -0,0 +1,32 @@
+package dev.masterflomaster1.jfxc.crypto.passwords;
+
+import com.nulabinc.zxcvbn.Zxcvbn;
+
+import java.util.List;
+
+public class ZxcvbnPasswordEvaluator implements PasswordEvaluator {
+
+ private final Zxcvbn zxcvbn = new Zxcvbn();
+
+ public ZxcvbnPasswordEvaluator() {
+ }
+
+ @Override
+ public int getStrengthScore(String password) {
+ return (int) ((zxcvbn.measure(password).getScore() / 4F) * 100);
+ }
+
+ @Override
+ public PasswordEvaluatorFeedback getStrengthReport(String password) {
+ return new ZxcvbnPasswordEvaluatorFeedback(zxcvbn.measure(password).getFeedback());
+ }
+
+ public String getWarning(String password) {
+ return zxcvbn.measure(password).getFeedback().getWarning();
+ }
+
+ public List getSuggestions(String password) {
+ return zxcvbn.measure(password).getFeedback().getSuggestions();
+ }
+
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/crypto/passwords/ZxcvbnPasswordEvaluatorFeedback.java b/src/main/java/dev/masterflomaster1/jfxc/crypto/passwords/ZxcvbnPasswordEvaluatorFeedback.java
new file mode 100644
index 0000000..9bfeeec
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/crypto/passwords/ZxcvbnPasswordEvaluatorFeedback.java
@@ -0,0 +1,21 @@
+package dev.masterflomaster1.jfxc.crypto.passwords;
+
+import com.nulabinc.zxcvbn.Feedback;
+
+public class ZxcvbnPasswordEvaluatorFeedback implements PasswordEvaluatorFeedback {
+
+ private final Feedback feedback;
+
+ public ZxcvbnPasswordEvaluatorFeedback(Feedback feedback) {
+ this.feedback = feedback;
+ }
+
+ @Override
+ public String getCombined() {
+
+ String builder = "Warning: " + feedback.getWarning() + "\n\n" +
+ "Suggestions: " + String.join("\n", feedback.getSuggestions());
+ return builder;
+ }
+
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/gui/GUI.java b/src/main/java/dev/masterflomaster1/jfxc/gui/GUI.java
new file mode 100644
index 0000000..8221be8
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/gui/GUI.java
@@ -0,0 +1,18 @@
+package dev.masterflomaster1.jfxc.gui;
+
+import atlantafx.base.theme.PrimerDark;
+import javafx.application.Application;
+import javafx.stage.Stage;
+
+import java.io.IOException;
+
+public class GUI extends Application {
+
+ @Override
+ public void start(Stage primaryStage) throws IOException {
+ Application.setUserAgentStylesheet(new PrimerDark().getUserAgentStylesheet());
+ primaryStage.setTitle("SJC");
+ primaryStage.show();
+ }
+
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/gui/event/BrowseEvent.java b/src/main/java/dev/masterflomaster1/jfxc/gui/event/BrowseEvent.java
new file mode 100644
index 0000000..ce1daa2
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/gui/event/BrowseEvent.java
@@ -0,0 +1,27 @@
+package dev.masterflomaster1.jfxc.gui.event;
+
+import java.net.URI;
+
+public final class BrowseEvent extends Event {
+
+ private final URI uri;
+
+ public BrowseEvent(URI uri) {
+ this.uri = uri;
+ }
+
+ public URI getUri() {
+ return uri;
+ }
+
+ @Override
+ public String toString() {
+ return "BrowseEvent{"
+ + "uri=" + uri
+ + "} " + super.toString();
+ }
+
+ public static void fire(String url) {
+ Event.publish(new BrowseEvent(URI.create(url)));
+ }
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/gui/event/DefaultEventBus.java b/src/main/java/dev/masterflomaster1/jfxc/gui/event/DefaultEventBus.java
new file mode 100644
index 0000000..bf509ca
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/gui/event/DefaultEventBus.java
@@ -0,0 +1,92 @@
+package dev.masterflomaster1.jfxc.gui.event;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.function.Consumer;
+
+/**
+ * Simple event bus implementation.
+ *
+ *
Subscribe and publish events. Events are published in channels distinguished by event type.
+ * Channels can be grouped using an event type hierarchy.
+ *
+ *
You can use the default event bus instance {@link #getInstance}, which is a singleton,
+ * or you can create one or multiple instances of {@link DefaultEventBus}.
+ */
+@SuppressWarnings({"unchecked", "rawtypes"})
+public final class DefaultEventBus implements EventBus {
+
+ public DefaultEventBus() {
+ }
+
+ private final Map, Set> subscribers = new ConcurrentHashMap<>();
+
+ @Override
+ public void subscribe(Class extends E> eventType, Consumer subscriber) {
+ Objects.requireNonNull(eventType);
+ Objects.requireNonNull(subscriber);
+
+ Set eventSubscribers = getOrCreateSubscribers(eventType);
+ eventSubscribers.add(subscriber);
+ }
+
+ private Set getOrCreateSubscribers(Class eventType) {
+ Set eventSubscribers = subscribers.get(eventType);
+ if (eventSubscribers == null) {
+ eventSubscribers = new CopyOnWriteArraySet<>();
+ subscribers.put(eventType, eventSubscribers);
+ }
+ return eventSubscribers;
+ }
+
+ @Override
+ public void unsubscribe(Consumer subscriber) {
+ Objects.requireNonNull(subscriber);
+
+ subscribers.values().forEach(eventSubscribers -> eventSubscribers.remove(subscriber));
+ }
+
+ @Override
+ public void unsubscribe(Class extends E> eventType, Consumer subscriber) {
+ Objects.requireNonNull(eventType);
+ Objects.requireNonNull(subscriber);
+
+ subscribers.keySet().stream()
+ .filter(eventType::isAssignableFrom)
+ .map(subscribers::get)
+ .forEach(eventSubscribers -> eventSubscribers.remove(subscriber));
+ }
+
+ @Override
+ public void publish(E event) {
+ Objects.requireNonNull(event);
+
+ Class> eventType = event.getClass();
+ subscribers.keySet().stream()
+ .filter(type -> type.isAssignableFrom(eventType))
+ .flatMap(type -> subscribers.get(type).stream())
+ .forEach(subscriber -> publish(event, subscriber));
+ }
+
+ private void publish(E event, Consumer subscriber) {
+ try {
+ subscriber.accept(event);
+ } catch (Exception e) {
+ Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e);
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+
+ private final static class InstanceHolder {
+
+ private static final DefaultEventBus INSTANCE = new DefaultEventBus();
+ }
+
+ public static DefaultEventBus getInstance() {
+ return InstanceHolder.INSTANCE;
+ }
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/gui/event/Event.java b/src/main/java/dev/masterflomaster1/jfxc/gui/event/Event.java
new file mode 100644
index 0000000..861943a
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/gui/event/Event.java
@@ -0,0 +1,42 @@
+package dev.masterflomaster1.jfxc.gui.event;
+
+import java.util.UUID;
+
+public abstract class Event {
+
+ protected final UUID id = UUID.randomUUID();
+
+ protected Event() {
+ }
+
+ public UUID getId() {
+ return id;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof Event event)) {
+ return false;
+ }
+ return id.equals(event.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return id.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return "Event{"
+ + "id=" + id
+ + '}';
+ }
+
+ public static void publish(E event) {
+ DefaultEventBus.getInstance().publish(event);
+ }
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/gui/event/EventBus.java b/src/main/java/dev/masterflomaster1/jfxc/gui/event/EventBus.java
new file mode 100644
index 0000000..94cfcd9
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/gui/event/EventBus.java
@@ -0,0 +1,42 @@
+package dev.masterflomaster1.jfxc.gui.event;
+
+import java.util.function.Consumer;
+
+public interface EventBus {
+
+ /**
+ * Subscribe to an event type.
+ *
+ * @param eventType the event type, can be a super class of all events to subscribe.
+ * @param subscriber the subscriber which will consume the events.
+ * @param the event type class.
+ */
+ void subscribe(Class extends T> eventType, Consumer subscriber);
+
+ /**
+ * Unsubscribe from all event types.
+ *
+ * @param subscriber the subscriber to unsubscribe.
+ */
+ void unsubscribe(Consumer subscriber);
+
+ /**
+ * Unsubscribe from an event type.
+ *
+ * @param eventType the event type, can be a super class of all events to unsubscribe.
+ * @param subscriber the subscriber to unsubscribe.
+ * @param the event type class.
+ */
+ void unsubscribe(Class extends T> eventType, Consumer subscriber);
+
+ /**
+ * Publish an event to all subscribers.
+ *
+ *
The event type is the class of event. The event is published to all consumers which subscribed to
+ * this event type or any super class.
+ *
+ * @param event the event.
+ */
+ void publish(T event);
+
+}
\ No newline at end of file
diff --git a/src/main/java/dev/masterflomaster1/jfxc/gui/event/HotkeyEvent.java b/src/main/java/dev/masterflomaster1/jfxc/gui/event/HotkeyEvent.java
new file mode 100644
index 0000000..d564bf8
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/gui/event/HotkeyEvent.java
@@ -0,0 +1,23 @@
+package dev.masterflomaster1.jfxc.gui.event;
+
+import javafx.scene.input.KeyCodeCombination;
+
+public final class HotkeyEvent extends Event {
+
+ private final KeyCodeCombination keys;
+
+ public HotkeyEvent(KeyCodeCombination keys) {
+ this.keys = keys;
+ }
+
+ public KeyCodeCombination getKeys() {
+ return keys;
+ }
+
+ @Override
+ public String toString() {
+ return "HotkeyEvent{"
+ + "keys=" + keys
+ + "} " + super.toString();
+ }
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/gui/event/Listener.java b/src/main/java/dev/masterflomaster1/jfxc/gui/event/Listener.java
new file mode 100644
index 0000000..dbc4ed0
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/gui/event/Listener.java
@@ -0,0 +1,8 @@
+package dev.masterflomaster1.jfxc.gui.event;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+@Target(ElementType.METHOD)
+public @interface Listener {
+}
\ No newline at end of file
diff --git a/src/main/java/dev/masterflomaster1/jfxc/gui/event/NavEvent.java b/src/main/java/dev/masterflomaster1/jfxc/gui/event/NavEvent.java
new file mode 100644
index 0000000..5603a80
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/gui/event/NavEvent.java
@@ -0,0 +1,23 @@
+package dev.masterflomaster1.jfxc.gui.event;
+
+import dev.masterflomaster1.jfxc.gui.page.Page;
+
+public final class NavEvent extends Event {
+
+ private final Class extends Page> page;
+
+ public NavEvent(Class extends Page> page) {
+ this.page = page;
+ }
+
+ public Class extends Page> getPage() {
+ return page;
+ }
+
+ @Override
+ public String toString() {
+ return "NavEvent{"
+ + "page=" + page
+ + "} " + super.toString();
+ }
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/gui/event/ThemeEvent.java b/src/main/java/dev/masterflomaster1/jfxc/gui/event/ThemeEvent.java
new file mode 100644
index 0000000..7fe9f78
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/gui/event/ThemeEvent.java
@@ -0,0 +1,33 @@
+package dev.masterflomaster1.jfxc.gui.event;
+
+public final class ThemeEvent extends Event {
+
+ public enum EventType {
+ // theme can change both, base font size and colors
+ THEME_CHANGE,
+ // font size or family only change
+ FONT_CHANGE,
+ // colors only change
+ COLOR_CHANGE,
+ // new theme added or removed
+ THEME_ADD,
+ THEME_REMOVE
+ }
+
+ private final EventType eventType;
+
+ public ThemeEvent(EventType eventType) {
+ this.eventType = eventType;
+ }
+
+ public EventType getEventType() {
+ return eventType;
+ }
+
+ @Override
+ public String toString() {
+ return "ThemeEvent{"
+ + "eventType=" + eventType
+ + "} " + super.toString();
+ }
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/gui/layout/ApplicationWindow.java b/src/main/java/dev/masterflomaster1/jfxc/gui/layout/ApplicationWindow.java
new file mode 100644
index 0000000..34a23a2
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/gui/layout/ApplicationWindow.java
@@ -0,0 +1,30 @@
+package dev.masterflomaster1.jfxc.gui.layout;
+
+import atlantafx.base.controls.ModalPane;
+import dev.masterflomaster1.jfxc.gui.util.NodeUtils;
+import javafx.geometry.Insets;
+import javafx.scene.layout.AnchorPane;
+import javafx.scene.layout.StackPane;
+
+public final class ApplicationWindow extends AnchorPane {
+
+ public static final int MIN_WIDTH = 1000;
+ public static final int SIDEBAR_WIDTH = 250;
+ public static final String MAIN_MODAL_ID = "modal-pane";
+
+
+ public ApplicationWindow() {
+ // this is the place to apply user custom CSS,
+ // one level below the ':root'
+ var body = new StackPane();
+ body.getStyleClass().add("body");
+
+ var modalPane = new ModalPane();
+ modalPane.setId(MAIN_MODAL_ID);
+
+ body.getChildren().setAll(modalPane, new MainLayer());
+ NodeUtils.setAnchors(body, Insets.EMPTY);
+
+ getChildren().setAll(body);
+ }
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/gui/layout/MainLayer.java b/src/main/java/dev/masterflomaster1/jfxc/gui/layout/MainLayer.java
new file mode 100644
index 0000000..88f9745
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/gui/layout/MainLayer.java
@@ -0,0 +1,91 @@
+package dev.masterflomaster1.jfxc.gui.layout;
+
+import dev.masterflomaster1.jfxc.gui.page.Page;
+import javafx.animation.FadeTransition;
+import javafx.application.Platform;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Pane;
+import javafx.scene.layout.StackPane;
+import javafx.util.Duration;
+
+import java.util.Objects;
+
+import static javafx.scene.layout.Priority.ALWAYS;
+
+class MainLayer extends BorderPane {
+
+ static final int PAGE_TRANSITION_DURATION = 250; // ms
+
+ private final MainModel model = new MainModel();
+ private final Sidebar sidebar = new Sidebar(model);
+ private final StackPane subLayerPane = new StackPane();
+
+ MainLayer() {
+ super();
+
+ createView();
+ initListeners();
+
+ model.navigate(MainModel.DEFAULT_PAGE);
+
+ // keyboard navigation won't work without focus
+ Platform.runLater(sidebar::begForFocus);
+ }
+
+ private void createView() {
+ sidebar.setMinWidth(ApplicationWindow.SIDEBAR_WIDTH);
+ sidebar.setMaxWidth(ApplicationWindow.SIDEBAR_WIDTH);
+
+ HBox.setHgrow(subLayerPane, ALWAYS);
+
+ setId("main");
+ setLeft(sidebar);
+ setCenter(subLayerPane);
+ }
+
+ private void initListeners() {
+ model.selectedPageProperty().addListener((obs, old, val) -> {
+ if (val != null) {
+ loadPage(val);
+ }
+ });
+ }
+
+ private void loadPage(Class extends Page> pageClass) {
+ try {
+ final Page prevPage = (Page) subLayerPane.getChildren().stream()
+ .filter(c -> c instanceof Page)
+ .findFirst()
+ .orElse(null);
+ final Page nextPage = pageClass.getDeclaredConstructor().newInstance();
+
+ // startup, no prev page, no animation
+ if (getScene() == null) {
+ subLayerPane.getChildren().add(nextPage.getView());
+ return;
+ }
+
+ Objects.requireNonNull(prevPage);
+
+ // reset previous page, e.g. to free resources
+ prevPage.reset();
+
+ // animate switching between pages
+ subLayerPane.getChildren().add(nextPage.getView());
+ subLayerPane.getChildren().remove(prevPage.getView());
+ var transition = new FadeTransition(Duration.millis(PAGE_TRANSITION_DURATION), nextPage.getView());
+ transition.setFromValue(0.0);
+ transition.setToValue(1.0);
+ transition.setOnFinished(t -> {
+ if (nextPage instanceof Pane nextPane) {
+ nextPane.toFront();
+ }
+ });
+ transition.play();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/gui/layout/MainModel.java b/src/main/java/dev/masterflomaster1/jfxc/gui/layout/MainModel.java
new file mode 100644
index 0000000..06d6bdb
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/gui/layout/MainModel.java
@@ -0,0 +1,158 @@
+package dev.masterflomaster1.jfxc.gui.layout;
+
+import dev.masterflomaster1.jfxc.gui.event.DefaultEventBus;
+import dev.masterflomaster1.jfxc.gui.event.NavEvent;
+import dev.masterflomaster1.jfxc.gui.page.Page;
+import dev.masterflomaster1.jfxc.gui.page.view.AdfgvxPage;
+import dev.masterflomaster1.jfxc.gui.page.view.AffinePage;
+import dev.masterflomaster1.jfxc.gui.page.view.AsymmetricCipherTextPage;
+import dev.masterflomaster1.jfxc.gui.page.view.HashTextPage;
+import dev.masterflomaster1.jfxc.gui.page.view.AtbashPage;
+import dev.masterflomaster1.jfxc.gui.page.view.BlockCipherFilesPage;
+import dev.masterflomaster1.jfxc.gui.page.view.BlockCipherTextPage;
+import dev.masterflomaster1.jfxc.gui.page.view.CaesarPage;
+import dev.masterflomaster1.jfxc.gui.page.view.EnigmaPage;
+import dev.masterflomaster1.jfxc.gui.page.view.HashFilesPage;
+import dev.masterflomaster1.jfxc.gui.page.view.HmacPage;
+import dev.masterflomaster1.jfxc.gui.page.view.PasswordStrengthPage;
+import dev.masterflomaster1.jfxc.gui.page.view.Pbkdf2Page;
+import dev.masterflomaster1.jfxc.gui.page.view.PlayfairCipherPage;
+import dev.masterflomaster1.jfxc.gui.page.view.PwnedPasswordsPage;
+import dev.masterflomaster1.jfxc.gui.page.view.StreamCipherFilesPage;
+import dev.masterflomaster1.jfxc.gui.page.view.StreamCipherTextPage;
+import dev.masterflomaster1.jfxc.gui.page.view.ThemePage;
+import dev.masterflomaster1.jfxc.gui.page.view.VigenereCipherPage;
+import javafx.beans.property.ReadOnlyObjectProperty;
+import javafx.beans.property.ReadOnlyObjectWrapper;
+import org.kordamp.ikonli.bootstrapicons.BootstrapIcons;
+import org.kordamp.ikonli.javafx.FontIcon;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+public class MainModel {
+
+ public static final Class extends Page> DEFAULT_PAGE = HashTextPage.class;
+
+ private static final Map, NavTree.Item> NAV_TREE = createNavItems();
+
+ NavTree.Item getTreeItemForPage(Class extends Page> pageClass) {
+ return NAV_TREE.getOrDefault(pageClass, NAV_TREE.get(DEFAULT_PAGE));
+ }
+
+ List findPages(String filter) {
+ return NAV_TREE.values().stream()
+ .filter(item -> item.getValue() != null && item.getValue().matches(filter))
+ .toList();
+ }
+
+ public MainModel() {
+ DefaultEventBus.getInstance().subscribe(NavEvent.class, e -> navigate(e.getPage()));
+ }
+
+ private final ReadOnlyObjectWrapper> selectedPage = new ReadOnlyObjectWrapper<>();
+
+ public ReadOnlyObjectProperty> selectedPageProperty() {
+ return selectedPage.getReadOnlyProperty();
+ }
+
+ private final ReadOnlyObjectWrapper navTree = new ReadOnlyObjectWrapper<>(createTree());
+
+ public ReadOnlyObjectProperty navTreeProperty() {
+ return navTree.getReadOnlyProperty();
+ }
+
+ private NavTree.Item createTree() {
+ var generalGroup = NavTree.Item.group("General", new FontIcon(BootstrapIcons.FILE_EARMARK_LOCK2));
+ generalGroup.getChildren().setAll(
+ NAV_TREE.get(ThemePage.class)
+ );
+
+ var classicalGroup = NavTree.Item.group("Classical Cryptography", new FontIcon(BootstrapIcons.FILE_EARMARK_LOCK2));
+ classicalGroup.getChildren().setAll(
+ NAV_TREE.get(AdfgvxPage.class),
+ NAV_TREE.get(AtbashPage.class),
+ NAV_TREE.get(AffinePage.class),
+ NAV_TREE.get(CaesarPage.class),
+ NAV_TREE.get(EnigmaPage.class),
+ NAV_TREE.get(PlayfairCipherPage.class),
+ NAV_TREE.get(VigenereCipherPage.class)
+ );
+ classicalGroup.setExpanded(true);
+
+ var asymmetricGroup = NavTree.Item.group("Asymmetric Encryption", new FontIcon(BootstrapIcons.FILE_EARMARK_LOCK2));
+ asymmetricGroup.getChildren().setAll(
+ NAV_TREE.get(AsymmetricCipherTextPage.class)
+ );
+
+ var symmetricGroup = NavTree.Item.group("Symmetric Encryption", new FontIcon(BootstrapIcons.FILE_EARMARK_LOCK2));
+ symmetricGroup.getChildren().setAll(
+ NAV_TREE.get(BlockCipherTextPage.class),
+ NAV_TREE.get(BlockCipherFilesPage.class),
+ NAV_TREE.get(StreamCipherTextPage.class),
+ NAV_TREE.get(StreamCipherFilesPage.class)
+ );
+
+ var hashGroup = NavTree.Item.group("Unkeyed Hash Functions", new FontIcon(BootstrapIcons.FILE_EARMARK_LOCK2));
+ hashGroup.getChildren().setAll(
+ NAV_TREE.get(HashTextPage.class),
+ NAV_TREE.get(HashFilesPage.class)
+ );
+
+ var macGroup = NavTree.Item.group("Message Authentication Code", new FontIcon(BootstrapIcons.FILE_EARMARK_LOCK2));
+ macGroup.getChildren().setAll(NAV_TREE.get(HmacPage.class));
+
+ var passwordGroup = NavTree.Item.group("Passwords", new FontIcon(BootstrapIcons.FILE_EARMARK_LOCK2));
+ passwordGroup.getChildren().setAll(
+ NAV_TREE.get(Pbkdf2Page.class),
+ NAV_TREE.get(PasswordStrengthPage.class),
+ NAV_TREE.get(PwnedPasswordsPage.class)
+ );
+
+ var root = NavTree.Item.root();
+ root.getChildren().setAll(
+ generalGroup,
+ classicalGroup,
+ asymmetricGroup,
+ symmetricGroup,
+ hashGroup,
+ macGroup,
+ passwordGroup
+ );
+
+ return root;
+ }
+
+ public static Map, NavTree.Item> createNavItems() {
+ var map = new HashMap, NavTree.Item>();
+
+ map.put(ThemePage.class, NavTree.Item.page(ThemePage.NAME, ThemePage.class));
+ map.put(AdfgvxPage.class, NavTree.Item.page(AdfgvxPage.NAME, AdfgvxPage.class));
+ map.put(PlayfairCipherPage.class, NavTree.Item.page(PlayfairCipherPage.NAME, PlayfairCipherPage.class));
+ map.put(AtbashPage.class, NavTree.Item.page(AtbashPage.NAME, AtbashPage.class));
+ map.put(EnigmaPage.class, NavTree.Item.page(EnigmaPage.NAME, EnigmaPage.class));
+ map.put(AffinePage.class, NavTree.Item.page(AffinePage.NAME, AffinePage.class));
+ map.put(CaesarPage.class, NavTree.Item.page(CaesarPage.NAME, CaesarPage.class));
+ map.put(VigenereCipherPage.class, NavTree.Item.page(VigenereCipherPage.NAME, VigenereCipherPage.class));
+ map.put(AsymmetricCipherTextPage.class, NavTree.Item.page(AsymmetricCipherTextPage.NAME, AsymmetricCipherTextPage.class));
+ map.put(BlockCipherTextPage.class, NavTree.Item.page(BlockCipherTextPage.NAME, BlockCipherTextPage.class));
+ map.put(BlockCipherFilesPage.class, NavTree.Item.page(BlockCipherFilesPage.NAME, BlockCipherFilesPage.class));
+ map.put(StreamCipherTextPage.class, NavTree.Item.page(StreamCipherTextPage.NAME, StreamCipherTextPage.class));
+ map.put(StreamCipherFilesPage.class, NavTree.Item.page(StreamCipherFilesPage.NAME, StreamCipherFilesPage.class));
+ map.put(HashTextPage.class, NavTree.Item.page(HashTextPage.NAME, HashTextPage.class));
+ map.put(HashFilesPage.class, NavTree.Item.page(HashFilesPage.NAME, HashFilesPage.class));
+ map.put(HmacPage.class, NavTree.Item.page(HmacPage.NAME, HmacPage.class));
+ map.put(PasswordStrengthPage.class, NavTree.Item.page(PasswordStrengthPage.NAME, PasswordStrengthPage.class));
+ map.put(Pbkdf2Page.class, NavTree.Item.page(Pbkdf2Page.NAME, Pbkdf2Page.class));
+ map.put(PwnedPasswordsPage.class, NavTree.Item.page(PwnedPasswordsPage.NAME, PwnedPasswordsPage.class));
+
+ return map;
+ }
+
+ public void navigate(Class extends Page> page) {
+ selectedPage.set(Objects.requireNonNull(page));
+ }
+
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/gui/layout/ModalDialog.java b/src/main/java/dev/masterflomaster1/jfxc/gui/layout/ModalDialog.java
new file mode 100644
index 0000000..77797e5
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/gui/layout/ModalDialog.java
@@ -0,0 +1,64 @@
+package dev.masterflomaster1.jfxc.gui.layout;
+
+import atlantafx.base.controls.Card;
+import atlantafx.base.controls.ModalPane;
+import atlantafx.base.controls.Spacer;
+import atlantafx.base.controls.Tile;
+import atlantafx.base.layout.ModalBox;
+import atlantafx.base.theme.Tweaks;
+import javafx.geometry.Pos;
+import javafx.scene.Scene;
+import javafx.scene.control.Button;
+import javafx.scene.layout.AnchorPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.VBox;
+
+public abstract class ModalDialog extends ModalBox {
+
+ protected final Card content = new Card();
+ protected final Tile header = new Tile();
+
+ public ModalDialog() {
+ super("#" + ApplicationWindow.MAIN_MODAL_ID);
+ createView();
+ }
+
+ public void show(Scene scene) {
+ var modalPane = (ModalPane) scene.lookup("#" + ApplicationWindow.MAIN_MODAL_ID);
+ modalPane.show(this);
+ }
+
+ protected void createView() {
+ content.setHeader(header);
+ content.getStyleClass().add(Tweaks.EDGE_TO_EDGE);
+
+ // IMPORTANT: this guarantees client will use correct width and height
+ setMinWidth(USE_PREF_SIZE);
+ setMaxWidth(USE_PREF_SIZE);
+ setMinHeight(USE_PREF_SIZE);
+ setMaxHeight(USE_PREF_SIZE);
+
+ AnchorPane.setTopAnchor(content, 0d);
+ AnchorPane.setRightAnchor(content, 0d);
+ AnchorPane.setBottomAnchor(content, 0d);
+ AnchorPane.setLeftAnchor(content, 0d);
+
+ addContent(content);
+ getStyleClass().add("modal-dialog");
+ }
+
+ protected HBox createDefaultFooter() {
+ var closeBtn = new Button("Close");
+ closeBtn.getStyleClass().add("form-action");
+ closeBtn.setCancelButton(true);
+ closeBtn.setOnAction(e -> close());
+
+ var footer = new HBox(10, new Spacer(), closeBtn);
+ footer.getStyleClass().add("footer");
+ footer.setAlignment(Pos.CENTER_RIGHT);
+ VBox.setVgrow(footer, Priority.NEVER);
+
+ return footer;
+ }
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/gui/layout/Nav.java b/src/main/java/dev/masterflomaster1/jfxc/gui/layout/Nav.java
new file mode 100644
index 0000000..2cdcdf3
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/gui/layout/Nav.java
@@ -0,0 +1,44 @@
+package dev.masterflomaster1.jfxc.gui.layout;
+
+import dev.masterflomaster1.jfxc.gui.page.Page;
+import javafx.scene.Node;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+record Nav(String title,
+ @Nullable Node graphic,
+ @Nullable Class extends Page> pageClass,
+ @Nullable List searchKeywords) {
+
+ public static final Nav ROOT = new Nav("ROOT", null, null, null);
+
+ private static final Set> TAGGED_PAGES = Set.of(
+ );
+
+ public Nav {
+ Objects.requireNonNull(title, "title");
+ searchKeywords = Objects.requireNonNullElse(searchKeywords, Collections.emptyList());
+ }
+
+ public boolean isGroup() {
+ return pageClass == null;
+ }
+
+ public boolean matches(String filter) {
+ Objects.requireNonNull(filter);
+ return contains(title, filter)
+ || (searchKeywords != null && searchKeywords.stream().anyMatch(keyword -> contains(keyword, filter)));
+ }
+
+ public boolean isTagged() {
+ return pageClass != null && TAGGED_PAGES.contains(pageClass);
+ }
+
+ private boolean contains(String text, String filter) {
+ return text.toLowerCase().contains(filter.toLowerCase());
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/dev/masterflomaster1/jfxc/gui/layout/NavTree.java b/src/main/java/dev/masterflomaster1/jfxc/gui/layout/NavTree.java
new file mode 100644
index 0000000..06c45f4
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/gui/layout/NavTree.java
@@ -0,0 +1,150 @@
+package dev.masterflomaster1.jfxc.gui.layout;
+
+import atlantafx.base.controls.Spacer;
+import atlantafx.base.theme.Tweaks;
+import dev.masterflomaster1.jfxc.gui.page.Page;
+import dev.masterflomaster1.jfxc.gui.util.NodeUtils;
+import javafx.css.PseudoClass;
+import javafx.geometry.Pos;
+import javafx.scene.Cursor;
+import javafx.scene.Node;
+import javafx.scene.control.Label;
+import javafx.scene.control.TreeCell;
+import javafx.scene.control.TreeItem;
+import javafx.scene.control.TreeView;
+import javafx.scene.input.MouseButton;
+import javafx.scene.layout.HBox;
+import org.jetbrains.annotations.Nullable;
+import org.kordamp.ikonli.javafx.FontIcon;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+public final class NavTree extends TreeView
+ */
+public final class SamplerTheme implements Theme {
+
+ private static final int PARSE_LIMIT = 250;
+ private static final Pattern COLOR_PATTERN =
+ Pattern.compile("\s*?(-color-(fg|bg|accent|success|danger|warning)-.+?):\s*?(.+?);");
+
+ private final Theme theme;
+
+ private FileTime lastModified;
+ private Map colors;
+
+ public SamplerTheme(Theme theme) {
+ Objects.requireNonNull(theme);
+
+ if (theme instanceof SamplerTheme) {
+ throw new IllegalArgumentException("Sampler theme must not be wrapped into itself.");
+ }
+
+ this.theme = theme;
+ }
+
+ @Override
+ public String getName() {
+ return theme.getName();
+ }
+
+ // Application.setUserAgentStylesheet() only accepts URL (or URL string representation),
+ // any external file path must have "file://" prefix
+ @Override
+ public String getUserAgentStylesheet() {
+ return IS_DEV_MODE ? DUMMY_STYLESHEET : getResource().toURI().toString();
+ }
+
+ @Override
+ public @Nullable String getUserAgentStylesheetBSS() {
+ return theme.getUserAgentStylesheetBSS();
+ }
+
+ @Override
+ public boolean isDarkMode() {
+ return theme.isDarkMode();
+ }
+
+ public Set getAllStylesheets() {
+ return IS_DEV_MODE ? merge(getResource().toURI().toString(), APP_STYLESHEETS) : Set.of(APP_STYLESHEETS);
+ }
+
+ // Checks whether wrapped theme is a project theme or user external theme.
+ public boolean isProjectTheme() {
+ return PROJECT_THEMES.contains(theme.getClass());
+ }
+
+ // Tries to parse theme CSS and extract conventional looked-up colors. There are few limitations:
+ // - minified CSS files are not supported
+ // - only first PARSE_LIMIT lines will be read
+ public Map parseColors() throws IOException {
+ FileResource file = getResource();
+ return file.internal() ? parseColorsForClasspath(file) : parseColorsForFilesystem(file);
+ }
+
+ private Map parseColors(BufferedReader br) throws IOException {
+ Map colors = new HashMap<>();
+
+ String line;
+ int lineCount = 0;
+
+ while ((line = br.readLine()) != null) {
+ Matcher matcher = COLOR_PATTERN.matcher(line);
+ if (matcher.matches()) {
+ colors.put(matcher.group(1), matcher.group(3));
+ }
+
+ lineCount++;
+ if (lineCount > PARSE_LIMIT) {
+ break;
+ }
+ }
+
+ return colors;
+ }
+
+ private Map parseColorsForClasspath(FileResource file) throws IOException {
+ // classpath resources are static, no need to parse project theme more than once
+ if (colors != null) {
+ return colors;
+ }
+
+ try (var br = new BufferedReader(new InputStreamReader(file.getInputStream(), UTF_8))) {
+ colors = parseColors(br);
+ }
+
+ return colors;
+ }
+
+ private Map parseColorsForFilesystem(FileResource file) throws IOException {
+ // return cached colors if file wasn't changed since the last read
+ FileTime fileTime = Files.getLastModifiedTime(file.toPath(), NOFOLLOW_LINKS);
+ if (Objects.equals(fileTime, lastModified)) {
+ return colors;
+ }
+
+ try (var br = new BufferedReader(new InputStreamReader(file.getInputStream(), UTF_8))) {
+ colors = parseColors(br);
+ }
+
+ // don't save time before parsing is finished to avoid
+ // remembering operation that might end up with an error
+ lastModified = fileTime;
+
+ return colors;
+ }
+
+ public String getPath() {
+ return getResource().toPath().toString();
+ }
+
+ public FileResource getResource() {
+ if (!isProjectTheme()) {
+ return FileResource.createExternal(theme.getUserAgentStylesheet());
+ }
+
+ FileResource classpathTheme = FileResource.createInternal(theme.getUserAgentStylesheet(), Theme.class);
+ if (!IS_DEV_MODE) {
+ return classpathTheme;
+ }
+
+ String filename = classpathTheme.getFilename();
+
+ try {
+ FileResource testTheme = FileResource.createInternal(
+ Resources.resolve("theme-test/" + filename), JFXCrypto.class
+ );
+ if (!testTheme.exists()) {
+ throw new IOException();
+ }
+ return testTheme;
+ } catch (Exception e) {
+ var failedPath = resolve("theme-test/" + filename);
+ System.err.println(
+ "[WARNING] Unable to find theme file \"" + failedPath + "\". Fall back to the classpath.");
+ return classpathTheme;
+ }
+ }
+
+ public Theme unwrap() {
+ return theme;
+ }
+
+ @SafeVarargs
+ private Set merge(T first, T... arr) {
+ var set = new LinkedHashSet();
+ set.add(first);
+ Collections.addAll(set, arr);
+ return set;
+ }
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/gui/theme/ThemeManager.java b/src/main/java/dev/masterflomaster1/jfxc/gui/theme/ThemeManager.java
new file mode 100644
index 0000000..06f88a6
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/gui/theme/ThemeManager.java
@@ -0,0 +1,158 @@
+package dev.masterflomaster1.jfxc.gui.theme;
+
+import atlantafx.base.theme.CupertinoDark;
+import atlantafx.base.theme.CupertinoLight;
+import atlantafx.base.theme.Dracula;
+import atlantafx.base.theme.NordDark;
+import atlantafx.base.theme.NordLight;
+import atlantafx.base.theme.PrimerDark;
+import atlantafx.base.theme.PrimerLight;
+import atlantafx.base.theme.Theme;
+import dev.masterflomaster1.jfxc.Resources;
+import dev.masterflomaster1.jfxc.gui.event.DefaultEventBus;
+import dev.masterflomaster1.jfxc.gui.event.EventBus;
+import dev.masterflomaster1.jfxc.gui.event.ThemeEvent;
+import dev.masterflomaster1.jfxc.gui.event.ThemeEvent.EventType;
+import javafx.animation.Interpolator;
+import javafx.animation.KeyFrame;
+import javafx.animation.KeyValue;
+import javafx.animation.Timeline;
+import javafx.application.Application;
+import javafx.css.PseudoClass;
+import javafx.scene.Scene;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.Pane;
+import javafx.util.Duration;
+
+import java.util.Objects;
+import java.util.Set;
+
+import static dev.masterflomaster1.jfxc.Resources.getResource;
+
+public final class ThemeManager {
+
+ static final String DUMMY_STYLESHEET = getResource("assets/styles/empty.css").toString();
+ static final String[] APP_STYLESHEETS = new String[] {
+ Resources.resolve("assets/styles/index.css")
+ };
+ static final Set> PROJECT_THEMES = Set.of(
+ PrimerLight.class, PrimerDark.class,
+ NordLight.class, NordDark.class,
+ CupertinoLight.class, CupertinoDark.class,
+ Dracula.class
+ );
+
+ private static final PseudoClass DARK = PseudoClass.getPseudoClass("dark");
+ private static final PseudoClass USER_CUSTOM = PseudoClass.getPseudoClass("user-custom");
+ private static final EventBus EVENT_BUS = DefaultEventBus.getInstance();
+
+ public static final AccentColor DEFAULT_ACCENT_COLOR = null;
+
+ private final ThemeRepository repository = new ThemeRepository();
+
+ private Scene scene;
+
+ private SamplerTheme currentTheme;
+ private AccentColor accentColor = DEFAULT_ACCENT_COLOR;
+
+ public ThemeRepository getRepository() {
+ return repository;
+ }
+
+ public Scene getScene() {
+ return scene;
+ }
+
+ // MUST BE SET ON STARTUP
+ // (this is supposed to be a constructor arg, but since app don't use DI..., sorry)
+ public void setScene(Scene scene) {
+ this.scene = Objects.requireNonNull(scene);
+ }
+
+ public SamplerTheme getTheme() {
+ return currentTheme;
+ }
+
+ public SamplerTheme getDefaultTheme() {
+ return getRepository().getAll().get(1);
+ }
+
+ /**
+ * See {@link SamplerTheme}.
+ */
+ public void setTheme(SamplerTheme theme) {
+ Objects.requireNonNull(theme);
+
+ if (currentTheme != null) {
+ animateThemeChange(Duration.millis(500));
+ }
+
+ Application.setUserAgentStylesheet(Objects.requireNonNull(theme.getUserAgentStylesheet()));
+ getScene().getStylesheets().setAll(theme.getAllStylesheets());
+ getScene().getRoot().pseudoClassStateChanged(DARK, theme.isDarkMode());
+
+ // remove user CSS customizations and reset accent on theme change
+ resetAccentColor();
+ resetCustomCSS();
+
+ currentTheme = theme;
+ EVENT_BUS.publish(new ThemeEvent(EventType.THEME_CHANGE));
+ }
+
+ public void setAccentColor(AccentColor color) {
+ Objects.requireNonNull(color);
+
+ animateThemeChange(Duration.millis(350));
+
+ if (accentColor != null) {
+ getScene().getRoot().pseudoClassStateChanged(accentColor.pseudoClass(), false);
+ }
+
+ getScene().getRoot().pseudoClassStateChanged(color.pseudoClass(), true);
+ this.accentColor = color;
+
+ EVENT_BUS.publish(new ThemeEvent(EventType.COLOR_CHANGE));
+ }
+
+ public void resetAccentColor() {
+ animateThemeChange(Duration.millis(350));
+
+ if (accentColor != null) {
+ getScene().getRoot().pseudoClassStateChanged(accentColor.pseudoClass(), false);
+ accentColor = null;
+ }
+
+ EVENT_BUS.publish(new ThemeEvent(EventType.COLOR_CHANGE));
+ }
+
+ private void animateThemeChange(Duration duration) {
+ Image snapshot = scene.snapshot(null);
+ Pane root = (Pane) scene.getRoot();
+
+ ImageView imageView = new ImageView(snapshot);
+ root.getChildren().add(imageView); // add snapshot on top
+
+ var transition = new Timeline(
+ new KeyFrame(Duration.ZERO, new KeyValue(imageView.opacityProperty(), 1, Interpolator.EASE_OUT)),
+ new KeyFrame(duration, new KeyValue(imageView.opacityProperty(), 0, Interpolator.EASE_OUT))
+ );
+ transition.setOnFinished(e -> root.getChildren().remove(imageView));
+ transition.play();
+ }
+
+ public void resetCustomCSS() {
+ getScene().getRoot().pseudoClassStateChanged(USER_CUSTOM, false);
+ }
+
+ private ThemeManager() {
+ }
+
+ private final static class InstanceHolder {
+ private static final ThemeManager INSTANCE = new ThemeManager();
+ }
+
+ public static ThemeManager getInstance() {
+ return InstanceHolder.INSTANCE;
+ }
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/gui/theme/ThemeRepository.java b/src/main/java/dev/masterflomaster1/jfxc/gui/theme/ThemeRepository.java
new file mode 100644
index 0000000..4b00220
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/gui/theme/ThemeRepository.java
@@ -0,0 +1,87 @@
+package dev.masterflomaster1.jfxc.gui.theme;
+
+import atlantafx.base.theme.CupertinoDark;
+import atlantafx.base.theme.CupertinoLight;
+import atlantafx.base.theme.Dracula;
+import atlantafx.base.theme.NordDark;
+import atlantafx.base.theme.NordLight;
+import atlantafx.base.theme.PrimerDark;
+import atlantafx.base.theme.PrimerLight;
+import atlantafx.base.theme.Theme;
+import dev.masterflomaster1.jfxc.Resources;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.prefs.BackingStoreException;
+import java.util.prefs.Preferences;
+
+import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
+
+public final class ThemeRepository {
+
+ private static final Comparator THEME_COMPARATOR = Comparator.comparing(SamplerTheme::getName);
+
+ private final List internalThemes = Arrays.asList(
+ new SamplerTheme(new PrimerLight()),
+ new SamplerTheme(new PrimerDark()),
+ new SamplerTheme(new NordLight()),
+ new SamplerTheme(new NordDark()),
+ new SamplerTheme(new CupertinoLight()),
+ new SamplerTheme(new CupertinoDark()),
+ new SamplerTheme(new Dracula())
+ );
+
+ private final List externalThemes = new ArrayList<>();
+ private final Preferences themePreferences = Resources.getPreferences().node("theme");
+
+ public ThemeRepository() {
+ try {
+ loadPreferences();
+ } catch (BackingStoreException e) {
+ System.out.println("[WARNING] Unable to load themes from the preferences.");
+ e.printStackTrace();
+ }
+ }
+
+ public List getAll() {
+ var list = new ArrayList<>(internalThemes);
+ list.addAll(externalThemes);
+ return list;
+ }
+
+ public boolean isFileValid(Path path) {
+ Objects.requireNonNull(path);
+ return !Files.isDirectory(path, NOFOLLOW_LINKS)
+ && Files.isRegularFile(path, NOFOLLOW_LINKS)
+ && Files.isReadable(path)
+ && path.getFileName().toString().endsWith(".css");
+ }
+
+ private void loadPreferences() throws BackingStoreException {
+ for (String themeName : themePreferences.keys()) {
+ var uaStylesheet = themePreferences.get(themeName, "");
+ var uaStylesheetPath = Paths.get(uaStylesheet);
+
+ // cleanup broken links, e.g. if theme was added for testing
+ // but then CSS file was removed from the filesystem
+ if (!isFileValid(uaStylesheetPath)) {
+ System.err.println(
+ "[WARNING] CSS file invalid or missing: \"" + uaStylesheetPath + "\". Removing silently.");
+ themePreferences.remove(themeName);
+ continue;
+ }
+
+ externalThemes.add(new SamplerTheme(
+ Theme.of(themeName, uaStylesheet, uaStylesheetPath.getFileName().toString().contains("dark"))
+ ));
+ externalThemes.sort(THEME_COMPARATOR);
+ }
+ }
+
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/gui/util/JColor.java b/src/main/java/dev/masterflomaster1/jfxc/gui/util/JColor.java
new file mode 100644
index 0000000..89f9bb9
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/gui/util/JColor.java
@@ -0,0 +1,979 @@
+/**
+ * MIT License
+ *
+ *
Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ *
The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ *
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package dev.masterflomaster1.jfxc.gui.util;
+
+/**
+ * Color representation with support for hex, RGB, arithmetic RGB, HSL, and
+ * integer colors.
+ *
+ * @author osbornb
+ */
+public class JColor {
+
+ /**
+ * Red arithmetic color value.
+ */
+ private float red;
+
+ /**
+ * Green arithmetic color value.
+ */
+ private float green;
+
+ /**
+ * Blue arithmetic color value.
+ */
+ private float blue;
+
+ /**
+ * Opacity arithmetic value.
+ */
+ private float opacity = 1.0f;
+
+ /**
+ * Create the color in hex.
+ *
+ * @param color hex color in format #RRGGBB, RRGGBB, #RGB, RGB, #AARRGGBB,
+ * AARRGGBB, #ARGB, or ARGB
+ * @return color
+ */
+ public static JColor color(String color) {
+ return new JColor(color);
+ }
+
+ /**
+ * Create the color in hex with an opacity.
+ *
+ * @param color hex color in format #RRGGBB, RRGGBB, #RGB, RGB, #AARRGGBB,
+ * AARRGGBB, #ARGB, or ARGB
+ * @param opacity opacity float inclusively between 0.0 and 1.0
+ * @return color
+ */
+ public static JColor color(String color, float opacity) {
+ return new JColor(color, opacity);
+ }
+
+ /**
+ * Create the color in hex with an alpha.
+ *
+ * @param color hex color in format #RRGGBB, RRGGBB, #RGB, RGB, #AARRGGBB,
+ * AARRGGBB, #ARGB, or ARGB
+ * @param alpha alpha integer color inclusively between 0 and 255
+ * @return color
+ */
+ public static JColor color(String color, int alpha) {
+ return new JColor(color, alpha);
+ }
+
+ /**
+ * Create the color with individual hex colors.
+ *
+ * @param red red hex color in format RR
+ * @param green green hex color in format GG
+ * @param blue blue hex color in format BB
+ * @return color
+ */
+ public static JColor color(String red, String green, String blue) {
+ return new JColor(red, green, blue);
+ }
+
+ /**
+ * Create the color with individual hex colors and alpha.
+ *
+ * @param red red hex color in format RR
+ * @param green green hex color in format GG
+ * @param blue blue hex color in format BB
+ * @param alpha alpha hex color in format AA
+ * @return color
+ */
+ public static JColor color(String red, String green, String blue,
+ String alpha) {
+ return new JColor(red, green, blue, alpha);
+ }
+
+ /**
+ * Create the color with individual hex colors and opacity.
+ *
+ * @param red red hex color in format RR
+ * @param green green hex color in format GG
+ * @param blue blue hex color in format BB
+ * @param opacity opacity float inclusively between 0.0 and 1.0
+ * @return color
+ */
+ public static JColor color(String red, String green, String blue,
+ float opacity) {
+ return new JColor(red, green, blue, opacity);
+ }
+
+ /**
+ * Create the color with RGB values.
+ *
+ * @param red red integer color inclusively between 0 and 255
+ * @param green green integer color inclusively between 0 and 255
+ * @param blue blue integer color inclusively between 0 and 255
+ * @return color
+ */
+ public static JColor color(int red, int green, int blue) {
+ return new JColor(red, green, blue);
+ }
+
+ /**
+ * Create the color with RGBA values.
+ *
+ * @param red red integer color inclusively between 0 and 255
+ * @param green green integer color inclusively between 0 and 255
+ * @param blue blue integer color inclusively between 0 and 255
+ * @param alpha alpha integer color inclusively between 0 and 255
+ * @return color
+ */
+ public static JColor color(int red, int green, int blue, int alpha) {
+ return new JColor(red, green, blue, alpha);
+ }
+
+ /**
+ * Create the color with RGBA values.
+ *
+ * @param red red integer color inclusively between 0 and 255
+ * @param green green integer color inclusively between 0 and 255
+ * @param blue blue integer color inclusively between 0 and 255
+ * @param opacity opacity float inclusively between 0.0 and 1.0
+ * @return color
+ */
+ public static JColor color(int red, int green, int blue, float opacity) {
+ return new JColor(red, green, blue, opacity);
+ }
+
+ /**
+ * Create the color with arithmetic RGB values.
+ *
+ * @param red red float color inclusively between 0.0 and 1.0
+ * @param green green float color inclusively between 0.0 and 1.0
+ * @param blue blue float color inclusively between 0.0 and 1.0
+ * @return color
+ */
+ public static JColor color(float red, float green, float blue) {
+ return new JColor(red, green, blue);
+ }
+
+ /**
+ * Create the color with arithmetic RGB values.
+ *
+ * @param red red float color inclusively between 0.0 and 1.0
+ * @param green green float color inclusively between 0.0 and 1.0
+ * @param blue blue float color inclusively between 0.0 and 1.0
+ * @param opacity opacity float inclusively between 0.0 and 1.0
+ * @return color
+ */
+ public static JColor color(float red, float green, float blue,
+ float opacity) {
+ return new JColor(red, green, blue, opacity);
+ }
+
+ /**
+ * Create the color with HSL (hue, saturation, lightness) or HSL (alpha)
+ * values.
+ *
+ * @param hsl HSL array where: 0 = hue, 1 = saturation, 2 = lightness,
+ * optional 3 = alpha
+ * @return color
+ */
+ public static JColor color(float[] hsl) {
+ return new JColor(hsl);
+ }
+
+ /**
+ * Create the color with HSLA (hue, saturation, lightness, alpha) values.
+ *
+ * @param hsl HSL array where: 0 = hue, 1 = saturation, 2 = lightness
+ * @param alpha alpha inclusively between 0.0 and 1.0
+ * @return color
+ */
+ public static JColor color(float[] hsl, float alpha) {
+ return new JColor(hsl, alpha);
+ }
+
+ /**
+ * Create the color as a single integer.
+ *
+ * @param color color integer
+ * @return color
+ */
+ public static JColor color(int color) {
+ return new JColor(color);
+ }
+
+ /**
+ * Default color constructor, opaque black.
+ */
+ public JColor() {
+
+ }
+
+ /**
+ * Create the color in hex.
+ *
+ * @param color hex color in format #RRGGBB, RRGGBB, #RGB, RGB, #AARRGGBB,
+ * AARRGGBB, #ARGB, or ARGB
+ */
+ public JColor(String color) {
+ setColor(color);
+ }
+
+ /**
+ * Create the color in hex with an opacity.
+ *
+ * @param color hex color in format #RRGGBB, RRGGBB, #RGB, RGB, #AARRGGBB,
+ * AARRGGBB, #ARGB, or ARGB
+ * @param opacity opacity float inclusively between 0.0 and 1.0
+ */
+ public JColor(String color, float opacity) {
+ setColor(color, opacity);
+ }
+
+ /**
+ * Create the color in hex with an alpha.
+ *
+ * @param color hex color in format #RRGGBB, RRGGBB, #RGB, RGB, #AARRGGBB,
+ * AARRGGBB, #ARGB, or ARGB
+ * @param alpha alpha integer color inclusively between 0 and 255
+ */
+ public JColor(String color, int alpha) {
+ setColor(color, alpha);
+ }
+
+ /**
+ * Create the color with individual hex colors.
+ *
+ * @param red red hex color in format RR
+ * @param green green hex color in format GG
+ * @param blue blue hex color in format BB
+ */
+ public JColor(String red, String green, String blue) {
+ setColor(red, green, blue);
+ }
+
+ /**
+ * Create the color with individual hex colors and alpha.
+ *
+ * @param red red hex color in format RR
+ * @param green green hex color in format GG
+ * @param blue blue hex color in format BB
+ * @param alpha alpha hex color in format AA
+ */
+ public JColor(String red, String green, String blue, String alpha) {
+ setColor(red, green, blue, alpha);
+ }
+
+ /**
+ * Create the color with individual hex colors and opacity.
+ *
+ * @param red red hex color in format RR
+ * @param green green hex color in format GG
+ * @param blue blue hex color in format BB
+ * @param opacity opacity float inclusively between 0.0 and 1.0
+ */
+ public JColor(String red, String green, String blue, float opacity) {
+ setColor(red, green, blue, opacity);
+ }
+
+ /**
+ * Create the color with RGB values.
+ *
+ * @param red red integer color inclusively between 0 and 255
+ * @param green green integer color inclusively between 0 and 255
+ * @param blue blue integer color inclusively between 0 and 255
+ */
+ public JColor(int red, int green, int blue) {
+ setColor(red, green, blue);
+ }
+
+ /**
+ * Create the color with RGBA values.
+ *
+ * @param red red integer color inclusively between 0 and 255
+ * @param green green integer color inclusively between 0 and 255
+ * @param blue blue integer color inclusively between 0 and 255
+ * @param alpha alpha integer color inclusively between 0 and 255
+ */
+ public JColor(int red, int green, int blue, int alpha) {
+ setColor(red, green, blue, alpha);
+ }
+
+ /**
+ * Create the color with RGBA values.
+ *
+ * @param red red integer color inclusively between 0 and 255
+ * @param green green integer color inclusively between 0 and 255
+ * @param blue blue integer color inclusively between 0 and 255
+ * @param opacity opacity float inclusively between 0.0 and 1.0
+ */
+ public JColor(int red, int green, int blue, float opacity) {
+ setColor(red, green, blue, opacity);
+ }
+
+ /**
+ * Create the color with arithmetic RGB values.
+ *
+ * @param red red float color inclusively between 0.0 and 1.0
+ * @param green green float color inclusively between 0.0 and 1.0
+ * @param blue blue float color inclusively between 0.0 and 1.0
+ */
+ public JColor(float red, float green, float blue) {
+ setColor(red, green, blue);
+ }
+
+ /**
+ * Create the color with arithmetic RGB values.
+ *
+ * @param red red float color inclusively between 0.0 and 1.0
+ * @param green green float color inclusively between 0.0 and 1.0
+ * @param blue blue float color inclusively between 0.0 and 1.0
+ * @param opacity opacity float inclusively between 0.0 and 1.0
+ */
+ public JColor(float red, float green, float blue, float opacity) {
+ setColor(red, green, blue, opacity);
+ }
+
+ /**
+ * Create the color with HSL (hue, saturation, lightness) or HSL (alpha).
+ * values
+ *
+ * @param hsl HSL array where: 0 = hue, 1 = saturation, 2 = lightness,
+ * optional 3 = alpha
+ */
+ public JColor(float[] hsl) {
+ if (hsl.length > 3) {
+ setColorByHSL(hsl[0], hsl[1], hsl[2], hsl[3]);
+ } else {
+ setColorByHSL(hsl[0], hsl[1], hsl[2]);
+ }
+ }
+
+ /**
+ * Create the color with HSLA (hue, saturation, lightness, alpha) values.
+ *
+ * @param hsl HSL array where: 0 = hue, 1 = saturation, 2 = lightness
+ * @param alpha alpha inclusively between 0.0 and 1.0
+ */
+ public JColor(float[] hsl, float alpha) {
+ setColorByHSL(hsl[0], hsl[1], hsl[2], alpha);
+ }
+
+ /**
+ * Create the color as a single integer.
+ *
+ * @param color color integer
+ */
+ public JColor(int color) {
+ setColor(color);
+ }
+
+ /**
+ * Copy constructor.
+ *
+ * @param color color to copy
+ */
+ public JColor(JColor color) {
+ this.red = color.red;
+ this.green = color.green;
+ this.blue = color.blue;
+ this.opacity = color.opacity;
+ }
+
+ /**
+ * Set the color in hex.
+ *
+ * @param color hex color in format #RRGGBB, RRGGBB, #RGB, RGB, #AARRGGBB,
+ * AARRGGBB, #ARGB, or ARGB
+ */
+ public void setColor(String color) {
+ setRed(JColorUtils.getRed(color));
+ setGreen(JColorUtils.getGreen(color));
+ setBlue(JColorUtils.getBlue(color));
+ String alpha = JColorUtils.getAlpha(color);
+ if (alpha != null) {
+ setAlpha(alpha);
+ }
+ }
+
+ /**
+ * Set the color in hex with an opacity.
+ *
+ * @param color hex color in format #RRGGBB, RRGGBB, #RGB, RGB, #AARRGGBB,
+ * AARRGGBB, #ARGB, or ARGB
+ * @param opacity opacity float inclusively between 0.0 and 1.0
+ */
+ public void setColor(String color, float opacity) {
+ setColor(color);
+ setOpacity(opacity);
+ }
+
+ /**
+ * Set the color in hex with an alpha.
+ *
+ * @param color hex color in format #RRGGBB, RRGGBB, #RGB, RGB, #AARRGGBB,
+ * AARRGGBB, #ARGB, or ARGB
+ * @param alpha alpha integer color inclusively between 0 and 255
+ */
+ public void setColor(String color, int alpha) {
+ setColor(color);
+ setAlpha(alpha);
+ }
+
+ /**
+ * Set the color with individual hex colors.
+ *
+ * @param red red hex color in format RR
+ * @param green green hex color in format GG
+ * @param blue blue hex color in format BB
+ */
+ public void setColor(String red, String green, String blue) {
+ setRed(red);
+ setGreen(green);
+ setBlue(blue);
+ }
+
+ /**
+ * Set the color with individual hex colors and alpha.
+ *
+ * @param red red hex color in format RR
+ * @param green green hex color in format GG
+ * @param blue blue hex color in format BB
+ * @param alpha alpha hex color in format AA
+ */
+ public void setColor(String red, String green, String blue, String alpha) {
+ setColor(red, green, blue);
+ setAlpha(alpha);
+ }
+
+ /**
+ * Set the color with individual hex colors and opacity.
+ *
+ * @param red red hex color in format RR
+ * @param green green hex color in format GG
+ * @param blue blue hex color in format BB
+ * @param opacity opacity float inclusively between 0.0 and 1.0
+ */
+ public void setColor(String red, String green, String blue, float opacity) {
+ setColor(red, green, blue);
+ setOpacity(opacity);
+ }
+
+ /**
+ * Set the color with RGB values.
+ *
+ * @param red red integer color inclusively between 0 and 255
+ * @param green green integer color inclusively between 0 and 255
+ * @param blue blue integer color inclusively between 0 and 255
+ */
+ public void setColor(int red, int green, int blue) {
+ setRed(red);
+ setGreen(green);
+ setBlue(blue);
+ }
+
+ /**
+ * Set the color with RGBA values.
+ *
+ * @param red red integer color inclusively between 0 and 255
+ * @param green green integer color inclusively between 0 and 255
+ * @param blue blue integer color inclusively between 0 and 255
+ * @param alpha alpha integer color inclusively between 0 and 255
+ */
+ public void setColor(int red, int green, int blue, int alpha) {
+ setColor(red, green, blue);
+ setAlpha(alpha);
+ }
+
+ /**
+ * Set the color with RGBA values.
+ *
+ * @param red red integer color inclusively between 0 and 255
+ * @param green green integer color inclusively between 0 and 255
+ * @param blue blue integer color inclusively between 0 and 255
+ * @param opacity opacity float inclusively between 0.0 and 1.0
+ */
+ public void setColor(int red, int green, int blue, float opacity) {
+ setColor(red, green, blue);
+ setOpacity(opacity);
+ }
+
+ /**
+ * Set the color with arithmetic RGB values.
+ *
+ * @param red red float color inclusively between 0.0 and 1.0
+ * @param green green float color inclusively between 0.0 and 1.0
+ * @param blue blue float color inclusively between 0.0 and 1.0
+ */
+ public void setColor(float red, float green, float blue) {
+ setRed(red);
+ setGreen(green);
+ setBlue(blue);
+ }
+
+ /**
+ * Set the color with arithmetic RGB values.
+ *
+ * @param red red float color inclusively between 0.0 and 1.0
+ * @param green green float color inclusively between 0.0 and 1.0
+ * @param blue blue float color inclusively between 0.0 and 1.0
+ * @param opacity opacity float inclusively between 0.0 and 1.0
+ */
+ public void setColor(float red, float green, float blue, float opacity) {
+ setColor(red, green, blue);
+ setOpacity(opacity);
+ }
+
+ /**
+ * Set the color as a single integer.
+ *
+ * @param color color integer
+ */
+ public void setColor(int color) {
+ setRed(JColorUtils.getRed(color));
+ setGreen(JColorUtils.getGreen(color));
+ setBlue(JColorUtils.getBlue(color));
+ if (color > 16777215 || color < 0) {
+ setAlpha(JColorUtils.getAlpha(color));
+ }
+ }
+
+ /**
+ * Set the color with HSL (hue, saturation, lightness) values.
+ *
+ * @param hue hue value inclusively between 0.0 and 360.0
+ * @param saturation saturation inclusively between 0.0 and 1.0
+ * @param lightness lightness inclusively between 0.0 and 1.0
+ */
+ public void setColorByHSL(float hue, float saturation, float lightness) {
+ float[] arithmeticRGB = JColorUtils.toArithmeticRGB(hue, saturation,
+ lightness);
+ setRed(arithmeticRGB[0]);
+ setGreen(arithmeticRGB[1]);
+ setBlue(arithmeticRGB[2]);
+ }
+
+ /**
+ * Set the color with HSLA (hue, saturation, lightness, alpha) values.
+ *
+ * @param hue hue value inclusively between 0.0 and 360.0
+ * @param saturation saturation inclusively between 0.0 and 1.0
+ * @param lightness lightness inclusively between 0.0 and 1.0
+ * @param alpha alpha inclusively between 0.0 and 1.0
+ */
+ public void setColorByHSL(float hue, float saturation, float lightness,
+ float alpha) {
+ setColorByHSL(hue, saturation, lightness);
+ setAlpha(alpha);
+ }
+
+ /**
+ * Set the red color in hex.
+ *
+ * @param red red hex color in format RR or R
+ */
+ public void setRed(String red) {
+ setRed(JColorUtils.toArithmeticRGB(red));
+ }
+
+ /**
+ * Set the red color as an integer.
+ *
+ * @param red red integer color inclusively between 0 and 255
+ */
+ public void setRed(int red) {
+ setRed(JColorUtils.toHex(red));
+ }
+
+ /**
+ * Set the red color as an arithmetic float.
+ *
+ * @param red red float color inclusively between 0.0 and 1.0
+ */
+ public void setRed(float red) {
+ JColorUtils.validateArithmeticRGB(red);
+ this.red = red;
+ }
+
+ /**
+ * Set the green color in hex.
+ *
+ * @param green green hex color in format GG or G
+ */
+ public void setGreen(String green) {
+ setGreen(JColorUtils.toArithmeticRGB(green));
+ }
+
+ /**
+ * Set the green color as an integer.
+ *
+ * @param green green integer color inclusively between 0 and 255
+ */
+ public void setGreen(int green) {
+ setGreen(JColorUtils.toHex(green));
+ }
+
+ /**
+ * Set the green color as an arithmetic float.
+ *
+ * @param green green float color inclusively between 0.0 and 1.0
+ */
+ public void setGreen(float green) {
+ JColorUtils.validateArithmeticRGB(green);
+ this.green = green;
+ }
+
+ /**
+ * Set the blue color in hex.
+ *
+ * @param blue blue hex color in format BB or B
+ */
+ public void setBlue(String blue) {
+ setBlue(JColorUtils.toArithmeticRGB(blue));
+ }
+
+ /**
+ * Set the blue color as an integer.
+ *
+ * @param blue blue integer color inclusively between 0 and 255
+ */
+ public void setBlue(int blue) {
+ setBlue(JColorUtils.toHex(blue));
+ }
+
+ /**
+ * Set the blue color as an arithmetic float.
+ *
+ * @param blue blue float color inclusively between 0.0 and 1.0
+ */
+ public void setBlue(float blue) {
+ JColorUtils.validateArithmeticRGB(blue);
+ this.blue = blue;
+ }
+
+ /**
+ * Set the alpha color in hex.
+ *
+ * @param alpha alpha hex color in format AA or A
+ */
+ public void setAlpha(String alpha) {
+ setOpacity(JColorUtils.toArithmeticRGB(alpha));
+ }
+
+ /**
+ * Set the alpha color as an integer.
+ *
+ * @param alpha alpha integer color inclusively between 0 and 255
+ */
+ public void setAlpha(int alpha) {
+ setOpacity(JColorUtils.toArithmeticRGB(alpha));
+ }
+
+ /**
+ * Set the alpha color as an arithmetic float.
+ *
+ * @param alpha alpha float color inclusively between 0.0 and 1.0
+ */
+ public void setAlpha(float alpha) {
+ setOpacity(alpha);
+ }
+
+ /**
+ * Set the opacity as an arithmetic float.
+ *
+ * @param opacity opacity float color inclusively between 0.0 and 1.0
+ */
+ public void setOpacity(float opacity) {
+ JColorUtils.validateArithmeticRGB(opacity);
+ this.opacity = opacity;
+ }
+
+ /**
+ * Check if the color is opaque (opacity or alpha of 1.0, 255, or x00)
+ *
+ * @return true if opaque
+ */
+ public boolean isOpaque() {
+ return opacity == 1.0f;
+ }
+
+ /**
+ * Get the color as a hex string.
+ *
+ * @return hex color in the format #RRGGBB
+ */
+ public String getColorHex() {
+ return JColorUtils.toColor(getRedHex(), getGreenHex(), getBlueHex());
+ }
+
+ /**
+ * Get the color as a hex string with alpha.
+ *
+ * @return hex color in the format #AARRGGBB
+ */
+ public String getColorHexWithAlpha() {
+ return JColorUtils.toColorWithAlpha(getRedHex(), getGreenHex(),
+ getBlueHex(), getAlphaHex());
+ }
+
+ /**
+ * Get the color as a hex string, shorthanded when possible.
+ *
+ * @return hex color in the format #RGB or #RRGGBB
+ */
+ public String getColorHexShorthand() {
+ return JColorUtils.toColorShorthand(getRedHex(), getGreenHex(),
+ getBlueHex());
+ }
+
+ /**
+ * Get the color as a hex string with alpha, shorthanded when possible.
+ *
+ * @return hex color in the format #ARGB or #AARRGGBB
+ */
+ public String getColorHexShorthandWithAlpha() {
+ return JColorUtils.toColorShorthandWithAlpha(getRedHex(), getGreenHex(),
+ getBlueHex(), getAlphaHex());
+ }
+
+ /**
+ * Get the color as an integer.
+ *
+ * @return integer color
+ */
+ public int getColor() {
+ return JColorUtils.toColor(getRed(), getGreen(), getBlue());
+ }
+
+ /**
+ * Get the color as an integer including the alpha.
+ *
+ * @return integer color
+ */
+ public int getColorWithAlpha() {
+ return JColorUtils.toColorWithAlpha(getRed(), getGreen(), getBlue(),
+ getAlpha());
+ }
+
+ /**
+ * Get the red color in hex.
+ *
+ * @return red hex color in format RR
+ */
+ public String getRedHex() {
+ return JColorUtils.toHex(red);
+ }
+
+ /**
+ * Get the green color in hex.
+ *
+ * @return green hex color in format GG
+ */
+ public String getGreenHex() {
+ return JColorUtils.toHex(green);
+ }
+
+ /**
+ * Get the blue color in hex.
+ *
+ * @return blue hex color in format BB
+ */
+ public String getBlueHex() {
+ return JColorUtils.toHex(blue);
+ }
+
+ /**
+ * Get the alpha color in hex.
+ *
+ * @return alpha hex color in format AA
+ */
+ public String getAlphaHex() {
+ return JColorUtils.toHex(opacity);
+ }
+
+ /**
+ * Get the red color in hex, shorthand when possible.
+ *
+ * @return red hex color in format R or RR
+ */
+ public String getRedHexShorthand() {
+ return JColorUtils.shorthandHexSingle(getRedHex());
+ }
+
+ /**
+ * Get the green color in hex, shorthand when possible.
+ *
+ * @return green hex color in format G or GG
+ */
+ public String getGreenHexShorthand() {
+ return JColorUtils.shorthandHexSingle(getGreenHex());
+ }
+
+ /**
+ * Get the blue color in hex, shorthand when possible.
+ *
+ * @return blue hex color in format B or BB
+ */
+ public String getBlueHexShorthand() {
+ return JColorUtils.shorthandHexSingle(getBlueHex());
+ }
+
+ /**
+ * Get the alpha color in hex, shorthand when possible.
+ *
+ * @return alpha hex color in format A or AA
+ */
+ public String getAlphaHexShorthand() {
+ return JColorUtils.shorthandHexSingle(getAlphaHex());
+ }
+
+ /**
+ * Get the red color as an integer.
+ *
+ * @return red integer color inclusively between 0 and 255
+ */
+ public int getRed() {
+ return JColorUtils.toRGB(red);
+ }
+
+ /**
+ * Get the green color as an integer.
+ *
+ * @return green integer color inclusively between 0 and 255
+ */
+ public int getGreen() {
+ return JColorUtils.toRGB(green);
+ }
+
+ /**
+ * Get the blue color as an integer.
+ *
+ * @return blue integer color inclusively between 0 and 255
+ */
+ public int getBlue() {
+ return JColorUtils.toRGB(blue);
+ }
+
+ /**
+ * Get the alpha color as an integer.
+ *
+ * @return alpha integer color inclusively between 0 and 255
+ */
+ public int getAlpha() {
+ return JColorUtils.toRGB(opacity);
+ }
+
+ /**
+ * Get the red color as an arithmetic float.
+ *
+ * @return red float color inclusively between 0.0 and 1.0
+ */
+ public float getRedArithmetic() {
+ return red;
+ }
+
+ /**
+ * Get the green color as an arithmetic float.
+ *
+ * @return green float color inclusively between 0.0 and 1.0
+ */
+ public float getGreenArithmetic() {
+ return green;
+ }
+
+ /**
+ * Get the blue color as an arithmetic float.
+ *
+ * @return blue float color inclusively between 0.0 and 1.0
+ */
+ public float getBlueArithmetic() {
+ return blue;
+ }
+
+ /**
+ * Get the opacity as an arithmetic float.
+ *
+ * @return opacity float inclusively between 0.0 and 1.0
+ */
+ public float getOpacity() {
+ return opacity;
+ }
+
+ /**
+ * Get the alpha color as an arithmetic float.
+ *
+ * @return alpha float color inclusively between 0.0 and 1.0
+ */
+ public float getAlphaArithmetic() {
+ return getOpacity();
+ }
+
+ /**
+ * Get the HSL (hue, saturation, lightness) values.
+ *
+ * @return HSL array where: 0 = hue, 1 = saturation, 2 = lightness
+ */
+ public float[] getHSL() {
+ return JColorUtils.toHSL(red, green, blue);
+ }
+
+ /**
+ * Get the HSL hue value.
+ *
+ * @return hue value
+ */
+ public float getHue() {
+ return getHSL()[0];
+ }
+
+ /**
+ * Get the HSL saturation value.
+ *
+ * @return saturation value
+ */
+ public float getSaturation() {
+ return getHSL()[1];
+ }
+
+ /**
+ * Get the HSL lightness value.
+ *
+ * @return lightness value
+ */
+ public float getLightness() {
+ return getHSL()[2];
+ }
+
+ /**
+ * Copy the color.
+ *
+ * @return color copy
+ */
+ public JColor copy() {
+ return new JColor(this);
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/dev/masterflomaster1/jfxc/gui/util/JColorUtils.java b/src/main/java/dev/masterflomaster1/jfxc/gui/util/JColorUtils.java
new file mode 100644
index 0000000..0274e06
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/gui/util/JColorUtils.java
@@ -0,0 +1,853 @@
+/**
+ * MIT License
+ *
+ *
Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ *
The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ *
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package dev.masterflomaster1.jfxc.gui.util;
+
+import javafx.scene.paint.Color;
+
+import java.util.regex.Pattern;
+
+/**
+ * Color utilities with support for hex, RGB, arithmetic RGB, HSL, and integer colors.
+ *
+ * @author osbornb
+ */
+public final class JColorUtils {
+
+ /**
+ * Hex color pattern.
+ */
+ private static final Pattern HEX_COLOR_PATTERN = Pattern
+ .compile("^#?((\\p{XDigit}{3}){1,2}|(\\p{XDigit}{4}){1,2})$");
+
+ /**
+ * Hex single color pattern.
+ */
+ private static final Pattern HEX_SINGLE_COLOR_PATTERN = Pattern
+ .compile("^\\p{XDigit}{1,2}$");
+
+ private JColorUtils() { }
+
+ /**
+ * Convert the hex color values to a hex color.
+ *
+ * @param red red hex color in format RR or R
+ * @param green green hex color in format GG or G
+ * @param blue blue hex color in format BB or B
+ * @return hex color in format #RRGGBB
+ */
+ public static String toColor(String red, String green, String blue) {
+ return toColorWithAlpha(red, green, blue, null);
+ }
+
+ /**
+ * Convert the RGB values to a color integer.
+ *
+ * @param red red integer color inclusively between 0 and 255
+ * @param green green integer color inclusively between 0 and 255
+ * @param blue blue integer color inclusively between 0 and 255
+ * @return integer color
+ */
+ public static int toColor(int red, int green, int blue) {
+ return toColorWithAlpha(red, green, blue, -1);
+ }
+
+ /**
+ * Convert the hex color values to a hex color, shorthanded when possible.
+ *
+ * @param red red hex color in format RR or R
+ * @param green green hex color in format GG or G
+ * @param blue blue hex color in format BB or B
+ * @return hex color in format #RGB or #RRGGBB
+ */
+ public static String toColorShorthand(String red, String green,
+ String blue) {
+ return shorthandHex(toColor(red, green, blue));
+ }
+
+ /**
+ * Convert the hex color values to a hex color including an opaque alpha
+ * value of FF.
+ *
+ * @param red red hex color in format RR or R
+ * @param green green hex color in format GG or G
+ * @param blue blue hex color in format BB or B
+ * @return hex color in format #AARRGGBB
+ */
+ public static String toColorWithAlpha(String red, String green,
+ String blue) {
+ String defaultAlpha = "FF";
+ if (red != null && !red.isEmpty()
+ && Character.isLowerCase(red.charAt(0))) {
+ defaultAlpha = defaultAlpha.toLowerCase();
+ }
+ return toColorWithAlpha(red, green, blue, defaultAlpha);
+ }
+
+ /**
+ * Convert the hex color values to a hex color.
+ *
+ * @param red red hex color in format RR or R
+ * @param green green hex color in format GG or G
+ * @param blue blue hex color in format BB or B
+ * @param alpha alpha hex color in format AA or A, null to not include alpha
+ * @return hex color in format #AARRGGBB or #RRGGBB
+ */
+ public static String toColorWithAlpha(String red, String green, String blue,
+ String alpha) {
+ validateHexSingle(red);
+ validateHexSingle(green);
+ validateHexSingle(blue);
+ StringBuilder color = new StringBuilder("#");
+ color.append(expandShorthandHexSingle(red));
+ color.append(expandShorthandHexSingle(green));
+ color.append(expandShorthandHexSingle(blue));
+ // alpha must be at the end of the string
+ // not at the start, like it was originally
+ if (alpha != null) {
+ color.append(expandShorthandHexSingle(alpha));
+ }
+ return color.toString();
+ }
+
+ /**
+ * Convert the RGB values to a color integer including an opaque alpha value
+ * of 255.
+ *
+ * @param red red integer color inclusively between 0 and 255
+ * @param green green integer color inclusively between 0 and 255
+ * @param blue blue integer color inclusively between 0 and 255
+ * @return integer color
+ */
+ public static int toColorWithAlpha(int red, int green, int blue) {
+ return toColorWithAlpha(red, green, blue, 255);
+ }
+
+ /**
+ * Convert the RGBA values to a color integer.
+ *
+ * @param red red integer color inclusively between 0 and 255
+ * @param green green integer color inclusively between 0 and 255
+ * @param blue blue integer color inclusively between 0 and 255
+ * @param alpha alpha integer color inclusively between 0 and 255, -1 to not
+ * include alpha
+ * @return integer color
+ */
+ public static int toColorWithAlpha(int red, int green, int blue,
+ int alpha) {
+ validateRGB(red);
+ validateRGB(green);
+ validateRGB(blue);
+ int color = (red & 0xff) << 16 | (green & 0xff) << 8 | (blue & 0xff);
+ if (alpha != -1) {
+ validateRGB(alpha);
+ color = (alpha & 0xff) << 24 | color;
+ }
+ return color;
+ }
+
+ /**
+ * Convert the hex color values to a hex color including an opaque alpha
+ * value of FF or F, shorthanded when possible.
+ *
+ * @param red red hex color in format RR or R
+ * @param green green hex color in format GG or G
+ * @param blue blue hex color in format BB or B
+ * @return hex color in format #ARGB or #AARRGGBB
+ */
+ public static String toColorShorthandWithAlpha(String red, String green,
+ String blue) {
+ return shorthandHex(toColorWithAlpha(red, green, blue));
+ }
+
+ /**
+ * Convert the hex color values to a hex color, shorthanded when possible.
+ *
+ * @param red red hex color in format RR or R
+ * @param green green hex color in format GG or G
+ * @param blue blue hex color in format BB or B
+ * @param alpha alpha hex color in format AA or A, null to not include alpha
+ * @return hex color in format #ARGB, #RGB, #AARRGGBB, or #RRGGBB
+ */
+ public static String toColorShorthandWithAlpha(String red, String green,
+ String blue, String alpha) {
+ return shorthandHex(toColorWithAlpha(red, green, blue, alpha));
+ }
+
+ /**
+ * Convert the RGB integer to a hex single color.
+ *
+ * @param color integer color inclusively between 0 and 255
+ * @return hex single color in format FF
+ */
+ public static String toHex(int color) {
+ validateRGB(color);
+ String hex = Integer.toHexString(color).toUpperCase();
+ if (hex.length() == 1) {
+ hex = "0" + hex;
+ }
+ return hex;
+ }
+
+ /**
+ * Convert the arithmetic RGB float to a hex single color.
+ *
+ * @param color float color inclusively between 0.0 and 1.0
+ * @return hex single color in format FF
+ */
+ public static String toHex(float color) {
+ return toHex(toRGB(color));
+ }
+
+ /**
+ * Convert the hex single color to an RGB integer.
+ *
+ * @param color hex single color in format FF or F
+ * @return integer color inclusively between 0 and 255
+ */
+ public static int toRGB(String color) {
+ validateHexSingle(color);
+ if (color.length() == 1) {
+ color += color;
+ }
+ return Integer.parseInt(color, 16);
+ }
+
+ /**
+ * Convert the arithmetic RGB float to an RGB integer.
+ *
+ * @param color float color inclusively between 0.0 and 1.0
+ * @return integer color inclusively between 0 and 255
+ */
+ public static int toRGB(float color) {
+ validateArithmeticRGB(color);
+ return Math.round(255 * color);
+ }
+
+ /**
+ * Convert HSL (hue, saturation, and lightness) values to RGB integer values.
+ *
+ * @param hue hue value inclusively between 0.0 and 360.0
+ * @param saturation saturation inclusively between 0.0 and 1.0
+ * @param lightness lightness inclusively between 0.0 and 1.0
+ * @return RGB integer array where: 0 = red, 1 = green, 2 = blue
+ */
+ public static int[] toRGB(float hue, float saturation, float lightness) {
+ float[] arithmeticRGB = toArithmeticRGB(hue, saturation, lightness);
+ return new int[] {toRGB(arithmeticRGB[0]), toRGB(arithmeticRGB[1]), toRGB(arithmeticRGB[2])};
+ }
+
+ /**
+ * Convert the hex single color to an arithmetic RGB float.
+ *
+ * @param color hex single color in format FF or F
+ * @return float color inclusively between 0.0 and 1.0
+ */
+ public static float toArithmeticRGB(String color) {
+ return toArithmeticRGB(toRGB(color));
+ }
+
+ /**
+ * Convert the RGB integer to an arithmetic RGB float.
+ *
+ * @param color integer color inclusively between 0 and 255
+ * @return float color inclusively between 0.0 and 1.0
+ */
+ public static float toArithmeticRGB(int color) {
+ validateRGB(color);
+ return color / 255.0f;
+ }
+
+ /**
+ * Convert HSL (hue, saturation, and lightness) values to RGB arithmetic
+ * values.
+ *
+ * @param hue hue value inclusively between 0.0 and 360.0
+ * @param saturation saturation inclusively between 0.0 and 1.0
+ * @param lightness lightness inclusively between 0.0 and 1.0
+ * @return arithmetic RGB array where: 0 = red, 1 = green, 2 = blue
+ */
+ public static float[] toArithmeticRGB(float hue, float saturation, float lightness) {
+ validateHue(hue);
+ validateSaturation(saturation);
+ validateLightness(lightness);
+
+ hue /= 60.0f;
+ float t2;
+ if (lightness <= 0.5f) {
+ t2 = lightness * (saturation + 1);
+ } else {
+ t2 = lightness + saturation - (lightness * saturation);
+ }
+ float t1 = lightness * 2.0f - t2;
+
+ float red = hslConvert(t1, t2, hue + 2);
+ float green = hslConvert(t1, t2, hue);
+ float blue = hslConvert(t1, t2, hue - 2);
+
+ return new float[] {red, green, blue};
+ }
+
+ /**
+ * Convert red, green, and blue arithmetic values to HSL (hue, saturation,
+ * lightness) values.
+ *
+ * @param red red color inclusively between 0.0 and 1.0
+ * @param green green color inclusively between 0.0 and 1.0
+ * @param blue blue color inclusively between 0.0 and 1.0
+ * @return HSL array where: 0 = hue, 1 = saturation, 2 = lightness
+ */
+ public static float[] toHSL(float red, float green, float blue) {
+
+ validateArithmeticRGB(red);
+ validateArithmeticRGB(green);
+ validateArithmeticRGB(blue);
+
+ float min = Math.min(Math.min(red, green), blue);
+ float max = Math.max(Math.max(red, green), blue);
+
+ float range = max - min;
+
+ float hue = 0.0f;
+ if (range > 0.0f) {
+ if (red >= green && red >= blue) {
+ hue = (green - blue) / range;
+ } else if (green >= blue) {
+ hue = 2 + (blue - red) / range;
+ } else {
+ hue = 4 + (red - green) / range;
+ }
+ }
+
+ hue *= 60.0f;
+ if (hue < 0.0f) {
+ hue += 360.0f;
+ }
+
+ float sum = min + max;
+
+ float lightness = sum / 2.0f;
+
+ float saturation;
+ if (min == max) {
+ saturation = 0.0f;
+ } else {
+ if (lightness < 0.5f) {
+ saturation = range / sum;
+ } else {
+ saturation = range / (2.0f - max - min);
+ }
+ }
+
+ return new float[] {hue, saturation, lightness};
+ }
+
+ /**
+ * Convert red, green, and blue integer values to HSL (hue, saturation,
+ * lightness) values.
+ *
+ * @param red red color inclusively between 0 and 255
+ * @param green green color inclusively between 0 and 255
+ * @param blue blue color inclusively between 0 and 255
+ * @return HSL array where: 0 = hue, 1 = saturation, 2 = lightness
+ */
+ public static float[] toHSL(int red, int green, int blue) {
+ return toHSL(toArithmeticRGB(red), toArithmeticRGB(green),
+ toArithmeticRGB(blue));
+ }
+
+ /**
+ * HSL convert helper method.
+ *
+ * @param t1 t1
+ * @param t2 t2
+ * @param hue hue
+ * @return arithmetic RGB value
+ */
+ private static float hslConvert(float t1, float t2, float hue) {
+ float value;
+ if (hue < 0) {
+ hue += 6;
+ }
+ if (hue >= 6) {
+ hue -= 6;
+ }
+ if (hue < 1) {
+ value = (t2 - t1) * hue + t1;
+ } else if (hue < 3) {
+ value = t2;
+ } else if (hue < 4) {
+ value = (t2 - t1) * (4 - hue) + t1;
+ } else {
+ value = t1;
+ }
+ return value;
+ }
+
+ /**
+ * Get the hex red color from the hex string.
+ *
+ * @param hex hex color
+ * @return hex red color in format RR
+ */
+ public static String getRed(String hex) {
+ return getHexSingle(hex, 0);
+ }
+
+ /**
+ * Get the red color from color integer.
+ *
+ * @param color color integer
+ * @return red color
+ */
+ public static int getRed(int color) {
+ return (color >> 16) & 0xff;
+ }
+
+ /**
+ * Get the hex green color from the hex string.
+ *
+ * @param hex hex color
+ * @return hex green color in format GG
+ */
+ public static String getGreen(String hex) {
+ return getHexSingle(hex, 1);
+ }
+
+ /**
+ * Get the green color from color integer.
+ *
+ * @param color color integer
+ * @return green color
+ */
+ public static int getGreen(int color) {
+ return (color >> 8) & 0xff;
+ }
+
+ /**
+ * Get the hex blue color from the hex string.
+ *
+ * @param hex hex color
+ * @return hex blue color in format BB
+ */
+ public static String getBlue(String hex) {
+ return getHexSingle(hex, 2);
+ }
+
+ /**
+ * Get the blue color from color integer.
+ *
+ * @param color color integer
+ * @return blue color
+ */
+ public static int getBlue(int color) {
+ return color & 0xff;
+ }
+
+ /**
+ * Get the hex alpha color from the hex string if it exists.
+ *
+ * @param hex hex color
+ * @return hex alpha color in format AA or null
+ */
+ public static String getAlpha(String hex) {
+ return getHexSingle(hex, -1);
+ }
+
+ /**
+ * Get the alpha color from color integer.
+ *
+ * @param color color integer
+ * @return alpha color
+ */
+ public static int getAlpha(int color) {
+ return (color >> 24) & 0xff;
+ }
+
+ /**
+ * Get the hex single color.
+ *
+ * @param hex hex color
+ * @param colorIndex red=0, green=1, blue=2, alpha=-1
+ * @return hex single color in format FF or null
+ */
+ private static String getHexSingle(String hex, int colorIndex) {
+ validateHex(hex);
+
+ if (hex.startsWith("#")) {
+ hex = hex.substring(1);
+ }
+
+ int colorCharacters = 1;
+ int numColors = hex.length();
+ if (numColors > 4) {
+ colorCharacters++;
+ numColors /= 2;
+ }
+
+ String color = null;
+ if (colorIndex >= 0 || numColors > 3) {
+ if (numColors > 3) {
+ colorIndex++;
+ }
+ int startIndex = colorIndex;
+ if (colorCharacters > 1) {
+ startIndex *= 2;
+ }
+ color = hex.substring(startIndex, startIndex + colorCharacters);
+ color = expandShorthandHexSingle(color);
+ }
+
+ return color;
+ }
+
+ /**
+ * Shorthand the hex color if possible.
+ *
+ * @param color hex color
+ * @return shorthand hex color or original value
+ */
+ public static String shorthandHex(String color) {
+ validateHex(color);
+ if (color.length() > 5) {
+ StringBuilder shorthandColor = new StringBuilder();
+ int startIndex = 0;
+ if (color.startsWith("#")) {
+ shorthandColor.append("#");
+ startIndex++;
+ }
+ for (; startIndex < color.length(); startIndex += 2) {
+ String shorthand = shorthandHexSingle(
+ color.substring(startIndex, startIndex + 2));
+ if (shorthand.length() > 1) {
+ shorthandColor = null;
+ break;
+ }
+ shorthandColor.append(shorthand);
+ }
+ if (shorthandColor != null) {
+ color = shorthandColor.toString();
+ }
+ }
+
+ return color;
+ }
+
+ /**
+ * Expand the hex if it is in shorthand.
+ *
+ * @param color hex color
+ * @return expanded hex color or original value
+ */
+ public static String expandShorthandHex(String color) {
+ validateHex(color);
+ if (color.length() < 6) {
+ StringBuilder expandColor = new StringBuilder();
+ int startIndex = 0;
+ if (color.startsWith("#")) {
+ expandColor.append("#");
+ startIndex++;
+ }
+ for (; startIndex < color.length(); startIndex++) {
+ String expand = expandShorthandHexSingle(
+ color.substring(startIndex, startIndex + 1));
+ expandColor.append(expand);
+ }
+ color = expandColor.toString();
+ }
+ return color;
+ }
+
+ /**
+ * Shorthand the hex single color if possible.
+ *
+ * @param color hex single color
+ * @return shorthand hex color or original value
+ */
+ public static String shorthandHexSingle(String color) {
+ validateHexSingle(color);
+ if (color.length() > 1
+ && Character.toUpperCase(color.charAt(0)) == Character
+ .toUpperCase(color.charAt(1))) {
+ color = color.substring(0, 1);
+ }
+ return color;
+ }
+
+ /**
+ * Expand the hex single if it is in shorthand.
+ *
+ * @param color hex single color
+ * @return expanded hex color or original value
+ */
+ public static String expandShorthandHexSingle(String color) {
+ validateHexSingle(color);
+ if (color.length() == 1) {
+ color += color;
+ }
+ return color;
+ }
+
+ /**
+ * Check if the hex color value is valid.
+ *
+ * @param color hex color
+ * @return true if valid
+ */
+ public static boolean isValidHex(String color) {
+ return color != null && HEX_COLOR_PATTERN.matcher(color).matches();
+ }
+
+ /**
+ * Validate the hex color value.
+ *
+ * @param color hex color
+ */
+ public static void validateHex(String color) {
+ if (!isValidHex(color)) {
+ throw new IllegalArgumentException(
+ "Hex color must be in format #RRGGBB, #RGB, #AARRGGBB, #ARGB, RRGGBB, RGB, AARRGGBB,"
+ + " or ARGB, invalid value: " + color
+ );
+ }
+ }
+
+ /**
+ * Check if the hex single color value is valid.
+ *
+ * @param color hex single color
+ * @return true if valid
+ */
+ public static boolean isValidHexSingle(String color) {
+ return color != null && HEX_SINGLE_COLOR_PATTERN.matcher(color).matches();
+ }
+
+ /**
+ * Validate the hex single color value.
+ *
+ * @param color hex single color
+ */
+ public static void validateHexSingle(String color) {
+ if (!isValidHexSingle(color)) {
+ throw new IllegalArgumentException(
+ "Must be in format FF or F, invalid value: " + color);
+ }
+ }
+
+ /**
+ * Check if the RGB integer color is valid, inclusively between 0 and 255.
+ *
+ * @param color decimal color
+ * @return true if valid
+ */
+ public static boolean isValidRGB(int color) {
+ return color >= 0 && color <= 255;
+ }
+
+ /**
+ * Validate the RGB integer color is inclusively between 0 and 255.
+ *
+ * @param color decimal color
+ */
+ public static void validateRGB(int color) {
+ if (!isValidRGB(color)) {
+ throw new IllegalArgumentException(
+ "Must be inclusively between 0 and 255, invalid value: "
+ + color);
+ }
+ }
+
+ /**
+ * Check if the arithmetic RGB float color is valid, inclusively between 0.0 and 1.0.
+ *
+ * @param color decimal color
+ * @return true if valid
+ */
+ public static boolean isValidArithmeticRGB(float color) {
+ return color >= 0.0 && color <= 1.0;
+ }
+
+ /**
+ * Validate the arithmetic RGB float color is inclusively between 0.0 and 1.0.
+ *
+ * @param color decimal color
+ */
+ public static void validateArithmeticRGB(float color) {
+ if (!isValidArithmeticRGB(color)) {
+ throw new IllegalArgumentException(
+ "Must be inclusively between 0.0 and 1.0, invalid value: "
+ + color);
+ }
+ }
+
+ /**
+ * Check if the HSL hue float value is valid, inclusively between 0.0 and 360.0.
+ *
+ * @param hue hue value
+ * @return true if valid
+ */
+ public static boolean isValidHue(float hue) {
+ return hue >= 0.0 && hue <= 360.0;
+ }
+
+ /**
+ * Validate the HSL hue float value is inclusively between 0.0 and 360.0
+ *
+ * @param hue hue value
+ */
+ public static void validateHue(float hue) {
+ if (!isValidHue(hue)) {
+ throw new IllegalArgumentException(
+ "Must be inclusively between 0.0 and 360.0, invalid value: "
+ + hue);
+ }
+ }
+
+ /**
+ * Check if the HSL saturation float value is valid, inclusively between 0.0 and 1.0.
+ *
+ * @param saturation saturation value
+ * @return true if valid
+ */
+ public static boolean isValidSaturation(float saturation) {
+ return saturation >= 0.0 && saturation <= 1.0;
+ }
+
+ /**
+ * Validate the HSL saturation float value is inclusively between 0.0 and 1.0.
+ *
+ * @param saturation saturation value
+ */
+ public static void validateSaturation(float saturation) {
+ if (!isValidSaturation(saturation)) {
+ throw new IllegalArgumentException(
+ "Must be inclusively between 0.0 and 1.0, invalid value: "
+ + saturation);
+ }
+ }
+
+ /**
+ * Check if the HSL lightness float value is valid, inclusively between 0.0 and 1.0.
+ *
+ * @param lightness lightness value
+ * @return true if valid
+ */
+ public static boolean isValidLightness(float lightness) {
+ return lightness >= 0.0 && lightness <= 1.0;
+ }
+
+ /**
+ * Validate the HSL lightness float value is inclusively between 0.0 and 1.0.
+ *
+ * @param lightness lightness value
+ */
+ public static void validateLightness(float lightness) {
+ if (!isValidLightness(lightness)) {
+ throw new IllegalArgumentException(
+ "Must be inclusively between 0.0 and 1.0, invalid value: "
+ + lightness);
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Additional Utils, mkpaz (c) //
+ ///////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Removes given color opacity, if present.
+ *
+ * When implementing designs, you'll sometimes want to use a lighter shade
+ * of a color for a background. A simple way to achieve lightness is by
+ * increasing the transparency or reducing the opacity of the color
+ * (changing what is known as the alpha channel). Against a white background,
+ * the color will look lighter.
+ *
+ * There are however several issues. Adding an alpha channel means that the
+ * rendered color depends on what color lies underneath. Your elements may
+ * look fine when drawn over a default white background, but if they end up
+ * over another color, the foreground will be affected. Even if a white
+ * background is enforced, if your elements ever overlap, you'll also run
+ * into a problem when using transparency: the overlapping regions will get
+ * darker than the individual elements.
+ *
+ * To remove the transparency we need to blend the foreground color with the
+ * background color, using the transparency value to determine how much to
+ * weight the foreground layer.
+ *
+ * Source.
+ */
+ public static double[] flattenColor(Color bg, Color fgColor) {
+ var opacity = fgColor.getOpacity();
+ return opacity < 1
+ ? new double[] {
+ opacity * fgColor.getRed() + (1 - opacity) * bg.getRed(),
+ opacity * fgColor.getGreen() + (1 - opacity) * bg.getGreen(),
+ opacity * fgColor.getBlue() + (1 - opacity) * bg.getBlue(),
+ }
+ : new double[] {
+ fgColor.getRed(),
+ fgColor.getGreen(),
+ fgColor.getBlue(),
+ };
+ }
+
+ /**
+ * The opposite to the {@link JColorUtils#flattenColor(Color, Color)}. It converts target opaque color
+ * to its equivalent with the desired opacity level.
+ */
+ public static Color opaqueColor(Color bgColor, Color targetColor, double targetOpacity) {
+ return Color.color(
+ bgColor.getRed() + (targetColor.getRed() - bgColor.getRed()) * targetOpacity,
+ bgColor.getGreen() + (targetColor.getGreen() - bgColor.getGreen()) * targetOpacity,
+ bgColor.getBlue() + (targetColor.getBlue() - bgColor.getBlue()) * targetOpacity,
+ targetOpacity
+ );
+ }
+
+ public static float[] toHSL(Color color) {
+ return JColorUtils.toHSL(
+ (float) color.getRed(),
+ (float) color.getGreen(),
+ (float) color.getBlue()
+ );
+ }
+
+ public static String toHexWithAlpha(Color color) {
+ return JColor.color(
+ (float) color.getRed(),
+ (float) color.getGreen(),
+ (float) color.getBlue(),
+ (float) color.getOpacity()
+ ).getColorHexShorthandWithAlpha();
+ }
+
+ public static String toHexOpaque(Color color) {
+ return JColor.color(
+ (float) color.getRed(),
+ (float) color.getGreen(),
+ (float) color.getBlue()
+ ).getColorHexShorthandWithAlpha();
+ }
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/gui/util/Lazy.java b/src/main/java/dev/masterflomaster1/jfxc/gui/util/Lazy.java
new file mode 100644
index 0000000..83339f0
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/gui/util/Lazy.java
@@ -0,0 +1,38 @@
+package dev.masterflomaster1.jfxc.gui.util;
+
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Objects;
+import java.util.function.Supplier;
+
+/**
+ * Auxiliary object wrapper to support lazy initialization.
+ * DO NOT override {@code hashCode()} / {@code equals()}, because each instance
+ * of this object must remain unique.
+ */
+public class Lazy implements Supplier {
+
+ protected final Supplier supplier;
+ protected @Nullable T value;
+
+ public Lazy(Supplier supplier) {
+ this.supplier = Objects.requireNonNull(supplier, "supplier");
+ }
+
+ @Override
+ public T get() {
+ if (value == null) {
+ value = supplier.get();
+ }
+ return value;
+ }
+
+ public boolean initialized() {
+ return value != null;
+ }
+
+ @Override
+ public String toString() {
+ return String.valueOf(value);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/dev/masterflomaster1/jfxc/gui/util/NodeUtils.java b/src/main/java/dev/masterflomaster1/jfxc/gui/util/NodeUtils.java
new file mode 100644
index 0000000..72253dd
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/gui/util/NodeUtils.java
@@ -0,0 +1,72 @@
+package dev.masterflomaster1.jfxc.gui.util;
+
+import javafx.geometry.Insets;
+import javafx.scene.Node;
+import javafx.scene.Parent;
+import javafx.scene.control.ScrollPane;
+import javafx.scene.input.MouseButton;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.AnchorPane;
+
+import java.util.List;
+
+public final class NodeUtils {
+
+ private NodeUtils() { }
+
+ public static void toggleVisibility(Node node, boolean on) {
+ node.setVisible(on);
+ node.setManaged(on);
+ }
+
+ public static void setAnchors(Node node, Insets insets) {
+ if (insets.getTop() >= 0) {
+ AnchorPane.setTopAnchor(node, insets.getTop());
+ }
+ if (insets.getRight() >= 0) {
+ AnchorPane.setRightAnchor(node, insets.getRight());
+ }
+ if (insets.getBottom() >= 0) {
+ AnchorPane.setBottomAnchor(node, insets.getBottom());
+ }
+ if (insets.getLeft() >= 0) {
+ AnchorPane.setLeftAnchor(node, insets.getLeft());
+ }
+ }
+
+ public static void setScrollConstraints(ScrollPane scrollPane,
+ ScrollPane.ScrollBarPolicy vbarPolicy, boolean fitHeight,
+ ScrollPane.ScrollBarPolicy hbarPolicy, boolean fitWidth) {
+ scrollPane.setVbarPolicy(vbarPolicy);
+ scrollPane.setFitToHeight(fitHeight);
+ scrollPane.setHbarPolicy(hbarPolicy);
+ scrollPane.setFitToWidth(fitWidth);
+ }
+
+ public static boolean isDoubleClick(MouseEvent e) {
+ return e.getButton().equals(MouseButton.PRIMARY) && e.getClickCount() == 2;
+ }
+
+ public static T getChildByIndex(Parent parent, int index, Class contentType) {
+ List children = parent.getChildrenUnmodifiable();
+ if (index < 0 || index >= children.size()) {
+ return null;
+ }
+ Node node = children.get(index);
+ return contentType.isInstance(node) ? contentType.cast(node) : null;
+ }
+
+ public static boolean isDescendant(Node ancestor, Node descendant) {
+ if (ancestor == null) {
+ return true;
+ }
+
+ while (descendant != null) {
+ if (descendant == ancestor) {
+ return true;
+ }
+ descendant = descendant.getParent();
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/dev/masterflomaster1/jfxc/utils/StringUtils.java b/src/main/java/dev/masterflomaster1/jfxc/utils/StringUtils.java
new file mode 100644
index 0000000..8399f61
--- /dev/null
+++ b/src/main/java/dev/masterflomaster1/jfxc/utils/StringUtils.java
@@ -0,0 +1,56 @@
+package dev.masterflomaster1.jfxc.utils;
+
+import java.text.CharacterIterator;
+import java.text.StringCharacterIterator;
+
+public final class StringUtils {
+
+ private StringUtils() { }
+
+ public static String spaceAfterN(String input, int n) {
+ StringBuilder result = new StringBuilder();
+
+ for (int i = 0; i < input.length(); i += n) {
+ if (i > 0) {
+ result.append(" ");
+ }
+ int end = Math.min(i + n, input.length());
+ result.append(input, i, end);
+ }
+
+ return result.toString();
+ }
+
+ public static String removeSpaces(String input) {
+ return input.replaceAll(" ", "");
+ }
+
+ public static String removePunctuation(String value) {
+ return value.replaceAll("[^a-zA-Z0-9]", "");
+ }
+
+ /**
+ * Formatting byte size to human-readable format.
+ *
+ * @param bytes the value to convert
+ * @return formatted {@code String} e.g. 108.0 KiB
+ *
+ * @see Algorithm information
+ * @see Algorithm information
+ */
+ public static String convert(long bytes) {
+ long absB = bytes == Long.MIN_VALUE ? Long.MAX_VALUE : Math.abs(bytes);
+ if (absB < 1024) {
+ return bytes + " B";
+ }
+ long value = absB;
+ CharacterIterator ci = new StringCharacterIterator("KMGTPE");
+ for (int i = 40; i >= 0 && absB > 0xfffccccccccccccL >> i; i -= 10) {
+ value >>= 10;
+ ci.next();
+ }
+ value *= Long.signum(bytes);
+ return String.format("%.1f %ciB", value / 1024.0, ci.current());
+ }
+
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
new file mode 100644
index 0000000..ad1b9c9
--- /dev/null
+++ b/src/main/resources/application.properties
@@ -0,0 +1,3 @@
+app.name=JFXCrypto
+app.homepage=${project.parent.url}
+app.version=${project.version}
diff --git a/src/main/resources/assets/fonts/Inter/Inter-Black.otf b/src/main/resources/assets/fonts/Inter/Inter-Black.otf
new file mode 100644
index 0000000..8684287
Binary files /dev/null and b/src/main/resources/assets/fonts/Inter/Inter-Black.otf differ
diff --git a/src/main/resources/assets/fonts/Inter/Inter-BlackItalic.otf b/src/main/resources/assets/fonts/Inter/Inter-BlackItalic.otf
new file mode 100644
index 0000000..7001434
Binary files /dev/null and b/src/main/resources/assets/fonts/Inter/Inter-BlackItalic.otf differ
diff --git a/src/main/resources/assets/fonts/Inter/Inter-Bold.otf b/src/main/resources/assets/fonts/Inter/Inter-Bold.otf
new file mode 100644
index 0000000..502bba3
Binary files /dev/null and b/src/main/resources/assets/fonts/Inter/Inter-Bold.otf differ
diff --git a/src/main/resources/assets/fonts/Inter/Inter-BoldItalic.otf b/src/main/resources/assets/fonts/Inter/Inter-BoldItalic.otf
new file mode 100644
index 0000000..a1f7d88
Binary files /dev/null and b/src/main/resources/assets/fonts/Inter/Inter-BoldItalic.otf differ
diff --git a/src/main/resources/assets/fonts/Inter/Inter-ExtraBold.otf b/src/main/resources/assets/fonts/Inter/Inter-ExtraBold.otf
new file mode 100644
index 0000000..7410f73
Binary files /dev/null and b/src/main/resources/assets/fonts/Inter/Inter-ExtraBold.otf differ
diff --git a/src/main/resources/assets/fonts/Inter/Inter-ExtraBoldItalic.otf b/src/main/resources/assets/fonts/Inter/Inter-ExtraBoldItalic.otf
new file mode 100644
index 0000000..7d451cb
Binary files /dev/null and b/src/main/resources/assets/fonts/Inter/Inter-ExtraBoldItalic.otf differ
diff --git a/src/main/resources/assets/fonts/Inter/Inter-ExtraLight.otf b/src/main/resources/assets/fonts/Inter/Inter-ExtraLight.otf
new file mode 100644
index 0000000..6e9672f
Binary files /dev/null and b/src/main/resources/assets/fonts/Inter/Inter-ExtraLight.otf differ
diff --git a/src/main/resources/assets/fonts/Inter/Inter-ExtraLightItalic.otf b/src/main/resources/assets/fonts/Inter/Inter-ExtraLightItalic.otf
new file mode 100644
index 0000000..e7789f9
Binary files /dev/null and b/src/main/resources/assets/fonts/Inter/Inter-ExtraLightItalic.otf differ
diff --git a/src/main/resources/assets/fonts/Inter/Inter-Italic.otf b/src/main/resources/assets/fonts/Inter/Inter-Italic.otf
new file mode 100644
index 0000000..4e2906e
Binary files /dev/null and b/src/main/resources/assets/fonts/Inter/Inter-Italic.otf differ
diff --git a/src/main/resources/assets/fonts/Inter/Inter-Light.otf b/src/main/resources/assets/fonts/Inter/Inter-Light.otf
new file mode 100644
index 0000000..80ee72b
Binary files /dev/null and b/src/main/resources/assets/fonts/Inter/Inter-Light.otf differ
diff --git a/src/main/resources/assets/fonts/Inter/Inter-LightItalic.otf b/src/main/resources/assets/fonts/Inter/Inter-LightItalic.otf
new file mode 100644
index 0000000..ba2cb1b
Binary files /dev/null and b/src/main/resources/assets/fonts/Inter/Inter-LightItalic.otf differ
diff --git a/src/main/resources/assets/fonts/Inter/Inter-Medium.otf b/src/main/resources/assets/fonts/Inter/Inter-Medium.otf
new file mode 100644
index 0000000..6604db3
Binary files /dev/null and b/src/main/resources/assets/fonts/Inter/Inter-Medium.otf differ
diff --git a/src/main/resources/assets/fonts/Inter/Inter-MediumItalic.otf b/src/main/resources/assets/fonts/Inter/Inter-MediumItalic.otf
new file mode 100644
index 0000000..ea66c5a
Binary files /dev/null and b/src/main/resources/assets/fonts/Inter/Inter-MediumItalic.otf differ
diff --git a/src/main/resources/assets/fonts/Inter/Inter-Regular.otf b/src/main/resources/assets/fonts/Inter/Inter-Regular.otf
new file mode 100644
index 0000000..fdb121d
Binary files /dev/null and b/src/main/resources/assets/fonts/Inter/Inter-Regular.otf differ
diff --git a/src/main/resources/assets/fonts/Inter/Inter-SemiBold.otf b/src/main/resources/assets/fonts/Inter/Inter-SemiBold.otf
new file mode 100644
index 0000000..78482e6
Binary files /dev/null and b/src/main/resources/assets/fonts/Inter/Inter-SemiBold.otf differ
diff --git a/src/main/resources/assets/fonts/Inter/Inter-SemiBoldItalic.otf b/src/main/resources/assets/fonts/Inter/Inter-SemiBoldItalic.otf
new file mode 100644
index 0000000..e74b874
Binary files /dev/null and b/src/main/resources/assets/fonts/Inter/Inter-SemiBoldItalic.otf differ
diff --git a/src/main/resources/assets/fonts/Inter/Inter-Thin.otf b/src/main/resources/assets/fonts/Inter/Inter-Thin.otf
new file mode 100644
index 0000000..90def70
Binary files /dev/null and b/src/main/resources/assets/fonts/Inter/Inter-Thin.otf differ
diff --git a/src/main/resources/assets/fonts/Inter/Inter-ThinItalic.otf b/src/main/resources/assets/fonts/Inter/Inter-ThinItalic.otf
new file mode 100644
index 0000000..cc7419c
Binary files /dev/null and b/src/main/resources/assets/fonts/Inter/Inter-ThinItalic.otf differ
diff --git a/src/main/resources/assets/fonts/Inter/LICENSE.txt b/src/main/resources/assets/fonts/Inter/LICENSE.txt
new file mode 100644
index 0000000..ff80f8c
--- /dev/null
+++ b/src/main/resources/assets/fonts/Inter/LICENSE.txt
@@ -0,0 +1,94 @@
+Copyright (c) 2016-2020 The Inter Project Authors.
+"Inter" is trademark of Rasmus Andersson.
+https://github.com/rsms/inter
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION AND CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/src/main/resources/assets/styles/.editorconfig b/src/main/resources/assets/styles/.editorconfig
new file mode 100644
index 0000000..c6c8b36
--- /dev/null
+++ b/src/main/resources/assets/styles/.editorconfig
@@ -0,0 +1,9 @@
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
diff --git a/src/main/resources/assets/styles/empty.css b/src/main/resources/assets/styles/empty.css
new file mode 100644
index 0000000..4d8db7d
--- /dev/null
+++ b/src/main/resources/assets/styles/empty.css
@@ -0,0 +1 @@
+/* This is dummy file to clear user agent stylesheet. */
diff --git a/src/main/resources/assets/styles/scss/index.scss b/src/main/resources/assets/styles/scss/index.scss
new file mode 100644
index 0000000..d933a4e
--- /dev/null
+++ b/src/main/resources/assets/styles/scss/index.scss
@@ -0,0 +1,6 @@
+// SPDX-License-Identifier: MIT
+
+@use "layout";
+@use "page";
+@use "theme";
+@use "util"
diff --git a/src/main/resources/assets/styles/scss/layout/_fonts.scss b/src/main/resources/assets/styles/scss/layout/_fonts.scss
new file mode 100644
index 0000000..a186f64
--- /dev/null
+++ b/src/main/resources/assets/styles/scss/layout/_fonts.scss
@@ -0,0 +1,118 @@
+// SPDX-License-Identifier: MIT
+
+@font-face {
+ font-family: "Inter";
+ font-weight: 900;
+ src: url('http://github.com/fonts/Inter/Inter-Black.otf') format('truetype');
+}
+
+@font-face {
+ font-family: "Inter";
+ font-weight: 900;
+ font-style: italic, oblique;
+ src: url('http://github.com/fonts/Inter/Inter-BlackItalic.otf') format('truetype');
+}
+
+@font-face {
+ font-family: "Inter";
+ font-weight: 800;
+ src: url('http://github.com/fonts/Inter/Inter-ExtraBold.otf') format('truetype');
+}
+
+@font-face {
+ font-family: "Inter";
+ font-weight: 800;
+ font-style: italic, oblique;
+ src: url('http://github.com/fonts/Inter/Inter-ExtraBoldItalic.otf') format('truetype');
+}
+
+@font-face {
+ font-family: "Inter";
+ font-weight: 700;
+ src: url('http://github.com/fonts/Inter/Inter-Bold.otf') format('truetype');
+}
+
+@font-face {
+ font-family: "Inter";
+ font-weight: 700;
+ font-style: italic, oblique;
+ src: url('http://github.com/fonts/Inter/Inter-BoldItalic.otf') format('truetype');
+}
+
+@font-face {
+ font-family: "Inter";
+ font-weight: 600;
+ src: url('http://github.com/fonts/Inter/Inter-SemiBold.otf') format('truetype');
+}
+
+@font-face {
+ font-family: "Inter";
+ font-weight: 600;
+ font-style: italic, oblique;
+ src: url('http://github.com/fonts/Inter/Inter-SemiBoldItalic.otf') format('truetype');
+}
+
+@font-face {
+ font-family: "Inter";
+ font-weight: 500;
+ src: url('http://github.com/fonts/Inter/Inter-Medium.otf') format('truetype');
+}
+
+@font-face {
+ font-family: "Inter";
+ font-weight: 500;
+ font-style: italic, oblique;
+ src: url('http://github.com/fonts/Inter/Inter-MediumItalic.otf') format('truetype');
+}
+
+@font-face {
+ font-family: "Inter";
+ font-weight: 400;
+ src: url('http://github.com/fonts/Inter/Inter-Regular.otf') format('truetype');
+}
+
+@font-face {
+ font-family: "Inter";
+ font-weight: 400;
+ font-style: italic, oblique;
+ src: url('http://github.com/fonts/Inter/Inter-Italic.otf') format('truetype');
+}
+
+@font-face {
+ font-family: "Inter";
+ font-weight: 300;
+ src: url('http://github.com/fonts/Inter/Inter-Light.otf') format('truetype');
+}
+
+@font-face {
+ font-family: "Inter";
+ font-weight: 300;
+ font-style: italic, oblique;
+ src: url('http://github.com/fonts/Inter/Inter-LightItalic.otf') format('truetype');
+}
+
+@font-face {
+ font-family: "Inter";
+ font-weight: 200;
+ src: url('http://github.com/fonts/Inter/Inter-ExtraLight.otf') format('truetype');
+}
+
+@font-face {
+ font-family: "Inter";
+ font-weight: 200;
+ font-style: italic, oblique;
+ src: url('http://github.com/fonts/Inter/Inter-ExtraLightItalic.otf') format('truetype');
+}
+
+@font-face {
+ font-family: "Inter";
+ font-weight: 100;
+ src: url('http://github.com/fonts/Inter/Inter-Thin.otf') format('truetype');
+}
+
+@font-face {
+ font-family: "Inter";
+ font-weight: 100;
+ font-style: italic, oblique;
+ src: url('http://github.com/fonts/Inter/Inter-ThinItalic.otf') format('truetype');
+}
\ No newline at end of file
diff --git a/src/main/resources/assets/styles/scss/layout/_index.scss b/src/main/resources/assets/styles/scss/layout/_index.scss
new file mode 100644
index 0000000..47e6351
--- /dev/null
+++ b/src/main/resources/assets/styles/scss/layout/_index.scss
@@ -0,0 +1,6 @@
+// SPDX-License-Identifier: MIT
+
+@use "fonts";
+@use "root";
+@use "main";
+@use "sidebar";
diff --git a/src/main/resources/assets/styles/scss/layout/_main.scss b/src/main/resources/assets/styles/scss/layout/_main.scss
new file mode 100644
index 0000000..ceff26b
--- /dev/null
+++ b/src/main/resources/assets/styles/scss/layout/_main.scss
@@ -0,0 +1,19 @@
+// SPDX-License-Identifier: MIT
+
+#main {
+ .source-code {
+ -fx-background-color: -color-bg-default;
+ -fx-alignment: TOP_CENTER;
+
+ >.code-viewer {
+ -fx-background-color: transparent;
+ -fx-padding: 0 0 0 20px;
+ -fx-max-width: 1000px;
+
+ .web-view {
+ -fx-padding: 0;
+ -fx-background-color: transparent;
+ }
+ }
+ }
+}
diff --git a/src/main/resources/assets/styles/scss/layout/_root.scss b/src/main/resources/assets/styles/scss/layout/_root.scss
new file mode 100644
index 0000000..005f792
--- /dev/null
+++ b/src/main/resources/assets/styles/scss/layout/_root.scss
@@ -0,0 +1,79 @@
+// SPDX-License-Identifier: MIT
+
+@use "../theme/accent-colors" as ac;
+
+@mixin hide() {
+ -fx-min-width: 0;
+ -fx-pref-width: 0;
+ -fx-max-width: 0;
+ -fx-min-height: 0;
+ -fx-pref-height: 0;
+ -fx-max-height: 0;
+ visibility: false;
+}
+
+.root {
+ -fx-font-family: "Inter";
+
+ &:showcase-mode {
+
+ #sidebar {
+ @include hide();
+ }
+
+ .page {
+ >.scroll-pane .user-content {
+ -fx-padding: 0;
+
+ >* {
+ -fx-max-width: 4096px;
+ }
+ }
+
+ .about {
+ @include hide();
+ }
+
+ .credits {
+ @include hide();
+ }
+ }
+ }
+
+ // accent colors
+ &:accent-primer-purple {
+ @include ac.primerPurpleLight();
+ }
+
+ &:accent-primer-pink {
+ @include ac.primerPinkLight();
+ }
+
+ &:accent-primer-coral {
+ @include ac.primerCoralLight();
+ }
+
+ &:dark {
+ &:accent-primer-purple {
+ @include ac.primerPurpleDark();
+ }
+
+ &:accent-primer-pink {
+ @include ac.primerPinkDark();
+ }
+
+ &:accent-primer-coral {
+ @include ac.primerCoralDark();
+ }
+ }
+
+ .modal-dialog {
+ -fx-background-color: transparent;
+ -fx-border-width: 1;
+ -fx-border-color: -color-border-default;
+
+ >.card > VBox {
+ -fx-padding: 20px 10px 20px 10px;
+ }
+ }
+}
diff --git a/src/main/resources/assets/styles/scss/layout/_sidebar.scss b/src/main/resources/assets/styles/scss/layout/_sidebar.scss
new file mode 100644
index 0000000..a216786
--- /dev/null
+++ b/src/main/resources/assets/styles/scss/layout/_sidebar.scss
@@ -0,0 +1,117 @@
+// SPDX-License-Identifier: MIT
+
+#sidebar {
+ -fx-background-color: -color-bg-inset;
+ -fx-border-color: -color-border-default;
+ -fx-border-width: 0 1px 0 0;
+
+ >.header {
+ -fx-padding: 20px 10px 20px 10px;
+ -fx-spacing: 20px;
+
+ >.logo {
+ .label {
+ -fx-text-fill: -color-fg-muted;
+ }
+
+ >.palette {
+ >.ikonli-font-icon {
+ -fx-fill: -color-fg-muted;
+ -fx-icon-color: -color-fg-muted;
+ }
+ }
+
+ .dev-indicator {
+ -fx-padding: -0.75em 0 0 0;
+ -fx-font-size: 12px;
+ -fx-font-weight: normal;
+ -fx-cursor: hand;
+ }
+ }
+
+ >.search-button {
+ -color-button-bg: -color-bg-default;
+ -color-button-border: -color-border-default;
+ -color-button-border-hover: -color-border-default;
+ -color-button-border-focused: -color-border-default;
+ -color-button-border-pressed: -color-border-default;
+ -color-button-shadow: transparent;
+ }
+ }
+
+ >.footer {
+ -fx-padding: 4px 10px 4px 10px;
+ -fx-alignment: CENTER;
+ }
+
+ .nav-tree-cell {
+ -fx-padding: 0;
+ -fx-indent: 0;
+ -color-cell-bg: -color-bg-inset;
+ -color-cell-bg-selected: -color-accent-subtle;
+ -color-cell-bg-selected-focused: -color-accent-subtle;
+ -color-cell-border: -color-bg-inset;
+ -fx-background-radius: 5px;
+
+ >.tree-disclosure-node,
+ >.tree-disclosure-node>.arrow {
+ -fx-min-width: 0;
+ -fx-pref-width: 0;
+ -fx-max-width: 0;
+ -fx-min-height: 0;
+ -fx-pref-height: 0;
+ -fx-max-height: 0;
+ visibility: hidden;
+ }
+
+ >.container {
+ -fx-min-height: 2.1em;
+ -fx-pref-height: 2.1em;
+ -fx-max-height: 2.1em;
+ -fx-padding: 0 6px 0 1.2em;
+
+ -fx-border-color: -color-border-muted;
+ -fx-border-width: 0 0 0 1;
+ }
+
+ &:selected>.container>.title {
+ -fx-font-weight: bold;
+ -fx-text-fill: -color-accent-fg;
+ }
+
+ &:hover:filled {
+ -color-cell-bg: -color-accent-subtle;
+ -color-cell-bg-selected: -color-accent-subtle;
+ -color-cell-bg-selected-focused: -color-accent-subtle;
+ }
+
+ &:group {
+ >.container {
+ -fx-min-height: 2.5em;
+ -fx-pref-height: 2.5em;
+ -fx-max-height: 2.5em;
+ -fx-padding: 0 0 0 8px;
+
+ .ikonli-font-icon {
+ -fx-fill: -color-fg-muted;
+ -fx-icon-color: -color-fg-muted;
+ }
+
+ >.arrow {
+ -fx-icon-code: mdal-add;
+ }
+ }
+
+ &:expanded>.container>.arrow {
+ -fx-icon-code: mdmz-remove;
+ }
+ }
+
+ .tag {
+ -fx-padding: 2px 4px 2px 4px;
+ -fx-background-radius: 4px;
+ -fx-font-size: 0.75em;
+ -fx-background-color: -color-success-subtle;
+ }
+ }
+}
diff --git a/src/main/resources/assets/styles/scss/page/_html-editor-fix.scss b/src/main/resources/assets/styles/scss/page/_html-editor-fix.scss
new file mode 100644
index 0000000..8ea2686
--- /dev/null
+++ b/src/main/resources/assets/styles/scss/page/_html-editor-fix.scss
@@ -0,0 +1,183 @@
+// SPDX-License-Identifier: MIT
+
+.html-editor:use-local-url {
+ .color-picker.html-editor-foreground {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Text-Color.png");
+ }
+
+ .color-picker.html-editor-background {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Background-Color.png");
+ }
+
+ .html-editor-cut {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Cut.png");
+ }
+
+ .html-editor-copy {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Copy.png");
+ }
+
+ .html-editor-paste {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Paste.png");
+ }
+
+ .html-editor-align-left {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Left.png");
+ }
+
+ .html-editor-align-center {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Center.png");
+ }
+
+ .html-editor-align-right {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Right.png");
+ }
+
+ .html-editor-align-justify {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Justify.png");
+ }
+
+ .html-editor-outdent {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Outdent.png");
+ }
+
+ .html-editor-outdent:dir(rtl) {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Outdent-rtl.png");
+ }
+
+ .html-editor-indent {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Indent.png");
+ }
+
+ .html-editor-indent:dir(rtl) {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Indent-rtl.png");
+ }
+
+ .html-editor-bullets {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Bullets.png");
+ }
+
+ .html-editor-bullets:dir(rtl) {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Bullets-rtl.png");
+ }
+
+ .html-editor-numbers {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Numbered.png");
+ }
+
+ .html-editor-numbers:dir(rtl) {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Numbered-rtl.png");
+ }
+
+ .html-editor-bold {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Bold.png");
+ }
+
+ .html-editor-italic {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Italic.png");
+ }
+
+ .html-editor-underline {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Underline.png");
+ }
+
+ .html-editor-strike {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Strikethrough.png");
+ }
+
+ .html-editor-hr {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Break.png");
+ }
+}
+
+.root:dark {
+ .html-editor:use-local-url {
+ .color-picker.html-editor-foreground {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Text-Color-White.png");
+ }
+
+ .color-picker.html-editor-background {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Background-Color-White.png");
+ }
+
+ .html-editor-cut {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Cut-White.png");
+ }
+
+ .html-editor-copy {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Copy-White.png");
+ }
+
+ .html-editor-paste {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Paste-White.png");
+ }
+
+ .html-editor-align-left {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Left-White.png");
+ }
+
+ .html-editor-align-center {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Center-White.png");
+ }
+
+ .html-editor-align-right {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Right-White.png");
+ }
+
+ .html-editor-align-justify {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Justify-White.png");
+ }
+
+ .html-editor-outdent {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Outdent-White.png");
+ }
+
+ .html-editor-outdent:dir(rtl) {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Outdent-White-rtl.png");
+ }
+
+ .html-editor-indent {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Indent-White-White.png");
+ }
+
+ .html-editor-indent:dir(rtl) {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Indent-White-rtl.png");
+ }
+
+ .html-editor-bullets {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Bullets-White.png");
+ }
+
+ .html-editor-bullets:dir(rtl) {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Bullets-White-rtl.png");
+ }
+
+ .html-editor-numbers {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Numbered-White.png");
+ }
+
+ .html-editor-numbers:dir(rtl) {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Numbered-White-rtl.png");
+ }
+
+ .html-editor-bold {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Bold-White.png");
+ }
+
+ .html-editor-italic {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Italic-White.png");
+ }
+
+ .html-editor-underline {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Underline-White.png");
+ }
+
+ .html-editor-strike {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Strikethrough-White.png");
+ }
+
+ .html-editor-hr {
+ -fx-graphic: url("http://github.com/atlantafx/sampler/images/modena/HTMLEditor-Break-White.png");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/assets/styles/scss/page/_icon-browser.scss b/src/main/resources/assets/styles/scss/page/_icon-browser.scss
new file mode 100644
index 0000000..30ff2bb
--- /dev/null
+++ b/src/main/resources/assets/styles/scss/page/_icon-browser.scss
@@ -0,0 +1,26 @@
+// SPDX-License-Identifier: MIT
+
+.icon-browser {
+
+ -fx-border-color: transparent;
+ -color-cell-border: transparent;
+
+ >.column-header-background {
+ -fx-max-height: 0;
+ -fx-pref-height: 0;
+ -fx-min-height: 0;
+ }
+
+ .table-row-cell {
+ -fx-cell-size: 80px;
+
+ .icon-label {
+ -fx-padding: 10px;
+ -fx-cursor: hand;
+ }
+ }
+
+ .ikonli-font-icon {
+ -fx-icon-size: 36px;
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/assets/styles/scss/page/_index.scss b/src/main/resources/assets/styles/scss/page/_index.scss
new file mode 100644
index 0000000..49e0f05
--- /dev/null
+++ b/src/main/resources/assets/styles/scss/page/_index.scss
@@ -0,0 +1,6 @@
+// SPDX-License-Identifier: MIT
+
+@use "layout";
+@use "html-editor-fix";
+@use "icon-browser";
+@use "showcase";
diff --git a/src/main/resources/assets/styles/scss/page/_layout.scss b/src/main/resources/assets/styles/scss/page/_layout.scss
new file mode 100644
index 0000000..9c60d8d
--- /dev/null
+++ b/src/main/resources/assets/styles/scss/page/_layout.scss
@@ -0,0 +1,117 @@
+// SPDX-License-Identifier: MIT
+
+.page {
+ -fx-background-color: -color-bg-default;
+ -fx-alignment: TOP_CENTER;
+
+ >.scroll-pane {
+ -fx-padding: 50px 50px 50px 50px;
+
+ .user-content {
+ -fx-alignment: TOP_CENTER;
+ -fx-spacing: 20px;
+ }
+ }
+}
+
+.outline-page {
+
+ -fx-background-color: -color-bg-default;
+ -fx-alignment: TOP_CENTER;
+
+ >.body {
+ >.scroll-pane {
+ -fx-padding: 20px 0 20px 20px;
+
+ .user-content {
+ -fx-spacing: 20px;
+
+ >.header {
+ -fx-padding: 0 0 10px 0;
+ }
+ }
+
+ .tab-pane>.tab-content-area>* {
+ -fx-padding: 20px 0 0 0;
+ }
+
+ .example-box {
+ -fx-spacing: 20px;
+
+ >.tabs {
+ -fx-alignment: CENTER_LEFT;
+ -fx-background-insets: 0, 0 0 1 0;
+ -fx-background-color: -color-border-default, -color-bg-default;
+
+ >.label {
+ -fx-padding: 8 12 8 12;
+ -fx-background-insets: 0, 0 0 1 0;
+ -fx-background-color: -color-border-default, -color-bg-default;
+ -fx-text-fill: -color-fg-muted;
+
+ &:selected {
+ -fx-background-insets: 0, 0 0 3 0;
+ -fx-background-color: -color-accent-emphasis, -color-bg-default;
+ -fx-text-fill: -color-fg-default;
+ }
+
+ &:hover,
+ &:hover:selected {
+ -fx-text-fill: -color-fg-default;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ .outline {
+ -fx-spacing: 10px;
+
+ >.label {
+ -fx-text-fill: -color-fg-muted;
+ -fx-cursor: hand;
+
+ &:selected {
+ -fx-text-fill: -color-accent-fg;
+ }
+
+ &:hover {
+ -fx-text-fill: -color-fg-default;
+ }
+ }
+ }
+}
+
+.snippet {
+ -fx-border-radius: 8px;
+ -fx-border-color: transparent;
+ -fx-border-width: 3px;
+
+ &:hover {
+ -fx-border-color: -color-accent-muted;
+ }
+
+ >TextFlow {
+ -fx-padding: 10px;
+ -fx-background-color: -color-bg-inset;
+ -fx-background-radius: 8px;
+ -fx-font-family: monospace;
+ }
+
+ .keyword {
+ -fx-fill: -color-danger-fg;
+ }
+
+ .paren {
+ -fx-fill: -color-accent-fg;
+ }
+
+ .string {
+ -fx-fill: -color-success-fg;
+ }
+
+ .comment {
+ -fx-fill: -color-fg-subtle;
+ }
+}
diff --git a/src/main/resources/assets/styles/scss/page/_showcase.scss b/src/main/resources/assets/styles/scss/page/_showcase.scss
new file mode 100644
index 0000000..787c0d9
--- /dev/null
+++ b/src/main/resources/assets/styles/scss/page/_showcase.scss
@@ -0,0 +1,56 @@
+// SPDX-License-Identifier: MIT
+
+.showcase-page {
+ -fx-background-color: -color-bg-default;
+
+ >.window {
+ -fx-border-color: -color-accent-emphasis;
+ -fx-border-width: 1;
+ -fx-border-radius: 5px 5px 0 0;
+
+ >.header {
+ -fx-background-color: -color-accent-emphasis;
+ -fx-alignment: CENTER_LEFT;
+ -fx-padding: 10px;
+
+ >.title {
+ -fx-font-size: 1.2em;
+ -fx-text-fill: -color-fg-emphasis;
+ -fx-font-weight: bold;
+ -fx-graphic-text-gap: 10px;
+
+ >.ikonli-font-icon {
+ -fx-icon-color: -color-fg-emphasis;
+ -fx-fill: -color-fg-emphasis;
+ }
+ }
+
+ >.ikonli-font-icon {
+ -fx-icon-color: -color-fg-emphasis;
+ -fx-fill: -color-fg-emphasis;
+
+ &:hover {
+ -fx-effect: dropshadow(gaussian, -color-fg-emphasis, 20, 0.1, 0, 0)
+ }
+ }
+ }
+ }
+}
+
+.root:showcase-mode {
+
+ .showcase-page>.window {
+ -fx-border-width: 0;
+ -fx-border-radius: 0;
+ }
+}
+
+#blueprints {
+ -fx-background-color: -color-bg-inset;
+
+ .sample {
+ -fx-background-color: -color-border-subtle, -color-bg-default;
+ -fx-background-insets: 0, 1;
+ -fx-background-radius: 10px;
+ }
+}
diff --git a/src/main/resources/assets/styles/scss/theme/_accent-color-selector.scss b/src/main/resources/assets/styles/scss/theme/_accent-color-selector.scss
new file mode 100644
index 0000000..4cab76e
--- /dev/null
+++ b/src/main/resources/assets/styles/scss/theme/_accent-color-selector.scss
@@ -0,0 +1,19 @@
+// SPDX-License-Identifier: MIT
+
+.color-selector {
+ -color-primary: -color-accent-emphasis;
+
+ -fx-spacing: 1em;
+
+ >.color-button {
+ >.icon {
+ -fx-min-width: 1em;
+ -fx-pref-width: 1em;
+ -fx-max-height: 1em;
+ -fx-min-width: 1em;
+ -fx-pref-height: 1em;
+ -fx-max-height: 1em;
+ -fx-background-color: -color-primary;
+ }
+ }
+}
diff --git a/src/main/resources/assets/styles/scss/theme/_accent-colors.scss b/src/main/resources/assets/styles/scss/theme/_accent-colors.scss
new file mode 100644
index 0000000..c0f9bd1
--- /dev/null
+++ b/src/main/resources/assets/styles/scss/theme/_accent-colors.scss
@@ -0,0 +1,105 @@
+// SPDX-License-Identifier: MIT
+
+@use "sass:color";
+
+@mixin primerPurpleLight() {
+ -color-accent-0: #fbefff;
+ -color-accent-1: #ecd8ff;
+ -color-accent-2: #d8b9ff;
+ -color-accent-3: #c297ff;
+ -color-accent-4: #a475f9;
+ -color-accent-5: #8250df;
+ -color-accent-6: #6639ba;
+ -color-accent-7: #512a97;
+ -color-accent-8: #3e1f79;
+ -color-accent-9: #2e1461;
+ -color-accent-fg: #8250df;
+ -color-accent-emphasis: #8250df;
+ -color-accent-muted: color.change(#c297ff, $alpha: 0.4);
+ -color-accent-subtle: #fbefff;
+}
+
+@mixin primerPinkLight() {
+ -color-accent-0: #ffeff7;
+ -color-accent-1: #ffd3eb;
+ -color-accent-2: #ffadda;
+ -color-accent-3: #ff80c8;
+ -color-accent-4: #e85aad;
+ -color-accent-5: #bf3989;
+ -color-accent-6: #99286e;
+ -color-accent-7: #772057;
+ -color-accent-8: #611347;
+ -color-accent-9: #4d0336;
+ -color-accent-fg: #bf3989;
+ -color-accent-emphasis: #bf3989;
+ -color-accent-muted: color.change(#ff80c8, $alpha: 0.4);
+ -color-accent-subtle: #ffeff7;
+}
+
+@mixin primerCoralLight() {
+ -color-accent-0: #fff0eb;
+ -color-accent-1: #ffd6cc;
+ -color-accent-2: #ffb4a1;
+ -color-accent-3: #fd8c73;
+ -color-accent-4: #ec6547;
+ -color-accent-5: #c4432b;
+ -color-accent-6: #9e2f1c;
+ -color-accent-7: #801f0f;
+ -color-accent-8: #691105;
+ -color-accent-9: #510901;
+ -color-accent-fg: #c4432b;
+ -color-accent-emphasis: #c4432b;
+ -color-accent-muted: color.change(#fd8c73, $alpha: 0.4);
+ -color-accent-subtle: #fff0eb;
+}
+
+@mixin primerPurpleDark() {
+ -color-accent-0: #eddeff;
+ -color-accent-1: #e2c5ff;
+ -color-accent-2: #d2a8ff;
+ -color-accent-3: #bc8cff;
+ -color-accent-4: #a371f7;
+ -color-accent-5: #8957e5;
+ -color-accent-6: #6e40c9;
+ -color-accent-7: #553098;
+ -color-accent-8: #3c1e70;
+ -color-accent-9: #271052;
+ -color-accent-fg: #bc8cff;
+ -color-accent-emphasis: #8957e5;
+ -color-accent-muted: color.change(#a371f7, $alpha: 0.4);
+ -color-accent-subtle: color.change(#a371f7, $alpha: 0.15);
+}
+
+@mixin primerPinkDark() {
+ -color-accent-0: #ffdaec;
+ -color-accent-1: #ffbedd;
+ -color-accent-2: #ff9bce;
+ -color-accent-3: #f778ba;
+ -color-accent-4: #db61a2;
+ -color-accent-5: #bf4b8a;
+ -color-accent-6: #9e3670;
+ -color-accent-7: #7d2457;
+ -color-accent-8: #5e103e;
+ -color-accent-9: #42062a;
+ -color-accent-fg: #f778ba;
+ -color-accent-emphasis: #bf4b8a;
+ -color-accent-muted: color.change(#db61a2, $alpha: 0.4);
+ -color-accent-subtle: color.change(#db61a2, $alpha: 0.15);
+}
+
+@mixin primerCoralDark() {
+ -color-accent-0: #ffddd2;
+ -color-accent-1: #ffc2b2;
+ -color-accent-2: #ffa28b;
+ -color-accent-3: #f78166;
+ -color-accent-4: #ea6045;
+ -color-accent-5: #cf462d;
+ -color-accent-6: #ac3220;
+ -color-accent-7: #872012;
+ -color-accent-8: #640d04;
+ -color-accent-9: #460701;
+ -color-accent-fg: #f78166;
+ -color-accent-emphasis: #cf462d;
+ -color-accent-muted: color.change(#ea6045, $alpha: 0.4);
+ -color-accent-subtle: color.change(#ea6045, $alpha: 0.15);
+}
diff --git a/src/main/resources/assets/styles/scss/theme/_color-palette.scss b/src/main/resources/assets/styles/scss/theme/_color-palette.scss
new file mode 100644
index 0000000..b2f581b
--- /dev/null
+++ b/src/main/resources/assets/styles/scss/theme/_color-palette.scss
@@ -0,0 +1,41 @@
+// SPDX-License-Identifier: MIT
+
+$color-wcag-bg-passed: #388e3c;
+$color-wcag-bg-failed: #ef5350;
+$color-wcag-fg: white;
+
+#color-palette {
+ -fx-vgap: 20px;
+ -fx-hgap: 25px;
+
+ // mandatory base bg for flatten color calc
+ -fx-background-color: -color-bg-default;
+
+ >.block {
+ -fx-spacing: 5px;
+
+ >.rectangle {
+ -fx-min-width: 150px;
+ -fx-min-height: 70px;
+ -fx-max-width: 150px;
+ -fx-max-height: 70px;
+ -fx-cursor: hand;
+
+ &:passed>.contrast-level-label {
+ -fx-background-color: $color-wcag-bg-passed;
+ }
+
+ >.contrast-level-label {
+ -fx-text-fill: $color-wcag-fg;
+ -fx-background-color: $color-wcag-bg-failed;
+ -fx-background-radius: 6px;
+ -fx-padding: 3px;
+
+ >.ikonli-font-icon {
+ -fx-fill: $color-wcag-fg;
+ -fx-icon-color: $color-wcag-fg;
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/resources/assets/styles/scss/theme/_color-scale.scss b/src/main/resources/assets/styles/scss/theme/_color-scale.scss
new file mode 100644
index 0000000..474d9c6
--- /dev/null
+++ b/src/main/resources/assets/styles/scss/theme/_color-scale.scss
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: MIT
+
+#color-scale {
+ -fx-hgap: 20px;
+ -fx-vgap: 20px;
+
+ >.column {
+ >.cell {
+ -fx-text-fill: -color-fg-default;
+ -fx-font-size: 1.1em;
+ -fx-padding: 0 0 0 10px;
+ }
+ }
+}
diff --git a/src/main/resources/assets/styles/scss/theme/_contrast-checker.scss b/src/main/resources/assets/styles/scss/theme/_contrast-checker.scss
new file mode 100644
index 0000000..3ab7fb2
--- /dev/null
+++ b/src/main/resources/assets/styles/scss/theme/_contrast-checker.scss
@@ -0,0 +1,137 @@
+// SPDX-License-Identifier: MIT
+
+@use "color-palette" as palette;
+
+.contrast-checker {
+
+ -fx-hgap: 40px;
+ -fx-vgap: 20px;
+
+ .label {
+ -fx-text-fill: -color-contrast-checker-fg;
+ }
+
+ .text-field {
+ -color-input-bg: transparent;
+ -color-input-fg: -color-contrast-checker-fg;
+ -color-input-border: transparent;
+ -color-input-bg-readonly: transparent;
+ -color-input-bg-focused: transparent;
+ -color-input-border-focused: transparent;
+
+ -fx-background-insets: 0;
+ -fx-background-radius: 0;
+ -fx-border-color: -color-contrast-checker-fg;
+ -fx-border-width: 0 0 1 0;
+ }
+
+ .button {
+ -color-button-bg: transparent;
+ -color-button-fg: -color-contrast-checker-fg;
+ -color-button-border: transparent;
+
+ -color-button-bg-hover: transparent;
+ -color-button-fg-hover: -color-contrast-checker-fg;
+ -color-button-border-hover: transparent;
+
+ -color-button-bg-focused: transparent;
+ -color-button-fg-focused: -color-contrast-checker-fg;
+ -color-button-border-focused: transparent;
+
+ -color-button-bg-pressed: transparent;
+ -color-button-fg-pressed: -color-contrast-checker-fg;
+ -color-button-border-pressed: transparent;
+
+ &:armed,
+ &:focused:armed {
+ -fx-border-color: transparent;
+ }
+ }
+
+ .slider {
+ >.thumb {
+ -color-slider-thumb: -color-contrast-checker-fg;
+ -color-slider-thumb-border: -color-contrast-checker-fg;
+ }
+
+ >.track {
+ -color-slider-track: -color-contrast-checker-fg;
+ -fx-opacity: 0.5;
+ }
+ }
+
+ .ikonli-font-icon {
+ -fx-icon-color: -color-contrast-checker-fg;
+ -fx-fill: -color-contrast-checker-fg;
+ }
+
+ .contrast-ratio {
+ -fx-padding: 0 40px 0 0;
+
+ >.large-font {
+ -fx-font-size: 4em;
+ }
+
+ >.ratio {
+ -fx-font-size: 2em;
+ }
+ }
+
+ .contrast-level {
+ >.state {
+ -fx-padding: 0.5em 1em 0.5em 1em;
+ -fx-background-color: palette.$color-wcag-bg-failed;
+ -fx-background-radius: 4px;
+ -fx-text-fill: palette.$color-wcag-fg;
+
+ &:passed {
+ -fx-background-color: palette.$color-wcag-bg-passed;
+ }
+
+ >.ikonli-font-icon {
+ -fx-fill: palette.$color-wcag-fg;
+ -fx-icon-color: palette.$color-wcag-fg;
+ }
+ }
+ }
+
+ >.actions {
+ >.button {
+ -fx-border-width: 1px;
+ -fx-border-color: -color-contrast-checker-fg;
+ -fx-border-radius: 4px;
+ }
+ }
+
+ .context-menu {
+ -fx-background-color: -color-border-muted, -color-bg-default;
+
+ >.label {
+ -fx-text-fill: -color-fg-default;
+ }
+ }
+}
+
+.contrast-checker-dialog {
+ -color-modal-box-bg: -color-contrast-checker-bg;
+ -color-modal-box-close-fg: -color-contrast-checker-fg;
+ -color-modal-box-close-bg-hover: derive(-color-contrast-checker-bg, 30%);
+
+ -fx-background-color: transparent;
+
+ &.modal-dialog {
+ -fx-border-width: 0;
+ }
+
+ .card {
+ -fx-border-width: 0;
+
+ > VBox {
+ -fx-background-color: -color-contrast-checker-bg;
+ }
+
+ .title {
+ -fx-text-fill: -color-contrast-checker-fg;
+ }
+ }
+}
diff --git a/src/main/resources/assets/styles/scss/theme/_index.scss b/src/main/resources/assets/styles/scss/theme/_index.scss
new file mode 100644
index 0000000..e16138c
--- /dev/null
+++ b/src/main/resources/assets/styles/scss/theme/_index.scss
@@ -0,0 +1,9 @@
+// SPDX-License-Identifier: MIT
+
+@use "accent-color-selector";
+@use "color-palette";
+@use "color-scale";
+@use "contrast-checker";
+@use "scene-builder-wizard";
+@use "theme-repo-manager";
+@use "theme-thumbnail";
diff --git a/src/main/resources/assets/styles/scss/theme/_scene-builder-wizard.scss b/src/main/resources/assets/styles/scss/theme/_scene-builder-wizard.scss
new file mode 100644
index 0000000..579e412
--- /dev/null
+++ b/src/main/resources/assets/styles/scss/theme/_scene-builder-wizard.scss
@@ -0,0 +1,9 @@
+// SPDX-License-Identifier: MIT
+
+#scene-builder-wizard {
+ .screen {
+ -fx-padding: 10px 0 10px 0;
+ -fx-spacing: 10px;
+ -fx-background-color: -color-bg-default;
+ }
+}
diff --git a/src/main/resources/assets/styles/scss/theme/_theme-repo-manager.scss b/src/main/resources/assets/styles/scss/theme/_theme-repo-manager.scss
new file mode 100644
index 0000000..a3314dc
--- /dev/null
+++ b/src/main/resources/assets/styles/scss/theme/_theme-repo-manager.scss
@@ -0,0 +1,71 @@
+// SPDX-License-Identifier: MIT
+
+@use "color-palette" as palette;
+
+#theme-repo-manager {
+ -fx-spacing: 10px;
+
+ >.info {
+ -fx-background-color: -color-accent-subtle;
+ -fx-padding: 0.5em;
+ -fx-border-width: 1px;
+ -fx-border-color: -color-accent-muted;
+
+ >.label {
+ -fx-text-fill: -color-accent-fg;
+ }
+ }
+
+ .scroll-pane {
+ -fx-border-width: 1px;
+ -fx-border-color: -color-border-muted;
+ }
+
+ .theme-list {
+ -fx-padding: 10px;
+ -fx-spacing: 5px;
+
+ >.theme {
+ -fx-spacing: 10px;
+ -fx-min-height: 3em;
+ -fx-padding: 10px;
+
+ &:hover {
+ -fx-background-color: -color-bg-subtle;
+ }
+
+ &:selected {
+ >.title {
+ >.text {
+ -fx-fill: -color-accent-fg;
+ }
+ }
+ }
+
+ >.title {
+ >.sub-text {
+ -fx-fill: -color-fg-muted;
+ }
+ }
+
+ >.preview {
+ >.label {
+ -fx-alignment: CENTER;
+ -fx-font-size: 1.2em;
+ -fx-min-width: 2em;
+ -fx-pref-width: 2em;
+ -fx-max-width: 2em;
+ -fx-min-height: 2em;
+ -fx-pref-height: 2em;
+ -fx-max-height: 2em;
+ }
+ }
+
+ >.controls {
+ -fx-min-width: 4em;
+ -fx-pref-width: 4em;
+ -fx-max-width: 4em;
+ }
+ }
+ }
+}
diff --git a/src/main/resources/assets/styles/scss/theme/_theme-thumbnail.scss b/src/main/resources/assets/styles/scss/theme/_theme-thumbnail.scss
new file mode 100644
index 0000000..b676f34
--- /dev/null
+++ b/src/main/resources/assets/styles/scss/theme/_theme-thumbnail.scss
@@ -0,0 +1,19 @@
+// SPDX-License-Identifier: MIT
+
+.theme-thumbnail {
+ -fx-spacing: 20px;
+ -fx-padding: 20px;
+ -fx-alignment: CENTER;
+
+ &:hover {
+ -color-thumbnail-border: -color-accent-muted;
+ }
+
+ &:selected {
+ -color-thumbnail-border: -color-accent-emphasis;
+
+ .label {
+ -fx-underline: true;
+ }
+ }
+}
diff --git a/src/main/resources/assets/styles/scss/util/_index.scss b/src/main/resources/assets/styles/scss/util/_index.scss
new file mode 100644
index 0000000..0993de5
--- /dev/null
+++ b/src/main/resources/assets/styles/scss/util/_index.scss
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: MIT
+
+.bordered {
+ -fx-border-width: 1px;
+ -fx-border-color: -color-border-muted;
+}
+
+.icon-subtle {
+ -fx-fill: -color-fg-subtle;
+ -fx-icon-color: -color-fg-subtle;
+}
diff --git a/src/resources/SJC.png b/src/main/resources/images/SJC.png
similarity index 100%
rename from src/resources/SJC.png
rename to src/main/resources/images/SJC.png
diff --git a/src/resources/SJC1.png b/src/main/resources/images/SJC1.png
similarity index 100%
rename from src/resources/SJC1.png
rename to src/main/resources/images/SJC1.png
diff --git a/src/resources/SJC25.png b/src/main/resources/images/SJC25.png
similarity index 100%
rename from src/resources/SJC25.png
rename to src/main/resources/images/SJC25.png
diff --git a/src/resources/d.png b/src/resources/d.png
deleted file mode 100644
index 172221e..0000000
Binary files a/src/resources/d.png and /dev/null differ
diff --git a/src/resources/d30.png b/src/resources/d30.png
deleted file mode 100644
index 2de3661..0000000
Binary files a/src/resources/d30.png and /dev/null differ
diff --git a/src/resources/e.png b/src/resources/e.png
deleted file mode 100644
index 4fe84b3..0000000
Binary files a/src/resources/e.png and /dev/null differ
diff --git a/src/resources/e30.png b/src/resources/e30.png
deleted file mode 100644
index 879d731..0000000
Binary files a/src/resources/e30.png and /dev/null differ
diff --git a/src/resources/menu.png b/src/resources/menu.png
deleted file mode 100644
index 771f175..0000000
Binary files a/src/resources/menu.png and /dev/null differ
diff --git a/src/resources/menu1.png b/src/resources/menu1.png
deleted file mode 100644
index 89a5c96..0000000
Binary files a/src/resources/menu1.png and /dev/null differ
diff --git a/src/resources/menu2.png b/src/resources/menu2.png
deleted file mode 100644
index cf23059..0000000
Binary files a/src/resources/menu2.png and /dev/null differ
diff --git a/src/resources/profile.jpg b/src/resources/profile.jpg
deleted file mode 100644
index 8474538..0000000
Binary files a/src/resources/profile.jpg and /dev/null differ
diff --git a/src/test/java/dev/masterflomaster1/jfxc/crypto/AsymmetricCipherImplTest.java b/src/test/java/dev/masterflomaster1/jfxc/crypto/AsymmetricCipherImplTest.java
new file mode 100644
index 0000000..c39bcc2
--- /dev/null
+++ b/src/test/java/dev/masterflomaster1/jfxc/crypto/AsymmetricCipherImplTest.java
@@ -0,0 +1,121 @@
+package dev.masterflomaster1.jfxc.crypto;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
+import java.util.HexFormat;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class AsymmetricCipherImplTest {
+
+ @BeforeAll
+ static void beforeAll() {
+ SecurityUtils.init();
+ }
+
+ @Test
+ void shouldEncryptAndDecryptUsingRSA() {
+ var keyPair = AsymmetricCipherImpl.generateKeyPair("RSA");
+
+ System.out.println(HexFormat.of().formatHex(keyPair.getPublic().getEncoded()));
+ System.out.println(keyPair.getPublic().getFormat());
+ System.out.println(HexFormat.of().formatHex(keyPair.getPrivate().getEncoded()));
+ System.out.println(keyPair.getPrivate().getFormat());
+
+ var payload = "Hello World!".getBytes(StandardCharsets.UTF_8);
+
+ var enc = AsymmetricCipherImpl.encrypt("RSA", payload, keyPair.getPublic());
+ var dec = AsymmetricCipherImpl.decrypt("RSA", enc, keyPair.getPrivate());
+
+ System.out.println(HexFormat.of().formatHex(enc));
+ System.out.println(new String(dec));
+
+ assertArrayEquals(payload, dec);
+ }
+
+ @Test
+ void shouldEncryptAndDecryptUsingVariousAlgorithms() {
+ var payload = "Hello World!".getBytes(StandardCharsets.UTF_8);
+
+ SecurityUtils.getAsymmetricCiphers().forEach(cipher -> {
+ var keyPair = AsymmetricCipherImpl.generateKeyPair(AsymmetricCipherImpl.getProperKeyGenAlgorithm(cipher));
+ var enc = AsymmetricCipherImpl.encrypt(cipher, payload, keyPair.getPublic());
+ var dec = AsymmetricCipherImpl.decrypt(cipher, enc, keyPair.getPrivate());
+
+ assertArrayEquals(payload, dec);
+ });
+ }
+
+ @Test
+ void shouldEncryptAndDecryptUsingVariousAlgorithmsAndKeyOptions() {
+ var payload = "Hello World!".getBytes(StandardCharsets.UTF_8);
+
+ SecurityUtils.getAsymmetricCiphers().forEach(cipher -> {
+
+ AsymmetricCipherImpl.getAvailableKeyOptions(cipher).forEach(option -> {
+ System.out.println(cipher + " with " + option + " key option");
+
+ var keyPair = AsymmetricCipherImpl.generateKeyPair(AsymmetricCipherImpl.getProperKeyGenAlgorithm(cipher), option);
+ var enc = AsymmetricCipherImpl.encrypt(cipher, payload, keyPair.getPublic());
+ var dec = AsymmetricCipherImpl.decrypt(cipher, enc, keyPair.getPrivate());
+
+ assertArrayEquals(payload, dec);
+ });
+ });
+ }
+
+ @Test
+ void shouldEncryptAndDecryptHybridUsingVariousAlgorithms() {
+ var payload = "Hello World!".getBytes(StandardCharsets.UTF_8);
+
+ SecurityUtils.getHybridAsymmetricCiphers().forEach(cipher -> {
+ var keyPair = AsymmetricCipherImpl.generateKeyPair(AsymmetricCipherImpl.getProperKeyGenAlgorithm(cipher));
+
+ byte[] derivation = new byte[16];
+ byte[] encoding = new byte[16];
+ byte[] nonce = new byte[AsymmetricCipherImpl.getProperNonceLength(cipher)];
+
+ SecureRandom random = new SecureRandom();
+
+ random.nextBytes(derivation);
+ random.nextBytes(encoding);
+ random.nextBytes(nonce);
+
+ var enc = AsymmetricCipherImpl.encrypt(cipher, payload, keyPair.getPublic(), derivation, encoding, nonce);
+ var dec = AsymmetricCipherImpl.decrypt(cipher, enc, keyPair.getPrivate(), derivation, encoding, nonce);
+
+ assertArrayEquals(payload, dec);
+ });
+ }
+
+ @Test
+ void shouldEncryptAndDecryptHybridUsingVariousAlgorithmsAndOptions() {
+ var payload = "Hello World!".getBytes(StandardCharsets.UTF_8);
+
+ SecurityUtils.getHybridAsymmetricCiphers().forEach(cipher -> {
+ AsymmetricCipherImpl.getAvailableKeyOptions(cipher).forEach(option -> {
+ System.out.println(cipher + " with " + option + " key option");
+ var keyPair = AsymmetricCipherImpl.generateKeyPair(AsymmetricCipherImpl.getProperKeyGenAlgorithm(cipher), option);
+
+ byte[] derivation = new byte[16];
+ byte[] encoding = new byte[16];
+ byte[] nonce = new byte[AsymmetricCipherImpl.getProperNonceLength(cipher)];
+
+ SecureRandom random = new SecureRandom();
+
+ random.nextBytes(derivation);
+ random.nextBytes(encoding);
+ random.nextBytes(nonce);
+
+ var enc = AsymmetricCipherImpl.encrypt(cipher, payload, keyPair.getPublic(), derivation, encoding, nonce);
+ var dec = AsymmetricCipherImpl.decrypt(cipher, enc, keyPair.getPrivate(), derivation, encoding, nonce);
+
+ assertArrayEquals(payload, dec);
+ });
+ });
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/dev/masterflomaster1/jfxc/crypto/BlockCipherImplTest.java b/src/test/java/dev/masterflomaster1/jfxc/crypto/BlockCipherImplTest.java
new file mode 100644
index 0000000..4ed56f7
--- /dev/null
+++ b/src/test/java/dev/masterflomaster1/jfxc/crypto/BlockCipherImplTest.java
@@ -0,0 +1,186 @@
+package dev.masterflomaster1.jfxc.crypto;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.util.HexFormat;
+import java.util.concurrent.ExecutionException;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assumptions.*;
+
+class BlockCipherImplTest {
+
+ @BeforeAll
+ static void beforeAll() {
+ SecurityUtils.init();
+ }
+
+ @Test
+ void shouldGenerateKeysForAllAlgorithms() {
+ SecurityUtils.getBlockCiphers().forEach(cipher -> {
+ var list = BlockCipherImpl.getAvailableKeyLengths(cipher);
+
+ list.forEach(len -> {
+ var key = BlockCipherImpl.generateKey(cipher, len);
+ System.out.printf("%s key (%d): %s\n", cipher, key.length*8, HexFormat.of().formatHex(key));
+ });
+ });
+ }
+
+ @Test
+ void shouldGeneratePasswordBasedKeys() {
+ char[] pwd = "test_secret_password".toCharArray();
+
+ SecurityUtils.getBlockCiphers().forEach(cipher -> {
+ var list = BlockCipherImpl.getAvailableKeyLengths(cipher);
+
+ list.forEach(len -> {
+ var key = BlockCipherImpl.generatePasswordBasedKey(pwd, len);
+ System.out.printf("%s key (%d): %s\n", cipher, key.length*8, HexFormat.of().formatHex(key));
+ });
+ });
+ }
+
+ @Test
+ void shouldEncryptAndDecryptWithAllModes() {
+ char[] pwd = "test_secret_password".toCharArray();
+ byte[] data = "Payload".getBytes(StandardCharsets.UTF_8);
+
+ SecurityUtils.getBlockCiphers().forEach(cipher -> {
+ var lengths = BlockCipherImpl.getAvailableKeyLengths(cipher);
+
+ lengths.forEach(len -> {
+ var key = BlockCipherImpl.generatePasswordBasedKey(pwd, len);
+
+ for (var mode: BlockCipherImpl.Mode.values()) {
+ System.out.printf("%s key (%d): %s\n", cipher, key.length*8, mode);
+ byte[] a, b;
+
+ if (mode == BlockCipherImpl.Mode.ECB) {
+ a = BlockCipherImpl.encrypt(cipher, mode, BlockCipherImpl.Padding.PKCS5Padding, null, data, key);
+ b = BlockCipherImpl.decrypt(cipher, mode, BlockCipherImpl.Padding.PKCS5Padding, null, a, key);
+ } else {
+ var iv = BlockCipherImpl.generateIV(cipher);
+ System.out.printf("IV: %s\n".formatted(HexFormat.of().formatHex(iv)));
+ a = BlockCipherImpl.encrypt(cipher, mode, BlockCipherImpl.Padding.PKCS5Padding, iv, data, key);
+ b = BlockCipherImpl.decrypt(cipher, mode, BlockCipherImpl.Padding.PKCS5Padding, iv, a, key);
+ }
+
+ assertArrayEquals(data, b);
+ }
+ });
+ });
+ }
+
+ @Test
+ void shouldEncryptAndDecryptWithAllPaddings() {
+ char[] pwd = "test_secret_password".toCharArray();
+ byte[] data = "Payload".getBytes(StandardCharsets.UTF_8);
+
+ SecurityUtils.getBlockCiphers().forEach(cipher -> {
+ var lengths = BlockCipherImpl.getAvailableKeyLengths(cipher);
+
+ lengths.forEach(len -> {
+ var key = BlockCipherImpl.generatePasswordBasedKey(pwd, len);
+
+ for (var padding : BlockCipherImpl.Padding.values()) {
+ var a = BlockCipherImpl.encrypt(cipher, BlockCipherImpl.Mode.ECB, padding, null, data, key);
+
+ System.out.printf("%s key (%d) %s: %s\n",
+ cipher,
+ key.length*8,
+ padding.getPadding(),
+ HexFormat.of().formatHex(a)
+ );
+
+ var b = BlockCipherImpl.decrypt(cipher, BlockCipherImpl.Mode.ECB, padding, null, a, key);
+
+ assertArrayEquals(data, b);
+ }
+ });
+ });
+ }
+
+ @Test
+ void shouldEncryptAndDecryptFile() throws IOException, ExecutionException, InterruptedException {
+ Path input = Paths.get(System.getProperty("user.home"), "Desktop", "a.mp4");
+ Path output = Paths.get(System.getProperty("user.home"), "Desktop", "enc");
+ Path decrypted = Paths.get(System.getProperty("user.home"), "Desktop", "result.mp4");
+
+ assumeTrue(Files.exists(input), "Target file does not exist");
+ Files.write(output, new byte[0], StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
+ Files.write(decrypted, new byte[0], StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
+
+ var key = BlockCipherImpl.generatePasswordBasedKey(new char[] {'c', 'o', 'd', 'e'}, 128);
+
+ BlockCipherImpl.encrypt(
+ input.toAbsolutePath().toString(),
+ output.toAbsolutePath().toString(),
+ "AES",
+ BlockCipherImpl.Mode.ECB,
+ BlockCipherImpl.Padding.PKCS7Padding,
+ new byte[] {},
+ key
+ );
+
+ BlockCipherImpl.decrypt(
+ output.toAbsolutePath().toString(),
+ decrypted.toAbsolutePath().toString(),
+ "AES",
+ BlockCipherImpl.Mode.ECB,
+ BlockCipherImpl.Padding.PKCS7Padding,
+ new byte[] {},
+ key
+ );
+
+ var h1 = UnkeyedCryptoHash.asyncHash("SHA-256", input.toAbsolutePath().toString()).get();
+ var h2 = UnkeyedCryptoHash.asyncHash("SHA-256", decrypted.toAbsolutePath().toString()).get();
+ assertArrayEquals(h1, h2);
+ }
+
+ @Test
+ void shouldEncryptAndDecryptFileUsingNio() throws IOException, InterruptedException, ExecutionException {
+ Path input = Paths.get(System.getProperty("user.home"), "Desktop", "a.mp4");
+ Path output = Paths.get(System.getProperty("user.home"), "Desktop", "enc");
+ Path decrypted = Paths.get(System.getProperty("user.home"), "Desktop", "result.mp4");
+
+ assumeTrue(Files.exists(input), "Target file does not exist");
+ Files.write(output, new byte[0], StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
+ Files.write(decrypted, new byte[0], StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
+
+ var key = BlockCipherImpl.generatePasswordBasedKey(new char[] {'c', 'o', 'd', 'e'}, 128);
+
+ BlockCipherImpl.nioEncrypt(
+ input.toAbsolutePath().toString(),
+ output.toAbsolutePath().toString(),
+ "AES",
+ BlockCipherImpl.Mode.ECB,
+ BlockCipherImpl.Padding.PKCS7Padding,
+ new byte[] {},
+ key
+ );
+
+ BlockCipherImpl.nioDecrypt(
+ output.toAbsolutePath().toString(),
+ decrypted.toAbsolutePath().toString(),
+ "AES",
+ BlockCipherImpl.Mode.ECB,
+ BlockCipherImpl.Padding.PKCS7Padding,
+ new byte[] {},
+ key
+ );
+
+ var h1 = UnkeyedCryptoHash.asyncHash("SHA-256", input.toAbsolutePath().toString()).get();
+ var h2 = UnkeyedCryptoHash.asyncHash("SHA-256", decrypted.toAbsolutePath().toString()).get();
+ assertArrayEquals(h1, h2);
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/dev/masterflomaster1/jfxc/crypto/PbeImplTest.java b/src/test/java/dev/masterflomaster1/jfxc/crypto/PbeImplTest.java
new file mode 100644
index 0000000..8a5f240
--- /dev/null
+++ b/src/test/java/dev/masterflomaster1/jfxc/crypto/PbeImplTest.java
@@ -0,0 +1,35 @@
+package dev.masterflomaster1.jfxc.crypto;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import java.util.HexFormat;
+import java.util.concurrent.ExecutionException;
+
+class PbeImplTest {
+
+ @BeforeAll
+ static void beforeAll() {
+ SecurityUtils.init();
+ }
+
+ @Test
+ void shouldGeneratePbkdf2WithAllAlgorithmsDefaultSettings() {
+ var iter = 10000;
+ var len = 128;
+ char[] password = "testPassword".toCharArray();
+
+ SecurityUtils.getPbkdfs().forEach(alg -> {
+ var future = PbeImpl.asyncHash(alg, password, SecurityUtils.generateSalt(), iter, len);
+
+ try {
+ byte[] b = future.get();
+
+ System.out.println(alg + ": " + HexFormat.of().formatHex(b));
+ } catch (InterruptedException | ExecutionException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/dev/masterflomaster1/jfxc/crypto/SecurityUtilsTest.java b/src/test/java/dev/masterflomaster1/jfxc/crypto/SecurityUtilsTest.java
new file mode 100644
index 0000000..da97b01
--- /dev/null
+++ b/src/test/java/dev/masterflomaster1/jfxc/crypto/SecurityUtilsTest.java
@@ -0,0 +1,21 @@
+package dev.masterflomaster1.jfxc.crypto;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+class SecurityUtilsTest {
+
+ @BeforeAll
+ static void beforeAll() {
+ SecurityUtils.init();
+ }
+
+ @Test
+ void shouldAllPrintSupportedAlgorithms() {
+ System.out.println("Block ciphers: " + SecurityUtils.getBlockCiphers().size());
+ System.out.println("Digests: " + SecurityUtils.getDigests().size());
+ System.out.println("Hmacs: " + SecurityUtils.getHmacs().size());
+ System.out.println("Pbkdfs: " + SecurityUtils.getPbkdfs().size());
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/dev/masterflomaster1/jfxc/crypto/StreamCipherImplTest.java b/src/test/java/dev/masterflomaster1/jfxc/crypto/StreamCipherImplTest.java
new file mode 100644
index 0000000..f5ef8a0
--- /dev/null
+++ b/src/test/java/dev/masterflomaster1/jfxc/crypto/StreamCipherImplTest.java
@@ -0,0 +1,98 @@
+package dev.masterflomaster1.jfxc.crypto;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.util.Base64;
+import java.util.concurrent.ExecutionException;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+class StreamCipherImplTest {
+
+ @BeforeAll
+ static void beforeAll() {
+ SecurityUtils.init();
+ }
+
+ @Test
+ void shouldEncryptAndDecryptUsingAllAlgorithmsAndIvs() {
+ char[] pwd = "test_secret_password".toCharArray();
+ byte[] data = "Payload".getBytes(StandardCharsets.UTF_8);
+ byte[] salt = Base64.getDecoder().decode("4WHuOVNv8nIwjrPhLpyPwA==");
+
+ SecurityUtils.getStreamCiphers().forEach(cipher -> {
+ System.out.println(cipher);
+
+ var len = StreamCipherImpl.getCorrespondingKeyLengths(cipher).get(0);
+ byte[] pass = SecurityUtils.generatePasswordBasedKey(pwd, len, salt);
+
+ var ivLenOptional = StreamCipherImpl.getCorrespondingIvLengthBits(cipher);
+ if (ivLenOptional.isPresent()) {
+ ivLenOptional.get().forEach(ivLen -> {
+ System.out.println(ivLen);
+ byte[] iv = SecurityUtils.generateIV(ivLen);
+
+ var encrypted = StreamCipherImpl.encrypt(cipher, iv, data, pass);
+ var decrypted = StreamCipherImpl.decrypt(cipher, iv, encrypted, pass);
+
+ assertArrayEquals(data, decrypted);
+ });
+
+ return;
+ }
+
+ var encrypted = StreamCipherImpl.encrypt(cipher, new byte[] {}, data, pass);
+ var decrypted = StreamCipherImpl.decrypt(cipher, new byte[] {}, encrypted, pass);
+
+ assertArrayEquals(data, decrypted);
+ });
+ }
+
+ @Test
+ void shouldEncryptAndDecryptFileUsingNio() throws IOException, InterruptedException, ExecutionException {
+ Path input = Paths.get(System.getProperty("user.home"), "Desktop", "a.mp4");
+ Path output = Paths.get(System.getProperty("user.home"), "Desktop", "enc");
+ Path decrypted = Paths.get(System.getProperty("user.home"), "Desktop", "result.mp4");
+
+ assumeTrue(Files.exists(input), "Target file does not exist");
+ Files.write(output, new byte[0], StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
+ Files.write(decrypted, new byte[0], StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
+
+ String algo = "SALSA20";
+ var len = StreamCipherImpl.getCorrespondingIvLengthBits(algo).get().get(0);
+ var iv = SecurityUtils.generateIV(len);
+ char[] pwd = "test_secret_password".toCharArray();
+ byte[] salt = Base64.getDecoder().decode("4WHuOVNv8nIwjrPhLpyPwA==");
+ byte[] pass = SecurityUtils.generatePasswordBasedKey(pwd, 128, salt);
+
+ StreamCipherImpl.nioEncrypt(
+ input,
+ output,
+ algo,
+ iv,
+ pass
+ );
+
+ StreamCipherImpl.nioDecrypt(
+ output,
+ decrypted,
+ algo,
+ iv,
+ pass
+ );
+
+ var h1 = UnkeyedCryptoHash.asyncHash("SHA-256", input.toAbsolutePath().toString()).get();
+ var h2 = UnkeyedCryptoHash.asyncHash("SHA-256", decrypted.toAbsolutePath().toString()).get();
+ assertArrayEquals(h1, h2);
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/dev/masterflomaster1/jfxc/crypto/classic/ADFGVXImplTest.java b/src/test/java/dev/masterflomaster1/jfxc/crypto/classic/ADFGVXImplTest.java
new file mode 100644
index 0000000..7b795a0
--- /dev/null
+++ b/src/test/java/dev/masterflomaster1/jfxc/crypto/classic/ADFGVXImplTest.java
@@ -0,0 +1,51 @@
+package dev.masterflomaster1.jfxc.crypto.classic;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class ADFGVXImplTest {
+
+ private final String text = "Munitionierung beschleunigen Punkt Soweit nicht eingesehen auch bei Tag";
+ private final String code =
+ "FFGFDFFAGDAFDFGDGGDFAGDDGDFGDAGDGDDFAGDAADGDAFFGFAGDDAFAAAAAADAFFFDVFAVFXFFVGDDFVDFDVDVVVAFDFAADDFFXDDADVDADFVAVFDFDFAADDFDVDA";
+ private final String key = "CODE";
+
+ @Test
+ @DisplayName("Should encrypt message with key")
+ void shouldEncryptADFGVX() {
+ var a = ADFGVXImpl.encrypt(text, key);
+ String expected = "FFGFDFFAGDAFDFGDGGDFAGDDGDFGDAGDGDDFAGDAADGDAFFGFAGDDAFAAAAAADAFFFDVFAVFXFFVGDDFVDFDVDVVVAFDFAADDFFXDDADVDADFVAVFDFDFAADDFDVDA";
+
+ assertEquals(expected, a);
+ }
+
+ @Test
+ @DisplayName("Should decrypt message with key")
+ void shouldDecryptADFGVX() {
+ var a = ADFGVXImpl.decrypt(code, key);
+ String expected = text.replace(" ", "").toUpperCase();
+
+ System.out.println(a);
+
+ assertEquals(expected, a);
+ }
+
+ @Test
+ void shouldEncryptAndDecryptADFGVX1() {
+ var a = ADFGVXImpl.encrypt("Test", key);
+ var b = ADFGVXImpl.decrypt(a, key);
+
+ assertEquals("TEST", b);
+ }
+
+ @Test
+ void shouldEncryptAndDecryptADFGVX2() {
+ var a = ADFGVXImpl.encrypt("Attack at once", key);
+ var b = ADFGVXImpl.decrypt(a, key);
+
+ assertEquals("Attack at once".toUpperCase().replace(" ", ""), b);
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/dev/masterflomaster1/jfxc/crypto/classic/AffineCipherImplTest.java b/src/test/java/dev/masterflomaster1/jfxc/crypto/classic/AffineCipherImplTest.java
new file mode 100644
index 0000000..7e3c8ce
--- /dev/null
+++ b/src/test/java/dev/masterflomaster1/jfxc/crypto/classic/AffineCipherImplTest.java
@@ -0,0 +1,35 @@
+package dev.masterflomaster1.jfxc.crypto.classic;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class AffineCipherImplTest {
+
+ private final String text = "The quick brown fox jumps over 13 lazy dogs.";
+
+ @Test
+ void shouldEncryptAndDecryptAffineCipher() {
+ var a = AffineCipherImpl.encrypt(text, 5, 8);
+ var b = AffineCipherImpl.decrypt(a, 5, 8);
+
+ assertEquals(text.toUpperCase(), b);
+ }
+
+ @Test
+ void shouldEncryptAndDecryptAffineCipherWithAllOptions() {
+ for (int a = 1; a <= 25; a+=2) {
+
+ if (a == 13)
+ continue;
+
+ for (int b = 1; b <= 25; b++) {
+ var encrypted = AffineCipherImpl.encrypt(text, a, b);
+ var decrypted = AffineCipherImpl.decrypt(encrypted, a, b);
+
+ assertEquals(text.toUpperCase(), decrypted);
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/dev/masterflomaster1/jfxc/crypto/classic/AtbashCipherImplTest.java b/src/test/java/dev/masterflomaster1/jfxc/crypto/classic/AtbashCipherImplTest.java
new file mode 100644
index 0000000..4abf4f5
--- /dev/null
+++ b/src/test/java/dev/masterflomaster1/jfxc/crypto/classic/AtbashCipherImplTest.java
@@ -0,0 +1,17 @@
+package dev.masterflomaster1.jfxc.crypto.classic;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class AtbashCipherImplTest {
+
+ @Test
+ void shouldEncryptAndDecryptAtbash() {
+ var a = AtbashCipherImpl.encrypt("Hello world!");
+ var b = AtbashCipherImpl.decrypt(a);
+
+ assertEquals("Hello world!".toUpperCase(), b);
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/dev/masterflomaster1/jfxc/crypto/classic/CaesarCipherImplTest.java b/src/test/java/dev/masterflomaster1/jfxc/crypto/classic/CaesarCipherImplTest.java
new file mode 100644
index 0000000..3e2b6e8
--- /dev/null
+++ b/src/test/java/dev/masterflomaster1/jfxc/crypto/classic/CaesarCipherImplTest.java
@@ -0,0 +1,20 @@
+package dev.masterflomaster1.jfxc.crypto.classic;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class CaesarCipherImplTest {
+
+ @Test
+ void shouldEncryptAndDecryptCaesarCipher() {
+ var text = "attacking tonight";
+
+ var a = CaesarCipherImpl.encrypt(text, 5);
+ var b = CaesarCipherImpl.decrypt(a, 5);
+
+ assertEquals(text, b);
+ }
+
+
+}
\ No newline at end of file
diff --git a/src/test/java/dev/masterflomaster1/jfxc/crypto/classic/HillCipherImplTest.java b/src/test/java/dev/masterflomaster1/jfxc/crypto/classic/HillCipherImplTest.java
new file mode 100644
index 0000000..a8b75f3
--- /dev/null
+++ b/src/test/java/dev/masterflomaster1/jfxc/crypto/classic/HillCipherImplTest.java
@@ -0,0 +1,14 @@
+package dev.masterflomaster1.jfxc.crypto.classic;
+
+import org.junit.jupiter.api.Test;
+
+class HillCipherImplTest {
+
+ @Test
+ void shouldEncryptAndDecryptHillCipher() {
+ var a = HillCipherImpl.encrypt("Hello world", new int[][]{{6, 24, 1}, {13, 16, 10}, {20, 17, 15}});
+
+ System.out.println(a);
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/dev/masterflomaster1/jfxc/crypto/classic/PlayfairCipherImplTest.java b/src/test/java/dev/masterflomaster1/jfxc/crypto/classic/PlayfairCipherImplTest.java
new file mode 100644
index 0000000..e4cbbab
--- /dev/null
+++ b/src/test/java/dev/masterflomaster1/jfxc/crypto/classic/PlayfairCipherImplTest.java
@@ -0,0 +1,19 @@
+package dev.masterflomaster1.jfxc.crypto.classic;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class PlayfairCipherImplTest {
+
+ @Test
+ void shouldEncryptAndDecryptPlayfairCipher() {
+ var text = "Attack at dawn";
+
+ var a = PlayfairCipherImpl.encrypt(text, "CODE");
+ var b = PlayfairCipherImpl.decrypt(a, "CODE");
+
+ assertEquals(text.replace(" ", "").toUpperCase(), b);
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/dev/masterflomaster1/jfxc/crypto/classic/VigenereCipherImplTest.java b/src/test/java/dev/masterflomaster1/jfxc/crypto/classic/VigenereCipherImplTest.java
new file mode 100644
index 0000000..792c9c6
--- /dev/null
+++ b/src/test/java/dev/masterflomaster1/jfxc/crypto/classic/VigenereCipherImplTest.java
@@ -0,0 +1,19 @@
+package dev.masterflomaster1.jfxc.crypto.classic;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class VigenereCipherImplTest {
+
+ @Test
+ void shouldEncryptAndDecryptVigenereCipher() {
+ var text = "attacking tonight";
+
+ var a = VigenereCipherImpl.encrypt(text, "OCULORHINOLARINGOLOGY");
+ var b = VigenereCipherImpl.decrypt(a, "OCULORHINOLARINGOLOGY");
+
+ assertEquals(text.toUpperCase(), b);
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/dev/masterflomaster1/jfxc/crypto/enigma/EnigmaTest.java b/src/test/java/dev/masterflomaster1/jfxc/crypto/enigma/EnigmaTest.java
new file mode 100644
index 0000000..3f7e0d3
--- /dev/null
+++ b/src/test/java/dev/masterflomaster1/jfxc/crypto/enigma/EnigmaTest.java
@@ -0,0 +1,19 @@
+package dev.masterflomaster1.jfxc.crypto.enigma;
+
+import dev.masterflomaster1.jfxc.utils.StringUtils;
+import org.junit.jupiter.api.Test;
+
+class EnigmaTest {
+
+ @Test
+ void demo() {
+
+ Enigma enigma = new Enigma(new String[] {"VII", "V", "IV"}, "B", new int[] {10,5,12}, new int[] {1,1,1}, "");
+
+ var a = new String(enigma.encrypt("Hello World"));
+
+ System.out.println(StringUtils.spaceAfterN(a, 5));
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/dev/masterflomaster1/jfxc/crypto/passwords/HaveIBeenPwnedApiClientTest.java b/src/test/java/dev/masterflomaster1/jfxc/crypto/passwords/HaveIBeenPwnedApiClientTest.java
new file mode 100644
index 0000000..1bf29f2
--- /dev/null
+++ b/src/test/java/dev/masterflomaster1/jfxc/crypto/passwords/HaveIBeenPwnedApiClientTest.java
@@ -0,0 +1,33 @@
+package dev.masterflomaster1.jfxc.crypto.passwords;
+
+import dev.masterflomaster1.jfxc.crypto.SecurityUtils;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class HaveIBeenPwnedApiClientTest {
+
+ @BeforeAll
+ static void beforeAll() {
+ SecurityUtils.init();
+ }
+
+ @Test
+ void shouldReturnTrue() throws IOException, InterruptedException {
+ var password = "password1234".getBytes(StandardCharsets.UTF_8);
+
+ assertTrue(HaveIBeenPwnedApiClient.passwordRange(password).isPresent());
+ }
+
+ @Test
+ void shouldReturnEmpty() throws IOException, InterruptedException {
+ var password = "0(D)@#)*(JQ#ADdccLC;a{{Wdd3#)_(JFSA".getBytes(StandardCharsets.UTF_8);
+
+ assertTrue(HaveIBeenPwnedApiClient.passwordRange(password).isEmpty());
+ }
+
+}
\ No newline at end of file