Can playback video with controls, need to fix performance and reducing window size

This commit is contained in:
Guillaume Tâche
2024-08-01 19:42:25 +02:00
parent 75829244b9
commit a94eaff9ad
102 changed files with 2921 additions and 423 deletions

View File

@@ -11,6 +11,10 @@
<artifactId>autosubtitle-fx</artifactId>
<properties>
<javafx.version>22.0.1</javafx.version>
</properties>
<dependencies>
<dependency>
<groupId>com.github.gtache.autosubtitle</groupId>
@@ -24,15 +28,19 @@
<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>22.0.1</version>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>22.0.1</version>
<version>${javafx.version}</version>
</dependency>
</dependencies>

View File

@@ -1,146 +1,42 @@
package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.subtitle.EditableSubtitle;
import com.github.gtache.autosubtitle.subtitle.Subtitle;
import com.github.gtache.autosubtitle.subtitle.SubtitleExtractor;
import com.github.gtache.autosubtitle.VideoConverter;
import com.github.gtache.autosubtitle.gui.MainController;
import com.github.gtache.autosubtitle.impl.FileVideoImpl;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.layout.StackPane;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import javafx.scene.media.MediaView;
import javafx.stage.FileChooser;
import javafx.stage.Window;
import javafx.scene.control.TabPane;
import javax.inject.Inject;
import java.nio.file.Path;
import java.time.Duration;
import java.time.LocalTime;
import java.util.Comparator;
import static java.util.Objects.requireNonNull;
import javax.inject.Singleton;
import java.util.Objects;
/**
* FX implementation of {@link MainController}
*/
@Singleton
public class FXMainController implements MainController {
@FXML
private MediaView videoView;
@FXML
private TextField fileField;
@FXML
private Button extractButton;
@FXML
private Button resetButton;
@FXML
private Button exportButton;
@FXML
private TableView<EditableSubtitle> subtitlesTable;
@FXML
private TableColumn<EditableSubtitle, LocalTime> startColumn;
@FXML
private TableColumn<EditableSubtitle, LocalTime> endColumn;
@FXML
private TableColumn<EditableSubtitle, String> textColumn;
@FXML
private StackPane stackPane;
private TabPane tabPane;
private final FXMainModel model;
private final SubtitleExtractor subtitleExtractor;
private final VideoConverter videoConverter;
@Inject
FXMainController(final FXMainModel model, final SubtitleExtractor subtitleExtractor, final VideoConverter videoConverter) {
this.model = requireNonNull(model);
this.subtitleExtractor = requireNonNull(subtitleExtractor);
this.videoConverter = requireNonNull(videoConverter);
FXMainController(final FXMainModel model) {
this.model = Objects.requireNonNull(model);
}
@FXML
private void initialize() {
extractButton.disableProperty().bind(model.videoProperty().isNull());
resetButton.disableProperty().bind(Bindings.isEmpty(model.subtitles()));
exportButton.disableProperty().bind(Bindings.isEmpty(model.subtitles()));
subtitlesTable.setItems(model.subtitles());
startColumn.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue() == null ? null : param.getValue().start()));
endColumn.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue() == null ? null : param.getValue().end()));
textColumn.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue() == null ? "" : param.getValue().content()));
model.selectedSubtitleProperty().addListener(new ChangeListener<EditableSubtitle>() {
@Override
public void changed(final ObservableValue<? extends EditableSubtitle> observable, final EditableSubtitle oldValue, final EditableSubtitle newValue) {
if (newValue != null) {
videoView.getMediaPlayer().seek(Duration.of(newValue.start().to));
}
}
});
}
@FXML
private void fileButtonPressed() {
final var filePicker = new FileChooser();
final var file = filePicker.showOpenDialog(window());
if (file != null) {
loadVideo(file.toPath());
}
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 extractSubtitles() {
if (model.video() != null) {
final var subtitles = subtitleExtractor.extract(model.video());
model.subtitles().setAll(subtitles.stream().sorted(Comparator.comparing(Subtitle::start)).toList());
}
}
@Override
public void loadVideo(final Path file) {
fileField.setText(file.toAbsolutePath().toString());
model.videoProperty().set(new FileVideoImpl(file));
final var media = new Media(file.toUri().toString());
final var player = new MediaPlayer(media);
videoView.getMediaPlayer().dispose();
videoView.setMediaPlayer(player);
public void selectTab(final int index) {
model.selectTab(index);
}
@Override
public FXMainModel model() {
return model;
}
public Window window() {
return videoView.getScene().getWindow();
}
@FXML
private void extractPressed(final ActionEvent actionEvent) {
extractSubtitles();
}
@FXML
private void exportPressed(final ActionEvent actionEvent) {
final var filePicker = new FileChooser();
final var file = filePicker.showSaveDialog(window());
if (file != null) {
videoConverter.addSoftSubtitles(model.video(), model.subtitles());
}
}
@FXML
private void resetButtonPressed(final ActionEvent actionEvent) {
}
}

View File

@@ -1,51 +1,36 @@
package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.subtitle.EditableSubtitle;
import com.github.gtache.autosubtitle.Video;
import com.github.gtache.autosubtitle.gui.MainModel;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
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 ObjectProperty<Video> video;
private final ObservableList<EditableSubtitle> subtitles;
private final ObjectProperty<EditableSubtitle> subtitle;
private final IntegerProperty selectedTab;
@Inject
FXMainModel() {
video = new SimpleObjectProperty<>();
subtitles = FXCollections.observableArrayList();
subtitle = new SimpleObjectProperty<>();
selectedTab = new SimpleIntegerProperty(0);
}
@Override
public Video video() {
return video.get();
}
public ObjectProperty<Video> videoProperty() {
return video;
public int selectedTab() {
return selectedTab.get();
}
@Override
public ObservableList<EditableSubtitle> subtitles() {
return subtitles;
public void selectTab(final int index) {
selectedTab.set(index);
}
@Override
public EditableSubtitle selectedSubtitle() {
return subtitle.get();
}
public ObjectProperty<EditableSubtitle> selectedSubtitleProperty() {
return subtitle;
IntegerProperty selectedTabProperty() {
return selectedTab;
}
}

View File

@@ -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(workModel.subtitles(), mediaModel.subtitles());
}
}

View File

@@ -0,0 +1,171 @@
package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.gui.MediaController;
import com.github.gtache.autosubtitle.impl.FileVideoImpl;
import com.github.gtache.autosubtitle.modules.gui.Pause;
import com.github.gtache.autosubtitle.modules.gui.Play;
import com.github.gtache.autosubtitle.subtitle.Subtitle;
import com.github.gtache.autosubtitle.subtitle.fx.SubtitleLabel;
import javafx.beans.binding.Bindings;
import javafx.fxml.FXML;
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.StackPane;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import javafx.scene.media.MediaView;
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 Image playImage;
private final Image pauseImage;
@Inject
FXMediaController(final FXMediaModel model, @Play final Image playImage, @Pause final Image pauseImage) {
this.model = requireNonNull(model);
this.playImage = requireNonNull(playImage);
this.pauseImage = requireNonNull(pauseImage);
}
@FXML
private void initialize() {
volumeValueLabel.textProperty().bind(Bindings.createStringBinding(() -> String.valueOf((int) model.volume()), model.volumeProperty()));
playLabel.textProperty().bind(Bindings.createStringBinding(() -> 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)) {
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);
model.subtitles().forEach(s -> {
if (s.start() <= millis && s.end() >= millis) {
final var label = createDraggableLabel(s);
stackPane.getChildren().add(label);
}
});
});
playSlider.valueProperty().addListener((observable1, oldValue1, newValue1) -> seek(newValue1.longValue()));
player.volumeProperty().bindBidirectional(model.volumeProperty());
player.setOnPlaying(() -> model.setIsPlaying(true));
player.setOnPaused(() -> 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()));
}
@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.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;
}
}

