Finishes exact sound questions
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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() {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
Reference in New Issue
Block a user