Extraction works

This commit is contained in:
Guillaume Tâche
2024-08-04 21:55:30 +02:00
parent 8002fc6719
commit 5efdaa6f63
121 changed files with 3360 additions and 400 deletions

View File

@@ -42,6 +42,11 @@
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.controlsfx</groupId>
<artifactId>controlsfx</artifactId>
<version>11.2.1</version>
</dependency>
</dependencies>
</project>

View File

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

View File

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

View File

@@ -23,6 +23,6 @@ public class FXMediaBinder {
public void createBindings() {
mediaModel.videoProperty().bindBidirectional(workModel.videoProperty());
Bindings.bindContent(workModel.subtitles(), mediaModel.subtitles());
Bindings.bindContent(mediaModel.subtitles(), workModel.subtitles());
}
}

View File

@@ -1,6 +1,7 @@
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.Pause;
import com.github.gtache.autosubtitle.modules.gui.Play;
@@ -51,14 +52,19 @@ public class FXMediaController implements MediaController {
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, @Play final Image playImage, @Pause final Image pauseImage) {
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);
}
@@ -66,7 +72,7 @@ public class FXMediaController implements MediaController {
@FXML
private void initialize() {
volumeValueLabel.textProperty().bind(Bindings.createStringBinding(() -> String.valueOf((int) (model.volume() * 100)), model.volumeProperty()));
playLabel.textProperty().bind(Bindings.createStringBinding(() -> formatTime(model.position(), model.duration()), model.positionProperty(), model.durationProperty()));
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));
@@ -97,6 +103,7 @@ public class FXMediaController implements MediaController {
final var millis = newTime.toMillis();
playSlider.setValue(millis);
model.subtitles().forEach(s -> {
//TODO optimize
if (s.start() <= millis && s.end() >= millis) {
final var label = createDraggableLabel(s);
stackPane.getChildren().add(label);
@@ -144,6 +151,7 @@ public class FXMediaController implements MediaController {
view.setFitHeight(24);
return view;
}, model.isPlayingProperty()));
binder.createBindings();
}
@FXML
@@ -190,26 +198,4 @@ public class FXMediaController implements MediaController {
label.setOnMouseEntered(mouseEvent -> label.setCursor(Cursor.HAND));
return label;
}
private static String formatTime(final long position, final long duration) {
final var positionString = formatTime(position);
final var durationString = formatTime(duration);
return positionString + "/" + durationString;
}
private static String formatTime(final long time) {
var intDuration = (int) time / 1000;
final var durationHours = intDuration / (60 * 60);
if (durationHours > 0) {
intDuration -= durationHours * 60 * 60;
}
final var durationMinutes = intDuration / 60;
final var durationSeconds = intDuration - durationHours * 60 * 60
- durationMinutes * 60;
if (durationHours > 0) {
return String.format("%d:%02d:%02d", durationHours, durationMinutes, durationSeconds);
} else {
return String.format("%02d:%02d", durationMinutes, durationSeconds);
}
}
}

View File