View File

@@ -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 javafx.collections.ObservableList;
import javax.inject.Inject;
import javax.inject.Singleton;
/**
* 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 ObservableList<EditableSubtitle> subtitles;
@Inject
FXMediaModel() {
this.video = new SimpleObjectProperty<>();
this.volume = new SimpleDoubleProperty(0.0);
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 ObservableList<EditableSubtitle> subtitles() {
return subtitles;
}
LongProperty positionProperty() {
return position;
}
}

View File

@@ -1,24 +1,41 @@
package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.gui.SetupController;
import com.github.gtache.autosubtitle.setup.SetupException;
import com.github.gtache.autosubtitle.setup.SetupManager;
import com.github.gtache.autosubtitle.setup.modules.impl.SubtitleExtractor;
import com.github.gtache.autosubtitle.setup.modules.impl.Translator;
import com.github.gtache.autosubtitle.setup.modules.impl.VideoConverter;
import com.github.gtache.autosubtitle.setup.SetupStatus;
import com.github.gtache.autosubtitle.setup.modules.impl.SubtitleExtractorSetup;
import com.github.gtache.autosubtitle.setup.modules.impl.TranslatorSetup;
import com.github.gtache.autosubtitle.setup.modules.impl.VideoConverterSetup;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
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;
/**
* FX implementation of {@link SetupController}
*/
@Singleton
public class FXSetupController implements SetupController {
private static final Logger logger = LogManager.getLogger(FXSetupController.class);
@FXML
private ResourceBundle resources;
@FXML
@@ -39,89 +56,209 @@ public class FXSetupController implements SetupController {
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;
@Inject
FXSetupController(final FXSetupModel model, @VideoConverter final SetupManager converterManager,
@SubtitleExtractor final SetupManager extractorManager, @Translator final SetupManager translatorManager) {
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);
}
@FXML
private void initialize() {
statusMap.put(converterManager, model.videoConverterStatusProperty());
statusMap.put(extractorManager, model.subtitleExtractorStatusProperty());
statusMap.put(translatorManager, model.translatorStatusProperty());
model.setSubtitleExtractorStatus(extractorManager.status());
model.setVideoConverterStatus(converterManager.status());
model.setTranslatorStatus(translatorManager.status());
converterNameLabel.setText(converterManager.name());
converterStatusLabel.setText(converterManager.status().toString());
extractorNameLabel.setText(extractorManager.name());
extractorStatusLabel.setText(extractorManager.status().toString());
translatorNameLabel.setText(translatorManager.name());
translatorStatusLabel.setText(translatorManager.status().toString());
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(0));
extractorProgress.visibleProperty().bind(model.subtitleExtractorSetupProgressProperty().greaterThan(0));
translatorProgress.visibleProperty().bind(model.translatorSetupProgressProperty().greaterThan(0));
converterProgressLabel.textProperty().bind(model.videoConverterSetupProgressLabelProperty());
extractorProgressLabel.textProperty().bind(model.subtitleExtractorSetupProgressLabelProperty());
translatorProgressLabel.textProperty().bind(model.translatorSetupProgressLabelProperty());
converterProgressLabel.visibleProperty().bind(model.videoConverterSetupProgressProperty().greaterThan(0));
extractorProgressLabel.visibleProperty().bind(model.subtitleExtractorSetupProgressProperty().greaterThan(0));
translatorProgressLabel.visibleProperty().bind(model.translatorSetupProgressProperty().greaterThan(0));
bindMenu(converterButton, converterManager);
bindMenu(extractorButton, extractorManager);
bindMenu(translatorButton, translatorManager);
}
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"));
installItem.setOnAction(e -> tryInstall(setupManager));
button.getItems().add(installItem);
}
case INSTALLED -> {
final var reinstallItem = new MenuItem(resources.getString("setup.menu.reinstall"));
reinstallItem.setOnAction(e -> tryReinstall(setupManager));
final var uninstallItem = new MenuItem(resources.getString("setup.menu.uninstall"));
uninstallItem.setOnAction(e -> tryUninstall(setupManager));
button.getItems().addAll(reinstallItem, uninstallItem);
}
case UPDATE_AVAILABLE -> {
final var updateItem = new MenuItem(resources.getString("setup.menu.update"));
updateItem.setOnAction(e -> tryUpdate(setupManager));
final var reinstallItem = new MenuItem(resources.getString("setup.menu.reinstall"));
reinstallItem.setOnAction(e -> tryReinstall(setupManager));
final var uninstallItem = new MenuItem(resources.getString("setup.menu.uninstall"));
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() {
converterManager.install();
tryInstall(converterManager);
}
@Override
public void uninstallVideoConverter() {
converterManager.uninstall();
tryUninstall(converterManager);
}
@Override
public void updateVideoConverter() {
converterManager.update();
tryUpdate(converterManager);
}
@Override
public void reinstallVideoConverter() {
converterManager.reinstall();
tryReinstall(converterManager);
}
@Override
public void installSubtitleExtractor() {
extractorManager.install();
tryInstall(extractorManager);
}
@Override
public void uninstallSubtitleExtractor() {
extractorManager.uninstall();
tryUninstall(extractorManager);
}
@Override
public void updateSubtitleExtractor() {
extractorManager.update();
tryUpdate(extractorManager);
}
@Override
public void reinstallSubtitleExtractor() {
extractorManager.reinstall();
tryReinstall(extractorManager);
}
@Override
public void installTranslator() {
translatorManager.install();
tryInstall(translatorManager);
}
@Override
public void uninstallTranslator() {
translatorManager.uninstall();
tryUninstall(translatorManager);
}
@Override
public void updateTranslator() {
translatorManager.update();
tryUpdate(translatorManager);
}
@Override
public void reinstallTranslator() {
translatorManager.reinstall();
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) {
try {
consumer.accept(manager);
statusMap.get(manager).set(manager.status());
} catch (final SetupException e) {
logger.error("Error {}ing {}", operation, manager.name(), e);
showErrorDialog(resources.getString("setup." + operation + ".error.title"), MessageFormat.format(resources.getString("setup." + operation + ".error.message"), e.getMessage()));
}
}
@FunctionalInterface
private interface SetupConsumer {
void accept(SetupManager manager) throws SetupException;
}
private static void showErrorDialog(final String title, final String message) {
final var alert = new Alert(Alert.AlertType.ERROR, message, ButtonType.OK);
alert.setTitle(title);
alert.showAndWait();
}
@Override
@@ -130,6 +267,6 @@ public class FXSetupController implements SetupController {
}
public Window window() {
return extractorButton.getScene().getWindow();
return converterNameLabel.getScene().getWindow();
}
}

