Adds WhisperX, reworks UI (still needs some work), theoretically usable
This commit is contained in:
@@ -13,7 +13,8 @@
|
||||
|
||||
<properties>
|
||||
<controlsfx.version>11.2.1</controlsfx.version>
|
||||
<javafx.version>22.0.1</javafx.version>
|
||||
<javafx.version>22.0.2</javafx.version>
|
||||
<testfx.version>4.0.18</testfx.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -44,6 +45,23 @@
|
||||
<artifactId>controlsfx</artifactId>
|
||||
<version>${controlsfx.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testfx</groupId>
|
||||
<artifactId>testfx-core</artifactId>
|
||||
<version>${testfx.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testfx</groupId>
|
||||
<artifactId>testfx-junit5</artifactId>
|
||||
<version>${testfx.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -27,18 +27,19 @@ public class ColonTimeFormatter implements TimeFormatter {
|
||||
public String format(final long millis) {
|
||||
final var secondsInMinute = 60;
|
||||
final var secondsInHour = secondsInMinute * 60;
|
||||
var intDuration = (int) millis / 1000;
|
||||
final var durationHours = intDuration / secondsInHour;
|
||||
var secondsDuration = millis / 1000L;
|
||||
final var durationHours = secondsDuration / secondsInHour;
|
||||
if (durationHours > 0) {
|
||||
intDuration -= durationHours * secondsInHour;
|
||||
secondsDuration -= durationHours * secondsInHour;
|
||||
}
|
||||
final var durationMinutes = intDuration / secondsInMinute;
|
||||
final var durationSeconds = intDuration - durationHours * secondsInHour
|
||||
- durationMinutes * secondsInMinute;
|
||||
final var durationMinutes = secondsDuration / secondsInMinute;
|
||||
secondsDuration -= durationMinutes * secondsInMinute;
|
||||
final var durationSeconds = secondsDuration;
|
||||
final var durationMillis = millis % 1000L;
|
||||
if (durationHours > 0) {
|
||||
return String.format("%d:%02d:%02d", durationHours, durationMinutes, durationSeconds);
|
||||
return "%d:%02d:%02d.%03d".formatted(durationHours, durationMinutes, durationSeconds, durationMillis);
|
||||
} else {
|
||||
return String.format("%02d:%02d", durationMinutes, durationSeconds);
|
||||
return "%02d:%02d.%03d".formatted(durationMinutes, durationSeconds, durationMillis);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,14 +49,20 @@ public class ColonTimeFormatter implements TimeFormatter {
|
||||
final var secondsInMinute = 60;
|
||||
final var secondsInHour = secondsInMinute * 60;
|
||||
return switch (split.length) {
|
||||
case 1 -> toLong(split[0]) * 1000;
|
||||
case 2 -> (toLong(split[0]) * secondsInMinute + toLong(split[1])) * 1000;
|
||||
case 3 -> (toLong(split[0]) * secondsInHour + toLong(split[1]) * secondsInMinute + toLong(split[2])) * 1000;
|
||||
case 1 -> parseSecondsMillis(split[0]);
|
||||
case 2 -> toLong(split[0]) * secondsInMinute * 1000 + parseSecondsMillis(split[1]);
|
||||
case 3 ->
|
||||
(toLong(split[0]) * secondsInHour + toLong(split[1]) * secondsInMinute) * 1000 + parseSecondsMillis(split[2]);
|
||||
default -> 0;
|
||||
};
|
||||
}
|
||||
|
||||
private long toLong(final String time) {
|
||||
private static long parseSecondsMillis(final String time) {
|
||||
final var split = time.split("\\.");
|
||||
return toLong(split[0]) * 1000 + (split.length > 1 ? toLong(split[1]) : 0);
|
||||
}
|
||||
|
||||
private static long toLong(final String time) {
|
||||
if (time.startsWith("0")) {
|
||||
return Long.parseLong(time.substring(1));
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.github.gtache.autosubtitle.gui.fx;
|
||||
|
||||
/**
|
||||
* Binds multiple models together
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface FXBinder {
|
||||
|
||||
/**
|
||||
* Creates the bindings between the models
|
||||
*/
|
||||
void createBindings();
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.github.gtache.autosubtitle.gui.fx;
|
||||
import com.github.gtache.autosubtitle.gui.MainController;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.TabPane;
|
||||
import javafx.stage.Window;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
@@ -12,7 +13,7 @@ import java.util.Objects;
|
||||
* FX implementation of {@link MainController}
|
||||
*/
|
||||
@Singleton
|
||||
public class FXMainController implements MainController {
|
||||
public class FXMainController extends AbstractFXController implements MainController {
|
||||
|
||||
@FXML
|
||||
private TabPane tabPane;
|
||||
@@ -25,18 +26,23 @@ public class FXMainController implements MainController {
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void initialize() {
|
||||
tabPane.getSelectionModel().selectedIndexProperty().addListener((observable, oldValue, newValue) -> model.selectTab(newValue.intValue()));
|
||||
void initialize() {
|
||||
tabPane.getSelectionModel().selectedIndexProperty().addListener((observable, oldValue, newValue) -> model.setSelectedTab(newValue.intValue()));
|
||||
model.selectedTabProperty().addListener((observable, oldValue, newValue) -> tabPane.getSelectionModel().select(newValue.intValue()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void selectTab(final int index) {
|
||||
model.selectTab(index);
|
||||
model.setSelectedTab(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FXMainModel model() {
|
||||
return model;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Window window() {
|
||||
return tabPane.getScene().getWindow();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ public class FXMainModel implements MainModel {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void selectTab(final int index) {
|
||||
public void setSelectedTab(final int index) {
|
||||
selectedTab.set(index);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import java.util.Objects;
|
||||
* Binds the media model
|
||||
*/
|
||||
@Singleton
|
||||
public class FXMediaBinder {
|
||||
public class FXMediaBinder implements FXBinder {
|
||||
|
||||
private final FXWorkModel workModel;
|
||||
private final FXMediaModel mediaModel;
|
||||
@@ -21,6 +21,7 @@ public class FXMediaBinder {
|
||||
this.mediaModel = Objects.requireNonNull(mediaModel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createBindings() {
|
||||
mediaModel.videoProperty().bindBidirectional(workModel.videoProperty());
|
||||
Bindings.bindContent(mediaModel.subtitles(), workModel.subtitles());
|
||||
|
||||
@@ -5,12 +5,10 @@ import com.github.gtache.autosubtitle.gui.MediaController;
|
||||
import com.github.gtache.autosubtitle.gui.TimeFormatter;
|
||||
import com.github.gtache.autosubtitle.modules.gui.impl.Pause;
|
||||
import com.github.gtache.autosubtitle.modules.gui.impl.Play;
|
||||
import com.github.gtache.autosubtitle.subtitle.EditableSubtitle;
|
||||
import com.github.gtache.autosubtitle.subtitle.Subtitle;
|
||||
import com.github.gtache.autosubtitle.subtitle.gui.fx.SubtitleLabel;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Cursor;
|
||||
@@ -34,9 +32,7 @@ import org.apache.logging.log4j.Logger;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
@@ -69,8 +65,6 @@ public class FXMediaController implements MediaController {
|
||||
private final Image playImage;
|
||||
private final Image pauseImage;
|
||||
|
||||
private final List<Long> startTimes;
|
||||
|
||||
private boolean wasPlaying;
|
||||
|
||||
@Inject
|
||||
@@ -81,11 +75,10 @@ public class FXMediaController implements MediaController {
|
||||
this.timeFormatter = requireNonNull(timeFormatter);
|
||||
this.playImage = requireNonNull(playImage);
|
||||
this.pauseImage = requireNonNull(pauseImage);
|
||||
this.startTimes = new ArrayList<>();
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void initialize() {
|
||||
void initialize() {
|
||||
volumeValueLabel.textProperty().bind(Bindings.createStringBinding(() -> String.valueOf((int) (model.volume() * 100)), model.volumeProperty()));
|
||||
playLabel.textProperty().bind(Bindings.createStringBinding(() -> timeFormatter.format(model.position(), model.duration()), model.positionProperty(), model.durationProperty()));
|
||||
model.positionProperty().bindBidirectional(playSlider.valueProperty());
|
||||
@@ -115,13 +108,10 @@ public class FXMediaController implements MediaController {
|
||||
loadFileVideo(file.path());
|
||||
} else {
|
||||
logger.error("Unsupported video type : {}", newValue);
|
||||
Platform.runLater(() -> model.setVideo(null));
|
||||
}
|
||||
});
|
||||
|
||||
model.subtitles().addListener((ListChangeListener<EditableSubtitle>) c -> {
|
||||
startTimes.clear();
|
||||
model.subtitles().stream().mapToLong(Subtitle::start).forEach(startTimes::add);
|
||||
});
|
||||
bindPlayButton();
|
||||
binder.createBindings();
|
||||
}
|
||||
@@ -130,8 +120,8 @@ public class FXMediaController implements MediaController {
|
||||
final var media = new Media(fileVideoPath.toUri().toString());
|
||||
final var player = new MediaPlayer(media);
|
||||
player.statusProperty().addListener((observable12, oldValue1, newValue1) ->
|
||||
logger.info("New status: {}", newValue1));
|
||||
player.currentTimeProperty().addListener((ignored, oldTime, newTime) -> currentTimeChanged(oldTime.toMillis(), newTime.toMillis()));
|
||||
logger.debug("New player status: {}", newValue1));
|
||||
player.currentTimeProperty().addListener((ignored, oldTime, newTime) -> currentTimeChanged(newTime.toMillis()));
|
||||
playSlider.setOnMousePressed(e -> {
|
||||
wasPlaying = model.isPlaying();
|
||||
model.setIsPlaying(false);
|
||||
@@ -155,33 +145,37 @@ public class FXMediaController implements MediaController {
|
||||
playSlider.setMax(model.duration());
|
||||
playSlider.setValue(0L);
|
||||
videoView.setMediaPlayer(player);
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
Thread.sleep(3000L);
|
||||
} catch (final InterruptedException ignored) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
Platform.runLater(() -> {
|
||||
final var status = player.getStatus();
|
||||
if (status == null || status == MediaPlayer.Status.UNKNOWN) {
|
||||
logger.warn("Reloading video {} because player state is unknown or null", fileVideoPath);
|
||||
loadFileVideo(fileVideoPath);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void currentTimeChanged(final double oldMillis, final double millis) {
|
||||
private void currentTimeChanged(final double millis) {
|
||||
final var longMillis = (long) millis;
|
||||
playSlider.setValue(millis);
|
||||
final var subtitleLabels = stackPane.getChildren().stream().filter(SubtitleLabel.class::isInstance).map(SubtitleLabel.class::cast).toList();
|
||||
subtitleLabels.stream().filter(s -> !s.subtitle().isShowing((long) millis)).forEach(sl -> stackPane.getChildren().remove(sl));
|
||||
final var containedSubtitles = subtitleLabels.stream().map(SubtitleLabel::subtitle).filter(s -> s.isShowing((long) millis)).collect(Collectors.toSet());
|
||||
|
||||
subtitleLabels.stream().filter(s -> !s.subtitle().isShowing(longMillis)).forEach(sl -> stackPane.getChildren().remove(sl));
|
||||
final var containedSubtitles = subtitleLabels.stream().map(SubtitleLabel::subtitle).filter(s -> s.isShowing(longMillis)).collect(Collectors.toSet());
|
||||
//TODO optimize?
|
||||
model.subtitles().forEach(s -> {
|
||||
if (!containedSubtitles.contains(s)) {
|
||||
logger.info("Adding label {} at {}", s, millis);
|
||||
if (!containedSubtitles.contains(s) && s.isShowing(longMillis)) {
|
||||
final var label = createDraggableLabel(s);
|
||||
stackPane.getChildren().add(label);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void currentTimeChangedOptimized(final double oldMillis, final double millis) {
|
||||
final var forward = oldMillis <= millis;
|
||||
|
||||
var index = Collections.binarySearch(startTimes, (long) millis);
|
||||
if (index < 0) {
|
||||
index = forward ? -(index + 1) : -(index + 2);
|
||||
}
|
||||
//TODO
|
||||
}
|
||||
|
||||
private void bindPlayButton() {
|
||||
playButton.disableProperty().bind(model.videoProperty().isNull());
|
||||
playButton.graphicProperty().bind(Bindings.createObjectBinding(() -> {
|
||||
@@ -217,6 +211,7 @@ public class FXMediaController implements MediaController {
|
||||
public void seek(final long position) {
|
||||
if (videoView.getMediaPlayer() != null) {
|
||||
videoView.getMediaPlayer().seek(Duration.millis(position));
|
||||
currentTimeChanged(position);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -102,12 +102,12 @@ public class FXMediaModel implements MediaModel {
|
||||
this.position.set(position);
|
||||
}
|
||||
|
||||
LongProperty positionProperty() {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableList<EditableSubtitle> subtitles() {
|
||||
return subtitles;
|
||||
}
|
||||
|
||||
LongProperty positionProperty() {
|
||||
return position;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ public class FXParametersController extends AbstractFXController implements Para
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void initialize() {
|
||||
void initialize() {
|
||||
extractionModelCombobox.setItems(model.availableExtractionModels());
|
||||
extractionModelCombobox.valueProperty().bindBidirectional(model.extractionModelProperty());
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ public class FXSetupController extends AbstractFXController implements SetupCont
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void initialize() {
|
||||
void initialize() {
|
||||
statusMap.put(converterManager, model.videoConverterStatusProperty());
|
||||
statusMap.put(extractorManager, model.subtitleExtractorStatusProperty());
|
||||
statusMap.put(translatorManager, model.translatorStatusProperty());
|
||||
|
||||
@@ -2,11 +2,8 @@ package com.github.gtache.autosubtitle.gui.fx;
|
||||
|
||||
import com.github.gtache.autosubtitle.gui.SetupModel;
|
||||
import com.github.gtache.autosubtitle.setup.SetupStatus;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.beans.property.ReadOnlyBooleanWrapper;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
@@ -22,45 +19,26 @@ import javax.inject.Singleton;
|
||||
public class FXSetupModel implements SetupModel {
|
||||
|
||||
private final ObjectProperty<SetupStatus> subtitleExtractorStatus;
|
||||
private final ReadOnlyBooleanWrapper subtitleExtractorInstalled;
|
||||
private final ReadOnlyBooleanWrapper subtitleExtractorUpdateAvailable;
|
||||
private final DoubleProperty subtitleExtractorSetupProgress;
|
||||
private final StringProperty subtitleExtractorSetupProgressLabel;
|
||||
private final ObjectProperty<SetupStatus> videoConverterStatus;
|
||||
private final ReadOnlyBooleanWrapper videoConverterInstalled;
|
||||
private final ReadOnlyBooleanWrapper videoConverterUpdateAvailable;
|
||||
private final DoubleProperty videoConverterSetupProgress;
|
||||
private final StringProperty videoConverterSetupProgressLabel;
|
||||
private final ObjectProperty<SetupStatus> translatorStatus;
|
||||
private final ReadOnlyBooleanWrapper translatorInstalled;
|
||||
private final ReadOnlyBooleanWrapper translatorUpdateAvailable;
|
||||
private final DoubleProperty translatorSetupProgress;
|
||||
private final StringProperty translatorSetupProgressLabel;
|
||||
|
||||
@Inject
|
||||
FXSetupModel() {
|
||||
this.subtitleExtractorStatus = new SimpleObjectProperty<>(SetupStatus.ERRORED);
|
||||
this.subtitleExtractorInstalled = new ReadOnlyBooleanWrapper(false);
|
||||
this.subtitleExtractorUpdateAvailable = new ReadOnlyBooleanWrapper(false);
|
||||
this.subtitleExtractorSetupProgress = new SimpleDoubleProperty(-2);
|
||||
this.subtitleExtractorSetupProgressLabel = new SimpleStringProperty("");
|
||||
this.videoConverterStatus = new SimpleObjectProperty<>(SetupStatus.ERRORED);
|
||||
this.videoConverterInstalled = new ReadOnlyBooleanWrapper(false);
|
||||
this.videoConverterUpdateAvailable = new ReadOnlyBooleanWrapper(false);
|
||||
this.videoConverterSetupProgress = new SimpleDoubleProperty(-2);
|
||||
this.videoConverterSetupProgressLabel = new SimpleStringProperty("");
|
||||
this.translatorStatus = new SimpleObjectProperty<>(SetupStatus.ERRORED);
|
||||
this.translatorInstalled = new ReadOnlyBooleanWrapper(false);
|
||||
this.translatorUpdateAvailable = new ReadOnlyBooleanWrapper(false);
|
||||
this.translatorSetupProgress = new SimpleDoubleProperty(-2);
|
||||
this.translatorSetupProgressLabel = new SimpleStringProperty("");
|
||||
|
||||
subtitleExtractorInstalled.bind(Bindings.createBooleanBinding(() -> subtitleExtractorStatus.get().isInstalled(), subtitleExtractorStatus));
|
||||
videoConverterInstalled.bind(Bindings.createBooleanBinding(() -> videoConverterStatus.get().isInstalled(), videoConverterStatus));
|
||||
translatorInstalled.bind(Bindings.createBooleanBinding(() -> translatorStatus.get().isInstalled(), translatorStatus));
|
||||
subtitleExtractorUpdateAvailable.bind(Bindings.createBooleanBinding(() -> subtitleExtractorStatus.get() == SetupStatus.UPDATE_AVAILABLE, subtitleExtractorStatus));
|
||||
videoConverterUpdateAvailable.bind(Bindings.createBooleanBinding(() -> videoConverterStatus.get() == SetupStatus.UPDATE_AVAILABLE, videoConverterStatus));
|
||||
translatorUpdateAvailable.bind(Bindings.createBooleanBinding(() -> translatorStatus.get() == SetupStatus.UPDATE_AVAILABLE, translatorStatus));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -77,20 +55,6 @@ public class FXSetupModel implements SetupModel {
|
||||
return subtitleExtractorStatus;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSubtitleExtractorInstalled() {
|
||||
return subtitleExtractorInstalled.get();
|
||||
}
|
||||
|
||||
ReadOnlyBooleanProperty subtitleExtractorInstalledProperty() {
|
||||
return subtitleExtractorInstalled.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSubtitleExtractorUpdateAvailable() {
|
||||
return subtitleExtractorUpdateAvailable.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public double subtitleExtractorSetupProgress() {
|
||||
return subtitleExtractorSetupProgress.get();
|
||||
@@ -133,28 +97,6 @@ public class FXSetupModel implements SetupModel {
|
||||
return videoConverterStatus;
|
||||
}
|
||||
|
||||
ReadOnlyBooleanProperty subtitleExtractorUpdateAvailableProperty() {
|
||||
return subtitleExtractorUpdateAvailable.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVideoConverterInstalled() {
|
||||
return videoConverterInstalled.get();
|
||||
}
|
||||
|
||||
ReadOnlyBooleanProperty videoConverterInstalledProperty() {
|
||||
return videoConverterInstalled.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVideoConverterUpdateAvailable() {
|
||||
return videoConverterUpdateAvailable.get();
|
||||
}
|
||||
|
||||
ReadOnlyBooleanProperty videoConverterUpdateAvailableProperty() {
|
||||
return videoConverterUpdateAvailable.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public double videoConverterSetupProgress() {
|
||||
return videoConverterSetupProgress.get();
|
||||
@@ -197,24 +139,6 @@ public class FXSetupModel implements SetupModel {
|
||||
return translatorStatus;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTranslatorInstalled() {
|
||||
return translatorInstalled.get();
|
||||
}
|
||||
|
||||
ReadOnlyBooleanProperty translatorInstalledProperty() {
|
||||
return translatorInstalled.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTranslatorUpdateAvailable() {
|
||||
return translatorUpdateAvailable.get();
|
||||
}
|
||||
|
||||
ReadOnlyBooleanProperty translatorUpdateAvailableProperty() {
|
||||
return translatorUpdateAvailable.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public double translatorSetupProgress() {
|
||||
return translatorSetupProgress.get();
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.github.gtache.autosubtitle.gui.fx;
|
||||
|
||||
import com.github.gtache.autosubtitle.Language;
|
||||
import com.github.gtache.autosubtitle.gui.WorkStatus;
|
||||
import com.github.gtache.autosubtitle.subtitle.gui.fx.ObservableSubtitleCollectionImpl;
|
||||
import javafx.beans.binding.Bindings;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Binds the subtitles model
|
||||
*/
|
||||
@Singleton
|
||||
public class FXSubtitlesBinder implements FXBinder {
|
||||
|
||||
private final FXWorkModel workModel;
|
||||
private final FXSubtitlesModel subtitlesModel;
|
||||
|
||||
@Inject
|
||||
FXSubtitlesBinder(final FXWorkModel workModel, final FXSubtitlesModel subtitlesModel) {
|
||||
this.workModel = Objects.requireNonNull(workModel);
|
||||
this.subtitlesModel = Objects.requireNonNull(subtitlesModel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createBindings() {
|
||||
subtitlesModel.canLoadSubtitlesProperty().bind(workModel.videoProperty().isNotNull().and(workModel.statusProperty().isEqualTo(WorkStatus.IDLE)));
|
||||
subtitlesModel.canResetSubtitlesProperty().bind(workModel.videoProperty().isNotNull().and(workModel.statusProperty().isEqualTo(WorkStatus.IDLE)));
|
||||
subtitlesModel.canAddSubtitleProperty().bind(workModel.videoProperty().isNotNull().and(workModel.statusProperty().isEqualTo(WorkStatus.IDLE)).and(subtitlesModel.videoLanguageProperty().isNotEqualTo(Language.AUTO)));
|
||||
subtitlesModel.canEditTableProperty().bind(workModel.videoProperty().isNotNull().and(workModel.statusProperty().isEqualTo(WorkStatus.IDLE)).and(subtitlesModel.videoLanguageProperty().isNotEqualTo(Language.AUTO)));
|
||||
|
||||
workModel.selectedSubtitleProperty().bind(subtitlesModel.selectedSubtitleProperty());
|
||||
workModel.canExportProperty().bind(Bindings.isNotEmpty(subtitlesModel.collections()).and(workModel.statusProperty().isEqualTo(WorkStatus.IDLE)));
|
||||
workModel.videoLanguageProperty().bind(subtitlesModel.videoLanguageProperty());
|
||||
|
||||
subtitlesModel.translatingProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue) {
|
||||
workModel.statusProperty().set(WorkStatus.TRANSLATING);
|
||||
} else {
|
||||
workModel.statusProperty().set(WorkStatus.IDLE);
|
||||
}
|
||||
});
|
||||
|
||||
workModel.extractedCollectionProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
subtitlesModel.collections().put(newValue.language(), new ObservableSubtitleCollectionImpl(newValue));
|
||||
}
|
||||
});
|
||||
|
||||
Bindings.bindContent(workModel.collections(), subtitlesModel.collections());
|
||||
Bindings.bindContent(workModel.subtitles(), subtitlesModel.selectedSubtitles());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
package com.github.gtache.autosubtitle.gui.fx;
|
||||
|
||||
import com.github.gtache.autosubtitle.Language;
|
||||
import com.github.gtache.autosubtitle.Translator;
|
||||
import com.github.gtache.autosubtitle.gui.SubtitlesController;
|
||||
import com.github.gtache.autosubtitle.gui.TimeFormatter;
|
||||
import com.github.gtache.autosubtitle.subtitle.SubtitleCollection;
|
||||
import com.github.gtache.autosubtitle.subtitle.SubtitleImporterExporter;
|
||||
import com.github.gtache.autosubtitle.subtitle.converter.ParseException;
|
||||
import com.github.gtache.autosubtitle.subtitle.gui.fx.ObservableSubtitleCollectionImpl;
|
||||
import com.github.gtache.autosubtitle.subtitle.gui.fx.ObservableSubtitleImpl;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.collections.MapChangeListener;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.Tab;
|
||||
import javafx.scene.control.TabPane;
|
||||
import javafx.scene.control.TableColumn;
|
||||
import javafx.scene.control.TableView;
|
||||
import javafx.scene.control.cell.TextFieldTableCell;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Window;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.controlsfx.control.PrefixSelectionComboBox;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
/**
|
||||
* FX implementation of {@link SubtitlesController}
|
||||
*/
|
||||
@Singleton
|
||||
public class FXSubtitlesController extends AbstractFXController implements SubtitlesController {
|
||||
|
||||
private static final Logger logger = LogManager.getLogger(FXSubtitlesController.class);
|
||||
private static final String ARCHIVE = "Archive";
|
||||
private static final String ALL_SUPPORTED = "All supported";
|
||||
|
||||
@FXML
|
||||
private ResourceBundle resources;
|
||||
@FXML
|
||||
private Button loadButton;
|
||||
@FXML
|
||||
private Button resetButton;
|
||||
@FXML
|
||||
private Button saveButton;
|
||||
@FXML
|
||||
private Button addButton;
|
||||
@FXML
|
||||
private PrefixSelectionComboBox<Language> languageCombobox;
|
||||
@FXML
|
||||
private ComboBox<Language> translationsCombobox;
|
||||
@FXML
|
||||
private TabPane tabPane;
|
||||
@FXML
|
||||
private Tab mainSubtitlesTab;
|
||||
@FXML
|
||||
private TableView<ObservableSubtitleImpl> subtitlesTable;
|
||||
@FXML
|
||||
private TableColumn<ObservableSubtitleImpl, Long> startColumn;
|
||||
@FXML
|
||||
private TableColumn<ObservableSubtitleImpl, Long> endColumn;
|
||||
@FXML
|
||||
private TableColumn<ObservableSubtitleImpl, String> textColumn;
|
||||
|
||||
private final FXSubtitlesModel model;
|
||||
private final FXSubtitlesBinder binder;
|
||||
private final SubtitleImporterExporter<?> importerExporter;
|
||||
private final TimeFormatter timeFormatter;
|
||||
private final List<String> subtitleExtensions;
|
||||
private final Translator<?> translator;
|
||||
|
||||
@Inject
|
||||
FXSubtitlesController(final FXSubtitlesModel model, final FXSubtitlesBinder binder, final SubtitleImporterExporter importerExporter, final TimeFormatter timeFormatter,
|
||||
final Translator translator) {
|
||||
this.model = requireNonNull(model);
|
||||
this.binder = requireNonNull(binder);
|
||||
this.importerExporter = requireNonNull(importerExporter);
|
||||
this.timeFormatter = requireNonNull(timeFormatter);
|
||||
this.subtitleExtensions = importerExporter.supportedSingleFileExtensions().stream().map(c -> "*." + c).sorted().toList();
|
||||
this.translator = requireNonNull(translator);
|
||||
}
|
||||
|
||||
@FXML
|
||||
void initialize() {
|
||||
addButton.disableProperty().bind(model.canAddSubtitleProperty().not());
|
||||
loadButton.disableProperty().bind(model.canLoadSubtitlesProperty().not());
|
||||
resetButton.disableProperty().bind(model.canResetSubtitlesProperty().not());
|
||||
saveButton.disableProperty().bind(model.canSaveSubtitlesProperty().not());
|
||||
|
||||
// Can't bind because tab calls updateDisabled which sets disableProperty
|
||||
model.canEditTableProperty().addListener((observable, oldValue, newValue) -> subtitlesTable.setDisable(!newValue));
|
||||
|
||||
bindComboboxes();
|
||||
bindTable();
|
||||
mainSubtitlesTab.textProperty().bind(Bindings.createStringBinding(() -> model.videoLanguage().iso2(), model.videoLanguageProperty()));
|
||||
model.collections().addListener((MapChangeListener<Language, ObservableSubtitleCollectionImpl>) change -> {
|
||||
manageTabs();
|
||||
});
|
||||
tabPane.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
model.setSelectedCollection(model.collections().get(Language.getLanguage(newValue.getText())));
|
||||
if (oldValue != null) {
|
||||
oldValue.setContent(null);
|
||||
}
|
||||
newValue.setContent(subtitlesTable);
|
||||
}
|
||||
});
|
||||
model.selectedLanguageProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
tabPane.getTabs().stream().filter(t -> Language.getLanguage(t.getText()) == newValue)
|
||||
.findFirst().ifPresent(tab -> tabPane.getSelectionModel().select(tab));
|
||||
}
|
||||
});
|
||||
|
||||
translationsCombobox.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue != null && !model.collections().containsKey(newValue)) {
|
||||
model.setTranslating(true);
|
||||
CompletableFuture.supplyAsync(() -> translator.translate(model.collections().get(model.videoLanguage()), newValue))
|
||||
.whenCompleteAsync((r, t) -> {
|
||||
if (t == null) {
|
||||
loadCollection(r);
|
||||
model.setSelectedCollection(model.collections().get(newValue));
|
||||
} else {
|
||||
logger.error("Error while translating to {}", newValue, t);
|
||||
}
|
||||
model.setTranslating(false);
|
||||
}, Platform::runLater);
|
||||
}
|
||||
});
|
||||
binder.createBindings();
|
||||
}
|
||||
|
||||
private void bindTable() {
|
||||
subtitlesTable.setItems(model.selectedSubtitles());
|
||||
subtitlesTable.setOnKeyPressed(e -> {
|
||||
if (e.getCode().isLetterKey() || e.getCode().isDigitKey()) {
|
||||
editFocusedCell();
|
||||
} else if (e.getCode() == KeyCode.RIGHT ||
|
||||
e.getCode() == KeyCode.TAB) {
|
||||
subtitlesTable.getSelectionModel().selectNext();
|
||||
e.consume();
|
||||
} else if (e.getCode() == KeyCode.LEFT) {
|
||||
subtitlesTable.getSelectionModel().selectPrevious();
|
||||
e.consume();
|
||||
} else if (e.getCode() == KeyCode.DELETE) {
|
||||
deleteSelectedSubtitles();
|
||||
e.consume();
|
||||
}
|
||||
});
|
||||
startColumn.setCellFactory(TextFieldTableCell.forTableColumn(new TimeStringConverter(timeFormatter)));
|
||||
startColumn.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue() == null ? null : param.getValue().start()));
|
||||
startColumn.setOnEditCommit(e -> {
|
||||
final var subtitle = e.getRowValue();
|
||||
subtitle.setStart(e.getNewValue());
|
||||
subtitlesTable.refresh();
|
||||
subtitlesTable.requestFocus();
|
||||
});
|
||||
endColumn.setCellFactory(TextFieldTableCell.forTableColumn(new TimeStringConverter(timeFormatter)));
|
||||
endColumn.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue() == null ? null : param.getValue().end()));
|
||||
endColumn.setOnEditCommit(e -> {
|
||||
final var subtitle = e.getRowValue();
|
||||
subtitle.setEnd(e.getNewValue());
|
||||
subtitlesTable.refresh();
|
||||
subtitlesTable.requestFocus();
|
||||
});
|
||||
textColumn.setCellFactory(TextFieldTableCell.forTableColumn());
|
||||
textColumn.setCellValueFactory(param -> new SimpleStringProperty(param.getValue() == null ? null : param.getValue().content()));
|
||||
textColumn.setOnEditCommit(e -> {
|
||||
final var subtitle = e.getRowValue();
|
||||
subtitle.setContent(e.getNewValue());
|
||||
subtitlesTable.refresh();
|
||||
subtitlesTable.requestFocus();
|
||||
});
|
||||
|
||||
subtitlesTable.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> model.selectedSubtitleProperty().set(newValue));
|
||||
|
||||
}
|
||||
|
||||
private void manageTabs() {
|
||||
final var toRemove = new ArrayList<Tab>();
|
||||
final var toAdd = new ArrayList<Tab>();
|
||||
tabPane.getTabs().forEach(tab -> {
|
||||
if (!model.collections().containsKey(Language.getLanguage(tab.getText()))) {
|
||||
toRemove.add(tab);
|
||||
}
|
||||
});
|
||||
model.collections().forEach((language, collection) -> {
|
||||
if (tabPane.getTabs().stream().noneMatch(t -> Language.getLanguage(t.getText()) == language)) {
|
||||
toAdd.add(new Tab(language.iso2()));
|
||||
}
|
||||
});
|
||||
tabPane.getTabs().removeAll(toRemove);
|
||||
tabPane.getTabs().addAll(toAdd);
|
||||
tabPane.getTabs().sort(Comparator.comparing(Tab::getText));
|
||||
}
|
||||
|
||||
private void bindComboboxes() {
|
||||
languageCombobox.valueProperty().bindBidirectional(model.videoLanguageProperty());
|
||||
languageCombobox.setItems(model.availableVideoLanguages());
|
||||
languageCombobox.setConverter(new LanguageStringConverter());
|
||||
translationsCombobox.setConverter(new LanguageStringConverter());
|
||||
translationsCombobox.setItems(model.availableTranslationsLanguage());
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void resetButtonPressed() {
|
||||
model.setSelectedCollection(model.originalCollections().get(model.selectedLanguage()));
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void addPressed() {
|
||||
model.selectedCollection().subtitles().add(new ObservableSubtitleImpl("Enter text here..."));
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void loadPressed() {
|
||||
final var filePicker = new FileChooser();
|
||||
final var archiveFilter = new FileChooser.ExtensionFilter(ARCHIVE, subtitleExtensions);
|
||||
final var allSupportedFilter = new FileChooser.ExtensionFilter(ALL_SUPPORTED, subtitleExtensions);
|
||||
filePicker.getExtensionFilters().addAll(archiveFilter, allSupportedFilter);
|
||||
filePicker.setSelectedExtensionFilter(allSupportedFilter);
|
||||
final var file = filePicker.showOpenDialog(window());
|
||||
loadSubtitles(file.toPath());
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void savePressed() {
|
||||
final var filePicker = new FileChooser();
|
||||
final var archiveFilter = new FileChooser.ExtensionFilter(ARCHIVE, subtitleExtensions);
|
||||
final var allSupportedFilter = new FileChooser.ExtensionFilter(ALL_SUPPORTED, subtitleExtensions);
|
||||
filePicker.getExtensionFilters().addAll(archiveFilter, allSupportedFilter);
|
||||
filePicker.setSelectedExtensionFilter(allSupportedFilter);
|
||||
final var file = filePicker.showSaveDialog(window());
|
||||
if (file != null) {
|
||||
saveSubtitles(file.toPath());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void selectLanguage(final Language language) {
|
||||
model.setSelectedLanguage(language);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteLanguage(final Language language) {
|
||||
model.selectedTranslationsLanguages().remove(language);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveSubtitles(final Path file) {
|
||||
try {
|
||||
final var filename = file.getFileName().toString();
|
||||
final var extension = filename.substring(filename.lastIndexOf('.') + 1);
|
||||
if (subtitleExtensions.contains(extension)) {
|
||||
importerExporter.exportSubtitles(model.selectedCollection(), file);
|
||||
} else {
|
||||
importerExporter.exportSubtitles(model.collections().values(), file);
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
logger.error("Error saving subtitles {}", file, e);
|
||||
showErrorDialog(resources.getString("subtitles.save.error.title"), MessageFormat.format(resources.getString("subtitles.save.error.label"), file));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadSubtitles(final Path file) {
|
||||
try {
|
||||
final var map = importerExporter.importSubtitles(file);
|
||||
map.values().forEach(this::loadCollection);
|
||||
if (model.videoLanguage() == Language.AUTO) {
|
||||
model.setVideoLanguage(map.keySet().stream().findFirst().orElse(Language.AUTO));
|
||||
}
|
||||
} catch (final IOException | ParseException e) {
|
||||
logger.error("Error loading subtitles {}", file, e);
|
||||
showErrorDialog(resources.getString("subtitles.load.error.title"), MessageFormat.format(resources.getString("subtitles.load.error.label"), file));
|
||||
}
|
||||
}
|
||||
|
||||
private void loadCollection(final SubtitleCollection<?> collection) {
|
||||
final var observableCollection = new ObservableSubtitleCollectionImpl(collection);
|
||||
model.originalCollections().put(observableCollection.language(), observableCollection);
|
||||
model.collections().put(observableCollection.language(), observableCollection);
|
||||
}
|
||||
|
||||
private void deleteSelectedSubtitles() {
|
||||
model.selectedCollection().observableSubtitles().removeAll(subtitlesTable.getSelectionModel().getSelectedItems());
|
||||
}
|
||||
|
||||
private void editFocusedCell() {
|
||||
final var focusedCell = subtitlesTable.getFocusModel().getFocusedCell();
|
||||
if (focusedCell != null) {
|
||||
subtitlesTable.edit(focusedCell.getRow(), focusedCell.getTableColumn());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public FXSubtitlesModel model() {
|
||||
return model;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Window window() {
|
||||
return saveButton.getScene().getWindow();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
package com.github.gtache.autosubtitle.gui.fx;
|
||||
|
||||
import com.github.gtache.autosubtitle.Language;
|
||||
import com.github.gtache.autosubtitle.gui.SubtitlesModel;
|
||||
import com.github.gtache.autosubtitle.subtitle.gui.fx.ObservableSubtitleCollectionImpl;
|
||||
import com.github.gtache.autosubtitle.subtitle.gui.fx.ObservableSubtitleImpl;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.beans.property.ReadOnlyBooleanWrapper;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.MapChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.ObservableMap;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
|
||||
/**
|
||||
* FX implementation of {@link SubtitlesModel}
|
||||
*/
|
||||
@Singleton
|
||||
public class FXSubtitlesModel implements SubtitlesModel<ObservableSubtitleImpl, ObservableSubtitleCollectionImpl> {
|
||||
|
||||
private final ObservableList<Language> availableVideoLanguages;
|
||||
private final ObjectProperty<Language> videoLanguage;
|
||||
private final ObservableList<Language> availableTranslationLanguages;
|
||||
private final ObservableList<Language> selectedTranslationsLanguages;
|
||||
private final ObjectProperty<Language> selectedLanguage;
|
||||
private final ObservableMap<Language, ObservableSubtitleCollectionImpl> collections;
|
||||
private final ObservableMap<Language, ObservableSubtitleCollectionImpl> originalCollections;
|
||||
private final ObjectProperty<ObservableSubtitleCollectionImpl> selectedCollection;
|
||||
private final ObservableList<ObservableSubtitleImpl> selectedSubtitles;
|
||||
private final ObjectProperty<ObservableSubtitleImpl> selectedSubtitle;
|
||||
|
||||
private final BooleanProperty canLoadSubtitles;
|
||||
private final BooleanProperty canAddSubtitle;
|
||||
private final BooleanProperty canResetSubtitles;
|
||||
private final BooleanProperty canEditTable;
|
||||
private final ReadOnlyBooleanWrapper canSaveSubtitles;
|
||||
private final BooleanProperty isTranslating;
|
||||
|
||||
@Inject
|
||||
FXSubtitlesModel() {
|
||||
this.availableVideoLanguages = FXCollections.unmodifiableObservableList(FXCollections.observableArrayList(Arrays.stream(Language.values())
|
||||
.sorted((o1, o2) -> {
|
||||
if (o1 == Language.AUTO) {
|
||||
return -1;
|
||||
} else if (o2 == Language.AUTO) {
|
||||
return 1;
|
||||
} else {
|
||||
return o1.englishName().compareTo(o2.englishName());
|
||||
}
|
||||
}).toList()));
|
||||
this.availableTranslationLanguages = FXCollections.observableArrayList(Arrays.stream(Language.values()).filter(l -> l != Language.AUTO).toList());
|
||||
this.videoLanguage = new SimpleObjectProperty<>(Language.AUTO);
|
||||
this.selectedTranslationsLanguages = FXCollections.observableArrayList();
|
||||
this.selectedLanguage = new SimpleObjectProperty<>(Language.AUTO);
|
||||
this.collections = FXCollections.observableHashMap();
|
||||
this.originalCollections = FXCollections.observableHashMap();
|
||||
this.selectedCollection = new SimpleObjectProperty<>();
|
||||
this.selectedSubtitles = FXCollections.observableArrayList();
|
||||
this.selectedSubtitle = new SimpleObjectProperty<>();
|
||||
this.canLoadSubtitles = new SimpleBooleanProperty(false);
|
||||
this.canAddSubtitle = new SimpleBooleanProperty(false);
|
||||
this.canResetSubtitles = new SimpleBooleanProperty(false);
|
||||
this.canSaveSubtitles = new ReadOnlyBooleanWrapper(false);
|
||||
this.canEditTable = new SimpleBooleanProperty(false);
|
||||
this.isTranslating = new SimpleBooleanProperty(false);
|
||||
|
||||
canSaveSubtitles.bind(Bindings.isNotEmpty(collections));
|
||||
collections.addListener((MapChangeListener<Language, ObservableSubtitleCollectionImpl>) change ->
|
||||
availableTranslationLanguages.setAll(Arrays.stream(Language.values()).filter(l -> l != Language.AUTO && !collections.containsKey(l)).sorted(Comparator.comparing(Language::englishName)).toList()));
|
||||
|
||||
selectedCollection.addListener((observable, oldValue, newValue) -> {
|
||||
selectedSubtitle.set(null);
|
||||
if (newValue == null) {
|
||||
selectedSubtitles.clear();
|
||||
selectedLanguage.set(Language.AUTO);
|
||||
} else {
|
||||
selectedSubtitles.setAll(newValue.subtitles());
|
||||
selectedLanguage.set(newValue.language());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableList<Language> availableVideoLanguages() {
|
||||
return availableVideoLanguages;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Language videoLanguage() {
|
||||
return videoLanguage.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVideoLanguage(final Language language) {
|
||||
videoLanguage.set(language);
|
||||
}
|
||||
|
||||
ObjectProperty<Language> videoLanguageProperty() {
|
||||
return videoLanguage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableList<Language> availableTranslationsLanguage() {
|
||||
return FXCollections.unmodifiableObservableList(availableTranslationLanguages);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableList<Language> selectedTranslationsLanguages() {
|
||||
return selectedTranslationsLanguages;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Language selectedLanguage() {
|
||||
return selectedLanguage.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSelectedLanguage(final Language language) {
|
||||
selectedLanguage.set(language);
|
||||
}
|
||||
|
||||
ObjectProperty<Language> selectedLanguageProperty() {
|
||||
return selectedLanguage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableMap<Language, ObservableSubtitleCollectionImpl> collections() {
|
||||
return collections;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableSubtitleCollectionImpl selectedCollection() {
|
||||
return selectedCollection.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSelectedCollection(final ObservableSubtitleCollectionImpl collection) {
|
||||
selectedCollection.set(collection);
|
||||
}
|
||||
|
||||
ObjectProperty<ObservableSubtitleCollectionImpl> selectedCollectionProperty() {
|
||||
return selectedCollection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableMap<Language, ObservableSubtitleCollectionImpl> originalCollections() {
|
||||
return originalCollections;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableList<ObservableSubtitleImpl> selectedSubtitles() {
|
||||
return selectedSubtitles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableSubtitleImpl selectedSubtitle() {
|
||||
return selectedSubtitle.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSelectedSubtitle(final ObservableSubtitleImpl subtitle) {
|
||||
selectedSubtitle.set(subtitle);
|
||||
}
|
||||
|
||||
ObjectProperty<ObservableSubtitleImpl> selectedSubtitleProperty() {
|
||||
return selectedSubtitle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canLoadSubtitles() {
|
||||
return canLoadSubtitles.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCanLoadSubtitles(final boolean canLoadSubtitles) {
|
||||
this.canLoadSubtitles.set(canLoadSubtitles);
|
||||
}
|
||||
|
||||
BooleanProperty canLoadSubtitlesProperty() {
|
||||
return canLoadSubtitles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canAddSubtitle() {
|
||||
return canAddSubtitle.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCanAddSubtitle(final boolean canAddSubtitle) {
|
||||
this.canAddSubtitle.set(canAddSubtitle);
|
||||
}
|
||||
|
||||
BooleanProperty canAddSubtitleProperty() {
|
||||
return canAddSubtitle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canResetSubtitles() {
|
||||
return canResetSubtitles.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCanResetSubtitles(final boolean canResetSubtitles) {
|
||||
this.canResetSubtitles.set(canResetSubtitles);
|
||||
}
|
||||
|
||||
BooleanProperty canResetSubtitlesProperty() {
|
||||
return canResetSubtitles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canSaveSubtitles() {
|
||||
return canSaveSubtitles.get();
|
||||
}
|
||||
|
||||
ReadOnlyBooleanProperty canSaveSubtitlesProperty() {
|
||||
return canSaveSubtitles.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTranslating() {
|
||||
return isTranslating.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTranslating(final boolean translating) {
|
||||
this.isTranslating.set(translating);
|
||||
}
|
||||
|
||||
BooleanProperty translatingProperty() {
|
||||
return isTranslating;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canEditTable() {
|
||||
return canEditTable.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCanEditTable(final boolean canEditTable) {
|
||||
this.canEditTable.set(canEditTable);
|
||||
}
|
||||
|
||||
public BooleanProperty canEditTableProperty() {
|
||||
return canEditTable;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import javax.inject.Singleton;
|
||||
import java.util.Objects;
|
||||
|
||||
@Singleton
|
||||
public class FXWorkBinder {
|
||||
public class FXWorkBinder implements FXBinder {
|
||||
|
||||
private final FXWorkModel workModel;
|
||||
private final FXParametersModel parametersModel;
|
||||
@@ -16,7 +16,8 @@ public class FXWorkBinder {
|
||||
this.parametersModel = Objects.requireNonNull(parametersModel);
|
||||
}
|
||||
|
||||
void createBindings() {
|
||||
@Override
|
||||
public void createBindings() {
|
||||
workModel.extractionModelProperty().bind(parametersModel.extractionModelProperty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +1,32 @@
|
||||
package com.github.gtache.autosubtitle.gui.fx;
|
||||
|
||||
import com.github.gtache.autosubtitle.Language;
|
||||
import com.github.gtache.autosubtitle.Translator;
|
||||
import com.github.gtache.autosubtitle.VideoConverter;
|
||||
import com.github.gtache.autosubtitle.VideoLoader;
|
||||
import com.github.gtache.autosubtitle.gui.TimeFormatter;
|
||||
import com.github.gtache.autosubtitle.gui.WorkController;
|
||||
import com.github.gtache.autosubtitle.gui.WorkStatus;
|
||||
import com.github.gtache.autosubtitle.subtitle.EditableSubtitle;
|
||||
import com.github.gtache.autosubtitle.subtitle.SubtitleCollection;
|
||||
import com.github.gtache.autosubtitle.subtitle.converter.ParseException;
|
||||
import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverter;
|
||||
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractEvent;
|
||||
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractException;
|
||||
import com.github.gtache.autosubtitle.subtitle.extractor.SubtitleExtractor;
|
||||
import com.github.gtache.autosubtitle.subtitle.extractor.SubtitleExtractorListener;
|
||||
import com.github.gtache.autosubtitle.subtitle.gui.fx.ObservableSubtitleImpl;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ProgressBar;
|
||||
import javafx.scene.control.TableColumn;
|
||||
import javafx.scene.control.TableView;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.control.cell.TextFieldTableCell;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Window;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.controlsfx.control.CheckComboBox;
|
||||
import org.controlsfx.control.PrefixSelectionComboBox;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
@@ -67,34 +50,18 @@ public class FXWorkController extends AbstractFXController implements WorkContro
|
||||
|
||||
@FXML
|
||||
private TextField fileField;
|
||||
@FXML
|
||||
private Button loadSubtitlesButton;
|
||||
|
||||
@FXML
|
||||
private Button extractButton;
|
||||
@FXML
|
||||
private Button resetButton;
|
||||
|
||||
@FXML
|
||||
private Button exportSoftButton;
|
||||
@FXML
|
||||
private Button exportHardButton;
|
||||
@FXML
|
||||
private TableView<EditableSubtitle> subtitlesTable;
|
||||
@FXML
|
||||
private TableColumn<EditableSubtitle, Long> startColumn;
|
||||
@FXML
|
||||
private TableColumn<EditableSubtitle, Long> endColumn;
|
||||
@FXML
|
||||
private TableColumn<EditableSubtitle, String> textColumn;
|
||||
|
||||
@FXML
|
||||
private FXMediaController mediaController;
|
||||
@FXML
|
||||
private Button saveSubtitlesButton;
|
||||
@FXML
|
||||
private Button addSubtitleButton;
|
||||
@FXML
|
||||
private PrefixSelectionComboBox<Language> languageCombobox;
|
||||
@FXML
|
||||
private CheckComboBox<Language> translationsCombobox;
|
||||
|
||||
@FXML
|
||||
private Label progressLabel;
|
||||
@FXML
|
||||
@@ -107,34 +74,25 @@ public class FXWorkController extends AbstractFXController implements WorkContro
|
||||
private final FXWorkModel model;
|
||||
private final FXWorkBinder binder;
|
||||
private final SubtitleExtractor subtitleExtractor;
|
||||
private final Map<String, SubtitleConverter> subtitleConvertersMap;
|
||||
private final VideoConverter videoConverter;
|
||||
private final VideoLoader videoLoader;
|
||||
private final Translator translator;
|
||||
private final TimeFormatter timeFormatter;
|
||||
private final List<String> subtitleExtensions;
|
||||
|
||||
|
||||
@Inject
|
||||
FXWorkController(final FXWorkModel model, final FXWorkBinder binder, final SubtitleExtractor subtitleExtractor,
|
||||
final Map<String, SubtitleConverter> subtitleConvertersMap, final VideoLoader videoLoader,
|
||||
final VideoConverter videoConverter, final Translator translator, final TimeFormatter timeFormatter) {
|
||||
final VideoLoader videoLoader, final VideoConverter videoConverter) {
|
||||
this.model = requireNonNull(model);
|
||||
this.binder = requireNonNull(binder);
|
||||
this.subtitleExtractor = requireNonNull(subtitleExtractor);
|
||||
this.subtitleConvertersMap = requireNonNull(subtitleConvertersMap);
|
||||
this.videoConverter = requireNonNull(videoConverter);
|
||||
this.videoLoader = requireNonNull(videoLoader);
|
||||
this.translator = requireNonNull(translator);
|
||||
this.timeFormatter = requireNonNull(timeFormatter);
|
||||
this.subtitleExtensions = subtitleConvertersMap.values().stream().map(c -> "*." + c.formatName()).sorted().toList();
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void initialize() {
|
||||
bindComboboxes();
|
||||
bindButtons();
|
||||
bindTable();
|
||||
void initialize() {
|
||||
extractButton.disableProperty().bind(model.canExtractProperty().not());
|
||||
exportSoftButton.disableProperty().bind(model.canExportProperty().not());
|
||||
exportHardButton.disableProperty().bind(model.canExportProperty().not());
|
||||
|
||||
bindProgress();
|
||||
|
||||
model.selectedSubtitleProperty().addListener((observable, oldValue, newValue) -> {
|
||||
@@ -148,91 +106,17 @@ public class FXWorkController extends AbstractFXController implements WorkContro
|
||||
subtitleExtractor.addListener(this);
|
||||
}
|
||||
|
||||
private void bindComboboxes() {
|
||||
languageCombobox.valueProperty().bindBidirectional(model.videoLanguageProperty());
|
||||
languageCombobox.setItems(model.availableVideoLanguages());
|
||||
languageCombobox.setConverter(new LanguageStringConverter());
|
||||
translationsCombobox.setConverter(new LanguageStringConverter());
|
||||
Bindings.bindContent(translationsCombobox.getItems(), model.availableTranslationsLanguage());
|
||||
Bindings.bindContent(model.translations(), translationsCombobox.getCheckModel().getCheckedItems());
|
||||
}
|
||||
|
||||
private void bindButtons() {
|
||||
extractButton.disableProperty().bind(model.videoProperty().isNull().or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
|
||||
addSubtitleButton.disableProperty().bind(model.videoProperty().isNull().or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
|
||||
loadSubtitlesButton.disableProperty().bind(model.videoProperty().isNull().or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
|
||||
resetButton.disableProperty().bind(Bindings.isEmpty(model.subtitles()).or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
|
||||
exportSoftButton.disableProperty().bind(Bindings.isEmpty(model.subtitles()).or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
|
||||
exportHardButton.disableProperty().bind(Bindings.isEmpty(model.subtitles()).or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
|
||||
saveSubtitlesButton.disableProperty().bind(Bindings.isEmpty(model.subtitles()).or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
|
||||
}
|
||||
|
||||
private void bindTable() {
|
||||
subtitlesTable.setItems(model.subtitles());
|
||||
subtitlesTable.setOnKeyPressed(e -> {
|
||||
if (e.getCode().isLetterKey() || e.getCode().isDigitKey()) {
|
||||
editFocusedCell();
|
||||
} else if (e.getCode() == KeyCode.RIGHT ||
|
||||
e.getCode() == KeyCode.TAB) {
|
||||
subtitlesTable.getSelectionModel().selectNext();
|
||||
e.consume();
|
||||
} else if (e.getCode() == KeyCode.LEFT) {
|
||||
subtitlesTable.getSelectionModel().selectPrevious();
|
||||
e.consume();
|
||||
} else if (e.getCode() == KeyCode.DELETE) {
|
||||
deleteSelectedSubtitles();
|
||||
e.consume();
|
||||
}
|
||||
});
|
||||
startColumn.setCellFactory(TextFieldTableCell.forTableColumn(new TimeStringConverter(timeFormatter)));
|
||||
startColumn.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue() == null ? null : param.getValue().start()));
|
||||
startColumn.setOnEditCommit(e -> {
|
||||
final var subtitle = e.getRowValue();
|
||||
subtitle.setStart(e.getNewValue());
|
||||
subtitlesTable.refresh();
|
||||
subtitlesTable.requestFocus();
|
||||
});
|
||||
endColumn.setCellFactory(TextFieldTableCell.forTableColumn(new TimeStringConverter(timeFormatter)));
|
||||
endColumn.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue() == null ? null : param.getValue().end()));
|
||||
endColumn.setOnEditCommit(e -> {
|
||||
final var subtitle = e.getRowValue();
|
||||
subtitle.setEnd(e.getNewValue());
|
||||
subtitlesTable.refresh();
|
||||
subtitlesTable.requestFocus();
|
||||
});
|
||||
textColumn.setCellFactory(TextFieldTableCell.forTableColumn());
|
||||
textColumn.setCellValueFactory(param -> new SimpleStringProperty(param.getValue() == null ? null : param.getValue().content()));
|
||||
textColumn.setOnEditCommit(e -> {
|
||||
final var subtitle = e.getRowValue();
|
||||
subtitle.setContent(e.getNewValue());
|
||||
subtitlesTable.refresh();
|
||||
subtitlesTable.requestFocus();
|
||||
});
|
||||
|
||||
subtitlesTable.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> model.selectedSubtitleProperty().set(newValue));
|
||||
}
|
||||
|
||||
private void bindProgress() {
|
||||
progressLabel.textProperty().bind(Bindings.createStringBinding(() -> resources.getString("work.status." + model.status().name().toLowerCase() + ".label"), model.statusProperty()));
|
||||
progressLabel.visibleProperty().bind(model.statusProperty().isNotEqualTo(WorkStatus.IDLE));
|
||||
progressBar.visibleProperty().bind(model.statusProperty().isNotEqualTo(WorkStatus.IDLE));
|
||||
progressDetailLabel.visibleProperty().bind(model.statusProperty().isNotEqualTo(WorkStatus.IDLE));
|
||||
progressLabel.managedProperty().bind(model.statusProperty().isNotEqualTo(WorkStatus.IDLE));
|
||||
progressBar.managedProperty().bind(model.statusProperty().isNotEqualTo(WorkStatus.IDLE));
|
||||
progressDetailLabel.managedProperty().bind(model.statusProperty().isNotEqualTo(WorkStatus.IDLE));
|
||||
progressLabel.visibleProperty().bind(model.isProgressVisibleProperty());
|
||||
progressBar.visibleProperty().bind(model.isProgressVisibleProperty());
|
||||
progressDetailLabel.visibleProperty().bind(model.isProgressVisibleProperty());
|
||||
progressLabel.managedProperty().bind(model.isProgressVisibleProperty());
|
||||
progressBar.managedProperty().bind(model.isProgressVisibleProperty());
|
||||
progressDetailLabel.managedProperty().bind(model.isProgressVisibleProperty());
|
||||
progressBar.progressProperty().bindBidirectional(model.progressProperty());
|
||||
}
|
||||
|
||||
private void deleteSelectedSubtitles() {
|
||||
model.subtitles().removeAll(subtitlesTable.getSelectionModel().getSelectedItems());
|
||||
}
|
||||
|
||||
private void editFocusedCell() {
|
||||
final var focusedCell = subtitlesTable.getFocusModel().getFocusedCell();
|
||||
if (focusedCell != null) {
|
||||
subtitlesTable.edit(focusedCell.getRow(), focusedCell.getTableColumn());
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void fileButtonPressed() {
|
||||
@@ -254,17 +138,17 @@ public class FXWorkController extends AbstractFXController implements WorkContro
|
||||
}
|
||||
}
|
||||
|
||||
private SubtitleCollection extractAsync() {
|
||||
private SubtitleCollection<?> extractAsync() {
|
||||
try {
|
||||
return subtitleExtractor.extract(model.video(), model.videoLanguage(), model.extractionModel());
|
||||
return subtitleExtractor.extract(model.video(), model.videoLanguageProperty().get(), model.extractionModel());
|
||||
} catch (final ExtractException e) {
|
||||
throw new CompletionException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void manageExtractResult(final SubtitleCollection newCollection, final Throwable t) {
|
||||
private void manageExtractResult(final SubtitleCollection<?> newCollection, final Throwable t) {
|
||||
if (t == null) {
|
||||
loadCollection(newCollection);
|
||||
model.setExtractedCollection(newCollection);
|
||||
} else {
|
||||
logger.error("Error extracting subtitles", t);
|
||||
showErrorDialog(resources.getString("work.extract.error.title"), MessageFormat.format(resources.getString("work.extract.error.label"), t.getMessage()));
|
||||
@@ -284,45 +168,6 @@ public class FXWorkController extends AbstractFXController implements WorkContro
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveSubtitles(final Path file) {
|
||||
final var fileName = file.getFileName().toString();
|
||||
final var converter = subtitleConvertersMap.get(fileName.substring(fileName.lastIndexOf('.') + 1));
|
||||
if (converter == null) {
|
||||
logger.warn("No converter for {}", file);
|
||||
showErrorDialog(resources.getString("work.save.subtitles.missing.converter.title"), MessageFormat.format(resources.getString("work.save.subtitles.missing.converter.label"), file.getFileName()));
|
||||
} else {
|
||||
final var string = converter.format(model.subtitleCollection());
|
||||
try {
|
||||
Files.writeString(file, string);
|
||||
} catch (final IOException e) {
|
||||
logger.error("Error saving subtitles {}", file, e);
|
||||
showErrorDialog(resources.getString("work.save.subtitles.error.title"), MessageFormat.format(resources.getString("work.save.subtitles.error.label"), e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadSubtitles(final Path file) {
|
||||
final var fileName = file.getFileName().toString();
|
||||
final var parser = subtitleConvertersMap.get(fileName.substring(fileName.lastIndexOf('.') + 1));
|
||||
if (parser != null) {
|
||||
try {
|
||||
final var collection = parser.parse(file);
|
||||
loadCollection(collection);
|
||||
} catch (final ParseException e) {
|
||||
logger.error("Error loading subtitles {}", file, e);
|
||||
showErrorDialog(resources.getString("work.load.subtitles.error.title"), MessageFormat.format(resources.getString("work.load.subtitles.error.label"), e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void loadCollection(final SubtitleCollection collection) {
|
||||
model.subtitles().setAll(collection.subtitles().stream().map(ObservableSubtitleImpl::new).toList());
|
||||
model.originalSubtitles().clear();
|
||||
model.originalSubtitles().addAll(collection.subtitles().stream().map(ObservableSubtitleImpl::new).toList());
|
||||
model.videoLanguageProperty().set(collection.language());
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void exportSoftPressed() {
|
||||
@@ -332,27 +177,20 @@ public class FXWorkController extends AbstractFXController implements WorkContro
|
||||
filePicker.setSelectedExtensionFilter(extensionFilter);
|
||||
final var file = filePicker.showSaveDialog(window());
|
||||
if (file != null) {
|
||||
final var baseCollection = model.subtitleCollection();
|
||||
final var translations = model.translations();
|
||||
model.setStatus(WorkStatus.TRANSLATING);
|
||||
CompletableFuture.supplyAsync(() -> Stream.concat(Stream.of(baseCollection), translations.stream().map(l -> translator.translate(baseCollection, l))).toList())
|
||||
.thenApplyAsync(c -> {
|
||||
model.setStatus(WorkStatus.EXPORTING);
|
||||
return c;
|
||||
}, Platform::runLater)
|
||||
.thenAcceptAsync(collections -> {
|
||||
try {
|
||||
videoConverter.addSoftSubtitles(model.video(), collections, file.toPath());
|
||||
} catch (final IOException e) {
|
||||
throw new CompletionException(e);
|
||||
}
|
||||
}).whenCompleteAsync((v, t) -> {
|
||||
if (t != null) {
|
||||
logger.error("Error exporting subtitles", t);
|
||||
showErrorDialog(resources.getString("work.export.error.title"), MessageFormat.format(resources.getString("work.export.error.label"), t.getMessage()));
|
||||
}
|
||||
model.setStatus(WorkStatus.IDLE);
|
||||
}, Platform::runLater);
|
||||
model.setStatus(WorkStatus.EXPORTING);
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
videoConverter.addSoftSubtitles(model.video(), model.collections().values(), file.toPath());
|
||||
} catch (final IOException e) {
|
||||
throw new CompletionException(e);
|
||||
}
|
||||
}).whenCompleteAsync((v, t) -> {
|
||||
if (t != null) {
|
||||
logger.error("Error exporting subtitles", t);
|
||||
showErrorDialog(resources.getString("work.export.error.title"), MessageFormat.format(resources.getString("work.export.error.label"), t.getMessage()));
|
||||
} //else show info dialog
|
||||
model.setStatus(WorkStatus.IDLE);
|
||||
}, Platform::runLater);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,7 +204,7 @@ public class FXWorkController extends AbstractFXController implements WorkContro
|
||||
if (file != null) {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
videoConverter.addHardSubtitles(model.video(), model.subtitleCollection(), file.toPath());
|
||||
videoConverter.addHardSubtitles(model.video(), model.collections().get(model.videoLanguageProperty().get()), file.toPath());
|
||||
} catch (final IOException e) {
|
||||
throw new CompletionException(e);
|
||||
}
|
||||
@@ -374,7 +212,7 @@ public class FXWorkController extends AbstractFXController implements WorkContro
|
||||
if (t != null) {
|
||||
logger.error("Error exporting subtitles", t);
|
||||
showErrorDialog(resources.getString("work.export.error.title"), MessageFormat.format(resources.getString("work.export.error.label"), t.getMessage()));
|
||||
}
|
||||
} //else show info dialog
|
||||
model.setStatus(WorkStatus.IDLE);
|
||||
}, Platform::runLater);
|
||||
}
|
||||
@@ -386,6 +224,7 @@ public class FXWorkController extends AbstractFXController implements WorkContro
|
||||
return model;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Window window() {
|
||||
return fileField.getScene().getWindow();
|
||||
}
|
||||
@@ -395,16 +234,6 @@ public class FXWorkController extends AbstractFXController implements WorkContro
|
||||
extractSubtitles();
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void resetButtonPressed() {
|
||||
model.subtitles().setAll(model.originalSubtitles());
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void addSubtitlePressed() {
|
||||
model.subtitles().add(new ObservableSubtitleImpl("Enter text here..."));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void listen(final ExtractEvent event) {
|
||||
Platform.runLater(() -> {
|
||||
@@ -413,25 +242,5 @@ public class FXWorkController extends AbstractFXController implements WorkContro
|
||||
});
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void loadSubtitlesPressed() {
|
||||
final var filePicker = new FileChooser();
|
||||
final var extensionFilter = new FileChooser.ExtensionFilter(ALL_SUPPORTED, subtitleExtensions);
|
||||
filePicker.getExtensionFilters().add(extensionFilter);
|
||||
filePicker.setSelectedExtensionFilter(extensionFilter);
|
||||
final var file = filePicker.showOpenDialog(window());
|
||||
loadSubtitles(file.toPath());
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void saveSubtitlesPressed() {
|
||||
final var filePicker = new FileChooser();
|
||||
final var extensionFilter = new FileChooser.ExtensionFilter(ALL_SUPPORTED, subtitleExtensions);
|
||||
filePicker.getExtensionFilters().add(extensionFilter);
|
||||
filePicker.setSelectedExtensionFilter(extensionFilter);
|
||||
final var file = filePicker.showSaveDialog(window());
|
||||
if (file != null) {
|
||||
saveSubtitles(file.toPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,26 +7,22 @@ import com.github.gtache.autosubtitle.gui.WorkStatus;
|
||||
import com.github.gtache.autosubtitle.subtitle.EditableSubtitle;
|
||||
import com.github.gtache.autosubtitle.subtitle.SubtitleCollection;
|
||||
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractionModel;
|
||||
import com.github.gtache.autosubtitle.subtitle.impl.SubtitleCollectionImpl;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import com.github.gtache.autosubtitle.subtitle.gui.fx.ObservableSubtitleCollectionImpl;
|
||||
import com.github.gtache.autosubtitle.subtitle.gui.fx.ObservableSubtitleImpl;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.beans.property.ReadOnlyStringProperty;
|
||||
import javafx.beans.property.ReadOnlyStringWrapper;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.beans.property.ReadOnlyBooleanWrapper;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.ObservableMap;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* FX implementation of {@link WorkModel}
|
||||
@@ -35,51 +31,35 @@ import java.util.stream.Collectors;
|
||||
public class FXWorkModel implements WorkModel {
|
||||
|
||||
private final ObjectProperty<Video> video;
|
||||
private final ReadOnlyObjectWrapper<SubtitleCollection> subtitleCollection;
|
||||
private final ObservableList<EditableSubtitle> subtitles;
|
||||
private final List<EditableSubtitle> originalSubtitles;
|
||||
private final ObjectProperty<EditableSubtitle> subtitle;
|
||||
private final ObservableList<Language> availableVideoLanguages;
|
||||
private final ObservableList<Language> availableTranslationLanguages;
|
||||
private final ObjectProperty<ExtractionModel> extractionModel;
|
||||
private final ObjectProperty<Language> videoLanguage;
|
||||
private final ObservableList<Language> translations;
|
||||
private final ReadOnlyStringWrapper text;
|
||||
private final ObjectProperty<WorkStatus> workStatus;
|
||||
private final DoubleProperty progress;
|
||||
private final ObjectProperty<EditableSubtitle> selectedSubtitle;
|
||||
private final ObservableList<ObservableSubtitleImpl> subtitles;
|
||||
private final ObservableMap<Language, ObservableSubtitleCollectionImpl> collections;
|
||||
private final ObjectProperty<Language> videoLanguage;
|
||||
private final ReadOnlyBooleanWrapper canExtract;
|
||||
private final BooleanProperty canExport;
|
||||
private final ReadOnlyBooleanWrapper isProgressVisible;
|
||||
private final ObjectProperty<SubtitleCollection<?>> extractedCollection;
|
||||
|
||||
@Inject
|
||||
FXWorkModel() {
|
||||
this.video = new SimpleObjectProperty<>();
|
||||
this.subtitleCollection = new ReadOnlyObjectWrapper<>();
|
||||
this.subtitles = FXCollections.observableArrayList();
|
||||
this.originalSubtitles = new ArrayList<>();
|
||||
this.subtitle = new SimpleObjectProperty<>();
|
||||
this.availableVideoLanguages =
|
||||
FXCollections.unmodifiableObservableList(FXCollections.observableArrayList(Arrays.stream(Language.values())
|
||||
.sorted((o1, o2) -> {
|
||||
if (o1 == Language.AUTO) {
|
||||
return -1;
|
||||
} else if (o2 == Language.AUTO) {
|
||||
return 1;
|
||||
} else {
|
||||
return o1.compareTo(o2);
|
||||
}
|
||||
}).toList()));
|
||||
this.availableTranslationLanguages =
|
||||
FXCollections.observableArrayList(Arrays.stream(Language.values()).filter(l -> l != Language.AUTO).toList());
|
||||
this.videoLanguage = new SimpleObjectProperty<>(Language.AUTO);
|
||||
this.translations = FXCollections.observableArrayList();
|
||||
this.text = new ReadOnlyStringWrapper("");
|
||||
this.workStatus = new SimpleObjectProperty<>(WorkStatus.IDLE);
|
||||
this.extractionModel = new SimpleObjectProperty<>();
|
||||
this.progress = new SimpleDoubleProperty(-1);
|
||||
text.bind(Bindings.createStringBinding(() ->
|
||||
subtitles.stream().map(EditableSubtitle::content).collect(Collectors.joining("")),
|
||||
subtitles));
|
||||
subtitleCollection.bind(Bindings.createObjectBinding(() -> new SubtitleCollectionImpl(text(), subtitles, videoLanguage()), text, subtitles, videoLanguage));
|
||||
videoLanguage.addListener((observable, oldValue, newValue) -> FXCollections.observableArrayList(Arrays.stream(Language.values())
|
||||
.filter(l -> l != Language.AUTO && l != newValue).sorted(Comparator.naturalOrder()).toList()));
|
||||
this.selectedSubtitle = new SimpleObjectProperty<>();
|
||||
this.subtitles = FXCollections.observableArrayList();
|
||||
this.collections = FXCollections.observableHashMap();
|
||||
this.videoLanguage = new SimpleObjectProperty<>(Language.AUTO);
|
||||
this.canExtract = new ReadOnlyBooleanWrapper(false);
|
||||
this.canExport = new SimpleBooleanProperty(false);
|
||||
this.isProgressVisible = new ReadOnlyBooleanWrapper(false);
|
||||
this.extractedCollection = new SimpleObjectProperty<>();
|
||||
|
||||
isProgressVisible.bind(workStatus.isNotEqualTo(WorkStatus.IDLE));
|
||||
canExtract.bind(video.isNotNull().and(workStatus.isEqualTo(WorkStatus.IDLE)));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -87,6 +67,15 @@ public class FXWorkModel implements WorkModel {
|
||||
return video.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVideo(final Video video) {
|
||||
this.video.set(video);
|
||||
}
|
||||
|
||||
ObjectProperty<Video> videoProperty() {
|
||||
return video;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExtractionModel extractionModel() {
|
||||
return extractionModel.get();
|
||||
@@ -101,76 +90,6 @@ public class FXWorkModel implements WorkModel {
|
||||
return extractionModel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SubtitleCollection subtitleCollection() {
|
||||
return subtitleCollection.get();
|
||||
}
|
||||
|
||||
ReadOnlyObjectProperty<SubtitleCollection> subtitleCollectionProperty() {
|
||||
return subtitleCollection.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
ObjectProperty<Video> videoProperty() {
|
||||
return video;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableList<EditableSubtitle> subtitles() {
|
||||
return subtitles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<EditableSubtitle> originalSubtitles() {
|
||||
return originalSubtitles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String text() {
|
||||
return text.get();
|
||||
}
|
||||
|
||||
ReadOnlyStringProperty textProperty() {
|
||||
return text.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public EditableSubtitle selectedSubtitle() {
|
||||
return subtitle.get();
|
||||
}
|
||||
|
||||
ObjectProperty<EditableSubtitle> selectedSubtitleProperty() {
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableList<Language> availableVideoLanguages() {
|
||||
return availableVideoLanguages;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableList<Language> availableTranslationsLanguage() {
|
||||
return availableTranslationLanguages;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Language videoLanguage() {
|
||||
return videoLanguage.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVideoLanguage(final Language language) {
|
||||
videoLanguage.set(language);
|
||||
}
|
||||
|
||||
ObjectProperty<Language> videoLanguageProperty() {
|
||||
return videoLanguage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableList<Language> translations() {
|
||||
return translations;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WorkStatus status() {
|
||||
return workStatus.get();
|
||||
@@ -198,4 +117,66 @@ public class FXWorkModel implements WorkModel {
|
||||
DoubleProperty progressProperty() {
|
||||
return progress;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canExtract() {
|
||||
return canExtract.get();
|
||||
}
|
||||
|
||||
ReadOnlyBooleanProperty canExtractProperty() {
|
||||
return canExtract.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canExport() {
|
||||
return canExport.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCanExport(final boolean canExport) {
|
||||
this.canExport.set(canExport);
|
||||
}
|
||||
|
||||
BooleanProperty canExportProperty() {
|
||||
return canExport;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isProgressVisible() {
|
||||
return isProgressVisible.get();
|
||||
}
|
||||
|
||||
ReadOnlyBooleanProperty isProgressVisibleProperty() {
|
||||
return isProgressVisible.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SubtitleCollection<?> extractedCollection() {
|
||||
return extractedCollection.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setExtractedCollection(final SubtitleCollection<?> collection) {
|
||||
extractedCollection.set(collection);
|
||||
}
|
||||
|
||||
ObjectProperty<SubtitleCollection<?>> extractedCollectionProperty() {
|
||||
return extractedCollection;
|
||||
}
|
||||
|
||||
ObjectProperty<EditableSubtitle> selectedSubtitleProperty() {
|
||||
return selectedSubtitle;
|
||||
}
|
||||
|
||||
ObservableList<ObservableSubtitleImpl> subtitles() {
|
||||
return subtitles;
|
||||
}
|
||||
|
||||
ObservableMap<Language, ObservableSubtitleCollectionImpl> collections() {
|
||||
return collections;
|
||||
}
|
||||
|
||||
ObjectProperty<Language> videoLanguageProperty() {
|
||||
return videoLanguage;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ package com.github.gtache.autosubtitle.gui.fx;
|
||||
import com.github.gtache.autosubtitle.Language;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
/**
|
||||
* Converts a language to a string and vice-versa
|
||||
*/
|
||||
class LanguageStringConverter extends StringConverter<Language> {
|
||||
@Override
|
||||
public String toString(final Language object) {
|
||||
|
||||
@@ -5,6 +5,9 @@ import javafx.util.StringConverter;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Converts a time in milliseconds to a string and vice-versa
|
||||
*/
|
||||
class TimeStringConverter extends StringConverter<Long> {
|
||||
|
||||
private final TimeFormatter timeFormatter;
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.github.gtache.autosubtitle.gui.fx.FXMainController;
|
||||
import com.github.gtache.autosubtitle.gui.fx.FXMediaController;
|
||||
import com.github.gtache.autosubtitle.gui.fx.FXParametersController;
|
||||
import com.github.gtache.autosubtitle.gui.fx.FXSetupController;
|
||||
import com.github.gtache.autosubtitle.gui.fx.FXSubtitlesController;
|
||||
import com.github.gtache.autosubtitle.gui.fx.FXWorkController;
|
||||
import com.github.gtache.autosubtitle.modules.gui.impl.Pause;
|
||||
import com.github.gtache.autosubtitle.modules.gui.impl.Play;
|
||||
@@ -31,21 +32,25 @@ public abstract class FXModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
static FXMLLoader providesFXMLLoader(final FXMainController mainController, final FXSetupController setupController, final FXParametersController parametersController,
|
||||
final FXWorkController workController, final FXMediaController mediaController, final ResourceBundle bundle) {
|
||||
static FXMLLoader providesFXMLLoader(final FXMainController mainController, final FXMediaController mediaController,
|
||||
final FXParametersController parametersController, final FXSetupController setupController,
|
||||
final FXSubtitlesController subtitlesController, final FXWorkController workController,
|
||||
final ResourceBundle bundle) {
|
||||
final var loader = new FXMLLoader(FXModule.class.getResource("/com/github/gtache/autosubtitle/gui/fx/mainView.fxml"));
|
||||
loader.setResources(bundle);
|
||||
loader.setControllerFactory(c -> {
|
||||
if (c == FXMainController.class) {
|
||||
return mainController;
|
||||
} else if (c == FXSetupController.class) {
|
||||
return setupController;
|
||||
} else if (c == FXWorkController.class) {
|
||||
return workController;
|
||||
} else if (c == FXMediaController.class) {
|
||||
return mediaController;
|
||||
} else if (c == FXParametersController.class) {
|
||||
return parametersController;
|
||||
} else if (c == FXSetupController.class) {
|
||||
return setupController;
|
||||
} else if (c == FXSubtitlesController.class) {
|
||||
return subtitlesController;
|
||||
} else if (c == FXWorkController.class) {
|
||||
return workController;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown controller " + c);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.github.gtache.autosubtitle.subtitle.gui.fx;
|
||||
|
||||
import com.github.gtache.autosubtitle.Language;
|
||||
import com.github.gtache.autosubtitle.subtitle.SubtitleCollection;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* FX observable implementation of {@link SubtitleCollection}
|
||||
*/
|
||||
public record ObservableSubtitleCollectionImpl(StringProperty textProperty,
|
||||
ObservableList<ObservableSubtitleImpl> observableSubtitles,
|
||||
ObjectProperty<Language> languageProperty) implements SubtitleCollection<ObservableSubtitleImpl> {
|
||||
|
||||
public ObservableSubtitleCollectionImpl() {
|
||||
this(new SimpleStringProperty(""), FXCollections.observableArrayList(), new SimpleObjectProperty<>());
|
||||
}
|
||||
|
||||
public ObservableSubtitleCollectionImpl(final SubtitleCollection<?> subtitleCollection) {
|
||||
this(new SimpleStringProperty(subtitleCollection.text()),
|
||||
FXCollections.observableArrayList(subtitleCollection.subtitles().stream().map(ObservableSubtitleImpl::new).toList()),
|
||||
new SimpleObjectProperty<>(subtitleCollection.language()));
|
||||
}
|
||||
|
||||
public ObservableSubtitleCollectionImpl {
|
||||
Objects.requireNonNull(textProperty);
|
||||
Objects.requireNonNull(observableSubtitles);
|
||||
Objects.requireNonNull(languageProperty);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String text() {
|
||||
return textProperty.get();
|
||||
}
|
||||
|
||||
public void setText(final String text) {
|
||||
this.textProperty.set(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableList<ObservableSubtitleImpl> subtitles() {
|
||||
return observableSubtitles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Language language() {
|
||||
return languageProperty.get();
|
||||
}
|
||||
|
||||
public void setLanguage(final Language language) {
|
||||
this.languageProperty.set(language);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import com.github.gtache.autosubtitle.subtitle.Bounds;
|
||||
import com.github.gtache.autosubtitle.subtitle.EditableSubtitle;
|
||||
import com.github.gtache.autosubtitle.subtitle.Font;
|
||||
import com.github.gtache.autosubtitle.subtitle.Subtitle;
|
||||
import com.github.gtache.autosubtitle.subtitle.impl.BoundsImpl;
|
||||
import javafx.beans.property.LongProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleLongProperty;
|
||||
@@ -12,104 +11,86 @@ import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
|
||||
/**
|
||||
* FX implementation of {@link EditableSubtitle}
|
||||
*/
|
||||
public class ObservableSubtitleImpl implements EditableSubtitle {
|
||||
import java.util.Objects;
|
||||
|
||||
private final StringProperty content;
|
||||
private final LongProperty start;
|
||||
private final LongProperty end;
|
||||
private final ObjectProperty<Font> font;
|
||||
private final ObjectProperty<Bounds> location;
|
||||
/**
|
||||
* FX observable implementation of {@link EditableSubtitle}
|
||||
*/
|
||||
public record ObservableSubtitleImpl(StringProperty contentProperty, LongProperty startProperty,
|
||||
LongProperty endProperty, ObjectProperty<Font> fontProperty,
|
||||
ObjectProperty<Bounds> boundsProperty) implements EditableSubtitle {
|
||||
|
||||
public ObservableSubtitleImpl() {
|
||||
this("");
|
||||
}
|
||||
|
||||
public ObservableSubtitleImpl(final String content) {
|
||||
this.content = new SimpleStringProperty(content);
|
||||
this.start = new SimpleLongProperty(0);
|
||||
this.end = new SimpleLongProperty(0);
|
||||
this.font = new SimpleObjectProperty<>();
|
||||
this.location = new SimpleObjectProperty<>(new BoundsImpl(0, 0, 100, 12));
|
||||
this(content, 0, 0, null, null);
|
||||
}
|
||||
|
||||
public ObservableSubtitleImpl(final Subtitle subtitle) {
|
||||
this.content = new SimpleStringProperty(subtitle.content());
|
||||
this.start = new SimpleLongProperty(subtitle.start());
|
||||
this.end = new SimpleLongProperty(subtitle.end());
|
||||
this.font = new SimpleObjectProperty<>(subtitle.font());
|
||||
this.location = new SimpleObjectProperty<>(subtitle.bounds());
|
||||
this(subtitle.content(), subtitle.start(), subtitle.end(), subtitle.font(), subtitle.bounds());
|
||||
}
|
||||
|
||||
public ObservableSubtitleImpl(final String content, final long start, final long end, final Font font, final Bounds bounds) {
|
||||
this(new SimpleStringProperty(content), new SimpleLongProperty(start), new SimpleLongProperty(end), new SimpleObjectProperty<>(font), new SimpleObjectProperty<>(bounds));
|
||||
}
|
||||
|
||||
public ObservableSubtitleImpl {
|
||||
Objects.requireNonNull(contentProperty);
|
||||
Objects.requireNonNull(startProperty);
|
||||
Objects.requireNonNull(endProperty);
|
||||
Objects.requireNonNull(fontProperty);
|
||||
Objects.requireNonNull(boundsProperty);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String content() {
|
||||
return content.get();
|
||||
return contentProperty.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setContent(final String content) {
|
||||
this.content.set(content);
|
||||
}
|
||||
|
||||
public StringProperty contentProperty() {
|
||||
return content;
|
||||
this.contentProperty.set(content);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long start() {
|
||||
return start.get();
|
||||
return startProperty.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStart(final long start) {
|
||||
this.start.set(start);
|
||||
}
|
||||
|
||||
public LongProperty startProperty() {
|
||||
return start;
|
||||
this.startProperty.set(start);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long end() {
|
||||
return end.get();
|
||||
return endProperty.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnd(final long end) {
|
||||
this.end.set(end);
|
||||
}
|
||||
|
||||
public LongProperty endProperty() {
|
||||
return end;
|
||||
this.endProperty.set(end);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Font font() {
|
||||
return font.get();
|
||||
return fontProperty.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFont(final Font font) {
|
||||
this.font.set(font);
|
||||
}
|
||||
|
||||
public ObjectProperty<Font> fontProperty() {
|
||||
return font;
|
||||
this.fontProperty.set(font);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bounds bounds() {
|
||||
return location.get();
|
||||
return boundsProperty.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBounds(final Bounds bounds) {
|
||||
this.location.set(bounds);
|
||||
}
|
||||
|
||||
public ObjectProperty<Bounds> locationProperty() {
|
||||
return location;
|
||||
this.boundsProperty.set(bounds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,5 +14,6 @@ module com.github.gtache.autosubtitle.gui.fx {
|
||||
|
||||
exports com.github.gtache.autosubtitle.gui.fx;
|
||||
exports com.github.gtache.autosubtitle.modules.gui.fx;
|
||||
exports com.github.gtache.autosubtitle.subtitle.gui.fx;
|
||||
opens com.github.gtache.autosubtitle.gui.fx to javafx.fxml;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.ComboBox?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.Tab?>
|
||||
<?import javafx.scene.control.TabPane?>
|
||||
<?import javafx.scene.control.TableColumn?>
|
||||
<?import javafx.scene.control.TableView?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import org.controlsfx.control.PrefixSelectionComboBox?>
|
||||
|
||||
<VBox spacing="10" xmlns="http://javafx.com/javafx/22" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.github.gtache.autosubtitle.gui.fx.FXSubtitlesController">
|
||||
<children>
|
||||
<HBox alignment="CENTER_LEFT" spacing="10.0">
|
||||
<children>
|
||||
<Label text="%subtitles.language.label" />
|
||||
<PrefixSelectionComboBox fx:id="languageCombobox" />
|
||||
<Label text="%subtitles.translate.label" />
|
||||
<ComboBox fx:id="translationsCombobox" />
|
||||
</children>
|
||||
</HBox>
|
||||
<TabPane fx:id="tabPane" VBox.vgrow="ALWAYS">
|
||||
<tabs>
|
||||
<Tab fx:id="mainSubtitlesTab" closable="false">
|
||||
<content>
|
||||
<TableView fx:id="subtitlesTable" editable="true">
|
||||
<columns>
|
||||
<TableColumn fx:id="startColumn" prefWidth="50.0" sortable="false" text="%subtitles.table.column.from.label" />
|
||||
<TableColumn fx:id="endColumn" prefWidth="50.0" sortable="false" text="%subtitles.table.column.to.label" />
|
||||
<TableColumn fx:id="textColumn" prefWidth="75.0" sortable="false" text="%subtitles.table.column.text.label" />
|
||||
</columns>
|
||||
<columnResizePolicy>
|
||||
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
|
||||
</columnResizePolicy>
|
||||
</TableView>
|
||||
</content>
|
||||
</Tab>
|
||||
</tabs>
|
||||
</TabPane>
|
||||
<HBox spacing="10.0">
|
||||
<children>
|
||||
<Button fx:id="loadButton" mnemonicParsing="false" onAction="#loadPressed" text="%subtitles.button.load.label" />
|
||||
<Button fx:id="saveButton" mnemonicParsing="false" onAction="#savePressed" text="%subtitles.button.subtitles.save.label" />
|
||||
<Button fx:id="resetButton" mnemonicParsing="false" onAction="#resetButtonPressed" text="%subtitles.button.reset.label" />
|
||||
<Button fx:id="addButton" mnemonicParsing="false" onAction="#addPressed" text="+" />
|
||||
</children>
|
||||
</HBox>
|
||||
</children>
|
||||
</VBox>
|
||||
@@ -4,16 +4,12 @@
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.ProgressBar?>
|
||||
<?import javafx.scene.control.TableColumn?>
|
||||
<?import javafx.scene.control.TableView?>
|
||||
<?import javafx.scene.control.TextField?>
|
||||
<?import javafx.scene.control.Tooltip?>
|
||||
<?import javafx.scene.layout.ColumnConstraints?>
|
||||
<?import javafx.scene.layout.GridPane?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.RowConstraints?>
|
||||
<?import org.controlsfx.control.CheckComboBox?>
|
||||
<?import org.controlsfx.control.PrefixSelectionComboBox?>
|
||||
|
||||
<GridPane hgap="10.0" vgap="10.0" xmlns="http://javafx.com/javafx/22" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.github.gtache.autosubtitle.gui.fx.FXWorkController">
|
||||
<columnConstraints>
|
||||
@@ -22,9 +18,9 @@
|
||||
<ColumnConstraints hgrow="SOMETIMES" />
|
||||
</columnConstraints>
|
||||
<rowConstraints>
|
||||
<RowConstraints vgrow="SOMETIMES" />
|
||||
<RowConstraints vgrow="NEVER" />
|
||||
<RowConstraints vgrow="ALWAYS" />
|
||||
<RowConstraints vgrow="SOMETIMES" />
|
||||
<RowConstraints vgrow="NEVER" />
|
||||
<RowConstraints vgrow="NEVER" />
|
||||
</rowConstraints>
|
||||
<children>
|
||||
@@ -49,36 +45,8 @@
|
||||
</Button>
|
||||
</children>
|
||||
</HBox>
|
||||
<TableView fx:id="subtitlesTable" editable="true" GridPane.rowIndex="1">
|
||||
<columns>
|
||||
<TableColumn fx:id="startColumn" prefWidth="50.0" sortable="false" text="%work.table.column.from.label" />
|
||||
<TableColumn fx:id="endColumn" prefWidth="50.0" sortable="false" text="%work.table.column.to.label" />
|
||||
<TableColumn fx:id="textColumn" prefWidth="75.0" sortable="false" text="%work.table.column.text.label" />
|
||||
</columns>
|
||||
<columnResizePolicy>
|
||||
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
|
||||
</columnResizePolicy>
|
||||
</TableView>
|
||||
<HBox spacing="10.0" GridPane.rowIndex="2">
|
||||
<children>
|
||||
<Button fx:id="loadSubtitlesButton" mnemonicParsing="false" onAction="#loadSubtitlesPressed" text="%work.button.load.label" />
|
||||
<Button fx:id="saveSubtitlesButton" mnemonicParsing="false" onAction="#saveSubtitlesPressed" text="%work.button.subtitles.save.label" />
|
||||
<Button fx:id="resetButton" mnemonicParsing="false" onAction="#resetButtonPressed" text="%work.button.reset.label" />
|
||||
<Button fx:id="addSubtitleButton" mnemonicParsing="false" onAction="#addSubtitlePressed" text="+" />
|
||||
</children>
|
||||
<GridPane.margin>
|
||||
<Insets />
|
||||
</GridPane.margin>
|
||||
</HBox>
|
||||
<fx:include fx:id="subtitles" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" source="subtitlesView.fxml" GridPane.columnIndex="0" GridPane.columnSpan="1" GridPane.rowIndex="0" GridPane.rowSpan="3" GridPane.vgrow="ALWAYS" />
|
||||
<fx:include fx:id="media" source="mediaView.fxml" GridPane.columnIndex="1" GridPane.columnSpan="2147483647" GridPane.rowIndex="1" />
|
||||
<HBox alignment="CENTER_LEFT" spacing="10.0">
|
||||
<children>
|
||||
<Label text="%work.language.label" />
|
||||
<PrefixSelectionComboBox fx:id="languageCombobox" />
|
||||
<Label text="%work.translate.label" />
|
||||
<CheckComboBox fx:id="translationsCombobox" />
|
||||
</children>
|
||||
</HBox>
|
||||
<Label fx:id="progressLabel" GridPane.columnIndex="1" GridPane.rowIndex="3" />
|
||||
<HBox spacing="10.0" GridPane.columnIndex="2" GridPane.rowIndex="3">
|
||||
<children>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.github.gtache.autosubtitle.gui.fx;
|
||||
|
||||
import com.github.gtache.autosubtitle.gui.TimeFormatter;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class TestColonTimeFormatter {
|
||||
|
||||
private final TimeFormatter timeFormatter = new ColonTimeFormatter();
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"12:34:56,45296000",
|
||||
"12:34,754000",
|
||||
"01:02,62000",
|
||||
"1:2,62000",
|
||||
"01:02:03,3723000",
|
||||
"1:2:3,3723000",
|
||||
"00:00:03,3000",
|
||||
"00:03,3000",
|
||||
"1234:00:01,4442401000"
|
||||
})
|
||||
void testParse(final String time, final long millis) {
|
||||
assertEquals(millis, timeFormatter.parse(time));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"45296000,12:34:56",
|
||||
"45296521,12:34:56",
|
||||
"754000,12:34",
|
||||
"754620,12:34",
|
||||
"62000,01:02",
|
||||
"3723000,1:02:03",
|
||||
"3000,00:03",
|
||||
"4442401000,1234:00:01"
|
||||
})
|
||||
void testFormat(final long millis, final String time) {
|
||||
assertEquals(time, timeFormatter.format(millis));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.github.gtache.autosubtitle.gui.fx;
|
||||
|
||||
import com.github.gtache.autosubtitle.modules.gui.fx.DaggerResourceComponent;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.TabPane;
|
||||
import javafx.stage.Stage;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.testfx.api.FxRobot;
|
||||
import org.testfx.api.FxToolkit;
|
||||
import org.testfx.util.WaitForAsyncUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
|
||||
class TestFXMainController extends FxRobot {
|
||||
|
||||
private static final ResourceBundle BUNDLE = DaggerResourceComponent.builder().build().getBundle();
|
||||
private final FXMainModel model;
|
||||
private final FXMainController controller;
|
||||
private final Stage window;
|
||||
|
||||
TestFXMainController() throws TimeoutException {
|
||||
this.model = spy(new FXMainModel());
|
||||
this.controller = spy(new FXMainController(model));
|
||||
|
||||
this.window = FxToolkit.registerPrimaryStage();
|
||||
FxToolkit.setupStage(w -> {
|
||||
final var loader = new FXMLLoader(getClass().getResource("/com/github/gtache/autosubtitle/gui/fx/mainView.fxml"));
|
||||
loader.setResources(BUNDLE);
|
||||
loader.setControllerFactory(c -> {
|
||||
if (c == FXMainController.class) {
|
||||
return controller;
|
||||
} else {
|
||||
return mock(c);
|
||||
}
|
||||
});
|
||||
try {
|
||||
final Parent parent = loader.load();
|
||||
final var scene = new Scene(parent);
|
||||
w.setScene(scene);
|
||||
} catch (final IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
});
|
||||
FxToolkit.showStage();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void afterEach() throws Throwable {
|
||||
WaitForAsyncUtils.waitForFxEvents();
|
||||
WaitForAsyncUtils.checkException();
|
||||
FxToolkit.cleanupStages();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testModel() {
|
||||
assertEquals(model, controller.model());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWindow() {
|
||||
assertEquals(window, controller.window());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSelectTab() {
|
||||
final var tabPane = lookup("#tabPane").queryAs(TabPane.class);
|
||||
assertEquals(0, tabPane.getSelectionModel().getSelectedIndex());
|
||||
interact(() -> controller.selectTab(1));
|
||||
assertEquals(1, model.selectedTab());
|
||||
assertEquals(1, tabPane.getSelectionModel().getSelectedIndex());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSelectedTabBinding() {
|
||||
final var tabPane = lookup("#tabPane").queryAs(TabPane.class);
|
||||
assertEquals(0, model.selectedTab());
|
||||
interact(() -> controller.selectTab(1));
|
||||
assertEquals(1, model.selectedTab());
|
||||
assertEquals(1, tabPane.getSelectionModel().getSelectedIndex());
|
||||
|
||||
interact(() -> model.setSelectedTab(0));
|
||||
assertEquals(0, tabPane.getSelectionModel().getSelectedIndex());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.github.gtache.autosubtitle.gui.fx;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class TestFXMainModel {
|
||||
|
||||
private final FXMainModel model = new FXMainModel();
|
||||
|
||||
@Test
|
||||
void testSelectTab() {
|
||||
assertEquals(0, model.selectedTab());
|
||||
assertEquals(0, model.selectedTabProperty().get());
|
||||
final var newTab = 1;
|
||||
model.setSelectedTab(newTab);
|
||||
assertEquals(newTab, model.selectedTab());
|
||||
assertEquals(newTab, model.selectedTabProperty().get());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.github.gtache.autosubtitle.gui.fx;
|
||||
|
||||
import com.github.gtache.autosubtitle.Video;
|
||||
import com.github.gtache.autosubtitle.subtitle.gui.fx.ObservableSubtitleImpl;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
class TestFXMediaBinder {
|
||||
|
||||
@Test
|
||||
void testCreateBindings() {
|
||||
final var workModel = new FXWorkModel();
|
||||
final var mediaModel = new FXMediaModel();
|
||||
final var binder = new FXMediaBinder(workModel, mediaModel);
|
||||
binder.createBindings();
|
||||
|
||||
assertNull(mediaModel.video());
|
||||
assertNull(workModel.video());
|
||||
|
||||
final var video1 = mock(Video.class);
|
||||
mediaModel.setVideo(video1);
|
||||
assertEquals(video1, workModel.video());
|
||||
|
||||
final var video2 = mock(Video.class);
|
||||
workModel.setVideo(video2);
|
||||
assertEquals(video2, mediaModel.video());
|
||||
|
||||
final var subtitles = List.of(mock(ObservableSubtitleImpl.class));
|
||||
|
||||
assertEquals(List.of(), workModel.subtitles());
|
||||
assertEquals(List.of(), mediaModel.subtitles());
|
||||
|
||||
workModel.subtitles().setAll(subtitles);
|
||||
assertEquals(subtitles, mediaModel.subtitles());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
package com.github.gtache.autosubtitle.gui.fx;
|
||||
|
||||
import com.github.gtache.autosubtitle.Video;
|
||||
import com.github.gtache.autosubtitle.gui.TimeFormatter;
|
||||
import com.github.gtache.autosubtitle.impl.FileVideoImpl;
|
||||
import com.github.gtache.autosubtitle.impl.VideoInfoImpl;
|
||||
import com.github.gtache.autosubtitle.modules.gui.fx.DaggerResourceComponent;
|
||||
import com.github.gtache.autosubtitle.modules.gui.fx.ResourceComponent;
|
||||
import com.github.gtache.autosubtitle.subtitle.Subtitle;
|
||||
import com.github.gtache.autosubtitle.subtitle.gui.fx.ObservableSubtitleImpl;
|
||||
import com.github.gtache.autosubtitle.subtitle.gui.fx.SubtitleLabel;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Slider;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.media.MediaPlayer;
|
||||
import javafx.scene.media.MediaView;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.testfx.api.FxRobot;
|
||||
import org.testfx.api.FxToolkit;
|
||||
import org.testfx.util.WaitForAsyncUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class TestFXMediaController extends FxRobot {
|
||||
|
||||
private static final ResourceComponent RESOURCE_COMPONENT = DaggerResourceComponent.builder().build();
|
||||
private static final ResourceBundle BUNDLE = RESOURCE_COMPONENT.getBundle();
|
||||
private final FXMediaModel model;
|
||||
private final FXMediaController controller;
|
||||
private final FXMediaBinder binder;
|
||||
private final Image playImage;
|
||||
private final Image pauseImage;
|
||||
private final Video video;
|
||||
|
||||
TestFXMediaController(@Mock final FXMediaBinder binder, @Mock final TimeFormatter timeFormatter) throws TimeoutException, URISyntaxException {
|
||||
this.binder = requireNonNull(binder);
|
||||
when(timeFormatter.format(anyLong(), anyLong())).then(i -> i.getArgument(0) + "/" + i.getArgument(1));
|
||||
|
||||
this.playImage = RESOURCE_COMPONENT.getPlayImage();
|
||||
this.pauseImage = RESOURCE_COMPONENT.getPauseImage();
|
||||
this.model = spy(new FXMediaModel());
|
||||
this.controller = spy(new FXMediaController(model, binder, timeFormatter, playImage, pauseImage));
|
||||
final var info = new VideoInfoImpl("mp4", 1280, 720, 30000L);
|
||||
final var path = Paths.get(getClass().getResource("/com/github/gtache/autosubtitle/gui/fx/video.mp4").toURI());
|
||||
this.video = new FileVideoImpl(path, info);
|
||||
|
||||
FxToolkit.registerPrimaryStage();
|
||||
FxToolkit.setupStage(w -> {
|
||||
final var loader = new FXMLLoader(getClass().getResource("/com/github/gtache/autosubtitle/gui/fx/mediaView.fxml"));
|
||||
loader.setResources(BUNDLE);
|
||||
loader.setControllerFactory(c -> controller);
|
||||
try {
|
||||
final Parent parent = loader.load();
|
||||
final var scene = new Scene(parent);
|
||||
w.setScene(scene);
|
||||
} catch (final IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
});
|
||||
FxToolkit.showStage();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void afterEach() throws Throwable {
|
||||
WaitForAsyncUtils.waitForFxEvents();
|
||||
WaitForAsyncUtils.checkException();
|
||||
FxToolkit.cleanupStages();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInitialized() {
|
||||
verify(binder).createBindings();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testVolumeLabel() {
|
||||
final var label = lookup("#volumeValueLabel").queryLabeled();
|
||||
|
||||
assertEquals("100", label.getText());
|
||||
assertEquals(1, model.volume());
|
||||
|
||||
interact(() -> model.setVolume(0.5));
|
||||
assertEquals("50", label.getText());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testVolume() {
|
||||
final var slider = lookup("#volumeSlider").queryAs(Slider.class);
|
||||
|
||||
assertEquals(100, slider.getValue());
|
||||
assertEquals(1, model.volume());
|
||||
|
||||
interact(() -> model.setVolume(0.5));
|
||||
assertEquals(50, slider.getValue());
|
||||
|
||||
interact(() -> slider.setValue(80));
|
||||
assertEquals(0.8, model.volume());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testVolumePlayer() {
|
||||
loadVideo();
|
||||
final var player = lookup("#videoView").queryAs(MediaView.class).getMediaPlayer();
|
||||
assertEquals(1, model.volume());
|
||||
assertEquals(1, player.getVolume());
|
||||
interact(() -> model.setVolume(0.5));
|
||||
assertEquals(0.5, player.getVolume());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPlayLabel() {
|
||||
final var label = lookup("#playLabel").queryLabeled();
|
||||
assertEquals("0/0", label.getText());
|
||||
loadVideo();
|
||||
assertEquals("0/30000", label.getText());
|
||||
interact(() -> model.setPosition(15));
|
||||
assertEquals("15/30000", label.getText());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPosition() {
|
||||
final var slider = lookup("#playSlider").queryAs(Slider.class);
|
||||
assertEquals(100.0, slider.getMax());
|
||||
assertEquals(0, model.position());
|
||||
assertEquals(0, slider.getValue());
|
||||
loadVideo();
|
||||
assertEquals(video.info().duration(), slider.getMax());
|
||||
assertEquals(0, model.position());
|
||||
assertEquals(0, slider.getValue());
|
||||
interact(() -> model.setPosition(15));
|
||||
assertEquals(15, slider.getValue());
|
||||
interact(() -> slider.setValue(20));
|
||||
assertEquals(20, model.position());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPositionPlayer() {
|
||||
loadVideo();
|
||||
final var player = lookup("#videoView").queryAs(MediaView.class).getMediaPlayer();
|
||||
assertEquals(0, model.position());
|
||||
assertEquals(0, player.getCurrentTime().toMillis());
|
||||
clickOn("#playSlider");
|
||||
assertNotEquals(0, player.getCurrentTime().toMillis());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPlayButtonState() {
|
||||
final var button = lookup("#playButton").queryAs(Button.class);
|
||||
assertTrue(button.isDisabled());
|
||||
assertEquals(playImage, ((ImageView) button.getGraphic()).getImage());
|
||||
loadVideo();
|
||||
assertFalse(button.isDisabled());
|
||||
assertEquals(playImage, ((ImageView) button.getGraphic()).getImage());
|
||||
interact(() -> model.setIsPlaying(true));
|
||||
assertEquals(pauseImage, ((ImageView) button.getGraphic()).getImage());
|
||||
interact(() -> model.setIsPlaying(false));
|
||||
assertEquals(playImage, ((ImageView) button.getGraphic()).getImage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPlayPressed() {
|
||||
final var button = lookup("#playButton").queryAs(Button.class);
|
||||
loadVideo();
|
||||
assertFalse(model.isPlaying());
|
||||
clickOn(button);
|
||||
assertTrue(model.isPlaying());
|
||||
clickOn(button);
|
||||
assertFalse(model.isPlaying());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPlay() {
|
||||
loadVideo();
|
||||
assertFalse(model.isPlaying());
|
||||
interact(controller::play);
|
||||
assertTrue(model.isPlaying());
|
||||
interact(controller::play);
|
||||
assertTrue(model.isPlaying());
|
||||
interact(controller::pause);
|
||||
assertFalse(model.isPlaying());
|
||||
interact(controller::pause);
|
||||
assertFalse(model.isPlaying());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPlayPlayer() {
|
||||
loadVideo();
|
||||
final var player = lookup("#videoView").queryAs(MediaView.class).getMediaPlayer();
|
||||
assertFalse(model.isPlaying());
|
||||
assertNotEquals(MediaPlayer.Status.PLAYING, player.getStatus());
|
||||
interact(() -> model.setIsPlaying(true));
|
||||
assertEquals(MediaPlayer.Status.PLAYING, player.getStatus());
|
||||
interact(() -> model.setIsPlaying(false));
|
||||
assertEquals(MediaPlayer.Status.PAUSED, player.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSeek() {
|
||||
loadVideo();
|
||||
assertEquals(0, model.position());
|
||||
interact(() -> controller.seek(15));
|
||||
assertEquals(15, model.position());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRestart() {
|
||||
loadVideo();
|
||||
interact(() -> model.setPosition(model.duration()));
|
||||
interact(() -> model.setIsPlaying(true));
|
||||
interact(() -> model.setIsPlaying(false));
|
||||
assertTrue(model.position() < 1000);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoadNotFile() {
|
||||
final var newVideo = mock(Video.class);
|
||||
when(newVideo.info()).thenReturn(new VideoInfoImpl("mp4", 1, 1, 1));
|
||||
interact(() -> model.videoProperty().set(newVideo));
|
||||
assertNull(model.video());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSubtitles() {
|
||||
loadVideo();
|
||||
final var subtitle1 = new ObservableSubtitleImpl("subtitle1", 1000, 2000, null, null);
|
||||
final var subtitle2 = new ObservableSubtitleImpl("subtitle2", 1000, 3000, null, null);
|
||||
final var subtitle3 = new ObservableSubtitleImpl("subtitle3", 3000, 4000, null, null);
|
||||
interact(() -> model.subtitles().setAll(List.of(subtitle1, subtitle2, subtitle3)));
|
||||
interact(() -> controller.seek(1000));
|
||||
final var pane = lookup("#stackPane").queryAs(StackPane.class);
|
||||
var visibleSubtitles = getSubtitles(pane);
|
||||
assertEquals(2, visibleSubtitles.size());
|
||||
assertTrue(visibleSubtitles.contains(subtitle1));
|
||||
assertTrue(visibleSubtitles.contains(subtitle2));
|
||||
|
||||
interact(() -> controller.seek(2000));
|
||||
visibleSubtitles = getSubtitles(pane);
|
||||
assertEquals(2, visibleSubtitles.size());
|
||||
assertTrue(visibleSubtitles.contains(subtitle1));
|
||||
assertTrue(visibleSubtitles.contains(subtitle2));
|
||||
|
||||
interact(() -> controller.seek(3000));
|
||||
visibleSubtitles = getSubtitles(pane);
|
||||
assertEquals(2, visibleSubtitles.size());
|
||||
assertTrue(visibleSubtitles.contains(subtitle2));
|
||||
assertTrue(visibleSubtitles.contains(subtitle3));
|
||||
|
||||
interact(() -> controller.seek(4000));
|
||||
visibleSubtitles = getSubtitles(pane);
|
||||
assertEquals(1, visibleSubtitles.size());
|
||||
assertTrue(visibleSubtitles.contains(subtitle3));
|
||||
}
|
||||
|
||||
private static Set<Subtitle> getSubtitles(final StackPane stackPane) {
|
||||
return stackPane.getChildren().stream().filter(SubtitleLabel.class::isInstance).map(SubtitleLabel.class::cast).map(SubtitleLabel::subtitle).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testModel() {
|
||||
assertEquals(model, controller.model());
|
||||
}
|
||||
|
||||
private void loadVideo() {
|
||||
interact(() -> model.setVideo(video));
|
||||
waitForPlayerReady();
|
||||
}
|
||||
|
||||
private void waitForPlayerReady() {
|
||||
if (lookup("#videoView").queryAs(MediaView.class).getMediaPlayer().getStatus() == MediaPlayer.Status.UNKNOWN) {
|
||||
sleep(100L);
|
||||
waitForPlayerReady();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.github.gtache.autosubtitle.gui.fx;
|
||||
|
||||
import com.github.gtache.autosubtitle.Video;
|
||||
import com.github.gtache.autosubtitle.VideoInfo;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class TestFXMediaModel {
|
||||
private static final double DEFAULT_VOLUME = 1.0d;
|
||||
|
||||
private final FXMediaModel model = new FXMediaModel();
|
||||
|
||||
@Test
|
||||
void testVideo() {
|
||||
assertNull(model.video());
|
||||
assertNull(model.videoProperty().get());
|
||||
final var video = mock(Video.class);
|
||||
model.setVideo(video);
|
||||
assertEquals(video, model.video());
|
||||
assertEquals(video, model.videoProperty().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testVolume() {
|
||||
assertEquals(DEFAULT_VOLUME, model.volume());
|
||||
assertEquals(DEFAULT_VOLUME, model.volumeProperty().get());
|
||||
final var volume = 0.5d;
|
||||
model.setVolume(volume);
|
||||
assertEquals(volume, model.volume());
|
||||
assertEquals(volume, model.volumeProperty().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsPlaying() {
|
||||
assertFalse(model.isPlaying());
|
||||
assertFalse(model.isPlayingProperty().get());
|
||||
model.setIsPlaying(true);
|
||||
assertTrue(model.isPlaying());
|
||||
assertTrue(model.isPlayingProperty().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPosition() {
|
||||
assertEquals(0L, model.position());
|
||||
assertEquals(0L, model.positionProperty().get());
|
||||
final var position = 100L;
|
||||
model.setPosition(position);
|
||||
assertEquals(position, model.position());
|
||||
assertEquals(position, model.positionProperty().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDuration() {
|
||||
assertEquals(0L, model.duration());
|
||||
assertEquals(0L, model.durationProperty().get());
|
||||
final var video = mock(Video.class);
|
||||
final var info = mock(VideoInfo.class);
|
||||
final var duration = 100L;
|
||||
when(video.info()).thenReturn(info);
|
||||
when(info.duration()).thenReturn(duration);
|
||||
model.setVideo(video);
|
||||
assertEquals(duration, model.duration());
|
||||
assertEquals(duration, model.durationProperty().get());
|
||||
model.setVideo(null);
|
||||
assertEquals(0L, model.duration());
|
||||
assertEquals(0L, model.durationProperty().get());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.github.gtache.autosubtitle.gui.fx;
|
||||
|
||||
import com.github.gtache.autosubtitle.Translator;
|
||||
import com.github.gtache.autosubtitle.VideoConverter;
|
||||
import com.github.gtache.autosubtitle.VideoLoader;
|
||||
import com.github.gtache.autosubtitle.gui.TimeFormatter;
|
||||
import com.github.gtache.autosubtitle.modules.gui.fx.DaggerResourceComponent;
|
||||
import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverter;
|
||||
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractionModelProvider;
|
||||
import com.github.gtache.autosubtitle.subtitle.extractor.SubtitleExtractor;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.mockito.Mock;
|
||||
import org.testfx.api.FxRobot;
|
||||
import org.testfx.api.FxToolkit;
|
||||
import org.testfx.util.WaitForAsyncUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.Map;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
|
||||
class TestFXParametersController extends FxRobot {
|
||||
|
||||
private static final ResourceBundle BUNDLE = DaggerResourceComponent.builder().build().getBundle();
|
||||
private final FXParametersModel model;
|
||||
private final FXParametersController controller;
|
||||
private final FXWorkBinder binder;
|
||||
private final SubtitleExtractor extractor;
|
||||
private final Map<String, SubtitleConverter> subtitleConverters;
|
||||
private final VideoLoader videoLoader;
|
||||
private final VideoConverter videoConverter;
|
||||
private final Translator translator;
|
||||
private final TimeFormatter timeFormatter;
|
||||
private final Stage window;
|
||||
|
||||
TestFXParametersController(@Mock final SubtitleExtractor extractor, @Mock final SubtitleConverter subtitleConverter, @Mock final VideoLoader videoLoader,
|
||||
@Mock final VideoConverter videoConverter, @Mock final Translator translator, @Mock final TimeFormatter timeFormatter,
|
||||
@Mock final FXWorkBinder binder) throws TimeoutException {
|
||||
this.extractor = requireNonNull(extractor);
|
||||
this.subtitleConverters = Map.of("srt", requireNonNull(subtitleConverter));
|
||||
this.videoLoader = requireNonNull(videoLoader);
|
||||
this.videoConverter = requireNonNull(videoConverter);
|
||||
this.translator = requireNonNull(translator);
|
||||
this.timeFormatter = requireNonNull(timeFormatter);
|
||||
this.model = spy(new FXParametersModel(mock(ExtractionModelProvider.class), "Arial", 12));
|
||||
this.binder = requireNonNull(binder);
|
||||
this.window = FxToolkit.registerPrimaryStage();
|
||||
this.controller = spy(FXParametersController.class);
|
||||
|
||||
FxToolkit.setupStage(w -> {
|
||||
final var loader = new FXMLLoader(getClass().getResource("/com/github/gtache/autosubtitle/gui/fx/parametersView.fxml"));
|
||||
loader.setResources(BUNDLE);
|
||||
loader.setControllerFactory(c -> controller);
|
||||
try {
|
||||
final Parent parent = loader.load();
|
||||
final var scene = new Scene(parent);
|
||||
w.setScene(scene);
|
||||
} catch (final IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
});
|
||||
FxToolkit.showStage();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void afterEach() throws Throwable {
|
||||
WaitForAsyncUtils.waitForFxEvents();
|
||||
WaitForAsyncUtils.checkException();
|
||||
FxToolkit.cleanupStages();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.github.gtache.autosubtitle.gui.fx;
|
||||
|
||||
import com.github.gtache.autosubtitle.subtitle.OutputFormat;
|
||||
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractionModel;
|
||||
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractionModelProvider;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class TestFXParametersModel {
|
||||
|
||||
private static final String DEFAULT_FONT_FAMILY = "Arial";
|
||||
private static final int DEFAULT_FONT_SIZE = 12;
|
||||
private final List<ExtractionModel> availableExtractionModels;
|
||||
private final ExtractionModel defaultExtractionModel;
|
||||
private final ExtractionModelProvider provider;
|
||||
private final FXParametersModel model;
|
||||
|
||||
TestFXParametersModel(@Mock final ExtractionModelProvider extractionModelProvider,
|
||||
@Mock final ExtractionModel defaultExtractionModel,
|
||||
@Mock final ExtractionModel extractionModel) {
|
||||
this.provider = Objects.requireNonNull(extractionModelProvider);
|
||||
this.defaultExtractionModel = Objects.requireNonNull(defaultExtractionModel);
|
||||
when(provider.getDefaultExtractionModel()).thenReturn(defaultExtractionModel);
|
||||
this.availableExtractionModels = List.of(defaultExtractionModel, extractionModel);
|
||||
when(provider.getAvailableExtractionModels()).thenReturn(availableExtractionModels);
|
||||
model = new FXParametersModel(extractionModelProvider, DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAvailableExtractionModels() {
|
||||
assertEquals(availableExtractionModels, model.availableExtractionModels());
|
||||
assertThrows(UnsupportedOperationException.class, () -> model.availableExtractionModels().clear());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExtractionModel() {
|
||||
assertEquals(defaultExtractionModel, model.extractionModel());
|
||||
assertEquals(defaultExtractionModel, model.extractionModelProperty().get());
|
||||
final var otherModel = mock(ExtractionModel.class);
|
||||
model.setExtractionModel(otherModel);
|
||||
assertEquals(otherModel, model.extractionModel());
|
||||
assertEquals(otherModel, model.extractionModelProperty().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAvailableOutputFormats() {
|
||||
assertEquals(List.of(OutputFormat.SRT), model.availableOutputFormats());
|
||||
assertThrows(UnsupportedOperationException.class, () -> model.availableOutputFormats().clear());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOutputFormat() {
|
||||
assertEquals(OutputFormat.SRT, model.outputFormat());
|
||||
assertEquals(OutputFormat.SRT, model.outputFormatProperty().get());
|
||||
model.setOutputFormat(OutputFormat.ASS);
|
||||
assertEquals(OutputFormat.ASS, model.outputFormat());
|
||||
assertEquals(OutputFormat.ASS, model.outputFormatProperty().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAvailableFontFamilies() {
|
||||
assertEquals(List.of(DEFAULT_FONT_FAMILY), model.availableFontFamilies());
|
||||
assertThrows(UnsupportedOperationException.class, () -> model.availableFontFamilies().clear());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFontFamily() {
|
||||
assertEquals(DEFAULT_FONT_FAMILY, model.fontFamily());
|
||||
assertEquals(DEFAULT_FONT_FAMILY, model.fontFamilyProperty().get());
|
||||
final var fontFamily = DEFAULT_FONT_FAMILY + " A";
|
||||
model.setFontFamily(fontFamily);
|
||||
assertEquals(fontFamily, model.fontFamily());
|
||||
assertEquals(fontFamily, model.fontFamilyProperty().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFontSize() {
|
||||
assertEquals(DEFAULT_FONT_SIZE, model.fontSize());
|
||||
assertEquals(DEFAULT_FONT_SIZE, model.fontSizeProperty().get());
|
||||
final var fontSize = DEFAULT_FONT_SIZE + 2;
|
||||
model.setFontSize(fontSize);
|
||||
assertEquals(fontSize, model.fontSize());
|
||||
assertEquals(fontSize, model.fontSizeProperty().get());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.github.gtache.autosubtitle.gui.fx;
|
||||
|
||||
import com.github.gtache.autosubtitle.Translator;
|
||||
import com.github.gtache.autosubtitle.VideoConverter;
|
||||
import com.github.gtache.autosubtitle.VideoLoader;
|
||||
import com.github.gtache.autosubtitle.gui.TimeFormatter;
|
||||
import com.github.gtache.autosubtitle.modules.gui.fx.DaggerResourceComponent;
|
||||
import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverter;
|
||||
import com.github.gtache.autosubtitle.subtitle.extractor.SubtitleExtractor;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.mockito.Mock;
|
||||
import org.testfx.api.FxRobot;
|
||||
import org.testfx.api.FxToolkit;
|
||||
import org.testfx.util.WaitForAsyncUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.Map;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.mockito.Mockito.spy;
|
||||
|
||||
class TestFXSetupController extends FxRobot {
|
||||
|
||||
private static final ResourceBundle BUNDLE = DaggerResourceComponent.builder().build().getBundle();
|
||||
private final FXSetupModel model;
|
||||
private final FXSetupController controller;
|
||||
private final FXWorkBinder binder;
|
||||
private final SubtitleExtractor extractor;
|
||||
private final Map<String, SubtitleConverter> subtitleConverters;
|
||||
private final VideoLoader videoLoader;
|
||||
private final VideoConverter videoConverter;
|
||||
private final Translator translator;
|
||||
private final TimeFormatter timeFormatter;
|
||||
private final Stage window;
|
||||
|
||||
TestFXSetupController(@Mock final SubtitleExtractor extractor, @Mock final SubtitleConverter subtitleConverter, @Mock final VideoLoader videoLoader,
|
||||
@Mock final VideoConverter videoConverter, @Mock final Translator translator, @Mock final TimeFormatter timeFormatter,
|
||||
@Mock final FXWorkBinder binder) throws TimeoutException {
|
||||
this.extractor = requireNonNull(extractor);
|
||||
this.subtitleConverters = Map.of("srt", requireNonNull(subtitleConverter));
|
||||
this.videoLoader = requireNonNull(videoLoader);
|
||||
this.videoConverter = requireNonNull(videoConverter);
|
||||
this.translator = requireNonNull(translator);
|
||||
this.timeFormatter = requireNonNull(timeFormatter);
|
||||
this.model = spy(new FXSetupModel());
|
||||
this.binder = requireNonNull(binder);
|
||||
this.window = FxToolkit.registerPrimaryStage();
|
||||
this.controller = spy(FXSetupController.class);
|
||||
|
||||
FxToolkit.setupStage(w -> {
|
||||
final var loader = new FXMLLoader(getClass().getResource("/com/github/gtache/autosubtitle/gui/fx/setupView.fxml"));
|
||||
loader.setResources(BUNDLE);
|
||||
loader.setControllerFactory(c -> controller);
|
||||
try {
|
||||
final Parent parent = loader.load();
|
||||
final var scene = new Scene(parent);
|
||||
w.setScene(scene);
|
||||
} catch (final IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
});
|
||||
FxToolkit.showStage();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void afterEach() throws Throwable {
|
||||
WaitForAsyncUtils.waitForFxEvents();
|
||||
WaitForAsyncUtils.checkException();
|
||||
FxToolkit.cleanupStages();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.github.gtache.autosubtitle.gui.fx;
|
||||
|
||||
import com.github.gtache.autosubtitle.setup.SetupStatus;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static com.github.gtache.autosubtitle.setup.SetupStatus.ERRORED;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class TestFXSetupModel {
|
||||
|
||||
private final FXSetupModel model = new FXSetupModel();
|
||||
|
||||
@Test
|
||||
void testSubtitleExtractorStatus() {
|
||||
assertEquals(ERRORED, model.subtitleExtractorStatus());
|
||||
assertEquals(ERRORED, model.subtitleExtractorStatusProperty().get());
|
||||
model.setSubtitleExtractorStatus(SetupStatus.SYSTEM_INSTALLED);
|
||||
assertEquals(SetupStatus.SYSTEM_INSTALLED, model.subtitleExtractorStatus());
|
||||
assertEquals(SetupStatus.SYSTEM_INSTALLED, model.subtitleExtractorStatusProperty().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSubtitleExtractorSetupProgress() {
|
||||
assertEquals(-2, model.subtitleExtractorSetupProgress());
|
||||
assertEquals(-2, model.subtitleExtractorSetupProgressProperty().get());
|
||||
model.setSubtitleExtractorSetupProgress(0.5);
|
||||
assertEquals(0.5, model.subtitleExtractorSetupProgress());
|
||||
assertEquals(0.5, model.subtitleExtractorSetupProgressProperty().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSubtitleExtractorSetupProgressLabel() {
|
||||
assertEquals("", model.subtitleExtractorSetupProgressLabel());
|
||||
assertEquals("", model.subtitleExtractorSetupProgressLabelProperty().get());
|
||||
model.setSubtitleExtractorSetupProgressLabel("test");
|
||||
assertEquals("test", model.subtitleExtractorSetupProgressLabel());
|
||||
assertEquals("test", model.subtitleExtractorSetupProgressLabelProperty().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testVideoConverterStatus() {
|
||||
assertEquals(ERRORED, model.videoConverterStatus());
|
||||
assertEquals(ERRORED, model.videoConverterStatusProperty().get());
|
||||
model.setVideoConverterStatus(SetupStatus.SYSTEM_INSTALLED);
|
||||
assertEquals(SetupStatus.SYSTEM_INSTALLED, model.videoConverterStatus());
|
||||
assertEquals(SetupStatus.SYSTEM_INSTALLED, model.videoConverterStatusProperty().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testVideoConverterSetupProgress() {
|
||||
assertEquals(-2, model.videoConverterSetupProgress());
|
||||
assertEquals(-2, model.videoConverterSetupProgressProperty().get());
|
||||
model.setVideoConverterSetupProgress(0.5);
|
||||
assertEquals(0.5, model.videoConverterSetupProgress());
|
||||
assertEquals(0.5, model.videoConverterSetupProgressProperty().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testVideoConverterSetupProgressLabel() {
|
||||
assertEquals("", model.videoConverterSetupProgressLabel());
|
||||
assertEquals("", model.videoConverterSetupProgressLabelProperty().get());
|
||||
model.setVideoConverterSetupProgressLabel("test");
|
||||
assertEquals("test", model.videoConverterSetupProgressLabel());
|
||||
assertEquals("test", model.videoConverterSetupProgressLabelProperty().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testTranslatorStatus() {
|
||||
assertEquals(ERRORED, model.translatorStatus());
|
||||
assertEquals(ERRORED, model.translatorStatusProperty().get());
|
||||
model.setTranslatorStatus(SetupStatus.SYSTEM_INSTALLED);
|
||||
assertEquals(SetupStatus.SYSTEM_INSTALLED, model.translatorStatus());
|
||||
assertEquals(SetupStatus.SYSTEM_INSTALLED, model.translatorStatusProperty().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testTranslatorSetupProgress() {
|
||||
assertEquals(-2, model.translatorSetupProgress());
|
||||
assertEquals(-2, model.translatorSetupProgressProperty().get());
|
||||
model.setTranslatorSetupProgress(0.5);
|
||||
assertEquals(0.5, model.translatorSetupProgress());
|
||||
assertEquals(0.5, model.translatorSetupProgressProperty().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testTranslatorSetupProgressLabel() {
|
||||
assertEquals("", model.translatorSetupProgressLabel());
|
||||
assertEquals("", model.translatorSetupProgressLabelProperty().get());
|
||||
model.setTranslatorSetupProgressLabel("test");
|
||||
assertEquals("test", model.translatorSetupProgressLabel());
|
||||
assertEquals("test", model.translatorSetupProgressLabelProperty().get());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.github.gtache.autosubtitle.gui.fx;
|
||||
|
||||
import com.github.gtache.autosubtitle.Translator;
|
||||
import com.github.gtache.autosubtitle.VideoConverter;
|
||||
import com.github.gtache.autosubtitle.VideoLoader;
|
||||
import com.github.gtache.autosubtitle.gui.TimeFormatter;
|
||||
import com.github.gtache.autosubtitle.modules.gui.fx.DaggerResourceComponent;
|
||||
import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverter;
|
||||
import com.github.gtache.autosubtitle.subtitle.extractor.SubtitleExtractor;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.testfx.api.FxRobot;
|
||||
import org.testfx.api.FxToolkit;
|
||||
import org.testfx.util.WaitForAsyncUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.Map;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.mockito.Mockito.spy;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class TestFXSubtitlesController extends FxRobot {
|
||||
|
||||
private static final ResourceBundle BUNDLE = DaggerResourceComponent.builder().build().getBundle();
|
||||
private final FXSubtitlesModel model;
|
||||
private final FXSubtitlesController controller;
|
||||
private final FXSubtitlesBinder binder;
|
||||
private final SubtitleExtractor extractor;
|
||||
private final Map<String, SubtitleConverter> subtitleConverters;
|
||||
private final VideoLoader videoLoader;
|
||||
private final VideoConverter videoConverter;
|
||||
private final Translator translator;
|
||||
private final TimeFormatter timeFormatter;
|
||||
private final Stage window;
|
||||
|
||||
TestFXSubtitlesController(@Mock final SubtitleExtractor extractor, @Mock final SubtitleConverter subtitleConverter, @Mock final VideoLoader videoLoader,
|
||||
@Mock final VideoConverter videoConverter, @Mock final Translator translator, @Mock final TimeFormatter timeFormatter,
|
||||
@Mock final FXSubtitlesBinder binder) throws TimeoutException {
|
||||
this.extractor = requireNonNull(extractor);
|
||||
this.subtitleConverters = Map.of("srt", requireNonNull(subtitleConverter));
|
||||
this.videoLoader = requireNonNull(videoLoader);
|
||||
this.videoConverter = requireNonNull(videoConverter);
|
||||
this.translator = requireNonNull(translator);
|
||||
this.timeFormatter = requireNonNull(timeFormatter);
|
||||
this.model = spy(new FXSubtitlesModel());
|
||||
this.binder = requireNonNull(binder);
|
||||
this.window = FxToolkit.registerPrimaryStage();
|
||||
this.controller = spy(FXSubtitlesController.class);
|
||||
|
||||
FxToolkit.setupStage(w -> {
|
||||
final var loader = new FXMLLoader(getClass().getResource("/com/github/gtache/autosubtitle/gui/fx/workView.fxml"));
|
||||
loader.setResources(BUNDLE);
|
||||
loader.setControllerFactory(c -> controller);
|
||||
try {
|
||||
final Parent parent = loader.load();
|
||||
final var scene = new Scene(parent);
|
||||
w.setScene(scene);
|
||||
} catch (final IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
});
|
||||
FxToolkit.showStage();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void afterEach() throws Throwable {
|
||||
WaitForAsyncUtils.waitForFxEvents();
|
||||
WaitForAsyncUtils.checkException();
|
||||
FxToolkit.cleanupStages();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.github.gtache.autosubtitle.gui.fx;
|
||||
|
||||
import com.github.gtache.autosubtitle.Language;
|
||||
import com.github.gtache.autosubtitle.subtitle.gui.fx.ObservableSubtitleImpl;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
class TestFXSubtitlesModel {
|
||||
|
||||
private final FXSubtitlesModel model = new FXSubtitlesModel();
|
||||
|
||||
@Test
|
||||
void testSelectedSubtitle() {
|
||||
assertNull(model.selectedSubtitle());
|
||||
assertNull(model.selectedSubtitleProperty().get());
|
||||
final var subtitle = mock(ObservableSubtitleImpl.class);
|
||||
model.setSelectedSubtitle(subtitle);
|
||||
assertEquals(subtitle, model.selectedSubtitle());
|
||||
assertEquals(subtitle, model.selectedSubtitleProperty().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAvailableVideoLanguages() {
|
||||
final var expected = Arrays.stream(Language.values())
|
||||
.sorted((o1, o2) -> {
|
||||
if (o1 == Language.AUTO) {
|
||||
return -1;
|
||||
} else if (o2 == Language.AUTO) {
|
||||
return 1;
|
||||
} else {
|
||||
return o1.englishName().compareTo(o2.englishName());
|
||||
}
|
||||
}).toList();
|
||||
assertEquals(expected, model.availableVideoLanguages());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAvailableTranslationsLanguage() {
|
||||
final var expected = Arrays.stream(Language.values())
|
||||
.filter(l -> l != Language.AUTO).toList();
|
||||
assertEquals(expected, model.availableTranslationsLanguage());
|
||||
assertThrows(UnsupportedOperationException.class, () -> model.availableTranslationsLanguage().setAll(Language.DE));
|
||||
model.setVideoLanguage(Language.DE);
|
||||
final var expected2 = Arrays.stream(Language.values())
|
||||
.filter(l -> l != Language.AUTO && l != Language.DE).toList();
|
||||
assertEquals(expected2, model.availableTranslationsLanguage());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.github.gtache.autosubtitle.gui.fx;
|
||||
|
||||
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractionModel;
|
||||
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractionModelProvider;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
class TestFXWorkBinder {
|
||||
|
||||
@Test
|
||||
void testBindings() {
|
||||
final var workModel = new FXWorkModel();
|
||||
final var parametersModel = new FXParametersModel(mock(ExtractionModelProvider.class), "Arial", 12);
|
||||
final var binder = new FXWorkBinder(workModel, parametersModel);
|
||||
binder.createBindings();
|
||||
assertNull(workModel.extractionModel());
|
||||
assertNull(parametersModel.extractionModel());
|
||||
|
||||
final var extractionModel = mock(ExtractionModel.class);
|
||||
parametersModel.setExtractionModel(extractionModel);
|
||||
assertEquals(extractionModel, workModel.extractionModel());
|
||||
assertEquals(extractionModel, parametersModel.extractionModel());
|
||||
|
||||
assertThrows(RuntimeException.class, () -> workModel.setExtractionModel(mock(ExtractionModel.class)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.github.gtache.autosubtitle.gui.fx;
|
||||
|
||||
import com.github.gtache.autosubtitle.VideoConverter;
|
||||
import com.github.gtache.autosubtitle.VideoLoader;
|
||||
import com.github.gtache.autosubtitle.modules.gui.fx.DaggerResourceComponent;
|
||||
import com.github.gtache.autosubtitle.subtitle.extractor.SubtitleExtractor;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.testfx.api.FxRobot;
|
||||
import org.testfx.api.FxToolkit;
|
||||
import org.testfx.util.WaitForAsyncUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.mockito.Mockito.spy;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class TestFXWorkController extends FxRobot {
|
||||
|
||||
private static final ResourceBundle BUNDLE = DaggerResourceComponent.builder().build().getBundle();
|
||||
private final FXWorkModel model;
|
||||
private final FXWorkController controller;
|
||||
private final FXWorkBinder binder;
|
||||
private final SubtitleExtractor extractor;
|
||||
private final VideoLoader videoLoader;
|
||||
private final VideoConverter videoConverter;
|
||||
private final Stage window;
|
||||
|
||||
TestFXWorkController(@Mock final SubtitleExtractor extractor, @Mock final VideoLoader videoLoader,
|
||||
@Mock final VideoConverter videoConverter, @Mock final FXWorkBinder binder) throws TimeoutException {
|
||||
this.extractor = requireNonNull(extractor);
|
||||
this.videoLoader = requireNonNull(videoLoader);
|
||||
this.videoConverter = requireNonNull(videoConverter);
|
||||
this.model = spy(new FXWorkModel());
|
||||
this.binder = requireNonNull(binder);
|
||||
this.window = FxToolkit.registerPrimaryStage();
|
||||
this.controller = spy(new FXWorkController(model, binder, extractor, videoLoader, videoConverter));
|
||||
|
||||
FxToolkit.setupStage(w -> {
|
||||
final var loader = new FXMLLoader(getClass().getResource("/com/github/gtache/autosubtitle/gui/fx/workView.fxml"));
|
||||
loader.setResources(BUNDLE);
|
||||
loader.setControllerFactory(c -> controller);
|
||||
try {
|
||||
final Parent parent = loader.load();
|
||||
final var scene = new Scene(parent);
|
||||
w.setScene(scene);
|
||||
} catch (final IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
});
|
||||
FxToolkit.showStage();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void afterEach() throws Throwable {
|
||||
WaitForAsyncUtils.waitForFxEvents();
|
||||
WaitForAsyncUtils.checkException();
|
||||
FxToolkit.cleanupStages();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.github.gtache.autosubtitle.gui.fx;
|
||||
|
||||
import com.github.gtache.autosubtitle.Video;
|
||||
import com.github.gtache.autosubtitle.gui.WorkStatus;
|
||||
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractionModel;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
class TestFXWorkModel {
|
||||
|
||||
private final FXWorkModel model = new FXWorkModel();
|
||||
|
||||
@Test
|
||||
void testVideo() {
|
||||
assertNull(model.video());
|
||||
assertNull(model.videoProperty().get());
|
||||
final var video = mock(Video.class);
|
||||
model.setVideo(video);
|
||||
assertEquals(video, model.video());
|
||||
assertEquals(video, model.videoProperty().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExtractionModel() {
|
||||
assertNull(model.extractionModel());
|
||||
assertNull(model.extractionModelProperty().get());
|
||||
final var extractionModel = mock(ExtractionModel.class);
|
||||
model.setExtractionModel(extractionModel);
|
||||
assertEquals(extractionModel, model.extractionModel());
|
||||
assertEquals(extractionModel, model.extractionModelProperty().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWorkStatus() {
|
||||
assertEquals(WorkStatus.IDLE, model.status());
|
||||
assertEquals(WorkStatus.IDLE, model.statusProperty().get());
|
||||
model.setStatus(WorkStatus.TRANSLATING);
|
||||
assertEquals(WorkStatus.TRANSLATING, model.status());
|
||||
assertEquals(WorkStatus.TRANSLATING, model.statusProperty().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProgress() {
|
||||
assertEquals(-1.0, model.progress());
|
||||
assertEquals(-1.0, model.progressProperty().get());
|
||||
model.setProgress(0.5);
|
||||
assertEquals(0.5, model.progress());
|
||||
assertEquals(0.5, model.progressProperty().get());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.github.gtache.autosubtitle.gui.fx;
|
||||
|
||||
import com.github.gtache.autosubtitle.Language;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
class TestLanguageStringConverter {
|
||||
|
||||
private final LanguageStringConverter converter = new LanguageStringConverter();
|
||||
|
||||
@Test
|
||||
void testToString() {
|
||||
assertEquals(Language.EN.englishName(), converter.toString(Language.EN));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testToStringNull() {
|
||||
assertEquals("", converter.toString(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFromStringNull() {
|
||||
assertNull(converter.fromString("english"));
|
||||
assertNull(converter.fromString(null));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.github.gtache.autosubtitle.gui.fx;
|
||||
|
||||
import com.github.gtache.autosubtitle.gui.TimeFormatter;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class TestTimeStringConverter {
|
||||
private static final long LONG = 1L;
|
||||
private static final String STRING = "test";
|
||||
private final TimeFormatter timeFormatter;
|
||||
private final TimeStringConverter converter;
|
||||
|
||||
TestTimeStringConverter(@Mock final TimeFormatter timeFormatter) {
|
||||
this.timeFormatter = Objects.requireNonNull(timeFormatter);
|
||||
when(timeFormatter.format(LONG)).thenReturn(STRING);
|
||||
when(timeFormatter.parse(STRING)).thenReturn(LONG);
|
||||
this.converter = new TimeStringConverter(timeFormatter);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testToString() {
|
||||
assertEquals(STRING, converter.toString(LONG));
|
||||
verify(timeFormatter).format(LONG);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testToStringNull() {
|
||||
assertEquals("", converter.toString(null));
|
||||
verifyNoInteractions(timeFormatter);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFromString() {
|
||||
assertEquals(LONG, converter.fromString(STRING));
|
||||
verify(timeFormatter).parse(STRING);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFromStringNull() {
|
||||
assertEquals(0L, converter.fromString(null));
|
||||
verifyNoInteractions(timeFormatter);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.github.gtache.autosubtitle.modules.gui.fx;
|
||||
|
||||
import com.github.gtache.autosubtitle.modules.gui.impl.GuiCoreModule;
|
||||
import com.github.gtache.autosubtitle.modules.gui.impl.Pause;
|
||||
import com.github.gtache.autosubtitle.modules.gui.impl.Play;
|
||||
import dagger.Component;
|
||||
import javafx.scene.image.Image;
|
||||
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
@Component(modules = {FXModule.class, GuiCoreModule.class})
|
||||
public interface ResourceComponent {
|
||||
|
||||
ResourceBundle getBundle();
|
||||
|
||||
@Play
|
||||
Image getPlayImage();
|
||||
|
||||
@Pause
|
||||
Image getPauseImage();
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.github.gtache.autosubtitle.modules.gui.fx;
|
||||
|
||||
import com.github.gtache.autosubtitle.gui.fx.FXMainController;
|
||||
import com.github.gtache.autosubtitle.gui.fx.FXMediaController;
|
||||
import com.github.gtache.autosubtitle.gui.fx.FXParametersController;
|
||||
import com.github.gtache.autosubtitle.gui.fx.FXSetupController;
|
||||
import com.github.gtache.autosubtitle.gui.fx.FXSubtitlesController;
|
||||
import com.github.gtache.autosubtitle.gui.fx.FXWorkController;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.image.Image;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.prefs.Preferences;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
class TestFXModule {
|
||||
|
||||
private static final byte[] IMAGE = new byte[0];
|
||||
|
||||
@Test
|
||||
void testFXMLLoader() {
|
||||
assertInstanceOf(FXMLLoader.class, FXModule.providesFXMLLoader(mock(FXMainController.class), mock(FXMediaController.class),
|
||||
mock(FXParametersController.class), mock(FXSetupController.class), mock(FXSubtitlesController.class), mock(FXWorkController.class), mock(ResourceBundle.class)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPlayImage() {
|
||||
assertInstanceOf(Image.class, FXModule.providesPlayImage(IMAGE));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPauseImage() {
|
||||
assertInstanceOf(Image.class, FXModule.providesPauseImage(IMAGE));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPreferences() {
|
||||
assertInstanceOf(Preferences.class, FXModule.providesPreferences());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.github.gtache.autosubtitle.subtitle.gui.fx;
|
||||
|
||||
import com.github.gtache.autosubtitle.subtitle.Bounds;
|
||||
import com.github.gtache.autosubtitle.subtitle.Font;
|
||||
import com.github.gtache.autosubtitle.subtitle.impl.SubtitleImpl;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class TestObservableSubtitleImpl {
|
||||
|
||||
private final String content;
|
||||
private final long start;
|
||||
private final long end;
|
||||
private final Font font;
|
||||
private final Bounds bounds;
|
||||
private final ObservableSubtitleImpl subtitle;
|
||||
|
||||
TestObservableSubtitleImpl(@Mock final Font font, @Mock final Bounds bounds) {
|
||||
this.content = "content";
|
||||
this.start = 1000L;
|
||||
this.end = 3000L;
|
||||
this.font = Objects.requireNonNull(font);
|
||||
this.bounds = Objects.requireNonNull(bounds);
|
||||
this.subtitle = new ObservableSubtitleImpl();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testContent() {
|
||||
assertEquals("", subtitle.content());
|
||||
assertEquals("", subtitle.contentProperty().get());
|
||||
subtitle.setContent(content);
|
||||
assertEquals(content, subtitle.content());
|
||||
assertEquals(content, subtitle.contentProperty().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStart() {
|
||||
assertEquals(0L, subtitle.start());
|
||||
assertEquals(0L, subtitle.startProperty().get());
|
||||
subtitle.setStart(start);
|
||||
assertEquals(start, subtitle.start());
|
||||
assertEquals(start, subtitle.startProperty().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEnd() {
|
||||
assertEquals(0L, subtitle.end());
|
||||
assertEquals(0L, subtitle.endProperty().get());
|
||||
subtitle.setEnd(end);
|
||||
assertEquals(end, subtitle.end());
|
||||
assertEquals(end, subtitle.endProperty().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFont() {
|
||||
assertNull(subtitle.font());
|
||||
assertNull(subtitle.fontProperty().get());
|
||||
subtitle.setFont(font);
|
||||
assertEquals(font, subtitle.font());
|
||||
assertEquals(font, subtitle.fontProperty().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBounds() {
|
||||
assertNull(subtitle.bounds());
|
||||
assertNull(subtitle.boundsProperty().get());
|
||||
subtitle.setBounds(bounds);
|
||||
assertEquals(bounds, subtitle.bounds());
|
||||
assertEquals(bounds, subtitle.boundsProperty().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStringConstructor() {
|
||||
final var subtitle = new ObservableSubtitleImpl(content);
|
||||
assertEquals(content, subtitle.content());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWholeConstructor() {
|
||||
final var subtitle = new ObservableSubtitleImpl(content, start, end, font, bounds);
|
||||
assertEquals(content, subtitle.content());
|
||||
assertEquals(start, subtitle.start());
|
||||
assertEquals(end, subtitle.end());
|
||||
assertEquals(font, subtitle.font());
|
||||
assertEquals(bounds, subtitle.bounds());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCopyConstructor() {
|
||||
final var originalSubtitle = new SubtitleImpl(content, start, end, font, bounds);
|
||||
final var subtitle = new ObservableSubtitleImpl(originalSubtitle);
|
||||
assertEquals(content, subtitle.content());
|
||||
assertEquals(start, subtitle.start());
|
||||
assertEquals(end, subtitle.end());
|
||||
assertEquals(font, subtitle.font());
|
||||
assertEquals(bounds, subtitle.bounds());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.github.gtache.autosubtitle.subtitle.gui.fx;
|
||||
|
||||
import com.github.gtache.autosubtitle.subtitle.Subtitle;
|
||||
import javafx.application.Platform;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.testfx.api.FxToolkit;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class TestSubtitleLabel {
|
||||
|
||||
@BeforeAll
|
||||
static void startFX() {
|
||||
if (!FxToolkit.isFXApplicationThreadRunning()) {
|
||||
Platform.startup(() -> {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private final String content;
|
||||
private final Subtitle subtitle;
|
||||
private final SubtitleLabel label;
|
||||
|
||||
TestSubtitleLabel(@Mock final Subtitle subtitle) {
|
||||
this.subtitle = Objects.requireNonNull(subtitle);
|
||||
this.content = "content";
|
||||
when(subtitle.content()).thenReturn(content);
|
||||
this.label = new SubtitleLabel(subtitle);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetters() {
|
||||
assertEquals(content, label.getText());
|
||||
assertEquals(subtitle, label.subtitle());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDrag() {
|
||||
final var x = 10.0;
|
||||
final var y = 20.0;
|
||||
label.setDragged(x, y);
|
||||
assertEquals(x, label.getDraggedX());
|
||||
assertEquals(y, label.getDraggedY());
|
||||
}
|
||||
}
|
||||
Binary file not shown.
13
gui/fx/src/test/resources/log4j2-test.xml
Normal file
13
gui/fx/src/test/resources/log4j2-test.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="INFO">
|
||||
<Appenders>
|
||||
<Console name="Console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
|
||||
</Console>
|
||||
</Appenders>
|
||||
<Loggers>
|
||||
<Root level="info">
|
||||
<AppenderRef ref="Console"/>
|
||||
</Root>
|
||||
</Loggers>
|
||||
</Configuration>
|
||||
Reference in New Issue
Block a user