@@ -6,10 +6,10 @@ 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;
import java.util.List;
/**
* FX implementation of {@link com.github.gtache.autosubtitle.gui.MediaModel}
@@ -22,7 +22,7 @@ public class FXMediaModel implements MediaModel {
private final BooleanProperty isPlaying;
private final ReadOnlyLongWrapper duration;
private final LongProperty position;
private final ObservableList<EditableSubtitle> subtitles;
private final List<EditableSubtitle> subtitles;
@Inject
FXMediaModel() {
@@ -103,7 +103,7 @@ public class FXMediaModel implements MediaModel {
}
@Override
public ObservableList<EditableSubtitle> subtitles() {
public List<EditableSubtitle> subtitles() {
return subtitles;
}

View File

@@ -0,0 +1,130 @@
package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.gui.ParametersController;
import com.github.gtache.autosubtitle.subtitle.ExtractionModel;
import com.github.gtache.autosubtitle.subtitle.ExtractionModelProvider;
import com.github.gtache.autosubtitle.subtitle.OutputFormat;
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();
}
}

View File

@@ -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.FontFamily;
import com.github.gtache.autosubtitle.modules.gui.FontSize;
import com.github.gtache.autosubtitle.subtitle.ExtractionModel;
import com.github.gtache.autosubtitle.subtitle.ExtractionModelProvider;
import com.github.gtache.autosubtitle.subtitle.OutputFormat;
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;
}
}

View File

@@ -1,17 +1,20 @@
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 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.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.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem;
@@ -27,12 +30,13 @@ 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 implements SetupController {
public class FXSetupController extends AbstractFXController implements SetupController, SetupListener {
private static final Logger logger = LogManager.getLogger(FXSetupController.class);
@@ -75,6 +79,8 @@ public class FXSetupController implements SetupController {
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,
@@ -86,6 +92,8 @@ public class FXSetupController implements SetupController {
this.extractorManager = Objects.requireNonNull(extractorManager);
this.translatorManager = Objects.requireNonNull(translatorManager);
statusMap = HashMap.newHashMap(3);
setupProgressMessageMap = HashMap.newHashMap(3);
setupProgressMap = HashMap.newHashMap(3);
}
@FXML
@@ -93,6 +101,16 @@ public class FXSetupController implements SetupController {
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());
@@ -109,21 +127,17 @@ public class FXSetupController implements SetupController {
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));
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(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);
converterProgressLabel.visibleProperty().bind(converterProgress.visibleProperty());
extractorProgressLabel.visibleProperty().bind(extractorProgress.visibleProperty());
translatorProgressLabel.visibleProperty().bind(translatorProgress.visibleProperty());
}
private void bindMenu(final MenuButton button, final SetupManager setupManager) {
@@ -132,23 +146,23 @@ public class FXSetupController implements SetupController {
button.getItems().clear();
switch (newValue) {
case NOT_INSTALLED -> {
final var installItem = new MenuItem(resources.getString("setup.menu.install"));
final var installItem = new MenuItem(resources.getString("setup.menu.install.label"));
installItem.setOnAction(e -> tryInstall(setupManager));
button.getItems().add(installItem);
}
case INSTALLED -> {
final var reinstallItem = new MenuItem(resources.getString("setup.menu.reinstall"));
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"));
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"));
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"));
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"));
final var uninstallItem = new MenuItem(resources.getString("setup.menu.uninstall.label"));
uninstallItem.setOnAction(e -> tryUninstall(setupManager));
button.getItems().addAll(updateItem, reinstallItem, uninstallItem);
}
@@ -240,33 +254,67 @@ public class FXSetupController implements SetupController {
}
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()));
}
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;
}
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
public FXSetupModel model() {
return model;
}
public Window window() {
@Override
protected Window window() {
return converterNameLabel.getScene().getWindow();
}
}

View File

@@ -39,20 +39,20 @@ public class FXSetupModel implements SetupModel {
@Inject
FXSetupModel() {
this.subtitleExtractorStatus = new SimpleObjectProperty<>(SetupStatus.NOT_INSTALLED);
this.subtitleExtractorStatus = new SimpleObjectProperty<>(SetupStatus.ERRORED);
this.subtitleExtractorInstalled = new ReadOnlyBooleanWrapper(false);
this.subtitleExtractorUpdateAvailable = new ReadOnlyBooleanWrapper(false);
this.subtitleExtractorSetupProgress = new SimpleDoubleProperty(0);
this.subtitleExtractorSetupProgress = new SimpleDoubleProperty(-2);
this.subtitleExtractorSetupProgressLabel = new SimpleStringProperty("");
this.videoConverterStatus = new SimpleObjectProperty<>(SetupStatus.NOT_INSTALLED);
this.videoConverterStatus = new SimpleObjectProperty<>(SetupStatus.ERRORED);
this.videoConverterInstalled = new ReadOnlyBooleanWrapper(false);
this.videoConverterUpdateAvailable = new ReadOnlyBooleanWrapper(false);
this.videoConverterSetupProgress = new SimpleDoubleProperty(0);
this.videoConverterSetupProgress = new SimpleDoubleProperty(-2);
this.videoConverterSetupProgressLabel = new SimpleStringProperty("");
this.translatorStatus = new SimpleObjectProperty<>(SetupStatus.NOT_INSTALLED);
this.translatorStatus = new SimpleObjectProperty<>(SetupStatus.ERRORED);
this.translatorInstalled = new ReadOnlyBooleanWrapper(false);
this.translatorUpdateAvailable = new ReadOnlyBooleanWrapper(false);
this.translatorSetupProgress = new SimpleDoubleProperty(0);
this.translatorSetupProgress = new SimpleDoubleProperty(-2);
this.translatorSetupProgressLabel = new SimpleStringProperty("");
subtitleExtractorInstalled.bind(Bindings.createBooleanBinding(() -> subtitleExtractorStatus.get().isInstalled(), subtitleExtractorStatus));

View File

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

View File

@@ -1,38 +1,48 @@
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.Subtitle;
import com.github.gtache.autosubtitle.subtitle.ExtractEvent;
import com.github.gtache.autosubtitle.subtitle.ExtractException;
import com.github.gtache.autosubtitle.subtitle.SubtitleCollection;
import com.github.gtache.autosubtitle.subtitle.SubtitleExtractor;
import com.github.gtache.autosubtitle.subtitle.SubtitleExtractorListener;
import com.github.gtache.autosubtitle.subtitle.fx.ObservableSubtitleImpl;
import com.github.gtache.autosubtitle.subtitle.impl.SubtitleCollectionImpl;
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.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
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.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.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.stream.Stream;
import static java.util.Objects.requireNonNull;
@@ -41,14 +51,14 @@ import static java.util.Objects.requireNonNull;
* FX implementation of {@link WorkController}
*/
@Singleton
public class FXWorkController implements WorkController {
public class FXWorkController extends AbstractFXController implements WorkController, SubtitleExtractorListener {
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");
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;
@@ -69,56 +79,125 @@ public class FXWorkController implements WorkController {
@FXML
private TableColumn<EditableSubtitle, String> textColumn;
@FXML
private TextField translationField;
@FXML
private FXMediaController mediaController;
@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 VideoConverter videoConverter;
private final VideoLoader videoLoader;
private final Translator translator;
private final FXMediaBinder binder;
private final TimeFormatter timeFormatter;
@Inject
FXWorkController(final FXWorkModel model, final SubtitleExtractor subtitleExtractor, final VideoLoader videoLoader,
final VideoConverter videoConverter, final Translator translator, final FXMediaBinder binder) {
FXWorkController(final FXWorkModel model, final FXWorkBinder binder, final SubtitleExtractor subtitleExtractor, 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.videoConverter = requireNonNull(videoConverter);
this.videoLoader = requireNonNull(videoLoader);
this.translator = requireNonNull(translator);
this.binder = requireNonNull(binder);
this.timeFormatter = requireNonNull(timeFormatter);
}
@FXML
private void initialize() {
extractButton.disableProperty().bind(model.videoProperty().isNull());
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)));
resetButton.disableProperty().bind(Bindings.isEmpty(model.subtitles()));
exportSoftButton.disableProperty().bind(Bindings.isEmpty(model.subtitles()));
exportHardButton.disableProperty().bind(Bindings.isEmpty(model.subtitles()));
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(model.videoProperty().isNull());
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();
});
endColumn.setCellFactory(TextFieldTableCell.forTableColumn(new TimeStringConverter(timeFormatter)));
endColumn.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue() == null ? null : param.getValue().end()));
textColumn.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue() == null ? "" : param.getValue().content()));
endColumn.setOnEditCommit(e -> {
final var subtitle = e.getRowValue();
subtitle.setEnd(e.getNewValue());
subtitlesTable.refresh();
});
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.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();
filePicker.setSelectedExtensionFilter(new FileChooser.ExtensionFilter("Video", VIDEO_EXTENSIONS));
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());
@@ -128,12 +207,32 @@ public class FXWorkController implements WorkController {
@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.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) {
final var newSubtitles = newCollection.subtitles().stream().map(ObservableSubtitleImpl::new).toList();
final var subtitlesCopy = newSubtitles.stream().map(ObservableSubtitleImpl::new).toList();
model.subtitles().setAll(newSubtitles);
model.originalSubtitles().clear();
model.originalSubtitles().addAll(subtitlesCopy);
model.videoLanguageProperty().set(newCollection.language());
} 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
@@ -144,6 +243,7 @@ public class FXWorkController implements WorkController {
model.videoProperty().set(loadedVideo);
} catch (final IOException e) {
logger.error("Error loading video {}", file, e);
showErrorDialog(resources.getString("work.load.error.title"), MessageFormat.format(resources.getString("work.load.error.label"), e.getMessage()));
}
}
@@ -152,17 +252,27 @@ public class FXWorkController implements WorkController {
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();
}
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);
}
}
@@ -171,14 +281,19 @@ public class FXWorkController implements WorkController {
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();
}
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);
}
}
@@ -201,4 +316,17 @@ public class FXWorkController implements WorkController {
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());
});
}
}

