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

@@ -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);

View File

@@ -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);

View File

@@ -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<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
@@ -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();
}
}

View File

@@ -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

View File

@@ -3,34 +3,51 @@
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.control.Slider?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.media.MediaView?>
<?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"
fx:controller="ch.gtache.fro.practice.gui.fx.FXPracticeSoundExactController">
<columnConstraints>
<ColumnConstraints hgrow="ALWAYS"/>
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0"/>
<ColumnConstraints hgrow="SOMETIMES"/>
</columnConstraints>
<rowConstraints>
<RowConstraints vgrow="SOMETIMES"/>
<RowConstraints vgrow="SOMETIMES"/>
<RowConstraints vgrow="ALWAYS"/>
<RowConstraints vgrow="ALWAYS"/>
<RowConstraints vgrow="ALWAYS"/>
<RowConstraints vgrow="SOMETIMES"/>
<RowConstraints vgrow="SOMETIMES"/>
<RowConstraints vgrow="SOMETIMES"/>
</rowConstraints>
<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"
text="%practice.sound.exact.validate.button.label" GridPane.columnSpan="2147483647"
GridPane.halignment="CENTER" GridPane.rowIndex="4"/>
<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"/>
GridPane.halignment="CENTER" GridPane.rowIndex="5"/>
<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>
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>