Finishes exact sound questions

This commit is contained in:
2025-10-02 22:37:50 +02:00
parent 5914e19ae1
commit 329b7d5bbf
6 changed files with 194 additions and 23 deletions

View File

@@ -92,7 +92,7 @@ public class PracticeQuestionGeneratorImpl implements PracticeQuestionGenerator
p.enabledFetchers().stream().flatMap(f -> { p.enabledFetchers().stream().flatMap(f -> {
try { try {
final var fetcher = fetcherProvider.getObject(f); 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) { } catch (final ProvisionException | FetchException e) {
logger.error("Failed to fetch sounds for bird {}", p.bird(), e); logger.error("Failed to fetch sounds for bird {}", p.bird(), e);
return Stream.empty(); return Stream.empty();
@@ -106,7 +106,7 @@ public class PracticeQuestionGeneratorImpl implements PracticeQuestionGenerator
p.enabledFetchers().stream().flatMap(f -> { p.enabledFetchers().stream().flatMap(f -> {
try { try {
final var fetcher = fetcherProvider.getObject(f); 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) { } catch (final ProvisionException | FetchException e) {
logger.error("Failed to fetch images for bird {}", p.bird(), e); logger.error("Failed to fetch images for bird {}", p.bird(), e);
return Stream.empty(); return Stream.empty();

View File

@@ -25,7 +25,7 @@ public final class FXPracticePictureExactModel extends AbstractFXPraticeQuestion
FXPracticePictureExactModel() { FXPracticePictureExactModel() {
this.picture = new ReadOnlyObjectWrapper<>(); this.picture = new ReadOnlyObjectWrapper<>();
this.image = 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 -> { this.image.bind(picture.map(p -> {
try { try {
return new Image(p.path().toUri().toURL().toString(), 800, 600, true, true); return new Image(p.path().toUri().toURL().toString(), 800, 600, true, true);

View File

@@ -25,7 +25,7 @@ public final class FXPracticePictureMultichoiceModel extends AbstractFXPraticeQu
FXPracticePictureMultichoiceModel() { FXPracticePictureMultichoiceModel() {
this.picture = new ReadOnlyObjectWrapper<>(); this.picture = new ReadOnlyObjectWrapper<>();
this.image = 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 -> { this.image.bind(picture.map(p -> {
try { try {
return new Image(p.path().toUri().toURL().toString(), 800, 600, true, true); return new Image(p.path().toUri().toURL().toString(), 800, 600, true, true);

View File

@@ -1,16 +1,24 @@
package ch.gtache.fro.practice.gui.fx; 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.SoundType;
import ch.gtache.fro.practice.PracticeRunner; import ch.gtache.fro.practice.PracticeRunner;
import ch.gtache.fro.practice.gui.PracticeSoundExactController; import ch.gtache.fro.practice.gui.PracticeSoundExactController;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.inject.Singleton; import jakarta.inject.Singleton;
import javafx.application.Platform;
import javafx.collections.ListChangeListener;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.control.TextField; import javafx.scene.control.TextField;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import javafx.scene.media.MediaView; import javafx.scene.media.MediaView;
import javafx.util.Duration;
import org.controlsfx.control.PrefixSelectionComboBox; import org.controlsfx.control.PrefixSelectionComboBox;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
@@ -25,6 +33,7 @@ public final class FXPracticeSoundExactController implements PracticeSoundExactC
private final FXPracticeSoundExactModel model; private final FXPracticeSoundExactModel model;
private final UserInputNormalizer normalizer; private final UserInputNormalizer normalizer;
private final PracticeRunner runner; private final PracticeRunner runner;
private final BirdTranslator translator;
@FXML @FXML
private Button confirmButton; private Button confirmButton;
@@ -38,20 +47,135 @@ public final class FXPracticeSoundExactController implements PracticeSoundExactC
private Label progressLabel; private Label progressLabel;
@FXML @FXML
private VBox guessesBox; 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 @Inject
FXPracticeSoundExactController(final FXPracticeSoundExactModel model, final PracticeRunner runner, FXPracticeSoundExactController(final FXPracticeSoundExactModel model, final PracticeRunner runner,
final UserInputNormalizer normalizer) { final UserInputNormalizer normalizer, final BirdTranslator translator) {
this.model = requireNonNull(model); this.model = requireNonNull(model);
this.normalizer = requireNonNull(normalizer); this.normalizer = requireNonNull(normalizer);
this.runner = requireNonNull(runner); this.runner = requireNonNull(runner);
this.translator = requireNonNull(translator);
} }
@FXML @FXML
private void initialize() { private void initialize() {
inputField.textProperty().bindBidirectional(model.guessProperty()); 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()); progressLabel.textProperty().bind(model.progressProperty());
volumeSlider.setValue(100);
model.guesses().addListener((ListChangeListener<Bird>) _ -> 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 @Override
@@ -63,22 +187,52 @@ public final class FXPracticeSoundExactController implements PracticeSoundExactC
public void confirm() { public void confirm() {
final var bird = normalizer.getBird(model.guess()); final var bird = normalizer.getBird(model.guess());
model.setGuess(""); model.setGuess("");
final var newRun = runner.step(model.practiceRunProperty().get(), bird); model.guesses().add(bird);
model.practiceRunProperty().set(newRun); if (bird.equals(model.sound().bird()) ||
model.guessesCount() >= model.practiceRunProperty().get().parameters().guessesNumber()) {
model.showingResultProperty().set(true);
}
} }
@Override @Override
public void next() { 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 @FXML
private void confirmButton() { private void confirmButton() {
confirm(); confirmOrNext();
} }
@FXML @FXML
private void enterPressed() { private void enterPressed() {
confirmOrNext();
}
private void confirmOrNext() {
if (model.isShowingResult()) {
next();
} else {
confirm(); 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();
}
}

View File

@@ -20,7 +20,7 @@ public final class FXPracticeSoundExactModel extends AbstractFXPraticeQuestionEx
@Inject @Inject
FXPracticeSoundExactModel() { FXPracticeSoundExactModel() {
this.sound = new ReadOnlyObjectWrapper<>(); 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 @Override

View File

@@ -3,34 +3,51 @@
<?import javafx.geometry.Insets?> <?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?> <?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?> <?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?> <?import javafx.scene.control.Slider?>
<?import javafx.scene.layout.*?> <?import javafx.scene.layout.*?>
<?import javafx.scene.media.MediaView?> <?import javafx.scene.media.MediaView?>
<?import org.controlsfx.control.PrefixSelectionComboBox?> <?import org.controlsfx.control.PrefixSelectionComboBox?>
<?import org.controlsfx.control.textfield.CustomTextField?>
<GridPane hgap="10.0" vgap="10.0" xmlns="http://javafx.com/javafx/24.0.1" xmlns:fx="http://javafx.com/fxml/1" <GridPane hgap="10.0" vgap="10.0" xmlns="http://javafx.com/javafx/24.0.1" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="ch.gtache.fro.practice.gui.fx.FXPracticeSoundExactController"> fx:controller="ch.gtache.fro.practice.gui.fx.FXPracticeSoundExactController">
<columnConstraints> <columnConstraints>
<ColumnConstraints hgrow="ALWAYS"/> <ColumnConstraints hgrow="ALWAYS"/>
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0"/> <ColumnConstraints hgrow="SOMETIMES"/>
</columnConstraints> </columnConstraints>
<rowConstraints> <rowConstraints>
<RowConstraints vgrow="SOMETIMES"/> <RowConstraints vgrow="ALWAYS"/>
<RowConstraints vgrow="SOMETIMES"/> <RowConstraints vgrow="ALWAYS"/>
<RowConstraints vgrow="ALWAYS"/>
<RowConstraints vgrow="SOMETIMES"/> <RowConstraints vgrow="SOMETIMES"/>
<RowConstraints vgrow="SOMETIMES"/> <RowConstraints vgrow="SOMETIMES"/>
<RowConstraints vgrow="SOMETIMES"/> <RowConstraints vgrow="SOMETIMES"/>
</rowConstraints> </rowConstraints>
<children> <children>
<CustomTextField fx:id="inputField" onAction="#enterPressed" GridPane.rowIndex="3"/>
<MediaView fx:id="mediaView" GridPane.columnSpan="2147483647" GridPane.halignment="CENTER"
GridPane.rowIndex="1">
</MediaView>
<PrefixSelectionComboBox fx:id="typeCombobox" managed="false" visible="false" GridPane.columnIndex="1"
GridPane.rowIndex="3"/>
<Button fx:id="confirmButton" mnemonicParsing="false" onAction="#confirmButton" <Button fx:id="confirmButton" mnemonicParsing="false" onAction="#confirmButton"
text="%practice.sound.exact.validate.button.label" GridPane.columnSpan="2147483647" text="%practice.sound.exact.validate.button.label" GridPane.columnSpan="2147483647"
GridPane.halignment="CENTER" GridPane.rowIndex="4"/> GridPane.halignment="CENTER" GridPane.rowIndex="5"/>
<TextField fx:id="inputField" onAction="#enterPressed" GridPane.rowIndex="2"/>
<MediaView fx:id="mediaView" fitHeight="200.0" fitWidth="200.0" GridPane.columnSpan="2147483647"
GridPane.halignment="CENTER" GridPane.rowIndex="1"/>
<PrefixSelectionComboBox fx:id="typeCombobox" managed="false" visible="false" GridPane.columnIndex="1"
GridPane.rowIndex="2"/>
<Label fx:id="progressLabel" text="Label" GridPane.columnSpan="2147483647" GridPane.halignment="CENTER"/> <Label fx:id="progressLabel" text="Label" GridPane.columnSpan="2147483647" GridPane.halignment="CENTER"/>
<VBox fx:id="guessesBox" GridPane.halignment="CENTER" GridPane.rowIndex="3"/> <VBox fx:id="guessesBox" GridPane.halignment="CENTER" GridPane.rowIndex="4"/>
<Label fx:id="answerLabel" GridPane.columnSpan="2147483647" GridPane.halignment="CENTER" GridPane.rowIndex="2"/>
<HBox alignment="CENTER_LEFT" spacing="10.0" GridPane.rowIndex="1">
<children>
<Button fx:id="playPauseButton" mnemonicParsing="false" onAction="#playPausePressed" text="Button"/>
<Slider fx:id="mediaSlider" HBox.hgrow="ALWAYS"/>
<Label fx:id="timeLabel" text="Label"/>
<Label text="🔊"/>
<Slider fx:id="volumeSlider"/>
<Label fx:id="volumeLabel" text="Label"/>
</children>
<GridPane.margin>
<Insets/>
</GridPane.margin>
</HBox>
</children> </children>
<padding> <padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/> <Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>