View File

@@ -1,9 +1,21 @@
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.ExtractionModel;
import com.github.gtache.autosubtitle.subtitle.SubtitleCollection;
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;
@@ -11,8 +23,10 @@ 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.Locale;
import java.util.stream.Collectors;
/**
* FX implementation of {@link WorkModel}
@@ -21,18 +35,52 @@ import java.util.Locale;
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<Locale> translations;
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
@@ -40,7 +88,30 @@ public class FXWorkModel implements WorkModel {
return video.get();
}
public ObjectProperty<Video> videoProperty() {
@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;
}
@@ -54,17 +125,78 @@ public class FXWorkModel implements WorkModel {
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<Locale> translations() {
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;
}
public ObjectProperty<EditableSubtitle> selectedSubtitleProperty() {
return subtitle;
@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;
}
}

View File

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

View File

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

View File

@@ -1,11 +1,15 @@
package com.github.gtache.autosubtitle.gui.modules.fx;
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.Pause;
import com.github.gtache.autosubtitle.modules.gui.Play;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import javafx.fxml.FXMLLoader;
@@ -14,6 +18,7 @@ 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
@@ -21,9 +26,12 @@ import java.util.ResourceBundle;
@Module
public abstract class FXModule {
@Binds
abstract TimeFormatter bindsTimeFormatter(final ColonTimeFormatter formatter);
@Provides
@Singleton
static FXMLLoader providesFXMLLoader(final FXMainController mainController, final FXSetupController setupController,
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);
@@ -36,6 +44,8 @@ public abstract class FXModule {
return workController;
} else if (c == FXMediaController.class) {
return mediaController;
} else if (c == FXParametersController.class) {
return parametersController;
} else {
throw new IllegalArgumentException("Unknown controller " + c);
}
@@ -56,4 +66,10 @@ public abstract class FXModule {
static Image providesPauseImage(@Pause final byte[] pauseImage) {
return new Image(new ByteArrayInputStream(pauseImage));
}
@Provides
@Singleton
static Preferences providesPreferences() {
return Preferences.userNodeForPackage(FXParametersController.class);
}
}

View File

@@ -24,7 +24,11 @@ public class ObservableSubtitleImpl implements EditableSubtitle {
private final ObjectProperty<Bounds> location;
public ObservableSubtitleImpl() {
this.content = new SimpleStringProperty("");
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<>();

View File

@@ -11,12 +11,16 @@ module com.github.gtache.autosubtitle.fx {
requires transitive javafx.controls;
requires transitive javafx.media;
requires transitive javafx.fxml;
requires org.controlsfx.controls;
requires org.apache.logging.log4j;
exports com.github.gtache.autosubtitle.gui.fx;
exports com.github.gtache.autosubtitle.gui.modules.fx;
requires java.desktop;
requires transitive java.prefs;
exports com.github.gtache.autosubtitle.gui.fx;
opens com.github.gtache.autosubtitle.gui.fx to javafx.fxml;
exports com.github.gtache.autosubtitle.modules.gui.fx;
uses MainBundleProvider;
uses SetupBundleProvider;
uses WorkBundleProvider;

View File

@@ -2,17 +2,22 @@
<?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">
<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" />
<fx:include source="workView.fxml"/>
</content>
</Tab>
<Tab closable="false" text="%main.tab.setup.label">
<content>
<fx:include source="setupView.fxml" />
<fx:include source="setupView.fxml"/>
</content>
</Tab>
<Tab closable="false" text="%main.tab.parameters.label">
<content>
<fx:include source="parametersView.fxml"/>
</content>
</Tab>
</tabs>

View File

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

View File

@@ -3,7 +3,6 @@
<?import javafx.geometry.Insets?>
<?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?>
@@ -31,24 +30,9 @@
<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" />
</items>
</MenuButton>
<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" />
</items>
</MenuButton>
<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" />
</items>
</MenuButton>
<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" />

View File

@@ -1,9 +1,10 @@
<?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>
@@ -15,6 +16,7 @@
<RowConstraints vgrow="SOMETIMES"/>
<RowConstraints vgrow="ALWAYS"/>
<RowConstraints vgrow="SOMETIMES"/>
<RowConstraints vgrow="NEVER"/>
</rowConstraints>
<children>
<TextField fx:id="fileField" editable="false" GridPane.columnIndex="1"/>
@@ -38,11 +40,12 @@
</Button>
</children>
</HBox>
<TableView fx:id="subtitlesTable" GridPane.rowIndex="1">
<TableView fx:id="subtitlesTable" editable="true" 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"/>
<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"/>
@@ -52,15 +55,21 @@
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"/>
<Button fx:id="addSubtitleButton" mnemonicParsing="false" onAction="#addSubtitlePressed" text="+"
GridPane.halignment="RIGHT" GridPane.rowIndex="2"/>
<HBox alignment="CENTER_LEFT" spacing="10.0">
<children>
<Label text="%work.language.label"/>
<PrefixSelectionComboBox fx:id="languageCombobox"/>
<Label text="%work.translate.label"/>
<TextField fx:id="translationField">
<tooltip>
<Tooltip text="%work.translate.tooltip"/>
</tooltip>
</TextField>
<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>