From 329b7d5bbf72c3457d076dbb0afc2b87e2438024 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillaume=20T=C3=A2che?= Date: Thu, 2 Oct 2025 22:37:50 +0200 Subject: [PATCH] Finishes exact sound questions --- .../impl/PracticeQuestionGeneratorImpl.java | 4 +- .../gui/fx/FXPracticePictureExactModel.java | 2 +- .../fx/FXPracticePictureMultichoiceModel.java | 2 +- .../fx/FXPracticeSoundExactController.java | 168 +++++++++++++++++- .../gui/fx/FXPracticeSoundExactModel.java | 2 +- .../gui/fx/practiceSoundExactView.fxml | 39 ++-- 6 files changed, 194 insertions(+), 23 deletions(-) diff --git a/core/src/main/java/ch/gtache/fro/practice/impl/PracticeQuestionGeneratorImpl.java b/core/src/main/java/ch/gtache/fro/practice/impl/PracticeQuestionGeneratorImpl.java index 8154373..5b47c93 100644 --- a/core/src/main/java/ch/gtache/fro/practice/impl/PracticeQuestionGeneratorImpl.java +++ b/core/src/main/java/ch/gtache/fro/practice/impl/PracticeQuestionGeneratorImpl.java @@ -92,7 +92,7 @@ public class PracticeQuestionGeneratorImpl implements PracticeQuestionGenerator p.enabledFetchers().stream().flatMap(f -> { try { final var fetcher = fetcherProvider.getObject(f); - return fetcher.fetch(p.bird()).sounds().stream(); + return fetcher.fetch(p.bird()).sounds().stream().filter(i -> p.enabledSoundTypes().contains(i.type())); } catch (final ProvisionException | FetchException e) { logger.error("Failed to fetch sounds for bird {}", p.bird(), e); return Stream.empty(); @@ -106,7 +106,7 @@ public class PracticeQuestionGeneratorImpl implements PracticeQuestionGenerator p.enabledFetchers().stream().flatMap(f -> { try { final var fetcher = fetcherProvider.getObject(f); - return fetcher.fetch(p.bird()).pictures().stream(); + return fetcher.fetch(p.bird()).pictures().stream().filter(i -> p.enabledPictureTypes().contains(i.type())); } catch (final ProvisionException | FetchException e) { logger.error("Failed to fetch images for bird {}", p.bird(), e); return Stream.empty(); diff --git a/gui/fx/src/main/java/ch/gtache/fro/practice/gui/fx/FXPracticePictureExactModel.java b/gui/fx/src/main/java/ch/gtache/fro/practice/gui/fx/FXPracticePictureExactModel.java index 232269d..6345d49 100644 --- a/gui/fx/src/main/java/ch/gtache/fro/practice/gui/fx/FXPracticePictureExactModel.java +++ b/gui/fx/src/main/java/ch/gtache/fro/practice/gui/fx/FXPracticePictureExactModel.java @@ -25,7 +25,7 @@ public final class FXPracticePictureExactModel extends AbstractFXPraticeQuestion FXPracticePictureExactModel() { this.picture = new ReadOnlyObjectWrapper<>(); this.image = new ReadOnlyObjectWrapper<>(); - this.picture.bind(practiceRunProperty().map(r -> ((PicturePracticeQuestion) r.currentQuestion()).picture())); + this.picture.bind(practiceRunProperty().map(r -> r.currentQuestion() instanceof final PicturePracticeQuestion ppq ? ppq.picture() : null)); this.image.bind(picture.map(p -> { try { return new Image(p.path().toUri().toURL().toString(), 800, 600, true, true); diff --git a/gui/fx/src/main/java/ch/gtache/fro/practice/gui/fx/FXPracticePictureMultichoiceModel.java b/gui/fx/src/main/java/ch/gtache/fro/practice/gui/fx/FXPracticePictureMultichoiceModel.java index 3bb19a3..0e8ff52 100644 --- a/gui/fx/src/main/java/ch/gtache/fro/practice/gui/fx/FXPracticePictureMultichoiceModel.java +++ b/gui/fx/src/main/java/ch/gtache/fro/practice/gui/fx/FXPracticePictureMultichoiceModel.java @@ -25,7 +25,7 @@ public final class FXPracticePictureMultichoiceModel extends AbstractFXPraticeQu FXPracticePictureMultichoiceModel() { this.picture = new ReadOnlyObjectWrapper<>(); this.image = new ReadOnlyObjectWrapper<>(); - this.picture.bind(practiceRunProperty().map(r -> ((PicturePracticeQuestion) r.currentQuestion()).picture())); + this.picture.bind(practiceRunProperty().map(r -> r.currentQuestion() instanceof final PicturePracticeQuestion ppq ? ppq.picture() : null)); this.image.bind(picture.map(p -> { try { return new Image(p.path().toUri().toURL().toString(), 800, 600, true, true); diff --git a/gui/fx/src/main/java/ch/gtache/fro/practice/gui/fx/FXPracticeSoundExactController.java b/gui/fx/src/main/java/ch/gtache/fro/practice/gui/fx/FXPracticeSoundExactController.java index 785955d..52f2b6b 100644 --- a/gui/fx/src/main/java/ch/gtache/fro/practice/gui/fx/FXPracticeSoundExactController.java +++ b/gui/fx/src/main/java/ch/gtache/fro/practice/gui/fx/FXPracticeSoundExactController.java @@ -1,16 +1,24 @@ package ch.gtache.fro.practice.gui.fx; +import ch.gtache.fro.Bird; +import ch.gtache.fro.BirdTranslator; import ch.gtache.fro.SoundType; import ch.gtache.fro.practice.PracticeRunner; import ch.gtache.fro.practice.gui.PracticeSoundExactController; import jakarta.inject.Inject; import jakarta.inject.Singleton; +import javafx.application.Platform; +import javafx.collections.ListChangeListener; import javafx.fxml.FXML; import javafx.scene.control.Button; import javafx.scene.control.Label; +import javafx.scene.control.Slider; import javafx.scene.control.TextField; import javafx.scene.layout.VBox; +import javafx.scene.media.Media; +import javafx.scene.media.MediaPlayer; import javafx.scene.media.MediaView; +import javafx.util.Duration; import org.controlsfx.control.PrefixSelectionComboBox; import static java.util.Objects.requireNonNull; @@ -25,6 +33,7 @@ public final class FXPracticeSoundExactController implements PracticeSoundExactC private final FXPracticeSoundExactModel model; private final UserInputNormalizer normalizer; private final PracticeRunner runner; + private final BirdTranslator translator; @FXML private Button confirmButton; @@ -38,20 +47,135 @@ public final class FXPracticeSoundExactController implements PracticeSoundExactC private Label progressLabel; @FXML private VBox guessesBox; + @FXML + private Label answerLabel; + @FXML + private Button playPauseButton; + @FXML + private Slider mediaSlider; + @FXML + private Label timeLabel; + @FXML + private Slider volumeSlider; + @FXML + private Label volumeLabel; + + private boolean wasPlaying; @Inject FXPracticeSoundExactController(final FXPracticeSoundExactModel model, final PracticeRunner runner, - final UserInputNormalizer normalizer) { + final UserInputNormalizer normalizer, final BirdTranslator translator) { this.model = requireNonNull(model); this.normalizer = requireNonNull(normalizer); this.runner = requireNonNull(runner); + this.translator = requireNonNull(translator); } @FXML private void initialize() { inputField.textProperty().bindBidirectional(model.guessProperty()); - confirmButton.disableProperty().bind(model.guessProperty().isEmpty()); + inputField.disableProperty().bind(model.showingResultProperty()); + confirmButton.disableProperty().bind(model.guessProperty().isEmpty().and(model.showingResultProperty().not())); + answerLabel.visibleProperty().bind(model.showingResultProperty()); + answerLabel.textProperty().bind(model.soundProperty().map(p -> translator.translateAsync(p.bird()).join())); progressLabel.textProperty().bind(model.progressProperty()); + volumeSlider.setValue(100); + model.guesses().addListener((ListChangeListener) _ -> buildGuessesBox()); + model.soundProperty().addListener((_, _, newValue) -> { + final var oldPlayer = mediaView.getMediaPlayer(); + if (oldPlayer != null) { + oldPlayer.stop(); + oldPlayer.dispose(); + } + final var media = new Media(newValue.path().toUri().toString()); + final var player = new MediaPlayer(media); + player.setCycleCount(MediaPlayer.INDEFINITE); + mediaView.setMediaPlayer(player); + mediaSlider.setMin(0); + mediaSlider.setValue(0); + player.setOnPlaying(() -> playPauseButton.setText("⏸")); + player.setOnPaused(() -> playPauseButton.setText("▶")); + media.durationProperty().addListener((_, _, nv) -> mediaSlider.setMax(nv.toMillis())); + mediaSlider.setOnMousePressed(e -> { + wasPlaying = player.getStatus() == MediaPlayer.Status.PLAYING; + player.pause(); + }); + mediaSlider.valueProperty().addListener(_ -> { + if (mediaSlider.isValueChanging()) { + player.seek(Duration.millis(mediaSlider.getValue())); + } + timeLabel.textProperty().set(formatTime(player.getCurrentTime(), media.getDuration())); + }); + mediaSlider.setOnMouseReleased(e -> { + final var value = mediaSlider.getValue(); + Platform.runLater(() -> { + player.seek(Duration.millis(value)); + if (wasPlaying) { + player.play(); + } + }); + }); + player.currentTimeProperty().addListener((_, _, nv) -> mediaSlider.setValue(nv.toMillis())); + volumeSlider.valueProperty().addListener((_, _, nv) -> player.setVolume(nv.doubleValue() / 100.0)); + player.volumeProperty().addListener((_, _, nv) -> volumeSlider.setValue(nv.doubleValue() * 100.0)); + volumeLabel.textProperty().bind(volumeSlider.valueProperty().asString("%.2f")); + player.play(); + }); + } + + private static String formatTime(final Duration elapsed, final Duration duration) { + int intElapsed = (int) Math.floor(elapsed.toSeconds()); + final int elapsedHours = intElapsed / (60 * 60); + if (elapsedHours > 0) { + intElapsed -= elapsedHours * 60 * 60; + } + final int elapsedMinutes = intElapsed / 60; + final int elapsedSeconds = intElapsed - elapsedHours * 60 * 60 + - elapsedMinutes * 60; + + if (duration.greaterThan(Duration.ZERO)) { + int intDuration = (int) Math.floor(duration.toSeconds()); + final int durationHours = intDuration / (60 * 60); + if (durationHours > 0) { + intDuration -= durationHours * 60 * 60; + } + final int durationMinutes = intDuration / 60; + final int durationSeconds = intDuration - durationHours * 60 * 60 - + durationMinutes * 60; + if (durationHours > 0) { + return String.format("%d:%02d:%02d/%d:%02d:%02d", + elapsedHours, elapsedMinutes, elapsedSeconds, + durationHours, durationMinutes, durationSeconds); + } else { + return String.format("%02d:%02d/%02d:%02d", + elapsedMinutes, elapsedSeconds, durationMinutes, + durationSeconds); + } + } else { + if (elapsedHours > 0) { + return String.format("%d:%02d:%02d", elapsedHours, + elapsedMinutes, elapsedSeconds); + } else { + return String.format("%02d:%02d", elapsedMinutes, + elapsedSeconds); + } + } + } + + private void buildGuessesBox() { + final var labels = model.guesses().stream().map(this::getLabel).toList(); + guessesBox.getChildren().setAll(labels); + } + + private Label getLabel(final Bird bird) { + final String text; + final var birdName = translator.translateAsync(bird).join(); + if (bird.equals(model.sound().bird())) { + text = "✓ " + birdName; + } else { + text = "✗ " + birdName; + } + return new Label(text); } @Override @@ -63,22 +187,52 @@ public final class FXPracticeSoundExactController implements PracticeSoundExactC public void confirm() { final var bird = normalizer.getBird(model.guess()); model.setGuess(""); - final var newRun = runner.step(model.practiceRunProperty().get(), bird); - model.practiceRunProperty().set(newRun); + model.guesses().add(bird); + if (bird.equals(model.sound().bird()) || + model.guessesCount() >= model.practiceRunProperty().get().parameters().guessesNumber()) { + model.showingResultProperty().set(true); + } } @Override public void next() { - throw new UnsupportedOperationException(); + final var newRun = runner.step(model.practiceRunProperty().get(), model.guesses().getLast()); + model.guesses().clear(); + model.showingResultProperty().set(false); + model.practiceRunProperty().set(newRun); } @FXML private void confirmButton() { - confirm(); + confirmOrNext(); } @FXML private void enterPressed() { - confirm(); + confirmOrNext(); + } + + private void confirmOrNext() { + if (model.isShowingResult()) { + next(); + } else { + confirm(); + } + } + + private void playPause() { + final var player = mediaView.getMediaPlayer(); + if (player != null) { + if (player.getStatus() == MediaPlayer.Status.PLAYING) { + player.pause(); + } else { + player.play(); + } + } + } + + @FXML + private void playPausePressed() { + playPause(); } } diff --git a/gui/fx/src/main/java/ch/gtache/fro/practice/gui/fx/FXPracticeSoundExactModel.java b/gui/fx/src/main/java/ch/gtache/fro/practice/gui/fx/FXPracticeSoundExactModel.java index 4dc6d1e..ac6facb 100644 --- a/gui/fx/src/main/java/ch/gtache/fro/practice/gui/fx/FXPracticeSoundExactModel.java +++ b/gui/fx/src/main/java/ch/gtache/fro/practice/gui/fx/FXPracticeSoundExactModel.java @@ -20,7 +20,7 @@ public final class FXPracticeSoundExactModel extends AbstractFXPraticeQuestionEx @Inject FXPracticeSoundExactModel() { this.sound = new ReadOnlyObjectWrapper<>(); - this.sound.bind(practiceRunProperty().map(r -> ((SoundPracticeQuestion) r.currentQuestion()).sound())); + this.sound.bind(practiceRunProperty().map(r -> r.currentQuestion() instanceof final SoundPracticeQuestion spq ? spq.sound() : null)); } @Override diff --git a/gui/fx/src/main/resources/ch/gtache/fro/practice/gui/fx/practiceSoundExactView.fxml b/gui/fx/src/main/resources/ch/gtache/fro/practice/gui/fx/practiceSoundExactView.fxml index 218fa68..faaefb8 100644 --- a/gui/fx/src/main/resources/ch/gtache/fro/practice/gui/fx/practiceSoundExactView.fxml +++ b/gui/fx/src/main/resources/ch/gtache/fro/practice/gui/fx/practiceSoundExactView.fxml @@ -3,34 +3,51 @@ - + + - + - - + + + + + + +