View File

@@ -1,31 +1,80 @@
package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.gui.SetupModel;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
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 BooleanProperty subtitleExtractorInstalled;
private final BooleanProperty subtitleExtractorUpdateAvailable;
private final BooleanProperty videoConverterInstalled;
private final BooleanProperty videoConverterUpdateAvailable;
private final BooleanProperty translatorInstalled;
private final BooleanProperty translatorUpdateAvailable;
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.subtitleExtractorInstalled = new SimpleBooleanProperty(false);
this.subtitleExtractorUpdateAvailable = new SimpleBooleanProperty(false);
this.videoConverterInstalled = new SimpleBooleanProperty(false);
this.videoConverterUpdateAvailable = new SimpleBooleanProperty(false);
this.translatorInstalled = new SimpleBooleanProperty(false);
this.translatorUpdateAvailable = new SimpleBooleanProperty(false);
this.subtitleExtractorStatus = new SimpleObjectProperty<>(SetupStatus.NOT_INSTALLED);
this.subtitleExtractorInstalled = new ReadOnlyBooleanWrapper(false);
this.subtitleExtractorUpdateAvailable = new ReadOnlyBooleanWrapper(false);
this.subtitleExtractorSetupProgress = new SimpleDoubleProperty(0);
this.subtitleExtractorSetupProgressLabel = new SimpleStringProperty("");
this.videoConverterStatus = new SimpleObjectProperty<>(SetupStatus.NOT_INSTALLED);
this.videoConverterInstalled = new ReadOnlyBooleanWrapper(false);
this.videoConverterUpdateAvailable = new ReadOnlyBooleanWrapper(false);
this.videoConverterSetupProgress = new SimpleDoubleProperty(0);
this.videoConverterSetupProgressLabel = new SimpleStringProperty("");
this.translatorStatus = new SimpleObjectProperty<>(SetupStatus.NOT_INSTALLED);
this.translatorInstalled = new ReadOnlyBooleanWrapper(false);
this.translatorUpdateAvailable = new ReadOnlyBooleanWrapper(false);
this.translatorSetupProgress = new SimpleDoubleProperty(0);
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
@@ -33,13 +82,8 @@ public class FXSetupModel implements SetupModel {
return subtitleExtractorInstalled.get();
}
@Override
public void setSubtitleExtractorInstalled(final boolean installed) {
subtitleExtractorInstalled.set(installed);
}
BooleanProperty subtitleExtractorInstalledProperty() {
return subtitleExtractorInstalled;
ReadOnlyBooleanProperty subtitleExtractorInstalledProperty() {
return subtitleExtractorInstalled.getReadOnlyProperty();
}
@Override
@@ -48,12 +92,49 @@ public class FXSetupModel implements SetupModel {
}
@Override
public void setSubtitleExtractorUpdateAvailable(final boolean updateAvailable) {
subtitleExtractorUpdateAvailable.set(updateAvailable);
public double subtitleExtractorSetupProgress() {
return subtitleExtractorSetupProgress.get();
}
BooleanProperty subtitleExtractorUpdateAvailableProperty() {
return subtitleExtractorUpdateAvailable;
@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
@@ -61,13 +142,8 @@ public class FXSetupModel implements SetupModel {
return videoConverterInstalled.get();
}
@Override
public void setVideoConverterInstalled(final boolean installed) {
videoConverterInstalled.set(installed);
}
BooleanProperty videoConverterInstalledProperty() {
return videoConverterInstalled;
ReadOnlyBooleanProperty videoConverterInstalledProperty() {
return videoConverterInstalled.getReadOnlyProperty();
}
@Override
@@ -75,13 +151,50 @@ public class FXSetupModel implements SetupModel {
return videoConverterUpdateAvailable.get();
}
@Override
public void setVideoConverterUpdateAvailable(final boolean updateAvailable) {
videoConverterUpdateAvailable.set(updateAvailable);
ReadOnlyBooleanProperty videoConverterUpdateAvailableProperty() {
return videoConverterUpdateAvailable.getReadOnlyProperty();
}
BooleanProperty videoConverterUpdateAvailableProperty() {
return videoConverterUpdateAvailable;
@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
@@ -89,13 +202,8 @@ public class FXSetupModel implements SetupModel {
return translatorInstalled.get();
}
@Override
public void setTranslatorInstalled(final boolean installed) {
translatorInstalled.set(installed);
}
BooleanProperty translatorInstalledProperty() {
return translatorInstalled;
ReadOnlyBooleanProperty translatorInstalledProperty() {
return translatorInstalled.getReadOnlyProperty();
}
@Override
@@ -103,12 +211,35 @@ public class FXSetupModel implements SetupModel {
return translatorUpdateAvailable.get();
}
@Override
public void setTranslatorUpdateAvailable(final boolean updateAvailable) {
translatorUpdateAvailable.set(updateAvailable);
ReadOnlyBooleanProperty translatorUpdateAvailableProperty() {
return translatorUpdateAvailable.getReadOnlyProperty();
}
BooleanProperty translatorUpdateAvailableProperty() {
return translatorUpdateAvailable;
@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;
}
}

View File

@@ -0,0 +1,204 @@
package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.Translator;
import com.github.gtache.autosubtitle.VideoConverter;
import com.github.gtache.autosubtitle.VideoLoader;
import com.github.gtache.autosubtitle.gui.WorkController;
import com.github.gtache.autosubtitle.subtitle.EditableSubtitle;
import com.github.gtache.autosubtitle.subtitle.Subtitle;
import com.github.gtache.autosubtitle.subtitle.SubtitleExtractor;
import com.github.gtache.autosubtitle.subtitle.fx.ObservableSubtitleImpl;
import com.github.gtache.autosubtitle.subtitle.impl.SubtitleCollectionImpl;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleObjectProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.stage.FileChooser;
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.io.IOException;
import java.nio.file.Path;
import java.text.MessageFormat;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.util.Objects.requireNonNull;
/**
* FX implementation of {@link WorkController}
*/
@Singleton
public class FXWorkController implements WorkController {
private static final Logger logger = LogManager.getLogger(FXWorkController.class);
private static final List<String> VIDEO_EXTENSIONS = List.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");
@FXML
private TextField fileField;
@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 TextField translationField;
@FXML
private FXMediaController mediaController;
@FXML
private ResourceBundle resources;
private final FXWorkModel model;
private final SubtitleExtractor subtitleExtractor;
private final VideoConverter videoConverter;
private final VideoLoader videoLoader;
private final Translator translator;
private final FXMediaBinder binder;
@Inject
FXWorkController(final FXWorkModel model, final SubtitleExtractor subtitleExtractor, final VideoLoader videoLoader,
final VideoConverter videoConverter, final Translator translator, final FXMediaBinder binder) {
this.model = requireNonNull(model);
this.subtitleExtractor = requireNonNull(subtitleExtractor);
this.videoConverter = requireNonNull(videoConverter);
this.videoLoader = requireNonNull(videoLoader);
this.translator = requireNonNull(translator);
this.binder = requireNonNull(binder);
}
@FXML
private void initialize() {
extractButton.disableProperty().bind(model.videoProperty().isNull());
resetButton.disableProperty().bind(Bindings.isEmpty(model.subtitles()));
exportSoftButton.disableProperty().bind(Bindings.isEmpty(model.subtitles()));
exportHardButton.disableProperty().bind(Bindings.isEmpty(model.subtitles()));
subtitlesTable.setItems(model.subtitles());
startColumn.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue() == null ? null : param.getValue().start()));
endColumn.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue() == null ? null : param.getValue().end()));
textColumn.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue() == null ? "" : param.getValue().content()));
model.selectedSubtitleProperty().addListener((observable, oldValue, newValue) -> {
if (newValue != null) {
mediaController.seek(newValue.start());
}
});
binder.createBindings();
}
@FXML
private void fileButtonPressed() {
final var filePicker = new FileChooser();
filePicker.setSelectedExtensionFilter(new FileChooser.ExtensionFilter("Video", VIDEO_EXTENSIONS));
final var file = filePicker.showOpenDialog(window());
if (file != null) {
loadVideo(file.toPath());
}
}
@Override
public void extractSubtitles() {
if (model.video() != null) {
final var subtitles = subtitleExtractor.extract(model.video()).stream().sorted(Comparator.comparing(Subtitle::start).thenComparing(Subtitle::end)).toList();
final var subtitlesCopy = subtitles.stream().map(ObservableSubtitleImpl::new).toList();
model.subtitles().setAll(subtitles);
model.originalSubtitles().clear();
model.originalSubtitles().addAll(subtitlesCopy);
}
}
@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);
}
}
@FXML
private void exportSoftPressed() {
final var filePicker = new FileChooser();
final var file = filePicker.showSaveDialog(window());
if (file != null) {
final var text = model.subtitles().stream().map(Subtitle::content).collect(Collectors.joining(" "));
final var baseCollection = new SubtitleCollectionImpl(model.subtitles(), translator.getLocale(text));
final var collections = Stream.concat(Stream.of(baseCollection), model.translations().stream().map(l -> translator.translate(baseCollection, l))).toList();
try {
videoConverter.addSoftSubtitles(model.video(), collections);
} catch (final IOException e) {
logger.error("Error exporting subtitles", e);
final var alert = new Alert(Alert.AlertType.ERROR, MessageFormat.format(resources.getString("work.error.export.label"), e.getMessage()), ButtonType.OK);
alert.setTitle(resources.getString("work.error.export.title"));
alert.showAndWait();
}
}
}
@FXML
private void exportHardPressed() {
final var filePicker = new FileChooser();
final var file = filePicker.showSaveDialog(window());
if (file != null) {
try {
videoConverter.addHardSubtitles(model.video(), new SubtitleCollectionImpl(model.subtitles(), Locale.getDefault()));
} catch (final IOException e) {
logger.error("Error exporting subtitles", e);
final var alert = new Alert(Alert.AlertType.ERROR, MessageFormat.format(resources.getString("work.error.export.label"), e.getMessage()), ButtonType.OK);
alert.setTitle(resources.getString("work.error.export.title"));
alert.showAndWait();
}
}
}
@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());
}
}

