Moves some modules and files, adds save subtitles
This commit is contained in:
49
gui/fx/pom.xml
Normal file
49
gui/fx/pom.xml
Normal file
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.github.gtache.autosubtitle</groupId>
|
||||
<artifactId>autosubtitle-gui</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>autosubtitle-fx</artifactId>
|
||||
|
||||
<properties>
|
||||
<controlsfx.version>11.2.1</controlsfx.version>
|
||||
<javafx.version>22.0.1</javafx.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.github.gtache.autosubtitle</groupId>
|
||||
<artifactId>autosubtitle-gui-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.dagger</groupId>
|
||||
<artifactId>dagger</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-media</artifactId>
|
||||
<version>${javafx.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-fxml</artifactId>
|
||||
<version>${javafx.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.controlsfx</groupId>
|
||||
<artifactId>controlsfx</artifactId>
|
||||
<version>${controlsfx.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.github.gtache.autosubtitle.gui.fx;
|
||||
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.stage.Window;
|
||||
|
||||
/**
|
||||
* Base class for FX controllers
|
||||
*/
|
||||
public abstract class AbstractFXController {
|
||||
|
||||
/**
|
||||
* @return the current window
|
||||
*/
|
||||
protected abstract Window window();
|
||||
|
||||
/**
|
||||
* Show an error dialog
|
||||
*
|
||||
* @param title the dialog title
|
||||
* @param message the error message
|
||||
*/
|
||||
protected void showErrorDialog(final String title, final String message) {
|
||||
final var alert = new Alert(Alert.AlertType.ERROR, message, ButtonType.OK);
|
||||
alert.initOwner(window());
|
||||
alert.setHeaderText(null);
|
||||
alert.setTitle(title);
|
||||
alert.showAndWait();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.github.gtache.autosubtitle.gui.fx;
|
||||
|
||||
import com.github.gtache.autosubtitle.gui.TimeFormatter;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
/**
|
||||
* {@link TimeFormatter} separating values using a colon
|
||||
*/
|
||||
@Singleton
|
||||
public class ColonTimeFormatter implements TimeFormatter {
|
||||
|
||||
@Inject
|
||||
ColonTimeFormatter() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String format(final long elapsed, final long total) {
|
||||
final var elapsedString = format(elapsed);
|
||||
final var totalString = format(total);
|
||||
return elapsedString + "/" + totalString;
|
||||
}
|
||||
|
||||
@Override
|
||||
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;
|
||||
if (durationHours > 0) {
|
||||
intDuration -= durationHours * secondsInHour;
|
||||
}
|
||||
final var durationMinutes = intDuration / secondsInMinute;
|
||||
final var durationSeconds = intDuration - durationHours * secondsInHour
|
||||
- durationMinutes * secondsInMinute;
|
||||
if (durationHours > 0) {
|
||||
return String.format("%d:%02d:%02d", durationHours, durationMinutes, durationSeconds);
|
||||
} else {
|
||||
return String.format("%02d:%02d", durationMinutes, durationSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long parse(final String time) {
|
||||
final var split = time.split(":");
|
||||
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;
|
||||
default -> 0;
|
||||
};
|
||||
}
|
||||
|
||||
private long toLong(final String time) {
|
||||
if (time.startsWith("0")) {
|
||||
return Long.parseLong(time.substring(1));
|
||||
} else {
|
||||
return Long.parseLong(time);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.github.gtache.autosubtitle.gui.fx;
|
||||
|
||||
import com.github.gtache.autosubtitle.gui.MainController;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.TabPane;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* FX implementation of {@link MainController}
|
||||
*/
|
||||
@Singleton
|
||||
public class FXMainController implements MainController {
|
||||
|
||||
@FXML
|
||||
private TabPane tabPane;
|
||||
|
||||
private final FXMainModel model;
|
||||
|
||||
@Inject
|
||||
FXMainController(final FXMainModel model) {
|
||||
this.model = Objects.requireNonNull(model);
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void initialize() {
|
||||
tabPane.getSelectionModel().selectedIndexProperty().addListener((observable, oldValue, newValue) -> model.selectTab(newValue.intValue()));
|
||||
model.selectedTabProperty().addListener((observable, oldValue, newValue) -> tabPane.getSelectionModel().select(newValue.intValue()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void selectTab(final int index) {
|
||||
model.selectTab(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FXMainModel model() {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.github.gtache.autosubtitle.gui.fx;
|
||||
|
||||
import com.github.gtache.autosubtitle.gui.MainModel;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
/**
|
||||
* FX implementation of {@link MainModel}
|
||||
*/
|
||||
@Singleton
|
||||
public class FXMainModel implements MainModel {
|
||||
|
||||
private final IntegerProperty selectedTab;
|
||||
|
||||
@Inject
|
||||
FXMainModel() {
|
||||
selectedTab = new SimpleIntegerProperty(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int selectedTab() {
|
||||
return selectedTab.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void selectTab(final int index) {
|
||||
selectedTab.set(index);
|
||||
}
|
||||
|
||||
IntegerProperty selectedTabProperty() {
|
||||
return selectedTab;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.github.gtache.autosubtitle.gui.fx;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Binds the media model
|
||||
*/
|
||||
@Singleton
|
||||
public class FXMediaBinder {
|
||||
|
||||
private final FXWorkModel workModel;
|
||||
private final FXMediaModel mediaModel;
|
||||
|
||||
@Inject
|
||||
FXMediaBinder(final FXWorkModel workModel, final FXMediaModel mediaModel) {
|
||||
this.workModel = Objects.requireNonNull(workModel);
|
||||
this.mediaModel = Objects.requireNonNull(mediaModel);
|
||||
}
|
||||
|
||||
public void createBindings() {
|
||||
mediaModel.videoProperty().bindBidirectional(workModel.videoProperty());
|
||||
Bindings.bindContent(mediaModel.subtitles(), workModel.subtitles());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package com.github.gtache.autosubtitle.gui.fx;
|
||||
|
||||
import com.github.gtache.autosubtitle.gui.MediaController;
|
||||
import com.github.gtache.autosubtitle.gui.TimeFormatter;
|
||||
import com.github.gtache.autosubtitle.impl.FileVideoImpl;
|
||||
import com.github.gtache.autosubtitle.modules.gui.impl.Pause;
|
||||
import com.github.gtache.autosubtitle.modules.gui.impl.Play;
|
||||
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.fxml.FXML;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Slider;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.BackgroundFill;
|
||||
import javafx.scene.layout.CornerRadii;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.media.Media;
|
||||
import javafx.scene.media.MediaPlayer;
|
||||
import javafx.scene.media.MediaView;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.util.Duration;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
/**
|
||||
* FX implementation of {@link MediaController}
|
||||
*/
|
||||
@Singleton
|
||||
public class FXMediaController implements MediaController {
|
||||
|
||||
private static final Logger logger = LogManager.getLogger(FXMediaController.class);
|
||||
@FXML
|
||||
private StackPane stackPane;
|
||||
@FXML
|
||||
private MediaView videoView;
|
||||
@FXML
|
||||
private Slider playSlider;
|
||||
@FXML
|
||||
private Label playLabel;
|
||||
@FXML
|
||||
private Button playButton;
|
||||
@FXML
|
||||
private Slider volumeSlider;
|
||||
@FXML
|
||||
private Label volumeValueLabel;
|
||||
|
||||
private final FXMediaModel model;
|
||||
private final FXMediaBinder binder;
|
||||
private final TimeFormatter timeFormatter;
|
||||
private final Image playImage;
|
||||
private final Image pauseImage;
|
||||
|
||||
private boolean wasPlaying;
|
||||
|
||||
@Inject
|
||||
FXMediaController(final FXMediaModel model, final FXMediaBinder binder, final TimeFormatter timeFormatter,
|
||||
@Play final Image playImage, @Pause final Image pauseImage) {
|
||||
this.model = requireNonNull(model);
|
||||
this.binder = requireNonNull(binder);
|
||||
this.timeFormatter = requireNonNull(timeFormatter);
|
||||
this.playImage = requireNonNull(playImage);
|
||||
this.pauseImage = requireNonNull(pauseImage);
|
||||
}
|
||||
|
||||
@FXML
|
||||
private 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());
|
||||
|
||||
model.volumeProperty().addListener((observable, oldValue, newValue) -> volumeSlider.setValue(newValue.doubleValue() * 100));
|
||||
volumeSlider.valueProperty().addListener((observable, oldValue, newValue) -> model.setVolume(newValue.doubleValue() / 100));
|
||||
|
||||
model.isPlayingProperty().addListener((observable, oldValue, newValue) -> {
|
||||
final var player = videoView.getMediaPlayer();
|
||||
if (player != null) {
|
||||
if (Boolean.TRUE.equals(newValue)) {
|
||||
if (model.position() == model.duration()) {
|
||||
seek(0L);
|
||||
}
|
||||
player.play();
|
||||
} else {
|
||||
player.pause();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
model.videoProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (videoView.getMediaPlayer() != null) {
|
||||
videoView.getMediaPlayer().dispose();
|
||||
}
|
||||
if (newValue instanceof final FileVideoImpl fileVideo) {
|
||||
final var media = new Media(fileVideo.path().toUri().toString());
|
||||
final var player = new MediaPlayer(media);
|
||||
player.currentTimeProperty().addListener((ignored, oldTime, newTime) -> {
|
||||
final var millis = newTime.toMillis();
|
||||
playSlider.setValue(millis);
|
||||
stackPane.getChildren().removeIf(Label.class::isInstance);
|
||||
model.subtitles().forEach(s -> {
|
||||
//TODO optimize using e.g. direction of playback
|
||||
if (s.start() <= millis && s.end() >= millis) {
|
||||
logger.info("Adding label {} at {}", s, millis);
|
||||
final var label = createDraggableLabel(s);
|
||||
stackPane.getChildren().add(label);
|
||||
}
|
||||
});
|
||||
});
|
||||
playSlider.setOnMousePressed(e -> {
|
||||
wasPlaying = model.isPlaying();
|
||||
model.setIsPlaying(false);
|
||||
});
|
||||
playSlider.valueProperty().addListener(observable1 -> {
|
||||
if (playSlider.isValueChanging()) {
|
||||
seek((long) playSlider.getValue());
|
||||
}
|
||||
});
|
||||
playSlider.setOnMouseReleased(e -> {
|
||||
final var value = playSlider.getValue();
|
||||
Platform.runLater(() -> {
|
||||
seek((long) value);
|
||||
model.setIsPlaying(wasPlaying);
|
||||
});
|
||||
});
|
||||
player.volumeProperty().bindBidirectional(model.volumeProperty());
|
||||
player.setOnPlaying(() -> model.setIsPlaying(true));
|
||||
player.setOnPaused(() -> model.setIsPlaying(false));
|
||||
player.setOnEndOfMedia(() -> model.setIsPlaying(false));
|
||||
playSlider.setMax(model.duration());
|
||||
playSlider.setValue(0L);
|
||||
videoView.setMediaPlayer(player);
|
||||
} else {
|
||||
logger.error("Unsupported video type : {}", newValue);
|
||||
}
|
||||
});
|
||||
|
||||
playButton.disableProperty().bind(model.videoProperty().isNull());
|
||||
playButton.graphicProperty().bind(Bindings.createObjectBinding(() -> {
|
||||
final ImageView view;
|
||||
if (model.isPlaying()) {
|
||||
view = new ImageView(pauseImage);
|
||||
} else {
|
||||
view = new ImageView(playImage);
|
||||
}
|
||||
view.setPreserveRatio(true);
|
||||
view.setFitWidth(24);
|
||||
view.setFitHeight(24);
|
||||
return view;
|
||||
}, model.isPlayingProperty()));
|
||||
binder.createBindings();
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void playPressed() {
|
||||
model.setIsPlaying(!model.isPlaying());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void play() {
|
||||
model.setIsPlaying(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pause() {
|
||||
model.setIsPlaying(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seek(final long position) {
|
||||
if (videoView.getMediaPlayer() != null) {
|
||||
videoView.getMediaPlayer().seek(Duration.millis(position));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public FXMediaModel model() {
|
||||
return model;
|
||||
}
|
||||
|
||||
private static SubtitleLabel createDraggableLabel(final Subtitle subtitle) {
|
||||
final var label = new SubtitleLabel(subtitle);
|
||||
label.setOpacity(0.8);
|
||||
label.setBackground(new Background(new BackgroundFill(Color.BLACK, CornerRadii.EMPTY, Insets.EMPTY)));
|
||||
label.setTextFill(Color.WHITE);
|
||||
label.setOnMousePressed(mouseEvent -> {
|
||||
final var x = label.getLayoutX() - mouseEvent.getSceneX();
|
||||
final var y = label.getLayoutY() - mouseEvent.getSceneY();
|
||||
label.setDragged(x, y);
|
||||
label.setCursor(Cursor.MOVE);
|
||||
});
|
||||
label.setOnMouseReleased(mouseEvent -> label.setCursor(Cursor.HAND));
|
||||
label.setOnMouseDragged(mouseEvent -> {
|
||||
label.setLayoutX(mouseEvent.getSceneX() + label.getDraggedX());
|
||||
label.setLayoutY(mouseEvent.getSceneY() + label.getDraggedY());
|
||||
});
|
||||
label.setOnMouseEntered(mouseEvent -> label.setCursor(Cursor.HAND));
|
||||
return label;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package com.github.gtache.autosubtitle.gui.fx;
|
||||
|
||||
import com.github.gtache.autosubtitle.Video;
|
||||
import com.github.gtache.autosubtitle.gui.MediaModel;
|
||||
import com.github.gtache.autosubtitle.subtitle.EditableSubtitle;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* FX implementation of {@link com.github.gtache.autosubtitle.gui.MediaModel}
|
||||
*/
|
||||
@Singleton
|
||||
public class FXMediaModel implements MediaModel {
|
||||
|
||||
private final ObjectProperty<Video> video;
|
||||
private final DoubleProperty volume;
|
||||
private final BooleanProperty isPlaying;
|
||||
private final ReadOnlyLongWrapper duration;
|
||||
private final LongProperty position;
|
||||
private final List<EditableSubtitle> subtitles;
|
||||
|
||||
@Inject
|
||||
FXMediaModel() {
|
||||
this.video = new SimpleObjectProperty<>();
|
||||
this.volume = new SimpleDoubleProperty(1.0d);
|
||||
this.isPlaying = new SimpleBooleanProperty(false);
|
||||
this.duration = new ReadOnlyLongWrapper(0);
|
||||
this.position = new SimpleLongProperty(0);
|
||||
this.subtitles = FXCollections.observableArrayList();
|
||||
duration.bind(Bindings.createLongBinding(() -> {
|
||||
if (video() == null) {
|
||||
return 0L;
|
||||
} else {
|
||||
return video().info().duration();
|
||||
}
|
||||
}, video));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Video video() {
|
||||
return video.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVideo(final Video video) {
|
||||
this.video.set(video);
|
||||
}
|
||||
|
||||
ObjectProperty<Video> videoProperty() {
|
||||
return video;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double volume() {
|
||||
return volume.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVolume(final double volume) {
|
||||
this.volume.set(volume);
|
||||
}
|
||||
|
||||
DoubleProperty volumeProperty() {
|
||||
return volume;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPlaying() {
|
||||
return isPlaying.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setIsPlaying(final boolean playing) {
|
||||
this.isPlaying.set(playing);
|
||||
}
|
||||
|
||||
BooleanProperty isPlayingProperty() {
|
||||
return isPlaying;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long duration() {
|
||||
return duration.get();
|
||||
}
|
||||
|
||||
ReadOnlyLongProperty durationProperty() {
|
||||
return duration.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long position() {
|
||||
return position.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPosition(final long position) {
|
||||
this.position.set(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<EditableSubtitle> subtitles() {
|
||||
return subtitles;
|
||||
}
|
||||
|
||||
LongProperty positionProperty() {
|
||||
return position;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package com.github.gtache.autosubtitle.gui.fx;
|
||||
|
||||
import com.github.gtache.autosubtitle.gui.ParametersController;
|
||||
import com.github.gtache.autosubtitle.subtitle.OutputFormat;
|
||||
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractionModel;
|
||||
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractionModelProvider;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.control.TextFormatter;
|
||||
import javafx.stage.Window;
|
||||
import javafx.util.converter.IntegerStringConverter;
|
||||
import javafx.util.converter.NumberStringConverter;
|
||||
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.util.function.UnaryOperator;
|
||||
import java.util.prefs.BackingStoreException;
|
||||
import java.util.prefs.Preferences;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
/**
|
||||
* FX implementation of {@link ParametersController}
|
||||
*/
|
||||
@Singleton
|
||||
public class FXParametersController extends AbstractFXController implements ParametersController {
|
||||
|
||||
private static final Logger logger = LogManager.getLogger(FXParametersController.class);
|
||||
@FXML
|
||||
private PrefixSelectionComboBox<ExtractionModel> extractionModelCombobox;
|
||||
@FXML
|
||||
private PrefixSelectionComboBox<OutputFormat> extractionOutputFormat;
|
||||
@FXML
|
||||
private PrefixSelectionComboBox<String> fontFamilyCombobox;
|
||||
@FXML
|
||||
private TextField fontSizeField;
|
||||
|
||||
private final FXParametersModel model;
|
||||
private final Preferences preferences;
|
||||
private final ExtractionModelProvider extractionModelProvider;
|
||||
|
||||
@Inject
|
||||
FXParametersController(final FXParametersModel model, final Preferences preferences, final ExtractionModelProvider extractionModelProvider) {
|
||||
this.model = requireNonNull(model);
|
||||
this.preferences = requireNonNull(preferences);
|
||||
this.extractionModelProvider = requireNonNull(extractionModelProvider);
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void initialize() {
|
||||
extractionModelCombobox.setItems(model.availableExtractionModels());
|
||||
extractionModelCombobox.valueProperty().bindBidirectional(model.extractionModelProperty());
|
||||
|
||||
extractionOutputFormat.setItems(model.availableOutputFormats());
|
||||
extractionOutputFormat.valueProperty().bindBidirectional(model.outputFormatProperty());
|
||||
|
||||
fontFamilyCombobox.setItems(model.availableFontFamilies());
|
||||
fontFamilyCombobox.valueProperty().bindBidirectional(model.fontFamilyProperty());
|
||||
|
||||
final UnaryOperator<TextFormatter.Change> integerFilter = change -> {
|
||||
final var newText = change.getControlNewText();
|
||||
if (newText.matches("[1-9]\\d*")) {
|
||||
return change;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
fontSizeField.setTextFormatter(new TextFormatter<>(new IntegerStringConverter(), 0, integerFilter));
|
||||
|
||||
fontSizeField.textProperty().bindBidirectional(model.fontSizeProperty(), new NumberStringConverter());
|
||||
|
||||
loadPreferences();
|
||||
}
|
||||
|
||||
private void loadPreferences() {
|
||||
final var extractionModel = preferences.get("extractionModel", model.extractionModel().name());
|
||||
final var outputFormat = preferences.get("outputFormat", model.outputFormat().name());
|
||||
final var fontFamily = preferences.get("fontFamily", model.fontFamily());
|
||||
final var fontSize = preferences.getInt("fontSize", model.fontSize());
|
||||
|
||||
model.setExtractionModel(extractionModelProvider.getExtractionModel(extractionModel));
|
||||
model.setOutputFormat(OutputFormat.valueOf(outputFormat));
|
||||
model.setFontFamily(fontFamily);
|
||||
model.setFontSize(fontSize);
|
||||
logger.info("Loaded preferences");
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void save() {
|
||||
logger.info("Saving preferences");
|
||||
preferences.put("extractionModel", model.extractionModel().name());
|
||||
preferences.put("outputFormat", model.outputFormat().name());
|
||||
preferences.put("fontFamily", model.fontFamily());
|
||||
preferences.putInt("fontSize", model.fontSize());
|
||||
try {
|
||||
preferences.flush();
|
||||
logger.info("Preferences saved");
|
||||
} catch (final BackingStoreException e) {
|
||||
logger.error("Error saving preferences", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
loadPreferences();
|
||||
}
|
||||
|
||||
@Override
|
||||
public FXParametersModel model() {
|
||||
return model;
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void savePressed() {
|
||||
save();
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void resetPressed() {
|
||||
reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Window window() {
|
||||
return extractionModelCombobox.getScene().getWindow();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.github.gtache.autosubtitle.gui.fx;
|
||||
|
||||
import com.github.gtache.autosubtitle.gui.ParametersModel;
|
||||
import com.github.gtache.autosubtitle.modules.gui.impl.FontFamily;
|
||||
import com.github.gtache.autosubtitle.modules.gui.impl.FontSize;
|
||||
import com.github.gtache.autosubtitle.subtitle.OutputFormat;
|
||||
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractionModel;
|
||||
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractionModelProvider;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
/**
|
||||
* FX implementation of {@link ParametersModel}
|
||||
*/
|
||||
@Singleton
|
||||
public class FXParametersModel implements ParametersModel {
|
||||
|
||||
private final ObservableList<ExtractionModel> availableExtractionModels;
|
||||
private final ObjectProperty<ExtractionModel> extractionModel;
|
||||
private final ObservableList<OutputFormat> availableOutputFormats;
|
||||
private final ObjectProperty<OutputFormat> outputFormat;
|
||||
private final ObservableList<String> availableFontFamilies;
|
||||
private final StringProperty fontFamily;
|
||||
private final IntegerProperty fontSize;
|
||||
|
||||
@Inject
|
||||
FXParametersModel(final ExtractionModelProvider extractionModelProvider, @FontFamily final String defaultFontFamily, @FontSize final int defaultFontSize) {
|
||||
this.availableExtractionModels = FXCollections.unmodifiableObservableList(FXCollections.observableArrayList(extractionModelProvider.getAvailableExtractionModels()));
|
||||
this.extractionModel = new SimpleObjectProperty<>(extractionModelProvider.getDefaultExtractionModel());
|
||||
this.availableOutputFormats = FXCollections.unmodifiableObservableList(FXCollections.observableArrayList(OutputFormat.SRT));
|
||||
this.outputFormat = new SimpleObjectProperty<>(OutputFormat.SRT);
|
||||
this.availableFontFamilies = FXCollections.unmodifiableObservableList(FXCollections.observableArrayList("Arial"));
|
||||
this.fontFamily = new SimpleStringProperty(defaultFontFamily);
|
||||
this.fontSize = new SimpleIntegerProperty(defaultFontSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableList<ExtractionModel> availableExtractionModels() {
|
||||
return availableExtractionModels;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExtractionModel extractionModel() {
|
||||
return extractionModel.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setExtractionModel(final ExtractionModel model) {
|
||||
extractionModel.set(model);
|
||||
}
|
||||
|
||||
ObjectProperty<ExtractionModel> extractionModelProperty() {
|
||||
return extractionModel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableList<OutputFormat> availableOutputFormats() {
|
||||
return availableOutputFormats;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputFormat outputFormat() {
|
||||
return outputFormat.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOutputFormat(final OutputFormat format) {
|
||||
outputFormat.set(format);
|
||||
}
|
||||
|
||||
ObjectProperty<OutputFormat> outputFormatProperty() {
|
||||
return outputFormat;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableList<String> availableFontFamilies() {
|
||||
return availableFontFamilies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String fontFamily() {
|
||||
return fontFamily.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFontFamily(final String fontFamily) {
|
||||
this.fontFamily.set(fontFamily);
|
||||
}
|
||||
|
||||
StringProperty fontFamilyProperty() {
|
||||
return fontFamily;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int fontSize() {
|
||||
return fontSize.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFontSize(final int fontSize) {
|
||||
this.fontSize.set(fontSize);
|
||||
}
|
||||
|
||||
IntegerProperty fontSizeProperty() {
|
||||
return fontSize;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
package com.github.gtache.autosubtitle.gui.fx;
|
||||
|
||||
import com.github.gtache.autosubtitle.gui.SetupController;
|
||||
import com.github.gtache.autosubtitle.modules.setup.impl.SubtitleExtractorSetup;
|
||||
import com.github.gtache.autosubtitle.modules.setup.impl.TranslatorSetup;
|
||||
import com.github.gtache.autosubtitle.modules.setup.impl.VideoConverterSetup;
|
||||
import com.github.gtache.autosubtitle.setup.SetupEvent;
|
||||
import com.github.gtache.autosubtitle.setup.SetupException;
|
||||
import com.github.gtache.autosubtitle.setup.SetupListener;
|
||||
import com.github.gtache.autosubtitle.setup.SetupManager;
|
||||
import com.github.gtache.autosubtitle.setup.SetupStatus;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.MenuButton;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.control.ProgressBar;
|
||||
import javafx.stage.Window;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* FX implementation of {@link SetupController}
|
||||
*/
|
||||
@Singleton
|
||||
public class FXSetupController extends AbstractFXController implements SetupController, SetupListener {
|
||||
|
||||
private static final Logger logger = LogManager.getLogger(FXSetupController.class);
|
||||
|
||||
@FXML
|
||||
private ResourceBundle resources;
|
||||
@FXML
|
||||
private Label converterNameLabel;
|
||||
@FXML
|
||||
private Label converterStatusLabel;
|
||||
@FXML
|
||||
private Label extractorNameLabel;
|
||||
@FXML
|
||||
private Label extractorStatusLabel;
|
||||
@FXML
|
||||
private Label translatorNameLabel;
|
||||
@FXML
|
||||
private Label translatorStatusLabel;
|
||||
@FXML
|
||||
private MenuButton converterButton;
|
||||
@FXML
|
||||
private MenuButton extractorButton;
|
||||
@FXML
|
||||
private MenuButton translatorButton;
|
||||
@FXML
|
||||
private ProgressBar converterProgress;
|
||||
@FXML
|
||||
private ProgressBar extractorProgress;
|
||||
@FXML
|
||||
private ProgressBar translatorProgress;
|
||||
@FXML
|
||||
private Label converterProgressLabel;
|
||||
@FXML
|
||||
private Label extractorProgressLabel;
|
||||
@FXML
|
||||
private Label translatorProgressLabel;
|
||||
|
||||
private final FXSetupModel model;
|
||||
private final SetupManager converterManager;
|
||||
private final SetupManager extractorManager;
|
||||
private final SetupManager translatorManager;
|
||||
|
||||
private final Map<SetupManager, ObjectProperty<SetupStatus>> statusMap;
|
||||
private final Map<SetupManager, StringProperty> setupProgressMessageMap;
|
||||
private final Map<SetupManager, DoubleProperty> setupProgressMap;
|
||||
|
||||
@Inject
|
||||
FXSetupController(final FXSetupModel model,
|
||||
@VideoConverterSetup final SetupManager converterManager,
|
||||
@SubtitleExtractorSetup final SetupManager extractorManager,
|
||||
@TranslatorSetup final SetupManager translatorManager) {
|
||||
this.model = Objects.requireNonNull(model);
|
||||
this.converterManager = Objects.requireNonNull(converterManager);
|
||||
this.extractorManager = Objects.requireNonNull(extractorManager);
|
||||
this.translatorManager = Objects.requireNonNull(translatorManager);
|
||||
statusMap = HashMap.newHashMap(3);
|
||||
setupProgressMessageMap = HashMap.newHashMap(3);
|
||||
setupProgressMap = HashMap.newHashMap(3);
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void initialize() {
|
||||
statusMap.put(converterManager, model.videoConverterStatusProperty());
|
||||
statusMap.put(extractorManager, model.subtitleExtractorStatusProperty());
|
||||
statusMap.put(translatorManager, model.translatorStatusProperty());
|
||||
setupProgressMessageMap.put(converterManager, model.videoConverterSetupProgressLabelProperty());
|
||||
setupProgressMessageMap.put(extractorManager, model.subtitleExtractorSetupProgressLabelProperty());
|
||||
setupProgressMessageMap.put(translatorManager, model.translatorSetupProgressLabelProperty());
|
||||
setupProgressMap.put(converterManager, model.videoConverterSetupProgressProperty());
|
||||
setupProgressMap.put(extractorManager, model.subtitleExtractorSetupProgressProperty());
|
||||
setupProgressMap.put(translatorManager, model.translatorSetupProgressProperty());
|
||||
|
||||
bindMenu(converterButton, converterManager);
|
||||
bindMenu(extractorButton, extractorManager);
|
||||
bindMenu(translatorButton, translatorManager);
|
||||
|
||||
model.setSubtitleExtractorStatus(extractorManager.status());
|
||||
model.setVideoConverterStatus(converterManager.status());
|
||||
model.setTranslatorStatus(translatorManager.status());
|
||||
|
||||
converterNameLabel.setText(converterManager.name());
|
||||
extractorNameLabel.setText(extractorManager.name());
|
||||
translatorNameLabel.setText(translatorManager.name());
|
||||
bindLabelStatus(converterStatusLabel, model.videoConverterStatusProperty());
|
||||
bindLabelStatus(extractorStatusLabel, model.subtitleExtractorStatusProperty());
|
||||
bindLabelStatus(translatorStatusLabel, model.translatorStatusProperty());
|
||||
|
||||
converterProgress.progressProperty().bindBidirectional(model.videoConverterSetupProgressProperty());
|
||||
extractorProgress.progressProperty().bindBidirectional(model.subtitleExtractorSetupProgressProperty());
|
||||
translatorProgress.progressProperty().bindBidirectional(model.translatorSetupProgressProperty());
|
||||
|
||||
converterProgress.visibleProperty().bind(model.videoConverterSetupProgressProperty().greaterThan(-2));
|
||||
extractorProgress.visibleProperty().bind(model.subtitleExtractorSetupProgressProperty().greaterThan(-2));
|
||||
translatorProgress.visibleProperty().bind(model.translatorSetupProgressProperty().greaterThan(-2));
|
||||
|
||||
converterProgressLabel.textProperty().bind(model.videoConverterSetupProgressLabelProperty());
|
||||
extractorProgressLabel.textProperty().bind(model.subtitleExtractorSetupProgressLabelProperty());
|
||||
translatorProgressLabel.textProperty().bind(model.translatorSetupProgressLabelProperty());
|
||||
|
||||
converterProgressLabel.visibleProperty().bind(converterProgress.visibleProperty());
|
||||
extractorProgressLabel.visibleProperty().bind(extractorProgress.visibleProperty());
|
||||
translatorProgressLabel.visibleProperty().bind(translatorProgress.visibleProperty());
|
||||
}
|
||||
|
||||
private void bindMenu(final MenuButton button, final SetupManager setupManager) {
|
||||
button.disableProperty().bind(Bindings.isEmpty(button.getItems()));
|
||||
statusMap.get(setupManager).addListener((observable, oldValue, newValue) -> {
|
||||
button.getItems().clear();
|
||||
switch (newValue) {
|
||||
case NOT_INSTALLED -> {
|
||||
final var installItem = new MenuItem(resources.getString("setup.menu.install.label"));
|
||||
installItem.setOnAction(e -> tryInstall(setupManager));
|
||||
button.getItems().add(installItem);
|
||||
}
|
||||
case BUNDLE_INSTALLED -> {
|
||||
final var reinstallItem = new MenuItem(resources.getString("setup.menu.reinstall.label"));
|
||||
reinstallItem.setOnAction(e -> tryReinstall(setupManager));
|
||||
final var uninstallItem = new MenuItem(resources.getString("setup.menu.uninstall.label"));
|
||||
uninstallItem.setOnAction(e -> tryUninstall(setupManager));
|
||||
button.getItems().addAll(reinstallItem, uninstallItem);
|
||||
}
|
||||
case UPDATE_AVAILABLE -> {
|
||||
final var updateItem = new MenuItem(resources.getString("setup.menu.update.label"));
|
||||
updateItem.setOnAction(e -> tryUpdate(setupManager));
|
||||
final var reinstallItem = new MenuItem(resources.getString("setup.menu.reinstall.label"));
|
||||
reinstallItem.setOnAction(e -> tryReinstall(setupManager));
|
||||
final var uninstallItem = new MenuItem(resources.getString("setup.menu.uninstall.label"));
|
||||
uninstallItem.setOnAction(e -> tryUninstall(setupManager));
|
||||
button.getItems().addAll(updateItem, reinstallItem, uninstallItem);
|
||||
}
|
||||
case null, default -> {
|
||||
// Do nothing, buttons are cleared
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void bindLabelStatus(final Label label, final ObjectProperty<SetupStatus> status) {
|
||||
label.textProperty().bind(Bindings.createStringBinding(() -> resources.getString("setup.status." + status.get().name().toLowerCase() + ".label"), status));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void installVideoConverter() {
|
||||
tryInstall(converterManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uninstallVideoConverter() {
|
||||
tryUninstall(converterManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateVideoConverter() {
|
||||
tryUpdate(converterManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reinstallVideoConverter() {
|
||||
tryReinstall(converterManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void installSubtitleExtractor() {
|
||||
tryInstall(extractorManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uninstallSubtitleExtractor() {
|
||||
tryUninstall(extractorManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateSubtitleExtractor() {
|
||||
tryUpdate(extractorManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reinstallSubtitleExtractor() {
|
||||
tryReinstall(extractorManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void installTranslator() {
|
||||
tryInstall(translatorManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uninstallTranslator() {
|
||||
tryUninstall(translatorManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateTranslator() {
|
||||
tryUpdate(translatorManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reinstallTranslator() {
|
||||
tryReinstall(translatorManager);
|
||||
}
|
||||
|
||||
private void tryInstall(final SetupManager manager) {
|
||||
trySetup(manager, SetupManager::install, "install");
|
||||
}
|
||||
|
||||
private void tryUninstall(final SetupManager manager) {
|
||||
trySetup(manager, SetupManager::uninstall, "uninstall");
|
||||
}
|
||||
|
||||
private void tryReinstall(final SetupManager manager) {
|
||||
trySetup(manager, SetupManager::reinstall, "reinstall");
|
||||
}
|
||||
|
||||
private void tryUpdate(final SetupManager manager) {
|
||||
trySetup(manager, SetupManager::update, "update");
|
||||
}
|
||||
|
||||
private void trySetup(final SetupManager manager, final SetupConsumer consumer, final String operation) {
|
||||
manager.addListener(this);
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
consumer.accept(manager);
|
||||
Platform.runLater(() -> {
|
||||
statusMap.get(manager).set(manager.status());
|
||||
setupProgressMap.get(manager).set(-2);
|
||||
});
|
||||
} catch (final SetupException e) {
|
||||
logger.error("Error {}ing {}", operation, manager.name(), e);
|
||||
Platform.runLater(() -> {
|
||||
statusMap.get(manager).set(SetupStatus.ERRORED);
|
||||
setupProgressMap.get(manager).set(-2);
|
||||
showErrorDialog(resources.getString("setup." + operation + ".error.title"),
|
||||
MessageFormat.format(resources.getString("setup." + operation + ".error.label"), e.getMessage()));
|
||||
});
|
||||
} finally {
|
||||
manager.removeListener(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActionStart(final SetupEvent event) {
|
||||
final var action = event.action();
|
||||
final var target = event.target();
|
||||
final var display = MessageFormat.format(resources.getString("setup.event." + action.name().toLowerCase() + ".start.label"), target);
|
||||
onAction(event, display);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActionEnd(final SetupEvent event) {
|
||||
final var action = event.action();
|
||||
final var target = event.target();
|
||||
final var display = MessageFormat.format(resources.getString("setup.event." + action.name().toLowerCase() + ".end.label"), target);
|
||||
onAction(event, display);
|
||||
}
|
||||
|
||||
private void onAction(final SetupEvent event, final String display) {
|
||||
final var manager = event.setupManager();
|
||||
final var property = setupProgressMessageMap.get(manager);
|
||||
final var progress = event.progress();
|
||||
final var progressProperty = setupProgressMap.get(manager);
|
||||
Platform.runLater(() -> {
|
||||
property.set(display);
|
||||
progressProperty.set(progress);
|
||||
});
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface SetupConsumer {
|
||||
void accept(SetupManager manager) throws SetupException;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FXSetupModel model() {
|
||||
return model;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Window window() {
|
||||
return converterNameLabel.getScene().getWindow();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
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;
|
||||
import javafx.beans.property.StringProperty;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
/**
|
||||
* FX implementation of {@link SetupModel}
|
||||
*/
|
||||
@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
|
||||
public SetupStatus subtitleExtractorStatus() {
|
||||
return subtitleExtractorStatus.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSubtitleExtractorStatus(final SetupStatus status) {
|
||||
subtitleExtractorStatus.set(status);
|
||||
}
|
||||
|
||||
ObjectProperty<SetupStatus> subtitleExtractorStatusProperty() {
|
||||
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();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSubtitleExtractorSetupProgress(final double progress) {
|
||||
subtitleExtractorSetupProgress.set(progress);
|
||||
}
|
||||
|
||||
DoubleProperty subtitleExtractorSetupProgressProperty() {
|
||||
return subtitleExtractorSetupProgress;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String subtitleExtractorSetupProgressLabel() {
|
||||
return subtitleExtractorSetupProgressLabel.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSubtitleExtractorSetupProgressLabel(final String label) {
|
||||
subtitleExtractorSetupProgressLabel.set(label);
|
||||
}
|
||||
|
||||
StringProperty subtitleExtractorSetupProgressLabelProperty() {
|
||||
return subtitleExtractorSetupProgressLabel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SetupStatus videoConverterStatus() {
|
||||
return videoConverterStatus.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVideoConverterStatus(final SetupStatus status) {
|
||||
videoConverterStatus.set(status);
|
||||
}
|
||||
|
||||
ObjectProperty<SetupStatus> videoConverterStatusProperty() {
|
||||
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();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVideoConverterSetupProgress(final double progress) {
|
||||
videoConverterSetupProgress.set(progress);
|
||||
}
|
||||
|
||||
DoubleProperty videoConverterSetupProgressProperty() {
|
||||
return videoConverterSetupProgress;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String videoConverterSetupProgressLabel() {
|
||||
return videoConverterSetupProgressLabel.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVideoConverterSetupProgressLabel(final String label) {
|
||||
videoConverterSetupProgressLabel.set(label);
|
||||
}
|
||||
|
||||
StringProperty videoConverterSetupProgressLabelProperty() {
|
||||
return videoConverterSetupProgressLabel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SetupStatus translatorStatus() {
|
||||
return translatorStatus.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTranslatorStatus(final SetupStatus status) {
|
||||
translatorStatus.set(status);
|
||||
}
|
||||
|
||||
ObjectProperty<SetupStatus> translatorStatusProperty() {
|
||||
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();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTranslatorSetupProgress(final double progress) {
|
||||
translatorSetupProgress.set(progress);
|
||||
}
|
||||
|
||||
DoubleProperty translatorSetupProgressProperty() {
|
||||
return translatorSetupProgress;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String translatorSetupProgressLabel() {
|
||||
return translatorSetupProgressLabel.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTranslatorSetupProgressLabel(final String label) {
|
||||
translatorSetupProgressLabel.set(label);
|
||||
}
|
||||
|
||||
StringProperty translatorSetupProgressLabelProperty() {
|
||||
return translatorSetupProgressLabel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.github.gtache.autosubtitle.gui.fx;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.util.Objects;
|
||||
|
||||
@Singleton
|
||||
public class FXWorkBinder {
|
||||
|
||||
private final FXWorkModel workModel;
|
||||
private final FXParametersModel parametersModel;
|
||||
|
||||
@Inject
|
||||
FXWorkBinder(final FXWorkModel workModel, final FXParametersModel parametersModel) {
|
||||
this.workModel = Objects.requireNonNull(workModel);
|
||||
this.parametersModel = Objects.requireNonNull(parametersModel);
|
||||
}
|
||||
|
||||
void createBindings() {
|
||||
workModel.extractionModelProperty().bind(parametersModel.extractionModelProperty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
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;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
/**
|
||||
* FX implementation of {@link WorkController}
|
||||
*/
|
||||
@Singleton
|
||||
public class FXWorkController extends AbstractFXController implements WorkController, SubtitleExtractorListener {
|
||||
|
||||
private static final Logger logger = LogManager.getLogger(FXWorkController.class);
|
||||
|
||||
private static final List<String> VIDEO_EXTENSIONS = Stream.of("webm", "mkv", "flv", "vob", "ogv", "ogg",
|
||||
"drc", "gif", "gifv", "mng", "avi", "mts", "m2ts", "ts", "mov", "qt", "wmv", "yuv", "rm", "rmvb",
|
||||
"viv", "asf", "amv", "mp4", "m4p", "m4v", "mpg", "mp2", "mpeg", "mpe", "mpv", "m2v", "m4v", "svi",
|
||||
"3gp", "3g2", "mxf", "roq", "nsv", "flv", "f4v", "f4p", "f4a", "f4b").map(s -> "*." + s).toList();
|
||||
|
||||
@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
|
||||
private ProgressBar progressBar;
|
||||
@FXML
|
||||
private Label progressDetailLabel;
|
||||
@FXML
|
||||
private ResourceBundle resources;
|
||||
|
||||
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) {
|
||||
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() {
|
||||
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());
|
||||
|
||||
extractButton.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)));
|
||||
addSubtitleButton.disableProperty().bind(Bindings.isEmpty(model.subtitles()).or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
|
||||
loadSubtitlesButton.disableProperty().bind(Bindings.isEmpty(model.subtitles()).or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
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));
|
||||
model.selectedSubtitleProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
mediaController.seek(newValue.start());
|
||||
}
|
||||
});
|
||||
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));
|
||||
progressBar.progressProperty().bindBidirectional(model.progressProperty());
|
||||
binder.createBindings();
|
||||
|
||||
subtitleExtractor.addListener(this);
|
||||
}
|
||||
|
||||
private void editFocusedCell() {
|
||||
final var focusedCell = subtitlesTable.getFocusModel().getFocusedCell();
|
||||
if (focusedCell != null) {
|
||||
subtitlesTable.edit(focusedCell.getRow(), focusedCell.getTableColumn());
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void fileButtonPressed() {
|
||||
final var filePicker = new FileChooser();
|
||||
final var extensionFilter = new FileChooser.ExtensionFilter("All supported", VIDEO_EXTENSIONS);
|
||||
filePicker.getExtensionFilters().add(extensionFilter);
|
||||
filePicker.setSelectedExtensionFilter(extensionFilter);
|
||||
final var file = filePicker.showOpenDialog(window());
|
||||
if (file != null) {
|
||||
loadVideo(file.toPath());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void extractSubtitles() {
|
||||
if (model.video() != null) {
|
||||
model.setStatus(WorkStatus.EXTRACTING);
|
||||
CompletableFuture.supplyAsync(this::extractAsync).whenCompleteAsync(this::manageExtractResult, Platform::runLater);
|
||||
}
|
||||
}
|
||||
|
||||
private SubtitleCollection extractAsync() {
|
||||
try {
|
||||
return subtitleExtractor.extract(model.video(), model.videoLanguage(), model.extractionModel());
|
||||
} catch (final ExtractException e) {
|
||||
throw new CompletionException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void manageExtractResult(final SubtitleCollection newCollection, final Throwable t) {
|
||||
if (t == null) {
|
||||
loadCollection(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()));
|
||||
}
|
||||
model.setStatus(WorkStatus.IDLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadVideo(final Path file) {
|
||||
try {
|
||||
final var loadedVideo = videoLoader.loadVideo(file);
|
||||
fileField.setText(file.toAbsolutePath().toString());
|
||||
model.videoProperty().set(loadedVideo);
|
||||
} catch (final IOException e) {
|
||||
logger.error("Error loading video {}", file, e);
|
||||
showErrorDialog(resources.getString("work.load.video.error.title"), MessageFormat.format(resources.getString("work.load.video.error.label"), e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@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() {
|
||||
final var filePicker = new FileChooser();
|
||||
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);
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void exportHardPressed() {
|
||||
final var filePicker = new FileChooser();
|
||||
final var file = filePicker.showSaveDialog(window());
|
||||
if (file != null) {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
videoConverter.addHardSubtitles(model.video(), model.subtitleCollection());
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public FXWorkModel model() {
|
||||
return model;
|
||||
}
|
||||
|
||||
public Window window() {
|
||||
return fileField.getScene().getWindow();
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void extractPressed() {
|
||||
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(() -> {
|
||||
model.setProgress(event.progress());
|
||||
progressDetailLabel.setText(event.message());
|
||||
});
|
||||
}
|
||||
|
||||
@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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
package com.github.gtache.autosubtitle.gui.fx;
|
||||
|
||||
import com.github.gtache.autosubtitle.Language;
|
||||
import com.github.gtache.autosubtitle.Video;
|
||||
import com.github.gtache.autosubtitle.gui.WorkModel;
|
||||
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 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.SimpleDoubleProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
|
||||
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}
|
||||
*/
|
||||
@Singleton
|
||||
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;
|
||||
|
||||
@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())));
|
||||
videoLanguage.addListener((observable, oldValue, newValue) -> {
|
||||
FXCollections.observableArrayList(Arrays.stream(Language.values()).filter(l -> l != Language.AUTO && l != newValue).sorted(Comparator.naturalOrder()).toList());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Video video() {
|
||||
return video.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExtractionModel extractionModel() {
|
||||
return extractionModel.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setExtractionModel(final ExtractionModel model) {
|
||||
extractionModel.set(model);
|
||||
}
|
||||
|
||||
ObjectProperty<ExtractionModel> extractionModelProperty() {
|
||||
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();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStatus(final WorkStatus status) {
|
||||
workStatus.set(status);
|
||||
}
|
||||
|
||||
ObjectProperty<WorkStatus> statusProperty() {
|
||||
return workStatus;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double progress() {
|
||||
return progress.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setProgress(final double progress) {
|
||||
this.progress.set(progress);
|
||||
}
|
||||
|
||||
DoubleProperty progressProperty() {
|
||||
return progress;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.github.gtache.autosubtitle.gui.fx;
|
||||
|
||||
import com.github.gtache.autosubtitle.Language;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
class LanguageStringConverter extends StringConverter<Language> {
|
||||
@Override
|
||||
public String toString(final Language object) {
|
||||
return object == null ? "" : object.englishName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Language fromString(final String string) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.github.gtache.autosubtitle.gui.fx;
|
||||
|
||||
import com.github.gtache.autosubtitle.gui.TimeFormatter;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
class TimeStringConverter extends StringConverter<Long> {
|
||||
|
||||
private final TimeFormatter timeFormatter;
|
||||
|
||||
TimeStringConverter(final TimeFormatter timeFormatter) {
|
||||
this.timeFormatter = Objects.requireNonNull(timeFormatter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(final Long object) {
|
||||
return object == null ? "" : timeFormatter.format(object);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long fromString(final String string) {
|
||||
return string == null ? 0L : timeFormatter.parse(string);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.github.gtache.autosubtitle.modules.gui.fx;
|
||||
|
||||
import com.github.gtache.autosubtitle.gui.TimeFormatter;
|
||||
import com.github.gtache.autosubtitle.gui.fx.ColonTimeFormatter;
|
||||
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.FXWorkController;
|
||||
import com.github.gtache.autosubtitle.modules.gui.impl.Pause;
|
||||
import com.github.gtache.autosubtitle.modules.gui.impl.Play;
|
||||
import dagger.Binds;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.image.Image;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.prefs.Preferences;
|
||||
|
||||
/**
|
||||
* Dagger module for FX
|
||||
*/
|
||||
@Module
|
||||
public abstract class FXModule {
|
||||
|
||||
@Binds
|
||||
abstract TimeFormatter bindsTimeFormatter(final ColonTimeFormatter formatter);
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
static FXMLLoader providesFXMLLoader(final FXMainController mainController, final FXSetupController setupController, final FXParametersController parametersController,
|
||||
final FXWorkController workController, final FXMediaController mediaController, 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 {
|
||||
throw new IllegalArgumentException("Unknown controller " + c);
|
||||
}
|
||||
});
|
||||
return loader;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Play
|
||||
static Image providesPlayImage(@Play final byte[] playImage) {
|
||||
return new Image(new ByteArrayInputStream(playImage));
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Pause
|
||||
static Image providesPauseImage(@Pause final byte[] pauseImage) {
|
||||
return new Image(new ByteArrayInputStream(pauseImage));
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
static Preferences providesPreferences() {
|
||||
return Preferences.userNodeForPackage(FXParametersController.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package com.github.gtache.autosubtitle.subtitle.gui.fx;
|
||||
|
||||
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;
|
||||
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 {
|
||||
|
||||
private final StringProperty content;
|
||||
private final LongProperty start;
|
||||
private final LongProperty end;
|
||||
private final ObjectProperty<Font> font;
|
||||
private final ObjectProperty<Bounds> location;
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String content() {
|
||||
return content.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setContent(final String content) {
|
||||
this.content.set(content);
|
||||
}
|
||||
|
||||
public StringProperty contentProperty() {
|
||||
return content;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long start() {
|
||||
return start.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStart(final long start) {
|
||||
this.start.set(start);
|
||||
}
|
||||
|
||||
public LongProperty startProperty() {
|
||||
return start;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long end() {
|
||||
return end.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnd(final long end) {
|
||||
this.end.set(end);
|
||||
}
|
||||
|
||||
public LongProperty endProperty() {
|
||||
return end;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Font font() {
|
||||
return font.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFont(final Font font) {
|
||||
this.font.set(font);
|
||||
}
|
||||
|
||||
public ObjectProperty<Font> fontProperty() {
|
||||
return font;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bounds bounds() {
|
||||
return location.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBounds(final Bounds bounds) {
|
||||
this.location.set(bounds);
|
||||
}
|
||||
|
||||
public ObjectProperty<Bounds> locationProperty() {
|
||||
return location;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.github.gtache.autosubtitle.subtitle.gui.fx;
|
||||
|
||||
import com.github.gtache.autosubtitle.subtitle.Subtitle;
|
||||
import javafx.scene.control.Label;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Special label for subtitles
|
||||
*/
|
||||
public class SubtitleLabel extends Label {
|
||||
|
||||
private final Subtitle subtitle;
|
||||
private final Delta delta;
|
||||
|
||||
public SubtitleLabel(final Subtitle subtitle) {
|
||||
this.subtitle = Objects.requireNonNull(subtitle);
|
||||
this.delta = new Delta();
|
||||
setText(subtitle.content());
|
||||
}
|
||||
|
||||
public Subtitle subtitle() {
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
public double getDraggedX() {
|
||||
return delta.x;
|
||||
}
|
||||
|
||||
public double getDraggedY() {
|
||||
return delta.y;
|
||||
}
|
||||
|
||||
public void setDragged(final double x, final double y) {
|
||||
delta.x = x;
|
||||
delta.y = y;
|
||||
}
|
||||
|
||||
private static final class Delta {
|
||||
private double x;
|
||||
private double y;
|
||||
}
|
||||
}
|
||||
18
gui/fx/src/main/java/module-info.java
Normal file
18
gui/fx/src/main/java/module-info.java
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* FX module for auto-subtitle
|
||||
*/
|
||||
module com.github.gtache.autosubtitle.fx {
|
||||
requires transitive com.github.gtache.autosubtitle.core;
|
||||
requires transitive com.github.gtache.autosubtitle.gui.core;
|
||||
requires transitive javafx.controls;
|
||||
requires transitive javafx.media;
|
||||
requires transitive javafx.fxml;
|
||||
requires org.controlsfx.controls;
|
||||
requires org.apache.logging.log4j;
|
||||
requires java.desktop;
|
||||
requires transitive java.prefs;
|
||||
|
||||
exports com.github.gtache.autosubtitle.gui.fx;
|
||||
exports com.github.gtache.autosubtitle.modules.gui.fx;
|
||||
opens com.github.gtache.autosubtitle.gui.fx to javafx.fxml;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.control.Tab?>
|
||||
<?import javafx.scene.control.TabPane?>
|
||||
<TabPane fx:id="tabPane" xmlns="http://javafx.com/javafx/22" xmlns:fx="http://javafx.com/fxml/1"
|
||||
fx:controller="com.github.gtache.autosubtitle.gui.fx.FXMainController">
|
||||
<tabs>
|
||||
<Tab closable="false" text="%main.tab.work.label">
|
||||
<content>
|
||||
<fx:include source="workView.fxml"/>
|
||||
</content>
|
||||
</Tab>
|
||||
<Tab closable="false" text="%main.tab.setup.label">
|
||||
<content>
|
||||
<fx:include source="setupView.fxml"/>
|
||||
</content>
|
||||
</Tab>
|
||||
<Tab closable="false" text="%main.tab.parameters.label">
|
||||
<content>
|
||||
<fx:include source="parametersView.fxml"/>
|
||||
</content>
|
||||
</Tab>
|
||||
</tabs>
|
||||
</TabPane>
|
||||
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.Slider?>
|
||||
<?import javafx.scene.layout.BorderPane?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.StackPane?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import javafx.scene.media.MediaView?>
|
||||
<BorderPane xmlns="http://javafx.com/javafx/22" xmlns:fx="http://javafx.com/fxml/1"
|
||||
fx:controller="com.github.gtache.autosubtitle.gui.fx.FXMediaController">
|
||||
<bottom>
|
||||
<VBox BorderPane.alignment="CENTER">
|
||||
<children>
|
||||
<HBox alignment="CENTER" spacing="10.0">
|
||||
<children>
|
||||
<Slider fx:id="playSlider" HBox.hgrow="ALWAYS">
|
||||
<padding>
|
||||
<Insets left="10.0"/>
|
||||
</padding>
|
||||
</Slider>
|
||||
<Label fx:id="playLabel" text="Label">
|
||||
<padding>
|
||||
<Insets right="10.0"/>
|
||||
</padding>
|
||||
</Label>
|
||||
</children>
|
||||
<padding>
|
||||
<Insets top="10.0"/>
|
||||
</padding>
|
||||
</HBox>
|
||||
<HBox alignment="CENTER" spacing="10.0">
|
||||
<children>
|
||||
<Button fx:id="playButton" mnemonicParsing="false" onAction="#playPressed">
|
||||
<HBox.margin>
|
||||
<Insets right="20.0"/>
|
||||
</HBox.margin>
|
||||
</Button>
|
||||
<Label text="%media.volume.label"/>
|
||||
<Slider fx:id="volumeSlider" value="100"/>
|
||||
<Label fx:id="volumeValueLabel" text="Label"/>
|
||||
</children>
|
||||
<padding>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
|
||||
</padding>
|
||||
</HBox>
|
||||
</children>
|
||||
</VBox>
|
||||
</bottom>
|
||||
<center>
|
||||
<StackPane fx:id="stackPane">
|
||||
<children>
|
||||
<MediaView fx:id="videoView" fitHeight="${stackPane.height}" fitWidth="${stackPane.width}"/>
|
||||
</children>
|
||||
</StackPane>
|
||||
</center>
|
||||
</BorderPane>
|
||||
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.TextField?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import org.controlsfx.control.PrefixSelectionComboBox?>
|
||||
<GridPane hgap="10.0" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" vgap="10.0"
|
||||
xmlns="http://javafx.com/javafx/22" xmlns:fx="http://javafx.com/fxml/1"
|
||||
fx:controller="com.github.gtache.autosubtitle.gui.fx.FXParametersController">
|
||||
<columnConstraints>
|
||||
<ColumnConstraints hgrow="SOMETIMES"/>
|
||||
<ColumnConstraints hgrow="SOMETIMES"/>
|
||||
</columnConstraints>
|
||||
<rowConstraints>
|
||||
<RowConstraints vgrow="SOMETIMES"/>
|
||||
<RowConstraints vgrow="SOMETIMES"/>
|
||||
<RowConstraints vgrow="SOMETIMES"/>
|
||||
<RowConstraints vgrow="SOMETIMES"/>
|
||||
<RowConstraints vgrow="SOMETIMES"/>
|
||||
</rowConstraints>
|
||||
<children>
|
||||
<Label text="%parameters.extraction.model.label"/>
|
||||
<PrefixSelectionComboBox fx:id="extractionModelCombobox" GridPane.columnIndex="1"/>
|
||||
<PrefixSelectionComboBox fx:id="extractionOutputFormat" GridPane.columnIndex="1" GridPane.rowIndex="1"/>
|
||||
<Label text="%parameters.subtitles.output.format" GridPane.rowIndex="1"/>
|
||||
<Label text="%parameters.subtitles.font.size" GridPane.rowIndex="3"/>
|
||||
<Label text="%parameters.subtitles.font.family" GridPane.rowIndex="2"/>
|
||||
<PrefixSelectionComboBox fx:id="fontFamilyCombobox" GridPane.columnIndex="1" GridPane.rowIndex="2"/>
|
||||
<TextField fx:id="fontSizeField" GridPane.columnIndex="1" GridPane.rowIndex="3"/>
|
||||
<Button mnemonicParsing="false" onAction="#savePressed" text="%parameters.button.save.label"
|
||||
GridPane.columnIndex="1" GridPane.rowIndex="4"/>
|
||||
<Button mnemonicParsing="false" onAction="#resetPressed" text="%parameters.button.reset.label"
|
||||
GridPane.rowIndex="4"/>
|
||||
</children>
|
||||
<padding>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
|
||||
</padding>
|
||||
</GridPane>
|
||||
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.MenuButton?>
|
||||
<?import javafx.scene.control.ProgressBar?>
|
||||
<?import javafx.scene.layout.ColumnConstraints?>
|
||||
<?import javafx.scene.layout.GridPane?>
|
||||
<?import javafx.scene.layout.RowConstraints?>
|
||||
|
||||
<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.FXSetupController">
|
||||
<columnConstraints>
|
||||
<ColumnConstraints hgrow="SOMETIMES" />
|
||||
<ColumnConstraints hgrow="SOMETIMES" />
|
||||
<ColumnConstraints hgrow="SOMETIMES" />
|
||||
<ColumnConstraints hgrow="ALWAYS" />
|
||||
<ColumnConstraints hgrow="SOMETIMES" />
|
||||
</columnConstraints>
|
||||
<rowConstraints>
|
||||
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
|
||||
<RowConstraints vgrow="SOMETIMES" />
|
||||
<RowConstraints vgrow="SOMETIMES" />
|
||||
<RowConstraints vgrow="SOMETIMES" />
|
||||
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="ALWAYS" />
|
||||
</rowConstraints>
|
||||
<children>
|
||||
<Label fx:id="converterNameLabel" text="Label" GridPane.rowIndex="1" />
|
||||
<Label fx:id="converterStatusLabel" text="Label" GridPane.columnIndex="1" GridPane.rowIndex="1" />
|
||||
<Label fx:id="extractorNameLabel" text="Label" GridPane.rowIndex="2" />
|
||||
<Label fx:id="extractorStatusLabel" text="Label" GridPane.columnIndex="1" GridPane.rowIndex="2" />
|
||||
<Label fx:id="translatorNameLabel" text="Label" GridPane.rowIndex="3" />
|
||||
<Label fx:id="translatorStatusLabel" text="Label" GridPane.columnIndex="1" GridPane.rowIndex="3" />
|
||||
<MenuButton fx:id="converterButton" mnemonicParsing="false" text="%setup.menu.label" GridPane.columnIndex="2" GridPane.rowIndex="1" />
|
||||
<MenuButton fx:id="extractorButton" mnemonicParsing="false" text="%setup.menu.label" GridPane.columnIndex="2" GridPane.rowIndex="2" />
|
||||
<MenuButton fx:id="translatorButton" mnemonicParsing="false" text="%setup.menu.label" GridPane.columnIndex="2" GridPane.rowIndex="3" />
|
||||
<Label text="%setup.description.label" GridPane.columnSpan="2147483647" />
|
||||
<ProgressBar fx:id="converterProgress" prefWidth="200.0" progress="0.0" GridPane.columnIndex="3" GridPane.rowIndex="1" />
|
||||
<ProgressBar fx:id="extractorProgress" prefWidth="200.0" progress="0.0" GridPane.columnIndex="3" GridPane.rowIndex="2" />
|
||||
<ProgressBar fx:id="translatorProgress" prefWidth="200.0" progress="0.0" GridPane.columnIndex="3" GridPane.rowIndex="3" />
|
||||
<Label fx:id="converterProgressLabel" text="Label" GridPane.columnIndex="4" GridPane.rowIndex="1" />
|
||||
<Label fx:id="extractorProgressLabel" text="Label" GridPane.columnIndex="4" GridPane.rowIndex="2" />
|
||||
<Label fx:id="translatorProgressLabel" text="Label" GridPane.columnIndex="4" GridPane.rowIndex="3" />
|
||||
</children>
|
||||
<padding>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</padding>
|
||||
</GridPane>
|
||||
@@ -0,0 +1,93 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?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>
|
||||
<ColumnConstraints hgrow="SOMETIMES"/>
|
||||
<ColumnConstraints hgrow="ALWAYS"/>
|
||||
<ColumnConstraints hgrow="SOMETIMES"/>
|
||||
</columnConstraints>
|
||||
<rowConstraints>
|
||||
<RowConstraints vgrow="SOMETIMES"/>
|
||||
<RowConstraints vgrow="ALWAYS"/>
|
||||
<RowConstraints vgrow="SOMETIMES"/>
|
||||
<RowConstraints vgrow="NEVER"/>
|
||||
</rowConstraints>
|
||||
<children>
|
||||
<TextField fx:id="fileField" editable="false" GridPane.columnIndex="1"/>
|
||||
<Button mnemonicParsing="false" onAction="#fileButtonPressed" text="%work.button.file.label"
|
||||
GridPane.columnIndex="2"/>
|
||||
<HBox spacing="10.0" GridPane.columnIndex="1" GridPane.rowIndex="2">
|
||||
<children>
|
||||
<Button fx:id="loadSubtitlesButton" mnemonicParsing="false" onAction="#loadSubtitlesPressed"
|
||||
text="%work.button.load.label"/>
|
||||
<Button fx:id="extractButton" mnemonicParsing="false" onAction="#extractPressed"
|
||||
text="%work.button.extract.label"/>
|
||||
</children>
|
||||
</HBox>
|
||||
<HBox alignment="CENTER_RIGHT" spacing="10.0" GridPane.columnIndex="2" GridPane.rowIndex="2">
|
||||
<children>
|
||||
<Button fx:id="exportSoftButton" mnemonicParsing="false" onAction="#exportSoftPressed"
|
||||
text="%work.button.export.soft.label">
|
||||
<tooltip>
|
||||
<Tooltip text="%work.button.export.soft.tooltip"/>
|
||||
</tooltip>
|
||||
</Button>
|
||||
<Button fx:id="exportHardButton" mnemonicParsing="false" onAction="#exportHardPressed"
|
||||
text="%work.button.export.hard.label">
|
||||
<tooltip>
|
||||
<Tooltip text="%work.button.export.hard.tooltip"/>
|
||||
</tooltip>
|
||||
</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="resetButton" mnemonicParsing="false" onAction="#resetButtonPressed"
|
||||
text="%work.button.reset.label"/>
|
||||
<Button fx:id="saveSubtitlesButton" mnemonicParsing="false" onAction="#saveSubtitlesPressed"
|
||||
text="%work.button.subtitles.save.label"/>
|
||||
<Button fx:id="addSubtitleButton" mnemonicParsing="false" onAction="#addSubtitlePressed" text="+"/>
|
||||
</children>
|
||||
<GridPane.margin>
|
||||
<Insets/>
|
||||
</GridPane.margin>
|
||||
</HBox>
|
||||
<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>
|
||||
<Label fx:id="progressDetailLabel"/>
|
||||
<ProgressBar fx:id="progressBar" maxWidth="1.7976931348623157E308" HBox.hgrow="ALWAYS"/>
|
||||
</children>
|
||||
</HBox>
|
||||
</children>
|
||||
<padding>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
|
||||
</padding>
|
||||
</GridPane>
|
||||
Reference in New Issue
Block a user