diff --git a/CHANGELOG.md b/CHANGELOG.md index 36cc4762d8..37f5d41e7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ - **FEATURE:** Mit der Lucene-Suche können mittels des `duplicate`-Boolean Parameters Filmduplikate berücksichtigt werden. - **FEATURE:** Via `Ansicht/Übersicht aller Duplikate anzeigen...` werden in einem Dialog per Sender alle vorhandenen Duplikate dargestellt zzgl. der zugeordneten Filme. - **FEATURE:** Geschwindigkeitsoptimierungen für die moderne Suche. +- **FEATURE:** Mittels Menü `Downloads/Unterttiteldatei zu Video hinzufügen...` kann eine vorhandene Untertiteldatei mit der korrekten Sprachzuordnung zu einem Video hinzugefügt werden. Moderne Videoplayer erkennen die Untertitelspur automatisch und man muss keine separaten Untertiteldateien mehr verwalten. # **14.1.0** - JDK 21 wird nun mitgeliefert. Behebt primär Darstellungsfehler von Java Apps unter Windows. - **macOS/Windows:** ffmpeg 7.0 ist nun enthalten. diff --git a/pom.xml b/pom.xml index 9195d75839..dc93386993 100755 --- a/pom.xml +++ b/pom.xml @@ -100,6 +100,7 @@ 11.2.1 3.5.2 33.3.0-jre + 76.1 1.1.2 2.17.2 2023.09.10 @@ -147,6 +148,12 @@ + + com.ibm.icu + icu4j + ${icu4j.version} + + com.github.lgooddatepicker LGoodDatePicker diff --git a/src/main/java/mediathek/gui/actions/MergeSubtitleWithVideoAction.java b/src/main/java/mediathek/gui/actions/MergeSubtitleWithVideoAction.java new file mode 100644 index 0000000000..04b1598867 --- /dev/null +++ b/src/main/java/mediathek/gui/actions/MergeSubtitleWithVideoAction.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 derreisende77. + * This code was developed as part of the MediathekView project https://github.com/mediathekview/MediathekView + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package mediathek.gui.actions; + +import mediathek.gui.dialog.subripmerge.MergeSubripVideoDialog; +import mediathek.mainwindow.MediathekGui; + +import javax.swing.*; +import java.awt.event.ActionEvent; + +public class MergeSubtitleWithVideoAction extends AbstractAction { + private final MediathekGui ui; + + public MergeSubtitleWithVideoAction(MediathekGui mediathekGui) { + this.ui = mediathekGui; + putValue(NAME, "Untertiteldatei zu Video hinzufügen..."); + } + + @Override + public void actionPerformed(ActionEvent e) { + MergeSubripVideoDialog dlg = new MergeSubripVideoDialog(ui); + dlg.setVisible(true); + } +} diff --git a/src/main/java/mediathek/gui/dialog/subripmerge/MergeSubripVideoDialog.java b/src/main/java/mediathek/gui/dialog/subripmerge/MergeSubripVideoDialog.java new file mode 100644 index 0000000000..7440e0cddf --- /dev/null +++ b/src/main/java/mediathek/gui/dialog/subripmerge/MergeSubripVideoDialog.java @@ -0,0 +1,364 @@ +/* + * Copyright (c) 2024 derreisende77. + * This code was developed as part of the MediathekView project https://github.com/mediathekview/MediathekView + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package mediathek.gui.dialog.subripmerge; + +import com.github.kokorin.jaffree.ffmpeg.FFmpeg; +import com.github.kokorin.jaffree.ffmpeg.UrlInput; +import com.github.kokorin.jaffree.ffmpeg.UrlOutput; +import mediathek.config.Konstanten; +import mediathek.tool.FileDialogs; +import mediathek.tool.GuiFunktionenProgramme; +import mediathek.tool.LanguageCode; +import mediathek.tool.SwingErrorDialog; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jdesktop.swingx.JXBusyLabel; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import java.awt.*; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author christianfranzke + */ +public class MergeSubripVideoDialog extends JDialog { + private static final Logger logger = LogManager.getLogger(); + private static final Pattern PATTERN = Pattern.compile("\\[(.*?)]"); + + public MergeSubripVideoDialog(Window owner) { + super(owner); + initComponents(); + + getRootPane().setDefaultButton(btnMerge); + busyLabel.setVisible(false); + btnMerge.setEnabled(false); + + fillLanguageComboBox(); + + btnCancel.addActionListener(_ -> dispose()); + + setupTextFieldListener(); + + btnSelectInputSubrip.addActionListener(_ -> { + var file = FileDialogs.chooseLoadFileLocation(this, "Untertitel wählen", ""); + if (file != null) { + var fileStr = file.getAbsolutePath(); + if (!fileStr.toLowerCase().endsWith(".srt")) { + JOptionPane.showMessageDialog(this, "Untertiteldatei muss auf .srt enden.", Konstanten.PROGRAMMNAME, JOptionPane.ERROR_MESSAGE); + tfSubripFilePath.setText(""); + } + else { + tfSubripFilePath.setText(file.getAbsolutePath()); + } + } + }); + btnSelectInputVideo.addActionListener(_ -> { + var file = FileDialogs.chooseLoadFileLocation(this, "Video wählen", ""); + if (file != null) { + tfVideoFilePath.setText(file.getAbsolutePath()); + } + }); + + btnSelectVideoOutputPath.addActionListener(_ -> { + var file = FileDialogs.chooseSaveFileLocation(this, "Videospeicherort wählen", ""); + if (file != null) { + tfVideoOutputPath.setText(file.getAbsolutePath()); + } + }); + + btnMerge.addActionListener(_ -> { + try { + var lang = (String) cbLanguage.getSelectedItem(); + if (lang == null) + throw new IllegalArgumentException("Native language selected is null"); + + Matcher matcher = PATTERN.matcher(lang); + + if (matcher.find()) { + lang = matcher.group(1); + } else { + throw new IllegalArgumentException("Could not get ISO 639 3 letter code"); + } + + busyLabel.setVisible(true); + busyLabel.setBusy(true); + btnMerge.setEnabled(false); + btnCancel.setEnabled(false); + + var ffmpegPath = GuiFunktionenProgramme.findExecutableOnPath("ffmpeg").getParent(); + var ffmpeg = FFmpeg.atPath(ffmpegPath) + .setOverwriteOutput(true) + .addArgument("-xerror") + .addInput(UrlInput.fromUrl(tfVideoFilePath.getText())) + .addInput(UrlInput.fromUrl(tfSubripFilePath.getText())) + .addOutput(UrlOutput.toUrl(tfVideoOutputPath.getText())) + .addArguments("-c", "copy") + .addArguments("-c:s", "mov_text") + .addArgument("-metadata:s:s:0") + .addArgument("language=" + lang); + ffmpeg.executeAsync().toCompletableFuture() + .thenAccept(_ -> SwingUtilities.invokeLater(() -> { + shutdownMergeProcess(); + JOptionPane.showMessageDialog(MergeSubripVideoDialog.this, "Das Zusammenführen war erfolgreich", Konstanten.PROGRAMMNAME, JOptionPane.INFORMATION_MESSAGE); + dispose(); + })) + .exceptionally(ex -> { + SwingUtilities.invokeLater(() -> { + shutdownMergeProcess(); + SwingErrorDialog.showExceptionMessage(MergeSubripVideoDialog.this, "Der Vorgang war fehlerhaft", ex); + dispose(); + }); + return null; + }); + } catch (Exception e) { + logger.error("Error occured while merging video", e); + shutdownMergeProcess(); + SwingErrorDialog.showExceptionMessage(this, "Es ist ein Fehler aufgetreten.", e); + } + }); + } + + private void setupTextFieldListener() { + DocumentListener documentListener = new DocumentListener() { + @Override + public void insertUpdate(DocumentEvent e) { + updateButtonState(); + } + + @Override + public void removeUpdate(DocumentEvent e) { + updateButtonState(); + } + + @Override + public void changedUpdate(DocumentEvent e) { + updateButtonState(); + } + + private void updateButtonState() { + boolean allFieldsFilled = !tfSubripFilePath.getText().trim().isEmpty() && + !tfVideoFilePath.getText().trim().isEmpty() && + !tfVideoOutputPath.getText().trim().isEmpty(); + btnMerge.setEnabled(allFieldsFilled); + } + }; + + tfSubripFilePath.getDocument().addDocumentListener(documentListener); + tfVideoFilePath.getDocument().addDocumentListener(documentListener); + tfVideoOutputPath.getDocument().addDocumentListener(documentListener); + } + + private void shutdownMergeProcess() { + busyLabel.setBusy(false); + busyLabel.setVisible(false); + btnCancel.setEnabled(true); + } + + public void fillLanguageComboBox() { + List languages = new ArrayList<>(); + for (var item : LanguageCode.values()) { + var entry = String.format("%s [%s]", item.nativeName(), item.getISO3Language()); + languages.add(entry); + } + cbLanguage.setModel(new DefaultComboBoxModel<>(languages.toArray(new String[0]))); + } + + private void initComponents() { + // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents @formatter:off + // Generated using JFormDesigner non-commercial license + var dialogPane = new JPanel(); + var contentPanel = new JPanel(); + var label1 = new JLabel(); + tfSubripFilePath = new JTextField(); + btnSelectInputSubrip = new JButton(); + var label2 = new JLabel(); + tfVideoFilePath = new JTextField(); + btnSelectInputVideo = new JButton(); + var label3 = new JLabel(); + cbLanguage = new JComboBox<>(); + var label4 = new JLabel(); + tfVideoOutputPath = new JTextField(); + btnSelectVideoOutputPath = new JButton(); + busyLabel = new JXBusyLabel(); + var buttonBar = new JPanel(); + btnCancel = new JButton(); + btnMerge = new JButton(); + + //======== this ======== + setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + setModal(true); + setTitle("Untertitel zu Video hinzuf\u00fcgen"); //NON-NLS + var contentPane = getContentPane(); + contentPane.setLayout(new BorderLayout()); + + //======== dialogPane ======== + { + dialogPane.setBorder(new EmptyBorder(12, 12, 12, 12)); + dialogPane.setLayout(new BorderLayout()); + + //======== contentPanel ======== + { + + //---- label1 ---- + label1.setText("Untertitel-Datei:"); //NON-NLS + label1.setHorizontalAlignment(SwingConstants.RIGHT); + + //---- btnSelectInputSubrip ---- + btnSelectInputSubrip.setText("..."); //NON-NLS + + //---- label2 ---- + label2.setText("Video-Datei:"); //NON-NLS + label2.setHorizontalAlignment(SwingConstants.RIGHT); + + //---- btnSelectInputVideo ---- + btnSelectInputVideo.setText("..."); //NON-NLS + + //---- label3 ---- + label3.setText("Sprache:"); //NON-NLS + label3.setHorizontalAlignment(SwingConstants.RIGHT); + + //---- cbLanguage ---- + cbLanguage.setToolTipText("Sprache der Untertitel"); //NON-NLS + + //---- label4 ---- + label4.setText("Zieldatei:"); //NON-NLS + label4.setHorizontalAlignment(SwingConstants.RIGHT); + + //---- btnSelectVideoOutputPath ---- + btnSelectVideoOutputPath.setText("..."); //NON-NLS + + //---- busyLabel ---- + busyLabel.setText("F\u00fchre Video und Untertitel zusammen"); //NON-NLS + + GroupLayout contentPanelLayout = new GroupLayout(contentPanel); + contentPanel.setLayout(contentPanelLayout); + contentPanelLayout.setHorizontalGroup( + contentPanelLayout.createParallelGroup() + .addGroup(contentPanelLayout.createSequentialGroup() + .addContainerGap() + .addGroup(contentPanelLayout.createParallelGroup() + .addGroup(contentPanelLayout.createSequentialGroup() + .addGroup(contentPanelLayout.createParallelGroup(GroupLayout.Alignment.TRAILING, false) + .addComponent(label2, GroupLayout.DEFAULT_SIZE, 104, Short.MAX_VALUE) + .addComponent(label3, GroupLayout.PREFERRED_SIZE, 98, GroupLayout.PREFERRED_SIZE) + .addComponent(label1, GroupLayout.DEFAULT_SIZE, 104, Short.MAX_VALUE)) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addGroup(contentPanelLayout.createParallelGroup() + .addGroup(contentPanelLayout.createSequentialGroup() + .addComponent(tfVideoFilePath) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(btnSelectInputVideo, GroupLayout.PREFERRED_SIZE, 33, GroupLayout.PREFERRED_SIZE)) + .addGroup(contentPanelLayout.createSequentialGroup() + .addComponent(tfSubripFilePath) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(btnSelectInputSubrip, GroupLayout.PREFERRED_SIZE, 33, GroupLayout.PREFERRED_SIZE)) + .addGroup(contentPanelLayout.createSequentialGroup() + .addComponent(cbLanguage, GroupLayout.PREFERRED_SIZE, 155, GroupLayout.PREFERRED_SIZE) + .addGap(0, 0, Short.MAX_VALUE)))) + .addGroup(GroupLayout.Alignment.TRAILING, contentPanelLayout.createSequentialGroup() + .addGroup(contentPanelLayout.createParallelGroup(GroupLayout.Alignment.TRAILING) + .addGroup(contentPanelLayout.createSequentialGroup() + .addComponent(label4, GroupLayout.PREFERRED_SIZE, 104, GroupLayout.PREFERRED_SIZE) + .addGap(6, 6, 6) + .addComponent(tfVideoOutputPath, GroupLayout.PREFERRED_SIZE, 368, GroupLayout.PREFERRED_SIZE)) + .addGroup(GroupLayout.Alignment.LEADING, contentPanelLayout.createSequentialGroup() + .addGap(20, 20, 20) + .addComponent(busyLabel, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE))) + .addGap(6, 6, 6) + .addComponent(btnSelectVideoOutputPath, GroupLayout.PREFERRED_SIZE, 33, GroupLayout.PREFERRED_SIZE))) + .addContainerGap()) + ); + contentPanelLayout.setVerticalGroup( + contentPanelLayout.createParallelGroup() + .addGroup(contentPanelLayout.createSequentialGroup() + .addContainerGap() + .addGroup(contentPanelLayout.createParallelGroup(GroupLayout.Alignment.BASELINE) + .addComponent(btnSelectInputSubrip) + .addComponent(tfSubripFilePath, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) + .addComponent(label1)) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addGroup(contentPanelLayout.createParallelGroup(GroupLayout.Alignment.BASELINE) + .addComponent(label3) + .addComponent(cbLanguage, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addGroup(contentPanelLayout.createParallelGroup(GroupLayout.Alignment.BASELINE) + .addComponent(btnSelectInputVideo) + .addComponent(label2) + .addComponent(tfVideoFilePath, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)) + .addGap(18, 18, 18) + .addGroup(contentPanelLayout.createParallelGroup() + .addGroup(contentPanelLayout.createSequentialGroup() + .addGap(7, 7, 7) + .addComponent(label4)) + .addComponent(tfVideoOutputPath, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) + .addComponent(btnSelectVideoOutputPath)) + .addGap(18, 18, 18) + .addComponent(busyLabel, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) + .addContainerGap(GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + } + dialogPane.add(contentPanel, BorderLayout.CENTER); + + //======== buttonBar ======== + { + buttonBar.setBorder(new EmptyBorder(12, 0, 0, 0)); + buttonBar.setLayout(new GridBagLayout()); + ((GridBagLayout)buttonBar.getLayout()).columnWidths = new int[] {0, 0, 80}; + ((GridBagLayout)buttonBar.getLayout()).columnWeights = new double[] {1.0, 0.0, 0.0}; + + //---- btnCancel ---- + btnCancel.setText("Abbrechen"); //NON-NLS + buttonBar.add(btnCancel, new GridBagConstraints(1, 0, 1, 1, 0.0, 0.0, + GridBagConstraints.CENTER, GridBagConstraints.BOTH, + new Insets(0, 0, 0, 5), 0, 0)); + + //---- btnMerge ---- + btnMerge.setText("Zusammenf\u00fchren"); //NON-NLS + buttonBar.add(btnMerge, new GridBagConstraints(2, 0, 1, 1, 0.0, 0.0, + GridBagConstraints.CENTER, GridBagConstraints.BOTH, + new Insets(0, 0, 0, 0), 0, 0)); + } + dialogPane.add(buttonBar, BorderLayout.SOUTH); + } + contentPane.add(dialogPane, BorderLayout.CENTER); + pack(); + setLocationRelativeTo(getOwner()); + // JFormDesigner - End of component initialization //GEN-END:initComponents @formatter:on + } + + // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables @formatter:off + // Generated using JFormDesigner non-commercial license + private JTextField tfSubripFilePath; + private JButton btnSelectInputSubrip; + private JTextField tfVideoFilePath; + private JButton btnSelectInputVideo; + private JComboBox cbLanguage; + private JTextField tfVideoOutputPath; + private JButton btnSelectVideoOutputPath; + private JXBusyLabel busyLabel; + private JButton btnCancel; + private JButton btnMerge; + // JFormDesigner - End of variables declaration //GEN-END:variables @formatter:on +} diff --git a/src/main/java/mediathek/gui/dialog/subripmerge/MergeSubripVideoDialog.jfd b/src/main/java/mediathek/gui/dialog/subripmerge/MergeSubripVideoDialog.jfd new file mode 100644 index 0000000000..4b6e0cb730 --- /dev/null +++ b/src/main/java/mediathek/gui/dialog/subripmerge/MergeSubripVideoDialog.jfd @@ -0,0 +1,130 @@ +JFDML JFormDesigner: "8.2.4.0.393" Java: "21.0.4" encoding: "UTF-8" + +new FormModel { + contentType: "form/swing" + root: new FormRoot { + add( new FormWindow( "javax.swing.JDialog", new FormLayoutManager( class java.awt.BorderLayout ) ) { + name: "this" + "defaultCloseOperation": 2 + "modal": true + "title": "Untertitel zu Video hinzufügen" + add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class java.awt.BorderLayout ) ) { + name: "dialogPane" + "border": new javax.swing.border.EmptyBorder( 12, 12, 12, 12 ) + auxiliary() { + "JavaCodeGenerator.variableLocal": true + } + add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class org.jdesktop.layout.GroupLayout ) { + "$horizontalGroup": "par l {seq l {space :::p, par l {seq l {par t:::p {comp label2::::104:x, comp label3::t:p:98:p, comp label1::::104:x}, space :::p, par l {seq l {comp tfVideoFilePath:::::x, space :::p, comp btnSelectInputVideo:::p:33:p}, seq {comp tfSubripFilePath:::::x, space :::p, comp btnSelectInputSubrip:::p:33:p}, seq {comp cbLanguage:::p:155:p, space :0:0:x}}}, seq t {par t {seq {comp label4:::p:104:p, space :6:6:p, comp tfVideoOutputPath:::p:368:p}, seq l {space :p:20:p, comp busyLabel:::p::p}}, space :6:6:p, comp btnSelectVideoOutputPath:::p:33:p}}, space :::p}}" + "$verticalGroup": "par l {seq l {space :::p, par b {comp btnSelectInputSubrip::b:p::p, comp tfSubripFilePath::b:p::p, comp label1::b:p::p}, space :::p, par b {comp label3::b:p::p, comp cbLanguage::b:p::p}, space :::p, par b {comp btnSelectInputVideo::b:p::p, comp label2::b:p::p, comp tfVideoFilePath::b:p::p}, space s:::p, par l {seq l {space :7:7:p, comp label4:::p::p}, comp tfVideoOutputPath:::p::p, comp btnSelectVideoOutputPath:::p::p}, space s:::p, comp busyLabel:::p::p, space :::x}}" + } ) { + name: "contentPanel" + auxiliary() { + "JavaCodeGenerator.variableLocal": true + } + add( new FormComponent( "javax.swing.JLabel" ) { + name: "label1" + "text": "Untertitel-Datei:" + "horizontalAlignment": 4 + auxiliary() { + "JavaCodeGenerator.variableLocal": true + } + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "tfSubripFilePath" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "btnSelectInputSubrip" + "text": "..." + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "label2" + "text": "Video-Datei:" + "horizontalAlignment": 4 + auxiliary() { + "JavaCodeGenerator.variableLocal": true + } + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "tfVideoFilePath" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "btnSelectInputVideo" + "text": "..." + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "label3" + "text": "Sprache:" + "horizontalAlignment": 4 + auxiliary() { + "JavaCodeGenerator.variableLocal": true + } + } ) + add( new FormComponent( "javax.swing.JComboBox" ) { + name: "cbLanguage" + "toolTipText": "Sprache der Untertitel" + auxiliary() { + "JavaCodeGenerator.typeParameters": "String" + } + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "label4" + "text": "Zieldatei:" + "horizontalAlignment": 4 + auxiliary() { + "JavaCodeGenerator.variableLocal": true + } + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "tfVideoOutputPath" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "btnSelectVideoOutputPath" + "text": "..." + } ) + add( new FormComponent( "org.jdesktop.swingx.JXBusyLabel" ) { + name: "busyLabel" + "text": "Führe Video und Untertitel zusammen" + } ) + }, new FormLayoutConstraints( class java.lang.String ) { + "value": "Center" + } ) + add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class java.awt.GridBagLayout ) { + "$columnSpecs": "0:1.0, 0, 80" + "$rowSpecs": "0" + "$hGap": 5 + "$vGap": 5 + } ) { + name: "buttonBar" + "border": new javax.swing.border.EmptyBorder( 12, 0, 0, 0 ) + auxiliary() { + "JavaCodeGenerator.variableLocal": true + } + add( new FormComponent( "javax.swing.JButton" ) { + name: "btnCancel" + "text": "Abbrechen" + }, new FormLayoutConstraints( class com.jformdesigner.runtime.GridBagConstraintsEx ) { + "gridx": 1 + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "btnClose" + "text": "Zusammenführen" + auxiliary() { + "JavaCodeGenerator.variableName": "btnMerge" + } + }, new FormLayoutConstraints( class com.jformdesigner.runtime.GridBagConstraintsEx ) { + "gridy": 0 + "gridx": 2 + } ) + }, new FormLayoutConstraints( class java.lang.String ) { + "value": "South" + } ) + }, new FormLayoutConstraints( class java.lang.String ) { + "value": "Center" + } ) + }, new FormLayoutConstraints( null ) { + "location": new java.awt.Point( 0, 0 ) + "size": new java.awt.Dimension( 555, 295 ) + } ) + } +} diff --git a/src/main/java/mediathek/gui/tabs/tab_downloads/GuiDownloads.java b/src/main/java/mediathek/gui/tabs/tab_downloads/GuiDownloads.java index dc33870893..4a8e9f9c1a 100644 --- a/src/main/java/mediathek/gui/tabs/tab_downloads/GuiDownloads.java +++ b/src/main/java/mediathek/gui/tabs/tab_downloads/GuiDownloads.java @@ -106,6 +106,7 @@ public class GuiDownloads extends AGuiTabPanel { protected DeleteDownloadAction deleteDownloadAction = new DeleteDownloadAction(this); protected OpenTargetFolderAction openTargetFolderAction = new OpenTargetFolderAction(this); protected ToggleFilterPanelAction toggleFilterPanelAction = new ToggleFilterPanelAction(); + protected MergeSubtitleWithVideoAction mergeSubtitleWithVideoAction = new MergeSubtitleWithVideoAction(MediathekGui.ui()); protected JToolBar swingToolBar = new JToolBar(); private boolean onlyAbos; private boolean onlyDownloads; @@ -341,6 +342,8 @@ public void installMenuEntries(JMenu menu) { menu.add(deleteDownloadsAction); menu.add(editDownloadAction); menu.addSeparator(); + menu.add(mergeSubtitleWithVideoAction); + menu.addSeparator(); menu.add(cbShowDownloadDescription); menu.addSeparator(); menu.add(miMarkFilmAsSeen); diff --git a/src/main/java/mediathek/tool/ColorUtils.java b/src/main/java/mediathek/tool/ColorUtils.java index 981b08c2c0..7e32cd4f59 100644 --- a/src/main/java/mediathek/tool/ColorUtils.java +++ b/src/main/java/mediathek/tool/ColorUtils.java @@ -1,3 +1,21 @@ +/* + * Copyright (c) 2024 derreisende77. + * This code was developed as part of the MediathekView project https://github.com/mediathekview/MediathekView + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package mediathek.tool; import java.awt.*; diff --git a/src/main/java/mediathek/tool/DarkModeDetector.java b/src/main/java/mediathek/tool/DarkModeDetector.java index 31d0de6af1..415b63d889 100644 --- a/src/main/java/mediathek/tool/DarkModeDetector.java +++ b/src/main/java/mediathek/tool/DarkModeDetector.java @@ -1,3 +1,21 @@ +/* + * Copyright (c) 2024 derreisende77. + * This code was developed as part of the MediathekView project https://github.com/mediathekview/MediathekView + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package mediathek.tool; import org.apache.commons.lang3.SystemUtils; diff --git a/src/main/java/mediathek/tool/FileDialogs.kt b/src/main/java/mediathek/tool/FileDialogs.kt index 62d5b6c7b8..7b7e49c025 100644 --- a/src/main/java/mediathek/tool/FileDialogs.kt +++ b/src/main/java/mediathek/tool/FileDialogs.kt @@ -4,6 +4,7 @@ import org.apache.commons.lang3.SystemUtils import java.awt.FileDialog import java.awt.Frame import java.io.File +import javax.swing.JDialog import javax.swing.JFileChooser class FileDialogs { @@ -44,6 +45,40 @@ class FileDialogs { return resultFile } + @JvmStatic + fun chooseLoadFileLocation(parent: JDialog, title: String, initialFile: String): File? { + var resultFile: File? = null + + if (SystemUtils.IS_OS_MAC_OSX || SystemUtils.IS_OS_WINDOWS) { + val chooser = FileDialog(parent, title) + chooser.mode = FileDialog.LOAD + chooser.isMultipleMode = false + if (initialFile.isNotEmpty()) { + chooser.directory = initialFile + } + chooser.isVisible = true + if (chooser.file != null) { + val files = chooser.files + if (files.isNotEmpty()) { + resultFile = files[0] + } + } + } else { + val chooser = JFileChooser() + if (initialFile.isNotEmpty()) { + chooser.currentDirectory = File(initialFile) + } + chooser.fileSelectionMode = JFileChooser.FILES_ONLY + chooser.dialogTitle = title + chooser.isFileHidingEnabled = true + if (chooser.showOpenDialog(parent) == JFileChooser.APPROVE_OPTION) { + resultFile = File(chooser.selectedFile.absolutePath) + } + } + + return resultFile + } + @JvmStatic fun chooseLoadFileLocation(parent: Frame, title: String, initialFile: String): File? { var resultFile: File? = null @@ -117,5 +152,38 @@ class FileDialogs { } return resultFile } + + @JvmStatic + fun chooseSaveFileLocation(parent: JDialog, title: String, initialFile: String): File? { + var resultFile: File? = null + if (SystemUtils.IS_OS_MAC_OSX || SystemUtils.IS_OS_WINDOWS) { + val chooser = FileDialog(parent, title) + chooser.mode = FileDialog.SAVE + chooser.isMultipleMode = false + if (initialFile.isNotEmpty()) { + chooser.directory = initialFile + } + chooser.isVisible = true + if (chooser.file != null) { + val files = chooser.files + if (files.isNotEmpty()) { + resultFile = files[0] + } + } + } else { + //Linux HiDPI does not work with either AWT FileDialog or JavaFX FileChooser as of JFX 14.0.1 + val chooser = JFileChooser() + if (initialFile.isNotEmpty()) { + chooser.currentDirectory = File(initialFile) + } + chooser.fileSelectionMode = JFileChooser.FILES_ONLY + chooser.dialogTitle = title + chooser.isFileHidingEnabled = true + if (chooser.showSaveDialog(parent) == JFileChooser.APPROVE_OPTION) { + resultFile = File(chooser.selectedFile.absolutePath) + } + } + return resultFile + } } } \ No newline at end of file diff --git a/src/main/java/mediathek/tool/LanguageCode.java b/src/main/java/mediathek/tool/LanguageCode.java new file mode 100644 index 0000000000..e9350f17fd --- /dev/null +++ b/src/main/java/mediathek/tool/LanguageCode.java @@ -0,0 +1,222 @@ +/* + * Copyright (c) 2024 derreisende77. + * This code was developed as part of the MediathekView project https://github.com/mediathekview/MediathekView + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package mediathek.tool; + +import com.ibm.icu.util.ULocale; +import org.jetbrains.annotations.NotNull; + +public enum LanguageCode { + aa("Afar", "Afaraf"), + ab("Abkhazian", "Аҧсуа"), + ae("Avestan", "Avesta"), + af("Afrikaans", "Afrikaans"), + ak("Akan", "Akan"), + am("Amharic", "አማርኛ"), + an("Aragonese", "Aragonés"), + ar("Arabic", "العربية"), + av("Avaric", "авар мацӀ; магӀарул мацӀ"), + ay("Aymara", "aymar aru"), + az("Azerbaijani", "azərbaycan dili"), + ba("Bashkir", "башҡорт теле"), + be("Belarusian", "Беларуская"), + bi("Bislama", "Bislama"), + bg("Bulgarian", "български език"), + //bh("Bihari", "भोजपुरी"), + bm("Bambara", "bamanankan"), + bn("Bengali", "বাংলা"), + bo("Tibetan", "བོད་ཡིག"), + br("Breton", "brezhoneg"), + bs("Bosnian", "bosanski jezik"), + ca("Catalan", "Català"), + ch("Chamorro", "Chamoru"), + co("Corsican", "corsu; lingua corsa"), + cr("Cree", "ᓀᐦᐃᔭᐍᐏᐣ"), + cs("Czech", "česky; čeština"), + cu("Church Slavic", "ѩзыкъ словѣньскъ"), + cv("Chuvash", "чӑваш чӗлхи"), + cy("Welsh", "Cymraeg"), + da("Danish", "Dansk"), + de("German", "Deutsch"), + dv("Divehi", "ދިވެހި"), + dz("Dzongkha", "རྫོང་ཁ"), + ee("Ewe", "Ɛʋɛgbɛ"), + el("Greek", "Ελληνικά"), + en("English", "English"), + eo("Esperanto", "Esperanto"), + es("Spanish", "Español"), + et("Estonian", "eesti; eesti keel"), + eu("Basque", "euskara; euskera"), + fa("Persian", "فارسی"), + ff("Fulah", "Fulfulde"), + fi("Finnish", "suomi; suomen kieli"), + fj("Fijian", "vosa Vakaviti"), + fo("Faroese", "Føroyskt"), + fr("French", "Français"), + fy("Western Frisian", "Frysk"), + ga("Irish", "Gaeilge"), + gd("Scottish Gaelic", "Gàidhlig"), + gl("Galician", "Galego"), + gn("Guaraní", "Avañe'ẽ"), + gu("Gujarati", "ગુજરાતી"), + gv("Manx", "Gaelg, Gailck"), + ha("Hausa", "هَوُسَ"), + he("Hebrew", "עברית"), + hi("Hindi", "हिन्दी; हिंदी"), + ho("Hiri Motu", "Hiri Motu"), + hr("Croatian", "Hrvatski"), + ht("Haitian", "Kreyòl ayisyen"), + hu("Hungarian", "Magyar"), + hy("Armenian", "Հայերեն"), + hz("Herero", "Otjiherero"), + ia("Interlingua (International Auxiliary Language Association)", "Interlingua"), + id("Indonesian", "Bahasa Indonesia"), + ie("Interlingue", "Interlingue"), + ig("Igbo", "Igbo"), + ii("Sichuan Yi", "ꆇꉙ"), + ik("Inupiaq", "Iñupiaq; Iñupiatun"), + io("Ido", "Ido"), + is("Icelandic", "Íslenska"), + it("Italian", "Italiano"), + iu("Inuktitut", "ᐃᓄᒃᑎᑐᑦ"), + ja("Japanese", "Nihongo"), + ka("Georgian", "ქართული"), + kg("Kongo", "KiKongo"), + ki("Kikuyu", "Gĩkũyũ"), + kj("Kwanyama", "Kuanyama"), + ku("Kurdish", "كوردی"), + kk("Kazakh", "Қазақ тілі"), + kl("Kalaallisut", "kalaallisut; kalaallit oqaasii"), + km("Khmer", "ភាសាខ្មែរ"), + kn("Kannada", "ಕನ್ನಡ"), + ko("Korean", "Kanuri"), + ks("Kashmiri", "коми кыв"), + kw("Cornish", "Kernewek"), + ky("Kirghiz", "кыргыз тили"), + la("Latin", "Latine; lingua latina"), + lb("Luxembourgish", "Lëtzebuergesch"), + lg("Ganda", "Luganda"), + li("Limburgish", "Limburgs"), + ln("Lingala", "Lingála"), + lo("Lao", "ພາສາລາວ"), + lt("Lithuanian", "Lietuvių kalba"), + lu("Luba-Katanga", "Latviešu valoda"), + mg("Malagasy", "Malagasy fiteny"), + mh("Marshallese", "Kajin M̧ajeļ"), + mi("Māori", "Te reo Māori"), + mk("Macedonian", "македонски јазик"), + ml("Malayalam", "മലയാളം"), + mn("Mongolian", "Монгол"), + mr("Marathi", "मराठी"), + ms("Malay", "بهاس ملايو"), + mt("Maltese", "Malti"), + my("Burmese", "ဗမာစာ"), + na("Nauru", "Ekakairũ Naoero"), + nb("Norwegian Bokmål", "Norsk bokmål"), + nd("North Ndebele", "isiNdebele"), + ne("Nepali", "नेपाली"), + ng("Ndonga", "Owambo"), + nl("Dutch", "Nederlands"), + nn("Norwegian Nynorsk", "Norsk nynorsk"), + no("Norwegian", "Norsk"), + nr("South Ndebele", "isiNdebele"), + nv("Navajo", "Diné bizaad; Dinékʼehǰí"), + ny("Chichewa", "chiCheŵa; chinyanja"), + oc("Occitan", "Occitan"), + oj("Ojibwa", "ᐊᓂᔑᓈᐯᒧᐎᓐ"), + om("Oromo", "Afaan Oromoo"), + or("Oriya", "ଓଡ଼ିଆ"), + os("Ossetian", "Ирон æвзаг"), + pa("Panjabi", "पाऴि"), + pl("Polish", "Polski"), + ps("Pashto", "پښتو"), + pt("Portuguese", "Português"), + qu("Quechua", "Runa Simi; Kichwa"), + rm("Raeto-Romance", "rumantsch grischun"), + rn("Kirundi", "kiRundi"), + ro("Romanian", "Română"), + ru("Russian", "русский язык"), + rw("Kinyarwanda", "Ikinyarwanda"), + sa("Sanskrit", "संस्कृतम्"), + sc("Sardinian", "sardu"), + sd("Sindhi", "Davvisámegiella"), + sg("Sango", "yângâ tî sängö"), + si("Sinhala", "සිංහල"), + sk("Slovak", "Slovenčina"), + sl("Slovenian", "Slovenščina"), + sm("Samoan", "gagana fa'a Samoa"), + sn("Shona", "chiShona"), + so("Somali", "Soomaaliga; af Soomaali"), + sq("Albanian", "Shqip"), + sr("Serbian", "српски језик"), + ss("Swati", "SiSwati"), + st("Southern Sotho", "Sesotho"), + su("Sundanese", "Basa Sunda"), + sv("Swedish", "svenska"), + sw("Swahili", "Kiswahili"), + ta("Tamil", "தமிழ்"), + te("Telugu", "తెలుగు"), + tg("Tajik", "ไทย"), + ti("Tigrinya", "ትግርኛ"), + tk("Turkmen", "Türkmen; Түркмен"), + //tl("Tagalog", "Tagalog"), + tn("Tswana", "Setswana"), + to("Tonga", "faka Tonga"), + tr("Turkish", "Türkçe"), + ts("Tsonga", "Xitsonga"), + tt("Tatar", "Twi"), + ty("Tahitian", "Reo Mā`ohi"), + ug("Uighur", "Українська"), + ur("Urdu", "اردو"), + uk("Ukrainian", "Ukraïna"), + uz("Uzbek", "Tshivenḓa"), + vi("Vietnamese", "Tiếng Việt"), + vo("Volapük", "Volapük"), + wa("Walloon", "Walon"), + wo("Wolof", "Wollof"), + xh("Xhosa", "isiXhosa"), + yi("Yiddish", "ייִדיש"), + yo("Yoruba", "Yorùbá"), + za("Zhuang", "Saɯ cueŋƅ; Saw cuengh"), + zh("Chinese", "中文 (Zhōngwén), 汉语, 漢語"), + zu("Zulu", "isiZulu"); + + private final String readableName; + private final String nativeName; + LanguageCode(String readableName, String nativeName) { + this.readableName = readableName; + this.nativeName = nativeName; + } + public String readableName() { return readableName;} + public String nativeName() { return nativeName;} + public @NotNull String getISO3Language() throws IllegalArgumentException { + ULocale locale = new ULocale(this.name()); + var isocode = locale.getISO3Language(); + if (isocode.isEmpty()) + throw new IllegalArgumentException("Language code '" + this.name() + "' is empty"); + else return isocode; + } + + public static LanguageCode fromNativeName(@NotNull String nativeName) throws IllegalArgumentException { + for (var item: LanguageCode.values()) { + if (item.nativeName.equals(nativeName)) + return item; + } + throw new IllegalArgumentException("Language code '" + nativeName + "' not found"); + } +} diff --git a/src/main/java/mediathek/tool/SwingErrorDialog.java b/src/main/java/mediathek/tool/SwingErrorDialog.java index 79d35dfd0c..8c1ddd4027 100644 --- a/src/main/java/mediathek/tool/SwingErrorDialog.java +++ b/src/main/java/mediathek/tool/SwingErrorDialog.java @@ -12,7 +12,7 @@ public class SwingErrorDialog { public static void showExceptionMessage(@Nullable Component parentComponent, @NotNull String messageText, - @NotNull Exception exception) throws HeadlessException { + @NotNull Throwable exception) throws HeadlessException { StringWriter stringWriter = new StringWriter(); exception.printStackTrace(new PrintWriter(stringWriter)); diff --git a/src/test/java/mediathek/tool/LanguageCodeTest.java b/src/test/java/mediathek/tool/LanguageCodeTest.java new file mode 100644 index 0000000000..26351c24d7 --- /dev/null +++ b/src/test/java/mediathek/tool/LanguageCodeTest.java @@ -0,0 +1,21 @@ +package mediathek.tool; + +import org.junit.jupiter.api.Test; + +import java.util.EnumSet; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +class LanguageCodeTest { + /** + * Test if all codes can be converted to 3 letter code and there is no exception. + */ + @Test + public void testConversion() { + for (var code : EnumSet.allOf(LanguageCode.class)) { + var out = code.getISO3Language(); + assertFalse(out.isEmpty()); + } + + } +} \ No newline at end of file