View File

@@ -0,0 +1,70 @@
package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.Video;
import com.github.gtache.autosubtitle.gui.WorkModel;
import com.github.gtache.autosubtitle.subtitle.EditableSubtitle;
import javafx.beans.property.ObjectProperty;
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.List;
import java.util.Locale;
/**
* FX implementation of {@link WorkModel}
*/
@Singleton
public class FXWorkModel implements WorkModel {
private final ObjectProperty<Video> video;
private final ObservableList<EditableSubtitle> subtitles;
private final List<EditableSubtitle> originalSubtitles;
private final ObjectProperty<EditableSubtitle> subtitle;
private final ObservableList<Locale> translations;
@Inject
FXWorkModel() {
this.video = new SimpleObjectProperty<>();
this.subtitles = FXCollections.observableArrayList();
this.originalSubtitles = new ArrayList<>();
this.subtitle = new SimpleObjectProperty<>();
this.translations = FXCollections.observableArrayList();
}
@Override
public Video video() {
return video.get();
}
public ObjectProperty<Video> videoProperty() {
return video;
}
@Override
public ObservableList<EditableSubtitle> subtitles() {
return subtitles;
}
@Override
public List<EditableSubtitle> originalSubtitles() {
return List.of();
}
@Override
public EditableSubtitle selectedSubtitle() {
return subtitle.get();
}
@Override
public ObservableList<Locale> translations() {
return translations;
}
public ObjectProperty<EditableSubtitle> selectedSubtitleProperty() {
return subtitle;
}
}

