From 5b63c3b5faabe41ec241b0f71c77367cc09a3f58 Mon Sep 17 00:00:00 2001 From: CodeDead Date: Fri, 30 Oct 2026 22:10:22 +0100 Subject: [PATCH 1/2] feat: enhance playback control and custom sound handling - Added `isPlaying` method in `SoundPane` to improve playback status handling. - Unified play/pause logic for all sound panes, including custom sounds, in `MainWindowController`. - Optimized countdown timer logic for improved accuracy and clarity. - Added keyboard shortcut (Ctrl+O) for "Open Sound Preset" menu action in MainWindow. --- .../opal/controller/MainWindowController.java | 43 +++++++++++++------ .../com/codedead/opal/domain/SoundPane.java | 17 +++++++- src/main/resources/windows/MainWindow.fxml | 3 +- 3 files changed, 47 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/codedead/opal/controller/MainWindowController.java b/src/main/java/com/codedead/opal/controller/MainWindowController.java index 4f4f547..5f65d12 100644 --- a/src/main/java/com/codedead/opal/controller/MainWindowController.java +++ b/src/main/java/com/codedead/opal/controller/MainWindowController.java @@ -39,6 +39,7 @@ import java.nio.file.Path; import java.security.GeneralSecurityException; import java.util.*; +import java.util.concurrent.TimeUnit; import static com.codedead.opal.utils.SharedVariables.DEFAULT_LOCALE; @@ -477,6 +478,18 @@ private List getAllSoundPanes() { return elements; } + /** + * Get all {@link SoundPane} objects, including custom sounds + * + * @return The {@link List} of all {@link SoundPane} objects + */ + private List getAllSoundPanesIncludingCustom() { + final List elements = new ArrayList<>(getAllSoundPanes()); + elements.addAll(getSoundPanes(grpCustom)); + + return elements; + } + /** * Get all {@link SoundPane} objects from a {@link GridPane} object * @@ -828,13 +841,17 @@ private void saveSoundPresetAction() { @FXML private void playPauseAction() { logger.info("Play / pause all media"); - try { - for (final SoundPane soundPane : getAllSoundPanes()) { - soundPane.playPause(); - } - for (final SoundPane soundPane : getSoundPanes(grpCustom)) { - soundPane.playPause(); + final List soundPanes = getAllSoundPanesIncludingCustom(); + final boolean pauseAll = soundPanes.stream().anyMatch(SoundPane::isPlaying); + + try { + for (final SoundPane soundPane : soundPanes) { + if (pauseAll) { + soundPane.pause(); + } else if (soundPane.getSlider().getValue() > 0) { + soundPane.play(); + } } } catch (final MediaPlayerException ex) { logger.error("Unable to play / pause MediaPlayer", ex); @@ -1145,18 +1162,18 @@ public void run() { }; countDownTask = new TimerTask() { - final long seconds = delay / 1000; - int i = 0; + private final long startNanos = System.nanoTime(); @Override public void run() { - i++; - long timeLeft = (seconds - (i % seconds)); + final long elapsedMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos); + final long remainingMillis = Math.max(0, delay - elapsedMillis); + final long timeLeft = remainingMillis / 1000; // Calculate hours, minutes and seconds - long hours = timeLeft / 3600; - long minutes = (timeLeft % 3600) / 60; - long seconds = timeLeft % 60; + final long hours = timeLeft / 3600; + final long minutes = (timeLeft % 3600) / 60; + final long seconds = timeLeft % 60; // Format the values to HH:MM:SS with leading zeros if necessary final String timeLeftFormatted = String.format("%02d:%02d:%02d", hours, minutes, seconds); diff --git a/src/main/java/com/codedead/opal/domain/SoundPane.java b/src/main/java/com/codedead/opal/domain/SoundPane.java index bd985f0..3be9b5f 100644 --- a/src/main/java/com/codedead/opal/domain/SoundPane.java +++ b/src/main/java/com/codedead/opal/domain/SoundPane.java @@ -296,13 +296,26 @@ public void pause() { */ @FXML public void playPause() throws MediaPlayerException { - if (mediaPlayer != null && mediaPlayer.getStatus() == MediaPlayer.Status.PLAYING) { + if (isPlaying()) { pause(); - } else if (mediaPlayer != null && mediaPlayer.getVolume() != 0) { + } else if (sldVolume.getValue() > 0) { play(); } } + /** + * Check whether the {@link MediaPlayer} object is currently playing + * + * @return True if the media is playing or stalled, otherwise false + */ + public boolean isPlaying() { + if (mediaPlayer == null) + return false; + + final MediaPlayer.Status status = mediaPlayer.getStatus(); + return status == MediaPlayer.Status.PLAYING || status == MediaPlayer.Status.STALLED; + } + /** * Dispose of the {@link MediaPlayer} object and all bindings */ diff --git a/src/main/resources/windows/MainWindow.fxml b/src/main/resources/windows/MainWindow.fxml index 9b04f55..38387e1 100644 --- a/src/main/resources/windows/MainWindow.fxml +++ b/src/main/resources/windows/MainWindow.fxml @@ -42,7 +42,8 @@ - + From 0f7331bbf7308a49ed623711e85022e1b3d8ac40 Mon Sep 17 00:00:00 2001 From: CodeDead Date: Fri, 30 Oct 2026 22:34:12 +0100 Subject: [PATCH 2/2] feat: add "Fastest Metronome" sound option and improve settings validation - Introduced the "Fastest Metronome" sound option, including UI, translations, and functionality. - Enhanced settings validation logic, including proxy configuration and timer delay checks. - Added `SettingsControllerTest` to ensure reliable property handling and validation. - Make sounds play at 100% volume when pressing the play button, and the audio is set to zero to improve the user experience --- .../opal/controller/MainWindowController.java | 3 + .../opal/controller/SettingsController.java | 17 +- .../controller/SettingsWindowController.java | 373 ++++++++++++++---- .../com/codedead/opal/domain/SoundPane.java | 7 +- src/main/resources/audio/metronomefastest.mp3 | Bin 0 -> 44642 bytes .../translations/OpalApplication.properties | 2 + .../OpalApplication_de_DE.properties | 2 + .../OpalApplication_en_US.properties | 2 + .../OpalApplication_es_ES.properties | 2 + .../OpalApplication_fr_FR.properties | 2 + .../OpalApplication_hi_IN.properties | 2 + .../OpalApplication_ja_JP.properties | 2 + .../OpalApplication_nl_NL.properties | 2 + .../OpalApplication_ru_RU.properties | 2 + .../OpalApplication_tr_TR.properties | 2 + .../OpalApplication_uk_UA.properties | 2 + .../OpalApplication_zh_CN.properties | 2 + src/main/resources/windows/MainWindow.fxml | 4 + .../controller/SettingsControllerTest.java | 59 +++ 19 files changed, 412 insertions(+), 75 deletions(-) create mode 100644 src/main/resources/audio/metronomefastest.mp3 create mode 100644 src/test/java/com/codedead/opal/controller/SettingsControllerTest.java diff --git a/src/main/java/com/codedead/opal/controller/MainWindowController.java b/src/main/java/com/codedead/opal/controller/MainWindowController.java index 5f65d12..eab2f25 100644 --- a/src/main/java/com/codedead/opal/controller/MainWindowController.java +++ b/src/main/java/com/codedead/opal/controller/MainWindowController.java @@ -48,6 +48,8 @@ public final class MainWindowController implements IAudioTimer, TrayIconListener @FXML private SoundPane snpFastMetronome; @FXML + private SoundPane snpFastestMetronome; + @FXML private SoundPane snpSlowMetronome; @FXML private SoundPane snpTrain; @@ -355,6 +357,7 @@ public void setControllers(final SettingsController settingsController, final Up snpTrain.getNameLabel().textProperty().bind(resourceFactory.getStringBinding("Train")); snpSlowMetronome.getNameLabel().textProperty().bind(resourceFactory.getStringBinding("SlowMetronome")); snpFastMetronome.getNameLabel().textProperty().bind(resourceFactory.getStringBinding("FastMetronome")); + snpFastestMetronome.getNameLabel().textProperty().bind(resourceFactory.getStringBinding("FastestMetronome")); } /** diff --git a/src/main/java/com/codedead/opal/controller/SettingsController.java b/src/main/java/com/codedead/opal/controller/SettingsController.java index cd1da93..1d0f352 100644 --- a/src/main/java/com/codedead/opal/controller/SettingsController.java +++ b/src/main/java/com/codedead/opal/controller/SettingsController.java @@ -141,10 +141,25 @@ public void setProperties(final Properties properties) { * @throws IOException When the Properties object could not be stored */ public void saveProperties() throws IOException { + saveProperties(properties); + } + + /** + * Save the provided properties and replace the in-memory properties once the write succeeds + * + * @param newProperties The properties that should be stored + * @throws IOException When the Properties object could not be stored + */ + public void saveProperties(final Properties newProperties) throws IOException { + if (newProperties == null) + throw new NullPointerException("Properties cannot be null!"); + logger.info("Storing the Properties object"); try (final FileOutputStream fos = new FileOutputStream(getPropertiesFileLocation())) { - properties.store(fos, null); + newProperties.store(fos, null); } + + properties = newProperties; } /** diff --git a/src/main/java/com/codedead/opal/controller/SettingsWindowController.java b/src/main/java/com/codedead/opal/controller/SettingsWindowController.java index dfc2e9f..89a2bab 100644 --- a/src/main/java/com/codedead/opal/controller/SettingsWindowController.java +++ b/src/main/java/com/codedead/opal/controller/SettingsWindowController.java @@ -25,9 +25,12 @@ import org.apache.logging.log4j.core.config.Configurator; import java.io.IOException; +import java.io.Serial; import java.security.GeneralSecurityException; import java.util.Locale; +import java.util.MissingResourceException; import java.util.Objects; +import java.util.Properties; import java.util.ResourceBundle; import java.util.concurrent.TimeUnit; @@ -268,79 +271,231 @@ private void resetSettingsAction() { private void saveSettingsAction() { logger.info("Attempting to save all settings"); + final SettingsDraft draft; + final ResourceBundle newTranslationBundle; + try { + draft = readDraft(); + final Locale locale = LanguageController.getLocaleFromLanguageTag(draft.localeTag()); + newTranslationBundle = ResourceBundle.getBundle("translations.OpalApplication", locale); + } catch (final SettingsValidationException ex) { + logger.warn("Settings validation failed: {}", ex.getMessageKey()); + restoreSavedTheme(); + showSaveError(translationBundle.getString(ex.getMessageKey()), ex.getCause()); + return; + } catch (final MissingResourceException ex) { + restoreSavedTheme(); + showSaveError(ex.toString(), ex); + return; + } + + try { + settingsController.saveProperties(draft.toProperties(settingsController.getProperties())); + } catch (final IOException ex) { + restoreSavedTheme(); + showSaveError(ex.toString(), ex); + return; + } + + translationBundle = newTranslationBundle; + applySettings(draft, newTranslationBundle); + + final Stage stage = (Stage) sldAudioBalance.getScene().getWindow(); + stage.close(); + } + + /** + * Read and validate all settings without changing any application state + * + * @return The validated settings + * @throws SettingsValidationException When one of the settings is invalid + */ + private SettingsDraft readDraft() throws SettingsValidationException { final boolean updateProxyEnabled = chbUpdateProxy.isSelected(); - final String updateProxyHost = txtProxyHost.getText() == null ? "" : txtProxyHost.getText().trim(); - final String updateProxyPortValue = numProxyPort.getText() == null ? "" : numProxyPort.getText().trim(); + final String updateProxyHost = valueOrEmpty(txtProxyHost.getText()).trim(); + final String updateProxyPortValue = valueOrEmpty(numProxyPort.getText()).trim(); final boolean updateProxyAuthEnabled = chbProxyAuthentication.isSelected(); - final String updateProxyUsername = txtProxyUsername.getText() == null ? "" : txtProxyUsername.getText().trim(); - final String updateProxyPassword = pfProxyPassword.getText() == null ? "" : pfProxyPassword.getText(); - int updateProxyPort = 8080; + final String updateProxyUsername = valueOrEmpty(txtProxyUsername.getText()).trim(); + final String updateProxyPassword = valueOrEmpty(pfProxyPassword.getText()); - if (!updateProxyPortValue.isBlank()) { - try { - updateProxyPort = Integer.parseInt(updateProxyPortValue); - } catch (final NumberFormatException ex) { - if (updateProxyEnabled) { - logger.warn("Invalid proxy port: {}", updateProxyPortValue); - FxUtils.showErrorAlert(translationBundle.getString("SaveSettingsError"), translationBundle.getString("InvalidProxySettings"), getClass().getResourceAsStream(SharedVariables.ICON_URL)); - return; - } - } + final int updateProxyPort = parseProxyPort(updateProxyPortValue, updateProxyEnabled); + + if (updateProxyEnabled && updateProxyHost.isBlank()) { + throw new SettingsValidationException("InvalidProxySettings"); } - if (updateProxyEnabled && (updateProxyHost.isBlank() || updateProxyPort < 1 || updateProxyPort > 65535)) { - logger.warn("Invalid proxy settings: {}:{}", updateProxyHost, updateProxyPort); - FxUtils.showErrorAlert(translationBundle.getString("SaveSettingsError"), translationBundle.getString("InvalidProxySettings"), getClass().getResourceAsStream(SharedVariables.ICON_URL)); - return; + if (updateProxyEnabled && updateProxyAuthEnabled + && (updateProxyUsername.isBlank() || updateProxyPassword.isEmpty())) { + throw new SettingsValidationException("InvalidProxyAuthentication"); } - if (updateProxyEnabled && updateProxyAuthEnabled && (updateProxyUsername.isBlank() || updateProxyPassword.isEmpty())) { - logger.warn("Invalid proxy authentication settings"); - FxUtils.showErrorAlert(translationBundle.getString("SaveSettingsError"), translationBundle.getString("InvalidProxyAuthentication"), getClass().getResourceAsStream(SharedVariables.ICON_URL)); - return; + final long delay = parseTimerDelay(numDelay.getText()); + final int delayType = cboDelayType.getSelectionModel().getSelectedIndex(); + + if (delayType < 0 || delayType > 2) { + throw new SettingsValidationException("InvalidTimerDelay"); + } + + final long timerDelay = switch (delayType) { + case 0 -> TimeUnit.SECONDS.toMillis(delay); + case 1 -> TimeUnit.MINUTES.toMillis(delay); + case 2 -> TimeUnit.HOURS.toMillis(delay); + default -> throw new SettingsValidationException("InvalidTimerDelay"); + }; + + final String languageTag = LanguageController.getLocaleFromLanguageIndex(cboLanguage.getSelectionModel().getSelectedIndex()); + final String theme = cboTheme.getSelectionModel().getSelectedItem(); + final String logLevel = cboLogLevel.getValue(); + + if (theme == null || logLevel == null) { + throw new SettingsValidationException("SaveSettingsError"); + } + + final double audioBalance = sldAudioBalance.getValue(); + if (!Double.isFinite(audioBalance) || audioBalance < -1.0 || audioBalance > 1.0) { + throw new SettingsValidationException("SaveSettingsError"); } final String storedProxyPassword; try { storedProxyPassword = passwordCipher.encrypt(updateProxyPassword); } catch (final IOException | GeneralSecurityException ex) { - logger.error("Unable to encrypt the proxy password", ex); - FxUtils.showErrorAlert(translationBundle.getString("SaveSettingsError"), ex.toString(), getClass().getResourceAsStream(SharedVariables.ICON_URL)); - return; + throw new SettingsValidationException("SaveSettingsError", ex); } - settingsController.getProperties().setProperty("autoUpdate", Boolean.toString(chbAutoUpdate.isSelected())); - settingsController.getProperties().setProperty("updateProxyEnabled", Boolean.toString(updateProxyEnabled)); - settingsController.getProperties().setProperty("updateProxyHost", updateProxyHost); - settingsController.getProperties().setProperty("updateProxyPort", String.valueOf(updateProxyPort)); - settingsController.getProperties().setProperty("updateProxyAuthEnabled", Boolean.toString(updateProxyAuthEnabled)); - settingsController.getProperties().setProperty("updateProxyUsername", updateProxyUsername); - settingsController.getProperties().setProperty("updateProxyPassword", storedProxyPassword); - settingsController.getProperties().setProperty("mediaButtons", Boolean.toString(chbMediaButtons.isSelected())); - settingsController.getProperties().setProperty("dragDrop", Boolean.toString(chbDragDrop.isSelected())); - settingsController.getProperties().setProperty("trayIcon", Boolean.toString(chbTrayIcon.isSelected())); + return new SettingsDraft( + chbAutoUpdate.isSelected(), + updateProxyEnabled, + updateProxyHost, + updateProxyPort, + updateProxyAuthEnabled, + updateProxyUsername, + storedProxyPassword, + chbMediaButtons.isSelected(), + chbDragDrop.isSelected(), + chbTrayIcon.isSelected(), + languageTag, + theme, + logLevel, + timerDelay, + delayType, + chbTimerApplicationShutdown.isSelected(), + chbTimerComputerShutdown.isSelected(), + audioBalance + ); + } - final String languageTag = LanguageController.getLocaleFromLanguageIndex(cboLanguage.getSelectionModel().getSelectedIndex()); - final Locale locale = LanguageController.getLocaleFromLanguageTag(languageTag); - final ResourceBundle newTranslationBundle = ResourceBundle.getBundle("translations.OpalApplication", locale); + /** + * Parse a proxy port + * + * @param value The value of the proxy port field + * @param proxyEnabled True if the proxy is enabled + * @return The proxy port, or the default port if the value is empty + * @throws SettingsValidationException When the port is invalid and the proxy is enabled + */ + private int parseProxyPort(final String value, final boolean proxyEnabled) throws SettingsValidationException { + if (value.isBlank()) + return 8080; + + try { + final int port = Integer.parseInt(value); + + if (proxyEnabled && (port < 1 || port > 65535)) { + throw new SettingsValidationException("InvalidProxySettings"); + } + + return port; + } catch (final NumberFormatException ex) { + if (proxyEnabled) { + throw new SettingsValidationException("InvalidProxySettings", ex); + } + + return 8080; + } + } + + /** + * Parse the timer delay + * + * @param value The value of the timer delay field + * @return The timer delay + * @throws SettingsValidationException When the delay is missing, invalid or smaller than 1 + */ + private long parseTimerDelay(final String value) throws SettingsValidationException { + if (value == null || value.isBlank()) { + throw new SettingsValidationException("InvalidTimerDelay"); + } + + try { + final long delay = Long.parseLong(value.trim()); + + if (delay < 1) { + throw new SettingsValidationException("TimerDelayTooSmall"); + } + + return delay; + } catch (final NumberFormatException ex) { + throw new SettingsValidationException("InvalidTimerDelay", ex); + } + } + + /** + * Display an error that occurred while saving settings + * + * @param message The message that should be displayed + * @param cause The cause of the error, if any + */ + private void showSaveError(final String message, final Throwable cause) { + if (cause != null) { + logger.error("Unable to save settings", cause); + } - this.translationBundle = newTranslationBundle; + FxUtils.showErrorAlert(translationBundle.getString("SaveSettingsError"), message, getClass().getResourceAsStream(SharedVariables.ICON_URL)); + } + + /** + * Restore the theme that is currently stored in the settings + */ + private void restoreSavedTheme() { + final String savedTheme = settingsController.getProperties().getProperty("theme", "light"); + final int savedThemeIndex = ThemeController.getThemeIndex(savedTheme); + + if (cboTheme.getSelectionModel().getSelectedIndex() != savedThemeIndex) { + cboTheme.getSelectionModel().select(savedThemeIndex); + } + + ThemeController.setTheme(savedTheme); + } + + /** + * Apply the validated settings to the running application + * + * @param draft The validated settings + * @param newTranslationBundle The translation bundle for the selected language + */ + private void applySettings(final SettingsDraft draft, final ResourceBundle newTranslationBundle) { mainWindowController.updateResourceBundle(newTranslationBundle); + mainWindowController.loadMediaButtonVisibility(draft.mediaButtons()); + mainWindowController.setAudioBalance(draft.audioBalance()); - settingsController.getProperties().setProperty("locale", languageTag); + ThemeController.setTheme(draft.theme()); + Configurator.setAllLevels(LogManager.getRootLogger().getName(), toLogLevel(draft.logLevel())); - mainWindowController.loadMediaButtonVisibility(chbMediaButtons.isSelected()); try { - mainWindowController.setTrayIconEnabled(chbTrayIcon.isSelected()); + mainWindowController.setTrayIconEnabled(draft.trayIcon()); } catch (final IOException ex) { logger.error("Unable to create tray icon", ex); FxUtils.showErrorAlert(newTranslationBundle.getString("TrayIconError"), ex.toString(), getClass().getResourceAsStream(SharedVariables.ICON_URL)); } + } - settingsController.getProperties().setProperty("theme", cboTheme.getSelectionModel().getSelectedItem()); - settingsController.getProperties().setProperty("loglevel", cboLogLevel.getValue()); - - final Level level = switch (cboLogLevel.getValue()) { + /** + * Convert a log level name to a {@link Level} object + * + * @param logLevel The name of the log level + * @return The {@link Level} object + */ + private static Level toLogLevel(final String logLevel) { + return switch (logLevel) { case "OFF" -> Level.OFF; case "FATAL" -> Level.FATAL; case "ERROR" -> Level.ERROR; @@ -350,39 +505,113 @@ private void saveSettingsAction() { case "ALL" -> Level.ALL; default -> Level.INFO; }; + } - final int delayType = cboDelayType.getSelectionModel().getSelectedIndex(); - final long delay = Long.parseLong(numDelay.getText()); + /** + * Get a string value or an empty string when the value is null + * + * @param value The value + * @return The value or an empty string + */ + private static String valueOrEmpty(final String value) { + return value == null ? "" : value; + } - if (delay < 1) { - FxUtils.showErrorAlert(translationBundle.getString("SaveSettingsError"), translationBundle.getString("TimerDelayTooSmall"), getClass().getResourceAsStream(SharedVariables.ICON_URL)); - return; + /** + * A validated snapshot of all settings + */ + private record SettingsDraft( + boolean autoUpdate, + boolean updateProxyEnabled, + String updateProxyHost, + int updateProxyPort, + boolean updateProxyAuthEnabled, + String updateProxyUsername, + String encryptedProxyPassword, + boolean mediaButtons, + boolean dragDrop, + boolean trayIcon, + String localeTag, + String theme, + String logLevel, + long timerDelay, + int timerDelayType, + boolean timerApplicationShutdown, + boolean timerComputerShutdown, + double audioBalance + ) { + + /** + * Create a new {@link Properties} object based on the current settings and this draft + * + * @param base The current properties + * @return The new properties + */ + private Properties toProperties(final Properties base) { + final Properties properties = new Properties(); + properties.putAll(base); + + properties.setProperty("autoUpdate", Boolean.toString(autoUpdate)); + properties.setProperty("updateProxyEnabled", Boolean.toString(updateProxyEnabled)); + properties.setProperty("updateProxyHost", updateProxyHost); + properties.setProperty("updateProxyPort", Integer.toString(updateProxyPort)); + properties.setProperty("updateProxyAuthEnabled", Boolean.toString(updateProxyAuthEnabled)); + properties.setProperty("updateProxyUsername", updateProxyUsername); + properties.setProperty("updateProxyPassword", encryptedProxyPassword); + properties.setProperty("mediaButtons", Boolean.toString(mediaButtons)); + properties.setProperty("dragDrop", Boolean.toString(dragDrop)); + properties.setProperty("trayIcon", Boolean.toString(trayIcon)); + properties.setProperty("locale", localeTag); + properties.setProperty("theme", theme); + properties.setProperty("loglevel", logLevel); + properties.setProperty("timerDelay", Long.toString(timerDelay)); + properties.setProperty("timerDelayType", Integer.toString(timerDelayType)); + properties.setProperty("timerApplicationShutdown", Boolean.toString(timerApplicationShutdown)); + properties.setProperty("timerComputerShutdown", Boolean.toString(timerComputerShutdown)); + properties.setProperty("audioBalance", Double.toString(audioBalance)); + + return properties; } + } - final long correctDelay = switch (delayType) { - case 0 -> TimeUnit.SECONDS.toMillis(delay); - case 1 -> TimeUnit.MINUTES.toMillis(delay); - case 2 -> TimeUnit.HOURS.toMillis(delay); - default -> throw new IllegalStateException("Unexpected value: " + delayType); - }; + /** + * Exception that is thrown when a setting is invalid + */ + private static final class SettingsValidationException extends Exception { - 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())); + @Serial + private static final long serialVersionUID = 1L; - settingsController.getProperties().setProperty("audioBalance", String.valueOf(sldAudioBalance.getValue())); - mainWindowController.setAudioBalance(sldAudioBalance.getValue()); + private final String messageKey; - Configurator.setAllLevels(LogManager.getRootLogger().getName(), level); - try { - settingsController.saveProperties(); + /** + * Initialize a new SettingsValidationException + * + * @param messageKey The translation key of the error message + */ + private SettingsValidationException(final String messageKey) { + super(messageKey); + this.messageKey = messageKey; + } - Stage stage = (Stage) sldAudioBalance.getScene().getWindow(); - stage.close(); - } catch (final IOException ex) { - logger.error("Unable to save all settings", ex); - FxUtils.showErrorAlert(translationBundle.getString("SaveSettingsError"), ex.toString(), getClass().getResourceAsStream(SharedVariables.ICON_URL)); + /** + * Initialize a new SettingsValidationException + * + * @param messageKey The translation key of the error message + * @param cause The cause of the exception + */ + private SettingsValidationException(final String messageKey, final Throwable cause) { + super(messageKey, cause); + this.messageKey = messageKey; + } + + /** + * Get the translation key of the error message + * + * @return The translation key + */ + private String getMessageKey() { + return messageKey; } } diff --git a/src/main/java/com/codedead/opal/domain/SoundPane.java b/src/main/java/com/codedead/opal/domain/SoundPane.java index 3be9b5f..b8d9870 100644 --- a/src/main/java/com/codedead/opal/domain/SoundPane.java +++ b/src/main/java/com/codedead/opal/domain/SoundPane.java @@ -296,9 +296,12 @@ public void pause() { */ @FXML public void playPause() throws MediaPlayerException { - if (isPlaying()) { + if (sldVolume.getValue() <= 0) { + // The volume listener starts playback when the slider moves above zero + sldVolume.setValue(1.0); + } else if (isPlaying()) { pause(); - } else if (sldVolume.getValue() > 0) { + } else { play(); } } diff --git a/src/main/resources/audio/metronomefastest.mp3 b/src/main/resources/audio/metronomefastest.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..751adc9f19aaf1dc285a88d4096d8816b25d51a3 GIT binary patch literal 44642 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

#4DCC7NFCAutR)Z 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 V{vUt#|NJW}>M7_y{eRoQ{{fyBqlo|j literal 0 HcmV?d00001 diff --git a/src/main/resources/translations/OpalApplication.properties b/src/main/resources/translations/OpalApplication.properties index 4e43d4e..867c0d0 100644 --- a/src/main/resources/translations/OpalApplication.properties +++ b/src/main/resources/translations/OpalApplication.properties @@ -57,6 +57,7 @@ Seconds=Second(s) Minutes=Minute(s) Hours=Hour(s) TimerDelayTooSmall=Timer delay cannot be smaller than 1! +InvalidTimerDelay=Please enter a valid timer delay! Fantasy=Fantasy Fan=Fan TimerApplicationShutdown=Exit Opal @@ -94,6 +95,7 @@ Seagulls=Seagulls BellTower=Bell tower SlowMetronome=Metronome (slow) FastMetronome=Metronome (fast) +FastestMetronome=Metronome (fastest) PlayPause=Play / Pause PlayPauseError=Unable to play / pause! Dolphins=Dolphins diff --git a/src/main/resources/translations/OpalApplication_de_DE.properties b/src/main/resources/translations/OpalApplication_de_DE.properties index d46175a..c68dcc9 100644 --- a/src/main/resources/translations/OpalApplication_de_DE.properties +++ b/src/main/resources/translations/OpalApplication_de_DE.properties @@ -57,6 +57,7 @@ Seconds=Sekunde(n) Minutes=Minute(n) Hours=Stunde(n) TimerDelayTooSmall=Verzögerung darf nicht kleiner als 1 sein! +InvalidTimerDelay=Bitte geben Sie eine gültige Timer-Verzögerung ein! Fantasy=Fantasie Fan=Lüfter TimerApplicationShutdown=Opal beenden @@ -94,6 +95,7 @@ Seagulls=Möwen BellTower=Glockenturm SlowMetronome=Langsames Metronom FastMetronome=Schnelles Metronom +FastestMetronome=Schnellstes Metronom PlayPause=Abspielen / Pause PlayPauseError=Abspielen / Pause konnte nicht ausgeführt werden! Dolphins=Delfine diff --git a/src/main/resources/translations/OpalApplication_en_US.properties b/src/main/resources/translations/OpalApplication_en_US.properties index 4e43d4e..867c0d0 100644 --- a/src/main/resources/translations/OpalApplication_en_US.properties +++ b/src/main/resources/translations/OpalApplication_en_US.properties @@ -57,6 +57,7 @@ Seconds=Second(s) Minutes=Minute(s) Hours=Hour(s) TimerDelayTooSmall=Timer delay cannot be smaller than 1! +InvalidTimerDelay=Please enter a valid timer delay! Fantasy=Fantasy Fan=Fan TimerApplicationShutdown=Exit Opal @@ -94,6 +95,7 @@ Seagulls=Seagulls BellTower=Bell tower SlowMetronome=Metronome (slow) FastMetronome=Metronome (fast) +FastestMetronome=Metronome (fastest) PlayPause=Play / Pause PlayPauseError=Unable to play / pause! Dolphins=Dolphins diff --git a/src/main/resources/translations/OpalApplication_es_ES.properties b/src/main/resources/translations/OpalApplication_es_ES.properties index 14e0bfd..770e722 100644 --- a/src/main/resources/translations/OpalApplication_es_ES.properties +++ b/src/main/resources/translations/OpalApplication_es_ES.properties @@ -57,6 +57,7 @@ Seconds=Segundo(s) Minutes=Minuto(s) Hours=Hora(s) TimerDelayTooSmall=¡El retraso del temporizador no puede ser menor que 1! +InvalidTimerDelay=¡Introduce un retraso de temporizador válido! Fantasy=Fantasía Fan=Ventilador TimerApplicationShutdown=Salir de Opal @@ -94,6 +95,7 @@ Seagulls=Gaviotas BellTower=Campanario SlowMetronome=Metrónomo lento FastMetronome=Metrónomo rápido +FastestMetronome=Metrónomo muy rápido PlayPause=Reproducir / Pausa PlayPauseError=¡No se puede reproducir / pausar el sonido! Dolphins=Delfines diff --git a/src/main/resources/translations/OpalApplication_fr_FR.properties b/src/main/resources/translations/OpalApplication_fr_FR.properties index f08067f..725ae0e 100644 --- a/src/main/resources/translations/OpalApplication_fr_FR.properties +++ b/src/main/resources/translations/OpalApplication_fr_FR.properties @@ -57,6 +57,7 @@ Seconds=Second(s) Minutes=Minute(s) Hours=Heure(s) TimerDelayTooSmall=Le délai de la minuterie ne peut pas être inférieur à 1! +InvalidTimerDelay=Veuillez saisir un délai de minuterie valide ! Fantasy=Fantaisie Fan=Ventilateur TimerApplicationShutdown=Fermer Opal @@ -94,6 +95,7 @@ Seagulls=Mouettes BellTower=Clocher SlowMetronome=Métronome lent FastMetronome=Métronome rapide +FastestMetronome=Métronome très rapide PlayPause=Lecture / Pause PlayPauseError=Impossible de lire / mettre en pause! Dolphins=Dauphins diff --git a/src/main/resources/translations/OpalApplication_hi_IN.properties b/src/main/resources/translations/OpalApplication_hi_IN.properties index 39ffb20..c8eae90 100644 --- a/src/main/resources/translations/OpalApplication_hi_IN.properties +++ b/src/main/resources/translations/OpalApplication_hi_IN.properties @@ -57,6 +57,7 @@ Seconds=सेकंड Minutes=मिनट Hours=घंटे TimerDelayTooSmall=टाइमर विलंब 1 से कम नहीं हो सकता! +InvalidTimerDelay=कृपया एक मान्य टाइमर विलंब दर्ज करें! Fantasy=कल्पना Fan=पंखा TimerApplicationShutdown=Opal से बाहर निकलें @@ -94,6 +95,7 @@ Seagulls=सीगल BellTower=घंटी मीनार SlowMetronome=मेट्रोनोम (धीमा) FastMetronome=मेट्रोनोम (तेज़) +FastestMetronome=मेट्रोनोम (सबसे तेज़) PlayPause=चालू करे रोके PlayPauseError=चलाने/रोकने में असमर्थ! Dolphins=डाल्फिन diff --git a/src/main/resources/translations/OpalApplication_ja_JP.properties b/src/main/resources/translations/OpalApplication_ja_JP.properties index eaacb7c..d603769 100644 --- a/src/main/resources/translations/OpalApplication_ja_JP.properties +++ b/src/main/resources/translations/OpalApplication_ja_JP.properties @@ -57,6 +57,7 @@ Seconds=秒 Minutes=分 Hours=時間 TimerDelayTooSmall=タイマー遅延は 1 より小さくすることはできません! +InvalidTimerDelay=有効なタイマー遅延を入力してください。 Fantasy=ファンタジー Fan=ファン TimerApplicationShutdown=Opal を終了 @@ -94,6 +95,7 @@ Seagulls=カモメ BellTower=ベルタワー SlowMetronome=遅いメトロノーム FastMetronome=速いメトロノーム +FastestMetronome=最速メトロノーム PlayPause=再生/一時停止 PlayPauseError=再生/一時停止できません! Dolphins=イルカ diff --git a/src/main/resources/translations/OpalApplication_nl_NL.properties b/src/main/resources/translations/OpalApplication_nl_NL.properties index 7def498..5119ca9 100644 --- a/src/main/resources/translations/OpalApplication_nl_NL.properties +++ b/src/main/resources/translations/OpalApplication_nl_NL.properties @@ -57,6 +57,7 @@ Seconds=Second(en) Minutes=Minuut/Minuten Hours=Uur/Uren TimerDelayTooSmall=Tijdvertraging kan niet kleiner zijn dan 1! +InvalidTimerDelay=Voer een geldige timervertraging in! Fantasy=Fantasie Fan=Ventilator TimerApplicationShutdown=Opal afsluiten @@ -94,6 +95,7 @@ Seagulls=Meeuwen BellTower=Klokkentoren SlowMetronome=Metronoom (langzaam) FastMetronome=Metronoom (snel) +FastestMetronome=Metronoom (snelst) PlayPause=Afspelen / Pauzeren PlayPauseError=Kan niet afspelen / pauzeren! Dolphins=Dolfijnen diff --git a/src/main/resources/translations/OpalApplication_ru_RU.properties b/src/main/resources/translations/OpalApplication_ru_RU.properties index e971695..64b5b52 100644 --- a/src/main/resources/translations/OpalApplication_ru_RU.properties +++ b/src/main/resources/translations/OpalApplication_ru_RU.properties @@ -57,6 +57,7 @@ Seconds=Секунды Minutes=Минута(ы) Hours=Часы TimerDelayTooSmall=Задержка таймера не может быть меньше 1! +InvalidTimerDelay=Введите допустимую задержку таймера! Fantasy=Фантазия Fan=Вентилятор TimerApplicationShutdown=Выход Opal @@ -94,6 +95,7 @@ Seagulls=Чайки BellTower=Колокольня SlowMetronome=Медленный метроном FastMetronome=Быстрый метроном +FastestMetronome=Самый быстрый метроном PlayPause=Воспроизведение / Пауза PlayPauseError=Невозможно воспроизвести / приостановить воспроизведение! Dolphins=Дельфины diff --git a/src/main/resources/translations/OpalApplication_tr_TR.properties b/src/main/resources/translations/OpalApplication_tr_TR.properties index 18cff52..6987703 100644 --- a/src/main/resources/translations/OpalApplication_tr_TR.properties +++ b/src/main/resources/translations/OpalApplication_tr_TR.properties @@ -57,6 +57,7 @@ Seconds=Saniye(ler) Minutes=Dakika(lar) Hours=Saat(ler) TimerDelayTooSmall=Zamanlayıcı gecikmesi 1'den küçük olamaz! +InvalidTimerDelay=Lütfen geçerli bir zamanlayıcı gecikmesi girin! Fantasy=Fantezi Fan=Fan TimerApplicationShutdown=Opal'dan Çık @@ -94,6 +95,7 @@ Seagulls=Martılar BellTower=Çan kulesi SlowMetronome=Yavaş metronom FastMetronome=Hızlı metronom +FastestMetronome=En hızlı metronom PlayPause=Oynat / Duraklat PlayPauseError=Oynatma / duraklatma başarısız! Dolphins=Yunuslar diff --git a/src/main/resources/translations/OpalApplication_uk_UA.properties b/src/main/resources/translations/OpalApplication_uk_UA.properties index 25e757d..61200c2 100644 --- a/src/main/resources/translations/OpalApplication_uk_UA.properties +++ b/src/main/resources/translations/OpalApplication_uk_UA.properties @@ -57,6 +57,7 @@ Seconds=Секунда (и) Minutes=Хвилин(и) Hours=Година(и) TimerDelayTooSmall=Затримка таймера не може бути менше 1! +InvalidTimerDelay=Введіть припустиму затримку таймера! Fantasy=Фантазія Fan=Вентилятор TimerApplicationShutdown=Вихід з Opal @@ -94,6 +95,7 @@ Seagulls=Чайки BellTower=Дзвіниця SlowMetronome=Метроном (повільний) FastMetronome=Метроном (швидкий) +FastestMetronome=Найшвидший метроном PlayPause=Відтворення / Пауза PlayPauseError=Неможливо відтворити/призупинити! Dolphins=Дельфіни diff --git a/src/main/resources/translations/OpalApplication_zh_CN.properties b/src/main/resources/translations/OpalApplication_zh_CN.properties index e9dbff6..35efe79 100644 --- a/src/main/resources/translations/OpalApplication_zh_CN.properties +++ b/src/main/resources/translations/OpalApplication_zh_CN.properties @@ -57,6 +57,7 @@ Seconds=秒 Minutes=分钟 Hours=小时 TimerDelayTooSmall=定时器延迟不能小于1! +InvalidTimerDelay=请输入有效的定时器延迟! Fantasy=梦幻 Fan=风扇 TimerApplicationShutdown=退出Opal @@ -94,6 +95,7 @@ Seagulls=海鸥 BellTower=钟楼 SlowMetronome=节拍器(慢) FastMetronome=节拍器(快) +FastestMetronome=节拍器(最快) PlayPause=播放/暂停 PlayPauseError=无法播放/暂停! Dolphins=海豚 diff --git a/src/main/resources/windows/MainWindow.fxml b/src/main/resources/windows/MainWindow.fxml index 38387e1..64fd3ba 100644 --- a/src/main/resources/windows/MainWindow.fxml +++ b/src/main/resources/windows/MainWindow.fxml @@ -396,6 +396,7 @@ + @@ -432,6 +433,9 @@ + diff --git a/src/test/java/com/codedead/opal/controller/SettingsControllerTest.java b/src/test/java/com/codedead/opal/controller/SettingsControllerTest.java new file mode 100644 index 0000000..170c4ad --- /dev/null +++ b/src/test/java/com/codedead/opal/controller/SettingsControllerTest.java @@ -0,0 +1,59 @@ +package com.codedead.opal.controller; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Properties; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class SettingsControllerTest { + + @TempDir + private Path temporaryDirectory; + + @Test + void storesNewPropertiesAndReplacesTheLiveProperties() throws IOException { + final Path propertiesFile = temporaryDirectory.resolve("opal.properties"); + Files.writeString(propertiesFile, "locale=de-DE\ntheme=dark\n"); + + final SettingsController settingsController = new SettingsController(propertiesFile.toString(), "default.properties"); + final Properties updatedProperties = new Properties(); + updatedProperties.putAll(settingsController.getProperties()); + updatedProperties.setProperty("theme", "light"); + + settingsController.saveProperties(updatedProperties); + + assertEquals("light", settingsController.getProperties().getProperty("theme")); + assertEquals("de-DE", settingsController.getProperties().getProperty("locale")); + + final Properties storedProperties = settingsController.readPropertiesFile(); + assertEquals("light", storedProperties.getProperty("theme")); + assertEquals("de-DE", storedProperties.getProperty("locale")); + } + + @Test + void keepsTheLivePropertiesWhenStoringFails() throws IOException { + final Path propertiesFile = temporaryDirectory.resolve("opal.properties"); + Files.writeString(propertiesFile, "locale=en-US\n"); + + final SettingsController settingsController = new SettingsController(propertiesFile.toString(), "default.properties"); + final Properties originalProperties = settingsController.getProperties(); + + Files.delete(propertiesFile); + Files.createDirectory(propertiesFile); + + final Properties updatedProperties = new Properties(); + updatedProperties.putAll(originalProperties); + updatedProperties.setProperty("theme", "dark"); + + assertThrows(IOException.class, () -> settingsController.saveProperties(updatedProperties)); + assertSame(originalProperties, settingsController.getProperties()); + assertEquals("en-US", settingsController.getProperties().getProperty("locale")); + } +}