diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..ebadd39 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,116 @@ +name: Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + build: + if: startsWith(github.ref, 'refs/tags/') + strategy: + matrix: + os: [ ubuntu-latest, macos-latest, windows-latest ] + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v3 + + - name: Install dependencies + if: ${{ matrix.os == 'ubuntu-latest' }} + run: sudo apt-get install -y fuse libfuse2 + + - name: Set up JDK 20 + uses: actions/setup-java@v3 + with: + java-version: '20.0.1' + distribution: 'temurin' + + - name: Build + uses: gradle/gradle-build-action@v2 + with: + arguments: build + + - name: Build (jlink) + uses: gradle/gradle-build-action@v2 + with: + arguments: jlink + + - name: Build (jpackage) + uses: gradle/gradle-build-action@v2 + with: + arguments: jpackage + + - name: Publish artifacts + uses: softprops/action-gh-release@v1 + with: + prerelease: false + files: | + build/jpackage/* + env: + GITHUB_TOKEN: ${{ github.token }} + + build-portable: + if: startsWith(github.ref, 'refs/tags/') + strategy: + matrix: + os: [ ubuntu-latest, windows-latest ] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v3 + + - name: Install dependencies + if: ${{ matrix.os == 'ubuntu-latest' }} + run: sudo apt-get install -y fuse libfuse2 + + - name: Set up JDK 20 + uses: actions/setup-java@v3 + with: + java-version: '20.0.1' + distribution: 'temurin' + + - name: Switch portable flag (Linux) + if: ${{ matrix.os == 'ubuntu-latest' }} + run: sed -i 's/final boolean PORTABLE = false/final boolean PORTABLE = true/g' src/main/java/com/codedead/opal/utils/SharedVariables.java + + - name: Switch portable flag (Windows) + if: ${{ matrix.os == 'windows-latest' }} + run: (Get-Content src\main\java\com\codedead\opal\utils\SharedVariables.java).replace('final boolean PORTABLE = false', 'final boolean PORTABLE = true') | Set-Content src\main\java\com\codedead\opal\utils\SharedVariables.java + + - name: Build + uses: gradle/gradle-build-action@v2 + with: + arguments: build + + - name: Build (AppImage) + if: ${{ matrix.os == 'ubuntu-latest' }} + uses: gradle/gradle-build-action@v2 + with: + arguments: AppImage + + - name: Build (jlink) + uses: gradle/gradle-build-action@v2 + with: + arguments: jlink + + - name: Build (jpackage) + uses: gradle/gradle-build-action@v2 + with: + arguments: jpackage + + - name: ZIP artifacts + if: ${{ matrix.os == 'windows-latest' }} + run: Compress-Archive -Path build/jpackage/Opal/* -Destination build/jpackage/Opal-win-portable.zip + + - name: Publish artifacts + uses: softprops/action-gh-release@v1 + with: + prerelease: false + files: | + build/AppImage/*.AppImage + build/jpackage/*.zip + env: + GITHUB_TOKEN: ${{ github.token }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4899fa7..a5653d0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ jobs: build: strategy: matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [ ubuntu-latest, macos-latest, windows-latest ] runs-on: ${{ matrix.os }} steps: @@ -26,7 +26,7 @@ jobs: - name: Set up JDK 20 uses: actions/setup-java@v3 with: - java-version: '20' + java-version: '20.0.1' distribution: 'temurin' - name: Test diff --git a/.gitignore b/.gitignore index f73aa84..92186c0 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ logs/* /default.properties /help.pdf .opal/ +.com.codedead.opal/ diff --git a/README.md b/README.md index 7731671..664e609 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ You can create an executable installer by running the `jpackage` Gradle task on ```shell ./gradlew jpackage ``` -*Do note that you will need the [WiX Toolset](https://wixtoolset.org/) in order to create MSI packages.* +*Do note that you will need the [WiX Toolset](https://wixtoolset.org/) in order to create `msi` packages.* #### Portable image @@ -32,11 +32,11 @@ You can create a portable image by running the `jpackageImage` Gradle task on a #### rpm -You can create an RPM, by running the `jpackage` Gradle task on a Linux host: +You can create an `rpm`, by running the `jpackage` Gradle task on a Linux host: ```shell ./gradlew jpackage ``` -*Do note that you will need the `rpm-build` package in order to create an RPM.* +*Do note that you will need the `rpm-build` package in order to create an `rpm`.* #### AppImage @@ -44,7 +44,7 @@ You can create an [AppImage](https://appimage.github.io/) by executing the `AppI ```shell ./gradlew AppImage ``` -*Do note that running this task will execute a shell script in order to download and run the [appimagetool](https://appimage.github.io/appimagetool/) in order to create the AppImage file.* +*Do note that running this task will execute a shell script in order to download and run the [appimagetool](https://appimage.github.io/appimagetool/) in order to create the `AppImage` file.* #### Portable image @@ -53,6 +53,22 @@ You can create a portable image by running the `jpackageImage` Gradle task on a ./gradlew jpackageImage ``` +### macOS + +#### dmg + +You can create a `dmg`, by running the `jpackage` Gradle task on a macOS host: +```shell +./gradlew jpackage +``` + +#### Portable image + +You can create a portable image by running the `jpackageImage` Gradle task on a macOS host: +```shell +./gradlew jpackageImage +``` + ## Dependencies A couple of dependencies are required in order to build Opal. Some of which require a specific host machine, diff --git a/build.gradle b/build.gradle index 4ab2dc5..074b928 100644 --- a/build.gradle +++ b/build.gradle @@ -6,10 +6,11 @@ plugins { id 'eclipse' id 'application' id 'org.beryx.jlink' version '2.26.0' + id 'org.openjfx.javafxplugin' version '0.0.14' } group 'com.codedead' -version '1.1.0' +version '1.2.0' def currentOS = DefaultNativePlatform.currentOperatingSystem @@ -23,6 +24,12 @@ application { mainClass = 'com.codedead.opal.OpalApplication' } +javafx { + version = '20.0.1' + configuration = 'implementation' + modules = ['javafx.base', 'javafx.controls', 'javafx.fxml', 'javafx.media'] +} + jlink { options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages'] forceMerge('log4j-api', 'jackson') @@ -46,12 +53,16 @@ jlink { installerOptions = [ '--win-menu', '--win-menu-group', 'CodeDead', - '--win-shortcut', + '--win-shortcut-prompt', + '--win-upgrade-uuid', '876c5464-9a66-4913-89a4-c63a4b8b4bb9', + '--win-help-url', 'https://codedead.com/contact', '--win-dir-chooser', '--copyright', 'Copyright (c) 2023 CodeDead', '--description', 'Opal is a free and open-source JavaFX application that can play relaxing music in the background', '--vendor', 'CodeDead', - '--license-file', 'LICENSE' + '--license-file', 'LICENSE', + '--app-version', "${project.version.toString()}", + '--about-url', 'https://codedead.com' ] } } else if (currentOS.isLinux()) { @@ -65,7 +76,25 @@ jlink { '--copyright', 'Copyright (c) 2023 CodeDead', '--description', 'Opal is a free and open-source JavaFX application that can play relaxing music in the background', '--vendor', 'CodeDead', - '--license-file', 'LICENSE' + '--license-file', 'LICENSE', + '--app-version', "${project.version.toString()}", + '--about-url', 'https://codedead.com' + ] + } + } else if (currentOS.isMacOsX()) { + jpackage { + installerType = 'dmg' + icon = "${project.rootDir}/src/main/resources/images/opal.png" + installerOptions = [ + '--mac-package-name', 'Opal', + '--mac-package-identifier', 'com.codedead.opal', + '--mac-app-category', 'public.app-category.music', + '--copyright', 'Copyright (c) 2023 CodeDead', + '--description', 'Opal is a free and open-source JavaFX application that can play relaxing music in the background', + '--vendor', 'CodeDead', + '--license-file', 'LICENSE', + '--app-version', "${project.version.toString()}", + '--about-url', 'https://codedead.com' ] } } @@ -98,38 +127,26 @@ configure(AppImage) { description = 'Create an AppImage after creating the image of the application' } +def homeConfig = System.properties['user.home'] + '/.config/com.codedead.opal' clean.doFirst { delete 'default.properties' delete 'license.pdf' delete 'help.pdf' delete 'logs' - delete '.opal' + delete '.com.codedead.opal' + delete "$homeConfig" } repositories { mavenCentral() } -def platform -if (currentOS.isWindows()) { - platform = 'win' -} else if (currentOS.isLinux()) { - platform = 'linux' -} else if (currentOS.isMacOsX()) { - platform = 'mac' -} - dependencies { - implementation "org.openjfx:javafx-base:20:${platform}" - implementation "org.openjfx:javafx-controls:20:${platform}" - implementation "org.openjfx:javafx-graphics:20:${platform}" - implementation "org.openjfx:javafx-fxml:20:${platform}" - implementation "org.openjfx:javafx-media:20:${platform}" implementation 'org.apache.logging.log4j:log4j-core:2.20.0' - implementation 'io.github.mkpaz:atlantafx-base:1.2.0' - implementation 'com.fasterxml.jackson.core:jackson-databind:2.14.2' - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.2' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.2' + implementation 'io.github.mkpaz:atlantafx-base:2.0.1' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.3' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.3' } tasks.named('test') { diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index c1962a7..033e24c 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 05905b6..62f495d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions-snapshots/gradle-8.1-20230330100852+0000-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index aeb74cb..fcb6fca 100755 --- a/gradlew +++ b/gradlew @@ -130,10 +130,13 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. diff --git a/src/main/java/com/codedead/opal/OpalApplication.java b/src/main/java/com/codedead/opal/OpalApplication.java index 95833e4..4fa3be3 100644 --- a/src/main/java/com/codedead/opal/OpalApplication.java +++ b/src/main/java/com/codedead/opal/OpalApplication.java @@ -1,9 +1,6 @@ package com.codedead.opal; -import atlantafx.base.theme.NordDark; -import atlantafx.base.theme.NordLight; -import atlantafx.base.theme.PrimerDark; -import atlantafx.base.theme.PrimerLight; +import com.codedead.opal.controller.ThemeController; import com.codedead.opal.controller.UpdateController; import com.codedead.opal.utils.FxUtils; import com.codedead.opal.utils.SharedVariables; @@ -30,7 +27,7 @@ public class OpalApplication extends Application { - private static final Logger logger = LogManager.getLogger(OpalApplication.class); + private static Logger logger; /** * Initialize the application @@ -38,6 +35,9 @@ public class OpalApplication extends Application { * @param args The application arguments */ public static void main(final String[] args) { + System.setProperty("logBasePath", SharedVariables.PROPERTIES_BASE_PATH); + logger = LogManager.getLogger(OpalApplication.class); + Level logLevel = Level.ERROR; try (final FileInputStream fis = new FileInputStream(SharedVariables.PROPERTIES_FILE_LOCATION)) { final Properties prop = new Properties(); @@ -74,8 +74,8 @@ public void start(final Stage primaryStage) { try { settingsController = new SettingsController(SharedVariables.PROPERTIES_FILE_LOCATION, SharedVariables.PROPERTIES_RESOURCE_LOCATION); } catch (final IOException ex) { - logger.error("Unable to initialize the SettingsController", ex); - FxUtils.showErrorAlert("Exception occurred", ex.getMessage(), getClass().getResourceAsStream(SharedVariables.ICON_URL)); + logger.fatal("Unable to initialize the SettingsController", ex); + FxUtils.showErrorAlert("Exception occurred", ex.toString(), getClass().getResourceAsStream(SharedVariables.ICON_URL)); Platform.exit(); return; } @@ -83,13 +83,7 @@ public void start(final Stage primaryStage) { final Properties properties = settingsController.getProperties(); final String languageTag = properties.getProperty("locale", DEFAULT_LOCALE); - final String theme = properties.getProperty("theme", "light"); - switch (theme.toLowerCase()) { - case "nordlight" -> Application.setUserAgentStylesheet(new NordLight().getUserAgentStylesheet()); - case "norddark" -> Application.setUserAgentStylesheet(new NordDark().getUserAgentStylesheet()); - case "dark" -> Application.setUserAgentStylesheet(new PrimerDark().getUserAgentStylesheet()); - default -> Application.setUserAgentStylesheet(new PrimerLight().getUserAgentStylesheet()); - } + ThemeController.setTheme(properties.getProperty("theme", "light")); final Locale locale = Locale.forLanguageTag(languageTag); final ResourceBundle translationBundle = ResourceBundle.getBundle("translations.OpalApplication", locale); @@ -99,7 +93,7 @@ public void start(final Stage primaryStage) { try { root = loader.load(); } catch (final IOException ex) { - logger.error("Unable to load FXML for MainWindow", ex); + logger.fatal("Unable to load FXML for MainWindow", ex); Platform.exit(); return; } @@ -119,15 +113,5 @@ public void start(final Stage primaryStage) { logger.info("Showing the MainWindow"); primaryStage.show(); - - // Load tray icons after displaying the main stage to display the proper icon in the task bar / activities bar (linux) - if (Boolean.parseBoolean(properties.getProperty("trayIcon", "false"))) { - try { - mainWindowController.showTrayIcon(); - } catch (final IOException ex) { - logger.error("Unable to create tray icon", ex); - FxUtils.showErrorAlert(translationBundle.getString("TrayIconError"), ex.getMessage(), getClass().getResourceAsStream(SharedVariables.ICON_URL)); - } - } } } diff --git a/src/main/java/com/codedead/opal/controller/AboutWindowController.java b/src/main/java/com/codedead/opal/controller/AboutWindowController.java index 8dd6083..83e702b 100644 --- a/src/main/java/com/codedead/opal/controller/AboutWindowController.java +++ b/src/main/java/com/codedead/opal/controller/AboutWindowController.java @@ -40,6 +40,8 @@ public void setResourceBundle(final ResourceBundle resourceBundle) { /** * Method that is called when the close button is selected + * + * @param event The {@link ActionEvent} object */ @FXML private void closeAction(final ActionEvent event) { @@ -60,6 +62,6 @@ private void licenseAction() { */ @FXML private void codeDeadAction() { - helpUtils.openCodeDeadWebSite(translationBundle); + helpUtils.openWebsite("https://codedead.com", translationBundle); } } diff --git a/src/main/java/com/codedead/opal/controller/LanguageController.java b/src/main/java/com/codedead/opal/controller/LanguageController.java new file mode 100644 index 0000000..1bc7d94 --- /dev/null +++ b/src/main/java/com/codedead/opal/controller/LanguageController.java @@ -0,0 +1,53 @@ +package com.codedead.opal.controller; + +import static com.codedead.opal.utils.SharedVariables.DEFAULT_LOCALE; + +public final class LanguageController { + + /** + * Initialize a new LanguageController + */ + private LanguageController() { + // Default constructor + } + + /** + * Get the language index from the locale + * + * @param locale The locale + * @return The language index + */ + public static int getLanguageIndexFromLocale(final String locale) { + return switch (locale.toLowerCase()) { + case "de-de" -> 1; + case "es-es" -> 2; + case "fr-fr" -> 3; + case "jp-jp" -> 4; + case "nl-nl" -> 5; + case "ru-ru" -> 6; + case "tr-tr" -> 7; + case "zh-cn" -> 8; + default -> 0; + }; + } + + /** + * Get the locale from the language index + * + * @param index The language index + * @return The locale + */ + public static String getLocaleFromLanguageIndex(final int index) { + return switch (index) { + case 1 -> "de-DE"; + case 2 -> "es-ES"; + case 3 -> "fr-FR"; + case 4 -> "jp-JP"; + case 5 -> "nl-NL"; + case 6 -> "ru-RU"; + case 7 -> "tr-TR"; + case 8 -> "zh-CN"; + default -> DEFAULT_LOCALE; + }; + } +} diff --git a/src/main/java/com/codedead/opal/controller/MainWindowController.java b/src/main/java/com/codedead/opal/controller/MainWindowController.java index 6bfb6ae..491fba4 100644 --- a/src/main/java/com/codedead/opal/controller/MainWindowController.java +++ b/src/main/java/com/codedead/opal/controller/MainWindowController.java @@ -3,6 +3,7 @@ import com.codedead.opal.domain.*; import com.codedead.opal.interfaces.IAudioTimer; import com.codedead.opal.interfaces.IRunnableHelper; +import com.codedead.opal.interfaces.TrayIconListener; import com.codedead.opal.utils.*; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; @@ -23,27 +24,24 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import javax.imageio.ImageIO; -import java.awt.*; -import java.awt.image.BufferedImage; +import java.io.BufferedReader; import java.io.File; import java.io.IOException; +import java.io.InputStreamReader; import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; -import java.util.List; import static com.codedead.opal.utils.SharedVariables.DEFAULT_LOCALE; -public final class MainWindowController implements IAudioTimer { +public final class MainWindowController implements IAudioTimer, TrayIconListener { @FXML private GridPane grpControls; @FXML private CheckMenuItem mniTimerEnabled; - - private TrayIcon trayIcon; + private TrayIconController trayIconController; private SettingsController settingsController; private UpdateController updateController; private ResourceBundle translationBundle; @@ -107,19 +105,33 @@ public void setControllers(final SettingsController settingsController, final Up final boolean mediaButtons = Boolean.parseBoolean(properties.getProperty("mediaButtons", "true")); + trayIconController = new TrayIconController(translationBundle, this); + + // Load tray icons after displaying the main stage to display the proper icon in the task bar / activities bar (linux) + if (Boolean.parseBoolean(properties.getProperty("trayIcon", "false"))) { + try { + trayIconController.showTrayIcon(); + } catch (final IOException ex) { + logger.error("Unable to create tray icon", ex); + FxUtils.showErrorAlert(translationBundle.getString("TrayIconError"), ex.toString(), getClass().getResourceAsStream(SharedVariables.ICON_URL)); + } + } + if (!mediaButtons) { loadMediaButtonVisibility(false); } if (shouldUpdate) { - checkForUpdates(false); + checkForUpdates(false, false); } + + setAudioBalance(Double.parseDouble(properties.getProperty("audioBalance", "0.0"))); } /** * Load the media button visibility for all {@link SoundPane} objects * - * @param visible True if the media button should be visible, otherwise false + * @param visible True if the media buttons should be visible, otherwise false */ public void loadMediaButtonVisibility(final boolean visible) { getAllSoundPanes(grpControls).forEach(s -> s.setMediaButton(visible)); @@ -129,8 +141,9 @@ public void loadMediaButtonVisibility(final boolean visible) { * Check for application updates * * @param showNoUpdates Show an {@link Alert} object when no updates are available + * @param showErrors Show an {@link Alert} object when an error occurs */ - private void checkForUpdates(final boolean showNoUpdates) { + private void checkForUpdates(final boolean showNoUpdates, final boolean showErrors) { logger.info("Attempting to check for updates"); try { @@ -163,6 +176,7 @@ private void checkForUpdates(final boolean showNoUpdates) { updateController.downloadFile(update.getDownloadUrl(), filePath); openFile(filePath); + exitAction(); } } } else { @@ -171,13 +185,14 @@ private void checkForUpdates(final boolean showNoUpdates) { FxUtils.showInformationAlert(translationBundle.getString("NoUpdateAvailable"), null); } } - } catch (final InterruptedException ex) { - logger.error("Unable to check for updates", ex); - FxUtils.showErrorAlert(translationBundle.getString("UpdateError"), ex.getMessage(), getClass().getResourceAsStream(SharedVariables.ICON_URL)); - Thread.currentThread().interrupt(); - } catch (final IOException | InvalidHttpResponseCodeException | URISyntaxException ex) { + } catch (final InterruptedException | IOException | InvalidHttpResponseCodeException | URISyntaxException ex) { + if (ex instanceof InterruptedException) + Thread.currentThread().interrupt(); + logger.error("Unable to check for updates", ex); - FxUtils.showErrorAlert(translationBundle.getString("UpdateError"), ex.getMessage(), getClass().getResourceAsStream(SharedVariables.ICON_URL)); + if (showErrors) { + FxUtils.showErrorAlert(translationBundle.getString("UpdateError"), ex.toString(), getClass().getResourceAsStream(SharedVariables.ICON_URL)); + } } } @@ -225,14 +240,14 @@ public void exceptionOccurred(final Exception ex) { @Override public void run() { logger.error("Error opening the file", ex); - FxUtils.showErrorAlert(translationBundle.getString("FileExecutionError"), ex.getMessage(), getClass().getResourceAsStream(SharedVariables.ICON_URL)); + FxUtils.showErrorAlert(translationBundle.getString("FileExecutionError"), ex.toString(), getClass().getResourceAsStream(SharedVariables.ICON_URL)); } }); } })); } catch (final IOException ex) { logger.error("Error opening the file", ex); - FxUtils.showErrorAlert(translationBundle.getString("FileExecutionError"), ex.getMessage(), getClass().getResourceAsStream(SharedVariables.ICON_URL)); + FxUtils.showErrorAlert(translationBundle.getString("FileExecutionError"), ex.toString(), getClass().getResourceAsStream(SharedVariables.ICON_URL)); } } @@ -241,8 +256,7 @@ public void run() { */ @FXML private void initialize() { - mniTimerEnabled.setOnAction(e -> - { + mniTimerEnabled.setOnAction(e -> { if (mniTimerEnabled.isSelected()) { final Properties properties = settingsController.getProperties(); final long timerDelay = Long.parseLong(properties.getProperty("timerDelay", "3600000")); @@ -254,96 +268,6 @@ private void initialize() { }); } - /** - * Create a tray icon - * - * @throws IOException When the {@link TrayIcon} could not be created - */ - private void createTrayIcon() throws IOException { - logger.info("Creating tray icon"); - if (!SystemTray.isSupported()) { - logger.warn("SystemTray is not supported"); - return; - } - - final SystemTray tray = SystemTray.getSystemTray(); - final Dimension trayIconSize = tray.getTrayIconSize(); - final PopupMenu popup = new PopupMenu(); - final BufferedImage trayIconImage = ImageIO.read(Objects.requireNonNull(getClass().getResource("/images/opal.png"))); - final TrayIcon localTrayIcon = new TrayIcon(trayIconImage.getScaledInstance(trayIconSize.width, trayIconSize.height, java.awt.Image.SCALE_SMOOTH)); - final java.awt.MenuItem displayItem = new java.awt.MenuItem(translationBundle.getString("Display")); - final java.awt.MenuItem settingsItem = new java.awt.MenuItem(translationBundle.getString("Settings")); - final java.awt.MenuItem aboutItem = new java.awt.MenuItem(translationBundle.getString("About")); - final java.awt.MenuItem exitItem = new java.awt.MenuItem(translationBundle.getString("Exit")); - - // Platform.runLater to run on the JavaFX thread - displayItem.addActionListener(e -> Platform.runLater(this::hideShowStage)); - settingsItem.addActionListener(e -> Platform.runLater(this::settingsAction)); - aboutItem.addActionListener(e -> Platform.runLater(this::aboutAction)); - exitItem.addActionListener(e -> Platform.runLater(this::exitAction)); - - popup.add(displayItem); - popup.addSeparator(); - popup.add(settingsItem); - popup.add(aboutItem); - popup.addSeparator(); - popup.add(exitItem); - - localTrayIcon.setToolTip("Opal"); - localTrayIcon.setPopupMenu(popup); - localTrayIcon.addMouseListener(new java.awt.event.MouseAdapter() { - @Override - public void mouseClicked(final java.awt.event.MouseEvent evt) { - if (evt.getClickCount() == 2) { - Platform.runLater(MainWindowController.this::hideShowStage); - } - } - }); - - this.trayIcon = localTrayIcon; - } - - /** - * Display the tray icon - * - * @throws IOException When the {@link TrayIcon} could not be created - */ - public void showTrayIcon() throws IOException { - logger.info("Displaying tray icon"); - if (trayIcon == null) { - createTrayIcon(); - if (trayIcon == null) { - logger.warn("TrayIcon cannot be null!"); - return; - } - } - - final SystemTray tray = SystemTray.getSystemTray(); - try { - if (!Arrays.asList(tray.getTrayIcons()).contains(trayIcon)) { - tray.add(trayIcon); - } - } catch (final AWTException e) { - logger.error("TrayIcon could not be added", e); - } - } - - /** - * Hide the tray icon - */ - public void hideTrayIcon() { - logger.info("Hiding tray icon"); - if (trayIcon == null) { - logger.warn("TrayIcon cannot be null!"); - return; - } - - final SystemTray tray = SystemTray.getSystemTray(); - tray.remove(trayIcon); - - trayIcon = null; - } - /** * Hide the current stage */ @@ -403,7 +327,7 @@ private void openSoundPreset(final String path) { mediaVolumes.forEach((key, value) -> soundPanes.stream().filter(e -> e.getMediaKey().equals(key)).forEach(e -> e.getSlider().setValue(value))); } catch (final IOException ex) { logger.error("Unable to open the sound preset from {}", path, ex); - FxUtils.showErrorAlert(translationBundle.getString("OpenSoundPresetError"), ex.getMessage(), getClass().getResourceAsStream(SharedVariables.ICON_URL)); + FxUtils.showErrorAlert(translationBundle.getString("OpenSoundPresetError"), ex.toString(), getClass().getResourceAsStream(SharedVariables.ICON_URL)); } } @@ -433,7 +357,7 @@ private void saveSoundPresetAction() { objectMapper.writeValue(new File(filePath), mediaVolumes); } catch (final IOException ex) { logger.error("Unable to save the sound settings to {}", filePath, ex); - FxUtils.showErrorAlert(translationBundle.getString("SaveSoundPresetError"), ex.getMessage(), getClass().getResourceAsStream(SharedVariables.ICON_URL)); + FxUtils.showErrorAlert(translationBundle.getString("SaveSoundPresetError"), ex.toString(), getClass().getResourceAsStream(SharedVariables.ICON_URL)); } } else { logger.info("Cancelled saving a sound settings"); @@ -472,6 +396,7 @@ private void settingsAction() { final SettingsWindowController settingsWindowController = loader.getController(); settingsWindowController.setSettingsController(getSettingsController()); settingsWindowController.setMainWindowController(this); + settingsWindowController.setTrayIconController(trayIconController); final Stage primaryStage = new Stage(); @@ -479,11 +404,13 @@ private void settingsAction() { primaryStage.getIcons().add(new Image(Objects.requireNonNull(getClass().getResourceAsStream(SharedVariables.ICON_URL)))); primaryStage.setScene(new Scene(root)); + primaryStage.setOnHiding(event -> ThemeController.setTheme(settingsController.getProperties().getProperty("theme", "Light").toLowerCase())); + logger.info("Showing the SettingsWindow"); primaryStage.show(); } catch (final IOException ex) { logger.error("Unable to open the SettingsWindow", ex); - FxUtils.showErrorAlert(translationBundle.getString("SettingsWindowError"), ex.getMessage(), getClass().getResourceAsStream(SharedVariables.ICON_URL)); + FxUtils.showErrorAlert(translationBundle.getString("SettingsWindowError"), ex.toString(), getClass().getResourceAsStream(SharedVariables.ICON_URL)); } } @@ -507,14 +434,14 @@ public void exceptionOccurred(final Exception ex) { @Override public void run() { logger.error("Error opening the help file", ex); - FxUtils.showErrorAlert(translationBundle.getString("HelpFileError"), ex.getMessage(), getClass().getResourceAsStream(SharedVariables.ICON_URL)); + FxUtils.showErrorAlert(translationBundle.getString("HelpFileError"), ex.toString(), getClass().getResourceAsStream(SharedVariables.ICON_URL)); } }); } }), SharedVariables.HELP_DOCUMENTATION_RESOURCE_LOCATION); } catch (final IOException ex) { logger.error("Error opening the help file", ex); - FxUtils.showErrorAlert(translationBundle.getString("HelpFileError"), ex.getMessage(), getClass().getResourceAsStream(SharedVariables.ICON_URL)); + FxUtils.showErrorAlert(translationBundle.getString("HelpFileError"), ex.toString(), getClass().getResourceAsStream(SharedVariables.ICON_URL)); } } @@ -523,7 +450,7 @@ public void run() { */ @FXML private void homepageAction() { - helpUtils.openCodeDeadWebSite(translationBundle); + helpUtils.openWebsite("https://codedead.com", translationBundle); } /** @@ -539,27 +466,7 @@ private void licenseAction() { */ @FXML private void donateAction() { - logger.info("Opening the CodeDead donation website"); - - final RunnableSiteOpener runnableSiteOpener = new RunnableSiteOpener("https://codedead.com/donate", new IRunnableHelper() { - @Override - public void executed() { - Platform.runLater(() -> logger.info("Successfully opened website")); - } - - @Override - public void exceptionOccurred(final Exception ex) { - Platform.runLater(new Runnable() { - @Override - public void run() { - logger.error("Error opening the CodeDead donation website", ex); - FxUtils.showErrorAlert(translationBundle.getString("WebsiteError"), ex.getMessage(), getClass().getResourceAsStream(SharedVariables.ICON_URL)); - } - }); - } - }); - - new Thread(runnableSiteOpener).start(); + helpUtils.openWebsite("https://codedead.com/donate", translationBundle); } /** @@ -586,7 +493,7 @@ private void aboutAction() { primaryStage.show(); } catch (final IOException ex) { logger.error("Unable to open the AboutWindow", ex); - FxUtils.showErrorAlert(translationBundle.getString("AboutWindowError"), ex.getMessage(), getClass().getResourceAsStream(SharedVariables.ICON_URL)); + FxUtils.showErrorAlert(translationBundle.getString("AboutWindowError"), ex.toString(), getClass().getResourceAsStream(SharedVariables.ICON_URL)); } } @@ -595,7 +502,7 @@ private void aboutAction() { */ @FXML private void updateAction() { - checkForUpdates(true); + checkForUpdates(true, true); } /** @@ -606,6 +513,36 @@ public void fired() { getAllSoundPanes(grpControls).forEach(SoundPane::pause); mniTimerEnabled.setSelected(false); + if (Boolean.parseBoolean(settingsController.getProperties().getProperty("timerComputerShutdown", "false"))) { + final String command = switch (platformName.toLowerCase()) { + case "windows" -> "shutdown -s -t 0"; + case "linux", "macos" -> "shutdown -h now"; + default -> null; + }; + + if (command != null) { + try { + final ProcessBuilder p = new ProcessBuilder(command.split(" ")); + + try (final BufferedReader reader = new BufferedReader(new InputStreamReader(p.start().getInputStream()))) { + final StringBuilder builder = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + builder.append(line); + builder.append(System.getProperty("line.separator")); + } + final String result = builder.toString(); + logger.info("Shutdown command result: {}", result); + } + exitAction(); + } catch (final IOException ex) { + logger.error("Unable to execute shutdown command", ex); + } + } else { + logger.error("Unable to execute shutdown command, unsupported platform {}", platformName); + } + } + if (Boolean.parseBoolean(settingsController.getProperties().getProperty("timerApplicationShutdown", "false"))) { exitAction(); } @@ -706,4 +643,49 @@ public void run() { timer.schedule(timerTask, delay); } + + /** + * Set the audio balance + * + * @param audioBalance The audio balance + */ + public void setAudioBalance(final double audioBalance) { + if (audioBalance < -1 || audioBalance > 1) + throw new IllegalArgumentException("Balance must be between -1.0 and 1.0!"); + + logger.info("Setting the audio balance to {}", audioBalance); + getAllSoundPanes(grpControls).forEach(s -> s.setBalance(audioBalance)); + } + + /** + * Method that is called when the Window should be hidden or shown + */ + @Override + public void onShowHide() { + hideShowStage(); + } + + /** + * Method that is called when the SettingsWindow should be opened + */ + @Override + public void onSettings() { + settingsAction(); + } + + /** + * Method that is called when the AboutWindow should be opened + */ + @Override + public void onAbout() { + aboutAction(); + } + + /** + * Method that is called when the application should exit + */ + @Override + public void onExit() { + exitAction(); + } } diff --git a/src/main/java/com/codedead/opal/controller/SettingsWindowController.java b/src/main/java/com/codedead/opal/controller/SettingsWindowController.java index 7bdf423..9675f4c 100644 --- a/src/main/java/com/codedead/opal/controller/SettingsWindowController.java +++ b/src/main/java/com/codedead/opal/controller/SettingsWindowController.java @@ -1,10 +1,9 @@ package com.codedead.opal.controller; -import atlantafx.base.theme.NordDark; -import atlantafx.base.theme.NordLight; -import atlantafx.base.theme.PrimerDark; -import atlantafx.base.theme.PrimerLight; +import atlantafx.base.theme.*; import com.codedead.opal.domain.NumberTextField; +import com.codedead.opal.domain.OSType; +import com.codedead.opal.domain.OsCheck; import com.codedead.opal.utils.FxUtils; import com.codedead.opal.utils.SharedVariables; import javafx.application.Application; @@ -15,6 +14,7 @@ import javafx.scene.control.Button; import javafx.scene.control.CheckBox; import javafx.scene.control.ComboBox; +import javafx.scene.control.Slider; import javafx.stage.Stage; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; @@ -23,6 +23,7 @@ import java.io.IOException; import java.util.Locale; +import java.util.Objects; import java.util.ResourceBundle; import java.util.concurrent.TimeUnit; @@ -30,6 +31,10 @@ public final class SettingsWindowController { + @FXML + private Slider sldAudioBalance; + @FXML + private CheckBox chbTimerComputerShutdown; @FXML private CheckBox chbTrayIcon; @FXML @@ -53,6 +58,7 @@ public final class SettingsWindowController { private MainWindowController mainWindowController; private SettingsController settingsController; + private TrayIconController trayIconController; private ResourceBundle translationBundle; private final Logger logger; @@ -65,6 +71,20 @@ public SettingsWindowController() { logger.info("Initializing new SettingsWindowController object"); } + /** + * FXML initialize method + */ + @FXML + private void initialize() { + cboTheme.getSelectionModel() + .selectedItemProperty() + .addListener((options, oldValue, newValue) -> ThemeController.setTheme(cboTheme.getValue())); + + if (Objects.requireNonNull(OsCheck.getOperatingSystemType()) == OSType.OTHER) { + chbTimerComputerShutdown.setDisable(true); + } + } + /** * Set the {@link SettingsController} object * @@ -87,17 +107,7 @@ public void setSettingsController(final SettingsController settingsController) { translationBundle.getString("Minutes"), translationBundle.getString("Hours") ); - - final ObservableList themes = FXCollections.observableArrayList( - translationBundle.getString("Light"), - translationBundle.getString("Dark"), - translationBundle.getString("NordLight"), - translationBundle.getString("NordDark") - ); - - cboTheme.setItems(themes); cboDelayType.setItems(options); - loadSettings(); } @@ -110,6 +120,18 @@ public void setMainWindowController(final MainWindowController mainWindowControl this.mainWindowController = mainWindowController; } + /** + * Set the {@link TrayIconController} object + * + * @param trayIconController The {@link TrayIconController} object + */ + public void setTrayIconController(final TrayIconController trayIconController) { + if (trayIconController == null) + throw new NullPointerException("TrayIconController cannot be null!"); + + this.trayIconController = trayIconController; + } + /** * Load all the settings into the UI */ @@ -122,15 +144,7 @@ private void loadSettings() { timerDelay = 1; } - switch (settingsController.getProperties().getProperty("locale", DEFAULT_LOCALE).toLowerCase()) { - case "de-de" -> cboLanguage.getSelectionModel().select(1); - case "es-es" -> cboLanguage.getSelectionModel().select(2); - case "fr-fr" -> cboLanguage.getSelectionModel().select(3); - case "jp-jp" -> cboLanguage.getSelectionModel().select(4); - case "nl-nl" -> cboLanguage.getSelectionModel().select(5); - case "ru-ru" -> cboLanguage.getSelectionModel().select(6); - default -> cboLanguage.getSelectionModel().select(0); - } + cboLanguage.getSelectionModel().select(LanguageController.getLanguageIndexFromLocale(settingsController.getProperties().getProperty("locale", DEFAULT_LOCALE))); switch (settingsController.getProperties().getProperty("loglevel", "INFO").toUpperCase()) { case "OFF" -> cboLogLevel.getSelectionModel().select(0); @@ -143,13 +157,6 @@ private void loadSettings() { default -> cboLogLevel.getSelectionModel().select(4); } - final int themeIndex = switch (settingsController.getProperties().getProperty("theme", "light").toLowerCase()) { - case "dark" -> 1; - case "nordlight" -> 2; - case "norddark" -> 3; - default -> 0; - }; - final long correctDelay = switch (delayType) { case 0 -> TimeUnit.MILLISECONDS.toSeconds(timerDelay); case 1 -> TimeUnit.MILLISECONDS.toMinutes(timerDelay); @@ -163,8 +170,11 @@ private void loadSettings() { chbTrayIcon.setSelected(Boolean.parseBoolean(settingsController.getProperties().getProperty("trayIcon", "false"))); chbTimerApplicationShutdown.setSelected(Boolean.parseBoolean(settingsController.getProperties().getProperty("timerApplicationShutdown", "false"))); cboDelayType.getSelectionModel().select(delayType); - cboTheme.getSelectionModel().select(themeIndex); + cboTheme.getSelectionModel().select(ThemeController.getThemeIndex(settingsController.getProperties().getProperty("theme", "light"))); numDelay.setText(String.valueOf(correctDelay)); + chbTimerComputerShutdown.setSelected(Boolean.parseBoolean(settingsController.getProperties().getProperty("timerComputerShutdown", "false"))); + + sldAudioBalance.setValue(Double.parseDouble(settingsController.getProperties().getProperty("audioBalance", "0.0"))); } /** @@ -180,13 +190,16 @@ private void resetSettingsAction() { try { settingsController.createDefaultProperties(); settingsController.setProperties(settingsController.readPropertiesFile()); + mainWindowController.loadMediaButtonVisibility(Boolean.parseBoolean(settingsController.getProperties().getProperty("mediaButtons", "true"))); - mainWindowController.hideTrayIcon(); + mainWindowController.setAudioBalance(Double.parseDouble(settingsController.getProperties().getProperty("audioBalance", "0.0"))); + + trayIconController.hideTrayIcon(); loadSettings(); } catch (final IOException ex) { logger.error("Unable to reset all settings", ex); - FxUtils.showErrorAlert(translationBundle.getString("ResetSettingsError"), ex.getMessage(), getClass().getResourceAsStream(SharedVariables.ICON_URL)); + FxUtils.showErrorAlert(translationBundle.getString("ResetSettingsError"), ex.toString(), getClass().getResourceAsStream(SharedVariables.ICON_URL)); } } } @@ -206,46 +219,20 @@ private void saveSettingsAction() { mainWindowController.loadMediaButtonVisibility(chbMediaButtons.isSelected()); if (chbTrayIcon.isSelected()) { try { - mainWindowController.showTrayIcon(); + trayIconController.showTrayIcon(); } catch (final IOException ex) { logger.error("Unable to create tray icon", ex); - FxUtils.showErrorAlert(translationBundle.getString("TrayIconError"), ex.getMessage(), getClass().getResourceAsStream(SharedVariables.ICON_URL)); + FxUtils.showErrorAlert(translationBundle.getString("TrayIconError"), ex.toString(), getClass().getResourceAsStream(SharedVariables.ICON_URL)); } } else { - mainWindowController.hideTrayIcon(); + trayIconController.hideTrayIcon(); } showAlertIfLanguageMismatch(settingsController.getProperties().getProperty("locale", DEFAULT_LOCALE)); - switch (cboLanguage.getSelectionModel().getSelectedIndex()) { - case 1 -> settingsController.getProperties().setProperty("locale", "de-DE"); - case 2 -> settingsController.getProperties().setProperty("locale", "es-es"); - case 3 -> settingsController.getProperties().setProperty("locale", "fr-FR"); - case 4 -> settingsController.getProperties().setProperty("locale", "jp-JP"); - case 5 -> settingsController.getProperties().setProperty("locale", "nl-NL"); - case 6 -> settingsController.getProperties().setProperty("locale", "ru-RU"); - default -> settingsController.getProperties().setProperty("locale", DEFAULT_LOCALE); - } - - switch (cboTheme.getSelectionModel().getSelectedIndex()) { - case 1 -> { - settingsController.getProperties().setProperty("theme", "dark"); - Application.setUserAgentStylesheet(new PrimerDark().getUserAgentStylesheet()); - } - case 2 -> { - settingsController.getProperties().setProperty("theme", "nordlight"); - Application.setUserAgentStylesheet(new NordLight().getUserAgentStylesheet()); - } - case 3 -> { - settingsController.getProperties().setProperty("theme", "norddark"); - Application.setUserAgentStylesheet(new NordDark().getUserAgentStylesheet()); - } - default -> { - settingsController.getProperties().setProperty("theme", "light"); - Application.setUserAgentStylesheet(new PrimerLight().getUserAgentStylesheet()); - } - } + settingsController.getProperties().setProperty("locale", LanguageController.getLocaleFromLanguageIndex(cboLanguage.getSelectionModel().getSelectedIndex())); + settingsController.getProperties().setProperty("theme", cboTheme.getSelectionModel().getSelectedItem()); settingsController.getProperties().setProperty("loglevel", cboLogLevel.getValue()); final Level level = switch (cboLogLevel.getValue()) { @@ -277,13 +264,17 @@ private void saveSettingsAction() { settingsController.getProperties().setProperty("timerDelay", String.valueOf(correctDelay)); settingsController.getProperties().setProperty("timerDelayType", String.valueOf(delayType)); settingsController.getProperties().setProperty("timerApplicationShutdown", String.valueOf(chbTimerApplicationShutdown.isSelected())); + settingsController.getProperties().setProperty("timerComputerShutdown", String.valueOf(chbTimerComputerShutdown.isSelected())); + + settingsController.getProperties().setProperty("audioBalance", String.valueOf(sldAudioBalance.getValue())); + mainWindowController.setAudioBalance(sldAudioBalance.getValue()); Configurator.setAllLevels(LogManager.getRootLogger().getName(), level); try { settingsController.saveProperties(); } catch (final IOException ex) { logger.error("Unable to save all settings", ex); - FxUtils.showErrorAlert(translationBundle.getString("SaveSettingsError"), ex.getMessage(), getClass().getResourceAsStream(SharedVariables.ICON_URL)); + FxUtils.showErrorAlert(translationBundle.getString("SaveSettingsError"), ex.toString(), getClass().getResourceAsStream(SharedVariables.ICON_URL)); } } @@ -293,15 +284,7 @@ private void saveSettingsAction() { * @param languageToMatch The language that needs to be matched to the combobox */ private void showAlertIfLanguageMismatch(final String languageToMatch) { - final String newLanguage = switch (cboLanguage.getSelectionModel().getSelectedIndex()) { - case 1 -> "de-DE"; - case 2 -> "es-es"; - case 3 -> "fr-FR"; - case 4 -> "jp-JP"; - case 5 -> "nl-NL"; - case 6 -> "ru-RU"; - default -> DEFAULT_LOCALE; - }; + final String newLanguage = LanguageController.getLocaleFromLanguageIndex(cboLanguage.getSelectionModel().getSelectedIndex()); if (!languageToMatch.equals(newLanguage)) { FxUtils.showInformationAlert(translationBundle.getString("RestartRequired"), getClass().getResourceAsStream(SharedVariables.ICON_URL)); diff --git a/src/main/java/com/codedead/opal/controller/ThemeController.java b/src/main/java/com/codedead/opal/controller/ThemeController.java new file mode 100644 index 0000000..0865c5e --- /dev/null +++ b/src/main/java/com/codedead/opal/controller/ThemeController.java @@ -0,0 +1,59 @@ +package com.codedead.opal.controller; + +import atlantafx.base.theme.*; +import javafx.application.Application; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public final class ThemeController { + + private static final Logger logger = LogManager.getLogger(ThemeController.class); + + /** + * Initialize a new ThemeController + */ + private ThemeController() { + // Default constructor + } + + /** + * Change the application theme + * + * @param themeName The theme name + */ + public static void setTheme(final String themeName) { + logger.info("Setting theme to {}", themeName); + + switch (themeName.toLowerCase()) { + case "modena" -> Application.setUserAgentStylesheet(Application.STYLESHEET_MODENA); + case "caspian" -> Application.setUserAgentStylesheet(Application.STYLESHEET_CASPIAN); + case "nordlight" -> Application.setUserAgentStylesheet(new NordLight().getUserAgentStylesheet()); + case "norddark" -> Application.setUserAgentStylesheet(new NordDark().getUserAgentStylesheet()); + case "dark" -> Application.setUserAgentStylesheet(new PrimerDark().getUserAgentStylesheet()); + case "cupertinodark" -> Application.setUserAgentStylesheet(new CupertinoDark().getUserAgentStylesheet()); + case "cuptertinolight" -> Application.setUserAgentStylesheet(new CupertinoLight().getUserAgentStylesheet()); + case "dracula" -> Application.setUserAgentStylesheet(new Dracula().getUserAgentStylesheet()); + default -> Application.setUserAgentStylesheet(new PrimerLight().getUserAgentStylesheet()); + } + } + + /** + * Get the theme index from the theme name + * + * @param themeName The theme name + * @return The theme index + */ + public static int getThemeIndex(final String themeName) { + return switch (themeName.toLowerCase()) { + case "cupertinodark" -> 0; + case "cupertinolight" -> 1; + case "dracula" -> 2; + case "dark" -> 4; + case "nordlight" -> 5; + case "norddark" -> 6; + case "modena" -> 7; + case "caspian" -> 8; + default -> 3; + }; + } +} diff --git a/src/main/java/com/codedead/opal/controller/TrayIconController.java b/src/main/java/com/codedead/opal/controller/TrayIconController.java new file mode 100644 index 0000000..46e10c9 --- /dev/null +++ b/src/main/java/com/codedead/opal/controller/TrayIconController.java @@ -0,0 +1,132 @@ +package com.codedead.opal.controller; + +import com.codedead.opal.interfaces.TrayIconListener; +import javafx.application.Platform; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.Arrays; +import java.util.Objects; +import java.util.ResourceBundle; + +public final class TrayIconController { + + private TrayIcon trayIcon; + private final ResourceBundle resourceBundle; + private final TrayIconListener trayIconListener; + private final Logger logger; + + /** + * Initialize a new TrayIconController + * + * @param resourceBundle The {@link ResourceBundle} object + * @param trayIconListener The {@link TrayIconListener} interface + */ + public TrayIconController(final ResourceBundle resourceBundle, final TrayIconListener trayIconListener) { + if (resourceBundle == null) + throw new NullPointerException("ResourceBundle cannot be null!"); + if (trayIconListener == null) + throw new NullPointerException("TrayIconListener cannot be null!"); + + this.resourceBundle = resourceBundle; + this.trayIconListener = trayIconListener; + this.logger = LogManager.getLogger(TrayIconController.class); + } + + /** + * Create a tray icon + * + * @throws IOException When the {@link TrayIcon} could not be created + */ + private void createTrayIcon() throws IOException { + logger.info("Creating tray icon"); + if (!SystemTray.isSupported()) { + logger.warn("SystemTray is not supported"); + return; + } + + final SystemTray tray = SystemTray.getSystemTray(); + final Dimension trayIconSize = tray.getTrayIconSize(); + final PopupMenu popup = new PopupMenu(); + final BufferedImage trayIconImage = ImageIO.read(Objects.requireNonNull(getClass().getResource("/images/opal.png"))); + final TrayIcon localTrayIcon = new TrayIcon(trayIconImage.getScaledInstance(trayIconSize.width, trayIconSize.height, java.awt.Image.SCALE_SMOOTH)); + final java.awt.MenuItem displayItem = new java.awt.MenuItem(resourceBundle.getString("Display")); + final java.awt.MenuItem settingsItem = new java.awt.MenuItem(resourceBundle.getString("Settings")); + final java.awt.MenuItem aboutItem = new java.awt.MenuItem(resourceBundle.getString("About")); + final java.awt.MenuItem exitItem = new java.awt.MenuItem(resourceBundle.getString("Exit")); + + if (trayIconListener != null) { + // Platform.runLater to run on the JavaFX thread + displayItem.addActionListener(e -> Platform.runLater(trayIconListener::onShowHide)); + settingsItem.addActionListener(e -> Platform.runLater(trayIconListener::onSettings)); + aboutItem.addActionListener(e -> Platform.runLater(trayIconListener::onAbout)); + exitItem.addActionListener(e -> Platform.runLater(trayIconListener::onExit)); + + localTrayIcon.addMouseListener(new java.awt.event.MouseAdapter() { + @Override + public void mouseClicked(final java.awt.event.MouseEvent evt) { + if (evt.getClickCount() == 2) { + Platform.runLater(trayIconListener::onShowHide); + } + } + }); + } + + popup.add(displayItem); + popup.addSeparator(); + popup.add(settingsItem); + popup.add(aboutItem); + popup.addSeparator(); + popup.add(exitItem); + + localTrayIcon.setToolTip("Opal"); + localTrayIcon.setPopupMenu(popup); + + this.trayIcon = localTrayIcon; + } + + /** + * Display the tray icon + * + * @throws IOException When the {@link TrayIcon} could not be created + */ + public void showTrayIcon() throws IOException { + logger.info("Displaying tray icon"); + if (trayIcon == null) { + createTrayIcon(); + if (trayIcon == null) { + logger.warn("TrayIcon cannot be null!"); + return; + } + } + + final SystemTray tray = SystemTray.getSystemTray(); + try { + if (!Arrays.asList(tray.getTrayIcons()).contains(trayIcon)) { + tray.add(trayIcon); + } + } catch (final AWTException e) { + logger.error("TrayIcon could not be added", e); + } + } + + /** + * Hide the tray icon + */ + public void hideTrayIcon() { + logger.info("Hiding tray icon"); + if (trayIcon == null) { + logger.warn("TrayIcon cannot be null!"); + return; + } + + final SystemTray tray = SystemTray.getSystemTray(); + tray.remove(trayIcon); + + trayIcon = null; + } +} diff --git a/src/main/java/com/codedead/opal/domain/MediaPlayerException.java b/src/main/java/com/codedead/opal/domain/MediaPlayerException.java new file mode 100644 index 0000000..7ea5ca1 --- /dev/null +++ b/src/main/java/com/codedead/opal/domain/MediaPlayerException.java @@ -0,0 +1,13 @@ +package com.codedead.opal.domain; + +public final class MediaPlayerException extends Exception { + + /** + * Initialize a new MediaPlayerException + * + * @param message The error message + */ + public MediaPlayerException(final String message) { + super(message); + } +} diff --git a/src/main/java/com/codedead/opal/domain/NumberTextField.java b/src/main/java/com/codedead/opal/domain/NumberTextField.java index 6042f1e..300ef97 100644 --- a/src/main/java/com/codedead/opal/domain/NumberTextField.java +++ b/src/main/java/com/codedead/opal/domain/NumberTextField.java @@ -73,7 +73,7 @@ private boolean validate(final String text) { /** * Get the minimum allowed value * - * @return The mimium allowed value + * @return The minimum allowed value */ @FXML public int getMin() { @@ -85,6 +85,7 @@ public int getMin() { * * @param min The minimum allowed value */ + @SuppressWarnings("unused") @FXML public void setMin(final int min) { this.min = min; diff --git a/src/main/java/com/codedead/opal/domain/SoundPane.java b/src/main/java/com/codedead/opal/domain/SoundPane.java index 20b138a..9e5ae38 100644 --- a/src/main/java/com/codedead/opal/domain/SoundPane.java +++ b/src/main/java/com/codedead/opal/domain/SoundPane.java @@ -1,9 +1,6 @@ package com.codedead.opal.domain; -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringProperty; -import javafx.beans.value.ChangeListener; -import javafx.beans.value.ObservableValue; +import javafx.application.Platform; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.scene.control.Button; @@ -15,6 +12,8 @@ import javafx.scene.media.Media; import javafx.scene.media.MediaPlayer; import javafx.util.Duration; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import java.io.IOException; import java.net.URISyntaxException; @@ -37,10 +36,12 @@ public final class SoundPane extends GridPane { @FXML private ImageView imgMediaButton; @FXML - private final StringProperty mediaPath = new SimpleStringProperty(); + private String mediaPath; @FXML private String mediaKey; + private double balance; private MediaPlayer mediaPlayer; + private final Logger logger; /** * Initialize a new SoundPane @@ -52,6 +53,8 @@ public SoundPane() throws IOException { fxmlLoader.setRoot(this); fxmlLoader.setController(this); fxmlLoader.load(); + + this.logger = LogManager.getLogger(SoundPane.class); } /** @@ -62,33 +65,13 @@ private void initialize() { sldVolume.valueProperty().addListener((observable, oldValue, newValue) -> { if (newValue != null && newValue.doubleValue() == 0 && (oldValue != null && oldValue.doubleValue() != 0)) { pause(); + disposeMediaPlayer(); } else if (newValue != null && newValue.doubleValue() > 0 && (oldValue != null && oldValue.doubleValue() == 0)) { - play(); - } - }); - } - - /** - * Initialize the {@link MediaPlayer} object and the {@link ChangeListener} for the mediaPath property - * - * @throws URISyntaxException When the media path could not be converted to a URI - */ - private void initializeMediaPlayerProperties() throws URISyntaxException { - initializeMediaPlayer(mediaPath.getValue()); - - mediaPath.addListener((observable, oldValue, newValue) -> { - if (newValue != null && !newValue.isEmpty()) { try { - initializeMediaPlayer(newValue); - } catch (final URISyntaxException e) { - throw new RuntimeException(e); - } - } else { - if (mediaPlayer != null) { - mediaPlayer.stop(); - mediaPlayer.dispose(); - - mediaPlayer = null; + play(); + } catch (final MediaPlayerException e) { + logger.fatal("Could not play the media file!", e); + Platform.exit(); } } }); @@ -106,26 +89,29 @@ private void initializeMediaPlayer(final String value) throws URISyntaxException if (value.isEmpty()) throw new IllegalArgumentException("Value cannot be empty!"); - if (mediaPlayer != null) { - mediaPlayer.stop(); - mediaPlayer.dispose(); - - mediaPlayer = null; - } + disposeMediaPlayer(); mediaPlayer = new MediaPlayer(new Media(Objects.requireNonNull(getClass().getResource(value)).toURI().toString())); - mediaPlayer.setOnEndOfMedia(() -> mediaPlayer.seek(Duration.ZERO)); + mediaPlayer.currentTimeProperty().addListener((observableValue, oldDuration, newDuration) -> { + // Quality of life improvement to reduce audio lag when restarting the media + if (mediaPlayer != null && newDuration.toSeconds() >= mediaPlayer.getMedia().getDuration().toSeconds() - 0.5) { + mediaPlayer.seek(Duration.ZERO); + } + }); + mediaPlayer.setOnEndOfMedia(() -> { + if (mediaPlayer != null) { + mediaPlayer.seek(Duration.ZERO); + } + }); mediaPlayer.volumeProperty().bindBidirectional(sldVolume.valueProperty()); - mediaPlayer.statusProperty().addListener(new ChangeListener<>() { - @Override - public void changed(ObservableValue observable, MediaPlayer.Status oldValue, MediaPlayer.Status newValue) { - if (newValue == MediaPlayer.Status.PLAYING) { - imgMediaButton.setImage(new Image(Objects.requireNonNull(getClass().getResourceAsStream("/images/pause.png")))); - } else { - imgMediaButton.setImage(new Image(Objects.requireNonNull(getClass().getResourceAsStream("/images/play.png")))); - } + mediaPlayer.statusProperty().addListener((observable, oldValue, newValue) -> { + if (newValue == MediaPlayer.Status.PLAYING) { + imgMediaButton.setImage(new Image(Objects.requireNonNull(getClass().getResourceAsStream("/images/pause.png")))); + } else { + imgMediaButton.setImage(new Image(Objects.requireNonNull(getClass().getResourceAsStream("/images/play.png")))); } }); + mediaPlayer.setBalance(balance); } /** @@ -214,7 +200,7 @@ public void setMediaButton(final boolean mediaButton) { */ @FXML public String getMediaPath() { - return mediaPath.getValue(); + return mediaPath; } /** @@ -229,7 +215,7 @@ public void setMediaPath(final String mediaPath) { if (mediaPath.isEmpty()) throw new IllegalArgumentException("Media path cannot be empty!"); - this.mediaPath.setValue(mediaPath); + this.mediaPath = mediaPath; } /** @@ -259,13 +245,16 @@ public void setMediaKey(final String mediaKey) { /** * Play the {@link Media} object + * + * @throws MediaPlayerException When the {@link MediaPlayer} object could not be initialized */ - public void play() { + public void play() throws MediaPlayerException { if (mediaPlayer == null) { try { - initializeMediaPlayerProperties(); + initializeMediaPlayer(mediaPath); } catch (final URISyntaxException e) { - throw new RuntimeException(e); + logger.fatal("Could not convert the media path to a URI!", e); + throw new MediaPlayerException(e.getMessage()); } } this.mediaPlayer.play(); @@ -275,18 +264,61 @@ public void play() { * Pause the {@link Media} object */ public void pause() { - this.mediaPlayer.pause(); + if (mediaPlayer != null) { + this.mediaPlayer.pause(); + } } /** * Play or pause the {@link Media} object + * + * @throws MediaPlayerException When the {@link MediaPlayer} object could not be initialized */ @FXML - private void playPause() { + private void playPause() throws MediaPlayerException { if (mediaPlayer != null && mediaPlayer.getStatus() == MediaPlayer.Status.PLAYING) { pause(); } else { play(); } } + + /** + * Dispose of the {@link MediaPlayer} object and all bindings + */ + private void disposeMediaPlayer() { + if (mediaPlayer != null) { + mediaPlayer.volumeProperty().unbindBidirectional(sldVolume.valueProperty()); + mediaPlayer.stop(); + mediaPlayer.dispose(); + + mediaPlayer = null; + } + } + + /** + * Get the audio balance + * + * @return The audio balance + */ + public double getBalance() { + return balance; + } + + /** + * Set the audio balance + * + * @param balance The audio balance + */ + public void setBalance(final double balance) { + if (balance < -1.0 || balance > 1.0) + throw new IllegalArgumentException("Balance must be between -1.0 and 1.0!"); + + logger.info("Setting audio balance to {}", balance); + + this.balance = balance; + if (mediaPlayer != null) { + mediaPlayer.setBalance(balance); + } + } } diff --git a/src/main/java/com/codedead/opal/interfaces/IAudioTimer.java b/src/main/java/com/codedead/opal/interfaces/IAudioTimer.java index 0defda1..792483c 100644 --- a/src/main/java/com/codedead/opal/interfaces/IAudioTimer.java +++ b/src/main/java/com/codedead/opal/interfaces/IAudioTimer.java @@ -1,6 +1,6 @@ package com.codedead.opal.interfaces; -public interface IAudioTimer { +public sealed interface IAudioTimer permits com.codedead.opal.controller.MainWindowController { void fired(); diff --git a/src/main/java/com/codedead/opal/interfaces/TrayIconListener.java b/src/main/java/com/codedead/opal/interfaces/TrayIconListener.java new file mode 100644 index 0000000..0fc719f --- /dev/null +++ b/src/main/java/com/codedead/opal/interfaces/TrayIconListener.java @@ -0,0 +1,12 @@ +package com.codedead.opal.interfaces; + +public sealed interface TrayIconListener permits com.codedead.opal.controller.MainWindowController { + + void onShowHide(); + + void onSettings(); + + void onAbout(); + + void onExit(); +} diff --git a/src/main/java/com/codedead/opal/utils/HelpUtils.java b/src/main/java/com/codedead/opal/utils/HelpUtils.java index 879032e..6fd3a7c 100644 --- a/src/main/java/com/codedead/opal/utils/HelpUtils.java +++ b/src/main/java/com/codedead/opal/utils/HelpUtils.java @@ -102,7 +102,7 @@ public void exceptionOccurred(final Exception ex) { @Override public void run() { logger.error("Error opening the license file", ex); - FxUtils.showErrorAlert(translationBundle.getString("LicenseFileError"), ex.getMessage(), getClass().getResourceAsStream(SharedVariables.ICON_URL)); + FxUtils.showErrorAlert(translationBundle.getString("LicenseFileError"), ex.toString(), getClass().getResourceAsStream(SharedVariables.ICON_URL)); } }); @@ -110,19 +110,25 @@ public void run() { }), SharedVariables.LICENSE_RESOURCE_LOCATION); } catch (final IOException ex) { logger.error("Error opening the license file", ex); - FxUtils.showErrorAlert(translationBundle.getString("LicenseFileError"), ex.getMessage(), getClass().getResourceAsStream(SharedVariables.ICON_URL)); + FxUtils.showErrorAlert(translationBundle.getString("LicenseFileError"), ex.toString(), getClass().getResourceAsStream(SharedVariables.ICON_URL)); } } /** - * Open the CodeDead website + * Open a website * - * @param translationBundle The {@link ResourceBundle} object that contains translations + * @param url The URL of the website + * @param resourceBundle The {@link ResourceBundle} object that contains translations */ - public void openCodeDeadWebSite(final ResourceBundle translationBundle) { - logger.info("Opening the CodeDead website"); + public void openWebsite(final String url, final ResourceBundle resourceBundle) { + if (url == null) + throw new NullPointerException("URL cannot be null!"); + if (url.isEmpty()) + throw new IllegalArgumentException("URL cannot be empty!"); + + logger.info("Opening the website {}", url); - final RunnableSiteOpener runnableSiteOpener = new RunnableSiteOpener("https://codedead.com", new IRunnableHelper() { + final RunnableSiteOpener runnableSiteOpener = new RunnableSiteOpener(url, new IRunnableHelper() { @Override public void executed() { Platform.runLater(() -> logger.info("Successfully opened website")); @@ -133,8 +139,8 @@ public void exceptionOccurred(final Exception ex) { Platform.runLater(new Runnable() { @Override public void run() { - logger.error("Error opening the CodeDead website", ex); - FxUtils.showErrorAlert(translationBundle.getString("WebsiteError"), ex.getMessage(), getClass().getResourceAsStream(SharedVariables.ICON_URL)); + logger.error("Error opening the website {}", url, ex); + FxUtils.showErrorAlert(resourceBundle.getString("WebsiteError"), ex.toString(), getClass().getResourceAsStream(SharedVariables.ICON_URL)); } }); } diff --git a/src/main/java/com/codedead/opal/utils/RunnableSiteOpener.java b/src/main/java/com/codedead/opal/utils/RunnableSiteOpener.java index 6b1bec5..c0914b6 100644 --- a/src/main/java/com/codedead/opal/utils/RunnableSiteOpener.java +++ b/src/main/java/com/codedead/opal/utils/RunnableSiteOpener.java @@ -36,7 +36,9 @@ public RunnableSiteOpener(final String url, final IRunnableHelper iRunnableHelpe @Override public void run() { try { - Desktop.getDesktop().browse(new URI(url)); + if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { + Desktop.getDesktop().browse(new URI(url)); + } if (iRunnableHelper != null) { iRunnableHelper.executed(); } diff --git a/src/main/java/com/codedead/opal/utils/SharedVariables.java b/src/main/java/com/codedead/opal/utils/SharedVariables.java index cbde382..7238b57 100644 --- a/src/main/java/com/codedead/opal/utils/SharedVariables.java +++ b/src/main/java/com/codedead/opal/utils/SharedVariables.java @@ -3,18 +3,22 @@ public final class SharedVariables { public static final String ICON_URL = "/images/opal.png"; - public static final String CURRENT_VERSION = "1.1.0.0"; + public static final String CURRENT_VERSION = "1.2.0.0"; public static final boolean PORTABLE = false; public static final String DEFAULT_LOCALE = "en-US"; + public static final String PROPERTIES_BASE_PATH = PORTABLE + ? System.getProperty("user.dir") + "/.com.codedead.opal" + : System.getProperty("user.home") + "/.config/com.codedead.opal"; + public static final String PROPERTIES_RESOURCE_LOCATION = "default.properties"; - public static final String PROPERTIES_FILE_LOCATION = System.getProperty("user.home") + "/.opal/opal.properties"; + public static final String PROPERTIES_FILE_LOCATION = PROPERTIES_BASE_PATH + "/opal.properties"; public static final String HELP_DOCUMENTATION_RESOURCE_LOCATION = "/documents/help.pdf"; - public static final String HELP_DOCUMENTATION_FILE_LOCATION = System.getProperty("user.home") + "/.opal/help.pdf"; + public static final String HELP_DOCUMENTATION_FILE_LOCATION = PROPERTIES_BASE_PATH + "/help.pdf"; public static final String LICENSE_RESOURCE_LOCATION = "/documents/license.pdf"; - public static final String LICENSE_FILE_LOCATION = System.getProperty("user.home") + "/.opal/license.pdf"; + public static final String LICENSE_FILE_LOCATION = PROPERTIES_BASE_PATH + "/license.pdf"; /** * Initialize a new SharedVariables diff --git a/src/main/resources/audio/belltower.mp3 b/src/main/resources/audio/belltower.mp3 new file mode 100644 index 0000000..dc6dcbf Binary files /dev/null and b/src/main/resources/audio/belltower.mp3 differ diff --git a/src/main/resources/audio/seagulls.mp3 b/src/main/resources/audio/seagulls.mp3 new file mode 100644 index 0000000..7f835dc Binary files /dev/null and b/src/main/resources/audio/seagulls.mp3 differ diff --git a/src/main/resources/controls/SoundPane.fxml b/src/main/resources/controls/SoundPane.fxml index 71c2116..af08c8a 100644 --- a/src/main/resources/controls/SoundPane.fxml +++ b/src/main/resources/controls/SoundPane.fxml @@ -8,6 +8,7 @@ + diff --git a/src/main/resources/default.properties b/src/main/resources/default.properties index 3670a67..e0bae84 100644 --- a/src/main/resources/default.properties +++ b/src/main/resources/default.properties @@ -5,7 +5,9 @@ loglevel=ERROR timerDelay=3600000 timerDelayType=2 timerApplicationShutdown=false +timerComputerShutdown=false mediaButtons=true dragDrop=true theme=light trayIcon=false +audioBalance=0.0 diff --git a/src/main/resources/documents/help.pdf b/src/main/resources/documents/help.pdf index 93f0ee8..04b7a26 100644 Binary files a/src/main/resources/documents/help.pdf and b/src/main/resources/documents/help.pdf differ diff --git a/src/main/resources/images/advanced.png b/src/main/resources/images/advanced.png new file mode 100644 index 0000000..0a24229 Binary files /dev/null and b/src/main/resources/images/advanced.png differ diff --git a/src/main/resources/images/belltower.png b/src/main/resources/images/belltower.png new file mode 100644 index 0000000..6cad5dc Binary files /dev/null and b/src/main/resources/images/belltower.png differ diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml index 1c674b5..8d9da0b 100644 --- a/src/main/resources/log4j2.xml +++ b/src/main/resources/log4j2.xml @@ -1,12 +1,12 @@ - .opal/logs + /logs - [%level][%d{yyyy-MM-dd HH:mm:ss.SSS}][%t] %c{1}\t-\t%msg%n @@ -15,7 +15,7 @@ - + diff --git a/src/main/resources/translations/OpalApplication.properties b/src/main/resources/translations/OpalApplication.properties index 6c67fcf..c5cab5f 100644 --- a/src/main/resources/translations/OpalApplication.properties +++ b/src/main/resources/translations/OpalApplication.properties @@ -1,5 +1,5 @@ About=About -AboutText=Opal was created by DeadLine\n\nAudio: ZapSplat.com\nImages: Remix Icon\nTheme: AtlantaFX\nVersion: 1.1.0\n\nCopyright © 2023 CodeDead +AboutText=Opal was created by DeadLine\n\nAudio: ZapSplat.com\nImages: Remix Icon\nTheme: AtlantaFX\nVersion: 1.2.0\n\nCopyright © 2023 CodeDead AboutWindowError=Unable to open the About Window! AboutWindowTitle=Opal - About AutoUpdate=Automatically check for updates @@ -77,10 +77,6 @@ Gong=Gong MediaButtons=Media buttons DragDrop=Drag and drop files Theme=Theme -Light=Light -Dark=Dark -NordLight=Nord light -NordDark=Nord dark Space=Space Restaurant=Restaurant Cancel=Cancel @@ -93,3 +89,8 @@ WhiteNoise=White noise RadioFrequencyStatic=Radio frequency static PinkNoise=Pink noise BrownNoise=Brown noise +TimerComputerShutdown=Shutdown computer +AudioBalance=Audio balance +Advanced=Advanced +Seagulls=Seagulls +Belltower=Bell tower diff --git a/src/main/resources/translations/OpalApplication_de_DE.properties b/src/main/resources/translations/OpalApplication_de_DE.properties index e386b94..59a6b29 100644 --- a/src/main/resources/translations/OpalApplication_de_DE.properties +++ b/src/main/resources/translations/OpalApplication_de_DE.properties @@ -1,5 +1,5 @@ About=Über -AboutText=Opal wurde erstellt von: DeadLine\n\nAudio: ZapSplat.com\nBilder: Remix Icon\nDesign: AtlantaFX\nÜbersetzung: github.com/uDEV2019\nVersion: 1.1.0\n\nCopyright © 2023 CodeDead +AboutText=Opal wurde erstellt von: DeadLine\n\nAudio: ZapSplat.com\nBilder: Remix Icon\nDesign: AtlantaFX\nÜbersetzung: github.com/uDEV2019\nVersion: 1.2.0\n\nCopyright © 2023 CodeDead AboutWindowError=Über-Dialog konnte nicht geöffnet werden! AboutWindowTitle=Opal - Über AutoUpdate=Automatisch auf Aktualisierungen prüfen @@ -77,10 +77,6 @@ Gong=Klingel MediaButtons=Medientasten DragDrop=Dateien ziehen und ablegen Theme=Thema -Light=Licht -Dark=Dunkel -NordLight=Nordlicht -NordDark=Norden dunkel Space=Weltraum Restaurant=Restaurant Cancel=Abbrechen @@ -93,3 +89,8 @@ WhiteNoise=Weißes Rauschen RadioFrequencyStatic=Radiofrequenzstörung PinkNoise=Pinkes Rauschen BrownNoise=Braunes Rauschen +TimerComputerShutdown=Computer herunterfahren +AudioBalance=Audiobalance +Advanced=Erweitert +Seagulls=Möwen +Belltower=Glockenturm diff --git a/src/main/resources/translations/OpalApplication_en_US.properties b/src/main/resources/translations/OpalApplication_en_US.properties index 6c67fcf..c5cab5f 100644 --- a/src/main/resources/translations/OpalApplication_en_US.properties +++ b/src/main/resources/translations/OpalApplication_en_US.properties @@ -1,5 +1,5 @@ About=About -AboutText=Opal was created by DeadLine\n\nAudio: ZapSplat.com\nImages: Remix Icon\nTheme: AtlantaFX\nVersion: 1.1.0\n\nCopyright © 2023 CodeDead +AboutText=Opal was created by DeadLine\n\nAudio: ZapSplat.com\nImages: Remix Icon\nTheme: AtlantaFX\nVersion: 1.2.0\n\nCopyright © 2023 CodeDead AboutWindowError=Unable to open the About Window! AboutWindowTitle=Opal - About AutoUpdate=Automatically check for updates @@ -77,10 +77,6 @@ Gong=Gong MediaButtons=Media buttons DragDrop=Drag and drop files Theme=Theme -Light=Light -Dark=Dark -NordLight=Nord light -NordDark=Nord dark Space=Space Restaurant=Restaurant Cancel=Cancel @@ -93,3 +89,8 @@ WhiteNoise=White noise RadioFrequencyStatic=Radio frequency static PinkNoise=Pink noise BrownNoise=Brown noise +TimerComputerShutdown=Shutdown computer +AudioBalance=Audio balance +Advanced=Advanced +Seagulls=Seagulls +Belltower=Bell tower diff --git a/src/main/resources/translations/OpalApplication_es_ES.properties b/src/main/resources/translations/OpalApplication_es_ES.properties index c52ee6e..8909c3c 100644 --- a/src/main/resources/translations/OpalApplication_es_ES.properties +++ b/src/main/resources/translations/OpalApplication_es_ES.properties @@ -1,5 +1,5 @@ About=Acerca de -AboutText=Opal fue creado por DeadLine\n\nAudio: ZapSplat.com\nImágenes: Remix Icon\nTema: AtlantaFX\nVersión: 1.1.0\n\nCopyright © 2023 CodeDead +AboutText=Opal fue creado por DeadLine\n\nAudio: ZapSplat.com\nImágenes: Remix Icon\nTema: AtlantaFX\nVersión: 1.2.0\n\nCopyright © 2023 CodeDead AboutWindowError=¡No se puede abrir la ventana Acerca de! AboutWindowTitle=Opal - Acerca de AutoUpdate=Buscar actualizaciones automáticamente @@ -77,10 +77,6 @@ Gong=Gongo MediaButtons=Botones multimedia DragDrop=Arrastrar y soltar archivos Theme=Tema -Light=Ligero -Dark=Oscuro -NordLight=Nord ligero -NordDark=Nord oscuro Space=Espacio Restaurant=Restaurante Cancel=Cancelar @@ -93,3 +89,8 @@ WhiteNoise=Ruido blanco RadioFrequencyStatic=Estático de frecuencia de radio PinkNoise=Ruido rosa BrownNoise=Ruido marrón +TimerComputerShutdown=Apagar la computadora +AudioBalance=Balance de audio +Advanced=Avanzado +Seagulls=Gaviotas +Belltower=Campanario diff --git a/src/main/resources/translations/OpalApplication_fr_FR.properties b/src/main/resources/translations/OpalApplication_fr_FR.properties index 5dda249..3412099 100644 --- a/src/main/resources/translations/OpalApplication_fr_FR.properties +++ b/src/main/resources/translations/OpalApplication_fr_FR.properties @@ -1,5 +1,5 @@ About=À propos -AboutText=Opal a été créé par DeadLine\n\nAudio: ZapSplat.com\nImages: Remix Icon\nThème: AtlantaFX\nVersion: 1.1.0\n\nCopyright © 2023 CodeDead +AboutText=Opal a été créé par DeadLine\n\nAudio: ZapSplat.com\nImages: Remix Icon\nThème: AtlantaFX\nVersion: 1.2.0\n\nCopyright © 2023 CodeDead AboutWindowError=Impossible d'ouvrir la fenêtre À propos! AboutWindowTitle=Opal - À propos AutoUpdate=Rechercher automatiquement les mises à jour @@ -77,10 +77,6 @@ Gong=Gong MediaButtons=Boutons multimédias DragDrop=Faites glisser et déposez des fichiers Theme=Thème -Light=Clair -Dark=Foncé -NordLight=Nord clair -NordDark=Nord foncé Space=L'espace Restaurant=Restaurant Cancel=Annuler @@ -93,3 +89,8 @@ WhiteNoise=Bruit blanc RadioFrequencyStatic=Bruit blanc de fréquence radio PinkNoise=Bruit rose BrownNoise=Bruit brun +TimerComputerShutdown=Éteindre l'ordinateur +AudioBalance=Balance audio +Advanced=Avancé +Seagulls=Mouettes +Belltower=Clocher diff --git a/src/main/resources/translations/OpalApplication_jp_JP.properties b/src/main/resources/translations/OpalApplication_jp_JP.properties index 9b2c084..471950f 100644 --- a/src/main/resources/translations/OpalApplication_jp_JP.properties +++ b/src/main/resources/translations/OpalApplication_jp_JP.properties @@ -1,19 +1,19 @@ -About=約 -AboutText=Opal は DeadLine によって作成されました\n\nオーディオ: ZapSplat.com\n画像: リミックス アイコン\nテーマ: AtlantaFX\nバージョン: 1.1.0\n\nCopyright © 2023 CodeDead +About=このアプリについて +AboutText=Opal は DeadLine によって作成されました\n\nオーディオ: ZapSplat.com\n画像: リミックス アイコン\nテーマ: AtlantaFX\nバージョン: 1.2.0\n\nCopyright © 2023 CodeDead AboutWindowError=バージョン情報ウィンドウを開けません! -AboutWindowTitle=Opal - 約 +AboutWindowTitle=Opal - このアプリについて AutoUpdate=アップデートを自動的に確認する Birds=鳥 Chatter=おしゃべり CheckForUpdates=アップデートを確認 -Close=近い +Close=閉じる ConfirmReset=すべての設定をリセットしてもよろしいですか? Donate=寄付 -Exit=出口 +Exit=終了 File=_ファイル FileExecutionError=ファイルを開けません! Fireplace=暖炉 -General=全般的 +General=全般 Help=ヘルプ HelpFileError=ヘルプ ファイルを開けません! HelpMenu=_ヘルプ @@ -51,17 +51,17 @@ Wind=風 River=川 Clock=時計 Static=静的 -Other=他の +Other=その他 Timer=タイマー Enabled=有効 -Delay=遅れ +Delay=遅延 Seconds=秒 Minutes=分 Hours=時間 TimerDelayTooSmall=タイマー遅延は 1 より小さくすることはできません! Fantasy=ファンタジー Fan=ファン -TimerApplicationShutdown=出口 Opal +TimerApplicationShutdown=Opal を終了 Cave=洞窟 Frogs=カエル Zen=禅 @@ -71,25 +71,26 @@ Audiences=オーディエンス NetworkingEvent=ネットワーキングイベント TribalFestival=部族のお祭り RugbyFootball=ラグビーフットボール -Sleepy=眠いです +Sleepy=眠り DrumTribalFestival=太鼓部族祭 Gong=ゴング MediaButtons=メディア ボタン DragDrop=ファイルをドラッグ アンド ドロップする Theme=テーマ -Light=光 -Dark=暗い -NordLight=ノルドライト -NordDark=ノルド・ダーク -Space=スペース +Space=宇宙 Restaurant=レストラン Cancel=キャンセル Display=画面 TrayIcon=トレイアイコン TrayIconError=トレイ アイコンを作成できません! Ocean=海洋 -Train=訓練 +Train=列車 WhiteNoise=ホワイトノイズ -RadioFrequencyStatic=ラジオ周波数の静電気 +RadioFrequencyStatic=ラジオのノイズ PinkNoise=ピンクノイズ BrownNoise=ブラウンノイズ +TimerComputerShutdown=コンピューターをシャットダウン +AudioBalance=オーディオ バランス +Advanced=高度な設定 +Seagulls=カモメ +Belltower=ベルタワー diff --git a/src/main/resources/translations/OpalApplication_nl_NL.properties b/src/main/resources/translations/OpalApplication_nl_NL.properties index e316756..e26aba1 100644 --- a/src/main/resources/translations/OpalApplication_nl_NL.properties +++ b/src/main/resources/translations/OpalApplication_nl_NL.properties @@ -1,5 +1,5 @@ About=Over -AboutText=Opal is gemaakt door DeadLine\n\nAudio: ZapSplat.com\nAfbeeldingen: Remix Icon\nThema: AtlantaFX\nVersie: 1.1.0\n\nCopyright © 2023 CodeDead +AboutText=Opal is gemaakt door DeadLine\n\nAudio: ZapSplat.com\nAfbeeldingen: Remix Icon\nThema: AtlantaFX\nVersie: 1.2.0\n\nCopyright © 2023 CodeDead AboutWindowError=Kan het Over venster niet openen! AboutWindowTitle=Opal - Over AutoUpdate=Automatisch controleren op updates @@ -50,7 +50,7 @@ WebsiteError=Kan website niet openen! Wind=Wind River=Rivier Clock=Klok -Static=Witte ruis +Static=Ruis Other=Varia Timer=Timer Enabled=Actief @@ -77,10 +77,6 @@ Gong=Gong MediaButtons=Mediaknoppen DragDrop=Bestanden slepen en neerzetten Theme=Thema -Light=Licht -Dark=Donker -NordLight=Nord licht -NordDark=Nord donker Space=De ruimte Restaurant=Restaurant Cancel=Annuleren @@ -93,3 +89,8 @@ WhiteNoise=Wit geluid RadioFrequencyStatic=Radiofrequentie ruis PinkNoise=Roze geluid BrownNoise=Bruin geluid +TimerComputerShutdown=Computer afsluiten +AudioBalance=Audio balans +Advanced=Geavanceerd +Seagulls=Meeuwen +Belltower=Klokkentoren diff --git a/src/main/resources/translations/OpalApplication_ru_RU.properties b/src/main/resources/translations/OpalApplication_ru_RU.properties index 60204c8..36a97ac 100644 --- a/src/main/resources/translations/OpalApplication_ru_RU.properties +++ b/src/main/resources/translations/OpalApplication_ru_RU.properties @@ -1,5 +1,5 @@ About=О -AboutText=Opal был создан DeadLine\n\nАудио: ZapSplat.com\nИзображения: Remix Icon\nТема: AtlantaFX\nВерсия: 1.1.0\n\nАвторские права © 2023 CodeDead +AboutText=Opal был создан DeadLine\n\nАудио: ZapSplat.com\nИзображения: Remix Icon\nТема: AtlantaFX\nВерсия: 1.2.0\n\nАвторские права © 2023 CodeDead AboutWindowError=Не удается открыть окно «О программе»! AboutWindowTitle=Opal - О компании AutoUpdate=Автоматически проверять наличие обновлений @@ -77,10 +77,6 @@ Gong=Гонг MediaButtons=Медиа-кнопки DragDrop=Перетащите файлы Theme=Тема -Light=Легкий -Dark=Темный -NordLight=Норд лайт -NordDark=Норд темный Space=Космос Restaurant=Ресторан Cancel=Отмена @@ -93,3 +89,8 @@ WhiteNoise=Белый шум RadioFrequencyStatic=Радиочастотный статический PinkNoise=Розовый шум BrownNoise=Коричневый шум +TimerComputerShutdown=Выключение компьютера +AudioBalance=Баланс аудио +Advanced=Дополнительно +Seagulls=Чайки +Belltower=Колокольня diff --git a/src/main/resources/translations/OpalApplication_tr_TR.properties b/src/main/resources/translations/OpalApplication_tr_TR.properties new file mode 100644 index 0000000..9b37003 --- /dev/null +++ b/src/main/resources/translations/OpalApplication_tr_TR.properties @@ -0,0 +1,96 @@ +About=Hakkında +AboutText=Opal, DeadLine tarafından oluşturuldu\n\nSes: ZapSplat.com\nGörüntüler: Remix Simgesi\nTema: AtlantaFX\nSürüm: 1.2.0\n\nTelif hakkı © 2023 CodeDead +AboutWindowError=Hakkında Penceresi açılamıyor! +AboutWindowTitle=Opal - Hakkında +AutoUpdate=Güncellemeleri otomatik olarak kontrol et +Birds=Kuşlar +Chatter=Gevezelik +CheckForUpdates=Güncellemeleri kontrol et +Close=Kapalı +ConfirmReset=Tüm ayarları sıfırlamak istediğinizden emin misiniz? +Donate=Bağış yapmak +Exit=Çıkış +File=_Dosya +FileExecutionError=Dosya açılamıyor! +Fireplace=Şömine +General=Genel +Help=Yardım +HelpFileError=Yardım dosyası açılamıyor! +HelpMenu=_Yardım +Homepage=Anasayfa +Language=Dil +License=Lisans +LicenseFileError=Lisans dosyası açılamıyor! +LogLevel=Günlük düzeyi +MainWindowTitle=Opal +Nature=Doğa +NewUpdateAvailable=Sürüm {v} artık mevcut. Bu güncellemeyi indirmek ister misiniz? +NoUpdateAvailable=Güncelleme yok! +Office=Ofis +OpenSoundPreset=Ses ön ayarını aç +OpenSoundPresetError=Ses ön ayarı açılamıyor! +Phone=Telefon +Rain=Yağmur +Reset=Sıfırla +ResetSettingsError=Tüm ayarlar sıfırlanamıyor! +RestartRequired=Dili değiştirmek için yeniden başlatma gereklidir! +Save=Kaydetmek +SaveSettingsError=Ayarlar kaydedilemiyor! +SaveSoundPreset=Ses ayarlarını kaydet +SaveSoundPresetError=Ses ayarları kaydedilemiyor! +Settings=Ayarlar +SettingsWindowError=Ayarlar Penceresi açılamıyor! +SettingsWindowTitle=Opal - Ayarlar +Thunder=Gök gürültüsü +Tools=_Aletler +Traffic=Trafik +Typing=Yazıyor +UpdateError=Güncellemeler kontrol edilemiyor! +WebsiteError=Web sitesi açılamıyor! +Wind=Rüzgâr +River=Nehir +Clock=Saat +Static=Statik +Other=Diğer +Timer=Zamanlayıcı +Enabled=Etkinleştirilmiş +Delay=Gecikme +Seconds=Saniye(ler) +Minutes=Dakika(lar) +Hours=Saat(ler) +TimerDelayTooSmall=Zamanlayıcı gecikmesi 1'den küçük olamaz! +Fantasy=Fantezi +Fan=Fan +TimerApplicationShutdown=Opal'dan Çık +Cave=Mağara +Frogs=Kurbağa +Zen=Zen +Coffee=Kahve +Zoo=Hayvanat bahçesi +Audiences=İzleyiciler +NetworkingEvent=Ağ oluşturma olayı +TribalFestival=Kabile festivali +RugbyFootball=Ragbi futbolu +Sleepy=Uykulu +DrumTribalFestival=Davul kabile festivali +Gong=Gong +MediaButtons=Medya düğmeleri +DragDrop=Dosyaları sürükleyip bırakın +Theme=Tema +Space=Uzay +Restaurant=Restoran +Cancel=İptal etmek +Display=Görüntülemek +TrayIcon=Tepsi ikonu +TrayIconError=Tepsi simgesi oluşturulamıyor! +Ocean=Okyanus +Train=Tren +WhiteNoise=Beyaz gürültü +RadioFrequencyStatic=Radyo frekansı statik +PinkNoise=Pembe gürültü +BrownNoise=Kahverengi gürültü +TimerComputerShutdown=Bilgisayarı Kapat +AudioBalance=Ses dengesi +Advanced=Gelişmiş +Seagulls=Martılar +Belltower=Çan kulesi diff --git a/src/main/resources/translations/OpalApplication_zh_CN.properties b/src/main/resources/translations/OpalApplication_zh_CN.properties new file mode 100644 index 0000000..2a5c822 --- /dev/null +++ b/src/main/resources/translations/OpalApplication_zh_CN.properties @@ -0,0 +1,96 @@ +About=关于 +AboutText=Opal 由 DeadLine 创建\n\n音频:ZapSplat.com\n图像:Remix Icon\n主题:AtlantaFX\n版本:1.2.0\n\n版权所有 © 2023 CodeDead +AboutWindowError=无法打开“关于”窗口! +AboutWindowTitle=Opal - 关于 +AutoUpdate=自动检查更新 +Birds=鸟类 +Chatter=喋喋不休 +CheckForUpdates=检查更新 +Close=关闭 +ConfirmReset=您确定要重置所有设置吗? +Donate=捐 +Exit=出口 +File=_文件 +FileExecutionError=无法打开文件! +Fireplace=壁炉 +General=一般的 +Help=帮助 +HelpFileError=无法打开帮助文件! +HelpMenu=_帮助 +Homepage=主页 +Language=语言 +License=执照 +LicenseFileError=无法打开许可证文件! +LogLevel=日志级别 +MainWindowTitle=Opal +Nature=自然 +NewUpdateAvailable=版本 {v} 现已推出。 您想下载此更新吗? +NoUpdateAvailable=没有可用更新! +Office=办公室 +OpenSoundPreset=打开声音预设 +OpenSoundPresetError=无法打开声音预设! +Phone=电话 +Rain=雨 +Reset=重置 +ResetSettingsError=无法重置所有设置! +RestartRequired=需要重新启动才能更改语言! +Save=节省 +SaveSettingsError=无法保存设置! +SaveSoundPreset=保存声音设置 +SaveSoundPresetError=无法保存声音设置! +Settings=设置 +SettingsWindowError=无法打开设置窗口! +SettingsWindowTitle=Opal - 设置 +Thunder=雷 +Tools=_工具 +Traffic=交通 +Typing=打字 +UpdateError=无法检查更新! +WebsiteError=无法打开网站! +Wind=风 +River=河 +Clock=钟 +Static=静止的 +Other=其他 +Timer=计时器 +Enabled=启用 +Delay=延迟 +Seconds=第二次 +Minutes=分钟 +Hours=小时 +TimerDelayTooSmall=定时器延迟不能小于1! +Fantasy=幻想 +Fan=扇子 +TimerApplicationShutdown=退出应用程序 +Cave=洞穴 +Frogs=青蛙 +Zen=禅 +Coffee=咖啡 +Zoo=动物园 +Audiences=观众 +NetworkingEvent=社交活动 +TribalFestival=部落节日 +RugbyFootball=橄榄球 +Sleepy=困 +DrumTribalFestival=鼓部落节 +Gong=锣 +MediaButtons=媒体按钮 +DragDrop=拖放文件 +Theme=主题 +Space=空间 +Restaurant=餐厅 +Cancel=取消 +Display=展示 +TrayIcon=任务栏图标 +TrayIconError=无法创建托盘图标! +Ocean=海洋 +Train=火车 +WhiteNoise=白噪声 +RadioFrequencyStatic=射频静电 +PinkNoise=粉红噪声 +BrownNoise=布朗噪声 +TimerComputerShutdown=关闭电脑 +AudioBalance=音频平衡 +Advanced=先进的 +Seagulls=海鸥 +Belltower=钟楼 diff --git a/src/main/resources/windows/MainWindow.fxml b/src/main/resources/windows/MainWindow.fxml index 159a487..017b843 100644 --- a/src/main/resources/windows/MainWindow.fxml +++ b/src/main/resources/windows/MainWindow.fxml @@ -20,9 +20,10 @@ + - + @@ -55,7 +56,7 @@ - + @@ -63,7 +64,7 @@ - + @@ -72,7 +73,7 @@ - + @@ -85,11 +86,11 @@ - + - + @@ -165,7 +166,7 @@