View File

@@ -0,0 +1,59 @@
package com.github.gtache.autosubtitle.gui.modules.fx;
import com.github.gtache.autosubtitle.gui.fx.FXMainController;
import com.github.gtache.autosubtitle.gui.fx.FXMediaController;
import com.github.gtache.autosubtitle.gui.fx.FXSetupController;
import com.github.gtache.autosubtitle.gui.fx.FXWorkController;
import com.github.gtache.autosubtitle.modules.gui.Pause;
import com.github.gtache.autosubtitle.modules.gui.Play;
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;
/**
* Dagger module for FX
*/
@Module
public abstract class FXModule {
@Provides
@Singleton
static FXMLLoader providesFXMLLoader(final FXMainController mainController, final FXSetupController setupController,
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 {
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));
}
}

View File

@@ -3,6 +3,7 @@ package com.github.gtache.autosubtitle.subtitle.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;
@@ -23,11 +24,19 @@ public class ObservableSubtitleImpl implements EditableSubtitle {
private final ObjectProperty<Bounds> location;
public ObservableSubtitleImpl() {
content = new SimpleStringProperty("");
start = new SimpleLongProperty(0);
end = new SimpleLongProperty(0);
font = new SimpleObjectProperty<>();
location = new SimpleObjectProperty<>(new BoundsImpl(0, 0, 100, 12));
this.content = new SimpleStringProperty("");
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

View File

@@ -0,0 +1,42 @@
package com.github.gtache.autosubtitle.subtitle.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();
}
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;
}
}

