Adds WhisperX, reworks UI (still needs some work), theoretically usable

This commit is contained in:
Guillaume Tâche
2024-08-17 22:05:04 +02:00
parent 7bddf53bab
commit 3fa51eb95b
204 changed files with 4787 additions and 1321 deletions

View File

@@ -13,5 +13,5 @@ public interface MainModel {
/**
* @param index The index of the tab to select
*/
void selectTab(int index);
void setSelectedTab(int index);
}

View File

@@ -19,20 +19,6 @@ public interface SetupModel {
*/
void setSubtitleExtractorStatus(SetupStatus status);
/**
* @return whether the subtitle extractor is installed
*/
default boolean isSubtitleExtractorInstalled() {
return subtitleExtractorStatus().isInstalled();
}
/**
* @return whether an update is available for the subtitle extractor
*/
default boolean isSubtitleExtractorUpdateAvailable() {
return subtitleExtractorStatus() == SetupStatus.UPDATE_AVAILABLE;
}
/**
* @return the progress of the subtitle extractor setup
*/
@@ -69,20 +55,6 @@ public interface SetupModel {
*/
void setVideoConverterStatus(SetupStatus status);
/**
* @return whether the video converter is installed
*/
default boolean isVideoConverterInstalled() {
return videoConverterStatus().isInstalled();
}
/**
* @return whether an update is available for the video converter
*/
default boolean isVideoConverterUpdateAvailable() {
return videoConverterStatus() == SetupStatus.UPDATE_AVAILABLE;
}
/**
* @return the progress of the video converter setup
*/
@@ -119,20 +91,6 @@ public interface SetupModel {
*/
void setTranslatorStatus(SetupStatus status);
/**
* @return whether the translator is installed
*/
default boolean isTranslatorInstalled() {
return translatorStatus().isInstalled();
}
/**
* @return whether an update is available for the translator
*/
default boolean isTranslatorUpdateAvailable() {
return translatorStatus() == SetupStatus.UPDATE_AVAILABLE;
}
/**
* @return the progress of the translator setup
*/

View File

@@ -0,0 +1,44 @@
package com.github.gtache.autosubtitle.gui;
import com.github.gtache.autosubtitle.Language;
import java.nio.file.Path;
/**
* Controller for the subtitles view
*/
public interface SubtitlesController {
/**
* Selects the given language for edition
*
* @param language The language
*/
void selectLanguage(final Language language);
/**
* Deletes a language
*
* @param language The language
*/
void deleteLanguage(final Language language);
/**
* Saves the subtitles to the given path
*
* @param file The output path
*/
void saveSubtitles(final Path file);
/**
* Loads a subtitles file
*
* @param file The path to the file
*/
void loadSubtitles(final Path file);
/**
* @return the model
*/
SubtitlesModel model();
}

View File

@@ -0,0 +1,146 @@
package com.github.gtache.autosubtitle.gui;
import com.github.gtache.autosubtitle.Language;
import com.github.gtache.autosubtitle.subtitle.Subtitle;
import com.github.gtache.autosubtitle.subtitle.SubtitleCollection;
import java.util.List;
import java.util.Map;
/**
* Model for the subtitles view
*
* @param <T> The type of subtitle
* @param <U> The type of subtitle collection
*/
public interface SubtitlesModel<T extends Subtitle, U extends SubtitleCollection<T>> {
/**
* @return The list of available video languages
*/
List<Language> availableVideoLanguages();
/**
* @return the video language
*/
Language videoLanguage();
/**
* Sets the video language
*
* @param language The new language
*/
void setVideoLanguage(Language language);
/**
* @return The list of available translations languages
*/
List<Language> availableTranslationsLanguage();
/**
* @return The list of selected translations languages
*/
List<Language> selectedTranslationsLanguages();
/**
* @return The currently selected language
*/
Language selectedLanguage();
/**
* @param language The new selected language
*/
void setSelectedLanguage(Language language);
/**
* @return The mapping of language to subtitles
*/
Map<Language, U> collections();
/**
* @return The currently selected collection
*/
U selectedCollection();
/**
* @param collection The new selected collection
*/
void setSelectedCollection(U collection);
/**
* @return The mapping of language to subtitles
*/
Map<Language, U> originalCollections();
/**
* @return The list of selected subtitles
*/
List<T> selectedSubtitles();
/**
* @return The currently selected subtitle
*/
T selectedSubtitle();
/**
* @param subtitle The new selected subtitle
*/
void setSelectedSubtitle(T subtitle);
/**
* @return Whether the user can load subtitles
*/
boolean canLoadSubtitles();
/**
* @param canLoadSubtitles Whether the user can load subtitles
*/
void setCanLoadSubtitles(boolean canLoadSubtitles);
/**
* @return Whether the user can add subtitles
*/
boolean canAddSubtitle();
/**
* @param canAddSubtitle Whether the user can add subtitles
*/
void setCanAddSubtitle(boolean canAddSubtitle);
/**
* @return Whether the user can reset subtitles
*/
boolean canResetSubtitles();
/**
* @param canResetSubtitles Whether the user can reset subtitles
*/
void setCanResetSubtitles(boolean canResetSubtitles);
/**
* @return Whether the user can save subtitles
*/
boolean canSaveSubtitles();
/**
* @return Whether subtitles are currently being translated
*/
boolean isTranslating();
/**
* @param translating Whether subtitles are currently being translated
*/
void setTranslating(boolean translating);
/**
* @return Whether the user can edit the table
*/
boolean canEditTable();
/**
* Sets whether the user can edit the table
*
* @param canEditTable Whether the user can edit the table
*/
void setCanEditTable(boolean canEditTable);
}

View File

@@ -18,21 +18,7 @@ public interface WorkController {
* @param file The path to the video
*/
void loadVideo(final Path file);
/**
* Saves the subtitles to the given path
*
* @param file The output path
*/
void saveSubtitles(final Path file);
/**
* Loads a subtitles file
*
* @param file The path to the file
*/
void loadSubtitles(final Path file);
/**
* @return The model
*/

View File

@@ -1,13 +1,9 @@
package com.github.gtache.autosubtitle.gui;
import com.github.gtache.autosubtitle.Language;
import com.github.gtache.autosubtitle.Video;
import com.github.gtache.autosubtitle.subtitle.EditableSubtitle;
import com.github.gtache.autosubtitle.subtitle.SubtitleCollection;
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractionModel;
import java.util.List;
/**
* Model for the main view
*/
@@ -18,6 +14,11 @@ public interface WorkModel {
*/
Video video();
/**
* @param video The new video
*/
void setVideo(Video video);
/**
* @return The current extraction model
*/
@@ -28,56 +29,6 @@ public interface WorkModel {
*/
void setExtractionModel(ExtractionModel model);
/**
* @return The current subtitle collection
*/
SubtitleCollection subtitleCollection();
/**
* @return The current list of subtitles
*/
List<EditableSubtitle> subtitles();
/**
* @return The current text
*/
String text();
/**
* @return The original extracted subtitles (used to reset)
*/
List<EditableSubtitle> originalSubtitles();
/**
* @return The currently selected subtitle
*/
EditableSubtitle selectedSubtitle();
/**
* @return The list of available video languages
*/
List<Language> availableVideoLanguages();
/**
* @return The list of available translations languages
*/
List<Language> availableTranslationsLanguage();
/**
* @return The video language
*/
Language videoLanguage();
/**
* @param language The video language
*/
void setVideoLanguage(Language language);
/**
* @return The list of selected translations
*/
List<Language> translations();
/**
* @return The current status
*/
@@ -97,4 +48,36 @@ public interface WorkModel {
* @param progress The new progress
*/
void setProgress(double progress);
/**
* @return Whether the user can extract subtitles
*/
boolean canExtract();
/**
* @return Whether the user can export subtitles
*/
boolean canExport();
/**
* @param canExport Whether the user can export subtitles
*/
void setCanExport(boolean canExport);
/**
* @return Whether the progress bar and label are currently visible
*/
boolean isProgressVisible();
/**
* @return The last extracted collection
*/
SubtitleCollection<?> extractedCollection();
/**
* Sets the last extracted collection
*
* @param collection The last extracted collection
*/
void setExtractedCollection(SubtitleCollection<?> collection);
}

View File

@@ -1,35 +1,52 @@
package com.github.gtache.autosubtitle.gui.impl;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.ResourceBundle;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.*;
/**
* Combines multiple resource bundles
*/
public class CombinedResourceBundle extends ResourceBundle {
private static final Logger logger = LogManager.getLogger(CombinedResourceBundle.class);
private final Map<String, String> resources;
private final Locale locale;
public CombinedResourceBundle(final ResourceBundle... bundles) {
this(Arrays.asList(bundles));
public CombinedResourceBundle(final ResourceBundle... resourceBundles) {
this(Arrays.asList(resourceBundles));
}
public CombinedResourceBundle(final Iterable<ResourceBundle> bundles) {
public CombinedResourceBundle(final List<ResourceBundle> resourceBundles) {
final var filteredBundles = resourceBundles.stream().filter(Objects::nonNull).toList();
if (filteredBundles.size() != resourceBundles.size()) {
logger.warn("There was one or more null bundles in the inner bundles");
}
if (filteredBundles.isEmpty()) {
throw new IllegalArgumentException("The bundle should contain at least one bundle");
}
this.resources = new HashMap<>();
bundles.forEach(rb -> rb.getKeys().asIterator().forEachRemaining(key -> resources.put(key, rb.getString(key))));
filteredBundles.forEach(r -> r.keySet().forEach(s -> resources.put(s, r.getString(s))));
this.locale = filteredBundles.getFirst().getLocale();
}
@Override
protected Object handleGetObject(final String key) {
return resources.get(key);
public Object handleGetObject(final String key) {
if (resources.containsKey(key)) {
return resources.get(key);
} else {
throw new MissingResourceException(key + " not found", "CombinedResourceBundle", key);
}
}
@Override
public Enumeration<String> getKeys() {
return Collections.enumeration(resources.keySet());
}
@Override
public Locale getLocale() {
return locale;
}
}

View File

@@ -0,0 +1,9 @@
package com.github.gtache.autosubtitle.gui.impl.spi;
import java.util.spi.ResourceBundleProvider;
/**
* Provider for SubtitlesBundle
*/
public interface SubtitlesBundleProvider extends ResourceBundleProvider {
}

View File

@@ -0,0 +1,9 @@
package com.github.gtache.autosubtitle.gui.impl.spi;
import java.util.spi.AbstractResourceBundleProvider;
/**
* Implementation of {@link SubtitlesBundleProvider}
*/
public class SubtitlesBundleProviderImpl extends AbstractResourceBundleProvider implements SubtitlesBundleProvider {
}

View File

@@ -23,6 +23,7 @@ public final class GuiCoreModule {
ResourceBundle.getBundle("com.github.gtache.autosubtitle.gui.impl.SetupBundle"),
ResourceBundle.getBundle("com.github.gtache.autosubtitle.gui.impl.WorkBundle"),
ResourceBundle.getBundle("com.github.gtache.autosubtitle.gui.impl.ParametersBundle"),
ResourceBundle.getBundle("com.github.gtache.autosubtitle.gui.impl.SubtitlesBundle"),
ResourceBundle.getBundle("com.github.gtache.autosubtitle.gui.impl.MediaBundle"));
}

View File

@@ -13,6 +13,7 @@ import com.github.gtache.autosubtitle.gui.impl.spi.WorkBundleProviderImpl;
module com.github.gtache.autosubtitle.gui.core {
requires transitive com.github.gtache.autosubtitle.gui.api;
requires transitive com.github.gtache.autosubtitle.core;
requires org.apache.logging.log4j;
exports com.github.gtache.autosubtitle.gui.impl;
exports com.github.gtache.autosubtitle.gui.impl.spi;
exports com.github.gtache.autosubtitle.modules.gui.impl;

View File

@@ -0,0 +1,14 @@
subtitles.button.load.label=Load subtitles...
subtitles.button.reset.label=Reset subtitles
subtitles.button.subtitles.save.label=Save subtitles...
subtitles.export.error.label=Error during the export : {0}
subtitles.export.error.title=Error exporting
subtitles.language.label=Video language
subtitles.load.error.label=Error loading subtitles : {0}
subtitles.load.error.title=Error loading
subtitles.save.error.label=Error saving subtitles : {0}
subtitles.save.error.title=Error saving
subtitles.table.column.from.label=From
subtitles.table.column.text.label=Text
subtitles.table.column.to.label=To
subtitles.translate.label=Automatic translations

View File

@@ -0,0 +1,14 @@
subtitles.button.load.label=Charger des sous-titres...
subtitles.button.reset.label=R\u00E9initialiser les sous-titres
subtitles.button.subtitles.save.label=Sauvegarder les sous-titres...
subtitles.export.error.label=Erreur durant l''export : {0}
subtitles.export.error.title=Erreur d'export
subtitles.language.label=Langage de la vid\u00E9o
subtitles.load.error.label=Erreur de chargement des sous-titres : {0}
subtitles.load.error.title=Erreur de chargement
subtitles.save.error.label=Erreur de sauvegarde des sous-titres : {0}
subtitles.save.error.title=Erreur lors de la sauvegarde
subtitles.table.column.from.label=De
subtitles.table.column.text.label=Texte
subtitles.table.column.to.label=\u00C0
subtitles.translate.label=Traductions

View File

@@ -4,27 +4,13 @@ work.button.export.soft.label=Export video...
work.button.export.soft.tooltip=Adds the subtitles to the video. This allows a video to have multiple subtitles and to enable them at will.
work.button.extract.label=Extract subtitles
work.button.file.label=Open video...
work.button.load.label=Load subtitles...
work.button.reset.label=Reset subtitles
work.button.subtitles.save.label=Save subtitles...
work.export.error.label=Error during the export : {0}
work.export.error.title=Error exporting
work.extract.error.label=Error extracting subtitles : {0}
work.extract.error.title=Error extracting
work.language.label=Video language
work.load.subtitles.error.label=Error loading subtitles : {0}
work.load.subtitles.error.title=Error loading
work.load.video.error.label=Error loading video : {0}
work.load.video.error.title=Error loading
work.save.subtitles.error.label=Error saving subtitles : {0}
work.save.subtitles.error.title=Error saving
work.save.subtitles.missing.converter.label=No converter found for {0}
work.save.subtitles.missing.converter.title=No converter found
work.status.exporting.label=Exporting...
work.status.extracting.label=Extracting...
work.status.idle.label=Idle
work.status.translating.label=Translating...
work.table.column.from.label=From
work.table.column.text.label=Text
work.table.column.to.label=To
work.translate.label=Automatic translations
work.status.translating.label=Translating...

View File

@@ -4,25 +4,13 @@ work.button.export.soft.label=Exporter la vid\u00E9o...
work.button.export.soft.tooltip=Ajoute les sous-titres \u00E0 la vid\u00E9o. Cela permet d'avoir plusieurs pistes de sous-titres dans une m\u00EAme vid\u00E9o et de les activer comme d\u00E9sir\u00E9.
work.button.extract.label=Extraire les sous-titres
work.button.file.label=Ouvrir une vid\u00E9o...
work.button.load.label=Charger des sous-titres...
work.button.reset.label=R\u00E9initialiser les sous-titres
work.button.subtitles.save.label=Sauvegarder les sous-titres...
work.export.error.label=Erreur durant l''export : {0}
work.export.error.title=Erreur d'export
work.extract.error.label=Erreur durant l''extraction des sous-titres : {0}
work.extract.error.title=Erreur d'extraction
work.language.label=Language de la vid\u00E9o
work.load.subtitles.error.label=Erreur de chargement des sous-titres : {0}
work.load.subtitles.error.title=Erreur de chargement
work.load.video.error.label=Erreur lors du chargement de la vid\u00E9o : {0}
work.load.video.error.title=Erreur de chargement
work.save.subtitles.missing.converter.label=Aucun convertisseur trouv\u00E9 pour {0}
work.save.subtitles.missing.converter.title=Aucun convertisseur trouv\u00E9
work.status.exporting.label=Exportation en cours...
work.status.extracting.label=Extraction en cours...
work.status.idle.label=Idle
work.status.translating.label=Traduction en cours...
work.table.column.from.label=De
work.table.column.text.label=Texte
work.table.column.to.label=\u00C0
work.translate.label=Traductions automatiques

View File

@@ -0,0 +1,54 @@
package com.github.gtache.autosubtitle.gui.impl;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.Collections;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class TestCombinedResourceBundle {
private static final ResourceBundle BUNDLE = new CombinedResourceBundle(
ResourceBundle.getBundle("com.github.gtache.autosubtitle.gui.impl.MultiBundle", Locale.FRENCH),
ResourceBundle.getBundle("com.github.gtache.autosubtitle.gui.impl.MultiBundleTwo", Locale.FRENCH));
@Test
void testIllegal() {
assertThrows(IllegalArgumentException.class, CombinedResourceBundle::new);
assertThrows(IllegalArgumentException.class, () -> new CombinedResourceBundle(null, null));
}
@Test
void testWorks() {
assertEquals("deux", BUNDLE.getString("a"));
assertEquals("deux", BUNDLE.getString("b"));
assertEquals("trois", BUNDLE.getString("c"));
assertEquals("un", BUNDLE.getString("d"));
assertEquals(Arrays.asList("a", "b", "c", "d"), Collections.list(BUNDLE.getKeys()));
}
@Test
void testNotFound() {
assertThrows(MissingResourceException.class, () -> BUNDLE.getString("e"));
}
@Test
void testLocale() {
final var bundle = mock(ResourceBundle.class);
when(bundle.keySet()).thenReturn(Set.of());
when(bundle.getString(anyString())).thenReturn("");
final var locale = mock(Locale.class);
when(bundle.getLocale()).thenReturn(locale);
final var combined = new CombinedResourceBundle(bundle);
assertEquals(locale, combined.getLocale());
}
}

View File

@@ -0,0 +1,34 @@
package com.github.gtache.autosubtitle.modules.gui.impl;
import com.github.gtache.autosubtitle.gui.impl.CombinedResourceBundle;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class TestGuiCoreModule {
@Test
void testBundle() {
assertInstanceOf(CombinedResourceBundle.class, GuiCoreModule.providesBundle());
}
@Test
void testPlayImage() {
assertTrue(GuiCoreModule.providesPlayImage().length > 0);
}
@Test
void testPauseImage() {
assertTrue(GuiCoreModule.providesPauseImage().length > 0);
}
@Test
void testFontFamily() {
assertEquals("Arial", GuiCoreModule.providesFontFamily());
}
@Test
void testFontSize() {
assertEquals(12, GuiCoreModule.providesFontSize());
}
}

View File

@@ -0,0 +1,3 @@
a=one
b=two
c=three

View File

@@ -0,0 +1,3 @@
a=eins
b=zwei
c=drei

View File

@@ -0,0 +1,3 @@
a=un
b=deux
c=trois

View File

@@ -13,7 +13,8 @@
<properties>
<controlsfx.version>11.2.1</controlsfx.version>
<javafx.version>22.0.1</javafx.version>
<javafx.version>22.0.2</javafx.version>
<testfx.version>4.0.18</testfx.version>
</properties>
<dependencies>
@@ -44,6 +45,23 @@
<artifactId>controlsfx</artifactId>
<version>${controlsfx.version}</version>
</dependency>
<dependency>
<groupId>org.testfx</groupId>
<artifactId>testfx-core</artifactId>
<version>${testfx.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testfx</groupId>
<artifactId>testfx-junit5</artifactId>
<version>${testfx.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -27,18 +27,19 @@ public class ColonTimeFormatter implements TimeFormatter {
public String format(final long millis) {
final var secondsInMinute = 60;
final var secondsInHour = secondsInMinute * 60;
var intDuration = (int) millis / 1000;
final var durationHours = intDuration / secondsInHour;
var secondsDuration = millis / 1000L;
final var durationHours = secondsDuration / secondsInHour;
if (durationHours > 0) {
intDuration -= durationHours * secondsInHour;
secondsDuration -= durationHours * secondsInHour;
}
final var durationMinutes = intDuration / secondsInMinute;
final var durationSeconds = intDuration - durationHours * secondsInHour
- durationMinutes * secondsInMinute;
final var durationMinutes = secondsDuration / secondsInMinute;
secondsDuration -= durationMinutes * secondsInMinute;
final var durationSeconds = secondsDuration;
final var durationMillis = millis % 1000L;
if (durationHours > 0) {
return String.format("%d:%02d:%02d", durationHours, durationMinutes, durationSeconds);
return "%d:%02d:%02d.%03d".formatted(durationHours, durationMinutes, durationSeconds, durationMillis);
} else {
return String.format("%02d:%02d", durationMinutes, durationSeconds);
return "%02d:%02d.%03d".formatted(durationMinutes, durationSeconds, durationMillis);
}
}
@@ -48,14 +49,20 @@ public class ColonTimeFormatter implements TimeFormatter {
final var secondsInMinute = 60;
final var secondsInHour = secondsInMinute * 60;
return switch (split.length) {
case 1 -> toLong(split[0]) * 1000;
case 2 -> (toLong(split[0]) * secondsInMinute + toLong(split[1])) * 1000;
case 3 -> (toLong(split[0]) * secondsInHour + toLong(split[1]) * secondsInMinute + toLong(split[2])) * 1000;
case 1 -> parseSecondsMillis(split[0]);
case 2 -> toLong(split[0]) * secondsInMinute * 1000 + parseSecondsMillis(split[1]);
case 3 ->
(toLong(split[0]) * secondsInHour + toLong(split[1]) * secondsInMinute) * 1000 + parseSecondsMillis(split[2]);
default -> 0;
};
}
private long toLong(final String time) {
private static long parseSecondsMillis(final String time) {
final var split = time.split("\\.");
return toLong(split[0]) * 1000 + (split.length > 1 ? toLong(split[1]) : 0);
}
private static long toLong(final String time) {
if (time.startsWith("0")) {
return Long.parseLong(time.substring(1));
} else {

View File

@@ -0,0 +1,13 @@
package com.github.gtache.autosubtitle.gui.fx;
/**
* Binds multiple models together
*/
@FunctionalInterface
public interface FXBinder {
/**
* Creates the bindings between the models
*/
void createBindings();
}

View File

@@ -3,6 +3,7 @@ package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.gui.MainController;
import javafx.fxml.FXML;
import javafx.scene.control.TabPane;
import javafx.stage.Window;
import javax.inject.Inject;
import javax.inject.Singleton;
@@ -12,7 +13,7 @@ import java.util.Objects;
* FX implementation of {@link MainController}
*/
@Singleton
public class FXMainController implements MainController {
public class FXMainController extends AbstractFXController implements MainController {
@FXML
private TabPane tabPane;
@@ -25,18 +26,23 @@ public class FXMainController implements MainController {
}
@FXML
private void initialize() {
tabPane.getSelectionModel().selectedIndexProperty().addListener((observable, oldValue, newValue) -> model.selectTab(newValue.intValue()));
void initialize() {
tabPane.getSelectionModel().selectedIndexProperty().addListener((observable, oldValue, newValue) -> model.setSelectedTab(newValue.intValue()));
model.selectedTabProperty().addListener((observable, oldValue, newValue) -> tabPane.getSelectionModel().select(newValue.intValue()));
}
@Override
public void selectTab(final int index) {
model.selectTab(index);
model.setSelectedTab(index);
}
@Override
public FXMainModel model() {
return model;
}
@Override
protected Window window() {
return tabPane.getScene().getWindow();
}
}

View File

@@ -26,7 +26,7 @@ public class FXMainModel implements MainModel {
}
@Override
public void selectTab(final int index) {
public void setSelectedTab(final int index) {
selectedTab.set(index);
}

View File

@@ -10,7 +10,7 @@ import java.util.Objects;
* Binds the media model
*/
@Singleton
public class FXMediaBinder {
public class FXMediaBinder implements FXBinder {
private final FXWorkModel workModel;
private final FXMediaModel mediaModel;
@@ -21,6 +21,7 @@ public class FXMediaBinder {
this.mediaModel = Objects.requireNonNull(mediaModel);
}
@Override
public void createBindings() {
mediaModel.videoProperty().bindBidirectional(workModel.videoProperty());
Bindings.bindContent(mediaModel.subtitles(), workModel.subtitles());

View File

@@ -5,12 +5,10 @@ import com.github.gtache.autosubtitle.gui.MediaController;
import com.github.gtache.autosubtitle.gui.TimeFormatter;
import com.github.gtache.autosubtitle.modules.gui.impl.Pause;
import com.github.gtache.autosubtitle.modules.gui.impl.Play;
import com.github.gtache.autosubtitle.subtitle.EditableSubtitle;
import com.github.gtache.autosubtitle.subtitle.Subtitle;
import com.github.gtache.autosubtitle.subtitle.gui.fx.SubtitleLabel;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.collections.ListChangeListener;
import javafx.fxml.FXML;
import javafx.geometry.Insets;
import javafx.scene.Cursor;
@@ -34,9 +32,7 @@ import org.apache.logging.log4j.Logger;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import static java.util.Objects.requireNonNull;
@@ -69,8 +65,6 @@ public class FXMediaController implements MediaController {
private final Image playImage;
private final Image pauseImage;
private final List<Long> startTimes;
private boolean wasPlaying;
@Inject
@@ -81,11 +75,10 @@ public class FXMediaController implements MediaController {
this.timeFormatter = requireNonNull(timeFormatter);
this.playImage = requireNonNull(playImage);
this.pauseImage = requireNonNull(pauseImage);
this.startTimes = new ArrayList<>();
}
@FXML
private void initialize() {
void initialize() {
volumeValueLabel.textProperty().bind(Bindings.createStringBinding(() -> String.valueOf((int) (model.volume() * 100)), model.volumeProperty()));
playLabel.textProperty().bind(Bindings.createStringBinding(() -> timeFormatter.format(model.position(), model.duration()), model.positionProperty(), model.durationProperty()));
model.positionProperty().bindBidirectional(playSlider.valueProperty());
@@ -115,13 +108,10 @@ public class FXMediaController implements MediaController {
loadFileVideo(file.path());
} else {
logger.error("Unsupported video type : {}", newValue);
Platform.runLater(() -> model.setVideo(null));
}
});
model.subtitles().addListener((ListChangeListener<EditableSubtitle>) c -> {
startTimes.clear();
model.subtitles().stream().mapToLong(Subtitle::start).forEach(startTimes::add);
});
bindPlayButton();
binder.createBindings();
}
@@ -130,8 +120,8 @@ public class FXMediaController implements MediaController {
final var media = new Media(fileVideoPath.toUri().toString());
final var player = new MediaPlayer(media);
player.statusProperty().addListener((observable12, oldValue1, newValue1) ->
logger.info("New status: {}", newValue1));
player.currentTimeProperty().addListener((ignored, oldTime, newTime) -> currentTimeChanged(oldTime.toMillis(), newTime.toMillis()));
logger.debug("New player status: {}", newValue1));
player.currentTimeProperty().addListener((ignored, oldTime, newTime) -> currentTimeChanged(newTime.toMillis()));
playSlider.setOnMousePressed(e -> {
wasPlaying = model.isPlaying();
model.setIsPlaying(false);
@@ -155,33 +145,37 @@ public class FXMediaController implements MediaController {
playSlider.setMax(model.duration());
playSlider.setValue(0L);
videoView.setMediaPlayer(player);
CompletableFuture.runAsync(() -> {
try {
Thread.sleep(3000L);
} catch (final InterruptedException ignored) {
Thread.currentThread().interrupt();
}
Platform.runLater(() -> {
final var status = player.getStatus();
if (status == null || status == MediaPlayer.Status.UNKNOWN) {
logger.warn("Reloading video {} because player state is unknown or null", fileVideoPath);
loadFileVideo(fileVideoPath);
}
});
});
}
private void currentTimeChanged(final double oldMillis, final double millis) {
private void currentTimeChanged(final double millis) {
final var longMillis = (long) millis;
playSlider.setValue(millis);
final var subtitleLabels = stackPane.getChildren().stream().filter(SubtitleLabel.class::isInstance).map(SubtitleLabel.class::cast).toList();
subtitleLabels.stream().filter(s -> !s.subtitle().isShowing((long) millis)).forEach(sl -> stackPane.getChildren().remove(sl));
final var containedSubtitles = subtitleLabels.stream().map(SubtitleLabel::subtitle).filter(s -> s.isShowing((long) millis)).collect(Collectors.toSet());
subtitleLabels.stream().filter(s -> !s.subtitle().isShowing(longMillis)).forEach(sl -> stackPane.getChildren().remove(sl));
final var containedSubtitles = subtitleLabels.stream().map(SubtitleLabel::subtitle).filter(s -> s.isShowing(longMillis)).collect(Collectors.toSet());
//TODO optimize?
model.subtitles().forEach(s -> {
if (!containedSubtitles.contains(s)) {
logger.info("Adding label {} at {}", s, millis);
if (!containedSubtitles.contains(s) && s.isShowing(longMillis)) {
final var label = createDraggableLabel(s);
stackPane.getChildren().add(label);
}
});
}
private void currentTimeChangedOptimized(final double oldMillis, final double millis) {
final var forward = oldMillis <= millis;
var index = Collections.binarySearch(startTimes, (long) millis);
if (index < 0) {
index = forward ? -(index + 1) : -(index + 2);
}
//TODO
}
private void bindPlayButton() {
playButton.disableProperty().bind(model.videoProperty().isNull());
playButton.graphicProperty().bind(Bindings.createObjectBinding(() -> {
@@ -217,6 +211,7 @@ public class FXMediaController implements MediaController {
public void seek(final long position) {
if (videoView.getMediaPlayer() != null) {
videoView.getMediaPlayer().seek(Duration.millis(position));
currentTimeChanged(position);
}
}

View File

@@ -102,12 +102,12 @@ public class FXMediaModel implements MediaModel {
this.position.set(position);
}
LongProperty positionProperty() {
return position;
}
@Override
public ObservableList<EditableSubtitle> subtitles() {
return subtitles;
}
LongProperty positionProperty() {
return position;
}
}

View File

@@ -50,7 +50,7 @@ public class FXParametersController extends AbstractFXController implements Para
}
@FXML
private void initialize() {
void initialize() {
extractionModelCombobox.setItems(model.availableExtractionModels());
extractionModelCombobox.valueProperty().bindBidirectional(model.extractionModelProperty());

View File

@@ -97,7 +97,7 @@ public class FXSetupController extends AbstractFXController implements SetupCont
}
@FXML
private void initialize() {
void initialize() {
statusMap.put(converterManager, model.videoConverterStatusProperty());
statusMap.put(extractorManager, model.subtitleExtractorStatusProperty());
statusMap.put(translatorManager, model.translatorStatusProperty());

View File

@@ -2,11 +2,8 @@ package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.gui.SetupModel;
import com.github.gtache.autosubtitle.setup.SetupStatus;
import javafx.beans.binding.Bindings;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
@@ -22,45 +19,26 @@ import javax.inject.Singleton;
public class FXSetupModel implements SetupModel {
private final ObjectProperty<SetupStatus> subtitleExtractorStatus;
private final ReadOnlyBooleanWrapper subtitleExtractorInstalled;
private final ReadOnlyBooleanWrapper subtitleExtractorUpdateAvailable;
private final DoubleProperty subtitleExtractorSetupProgress;
private final StringProperty subtitleExtractorSetupProgressLabel;
private final ObjectProperty<SetupStatus> videoConverterStatus;
private final ReadOnlyBooleanWrapper videoConverterInstalled;
private final ReadOnlyBooleanWrapper videoConverterUpdateAvailable;
private final DoubleProperty videoConverterSetupProgress;
private final StringProperty videoConverterSetupProgressLabel;
private final ObjectProperty<SetupStatus> translatorStatus;
private final ReadOnlyBooleanWrapper translatorInstalled;
private final ReadOnlyBooleanWrapper translatorUpdateAvailable;
private final DoubleProperty translatorSetupProgress;
private final StringProperty translatorSetupProgressLabel;
@Inject
FXSetupModel() {
this.subtitleExtractorStatus = new SimpleObjectProperty<>(SetupStatus.ERRORED);
this.subtitleExtractorInstalled = new ReadOnlyBooleanWrapper(false);
this.subtitleExtractorUpdateAvailable = new ReadOnlyBooleanWrapper(false);
this.subtitleExtractorSetupProgress = new SimpleDoubleProperty(-2);
this.subtitleExtractorSetupProgressLabel = new SimpleStringProperty("");
this.videoConverterStatus = new SimpleObjectProperty<>(SetupStatus.ERRORED);
this.videoConverterInstalled = new ReadOnlyBooleanWrapper(false);
this.videoConverterUpdateAvailable = new ReadOnlyBooleanWrapper(false);
this.videoConverterSetupProgress = new SimpleDoubleProperty(-2);
this.videoConverterSetupProgressLabel = new SimpleStringProperty("");
this.translatorStatus = new SimpleObjectProperty<>(SetupStatus.ERRORED);
this.translatorInstalled = new ReadOnlyBooleanWrapper(false);
this.translatorUpdateAvailable = new ReadOnlyBooleanWrapper(false);
this.translatorSetupProgress = new SimpleDoubleProperty(-2);
this.translatorSetupProgressLabel = new SimpleStringProperty("");
subtitleExtractorInstalled.bind(Bindings.createBooleanBinding(() -> subtitleExtractorStatus.get().isInstalled(), subtitleExtractorStatus));
videoConverterInstalled.bind(Bindings.createBooleanBinding(() -> videoConverterStatus.get().isInstalled(), videoConverterStatus));
translatorInstalled.bind(Bindings.createBooleanBinding(() -> translatorStatus.get().isInstalled(), translatorStatus));
subtitleExtractorUpdateAvailable.bind(Bindings.createBooleanBinding(() -> subtitleExtractorStatus.get() == SetupStatus.UPDATE_AVAILABLE, subtitleExtractorStatus));
videoConverterUpdateAvailable.bind(Bindings.createBooleanBinding(() -> videoConverterStatus.get() == SetupStatus.UPDATE_AVAILABLE, videoConverterStatus));
translatorUpdateAvailable.bind(Bindings.createBooleanBinding(() -> translatorStatus.get() == SetupStatus.UPDATE_AVAILABLE, translatorStatus));
}
@Override
@@ -77,20 +55,6 @@ public class FXSetupModel implements SetupModel {
return subtitleExtractorStatus;
}
@Override
public boolean isSubtitleExtractorInstalled() {
return subtitleExtractorInstalled.get();
}
ReadOnlyBooleanProperty subtitleExtractorInstalledProperty() {
return subtitleExtractorInstalled.getReadOnlyProperty();
}
@Override
public boolean isSubtitleExtractorUpdateAvailable() {
return subtitleExtractorUpdateAvailable.get();
}
@Override
public double subtitleExtractorSetupProgress() {
return subtitleExtractorSetupProgress.get();
@@ -133,28 +97,6 @@ public class FXSetupModel implements SetupModel {
return videoConverterStatus;
}
ReadOnlyBooleanProperty subtitleExtractorUpdateAvailableProperty() {
return subtitleExtractorUpdateAvailable.getReadOnlyProperty();
}
@Override
public boolean isVideoConverterInstalled() {
return videoConverterInstalled.get();
}
ReadOnlyBooleanProperty videoConverterInstalledProperty() {
return videoConverterInstalled.getReadOnlyProperty();
}
@Override
public boolean isVideoConverterUpdateAvailable() {
return videoConverterUpdateAvailable.get();
}
ReadOnlyBooleanProperty videoConverterUpdateAvailableProperty() {
return videoConverterUpdateAvailable.getReadOnlyProperty();
}
@Override
public double videoConverterSetupProgress() {
return videoConverterSetupProgress.get();
@@ -197,24 +139,6 @@ public class FXSetupModel implements SetupModel {
return translatorStatus;
}
@Override
public boolean isTranslatorInstalled() {
return translatorInstalled.get();
}
ReadOnlyBooleanProperty translatorInstalledProperty() {
return translatorInstalled.getReadOnlyProperty();
}
@Override
public boolean isTranslatorUpdateAvailable() {
return translatorUpdateAvailable.get();
}
ReadOnlyBooleanProperty translatorUpdateAvailableProperty() {
return translatorUpdateAvailable.getReadOnlyProperty();
}
@Override
public double translatorSetupProgress() {
return translatorSetupProgress.get();

View File

@@ -0,0 +1,55 @@
package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.Language;
import com.github.gtache.autosubtitle.gui.WorkStatus;
import com.github.gtache.autosubtitle.subtitle.gui.fx.ObservableSubtitleCollectionImpl;
import javafx.beans.binding.Bindings;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Objects;
/**
* Binds the subtitles model
*/
@Singleton
public class FXSubtitlesBinder implements FXBinder {
private final FXWorkModel workModel;
private final FXSubtitlesModel subtitlesModel;
@Inject
FXSubtitlesBinder(final FXWorkModel workModel, final FXSubtitlesModel subtitlesModel) {
this.workModel = Objects.requireNonNull(workModel);
this.subtitlesModel = Objects.requireNonNull(subtitlesModel);
}
@Override
public void createBindings() {
subtitlesModel.canLoadSubtitlesProperty().bind(workModel.videoProperty().isNotNull().and(workModel.statusProperty().isEqualTo(WorkStatus.IDLE)));
subtitlesModel.canResetSubtitlesProperty().bind(workModel.videoProperty().isNotNull().and(workModel.statusProperty().isEqualTo(WorkStatus.IDLE)));
subtitlesModel.canAddSubtitleProperty().bind(workModel.videoProperty().isNotNull().and(workModel.statusProperty().isEqualTo(WorkStatus.IDLE)).and(subtitlesModel.videoLanguageProperty().isNotEqualTo(Language.AUTO)));
subtitlesModel.canEditTableProperty().bind(workModel.videoProperty().isNotNull().and(workModel.statusProperty().isEqualTo(WorkStatus.IDLE)).and(subtitlesModel.videoLanguageProperty().isNotEqualTo(Language.AUTO)));
workModel.selectedSubtitleProperty().bind(subtitlesModel.selectedSubtitleProperty());
workModel.canExportProperty().bind(Bindings.isNotEmpty(subtitlesModel.collections()).and(workModel.statusProperty().isEqualTo(WorkStatus.IDLE)));
workModel.videoLanguageProperty().bind(subtitlesModel.videoLanguageProperty());
subtitlesModel.translatingProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) {
workModel.statusProperty().set(WorkStatus.TRANSLATING);
} else {
workModel.statusProperty().set(WorkStatus.IDLE);
}
});
workModel.extractedCollectionProperty().addListener((observable, oldValue, newValue) -> {
if (newValue != null) {
subtitlesModel.collections().put(newValue.language(), new ObservableSubtitleCollectionImpl(newValue));
}
});
Bindings.bindContent(workModel.collections(), subtitlesModel.collections());
Bindings.bindContent(workModel.subtitles(), subtitlesModel.selectedSubtitles());
}
}

View File

@@ -0,0 +1,322 @@
package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.Language;
import com.github.gtache.autosubtitle.Translator;
import com.github.gtache.autosubtitle.gui.SubtitlesController;
import com.github.gtache.autosubtitle.gui.TimeFormatter;
import com.github.gtache.autosubtitle.subtitle.SubtitleCollection;
import com.github.gtache.autosubtitle.subtitle.SubtitleImporterExporter;
import com.github.gtache.autosubtitle.subtitle.converter.ParseException;
import com.github.gtache.autosubtitle.subtitle.gui.fx.ObservableSubtitleCollectionImpl;
import com.github.gtache.autosubtitle.subtitle.gui.fx.ObservableSubtitleImpl;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.MapChangeListener;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.input.KeyCode;
import javafx.stage.FileChooser;
import javafx.stage.Window;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.controlsfx.control.PrefixSelectionComboBox;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.IOException;
import java.nio.file.Path;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.ResourceBundle;
import java.util.concurrent.CompletableFuture;
import static java.util.Objects.requireNonNull;
/**
* FX implementation of {@link SubtitlesController}
*/
@Singleton
public class FXSubtitlesController extends AbstractFXController implements SubtitlesController {
private static final Logger logger = LogManager.getLogger(FXSubtitlesController.class);
private static final String ARCHIVE = "Archive";
private static final String ALL_SUPPORTED = "All supported";
@FXML
private ResourceBundle resources;
@FXML
private Button loadButton;
@FXML
private Button resetButton;
@FXML
private Button saveButton;
@FXML
private Button addButton;
@FXML
private PrefixSelectionComboBox<Language> languageCombobox;
@FXML
private ComboBox<Language> translationsCombobox;
@FXML
private TabPane tabPane;
@FXML
private Tab mainSubtitlesTab;
@FXML
private TableView<ObservableSubtitleImpl> subtitlesTable;
@FXML
private TableColumn<ObservableSubtitleImpl, Long> startColumn;
@FXML
private TableColumn<ObservableSubtitleImpl, Long> endColumn;
@FXML
private TableColumn<ObservableSubtitleImpl, String> textColumn;
private final FXSubtitlesModel model;
private final FXSubtitlesBinder binder;
private final SubtitleImporterExporter<?> importerExporter;
private final TimeFormatter timeFormatter;
private final List<String> subtitleExtensions;
private final Translator<?> translator;
@Inject
FXSubtitlesController(final FXSubtitlesModel model, final FXSubtitlesBinder binder, final SubtitleImporterExporter importerExporter, final TimeFormatter timeFormatter,
final Translator translator) {
this.model = requireNonNull(model);
this.binder = requireNonNull(binder);
this.importerExporter = requireNonNull(importerExporter);
this.timeFormatter = requireNonNull(timeFormatter);
this.subtitleExtensions = importerExporter.supportedSingleFileExtensions().stream().map(c -> "*." + c).sorted().toList();
this.translator = requireNonNull(translator);
}
@FXML
void initialize() {
addButton.disableProperty().bind(model.canAddSubtitleProperty().not());
loadButton.disableProperty().bind(model.canLoadSubtitlesProperty().not());
resetButton.disableProperty().bind(model.canResetSubtitlesProperty().not());
saveButton.disableProperty().bind(model.canSaveSubtitlesProperty().not());
// Can't bind because tab calls updateDisabled which sets disableProperty
model.canEditTableProperty().addListener((observable, oldValue, newValue) -> subtitlesTable.setDisable(!newValue));
bindComboboxes();
bindTable();
mainSubtitlesTab.textProperty().bind(Bindings.createStringBinding(() -> model.videoLanguage().iso2(), model.videoLanguageProperty()));
model.collections().addListener((MapChangeListener<Language, ObservableSubtitleCollectionImpl>) change -> {
manageTabs();
});
tabPane.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
if (newValue != null) {
model.setSelectedCollection(model.collections().get(Language.getLanguage(newValue.getText())));
if (oldValue != null) {
oldValue.setContent(null);
}
newValue.setContent(subtitlesTable);
}
});
model.selectedLanguageProperty().addListener((observable, oldValue, newValue) -> {
if (newValue != null) {
tabPane.getTabs().stream().filter(t -> Language.getLanguage(t.getText()) == newValue)
.findFirst().ifPresent(tab -> tabPane.getSelectionModel().select(tab));
}
});
translationsCombobox.valueProperty().addListener((observable, oldValue, newValue) -> {
if (newValue != null && !model.collections().containsKey(newValue)) {
model.setTranslating(true);
CompletableFuture.supplyAsync(() -> translator.translate(model.collections().get(model.videoLanguage()), newValue))
.whenCompleteAsync((r, t) -> {
if (t == null) {
loadCollection(r);
model.setSelectedCollection(model.collections().get(newValue));
} else {
logger.error("Error while translating to {}", newValue, t);
}
model.setTranslating(false);
}, Platform::runLater);
}
});
binder.createBindings();
}
private void bindTable() {
subtitlesTable.setItems(model.selectedSubtitles());
subtitlesTable.setOnKeyPressed(e -> {
if (e.getCode().isLetterKey() || e.getCode().isDigitKey()) {
editFocusedCell();
} else if (e.getCode() == KeyCode.RIGHT ||
e.getCode() == KeyCode.TAB) {
subtitlesTable.getSelectionModel().selectNext();
e.consume();
} else if (e.getCode() == KeyCode.LEFT) {
subtitlesTable.getSelectionModel().selectPrevious();
e.consume();
} else if (e.getCode() == KeyCode.DELETE) {
deleteSelectedSubtitles();
e.consume();
}
});
startColumn.setCellFactory(TextFieldTableCell.forTableColumn(new TimeStringConverter(timeFormatter)));
startColumn.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue() == null ? null : param.getValue().start()));
startColumn.setOnEditCommit(e -> {
final var subtitle = e.getRowValue();
subtitle.setStart(e.getNewValue());
subtitlesTable.refresh();
subtitlesTable.requestFocus();
});
endColumn.setCellFactory(TextFieldTableCell.forTableColumn(new TimeStringConverter(timeFormatter)));
endColumn.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue() == null ? null : param.getValue().end()));
endColumn.setOnEditCommit(e -> {
final var subtitle = e.getRowValue();
subtitle.setEnd(e.getNewValue());
subtitlesTable.refresh();
subtitlesTable.requestFocus();
});
textColumn.setCellFactory(TextFieldTableCell.forTableColumn());
textColumn.setCellValueFactory(param -> new SimpleStringProperty(param.getValue() == null ? null : param.getValue().content()));
textColumn.setOnEditCommit(e -> {
final var subtitle = e.getRowValue();
subtitle.setContent(e.getNewValue());
subtitlesTable.refresh();
subtitlesTable.requestFocus();
});
subtitlesTable.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> model.selectedSubtitleProperty().set(newValue));
}
private void manageTabs() {
final var toRemove = new ArrayList<Tab>();
final var toAdd = new ArrayList<Tab>();
tabPane.getTabs().forEach(tab -> {
if (!model.collections().containsKey(Language.getLanguage(tab.getText()))) {
toRemove.add(tab);
}
});
model.collections().forEach((language, collection) -> {
if (tabPane.getTabs().stream().noneMatch(t -> Language.getLanguage(t.getText()) == language)) {
toAdd.add(new Tab(language.iso2()));
}
});
tabPane.getTabs().removeAll(toRemove);
tabPane.getTabs().addAll(toAdd);
tabPane.getTabs().sort(Comparator.comparing(Tab::getText));
}
private void bindComboboxes() {
languageCombobox.valueProperty().bindBidirectional(model.videoLanguageProperty());
languageCombobox.setItems(model.availableVideoLanguages());
languageCombobox.setConverter(new LanguageStringConverter());
translationsCombobox.setConverter(new LanguageStringConverter());
translationsCombobox.setItems(model.availableTranslationsLanguage());
}
@FXML
private void resetButtonPressed() {
model.setSelectedCollection(model.originalCollections().get(model.selectedLanguage()));
}
@FXML
private void addPressed() {
model.selectedCollection().subtitles().add(new ObservableSubtitleImpl("Enter text here..."));
}
@FXML
private void loadPressed() {
final var filePicker = new FileChooser();
final var archiveFilter = new FileChooser.ExtensionFilter(ARCHIVE, subtitleExtensions);
final var allSupportedFilter = new FileChooser.ExtensionFilter(ALL_SUPPORTED, subtitleExtensions);
filePicker.getExtensionFilters().addAll(archiveFilter, allSupportedFilter);
filePicker.setSelectedExtensionFilter(allSupportedFilter);
final var file = filePicker.showOpenDialog(window());
loadSubtitles(file.toPath());
}
@FXML
private void savePressed() {
final var filePicker = new FileChooser();
final var archiveFilter = new FileChooser.ExtensionFilter(ARCHIVE, subtitleExtensions);
final var allSupportedFilter = new FileChooser.ExtensionFilter(ALL_SUPPORTED, subtitleExtensions);
filePicker.getExtensionFilters().addAll(archiveFilter, allSupportedFilter);
filePicker.setSelectedExtensionFilter(allSupportedFilter);
final var file = filePicker.showSaveDialog(window());
if (file != null) {
saveSubtitles(file.toPath());
}
}
@Override
public void selectLanguage(final Language language) {
model.setSelectedLanguage(language);
}
@Override
public void deleteLanguage(final Language language) {
model.selectedTranslationsLanguages().remove(language);
}
@Override
public void saveSubtitles(final Path file) {
try {
final var filename = file.getFileName().toString();
final var extension = filename.substring(filename.lastIndexOf('.') + 1);
if (subtitleExtensions.contains(extension)) {
importerExporter.exportSubtitles(model.selectedCollection(), file);
} else {
importerExporter.exportSubtitles(model.collections().values(), file);
}
} catch (final IOException e) {
logger.error("Error saving subtitles {}", file, e);
showErrorDialog(resources.getString("subtitles.save.error.title"), MessageFormat.format(resources.getString("subtitles.save.error.label"), file));
}
}
@Override
public void loadSubtitles(final Path file) {
try {
final var map = importerExporter.importSubtitles(file);
map.values().forEach(this::loadCollection);
if (model.videoLanguage() == Language.AUTO) {
model.setVideoLanguage(map.keySet().stream().findFirst().orElse(Language.AUTO));
}
} catch (final IOException | ParseException e) {
logger.error("Error loading subtitles {}", file, e);
showErrorDialog(resources.getString("subtitles.load.error.title"), MessageFormat.format(resources.getString("subtitles.load.error.label"), file));
}
}
private void loadCollection(final SubtitleCollection<?> collection) {
final var observableCollection = new ObservableSubtitleCollectionImpl(collection);
model.originalCollections().put(observableCollection.language(), observableCollection);
model.collections().put(observableCollection.language(), observableCollection);
}
private void deleteSelectedSubtitles() {
model.selectedCollection().observableSubtitles().removeAll(subtitlesTable.getSelectionModel().getSelectedItems());
}
private void editFocusedCell() {
final var focusedCell = subtitlesTable.getFocusModel().getFocusedCell();
if (focusedCell != null) {
subtitlesTable.edit(focusedCell.getRow(), focusedCell.getTableColumn());
}
}
@Override
public FXSubtitlesModel model() {
return model;
}
@Override
protected Window window() {
return saveButton.getScene().getWindow();
}
}

View File

@@ -0,0 +1,256 @@
package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.Language;
import com.github.gtache.autosubtitle.gui.SubtitlesModel;
import com.github.gtache.autosubtitle.subtitle.gui.fx.ObservableSubtitleCollectionImpl;
import com.github.gtache.autosubtitle.subtitle.gui.fx.ObservableSubtitleImpl;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.MapChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Arrays;
import java.util.Comparator;
/**
* FX implementation of {@link SubtitlesModel}
*/
@Singleton
public class FXSubtitlesModel implements SubtitlesModel<ObservableSubtitleImpl, ObservableSubtitleCollectionImpl> {
private final ObservableList<Language> availableVideoLanguages;
private final ObjectProperty<Language> videoLanguage;
private final ObservableList<Language> availableTranslationLanguages;
private final ObservableList<Language> selectedTranslationsLanguages;
private final ObjectProperty<Language> selectedLanguage;
private final ObservableMap<Language, ObservableSubtitleCollectionImpl> collections;
private final ObservableMap<Language, ObservableSubtitleCollectionImpl> originalCollections;
private final ObjectProperty<ObservableSubtitleCollectionImpl> selectedCollection;
private final ObservableList<ObservableSubtitleImpl> selectedSubtitles;
private final ObjectProperty<ObservableSubtitleImpl> selectedSubtitle;
private final BooleanProperty canLoadSubtitles;
private final BooleanProperty canAddSubtitle;
private final BooleanProperty canResetSubtitles;
private final BooleanProperty canEditTable;
private final ReadOnlyBooleanWrapper canSaveSubtitles;
private final BooleanProperty isTranslating;
@Inject
FXSubtitlesModel() {
this.availableVideoLanguages = FXCollections.unmodifiableObservableList(FXCollections.observableArrayList(Arrays.stream(Language.values())
.sorted((o1, o2) -> {
if (o1 == Language.AUTO) {
return -1;
} else if (o2 == Language.AUTO) {
return 1;
} else {
return o1.englishName().compareTo(o2.englishName());
}
}).toList()));
this.availableTranslationLanguages = FXCollections.observableArrayList(Arrays.stream(Language.values()).filter(l -> l != Language.AUTO).toList());
this.videoLanguage = new SimpleObjectProperty<>(Language.AUTO);
this.selectedTranslationsLanguages = FXCollections.observableArrayList();
this.selectedLanguage = new SimpleObjectProperty<>(Language.AUTO);
this.collections = FXCollections.observableHashMap();
this.originalCollections = FXCollections.observableHashMap();
this.selectedCollection = new SimpleObjectProperty<>();
this.selectedSubtitles = FXCollections.observableArrayList();
this.selectedSubtitle = new SimpleObjectProperty<>();
this.canLoadSubtitles = new SimpleBooleanProperty(false);
this.canAddSubtitle = new SimpleBooleanProperty(false);
this.canResetSubtitles = new SimpleBooleanProperty(false);
this.canSaveSubtitles = new ReadOnlyBooleanWrapper(false);
this.canEditTable = new SimpleBooleanProperty(false);
this.isTranslating = new SimpleBooleanProperty(false);
canSaveSubtitles.bind(Bindings.isNotEmpty(collections));
collections.addListener((MapChangeListener<Language, ObservableSubtitleCollectionImpl>) change ->
availableTranslationLanguages.setAll(Arrays.stream(Language.values()).filter(l -> l != Language.AUTO && !collections.containsKey(l)).sorted(Comparator.comparing(Language::englishName)).toList()));
selectedCollection.addListener((observable, oldValue, newValue) -> {
selectedSubtitle.set(null);
if (newValue == null) {
selectedSubtitles.clear();
selectedLanguage.set(Language.AUTO);
} else {
selectedSubtitles.setAll(newValue.subtitles());
selectedLanguage.set(newValue.language());
}
});
}
@Override
public ObservableList<Language> availableVideoLanguages() {
return availableVideoLanguages;
}
@Override
public Language videoLanguage() {
return videoLanguage.get();
}
@Override
public void setVideoLanguage(final Language language) {
videoLanguage.set(language);
}
ObjectProperty<Language> videoLanguageProperty() {
return videoLanguage;
}
@Override
public ObservableList<Language> availableTranslationsLanguage() {
return FXCollections.unmodifiableObservableList(availableTranslationLanguages);
}
@Override
public ObservableList<Language> selectedTranslationsLanguages() {
return selectedTranslationsLanguages;
}
@Override
public Language selectedLanguage() {
return selectedLanguage.get();
}
@Override
public void setSelectedLanguage(final Language language) {
selectedLanguage.set(language);
}
ObjectProperty<Language> selectedLanguageProperty() {
return selectedLanguage;
}
@Override
public ObservableMap<Language, ObservableSubtitleCollectionImpl> collections() {
return collections;
}
@Override
public ObservableSubtitleCollectionImpl selectedCollection() {
return selectedCollection.get();
}
@Override
public void setSelectedCollection(final ObservableSubtitleCollectionImpl collection) {
selectedCollection.set(collection);
}
ObjectProperty<ObservableSubtitleCollectionImpl> selectedCollectionProperty() {
return selectedCollection;
}
@Override
public ObservableMap<Language, ObservableSubtitleCollectionImpl> originalCollections() {
return originalCollections;
}
@Override
public ObservableList<ObservableSubtitleImpl> selectedSubtitles() {
return selectedSubtitles;
}
@Override
public ObservableSubtitleImpl selectedSubtitle() {
return selectedSubtitle.get();
}
@Override
public void setSelectedSubtitle(final ObservableSubtitleImpl subtitle) {
selectedSubtitle.set(subtitle);
}
ObjectProperty<ObservableSubtitleImpl> selectedSubtitleProperty() {
return selectedSubtitle;
}
@Override
public boolean canLoadSubtitles() {
return canLoadSubtitles.get();
}
@Override
public void setCanLoadSubtitles(final boolean canLoadSubtitles) {
this.canLoadSubtitles.set(canLoadSubtitles);
}
BooleanProperty canLoadSubtitlesProperty() {
return canLoadSubtitles;
}
@Override
public boolean canAddSubtitle() {
return canAddSubtitle.get();
}
@Override
public void setCanAddSubtitle(final boolean canAddSubtitle) {
this.canAddSubtitle.set(canAddSubtitle);
}
BooleanProperty canAddSubtitleProperty() {
return canAddSubtitle;
}
@Override
public boolean canResetSubtitles() {
return canResetSubtitles.get();
}
@Override
public void setCanResetSubtitles(final boolean canResetSubtitles) {
this.canResetSubtitles.set(canResetSubtitles);
}
BooleanProperty canResetSubtitlesProperty() {
return canResetSubtitles;
}
@Override
public boolean canSaveSubtitles() {
return canSaveSubtitles.get();
}
ReadOnlyBooleanProperty canSaveSubtitlesProperty() {
return canSaveSubtitles.getReadOnlyProperty();
}
@Override
public boolean isTranslating() {
return isTranslating.get();
}
@Override
public void setTranslating(final boolean translating) {
this.isTranslating.set(translating);
}
BooleanProperty translatingProperty() {
return isTranslating;
}
@Override
public boolean canEditTable() {
return canEditTable.get();
}
@Override
public void setCanEditTable(final boolean canEditTable) {
this.canEditTable.set(canEditTable);
}
public BooleanProperty canEditTableProperty() {
return canEditTable;
}
}

View File

@@ -5,7 +5,7 @@ import javax.inject.Singleton;
import java.util.Objects;
@Singleton
public class FXWorkBinder {
public class FXWorkBinder implements FXBinder {
private final FXWorkModel workModel;
private final FXParametersModel parametersModel;
@@ -16,7 +16,8 @@ public class FXWorkBinder {
this.parametersModel = Objects.requireNonNull(parametersModel);
}
void createBindings() {
@Override
public void createBindings() {
workModel.extractionModelProperty().bind(parametersModel.extractionModelProperty());
}
}

View File

@@ -1,49 +1,32 @@
package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.Language;
import com.github.gtache.autosubtitle.Translator;
import com.github.gtache.autosubtitle.VideoConverter;
import com.github.gtache.autosubtitle.VideoLoader;
import com.github.gtache.autosubtitle.gui.TimeFormatter;
import com.github.gtache.autosubtitle.gui.WorkController;
import com.github.gtache.autosubtitle.gui.WorkStatus;
import com.github.gtache.autosubtitle.subtitle.EditableSubtitle;
import com.github.gtache.autosubtitle.subtitle.SubtitleCollection;
import com.github.gtache.autosubtitle.subtitle.converter.ParseException;
import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverter;
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractEvent;
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractException;
import com.github.gtache.autosubtitle.subtitle.extractor.SubtitleExtractor;
import com.github.gtache.autosubtitle.subtitle.extractor.SubtitleExtractorListener;
import com.github.gtache.autosubtitle.subtitle.gui.fx.ObservableSubtitleImpl;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.input.KeyCode;
import javafx.stage.FileChooser;
import javafx.stage.Window;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.controlsfx.control.CheckComboBox;
import org.controlsfx.control.PrefixSelectionComboBox;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.MessageFormat;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
@@ -67,34 +50,18 @@ public class FXWorkController extends AbstractFXController implements WorkContro
@FXML
private TextField fileField;
@FXML
private Button loadSubtitlesButton;
@FXML
private Button extractButton;
@FXML
private Button resetButton;
@FXML
private Button exportSoftButton;
@FXML
private Button exportHardButton;
@FXML
private TableView<EditableSubtitle> subtitlesTable;
@FXML
private TableColumn<EditableSubtitle, Long> startColumn;
@FXML
private TableColumn<EditableSubtitle, Long> endColumn;
@FXML
private TableColumn<EditableSubtitle, String> textColumn;
@FXML
private FXMediaController mediaController;
@FXML
private Button saveSubtitlesButton;
@FXML
private Button addSubtitleButton;
@FXML
private PrefixSelectionComboBox<Language> languageCombobox;
@FXML
private CheckComboBox<Language> translationsCombobox;
@FXML
private Label progressLabel;
@FXML
@@ -107,34 +74,25 @@ public class FXWorkController extends AbstractFXController implements WorkContro
private final FXWorkModel model;
private final FXWorkBinder binder;
private final SubtitleExtractor subtitleExtractor;
private final Map<String, SubtitleConverter> subtitleConvertersMap;
private final VideoConverter videoConverter;
private final VideoLoader videoLoader;
private final Translator translator;
private final TimeFormatter timeFormatter;
private final List<String> subtitleExtensions;
@Inject
FXWorkController(final FXWorkModel model, final FXWorkBinder binder, final SubtitleExtractor subtitleExtractor,
final Map<String, SubtitleConverter> subtitleConvertersMap, final VideoLoader videoLoader,
final VideoConverter videoConverter, final Translator translator, final TimeFormatter timeFormatter) {
final VideoLoader videoLoader, final VideoConverter videoConverter) {
this.model = requireNonNull(model);
this.binder = requireNonNull(binder);
this.subtitleExtractor = requireNonNull(subtitleExtractor);
this.subtitleConvertersMap = requireNonNull(subtitleConvertersMap);
this.videoConverter = requireNonNull(videoConverter);
this.videoLoader = requireNonNull(videoLoader);
this.translator = requireNonNull(translator);
this.timeFormatter = requireNonNull(timeFormatter);
this.subtitleExtensions = subtitleConvertersMap.values().stream().map(c -> "*." + c.formatName()).sorted().toList();
}
@FXML
private void initialize() {
bindComboboxes();
bindButtons();
bindTable();
void initialize() {
extractButton.disableProperty().bind(model.canExtractProperty().not());
exportSoftButton.disableProperty().bind(model.canExportProperty().not());
exportHardButton.disableProperty().bind(model.canExportProperty().not());
bindProgress();
model.selectedSubtitleProperty().addListener((observable, oldValue, newValue) -> {
@@ -148,91 +106,17 @@ public class FXWorkController extends AbstractFXController implements WorkContro
subtitleExtractor.addListener(this);
}
private void bindComboboxes() {
languageCombobox.valueProperty().bindBidirectional(model.videoLanguageProperty());
languageCombobox.setItems(model.availableVideoLanguages());
languageCombobox.setConverter(new LanguageStringConverter());
translationsCombobox.setConverter(new LanguageStringConverter());
Bindings.bindContent(translationsCombobox.getItems(), model.availableTranslationsLanguage());
Bindings.bindContent(model.translations(), translationsCombobox.getCheckModel().getCheckedItems());
}
private void bindButtons() {
extractButton.disableProperty().bind(model.videoProperty().isNull().or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
addSubtitleButton.disableProperty().bind(model.videoProperty().isNull().or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
loadSubtitlesButton.disableProperty().bind(model.videoProperty().isNull().or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
resetButton.disableProperty().bind(Bindings.isEmpty(model.subtitles()).or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
exportSoftButton.disableProperty().bind(Bindings.isEmpty(model.subtitles()).or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
exportHardButton.disableProperty().bind(Bindings.isEmpty(model.subtitles()).or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
saveSubtitlesButton.disableProperty().bind(Bindings.isEmpty(model.subtitles()).or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
}
private void bindTable() {
subtitlesTable.setItems(model.subtitles());
subtitlesTable.setOnKeyPressed(e -> {
if (e.getCode().isLetterKey() || e.getCode().isDigitKey()) {
editFocusedCell();
} else if (e.getCode() == KeyCode.RIGHT ||
e.getCode() == KeyCode.TAB) {
subtitlesTable.getSelectionModel().selectNext();
e.consume();
} else if (e.getCode() == KeyCode.LEFT) {
subtitlesTable.getSelectionModel().selectPrevious();
e.consume();
} else if (e.getCode() == KeyCode.DELETE) {
deleteSelectedSubtitles();
e.consume();
}
});
startColumn.setCellFactory(TextFieldTableCell.forTableColumn(new TimeStringConverter(timeFormatter)));
startColumn.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue() == null ? null : param.getValue().start()));
startColumn.setOnEditCommit(e -> {
final var subtitle = e.getRowValue();
subtitle.setStart(e.getNewValue());
subtitlesTable.refresh();
subtitlesTable.requestFocus();
});
endColumn.setCellFactory(TextFieldTableCell.forTableColumn(new TimeStringConverter(timeFormatter)));
endColumn.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue() == null ? null : param.getValue().end()));
endColumn.setOnEditCommit(e -> {
final var subtitle = e.getRowValue();
subtitle.setEnd(e.getNewValue());
subtitlesTable.refresh();
subtitlesTable.requestFocus();
});
textColumn.setCellFactory(TextFieldTableCell.forTableColumn());
textColumn.setCellValueFactory(param -> new SimpleStringProperty(param.getValue() == null ? null : param.getValue().content()));
textColumn.setOnEditCommit(e -> {
final var subtitle = e.getRowValue();
subtitle.setContent(e.getNewValue());
subtitlesTable.refresh();
subtitlesTable.requestFocus();
});
subtitlesTable.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> model.selectedSubtitleProperty().set(newValue));
}
private void bindProgress() {
progressLabel.textProperty().bind(Bindings.createStringBinding(() -> resources.getString("work.status." + model.status().name().toLowerCase() + ".label"), model.statusProperty()));
progressLabel.visibleProperty().bind(model.statusProperty().isNotEqualTo(WorkStatus.IDLE));
progressBar.visibleProperty().bind(model.statusProperty().isNotEqualTo(WorkStatus.IDLE));
progressDetailLabel.visibleProperty().bind(model.statusProperty().isNotEqualTo(WorkStatus.IDLE));
progressLabel.managedProperty().bind(model.statusProperty().isNotEqualTo(WorkStatus.IDLE));
progressBar.managedProperty().bind(model.statusProperty().isNotEqualTo(WorkStatus.IDLE));
progressDetailLabel.managedProperty().bind(model.statusProperty().isNotEqualTo(WorkStatus.IDLE));
progressLabel.visibleProperty().bind(model.isProgressVisibleProperty());
progressBar.visibleProperty().bind(model.isProgressVisibleProperty());
progressDetailLabel.visibleProperty().bind(model.isProgressVisibleProperty());
progressLabel.managedProperty().bind(model.isProgressVisibleProperty());
progressBar.managedProperty().bind(model.isProgressVisibleProperty());
progressDetailLabel.managedProperty().bind(model.isProgressVisibleProperty());
progressBar.progressProperty().bindBidirectional(model.progressProperty());
}
private void deleteSelectedSubtitles() {
model.subtitles().removeAll(subtitlesTable.getSelectionModel().getSelectedItems());
}
private void editFocusedCell() {
final var focusedCell = subtitlesTable.getFocusModel().getFocusedCell();
if (focusedCell != null) {
subtitlesTable.edit(focusedCell.getRow(), focusedCell.getTableColumn());
}
}
@FXML
private void fileButtonPressed() {
@@ -254,17 +138,17 @@ public class FXWorkController extends AbstractFXController implements WorkContro
}
}
private SubtitleCollection extractAsync() {
private SubtitleCollection<?> extractAsync() {
try {
return subtitleExtractor.extract(model.video(), model.videoLanguage(), model.extractionModel());
return subtitleExtractor.extract(model.video(), model.videoLanguageProperty().get(), model.extractionModel());
} catch (final ExtractException e) {
throw new CompletionException(e);
}
}
private void manageExtractResult(final SubtitleCollection newCollection, final Throwable t) {
private void manageExtractResult(final SubtitleCollection<?> newCollection, final Throwable t) {
if (t == null) {
loadCollection(newCollection);
model.setExtractedCollection(newCollection);
} else {
logger.error("Error extracting subtitles", t);
showErrorDialog(resources.getString("work.extract.error.title"), MessageFormat.format(resources.getString("work.extract.error.label"), t.getMessage()));
@@ -284,45 +168,6 @@ public class FXWorkController extends AbstractFXController implements WorkContro
}
}
@Override
public void saveSubtitles(final Path file) {
final var fileName = file.getFileName().toString();
final var converter = subtitleConvertersMap.get(fileName.substring(fileName.lastIndexOf('.') + 1));
if (converter == null) {
logger.warn("No converter for {}", file);
showErrorDialog(resources.getString("work.save.subtitles.missing.converter.title"), MessageFormat.format(resources.getString("work.save.subtitles.missing.converter.label"), file.getFileName()));
} else {
final var string = converter.format(model.subtitleCollection());
try {
Files.writeString(file, string);
} catch (final IOException e) {
logger.error("Error saving subtitles {}", file, e);
showErrorDialog(resources.getString("work.save.subtitles.error.title"), MessageFormat.format(resources.getString("work.save.subtitles.error.label"), e.getMessage()));
}
}
}
@Override
public void loadSubtitles(final Path file) {
final var fileName = file.getFileName().toString();
final var parser = subtitleConvertersMap.get(fileName.substring(fileName.lastIndexOf('.') + 1));
if (parser != null) {
try {
final var collection = parser.parse(file);
loadCollection(collection);
} catch (final ParseException e) {
logger.error("Error loading subtitles {}", file, e);
showErrorDialog(resources.getString("work.load.subtitles.error.title"), MessageFormat.format(resources.getString("work.load.subtitles.error.label"), e.getMessage()));
}
}
}
private void loadCollection(final SubtitleCollection collection) {
model.subtitles().setAll(collection.subtitles().stream().map(ObservableSubtitleImpl::new).toList());
model.originalSubtitles().clear();
model.originalSubtitles().addAll(collection.subtitles().stream().map(ObservableSubtitleImpl::new).toList());
model.videoLanguageProperty().set(collection.language());
}
@FXML
private void exportSoftPressed() {
@@ -332,27 +177,20 @@ public class FXWorkController extends AbstractFXController implements WorkContro
filePicker.setSelectedExtensionFilter(extensionFilter);
final var file = filePicker.showSaveDialog(window());
if (file != null) {
final var baseCollection = model.subtitleCollection();
final var translations = model.translations();
model.setStatus(WorkStatus.TRANSLATING);
CompletableFuture.supplyAsync(() -> Stream.concat(Stream.of(baseCollection), translations.stream().map(l -> translator.translate(baseCollection, l))).toList())
.thenApplyAsync(c -> {
model.setStatus(WorkStatus.EXPORTING);
return c;
}, Platform::runLater)
.thenAcceptAsync(collections -> {
try {
videoConverter.addSoftSubtitles(model.video(), collections, file.toPath());
} catch (final IOException e) {
throw new CompletionException(e);
}
}).whenCompleteAsync((v, t) -> {
if (t != null) {
logger.error("Error exporting subtitles", t);
showErrorDialog(resources.getString("work.export.error.title"), MessageFormat.format(resources.getString("work.export.error.label"), t.getMessage()));
}
model.setStatus(WorkStatus.IDLE);
}, Platform::runLater);
model.setStatus(WorkStatus.EXPORTING);
CompletableFuture.runAsync(() -> {
try {
videoConverter.addSoftSubtitles(model.video(), model.collections().values(), file.toPath());
} catch (final IOException e) {
throw new CompletionException(e);
}
}).whenCompleteAsync((v, t) -> {
if (t != null) {
logger.error("Error exporting subtitles", t);
showErrorDialog(resources.getString("work.export.error.title"), MessageFormat.format(resources.getString("work.export.error.label"), t.getMessage()));
} //else show info dialog
model.setStatus(WorkStatus.IDLE);
}, Platform::runLater);
}
}
@@ -366,7 +204,7 @@ public class FXWorkController extends AbstractFXController implements WorkContro
if (file != null) {
CompletableFuture.runAsync(() -> {
try {
videoConverter.addHardSubtitles(model.video(), model.subtitleCollection(), file.toPath());
videoConverter.addHardSubtitles(model.video(), model.collections().get(model.videoLanguageProperty().get()), file.toPath());
} catch (final IOException e) {
throw new CompletionException(e);
}
@@ -374,7 +212,7 @@ public class FXWorkController extends AbstractFXController implements WorkContro
if (t != null) {
logger.error("Error exporting subtitles", t);
showErrorDialog(resources.getString("work.export.error.title"), MessageFormat.format(resources.getString("work.export.error.label"), t.getMessage()));
}
} //else show info dialog
model.setStatus(WorkStatus.IDLE);
}, Platform::runLater);
}
@@ -386,6 +224,7 @@ public class FXWorkController extends AbstractFXController implements WorkContro
return model;
}
@Override
public Window window() {
return fileField.getScene().getWindow();
}
@@ -395,16 +234,6 @@ public class FXWorkController extends AbstractFXController implements WorkContro
extractSubtitles();
}
@FXML
private void resetButtonPressed() {
model.subtitles().setAll(model.originalSubtitles());
}
@FXML
private void addSubtitlePressed() {
model.subtitles().add(new ObservableSubtitleImpl("Enter text here..."));
}
@Override
public void listen(final ExtractEvent event) {
Platform.runLater(() -> {
@@ -413,25 +242,5 @@ public class FXWorkController extends AbstractFXController implements WorkContro
});
}
@FXML
private void loadSubtitlesPressed() {
final var filePicker = new FileChooser();
final var extensionFilter = new FileChooser.ExtensionFilter(ALL_SUPPORTED, subtitleExtensions);
filePicker.getExtensionFilters().add(extensionFilter);
filePicker.setSelectedExtensionFilter(extensionFilter);
final var file = filePicker.showOpenDialog(window());
loadSubtitles(file.toPath());
}
@FXML
private void saveSubtitlesPressed() {
final var filePicker = new FileChooser();
final var extensionFilter = new FileChooser.ExtensionFilter(ALL_SUPPORTED, subtitleExtensions);
filePicker.getExtensionFilters().add(extensionFilter);
filePicker.setSelectedExtensionFilter(extensionFilter);
final var file = filePicker.showSaveDialog(window());
if (file != null) {
saveSubtitles(file.toPath());
}
}
}

View File

@@ -7,26 +7,22 @@ import com.github.gtache.autosubtitle.gui.WorkStatus;
import com.github.gtache.autosubtitle.subtitle.EditableSubtitle;
import com.github.gtache.autosubtitle.subtitle.SubtitleCollection;
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractionModel;
import com.github.gtache.autosubtitle.subtitle.impl.SubtitleCollectionImpl;
import javafx.beans.binding.Bindings;
import com.github.gtache.autosubtitle.subtitle.gui.fx.ObservableSubtitleCollectionImpl;
import com.github.gtache.autosubtitle.subtitle.gui.fx.ObservableSubtitleImpl;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
/**
* FX implementation of {@link WorkModel}
@@ -35,51 +31,35 @@ import java.util.stream.Collectors;
public class FXWorkModel implements WorkModel {
private final ObjectProperty<Video> video;
private final ReadOnlyObjectWrapper<SubtitleCollection> subtitleCollection;
private final ObservableList<EditableSubtitle> subtitles;
private final List<EditableSubtitle> originalSubtitles;
private final ObjectProperty<EditableSubtitle> subtitle;
private final ObservableList<Language> availableVideoLanguages;
private final ObservableList<Language> availableTranslationLanguages;
private final ObjectProperty<ExtractionModel> extractionModel;
private final ObjectProperty<Language> videoLanguage;
private final ObservableList<Language> translations;
private final ReadOnlyStringWrapper text;
private final ObjectProperty<WorkStatus> workStatus;
private final DoubleProperty progress;
private final ObjectProperty<EditableSubtitle> selectedSubtitle;
private final ObservableList<ObservableSubtitleImpl> subtitles;
private final ObservableMap<Language, ObservableSubtitleCollectionImpl> collections;
private final ObjectProperty<Language> videoLanguage;
private final ReadOnlyBooleanWrapper canExtract;
private final BooleanProperty canExport;
private final ReadOnlyBooleanWrapper isProgressVisible;
private final ObjectProperty<SubtitleCollection<?>> extractedCollection;
@Inject
FXWorkModel() {
this.video = new SimpleObjectProperty<>();
this.subtitleCollection = new ReadOnlyObjectWrapper<>();
this.subtitles = FXCollections.observableArrayList();
this.originalSubtitles = new ArrayList<>();
this.subtitle = new SimpleObjectProperty<>();
this.availableVideoLanguages =
FXCollections.unmodifiableObservableList(FXCollections.observableArrayList(Arrays.stream(Language.values())
.sorted((o1, o2) -> {
if (o1 == Language.AUTO) {
return -1;
} else if (o2 == Language.AUTO) {
return 1;
} else {
return o1.compareTo(o2);
}
}).toList()));
this.availableTranslationLanguages =
FXCollections.observableArrayList(Arrays.stream(Language.values()).filter(l -> l != Language.AUTO).toList());
this.videoLanguage = new SimpleObjectProperty<>(Language.AUTO);
this.translations = FXCollections.observableArrayList();
this.text = new ReadOnlyStringWrapper("");
this.workStatus = new SimpleObjectProperty<>(WorkStatus.IDLE);
this.extractionModel = new SimpleObjectProperty<>();
this.progress = new SimpleDoubleProperty(-1);
text.bind(Bindings.createStringBinding(() ->
subtitles.stream().map(EditableSubtitle::content).collect(Collectors.joining("")),
subtitles));
subtitleCollection.bind(Bindings.createObjectBinding(() -> new SubtitleCollectionImpl(text(), subtitles, videoLanguage()), text, subtitles, videoLanguage));
videoLanguage.addListener((observable, oldValue, newValue) -> FXCollections.observableArrayList(Arrays.stream(Language.values())
.filter(l -> l != Language.AUTO && l != newValue).sorted(Comparator.naturalOrder()).toList()));
this.selectedSubtitle = new SimpleObjectProperty<>();
this.subtitles = FXCollections.observableArrayList();
this.collections = FXCollections.observableHashMap();
this.videoLanguage = new SimpleObjectProperty<>(Language.AUTO);
this.canExtract = new ReadOnlyBooleanWrapper(false);
this.canExport = new SimpleBooleanProperty(false);
this.isProgressVisible = new ReadOnlyBooleanWrapper(false);
this.extractedCollection = new SimpleObjectProperty<>();
isProgressVisible.bind(workStatus.isNotEqualTo(WorkStatus.IDLE));
canExtract.bind(video.isNotNull().and(workStatus.isEqualTo(WorkStatus.IDLE)));
}
@Override
@@ -87,6 +67,15 @@ public class FXWorkModel implements WorkModel {
return video.get();
}
@Override
public void setVideo(final Video video) {
this.video.set(video);
}
ObjectProperty<Video> videoProperty() {
return video;
}
@Override
public ExtractionModel extractionModel() {
return extractionModel.get();
@@ -101,76 +90,6 @@ public class FXWorkModel implements WorkModel {
return extractionModel;
}
@Override
public SubtitleCollection subtitleCollection() {
return subtitleCollection.get();
}
ReadOnlyObjectProperty<SubtitleCollection> subtitleCollectionProperty() {
return subtitleCollection.getReadOnlyProperty();
}
ObjectProperty<Video> videoProperty() {
return video;
}
@Override
public ObservableList<EditableSubtitle> subtitles() {
return subtitles;
}
@Override
public List<EditableSubtitle> originalSubtitles() {
return originalSubtitles;
}
@Override
public String text() {
return text.get();
}
ReadOnlyStringProperty textProperty() {
return text.getReadOnlyProperty();
}
@Override
public EditableSubtitle selectedSubtitle() {
return subtitle.get();
}
ObjectProperty<EditableSubtitle> selectedSubtitleProperty() {
return subtitle;
}
@Override
public ObservableList<Language> availableVideoLanguages() {
return availableVideoLanguages;
}
@Override
public ObservableList<Language> availableTranslationsLanguage() {
return availableTranslationLanguages;
}
@Override
public Language videoLanguage() {
return videoLanguage.get();
}
@Override
public void setVideoLanguage(final Language language) {
videoLanguage.set(language);
}
ObjectProperty<Language> videoLanguageProperty() {
return videoLanguage;
}
@Override
public ObservableList<Language> translations() {
return translations;
}
@Override
public WorkStatus status() {
return workStatus.get();
@@ -198,4 +117,66 @@ public class FXWorkModel implements WorkModel {
DoubleProperty progressProperty() {
return progress;
}
@Override
public boolean canExtract() {
return canExtract.get();
}
ReadOnlyBooleanProperty canExtractProperty() {
return canExtract.getReadOnlyProperty();
}
@Override
public boolean canExport() {
return canExport.get();
}
@Override
public void setCanExport(final boolean canExport) {
this.canExport.set(canExport);
}
BooleanProperty canExportProperty() {
return canExport;
}
@Override
public boolean isProgressVisible() {
return isProgressVisible.get();
}
ReadOnlyBooleanProperty isProgressVisibleProperty() {
return isProgressVisible.getReadOnlyProperty();
}
@Override
public SubtitleCollection<?> extractedCollection() {
return extractedCollection.get();
}
@Override
public void setExtractedCollection(final SubtitleCollection<?> collection) {
extractedCollection.set(collection);
}
ObjectProperty<SubtitleCollection<?>> extractedCollectionProperty() {
return extractedCollection;
}
ObjectProperty<EditableSubtitle> selectedSubtitleProperty() {
return selectedSubtitle;
}
ObservableList<ObservableSubtitleImpl> subtitles() {
return subtitles;
}
ObservableMap<Language, ObservableSubtitleCollectionImpl> collections() {
return collections;
}
ObjectProperty<Language> videoLanguageProperty() {
return videoLanguage;
}
}

View File

@@ -3,6 +3,9 @@ package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.Language;
import javafx.util.StringConverter;
/**
* Converts a language to a string and vice-versa
*/
class LanguageStringConverter extends StringConverter<Language> {
@Override
public String toString(final Language object) {

View File

@@ -5,6 +5,9 @@ import javafx.util.StringConverter;
import java.util.Objects;
/**
* Converts a time in milliseconds to a string and vice-versa
*/
class TimeStringConverter extends StringConverter<Long> {
private final TimeFormatter timeFormatter;

View File

@@ -6,6 +6,7 @@ import com.github.gtache.autosubtitle.gui.fx.FXMainController;
import com.github.gtache.autosubtitle.gui.fx.FXMediaController;
import com.github.gtache.autosubtitle.gui.fx.FXParametersController;
import com.github.gtache.autosubtitle.gui.fx.FXSetupController;
import com.github.gtache.autosubtitle.gui.fx.FXSubtitlesController;
import com.github.gtache.autosubtitle.gui.fx.FXWorkController;
import com.github.gtache.autosubtitle.modules.gui.impl.Pause;
import com.github.gtache.autosubtitle.modules.gui.impl.Play;
@@ -31,21 +32,25 @@ public abstract class FXModule {
@Provides
@Singleton
static FXMLLoader providesFXMLLoader(final FXMainController mainController, final FXSetupController setupController, final FXParametersController parametersController,
final FXWorkController workController, final FXMediaController mediaController, final ResourceBundle bundle) {
static FXMLLoader providesFXMLLoader(final FXMainController mainController, final FXMediaController mediaController,
final FXParametersController parametersController, final FXSetupController setupController,
final FXSubtitlesController subtitlesController, final FXWorkController workController,
final ResourceBundle bundle) {
final var loader = new FXMLLoader(FXModule.class.getResource("/com/github/gtache/autosubtitle/gui/fx/mainView.fxml"));
loader.setResources(bundle);
loader.setControllerFactory(c -> {
if (c == FXMainController.class) {
return mainController;
} else if (c == FXSetupController.class) {
return setupController;
} else if (c == FXWorkController.class) {
return workController;
} else if (c == FXMediaController.class) {
return mediaController;
} else if (c == FXParametersController.class) {
return parametersController;
} else if (c == FXSetupController.class) {
return setupController;
} else if (c == FXSubtitlesController.class) {
return subtitlesController;
} else if (c == FXWorkController.class) {
return workController;
} else {
throw new IllegalArgumentException("Unknown controller " + c);
}

View File

@@ -0,0 +1,59 @@
package com.github.gtache.autosubtitle.subtitle.gui.fx;
import com.github.gtache.autosubtitle.Language;
import com.github.gtache.autosubtitle.subtitle.SubtitleCollection;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import java.util.Objects;
/**
* FX observable implementation of {@link SubtitleCollection}
*/
public record ObservableSubtitleCollectionImpl(StringProperty textProperty,
ObservableList<ObservableSubtitleImpl> observableSubtitles,
ObjectProperty<Language> languageProperty) implements SubtitleCollection<ObservableSubtitleImpl> {
public ObservableSubtitleCollectionImpl() {
this(new SimpleStringProperty(""), FXCollections.observableArrayList(), new SimpleObjectProperty<>());
}
public ObservableSubtitleCollectionImpl(final SubtitleCollection<?> subtitleCollection) {
this(new SimpleStringProperty(subtitleCollection.text()),
FXCollections.observableArrayList(subtitleCollection.subtitles().stream().map(ObservableSubtitleImpl::new).toList()),
new SimpleObjectProperty<>(subtitleCollection.language()));
}
public ObservableSubtitleCollectionImpl {
Objects.requireNonNull(textProperty);
Objects.requireNonNull(observableSubtitles);
Objects.requireNonNull(languageProperty);
}
@Override
public String text() {
return textProperty.get();
}
public void setText(final String text) {
this.textProperty.set(text);
}
@Override
public ObservableList<ObservableSubtitleImpl> subtitles() {
return observableSubtitles;
}
@Override
public Language language() {
return languageProperty.get();
}
public void setLanguage(final Language language) {
this.languageProperty.set(language);
}
}

View File

@@ -4,7 +4,6 @@ import com.github.gtache.autosubtitle.subtitle.Bounds;
import com.github.gtache.autosubtitle.subtitle.EditableSubtitle;
import com.github.gtache.autosubtitle.subtitle.Font;
import com.github.gtache.autosubtitle.subtitle.Subtitle;
import com.github.gtache.autosubtitle.subtitle.impl.BoundsImpl;
import javafx.beans.property.LongProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleLongProperty;
@@ -12,104 +11,86 @@ import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
/**
* FX implementation of {@link EditableSubtitle}
*/
public class ObservableSubtitleImpl implements EditableSubtitle {
import java.util.Objects;
private final StringProperty content;
private final LongProperty start;
private final LongProperty end;
private final ObjectProperty<Font> font;
private final ObjectProperty<Bounds> location;
/**
* FX observable implementation of {@link EditableSubtitle}
*/
public record ObservableSubtitleImpl(StringProperty contentProperty, LongProperty startProperty,
LongProperty endProperty, ObjectProperty<Font> fontProperty,
ObjectProperty<Bounds> boundsProperty) implements EditableSubtitle {
public ObservableSubtitleImpl() {
this("");
}
public ObservableSubtitleImpl(final String content) {
this.content = new SimpleStringProperty(content);
this.start = new SimpleLongProperty(0);
this.end = new SimpleLongProperty(0);
this.font = new SimpleObjectProperty<>();
this.location = new SimpleObjectProperty<>(new BoundsImpl(0, 0, 100, 12));
this(content, 0, 0, null, null);
}
public ObservableSubtitleImpl(final Subtitle subtitle) {
this.content = new SimpleStringProperty(subtitle.content());
this.start = new SimpleLongProperty(subtitle.start());
this.end = new SimpleLongProperty(subtitle.end());
this.font = new SimpleObjectProperty<>(subtitle.font());
this.location = new SimpleObjectProperty<>(subtitle.bounds());
this(subtitle.content(), subtitle.start(), subtitle.end(), subtitle.font(), subtitle.bounds());
}
public ObservableSubtitleImpl(final String content, final long start, final long end, final Font font, final Bounds bounds) {
this(new SimpleStringProperty(content), new SimpleLongProperty(start), new SimpleLongProperty(end), new SimpleObjectProperty<>(font), new SimpleObjectProperty<>(bounds));
}
public ObservableSubtitleImpl {
Objects.requireNonNull(contentProperty);
Objects.requireNonNull(startProperty);
Objects.requireNonNull(endProperty);
Objects.requireNonNull(fontProperty);
Objects.requireNonNull(boundsProperty);
}
@Override
public String content() {
return content.get();
return contentProperty.get();
}
@Override
public void setContent(final String content) {
this.content.set(content);
}
public StringProperty contentProperty() {
return content;
this.contentProperty.set(content);
}
@Override
public long start() {
return start.get();
return startProperty.get();
}
@Override
public void setStart(final long start) {
this.start.set(start);
}
public LongProperty startProperty() {
return start;
this.startProperty.set(start);
}
@Override
public long end() {
return end.get();
return endProperty.get();
}
@Override
public void setEnd(final long end) {
this.end.set(end);
}
public LongProperty endProperty() {
return end;
this.endProperty.set(end);
}
@Override
public Font font() {
return font.get();
return fontProperty.get();
}
@Override
public void setFont(final Font font) {
this.font.set(font);
}
public ObjectProperty<Font> fontProperty() {
return font;
this.fontProperty.set(font);
}
@Override
public Bounds bounds() {
return location.get();
return boundsProperty.get();
}
@Override
public void setBounds(final Bounds bounds) {
this.location.set(bounds);
}
public ObjectProperty<Bounds> locationProperty() {
return location;
this.boundsProperty.set(bounds);
}
}

View File

@@ -14,5 +14,6 @@ module com.github.gtache.autosubtitle.gui.fx {
exports com.github.gtache.autosubtitle.gui.fx;
exports com.github.gtache.autosubtitle.modules.gui.fx;
exports com.github.gtache.autosubtitle.subtitle.gui.fx;
opens com.github.gtache.autosubtitle.gui.fx to javafx.fxml;
}

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ComboBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Tab?>
<?import javafx.scene.control.TabPane?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<?import org.controlsfx.control.PrefixSelectionComboBox?>
<VBox spacing="10" xmlns="http://javafx.com/javafx/22" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.github.gtache.autosubtitle.gui.fx.FXSubtitlesController">
<children>
<HBox alignment="CENTER_LEFT" spacing="10.0">
<children>
<Label text="%subtitles.language.label" />
<PrefixSelectionComboBox fx:id="languageCombobox" />
<Label text="%subtitles.translate.label" />
<ComboBox fx:id="translationsCombobox" />
</children>
</HBox>
<TabPane fx:id="tabPane" VBox.vgrow="ALWAYS">
<tabs>
<Tab fx:id="mainSubtitlesTab" closable="false">
<content>
<TableView fx:id="subtitlesTable" editable="true">
<columns>
<TableColumn fx:id="startColumn" prefWidth="50.0" sortable="false" text="%subtitles.table.column.from.label" />
<TableColumn fx:id="endColumn" prefWidth="50.0" sortable="false" text="%subtitles.table.column.to.label" />
<TableColumn fx:id="textColumn" prefWidth="75.0" sortable="false" text="%subtitles.table.column.text.label" />
</columns>
<columnResizePolicy>
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
</columnResizePolicy>
</TableView>
</content>
</Tab>
</tabs>
</TabPane>
<HBox spacing="10.0">
<children>
<Button fx:id="loadButton" mnemonicParsing="false" onAction="#loadPressed" text="%subtitles.button.load.label" />
<Button fx:id="saveButton" mnemonicParsing="false" onAction="#savePressed" text="%subtitles.button.subtitles.save.label" />
<Button fx:id="resetButton" mnemonicParsing="false" onAction="#resetButtonPressed" text="%subtitles.button.reset.label" />
<Button fx:id="addButton" mnemonicParsing="false" onAction="#addPressed" text="+" />
</children>
</HBox>
</children>
</VBox>

View File

@@ -4,16 +4,12 @@
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ProgressBar?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.control.Tooltip?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.RowConstraints?>
<?import org.controlsfx.control.CheckComboBox?>
<?import org.controlsfx.control.PrefixSelectionComboBox?>
<GridPane hgap="10.0" vgap="10.0" xmlns="http://javafx.com/javafx/22" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.github.gtache.autosubtitle.gui.fx.FXWorkController">
<columnConstraints>
@@ -22,9 +18,9 @@
<ColumnConstraints hgrow="SOMETIMES" />
</columnConstraints>
<rowConstraints>
<RowConstraints vgrow="SOMETIMES" />
<RowConstraints vgrow="NEVER" />
<RowConstraints vgrow="ALWAYS" />
<RowConstraints vgrow="SOMETIMES" />
<RowConstraints vgrow="NEVER" />
<RowConstraints vgrow="NEVER" />
</rowConstraints>
<children>
@@ -49,36 +45,8 @@
</Button>
</children>
</HBox>
<TableView fx:id="subtitlesTable" editable="true" GridPane.rowIndex="1">
<columns>
<TableColumn fx:id="startColumn" prefWidth="50.0" sortable="false" text="%work.table.column.from.label" />
<TableColumn fx:id="endColumn" prefWidth="50.0" sortable="false" text="%work.table.column.to.label" />
<TableColumn fx:id="textColumn" prefWidth="75.0" sortable="false" text="%work.table.column.text.label" />
</columns>
<columnResizePolicy>
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
</columnResizePolicy>
</TableView>
<HBox spacing="10.0" GridPane.rowIndex="2">
<children>
<Button fx:id="loadSubtitlesButton" mnemonicParsing="false" onAction="#loadSubtitlesPressed" text="%work.button.load.label" />
<Button fx:id="saveSubtitlesButton" mnemonicParsing="false" onAction="#saveSubtitlesPressed" text="%work.button.subtitles.save.label" />
<Button fx:id="resetButton" mnemonicParsing="false" onAction="#resetButtonPressed" text="%work.button.reset.label" />
<Button fx:id="addSubtitleButton" mnemonicParsing="false" onAction="#addSubtitlePressed" text="+" />
</children>
<GridPane.margin>
<Insets />
</GridPane.margin>
</HBox>
<fx:include fx:id="subtitles" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" source="subtitlesView.fxml" GridPane.columnIndex="0" GridPane.columnSpan="1" GridPane.rowIndex="0" GridPane.rowSpan="3" GridPane.vgrow="ALWAYS" />
<fx:include fx:id="media" source="mediaView.fxml" GridPane.columnIndex="1" GridPane.columnSpan="2147483647" GridPane.rowIndex="1" />
<HBox alignment="CENTER_LEFT" spacing="10.0">
<children>
<Label text="%work.language.label" />
<PrefixSelectionComboBox fx:id="languageCombobox" />
<Label text="%work.translate.label" />
<CheckComboBox fx:id="translationsCombobox" />
</children>
</HBox>
<Label fx:id="progressLabel" GridPane.columnIndex="1" GridPane.rowIndex="3" />
<HBox spacing="10.0" GridPane.columnIndex="2" GridPane.rowIndex="3">
<children>

View File

@@ -0,0 +1,43 @@
package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.gui.TimeFormatter;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
class TestColonTimeFormatter {
private final TimeFormatter timeFormatter = new ColonTimeFormatter();
@ParameterizedTest
@CsvSource({
"12:34:56,45296000",
"12:34,754000",
"01:02,62000",
"1:2,62000",
"01:02:03,3723000",
"1:2:3,3723000",
"00:00:03,3000",
"00:03,3000",
"1234:00:01,4442401000"
})
void testParse(final String time, final long millis) {
assertEquals(millis, timeFormatter.parse(time));
}
@ParameterizedTest
@CsvSource({
"45296000,12:34:56",
"45296521,12:34:56",
"754000,12:34",
"754620,12:34",
"62000,01:02",
"3723000,1:02:03",
"3000,00:03",
"4442401000,1234:00:01"
})
void testFormat(final long millis, final String time) {
assertEquals(time, timeFormatter.format(millis));
}
}

View File

@@ -0,0 +1,94 @@
package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.modules.gui.fx.DaggerResourceComponent;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.TabPane;
import javafx.stage.Stage;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.testfx.api.FxRobot;
import org.testfx.api.FxToolkit;
import org.testfx.util.WaitForAsyncUtils;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.ResourceBundle;
import java.util.concurrent.TimeoutException;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
class TestFXMainController extends FxRobot {
private static final ResourceBundle BUNDLE = DaggerResourceComponent.builder().build().getBundle();
private final FXMainModel model;
private final FXMainController controller;
private final Stage window;
TestFXMainController() throws TimeoutException {
this.model = spy(new FXMainModel());
this.controller = spy(new FXMainController(model));
this.window = FxToolkit.registerPrimaryStage();
FxToolkit.setupStage(w -> {
final var loader = new FXMLLoader(getClass().getResource("/com/github/gtache/autosubtitle/gui/fx/mainView.fxml"));
loader.setResources(BUNDLE);
loader.setControllerFactory(c -> {
if (c == FXMainController.class) {
return controller;
} else {
return mock(c);
}
});
try {
final Parent parent = loader.load();
final var scene = new Scene(parent);
w.setScene(scene);
} catch (final IOException e) {
throw new UncheckedIOException(e);
}
});
FxToolkit.showStage();
}
@AfterEach
void afterEach() throws Throwable {
WaitForAsyncUtils.waitForFxEvents();
WaitForAsyncUtils.checkException();
FxToolkit.cleanupStages();
}
@Test
void testModel() {
assertEquals(model, controller.model());
}
@Test
void testWindow() {
assertEquals(window, controller.window());
}
@Test
void testSelectTab() {
final var tabPane = lookup("#tabPane").queryAs(TabPane.class);
assertEquals(0, tabPane.getSelectionModel().getSelectedIndex());
interact(() -> controller.selectTab(1));
assertEquals(1, model.selectedTab());
assertEquals(1, tabPane.getSelectionModel().getSelectedIndex());
}
@Test
void testSelectedTabBinding() {
final var tabPane = lookup("#tabPane").queryAs(TabPane.class);
assertEquals(0, model.selectedTab());
interact(() -> controller.selectTab(1));
assertEquals(1, model.selectedTab());
assertEquals(1, tabPane.getSelectionModel().getSelectedIndex());
interact(() -> model.setSelectedTab(0));
assertEquals(0, tabPane.getSelectionModel().getSelectedIndex());
}
}

View File

@@ -0,0 +1,20 @@
package com.github.gtache.autosubtitle.gui.fx;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class TestFXMainModel {
private final FXMainModel model = new FXMainModel();
@Test
void testSelectTab() {
assertEquals(0, model.selectedTab());
assertEquals(0, model.selectedTabProperty().get());
final var newTab = 1;
model.setSelectedTab(newTab);
assertEquals(newTab, model.selectedTab());
assertEquals(newTab, model.selectedTabProperty().get());
}
}

View File

@@ -0,0 +1,41 @@
package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.Video;
import com.github.gtache.autosubtitle.subtitle.gui.fx.ObservableSubtitleImpl;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.Mockito.mock;
class TestFXMediaBinder {
@Test
void testCreateBindings() {
final var workModel = new FXWorkModel();
final var mediaModel = new FXMediaModel();
final var binder = new FXMediaBinder(workModel, mediaModel);
binder.createBindings();
assertNull(mediaModel.video());
assertNull(workModel.video());
final var video1 = mock(Video.class);
mediaModel.setVideo(video1);
assertEquals(video1, workModel.video());
final var video2 = mock(Video.class);
workModel.setVideo(video2);
assertEquals(video2, mediaModel.video());
final var subtitles = List.of(mock(ObservableSubtitleImpl.class));
assertEquals(List.of(), workModel.subtitles());
assertEquals(List.of(), mediaModel.subtitles());
workModel.subtitles().setAll(subtitles);
assertEquals(subtitles, mediaModel.subtitles());
}
}

View File

@@ -0,0 +1,297 @@
package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.Video;
import com.github.gtache.autosubtitle.gui.TimeFormatter;
import com.github.gtache.autosubtitle.impl.FileVideoImpl;
import com.github.gtache.autosubtitle.impl.VideoInfoImpl;
import com.github.gtache.autosubtitle.modules.gui.fx.DaggerResourceComponent;
import com.github.gtache.autosubtitle.modules.gui.fx.ResourceComponent;
import com.github.gtache.autosubtitle.subtitle.Subtitle;
import com.github.gtache.autosubtitle.subtitle.gui.fx.ObservableSubtitleImpl;
import com.github.gtache.autosubtitle.subtitle.gui.fx.SubtitleLabel;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Slider;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.StackPane;
import javafx.scene.media.MediaPlayer;
import javafx.scene.media.MediaView;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.testfx.api.FxRobot;
import org.testfx.api.FxToolkit;
import org.testfx.util.WaitForAsyncUtils;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URISyntaxException;
import java.nio.file.Paths;
import java.util.List;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import static java.util.Objects.requireNonNull;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class TestFXMediaController extends FxRobot {
private static final ResourceComponent RESOURCE_COMPONENT = DaggerResourceComponent.builder().build();
private static final ResourceBundle BUNDLE = RESOURCE_COMPONENT.getBundle();
private final FXMediaModel model;
private final FXMediaController controller;
private final FXMediaBinder binder;
private final Image playImage;
private final Image pauseImage;
private final Video video;
TestFXMediaController(@Mock final FXMediaBinder binder, @Mock final TimeFormatter timeFormatter) throws TimeoutException, URISyntaxException {
this.binder = requireNonNull(binder);
when(timeFormatter.format(anyLong(), anyLong())).then(i -> i.getArgument(0) + "/" + i.getArgument(1));
this.playImage = RESOURCE_COMPONENT.getPlayImage();
this.pauseImage = RESOURCE_COMPONENT.getPauseImage();
this.model = spy(new FXMediaModel());
this.controller = spy(new FXMediaController(model, binder, timeFormatter, playImage, pauseImage));
final var info = new VideoInfoImpl("mp4", 1280, 720, 30000L);
final var path = Paths.get(getClass().getResource("/com/github/gtache/autosubtitle/gui/fx/video.mp4").toURI());
this.video = new FileVideoImpl(path, info);
FxToolkit.registerPrimaryStage();
FxToolkit.setupStage(w -> {
final var loader = new FXMLLoader(getClass().getResource("/com/github/gtache/autosubtitle/gui/fx/mediaView.fxml"));
loader.setResources(BUNDLE);
loader.setControllerFactory(c -> controller);
try {
final Parent parent = loader.load();
final var scene = new Scene(parent);
w.setScene(scene);
} catch (final IOException e) {
throw new UncheckedIOException(e);
}
});
FxToolkit.showStage();
}
@AfterEach
void afterEach() throws Throwable {
WaitForAsyncUtils.waitForFxEvents();
WaitForAsyncUtils.checkException();
FxToolkit.cleanupStages();
}
@Test
void testInitialized() {
verify(binder).createBindings();
}
@Test
void testVolumeLabel() {
final var label = lookup("#volumeValueLabel").queryLabeled();
assertEquals("100", label.getText());
assertEquals(1, model.volume());
interact(() -> model.setVolume(0.5));
assertEquals("50", label.getText());
}
@Test
void testVolume() {
final var slider = lookup("#volumeSlider").queryAs(Slider.class);
assertEquals(100, slider.getValue());
assertEquals(1, model.volume());
interact(() -> model.setVolume(0.5));
assertEquals(50, slider.getValue());
interact(() -> slider.setValue(80));
assertEquals(0.8, model.volume());
}
@Test
void testVolumePlayer() {
loadVideo();
final var player = lookup("#videoView").queryAs(MediaView.class).getMediaPlayer();
assertEquals(1, model.volume());
assertEquals(1, player.getVolume());
interact(() -> model.setVolume(0.5));
assertEquals(0.5, player.getVolume());
}
@Test
void testPlayLabel() {
final var label = lookup("#playLabel").queryLabeled();
assertEquals("0/0", label.getText());
loadVideo();
assertEquals("0/30000", label.getText());
interact(() -> model.setPosition(15));
assertEquals("15/30000", label.getText());
}
@Test
void testPosition() {
final var slider = lookup("#playSlider").queryAs(Slider.class);
assertEquals(100.0, slider.getMax());
assertEquals(0, model.position());
assertEquals(0, slider.getValue());
loadVideo();
assertEquals(video.info().duration(), slider.getMax());
assertEquals(0, model.position());
assertEquals(0, slider.getValue());
interact(() -> model.setPosition(15));
assertEquals(15, slider.getValue());
interact(() -> slider.setValue(20));
assertEquals(20, model.position());
}
@Test
void testPositionPlayer() {
loadVideo();
final var player = lookup("#videoView").queryAs(MediaView.class).getMediaPlayer();
assertEquals(0, model.position());
assertEquals(0, player.getCurrentTime().toMillis());
clickOn("#playSlider");
assertNotEquals(0, player.getCurrentTime().toMillis());
}
@Test
void testPlayButtonState() {
final var button = lookup("#playButton").queryAs(Button.class);
assertTrue(button.isDisabled());
assertEquals(playImage, ((ImageView) button.getGraphic()).getImage());
loadVideo();
assertFalse(button.isDisabled());
assertEquals(playImage, ((ImageView) button.getGraphic()).getImage());
interact(() -> model.setIsPlaying(true));
assertEquals(pauseImage, ((ImageView) button.getGraphic()).getImage());
interact(() -> model.setIsPlaying(false));
assertEquals(playImage, ((ImageView) button.getGraphic()).getImage());
}
@Test
void testPlayPressed() {
final var button = lookup("#playButton").queryAs(Button.class);
loadVideo();
assertFalse(model.isPlaying());
clickOn(button);
assertTrue(model.isPlaying());
clickOn(button);
assertFalse(model.isPlaying());
}
@Test
void testPlay() {
loadVideo();
assertFalse(model.isPlaying());
interact(controller::play);
assertTrue(model.isPlaying());
interact(controller::play);
assertTrue(model.isPlaying());
interact(controller::pause);
assertFalse(model.isPlaying());
interact(controller::pause);
assertFalse(model.isPlaying());
}
@Test
void testPlayPlayer() {
loadVideo();
final var player = lookup("#videoView").queryAs(MediaView.class).getMediaPlayer();
assertFalse(model.isPlaying());
assertNotEquals(MediaPlayer.Status.PLAYING, player.getStatus());
interact(() -> model.setIsPlaying(true));
assertEquals(MediaPlayer.Status.PLAYING, player.getStatus());
interact(() -> model.setIsPlaying(false));
assertEquals(MediaPlayer.Status.PAUSED, player.getStatus());
}
@Test
void testSeek() {
loadVideo();
assertEquals(0, model.position());
interact(() -> controller.seek(15));
assertEquals(15, model.position());
}
@Test
void testRestart() {
loadVideo();
interact(() -> model.setPosition(model.duration()));
interact(() -> model.setIsPlaying(true));
interact(() -> model.setIsPlaying(false));
assertTrue(model.position() < 1000);
}
@Test
void testLoadNotFile() {
final var newVideo = mock(Video.class);
when(newVideo.info()).thenReturn(new VideoInfoImpl("mp4", 1, 1, 1));
interact(() -> model.videoProperty().set(newVideo));
assertNull(model.video());
}
@Test
void testSubtitles() {
loadVideo();
final var subtitle1 = new ObservableSubtitleImpl("subtitle1", 1000, 2000, null, null);
final var subtitle2 = new ObservableSubtitleImpl("subtitle2", 1000, 3000, null, null);
final var subtitle3 = new ObservableSubtitleImpl("subtitle3", 3000, 4000, null, null);
interact(() -> model.subtitles().setAll(List.of(subtitle1, subtitle2, subtitle3)));
interact(() -> controller.seek(1000));
final var pane = lookup("#stackPane").queryAs(StackPane.class);
var visibleSubtitles = getSubtitles(pane);
assertEquals(2, visibleSubtitles.size());
assertTrue(visibleSubtitles.contains(subtitle1));
assertTrue(visibleSubtitles.contains(subtitle2));
interact(() -> controller.seek(2000));
visibleSubtitles = getSubtitles(pane);
assertEquals(2, visibleSubtitles.size());
assertTrue(visibleSubtitles.contains(subtitle1));
assertTrue(visibleSubtitles.contains(subtitle2));
interact(() -> controller.seek(3000));
visibleSubtitles = getSubtitles(pane);
assertEquals(2, visibleSubtitles.size());
assertTrue(visibleSubtitles.contains(subtitle2));
assertTrue(visibleSubtitles.contains(subtitle3));
interact(() -> controller.seek(4000));
visibleSubtitles = getSubtitles(pane);
assertEquals(1, visibleSubtitles.size());
assertTrue(visibleSubtitles.contains(subtitle3));
}
private static Set<Subtitle> getSubtitles(final StackPane stackPane) {
return stackPane.getChildren().stream().filter(SubtitleLabel.class::isInstance).map(SubtitleLabel.class::cast).map(SubtitleLabel::subtitle).collect(Collectors.toSet());
}
@Test
void testModel() {
assertEquals(model, controller.model());
}
private void loadVideo() {
interact(() -> model.setVideo(video));
waitForPlayerReady();
}
private void waitForPlayerReady() {
if (lookup("#videoView").queryAs(MediaView.class).getMediaPlayer().getStatus() == MediaPlayer.Status.UNKNOWN) {
sleep(100L);
waitForPlayerReady();
}
}
}

View File

@@ -0,0 +1,71 @@
package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.Video;
import com.github.gtache.autosubtitle.VideoInfo;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class TestFXMediaModel {
private static final double DEFAULT_VOLUME = 1.0d;
private final FXMediaModel model = new FXMediaModel();
@Test
void testVideo() {
assertNull(model.video());
assertNull(model.videoProperty().get());
final var video = mock(Video.class);
model.setVideo(video);
assertEquals(video, model.video());
assertEquals(video, model.videoProperty().get());
}
@Test
void testVolume() {
assertEquals(DEFAULT_VOLUME, model.volume());
assertEquals(DEFAULT_VOLUME, model.volumeProperty().get());
final var volume = 0.5d;
model.setVolume(volume);
assertEquals(volume, model.volume());
assertEquals(volume, model.volumeProperty().get());
}
@Test
void testIsPlaying() {
assertFalse(model.isPlaying());
assertFalse(model.isPlayingProperty().get());
model.setIsPlaying(true);
assertTrue(model.isPlaying());
assertTrue(model.isPlayingProperty().get());
}
@Test
void testPosition() {
assertEquals(0L, model.position());
assertEquals(0L, model.positionProperty().get());
final var position = 100L;
model.setPosition(position);
assertEquals(position, model.position());
assertEquals(position, model.positionProperty().get());
}
@Test
void testDuration() {
assertEquals(0L, model.duration());
assertEquals(0L, model.durationProperty().get());
final var video = mock(Video.class);
final var info = mock(VideoInfo.class);
final var duration = 100L;
when(video.info()).thenReturn(info);
when(info.duration()).thenReturn(duration);
model.setVideo(video);
assertEquals(duration, model.duration());
assertEquals(duration, model.durationProperty().get());
model.setVideo(null);
assertEquals(0L, model.duration());
assertEquals(0L, model.durationProperty().get());
}
}

View File

@@ -0,0 +1,80 @@
package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.Translator;
import com.github.gtache.autosubtitle.VideoConverter;
import com.github.gtache.autosubtitle.VideoLoader;
import com.github.gtache.autosubtitle.gui.TimeFormatter;
import com.github.gtache.autosubtitle.modules.gui.fx.DaggerResourceComponent;
import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverter;
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractionModelProvider;
import com.github.gtache.autosubtitle.subtitle.extractor.SubtitleExtractor;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.junit.jupiter.api.AfterEach;
import org.mockito.Mock;
import org.testfx.api.FxRobot;
import org.testfx.api.FxToolkit;
import org.testfx.util.WaitForAsyncUtils;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.concurrent.TimeoutException;
import static java.util.Objects.requireNonNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
class TestFXParametersController extends FxRobot {
private static final ResourceBundle BUNDLE = DaggerResourceComponent.builder().build().getBundle();
private final FXParametersModel model;
private final FXParametersController controller;
private final FXWorkBinder binder;
private final SubtitleExtractor extractor;
private final Map<String, SubtitleConverter> subtitleConverters;
private final VideoLoader videoLoader;
private final VideoConverter videoConverter;
private final Translator translator;
private final TimeFormatter timeFormatter;
private final Stage window;
TestFXParametersController(@Mock final SubtitleExtractor extractor, @Mock final SubtitleConverter subtitleConverter, @Mock final VideoLoader videoLoader,
@Mock final VideoConverter videoConverter, @Mock final Translator translator, @Mock final TimeFormatter timeFormatter,
@Mock final FXWorkBinder binder) throws TimeoutException {
this.extractor = requireNonNull(extractor);
this.subtitleConverters = Map.of("srt", requireNonNull(subtitleConverter));
this.videoLoader = requireNonNull(videoLoader);
this.videoConverter = requireNonNull(videoConverter);
this.translator = requireNonNull(translator);
this.timeFormatter = requireNonNull(timeFormatter);
this.model = spy(new FXParametersModel(mock(ExtractionModelProvider.class), "Arial", 12));
this.binder = requireNonNull(binder);
this.window = FxToolkit.registerPrimaryStage();
this.controller = spy(FXParametersController.class);
FxToolkit.setupStage(w -> {
final var loader = new FXMLLoader(getClass().getResource("/com/github/gtache/autosubtitle/gui/fx/parametersView.fxml"));
loader.setResources(BUNDLE);
loader.setControllerFactory(c -> controller);
try {
final Parent parent = loader.load();
final var scene = new Scene(parent);
w.setScene(scene);
} catch (final IOException e) {
throw new UncheckedIOException(e);
}
});
FxToolkit.showStage();
}
@AfterEach
void afterEach() throws Throwable {
WaitForAsyncUtils.waitForFxEvents();
WaitForAsyncUtils.checkException();
FxToolkit.cleanupStages();
}
}

View File

@@ -0,0 +1,96 @@
package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.subtitle.OutputFormat;
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractionModel;
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractionModelProvider;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import java.util.Objects;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class TestFXParametersModel {
private static final String DEFAULT_FONT_FAMILY = "Arial";
private static final int DEFAULT_FONT_SIZE = 12;
private final List<ExtractionModel> availableExtractionModels;
private final ExtractionModel defaultExtractionModel;
private final ExtractionModelProvider provider;
private final FXParametersModel model;
TestFXParametersModel(@Mock final ExtractionModelProvider extractionModelProvider,
@Mock final ExtractionModel defaultExtractionModel,
@Mock final ExtractionModel extractionModel) {
this.provider = Objects.requireNonNull(extractionModelProvider);
this.defaultExtractionModel = Objects.requireNonNull(defaultExtractionModel);
when(provider.getDefaultExtractionModel()).thenReturn(defaultExtractionModel);
this.availableExtractionModels = List.of(defaultExtractionModel, extractionModel);
when(provider.getAvailableExtractionModels()).thenReturn(availableExtractionModels);
model = new FXParametersModel(extractionModelProvider, DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE);
}
@Test
void testAvailableExtractionModels() {
assertEquals(availableExtractionModels, model.availableExtractionModels());
assertThrows(UnsupportedOperationException.class, () -> model.availableExtractionModels().clear());
}
@Test
void testExtractionModel() {
assertEquals(defaultExtractionModel, model.extractionModel());
assertEquals(defaultExtractionModel, model.extractionModelProperty().get());
final var otherModel = mock(ExtractionModel.class);
model.setExtractionModel(otherModel);
assertEquals(otherModel, model.extractionModel());
assertEquals(otherModel, model.extractionModelProperty().get());
}
@Test
void testAvailableOutputFormats() {
assertEquals(List.of(OutputFormat.SRT), model.availableOutputFormats());
assertThrows(UnsupportedOperationException.class, () -> model.availableOutputFormats().clear());
}
@Test
void testOutputFormat() {
assertEquals(OutputFormat.SRT, model.outputFormat());
assertEquals(OutputFormat.SRT, model.outputFormatProperty().get());
model.setOutputFormat(OutputFormat.ASS);
assertEquals(OutputFormat.ASS, model.outputFormat());
assertEquals(OutputFormat.ASS, model.outputFormatProperty().get());
}
@Test
void testAvailableFontFamilies() {
assertEquals(List.of(DEFAULT_FONT_FAMILY), model.availableFontFamilies());
assertThrows(UnsupportedOperationException.class, () -> model.availableFontFamilies().clear());
}
@Test
void testFontFamily() {
assertEquals(DEFAULT_FONT_FAMILY, model.fontFamily());
assertEquals(DEFAULT_FONT_FAMILY, model.fontFamilyProperty().get());
final var fontFamily = DEFAULT_FONT_FAMILY + " A";
model.setFontFamily(fontFamily);
assertEquals(fontFamily, model.fontFamily());
assertEquals(fontFamily, model.fontFamilyProperty().get());
}
@Test
void testFontSize() {
assertEquals(DEFAULT_FONT_SIZE, model.fontSize());
assertEquals(DEFAULT_FONT_SIZE, model.fontSizeProperty().get());
final var fontSize = DEFAULT_FONT_SIZE + 2;
model.setFontSize(fontSize);
assertEquals(fontSize, model.fontSize());
assertEquals(fontSize, model.fontSizeProperty().get());
}
}

View File

@@ -0,0 +1,78 @@
package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.Translator;
import com.github.gtache.autosubtitle.VideoConverter;
import com.github.gtache.autosubtitle.VideoLoader;
import com.github.gtache.autosubtitle.gui.TimeFormatter;
import com.github.gtache.autosubtitle.modules.gui.fx.DaggerResourceComponent;
import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverter;
import com.github.gtache.autosubtitle.subtitle.extractor.SubtitleExtractor;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.junit.jupiter.api.AfterEach;
import org.mockito.Mock;
import org.testfx.api.FxRobot;
import org.testfx.api.FxToolkit;
import org.testfx.util.WaitForAsyncUtils;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.concurrent.TimeoutException;
import static java.util.Objects.requireNonNull;
import static org.mockito.Mockito.spy;
class TestFXSetupController extends FxRobot {
private static final ResourceBundle BUNDLE = DaggerResourceComponent.builder().build().getBundle();
private final FXSetupModel model;
private final FXSetupController controller;
private final FXWorkBinder binder;
private final SubtitleExtractor extractor;
private final Map<String, SubtitleConverter> subtitleConverters;
private final VideoLoader videoLoader;
private final VideoConverter videoConverter;
private final Translator translator;
private final TimeFormatter timeFormatter;
private final Stage window;
TestFXSetupController(@Mock final SubtitleExtractor extractor, @Mock final SubtitleConverter subtitleConverter, @Mock final VideoLoader videoLoader,
@Mock final VideoConverter videoConverter, @Mock final Translator translator, @Mock final TimeFormatter timeFormatter,
@Mock final FXWorkBinder binder) throws TimeoutException {
this.extractor = requireNonNull(extractor);
this.subtitleConverters = Map.of("srt", requireNonNull(subtitleConverter));
this.videoLoader = requireNonNull(videoLoader);
this.videoConverter = requireNonNull(videoConverter);
this.translator = requireNonNull(translator);
this.timeFormatter = requireNonNull(timeFormatter);
this.model = spy(new FXSetupModel());
this.binder = requireNonNull(binder);
this.window = FxToolkit.registerPrimaryStage();
this.controller = spy(FXSetupController.class);
FxToolkit.setupStage(w -> {
final var loader = new FXMLLoader(getClass().getResource("/com/github/gtache/autosubtitle/gui/fx/setupView.fxml"));
loader.setResources(BUNDLE);
loader.setControllerFactory(c -> controller);
try {
final Parent parent = loader.load();
final var scene = new Scene(parent);
w.setScene(scene);
} catch (final IOException e) {
throw new UncheckedIOException(e);
}
});
FxToolkit.showStage();
}
@AfterEach
void afterEach() throws Throwable {
WaitForAsyncUtils.waitForFxEvents();
WaitForAsyncUtils.checkException();
FxToolkit.cleanupStages();
}
}

View File

@@ -0,0 +1,93 @@
package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.setup.SetupStatus;
import org.junit.jupiter.api.Test;
import static com.github.gtache.autosubtitle.setup.SetupStatus.ERRORED;
import static org.junit.jupiter.api.Assertions.assertEquals;
class TestFXSetupModel {
private final FXSetupModel model = new FXSetupModel();
@Test
void testSubtitleExtractorStatus() {
assertEquals(ERRORED, model.subtitleExtractorStatus());
assertEquals(ERRORED, model.subtitleExtractorStatusProperty().get());
model.setSubtitleExtractorStatus(SetupStatus.SYSTEM_INSTALLED);
assertEquals(SetupStatus.SYSTEM_INSTALLED, model.subtitleExtractorStatus());
assertEquals(SetupStatus.SYSTEM_INSTALLED, model.subtitleExtractorStatusProperty().get());
}
@Test
void testSubtitleExtractorSetupProgress() {
assertEquals(-2, model.subtitleExtractorSetupProgress());
assertEquals(-2, model.subtitleExtractorSetupProgressProperty().get());
model.setSubtitleExtractorSetupProgress(0.5);
assertEquals(0.5, model.subtitleExtractorSetupProgress());
assertEquals(0.5, model.subtitleExtractorSetupProgressProperty().get());
}
@Test
void testSubtitleExtractorSetupProgressLabel() {
assertEquals("", model.subtitleExtractorSetupProgressLabel());
assertEquals("", model.subtitleExtractorSetupProgressLabelProperty().get());
model.setSubtitleExtractorSetupProgressLabel("test");
assertEquals("test", model.subtitleExtractorSetupProgressLabel());
assertEquals("test", model.subtitleExtractorSetupProgressLabelProperty().get());
}
@Test
void testVideoConverterStatus() {
assertEquals(ERRORED, model.videoConverterStatus());
assertEquals(ERRORED, model.videoConverterStatusProperty().get());
model.setVideoConverterStatus(SetupStatus.SYSTEM_INSTALLED);
assertEquals(SetupStatus.SYSTEM_INSTALLED, model.videoConverterStatus());
assertEquals(SetupStatus.SYSTEM_INSTALLED, model.videoConverterStatusProperty().get());
}
@Test
void testVideoConverterSetupProgress() {
assertEquals(-2, model.videoConverterSetupProgress());
assertEquals(-2, model.videoConverterSetupProgressProperty().get());
model.setVideoConverterSetupProgress(0.5);
assertEquals(0.5, model.videoConverterSetupProgress());
assertEquals(0.5, model.videoConverterSetupProgressProperty().get());
}
@Test
void testVideoConverterSetupProgressLabel() {
assertEquals("", model.videoConverterSetupProgressLabel());
assertEquals("", model.videoConverterSetupProgressLabelProperty().get());
model.setVideoConverterSetupProgressLabel("test");
assertEquals("test", model.videoConverterSetupProgressLabel());
assertEquals("test", model.videoConverterSetupProgressLabelProperty().get());
}
@Test
void testTranslatorStatus() {
assertEquals(ERRORED, model.translatorStatus());
assertEquals(ERRORED, model.translatorStatusProperty().get());
model.setTranslatorStatus(SetupStatus.SYSTEM_INSTALLED);
assertEquals(SetupStatus.SYSTEM_INSTALLED, model.translatorStatus());
assertEquals(SetupStatus.SYSTEM_INSTALLED, model.translatorStatusProperty().get());
}
@Test
void testTranslatorSetupProgress() {
assertEquals(-2, model.translatorSetupProgress());
assertEquals(-2, model.translatorSetupProgressProperty().get());
model.setTranslatorSetupProgress(0.5);
assertEquals(0.5, model.translatorSetupProgress());
assertEquals(0.5, model.translatorSetupProgressProperty().get());
}
@Test
void testTranslatorSetupProgressLabel() {
assertEquals("", model.translatorSetupProgressLabel());
assertEquals("", model.translatorSetupProgressLabelProperty().get());
model.setTranslatorSetupProgressLabel("test");
assertEquals("test", model.translatorSetupProgressLabel());
assertEquals("test", model.translatorSetupProgressLabelProperty().get());
}
}

View File

@@ -0,0 +1,81 @@
package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.Translator;
import com.github.gtache.autosubtitle.VideoConverter;
import com.github.gtache.autosubtitle.VideoLoader;
import com.github.gtache.autosubtitle.gui.TimeFormatter;
import com.github.gtache.autosubtitle.modules.gui.fx.DaggerResourceComponent;
import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverter;
import com.github.gtache.autosubtitle.subtitle.extractor.SubtitleExtractor;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.testfx.api.FxRobot;
import org.testfx.api.FxToolkit;
import org.testfx.util.WaitForAsyncUtils;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.concurrent.TimeoutException;
import static java.util.Objects.requireNonNull;
import static org.mockito.Mockito.spy;
@ExtendWith(MockitoExtension.class)
class TestFXSubtitlesController extends FxRobot {
private static final ResourceBundle BUNDLE = DaggerResourceComponent.builder().build().getBundle();
private final FXSubtitlesModel model;
private final FXSubtitlesController controller;
private final FXSubtitlesBinder binder;
private final SubtitleExtractor extractor;
private final Map<String, SubtitleConverter> subtitleConverters;
private final VideoLoader videoLoader;
private final VideoConverter videoConverter;
private final Translator translator;
private final TimeFormatter timeFormatter;
private final Stage window;
TestFXSubtitlesController(@Mock final SubtitleExtractor extractor, @Mock final SubtitleConverter subtitleConverter, @Mock final VideoLoader videoLoader,
@Mock final VideoConverter videoConverter, @Mock final Translator translator, @Mock final TimeFormatter timeFormatter,
@Mock final FXSubtitlesBinder binder) throws TimeoutException {
this.extractor = requireNonNull(extractor);
this.subtitleConverters = Map.of("srt", requireNonNull(subtitleConverter));
this.videoLoader = requireNonNull(videoLoader);
this.videoConverter = requireNonNull(videoConverter);
this.translator = requireNonNull(translator);
this.timeFormatter = requireNonNull(timeFormatter);
this.model = spy(new FXSubtitlesModel());
this.binder = requireNonNull(binder);
this.window = FxToolkit.registerPrimaryStage();
this.controller = spy(FXSubtitlesController.class);
FxToolkit.setupStage(w -> {
final var loader = new FXMLLoader(getClass().getResource("/com/github/gtache/autosubtitle/gui/fx/workView.fxml"));
loader.setResources(BUNDLE);
loader.setControllerFactory(c -> controller);
try {
final Parent parent = loader.load();
final var scene = new Scene(parent);
w.setScene(scene);
} catch (final IOException e) {
throw new UncheckedIOException(e);
}
});
FxToolkit.showStage();
}
@AfterEach
void afterEach() throws Throwable {
WaitForAsyncUtils.waitForFxEvents();
WaitForAsyncUtils.checkException();
FxToolkit.cleanupStages();
}
}

View File

@@ -0,0 +1,52 @@
package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.Language;
import com.github.gtache.autosubtitle.subtitle.gui.fx.ObservableSubtitleImpl;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.mock;
class TestFXSubtitlesModel {
private final FXSubtitlesModel model = new FXSubtitlesModel();
@Test
void testSelectedSubtitle() {
assertNull(model.selectedSubtitle());
assertNull(model.selectedSubtitleProperty().get());
final var subtitle = mock(ObservableSubtitleImpl.class);
model.setSelectedSubtitle(subtitle);
assertEquals(subtitle, model.selectedSubtitle());
assertEquals(subtitle, model.selectedSubtitleProperty().get());
}
@Test
void testAvailableVideoLanguages() {
final var expected = Arrays.stream(Language.values())
.sorted((o1, o2) -> {
if (o1 == Language.AUTO) {
return -1;
} else if (o2 == Language.AUTO) {
return 1;
} else {
return o1.englishName().compareTo(o2.englishName());
}
}).toList();
assertEquals(expected, model.availableVideoLanguages());
}
@Test
void testAvailableTranslationsLanguage() {
final var expected = Arrays.stream(Language.values())
.filter(l -> l != Language.AUTO).toList();
assertEquals(expected, model.availableTranslationsLanguage());
assertThrows(UnsupportedOperationException.class, () -> model.availableTranslationsLanguage().setAll(Language.DE));
model.setVideoLanguage(Language.DE);
final var expected2 = Arrays.stream(Language.values())
.filter(l -> l != Language.AUTO && l != Language.DE).toList();
assertEquals(expected2, model.availableTranslationsLanguage());
}
}

View File

@@ -0,0 +1,28 @@
package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractionModel;
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractionModelProvider;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.mock;
class TestFXWorkBinder {
@Test
void testBindings() {
final var workModel = new FXWorkModel();
final var parametersModel = new FXParametersModel(mock(ExtractionModelProvider.class), "Arial", 12);
final var binder = new FXWorkBinder(workModel, parametersModel);
binder.createBindings();
assertNull(workModel.extractionModel());
assertNull(parametersModel.extractionModel());
final var extractionModel = mock(ExtractionModel.class);
parametersModel.setExtractionModel(extractionModel);
assertEquals(extractionModel, workModel.extractionModel());
assertEquals(extractionModel, parametersModel.extractionModel());
assertThrows(RuntimeException.class, () -> workModel.setExtractionModel(mock(ExtractionModel.class)));
}
}

View File

@@ -0,0 +1,70 @@
package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.VideoConverter;
import com.github.gtache.autosubtitle.VideoLoader;
import com.github.gtache.autosubtitle.modules.gui.fx.DaggerResourceComponent;
import com.github.gtache.autosubtitle.subtitle.extractor.SubtitleExtractor;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.testfx.api.FxRobot;
import org.testfx.api.FxToolkit;
import org.testfx.util.WaitForAsyncUtils;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.ResourceBundle;
import java.util.concurrent.TimeoutException;
import static java.util.Objects.requireNonNull;
import static org.mockito.Mockito.spy;
@ExtendWith(MockitoExtension.class)
class TestFXWorkController extends FxRobot {
private static final ResourceBundle BUNDLE = DaggerResourceComponent.builder().build().getBundle();
private final FXWorkModel model;
private final FXWorkController controller;
private final FXWorkBinder binder;
private final SubtitleExtractor extractor;
private final VideoLoader videoLoader;
private final VideoConverter videoConverter;
private final Stage window;
TestFXWorkController(@Mock final SubtitleExtractor extractor, @Mock final VideoLoader videoLoader,
@Mock final VideoConverter videoConverter, @Mock final FXWorkBinder binder) throws TimeoutException {
this.extractor = requireNonNull(extractor);
this.videoLoader = requireNonNull(videoLoader);
this.videoConverter = requireNonNull(videoConverter);
this.model = spy(new FXWorkModel());
this.binder = requireNonNull(binder);
this.window = FxToolkit.registerPrimaryStage();
this.controller = spy(new FXWorkController(model, binder, extractor, videoLoader, videoConverter));
FxToolkit.setupStage(w -> {
final var loader = new FXMLLoader(getClass().getResource("/com/github/gtache/autosubtitle/gui/fx/workView.fxml"));
loader.setResources(BUNDLE);
loader.setControllerFactory(c -> controller);
try {
final Parent parent = loader.load();
final var scene = new Scene(parent);
w.setScene(scene);
} catch (final IOException e) {
throw new UncheckedIOException(e);
}
});
FxToolkit.showStage();
}
@AfterEach
void afterEach() throws Throwable {
WaitForAsyncUtils.waitForFxEvents();
WaitForAsyncUtils.checkException();
FxToolkit.cleanupStages();
}
}

View File

@@ -0,0 +1,53 @@
package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.Video;
import com.github.gtache.autosubtitle.gui.WorkStatus;
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractionModel;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.Mockito.mock;
class TestFXWorkModel {
private final FXWorkModel model = new FXWorkModel();
@Test
void testVideo() {
assertNull(model.video());
assertNull(model.videoProperty().get());
final var video = mock(Video.class);
model.setVideo(video);
assertEquals(video, model.video());
assertEquals(video, model.videoProperty().get());
}
@Test
void testExtractionModel() {
assertNull(model.extractionModel());
assertNull(model.extractionModelProperty().get());
final var extractionModel = mock(ExtractionModel.class);
model.setExtractionModel(extractionModel);
assertEquals(extractionModel, model.extractionModel());
assertEquals(extractionModel, model.extractionModelProperty().get());
}
@Test
void testWorkStatus() {
assertEquals(WorkStatus.IDLE, model.status());
assertEquals(WorkStatus.IDLE, model.statusProperty().get());
model.setStatus(WorkStatus.TRANSLATING);
assertEquals(WorkStatus.TRANSLATING, model.status());
assertEquals(WorkStatus.TRANSLATING, model.statusProperty().get());
}
@Test
void testProgress() {
assertEquals(-1.0, model.progress());
assertEquals(-1.0, model.progressProperty().get());
model.setProgress(0.5);
assertEquals(0.5, model.progress());
assertEquals(0.5, model.progressProperty().get());
}
}

View File

@@ -0,0 +1,29 @@
package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.Language;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
class TestLanguageStringConverter {
private final LanguageStringConverter converter = new LanguageStringConverter();
@Test
void testToString() {
assertEquals(Language.EN.englishName(), converter.toString(Language.EN));
}
@Test
void testToStringNull() {
assertEquals("", converter.toString(null));
}
@Test
void testFromStringNull() {
assertNull(converter.fromString("english"));
assertNull(converter.fromString(null));
}
}

View File

@@ -0,0 +1,51 @@
package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.gui.TimeFormatter;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Objects;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class TestTimeStringConverter {
private static final long LONG = 1L;
private static final String STRING = "test";
private final TimeFormatter timeFormatter;
private final TimeStringConverter converter;
TestTimeStringConverter(@Mock final TimeFormatter timeFormatter) {
this.timeFormatter = Objects.requireNonNull(timeFormatter);
when(timeFormatter.format(LONG)).thenReturn(STRING);
when(timeFormatter.parse(STRING)).thenReturn(LONG);
this.converter = new TimeStringConverter(timeFormatter);
}
@Test
void testToString() {
assertEquals(STRING, converter.toString(LONG));
verify(timeFormatter).format(LONG);
}
@Test
void testToStringNull() {
assertEquals("", converter.toString(null));
verifyNoInteractions(timeFormatter);
}
@Test
void testFromString() {
assertEquals(LONG, converter.fromString(STRING));
verify(timeFormatter).parse(STRING);
}
@Test
void testFromStringNull() {
assertEquals(0L, converter.fromString(null));
verifyNoInteractions(timeFormatter);
}
}

View File

@@ -0,0 +1,21 @@
package com.github.gtache.autosubtitle.modules.gui.fx;
import com.github.gtache.autosubtitle.modules.gui.impl.GuiCoreModule;
import com.github.gtache.autosubtitle.modules.gui.impl.Pause;
import com.github.gtache.autosubtitle.modules.gui.impl.Play;
import dagger.Component;
import javafx.scene.image.Image;
import java.util.ResourceBundle;
@Component(modules = {FXModule.class, GuiCoreModule.class})
public interface ResourceComponent {
ResourceBundle getBundle();
@Play
Image getPlayImage();
@Pause
Image getPauseImage();
}

View File

@@ -0,0 +1,43 @@
package com.github.gtache.autosubtitle.modules.gui.fx;
import com.github.gtache.autosubtitle.gui.fx.FXMainController;
import com.github.gtache.autosubtitle.gui.fx.FXMediaController;
import com.github.gtache.autosubtitle.gui.fx.FXParametersController;
import com.github.gtache.autosubtitle.gui.fx.FXSetupController;
import com.github.gtache.autosubtitle.gui.fx.FXSubtitlesController;
import com.github.gtache.autosubtitle.gui.fx.FXWorkController;
import javafx.fxml.FXMLLoader;
import javafx.scene.image.Image;
import org.junit.jupiter.api.Test;
import java.util.ResourceBundle;
import java.util.prefs.Preferences;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.mockito.Mockito.mock;
class TestFXModule {
private static final byte[] IMAGE = new byte[0];
@Test
void testFXMLLoader() {
assertInstanceOf(FXMLLoader.class, FXModule.providesFXMLLoader(mock(FXMainController.class), mock(FXMediaController.class),
mock(FXParametersController.class), mock(FXSetupController.class), mock(FXSubtitlesController.class), mock(FXWorkController.class), mock(ResourceBundle.class)));
}
@Test
void testPlayImage() {
assertInstanceOf(Image.class, FXModule.providesPlayImage(IMAGE));
}
@Test
void testPauseImage() {
assertInstanceOf(Image.class, FXModule.providesPauseImage(IMAGE));
}
@Test
void testPreferences() {
assertInstanceOf(Preferences.class, FXModule.providesPreferences());
}
}

View File

@@ -0,0 +1,106 @@
package com.github.gtache.autosubtitle.subtitle.gui.fx;
import com.github.gtache.autosubtitle.subtitle.Bounds;
import com.github.gtache.autosubtitle.subtitle.Font;
import com.github.gtache.autosubtitle.subtitle.impl.SubtitleImpl;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Objects;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
@ExtendWith(MockitoExtension.class)
class TestObservableSubtitleImpl {
private final String content;
private final long start;
private final long end;
private final Font font;
private final Bounds bounds;
private final ObservableSubtitleImpl subtitle;
TestObservableSubtitleImpl(@Mock final Font font, @Mock final Bounds bounds) {
this.content = "content";
this.start = 1000L;
this.end = 3000L;
this.font = Objects.requireNonNull(font);
this.bounds = Objects.requireNonNull(bounds);
this.subtitle = new ObservableSubtitleImpl();
}
@Test
void testContent() {
assertEquals("", subtitle.content());
assertEquals("", subtitle.contentProperty().get());
subtitle.setContent(content);
assertEquals(content, subtitle.content());
assertEquals(content, subtitle.contentProperty().get());
}
@Test
void testStart() {
assertEquals(0L, subtitle.start());
assertEquals(0L, subtitle.startProperty().get());
subtitle.setStart(start);
assertEquals(start, subtitle.start());
assertEquals(start, subtitle.startProperty().get());
}
@Test
void testEnd() {
assertEquals(0L, subtitle.end());
assertEquals(0L, subtitle.endProperty().get());
subtitle.setEnd(end);
assertEquals(end, subtitle.end());
assertEquals(end, subtitle.endProperty().get());
}
@Test
void testFont() {
assertNull(subtitle.font());
assertNull(subtitle.fontProperty().get());
subtitle.setFont(font);
assertEquals(font, subtitle.font());
assertEquals(font, subtitle.fontProperty().get());
}
@Test
void testBounds() {
assertNull(subtitle.bounds());
assertNull(subtitle.boundsProperty().get());
subtitle.setBounds(bounds);
assertEquals(bounds, subtitle.bounds());
assertEquals(bounds, subtitle.boundsProperty().get());
}
@Test
void testStringConstructor() {
final var subtitle = new ObservableSubtitleImpl(content);
assertEquals(content, subtitle.content());
}
@Test
void testWholeConstructor() {
final var subtitle = new ObservableSubtitleImpl(content, start, end, font, bounds);
assertEquals(content, subtitle.content());
assertEquals(start, subtitle.start());
assertEquals(end, subtitle.end());
assertEquals(font, subtitle.font());
assertEquals(bounds, subtitle.bounds());
}
@Test
void testCopyConstructor() {
final var originalSubtitle = new SubtitleImpl(content, start, end, font, bounds);
final var subtitle = new ObservableSubtitleImpl(originalSubtitle);
assertEquals(content, subtitle.content());
assertEquals(start, subtitle.start());
assertEquals(end, subtitle.end());
assertEquals(font, subtitle.font());
assertEquals(bounds, subtitle.bounds());
}
}

View File

@@ -0,0 +1,53 @@
package com.github.gtache.autosubtitle.subtitle.gui.fx;
import com.github.gtache.autosubtitle.subtitle.Subtitle;
import javafx.application.Platform;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.testfx.api.FxToolkit;
import java.util.Objects;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class TestSubtitleLabel {
@BeforeAll
static void startFX() {
if (!FxToolkit.isFXApplicationThreadRunning()) {
Platform.startup(() -> {
});
}
}
private final String content;
private final Subtitle subtitle;
private final SubtitleLabel label;
TestSubtitleLabel(@Mock final Subtitle subtitle) {
this.subtitle = Objects.requireNonNull(subtitle);
this.content = "content";
when(subtitle.content()).thenReturn(content);
this.label = new SubtitleLabel(subtitle);
}
@Test
void testGetters() {
assertEquals(content, label.getText());
assertEquals(subtitle, label.subtitle());
}
@Test
void testDrag() {
final var x = 10.0;
final var y = 20.0;
label.setDragged(x, y);
assertEquals(x, label.getDraggedX());
assertEquals(y, label.getDraggedY());
}
}

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="INFO">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>

View File

@@ -22,7 +22,7 @@
</dependency>
<dependency>
<groupId>com.github.gtache.autosubtitle</groupId>
<artifactId>autosubtitle-whisper</artifactId>
<artifactId>autosubtitle-whisperx</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>

View File

@@ -1,11 +1,14 @@
package com.github.gtache.autosubtitle.run;
package com.github.gtache.autosubtitle.gui.run;
import com.github.gtache.autosubtitle.modules.run.DaggerRunComponent;
import com.github.gtache.autosubtitle.modules.gui.run.DaggerRunComponent;
import javafx.application.Application;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
/**
* Main GUI class
*/
public final class Main extends Application {
@Override

View File

@@ -1,4 +1,4 @@
package com.github.gtache.autosubtitle.modules.run;
package com.github.gtache.autosubtitle.modules.gui.run;
import com.github.gtache.autosubtitle.Language;
import com.github.gtache.autosubtitle.Translator;
@@ -9,8 +9,6 @@ import com.github.gtache.autosubtitle.subtitle.SubtitleCollection;
import dagger.Module;
import dagger.Provides;
import javax.inject.Singleton;
/**
* Module for missing components
*/
@@ -22,9 +20,8 @@ public abstract class MissingComponentsModule {
}
@Provides
@Singleton
static Translator providesTranslator() {
return new Translator() {
return new Translator<>() {
@Override
public Language getLanguage(final String text) {
return Language.getDefault();
@@ -41,14 +38,13 @@ public abstract class MissingComponentsModule {
}
@Override
public SubtitleCollection translate(final SubtitleCollection collection, final Language to) {
return collection;
public SubtitleCollection<Subtitle> translate(final SubtitleCollection<?> collection, final Language to) {
return (SubtitleCollection<Subtitle>) collection;
}
};
}
@Provides
@Singleton
@TranslatorSetup
static SetupManager providesTranslatorSetupManager() {
return new NoOpSetupManager();

View File

@@ -1,4 +1,4 @@
package com.github.gtache.autosubtitle.modules.run;
package com.github.gtache.autosubtitle.modules.gui.run;
import com.github.gtache.autosubtitle.setup.SetupException;
import com.github.gtache.autosubtitle.setup.SetupListener;

View File

@@ -1,10 +1,10 @@
package com.github.gtache.autosubtitle.modules.run;
package com.github.gtache.autosubtitle.modules.gui.run;
import com.github.gtache.autosubtitle.modules.ffmpeg.FFmpegModule;
import com.github.gtache.autosubtitle.modules.gui.fx.FXModule;
import com.github.gtache.autosubtitle.modules.gui.impl.GuiCoreModule;
import com.github.gtache.autosubtitle.modules.impl.CoreModule;
import com.github.gtache.autosubtitle.modules.whisper.WhisperModule;
import com.github.gtache.autosubtitle.modules.whisperx.WhisperXModule;
import dagger.Component;
import javafx.fxml.FXMLLoader;
@@ -15,7 +15,7 @@ import javax.inject.Singleton;
*/
@Singleton
@Component(modules = {CoreModule.class, GuiCoreModule.class, FXModule.class, FFmpegModule.class,
WhisperModule.class, MissingComponentsModule.class})
WhisperXModule.class, MissingComponentsModule.class})
public interface RunComponent {
/**

View File

@@ -4,7 +4,9 @@
module com.github.gtache.autosubtitle.run {
requires com.github.gtache.autosubtitle.ffmpeg;
requires com.github.gtache.autosubtitle.gui.fx;
requires com.github.gtache.autosubtitle.whisper;
requires com.github.gtache.autosubtitle.whisperx;
requires javafx.fxml;
requires javafx.graphics;
opens com.github.gtache.autosubtitle.run to javafx.graphics;
opens com.github.gtache.autosubtitle.gui.run to javafx.graphics;
}