View File

@@ -0,0 +1,23 @@
import com.github.gtache.autosubtitle.gui.spi.MainBundleProvider;
import com.github.gtache.autosubtitle.gui.spi.SetupBundleProvider;
import com.github.gtache.autosubtitle.gui.spi.WorkBundleProvider;
/**
* 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;
requires transitive javafx.controls;
requires transitive javafx.media;
requires transitive javafx.fxml;
requires org.apache.logging.log4j;
exports com.github.gtache.autosubtitle.gui.fx;
exports com.github.gtache.autosubtitle.gui.modules.fx;
opens com.github.gtache.autosubtitle.gui.fx to javafx.fxml;
uses MainBundleProvider;
uses SetupBundleProvider;
uses WorkBundleProvider;
}

View File

@@ -1,51 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.media.MediaView?>
<?import javafx.scene.control.Tab?>
<?import javafx.scene.control.TabPane?>
<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="1080.0" prefWidth="1920.0" xmlns="http://javafx.com/javafx/22" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.github.gtache.autosubtitle.gui.fx.FXMainController">
<children>
<GridPane hgap="10.0" layoutX="152.0" layoutY="116.0" vgap="10.0" AnchorPane.bottomAnchor="10.0" AnchorPane.leftAnchor="10.0" AnchorPane.rightAnchor="10.0" AnchorPane.topAnchor="10.0">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" />
<ColumnConstraints hgrow="SOMETIMES" />
<ColumnConstraints hgrow="SOMETIMES" />
</columnConstraints>
<rowConstraints>
<RowConstraints vgrow="SOMETIMES" />
<RowConstraints vgrow="ALWAYS" />
<RowConstraints vgrow="SOMETIMES" />
</rowConstraints>
<children>
<TextField fx:id="fileField" editable="false" GridPane.columnIndex="1" />
<Button mnemonicParsing="false" onAction="#fileButtonPressed" text="%main.button.file.label" GridPane.columnIndex="2" />
<Button fx:id="extractButton" mnemonicParsing="false" onAction="#extractPressed" text="%main.button.extract.label" GridPane.columnIndex="1" GridPane.rowIndex="2" />
<Button fx:id="exportButton" mnemonicParsing="false" onAction="#exportPressed" text="%main.button.export.label" GridPane.columnIndex="2" GridPane.rowIndex="2" />
<TableView fx:id="subtitlesTable" GridPane.rowIndex="1">
<columns>
<TableColumn fx:id="startColumn" prefWidth="50.0" text="Column X" />
<TableColumn fx:id="endColumn" prefWidth="50.0" text="Column X" />
<TableColumn fx:id="textColumn" prefWidth="75.0" text="Column X" />
</columns>
<columnResizePolicy>
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
</columnResizePolicy>
</TableView>
<Button fx:id="resetButton" mnemonicParsing="false" onAction="#resetButtonPressed" text="%main.button.reset.label" GridPane.rowIndex="2" />
<StackPane fx:id="stackPane" prefHeight="150.0" prefWidth="200.0" GridPane.columnIndex="1" GridPane.columnSpan="2147483647" GridPane.rowIndex="1">
<children>
<MediaView fx:id="videoView" fitHeight="200.0" fitWidth="200.0" />
</children>
</StackPane>
</children>
</GridPane>
</children>
</AnchorPane>
<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>
</tabs>
</TabPane>

View File

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

View File

@@ -1,49 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<GridPane hgap="10.0" vgap="10.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/22"
fx:controller="com.github.gtache.autosubtitle.gui.fx.FXSetupController">
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.MenuButton?>
<?import javafx.scene.control.MenuItem?>
<?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="SOMETIMES" />
<ColumnConstraints hgrow="SOMETIMES" />
<ColumnConstraints hgrow="SOMETIMES" />
<ColumnConstraints hgrow="ALWAYS" />
<ColumnConstraints hgrow="SOMETIMES" />
</columnConstraints>
<rowConstraints>
<RowConstraints vgrow="SOMETIMES"/>
<RowConstraints vgrow="SOMETIMES"/>
<RowConstraints vgrow="SOMETIMES"/>
<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"/>
<Label fx:id="converterStatusLabel" text="Label" GridPane.columnIndex="1"/>
<Label fx:id="extractorNameLabel" text="Label" GridPane.rowIndex="1"/>
<Label fx:id="extractorStatusLabel" text="Label" GridPane.columnIndex="1" GridPane.rowIndex="1"/>
<Label fx:id="translatorNameLabel" text="Label" GridPane.rowIndex="2"/>
<Label fx:id="translatorStatusLabel" text="Label" GridPane.columnIndex="1" GridPane.rowIndex="2"/>
<MenuButton fx:id="converterButton" mnemonicParsing="false" text="MenuButton" GridPane.columnIndex="2">
<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">
<items>
<MenuItem mnemonicParsing="false" text="Action 1"/>
<MenuItem mnemonicParsing="false" text="Action 2"/>
<MenuItem mnemonicParsing="false" text="Action 1" />
<MenuItem mnemonicParsing="false" text="Action 2" />
</items>
</MenuButton>
<MenuButton fx:id="extractorButton" mnemonicParsing="false" text="MenuButton" GridPane.columnIndex="2"
GridPane.rowIndex="1">
<MenuButton fx:id="extractorButton" mnemonicParsing="false" text="%setup.menu.label" GridPane.columnIndex="2" GridPane.rowIndex="2">
<items>
<MenuItem mnemonicParsing="false" text="Action 1"/>
<MenuItem mnemonicParsing="false" text="Action 2"/>
<MenuItem mnemonicParsing="false" text="Action 1" />
<MenuItem mnemonicParsing="false" text="Action 2" />
</items>
</MenuButton>
<MenuButton fx:id="translatorButton" mnemonicParsing="false" text="MenuButton" GridPane.columnIndex="2"
GridPane.rowIndex="2">
<MenuButton fx:id="translatorButton" mnemonicParsing="false" text="%setup.menu.label" GridPane.columnIndex="2" GridPane.rowIndex="3">
<items>
<MenuItem mnemonicParsing="false" text="Action 1"/>
<MenuItem mnemonicParsing="false" text="Action 2"/>
<MenuItem mnemonicParsing="false" text="Action 1" />
<MenuItem mnemonicParsing="false" text="Action 2" />
</items>
</MenuButton>
<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"/>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</padding>
</GridPane>

View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<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>
<children>
<TextField fx:id="fileField" editable="false" GridPane.columnIndex="1"/>
<Button mnemonicParsing="false" onAction="#fileButtonPressed" text="%work.button.file.label"
GridPane.columnIndex="2"/>
<Button fx:id="extractButton" mnemonicParsing="false" onAction="#extractPressed"
text="%work.button.extract.label" GridPane.columnIndex="1" GridPane.rowIndex="2"/>
<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" GridPane.rowIndex="1">
<columns>
<TableColumn fx:id="startColumn" prefWidth="50.0" text="%work.table.column.from.label"/>
<TableColumn fx:id="endColumn" prefWidth="50.0" text="%work.table.column.to.label"/>
<TableColumn fx:id="textColumn" prefWidth="75.0" text="%work.table.column.text.label"/>
</columns>
<columnResizePolicy>
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY"/>
</columnResizePolicy>
</TableView>
<Button fx:id="resetButton" mnemonicParsing="false" onAction="#resetButtonPressed"
text="%work.button.reset.label" GridPane.rowIndex="2"/>
<fx:include fx:id="media" source="mediaView.fxml" GridPane.columnIndex="1" GridPane.columnSpan="2147483647"
GridPane.rowIndex="1"/>
<Button mnemonicParsing="false" text="+" GridPane.halignment="RIGHT" GridPane.rowIndex="2"/>
<HBox alignment="CENTER_LEFT" spacing="10.0">
<children>
<Label text="%work.translate.label"/>
<TextField fx:id="translationField">
<tooltip>
<Tooltip text="%work.translate.tooltip"/>
</tooltip>
</TextField>
</children>
</HBox>
</children>
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
</padding>
</GridPane>