From 1f4cd526ce95f1def8823ef5bf21902774f8b3d1 Mon Sep 17 00:00:00 2001 From: "Christian F." Date: Thu, 24 Oct 2024 14:47:42 +0200 Subject: [PATCH] - fix splashscreen not showing black background only on windows and linux - implement duplicate overview dialog - introduce config variable ApplicationConfiguration.FILM_EVALUATE_DUPLICATES - refactor out CommonStatsEvaluationTask - code cleanup - relax error reporting in DatenFilm.setDatumLong --- CHANGELOG.md | 2 + src/main/java/mediathek/daten/DatenFilm.java | 4 +- .../java/mediathek/filmlisten/FilmeLaden.java | 10 +- .../filmlisten/writer/FilmListWriter.java | 2 +- .../BigSenderPenaltyComparator.java | 22 ++ .../duplicates/CommonStatsEvaluationTask.java | 41 +++ .../FilmDuplicateEvaluationTask.java | 44 +--- .../overview/CustomTreeCellRenderer.java | 53 ++++ .../overview/FilmDuplicateOverviewDialog.java | 236 ++++++++++++++++++ .../overview/FilmDuplicateOverviewDialog.jfd | 88 +++++++ .../mediathek/mainwindow/MediathekGui.java | 22 +- .../tool/ApplicationConfiguration.java | 1 + src/main/kotlin/mediathek/SplashScreen.kt | 98 ++++---- 13 files changed, 531 insertions(+), 92 deletions(-) create mode 100644 src/main/java/mediathek/gui/duplicates/BigSenderPenaltyComparator.java create mode 100644 src/main/java/mediathek/gui/duplicates/CommonStatsEvaluationTask.java create mode 100644 src/main/java/mediathek/gui/duplicates/overview/CustomTreeCellRenderer.java create mode 100644 src/main/java/mediathek/gui/duplicates/overview/FilmDuplicateOverviewDialog.java create mode 100644 src/main/java/mediathek/gui/duplicates/overview/FilmDuplicateOverviewDialog.jfd diff --git a/CHANGELOG.md b/CHANGELOG.md index c42a7b6a55..4f55677c0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - **BUGFIX:** Der "Filmliste laden"-Dialog zeigt nun Scrollbars bei zu kleiner Dialoggröße an. - **BUGFIX(Linux):** Inkorrekte Verarbeitung der Parent-Beziehung bei Auswahldialogen wurde behoben. - **BUGFIX(Linux):** MV öffnet nun einen konfigurierten Browser im About-Dialog wenn Java keinen eigenen finden kann. +- **BUGFIX(Linux/Windows):** Splashscreen zeigte keinen uniformen schwarzen Hintergrund mehr an. - **FEATURE(Linux/Windows):** Das Aussehen von FlatLaf kann in `.mediathek3/flatlaf` angepasst werden. - **FEATURE:** Selektierte Einträge in `Suchhistorie bearbeiten`-Dialog können nun mittels Entfernen-Taste gelöscht werden. - **FEATURE:** Das Blacklist Filter-Icon in der Toolbar kann mittels *Einstellungen/Allgemein* mit einem aussagefähigen Zusatztext dargestellt werden. @@ -29,6 +30,7 @@ - **FEATURE:** Im Tab `Filme` können über das Kontextmenü `Zusammengehörige Filme anzeigen...` bei einem markierten Duplikat alle zusammengehörigen Filme angezeigt werden. - **FEATURE:** Mittels `Ansicht/Filmstatistik anzeigen` können nun für die vorhandenen Sender Informationen bzgl. Anzahl der Filme und der Duplikate angezeigt werden. Es wird hier nur die gesamte Filmliste ohne jegliche Filter abzüglich Livestreams berücksichtigt, so dass es zu Abweichungen zur Anzeige in der Statuszeile kommen kann. - **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. # **14.1.0** - JDK 21 wird nun mitgeliefert. Behebt primär Darstellungsfehler von Java Apps unter Windows. diff --git a/src/main/java/mediathek/daten/DatenFilm.java b/src/main/java/mediathek/daten/DatenFilm.java index 40defddfdf..625c9e6055 100644 --- a/src/main/java/mediathek/daten/DatenFilm.java +++ b/src/main/java/mediathek/daten/DatenFilm.java @@ -189,7 +189,7 @@ public void setDatumLong(String datumLong) { datum_long = Long.parseLong(datumLong); } catch (Exception e) { - logger.error("Failed to parse datum long string", e); + logger.warn("Failed to parse datum long string: {}", datumLong); datum_long = 0; } dataMap.put(MapKeys.TEMP_DATUM_LONG, datum_long); @@ -421,7 +421,7 @@ private void setupDatumFilm() { if (!getSendeDatum().isEmpty()) { // nur dann gibts ein Datum long datum_long = (long)dataMap.getOrDefault(MapKeys.TEMP_DATUM_LONG, 0L); - datumFilm = new DatumFilm(datum_long * 1000); // convert from SECONDS to MILLISECONDS + datumFilm = new DatumFilm(TimeUnit.MILLISECONDS.convert(datum_long, TimeUnit.SECONDS)); if (datum_long == 0) { setSendeDatum(""); diff --git a/src/main/java/mediathek/filmlisten/FilmeLaden.java b/src/main/java/mediathek/filmlisten/FilmeLaden.java index dd931ffd53..a62a6c5dac 100644 --- a/src/main/java/mediathek/filmlisten/FilmeLaden.java +++ b/src/main/java/mediathek/filmlisten/FilmeLaden.java @@ -9,6 +9,7 @@ import mediathek.filmeSuchen.ListenerFilmeLaden; import mediathek.filmeSuchen.ListenerFilmeLadenEvent; import mediathek.filmlisten.reader.FilmListReader; +import mediathek.gui.duplicates.CommonStatsEvaluationTask; import mediathek.gui.duplicates.FilmDuplicateEvaluationTask; import mediathek.gui.messages.FilmListReadStopEvent; import mediathek.gui.tasks.BlacklistFilterWorker; @@ -317,8 +318,13 @@ private void undEnde(ListenerFilmeLadenEvent event) { throw new RuntimeException(e); } var workerTask = CompletableFuture.runAsync(new RefreshAboWorker(progLabel, progressBar)) - .thenRun(new BlacklistFilterWorker(progLabel, progressBar)) - .thenRun(new FilmDuplicateEvaluationTask()); + .thenRun(new BlacklistFilterWorker(progLabel, progressBar)); + + var evaluateDuplicates = ApplicationConfiguration.getConfiguration().getBoolean(ApplicationConfiguration.FILM_EVALUATE_DUPLICATES, true); + if (evaluateDuplicates) { + workerTask = workerTask.thenRun(new FilmDuplicateEvaluationTask()); + } + workerTask = workerTask.thenRun(new CommonStatsEvaluationTask()); if (writeFilmList) { workerTask = workerTask.thenRun(new FilmlistWriterWorker(progLabel, progressBar)); diff --git a/src/main/java/mediathek/filmlisten/writer/FilmListWriter.java b/src/main/java/mediathek/filmlisten/writer/FilmListWriter.java index 5eadc490e1..dac5dcf3ca 100644 --- a/src/main/java/mediathek/filmlisten/writer/FilmListWriter.java +++ b/src/main/java/mediathek/filmlisten/writer/FilmListWriter.java @@ -152,7 +152,7 @@ private void writeEntry(DatenFilm datenFilm, JsonGenerator jg) throws IOExceptio skipEntry(jg); //DatenFilm.URL_RTMP_KLEIN writeHighQualityUrl(jg, datenFilm); skipEntry(jg); //DatenFilm.FILM_URL_RTMP_HD - jg.writeString(String.valueOf(datenFilm.getDatumFilm().getTime() / 1000)); + jg.writeString(String.valueOf(TimeUnit.SECONDS.convert(datenFilm.getDatumFilm().getTime(), TimeUnit.MILLISECONDS))); skipEntry(jg); //DatenFilm.FILM_URL_HISTORY if (datenFilm.countrySet.isEmpty()) jg.writeString(""); diff --git a/src/main/java/mediathek/gui/duplicates/BigSenderPenaltyComparator.java b/src/main/java/mediathek/gui/duplicates/BigSenderPenaltyComparator.java new file mode 100644 index 0000000000..dfcbb6743c --- /dev/null +++ b/src/main/java/mediathek/gui/duplicates/BigSenderPenaltyComparator.java @@ -0,0 +1,22 @@ +package mediathek.gui.duplicates; + +import mediathek.daten.DatenFilm; + +import java.util.Comparator; + +public class BigSenderPenaltyComparator implements Comparator { + @Override + public int compare(DatenFilm s1, DatenFilm s2) { + // "ARD" und "ZDF" immer am Ende um die kleineren Mediatheken nicht zu benachteiligen + final var s1_sender = s1.getSender(); + final var s2_sender = s2.getSender(); + if (s1_sender.equals("ARD") || s1_sender.equals("ZDF")) { + return 1; + } + if (s2_sender.equals("ARD") || s2_sender.equals("ZDF")) { + return -1; + } + // Alphabetisch sortieren für alle anderen + return s1.compareTo(s2); + } +} diff --git a/src/main/java/mediathek/gui/duplicates/CommonStatsEvaluationTask.java b/src/main/java/mediathek/gui/duplicates/CommonStatsEvaluationTask.java new file mode 100644 index 0000000000..af92b71b3a --- /dev/null +++ b/src/main/java/mediathek/gui/duplicates/CommonStatsEvaluationTask.java @@ -0,0 +1,41 @@ +package mediathek.gui.duplicates; + +import ca.odell.glazedlists.TransactionList; +import com.google.common.base.Stopwatch; +import mediathek.config.Daten; +import mediathek.daten.DatenFilm; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.Map; +import java.util.stream.Collectors; + +public class CommonStatsEvaluationTask implements Runnable { + private static final Logger logger = LogManager.getLogger(); + + @Override + public void run() { + Stopwatch watch = Stopwatch.createStarted(); + final var films = Daten.getInstance().getListeFilme().parallelStream() + .filter(f -> !f.isLivestream()) + .toList(); + + var statisticsList = Daten.getInstance().getCommonStatistics(); + + Map statisticsMap = films.parallelStream() + .collect(Collectors.groupingBy(DatenFilm::getSender, Collectors.counting())); + TransactionList tList = new TransactionList<>(statisticsList); + statisticsList.getReadWriteLock().writeLock().lock(); + tList.beginEvent(true); + tList.clear(); + for (var sender : statisticsMap.keySet()) { + tList.add(new FilmStatistics(sender, statisticsMap.get(sender))); + } + tList.commitEvent(); + statisticsList.getReadWriteLock().writeLock().unlock(); + watch.stop(); + + logger.trace("common stats calculation took: {}", watch); + logger.trace("Number of films: {}", films.size()); + } +} diff --git a/src/main/java/mediathek/gui/duplicates/FilmDuplicateEvaluationTask.java b/src/main/java/mediathek/gui/duplicates/FilmDuplicateEvaluationTask.java index 8b15d67f69..e01d601f3b 100644 --- a/src/main/java/mediathek/gui/duplicates/FilmDuplicateEvaluationTask.java +++ b/src/main/java/mediathek/gui/duplicates/FilmDuplicateEvaluationTask.java @@ -11,7 +11,6 @@ import org.apache.logging.log4j.Logger; import java.nio.charset.StandardCharsets; -import java.util.Comparator; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -35,10 +34,12 @@ private void printDuplicateStatistics() { Map statisticsMap = duplicates.parallelStream().collect(Collectors.groupingBy(DatenFilm::getSender, Collectors.counting())); TransactionList tList = new TransactionList<>(statisticsEventList); tList.getReadWriteLock().writeLock().lock(); + tList.beginEvent(true); tList.clear(); for (var sender : statisticsMap.keySet()) { tList.add(new FilmStatistics(sender, statisticsMap.get(sender))); } + tList.commitEvent(); tList.getReadWriteLock().writeLock().unlock(); watch.stop(); @@ -70,50 +71,9 @@ private void checkDuplicates() { urlCache.clear(); } - private void calculateCommonStats() { - Stopwatch watch = Stopwatch.createStarted(); - final var films = listeFilme.parallelStream() - .filter(f -> !f.isLivestream()) - .toList(); - - var statisticsList = Daten.getInstance().getCommonStatistics(); - - Map statisticsMap = films.parallelStream() - .collect(Collectors.groupingBy(DatenFilm::getSender, Collectors.counting())); - TransactionList tList = new TransactionList<>(statisticsList); - tList.getReadWriteLock().writeLock().lock(); - tList.clear(); - for (var sender : statisticsMap.keySet()) { - tList.add(new FilmStatistics(sender, statisticsMap.get(sender))); - } - tList.getReadWriteLock().writeLock().unlock(); - watch.stop(); - - logger.trace("common stats calculation took: {}", watch); - logger.trace("Number of films: {}", films.size()); - } - @Override public void run() { - calculateCommonStats(); checkDuplicates(); printDuplicateStatistics(); } - - private static class BigSenderPenaltyComparator implements Comparator { - @Override - public int compare(DatenFilm s1, DatenFilm s2) { - // "ARD" und "ZDF" immer am Ende um die kleineren Mediatheken nicht zu benachteiligen - final var s1_sender = s1.getSender(); - final var s2_sender = s2.getSender(); - if (s1_sender.equals("ARD") || s1_sender.equals("ZDF")) { - return 1; - } - if (s2_sender.equals("ARD") || s2_sender.equals("ZDF")) { - return -1; - } - // Alphabetisch sortieren für alle anderen - return s1.compareTo(s2); - } - } } diff --git a/src/main/java/mediathek/gui/duplicates/overview/CustomTreeCellRenderer.java b/src/main/java/mediathek/gui/duplicates/overview/CustomTreeCellRenderer.java new file mode 100644 index 0000000000..889c01fbed --- /dev/null +++ b/src/main/java/mediathek/gui/duplicates/overview/CustomTreeCellRenderer.java @@ -0,0 +1,53 @@ +package mediathek.gui.duplicates.overview; + +import mediathek.daten.DatenFilm; + +import javax.swing.*; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeCellRenderer; +import java.awt.*; + +class CustomTreeCellRenderer + extends DefaultTreeCellRenderer { + public Component getTreeCellRendererComponent( + JTree tree, + Object value, + boolean selected, + boolean expanded, + boolean leaf, + int row, + boolean hasFocus) { + // Allow the original renderer to set up the label + Component c = super.getTreeCellRendererComponent( + tree, value, selected, + expanded, leaf, row, + hasFocus); + + var node = (DefaultMutableTreeNode) value; + if (node.isRoot()) + return c; + + switch (node.getUserObject()) { + case DatenFilm f -> { + setText(f.getTitle()); + setToolTipText(prepareTooltipText(f)); + } + case String s -> setText(String.format("%s (%d)", s, node.getChildCount())); + default -> setText(value.toString()); + } + + return c; + } + + private String prepareTooltipText(DatenFilm f) { + var s = """ + + Thema: %s
+ Titel: %s
+ gesendet: %s %s
+ + """; + return String.format(s, f.getThema(), f.getTitle(), + f.getSendeDatum(), f.getSendeZeit()); + } +} diff --git a/src/main/java/mediathek/gui/duplicates/overview/FilmDuplicateOverviewDialog.java b/src/main/java/mediathek/gui/duplicates/overview/FilmDuplicateOverviewDialog.java new file mode 100644 index 0000000000..395571202d --- /dev/null +++ b/src/main/java/mediathek/gui/duplicates/overview/FilmDuplicateOverviewDialog.java @@ -0,0 +1,236 @@ +/* + * Created by JFormDesigner on Wed Oct 23 21:39:11 CEST 2024 + */ + +package mediathek.gui.duplicates.overview; + +import ca.odell.glazedlists.BasicEventList; +import ca.odell.glazedlists.EventList; +import ca.odell.glazedlists.swing.GlazedListsSwing; +import mediathek.config.Daten; +import mediathek.daten.DatenFilm; +import mediathek.gui.duplicates.details.DuplicateFilmDetailsTableFormat; +import mediathek.tool.EscapeKeyHandler; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreeSelectionModel; +import java.awt.*; +import java.util.List; + +/** + * @author christianfranzke + */ +public class FilmDuplicateOverviewDialog extends JDialog { + + private final EventList filmList = new BasicEventList<>(); + + public FilmDuplicateOverviewDialog(Window owner) { + super(owner); + initComponents(); + EscapeKeyHandler.installHandler(this, this::dispose); + + okButton.addActionListener(e -> dispose()); + //must be called for tooltips working + ToolTipManager.sharedInstance().registerComponent(tree); + + var rootNode = new DefaultMutableTreeNode("Filmduplikate", true); + var senderList = getSenderList(); + for (var sender : senderList) { + var node = getDuplicatesForSender(sender); + //skip empty nodes + if (node.getChildCount() > 0) + rootNode.add(node); + } + + var model = GlazedListsSwing.eventTableModelWithThreadProxyList(filmList, new DuplicateFilmDetailsTableFormat()); + table.setModel(model); + resetColumnWidths(); + + var treeModel = new DefaultTreeModel(rootNode); + tree.setModel(treeModel); + var selectionModel = tree.getSelectionModel(); + selectionModel.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); + tree.setCellRenderer(new CustomTreeCellRenderer()); + selectionModel.addTreeSelectionListener(e -> { + var node = (DefaultMutableTreeNode)tree.getLastSelectedPathComponent(); + if (node == null) + return; + if (node.getUserObject() instanceof DatenFilm f) { + var list = Daten.getInstance().getListeFilme() + .parallelStream() + .filter(item -> !item.isLivestream()) + .filter(item -> item.getUrlNormalQuality().equals(f.getUrlNormalQuality()) + && item.getHighQualityUrl().equals(f.getHighQualityUrl())) + .toList(); + + filmList.clear(); + filmList.addAll(list); + calculateColumnWidths(); + } + else { + // just clear table + filmList.clear(); + resetColumnWidths(); + } + }); + } + + private void calculateColumnWidths() { + table.getColumnModel().getColumns().asIterator().forEachRemaining(column -> { + int preferredWidth = column.getMinWidth(); + int maxWidth; + int colIdx = column.getModelIndex(); + var defaultHeaderRenderer = table.getTableHeader().getDefaultRenderer(); + var columnHeaderRenderer = column.getHeaderRenderer(); + if (columnHeaderRenderer == null) + columnHeaderRenderer = defaultHeaderRenderer; + var header = columnHeaderRenderer.getTableCellRendererComponent(table, column.getHeaderValue(), false, false, 0, colIdx); + maxWidth = header.getPreferredSize().width; + + for (int row = 0; row < table.getRowCount(); row++){ + var cellRenderer = table.getCellRenderer(row, colIdx); + var c = table.prepareRenderer(cellRenderer, row, colIdx); + int width = c.getPreferredSize().width + table.getIntercellSpacing().width; + preferredWidth = Math.max(preferredWidth, width); + + // We've exceeded the maximum width, no need to check other rows + if (preferredWidth <= maxWidth){ + preferredWidth = maxWidth; + break; + } + } + column.setPreferredWidth(preferredWidth); + }); + + //update must be manually triggered after width modification + table.doLayout(); + } + + private void resetColumnWidths() { + table.getColumnModel().getColumns().asIterator().forEachRemaining(column -> column.setPreferredWidth(90)); + table.doLayout(); + } + + private DefaultMutableTreeNode getDuplicatesForSender(String sender) + { + var root = new DefaultMutableTreeNode(sender); + + var list = Daten.getInstance().getListeFilme().parallelStream() + .filter(DatenFilm::isDuplicate) + .filter(f -> f.getSender().equals(sender)) + .toList(); + //System.out.println("List size: " + list.size() + " for sender: " + sender); + for (var f : list) { + var node = new DefaultMutableTreeNode(f); + root.add(node); + } + + return root; + } + + private List getSenderList() { + // "ARD" und "ZDF" immer am Ende um die kleineren Mediatheken nicht zu benachteiligen + // Alphabetisch sortieren für alle anderen + + return Daten.getInstance().getListeFilme().parallelStream() + .map(DatenFilm::getSender) + .distinct() + .sorted((o1, o2) -> { + // "ARD" und "ZDF" immer am Ende um die kleineren Mediatheken nicht zu benachteiligen + if (o1.equals("ARD") || o1.equals("ZDF")) { + return 1; + } + if (o2.equals("ARD") || o2.equals("ZDF")) { + return -1; + } + // Alphabetisch sortieren für alle anderen + return o1.compareTo(o2); + }) + .toList(); + } + + 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 splitPane1 = new JSplitPane(); + var scrollPane1 = new JScrollPane(); + tree = new JTree(); + var scrollPane2 = new JScrollPane(); + table = new JTable(); + var buttonBar = new JPanel(); + okButton = new JButton(); + + //======== this ======== + setTitle("\u00dcbersicht aller Duplikate"); //NON-NLS + setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + setModal(true); + setPreferredSize(new Dimension(640, 480)); + var contentPane = getContentPane(); + contentPane.setLayout(new BorderLayout()); + + //======== dialogPane ======== + { + dialogPane.setBorder(new EmptyBorder(12, 12, 12, 12)); + dialogPane.setPreferredSize(new Dimension(800, 600)); + dialogPane.setLayout(new BorderLayout()); + + //======== contentPanel ======== + { + contentPanel.setLayout(new BorderLayout()); + + //======== splitPane1 ======== + { + splitPane1.setDividerLocation(350); + + //======== scrollPane1 ======== + { + scrollPane1.setViewportView(tree); + } + splitPane1.setLeftComponent(scrollPane1); + + //======== scrollPane2 ======== + { + + //---- table ---- + table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); + scrollPane2.setViewportView(table); + } + splitPane1.setRightComponent(scrollPane2); + } + contentPanel.add(splitPane1, BorderLayout.CENTER); + } + 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, 80}; + ((GridBagLayout)buttonBar.getLayout()).columnWeights = new double[] {1.0, 0.0}; + + //---- okButton ---- + okButton.setText("Schlie\u00dfen"); //NON-NLS + buttonBar.add(okButton, new GridBagConstraints(1, 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 JTree tree; + private JTable table; + private JButton okButton; + // JFormDesigner - End of variables declaration //GEN-END:variables @formatter:on +} diff --git a/src/main/java/mediathek/gui/duplicates/overview/FilmDuplicateOverviewDialog.jfd b/src/main/java/mediathek/gui/duplicates/overview/FilmDuplicateOverviewDialog.jfd new file mode 100644 index 0000000000..8be73167e4 --- /dev/null +++ b/src/main/java/mediathek/gui/duplicates/overview/FilmDuplicateOverviewDialog.jfd @@ -0,0 +1,88 @@ +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" + "title": "Übersicht aller Duplikate" + "defaultCloseOperation": 2 + "modal": true + "preferredSize": new java.awt.Dimension( 640, 480 ) + add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class java.awt.BorderLayout ) ) { + name: "dialogPane" + "border": new javax.swing.border.EmptyBorder( 12, 12, 12, 12 ) + "preferredSize": new java.awt.Dimension( 800, 600 ) + auxiliary() { + "JavaCodeGenerator.variableLocal": true + } + add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class java.awt.BorderLayout ) ) { + name: "contentPanel" + auxiliary() { + "JavaCodeGenerator.variableLocal": true + } + add( new FormContainer( "javax.swing.JSplitPane", new FormLayoutManager( class javax.swing.JSplitPane ) ) { + name: "splitPane1" + "dividerLocation": 350 + auxiliary() { + "JavaCodeGenerator.variableLocal": true + } + add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + name: "scrollPane1" + auxiliary() { + "JavaCodeGenerator.variableLocal": true + } + add( new FormComponent( "javax.swing.JTree" ) { + name: "tree" + } ) + }, new FormLayoutConstraints( class java.lang.String ) { + "value": "left" + } ) + add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + name: "scrollPane2" + auxiliary() { + "JavaCodeGenerator.variableLocal": true + } + add( new FormComponent( "javax.swing.JTable" ) { + name: "table" + "autoResizeMode": 0 + } ) + }, new FormLayoutConstraints( class java.lang.String ) { + "value": "right" + } ) + }, new FormLayoutConstraints( class java.lang.String ) { + "value": "Center" + } ) + }, new FormLayoutConstraints( class java.lang.String ) { + "value": "Center" + } ) + add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class java.awt.GridBagLayout ) { + "$columnSpecs": "0:1.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: "okButton" + "text": "Schließen" + }, new FormLayoutConstraints( class com.jformdesigner.runtime.GridBagConstraintsEx ) { + "gridx": 1 + "gridy": 0 + } ) + }, 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( 530, 375 ) + } ) + } +} diff --git a/src/main/java/mediathek/mainwindow/MediathekGui.java b/src/main/java/mediathek/mainwindow/MediathekGui.java index 3c69cb80ad..d66a853f59 100644 --- a/src/main/java/mediathek/mainwindow/MediathekGui.java +++ b/src/main/java/mediathek/mainwindow/MediathekGui.java @@ -25,7 +25,9 @@ import mediathek.gui.dialog.DialogBeenden; import mediathek.gui.dialog.LoadFilmListDialog; import mediathek.gui.dialogEinstellungen.DialogEinstellungen; +import mediathek.gui.duplicates.CommonStatsEvaluationTask; import mediathek.gui.duplicates.FilmDuplicateEvaluationTask; +import mediathek.gui.duplicates.overview.FilmDuplicateOverviewDialog; import mediathek.gui.filmInformation.FilmInfoDialog; import mediathek.gui.history.ResetAboHistoryAction; import mediathek.gui.history.ResetDownloadHistoryAction; @@ -590,6 +592,8 @@ private void loadFilmlist() { swingStatusBar.add(progressLabel); swingStatusBar.add(progressBar); + var evaluateDuplicates = ApplicationConfiguration.getConfiguration().getBoolean(ApplicationConfiguration.FILM_EVALUATE_DUPLICATES, true); + var worker = CompletableFuture.runAsync(() -> { logger.trace("Reading local filmlist"); MessageBus.getMessageBus().publishAsync(new FilmListReadStartEvent()); @@ -605,12 +609,17 @@ private void loadFilmlist() { if (GuiFunktionen.getFilmListUpdateType() == FilmListUpdateType.AUTOMATIC && daten.getListeFilme().needsUpdate()) { daten.getFilmeLaden().loadFilmlist("", true); } - }) - .thenRun(new FilmDuplicateEvaluationTask()) + }); + + if (evaluateDuplicates) { + worker = worker.thenRun(new FilmDuplicateEvaluationTask()); + } + + worker.thenRun(new CommonStatsEvaluationTask()) .thenRun(new RefreshAboWorker(progressLabel, progressBar)) .thenRun(new BlacklistFilterWorker(progressLabel, progressBar)); - if (daten.getListeFilmeNachBlackList() instanceof IndexedFilmList){ + if (daten.getListeFilmeNachBlackList() instanceof IndexedFilmList) { worker = worker.thenRun(new LuceneIndexWorker(progressLabel, progressBar)); } @@ -998,7 +1007,14 @@ private void createViewMenu() { } } jMenuAnsicht.add(showBandwidthUsageAction); + jMenuAnsicht.addSeparator(); jMenuAnsicht.add(showDuplicateStatisticsAction); + var mi = new JMenuItem("Übersicht aller Duplikate anzeigen..."); + mi.addActionListener(l -> { + FilmDuplicateOverviewDialog dlg = new FilmDuplicateOverviewDialog(this); + dlg.setVisible(true); + }); + jMenuAnsicht.add(mi); jMenuAnsicht.addSeparator(); jMenuAnsicht.add(tabFilme.toggleFilterDialogVisibilityAction); jMenuAnsicht.addSeparator(); diff --git a/src/main/java/mediathek/tool/ApplicationConfiguration.java b/src/main/java/mediathek/tool/ApplicationConfiguration.java index 36c0b1fe31..93c580feaa 100644 --- a/src/main/java/mediathek/tool/ApplicationConfiguration.java +++ b/src/main/java/mediathek/tool/ApplicationConfiguration.java @@ -70,6 +70,7 @@ public class ApplicationConfiguration { public static final String SEARCH_USE_FILM_DESCRIPTIONS = "searchfield.film.search_through_description"; public static final String FILM_SHOW_DESCRIPTION = "film.show_description"; + public static final String FILM_EVALUATE_DUPLICATES = "film.evaluate_duplicates"; public static final String CONFIG_AUTOMATIC_UPDATE_CHECK = "application.automatic_update_check"; public static final String CLI_CLIENT_DOWNLOAD_LIST_FORMAT = "cli.client.download_list_format"; private static final String GEO_LOCATION = "geo.location"; diff --git a/src/main/kotlin/mediathek/SplashScreen.kt b/src/main/kotlin/mediathek/SplashScreen.kt index 4ce4fdd9f9..bb02cc0998 100644 --- a/src/main/kotlin/mediathek/SplashScreen.kt +++ b/src/main/kotlin/mediathek/SplashScreen.kt @@ -4,6 +4,7 @@ import mediathek.config.Konstanten import mediathek.tool.TimerPool.timerPool import mediathek.tool.UIProgressState import org.apache.commons.lang3.SystemUtils +import org.jdesktop.swingx.StackLayout import java.awt.Color import java.awt.Cursor import java.awt.Dimension @@ -14,19 +15,28 @@ import javax.swing.* import kotlin.math.roundToInt class SplashScreen : JWindow() { - private var versionLabel: JLabel? = null + private val versionLabel = JLabel() private var curSteps = 0.0 - private var appTitleLabel: JLabel? = null - private var imageLabel: JLabel? = null - private var progressBar: JProgressBar? = null - private var statusLabel: JLabel? = null + private val appTitleLabel = JLabel() + private val imageLabel = JLabel() + private val progressBar = JProgressBar() + private val statusLabel = JLabel() + private val stackPanel = JPanel(StackLayout()) + + + /** + * Needed for linux and windows where AWT doesnt seem to be able to properly draw JWindow black background anymore :( + */ + private val backgroundPanel = JPanel() + private val splashContent = JPanel() + init { initComponents() contentPane.background = Color.BLACK val res = String.format("Version: %s (%s %s)", Konstanten.MVVERSION, osName, SystemUtils.OS_ARCH) - versionLabel!!.text = res - progressBar!!.value = 0 + versionLabel.text = res + progressBar.value = 0 setLocationRelativeTo(null) //strange behaviour on win where window will not come to front or stay there... @@ -64,50 +74,54 @@ class SplashScreen : JWindow() { * @param percentComplete The new percentage. */ private fun updateStatus(statusText: String?, percentComplete: Int) { - appTitleLabel!!.paintImmediately(0, 0, appTitleLabel!!.width, appTitleLabel!!.height) - imageLabel!!.paintImmediately(0, 0, imageLabel!!.width, imageLabel!!.height) - versionLabel!!.paintImmediately(0, 0, versionLabel!!.width, versionLabel!!.height) - statusLabel!!.text = statusText - statusLabel!!.paintImmediately(0, 0, statusLabel!!.width, statusLabel!!.height) - progressBar!!.value = percentComplete - progressBar!!.paintImmediately(0, 0, progressBar!!.width, progressBar!!.height) + if (!SystemUtils.IS_OS_MAC_OSX) + backgroundPanel.paintImmediately(0,0, width, height) + appTitleLabel.paintImmediately(0, 0, appTitleLabel.width, appTitleLabel.height) + imageLabel.paintImmediately(0, 0, imageLabel.width, imageLabel.height) + versionLabel.paintImmediately(0, 0, versionLabel.width, versionLabel.height) + statusLabel.text = statusText + statusLabel.paintImmediately(0, 0, statusLabel.width, statusLabel.height) + progressBar.value = percentComplete + progressBar.paintImmediately(0, 0, progressBar.width, progressBar.height) } private fun initComponents() { - appTitleLabel = JLabel() - versionLabel = JLabel() - imageLabel = JLabel() - progressBar = JProgressBar() - statusLabel = JLabel() minimumSize = Dimension(640, 480) background = Color.black cursor = Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR) isAutoRequestFocus = false foreground = Color.black - val contentPane = contentPane - appTitleLabel!!.text = Konstanten.PROGRAMMNAME - appTitleLabel!!.font = appTitleLabel!!.font.deriveFont( - appTitleLabel!!.font.style or Font.BOLD, - appTitleLabel!!.font.size + 45f + appTitleLabel.text = Konstanten.PROGRAMMNAME + appTitleLabel.font = appTitleLabel.font.deriveFont( + appTitleLabel.font.style or Font.BOLD, + appTitleLabel.font.size + 45f ) - appTitleLabel!!.foreground = Color.white - appTitleLabel!!.background = Color.black - appTitleLabel!!.isOpaque = true - versionLabel!!.text = "Version" - versionLabel!!.isOpaque = true - versionLabel!!.foreground = Color.white - versionLabel!!.background = Color.black - imageLabel!!.icon = ImageIcon(javaClass.getResource("/mediathek/res/MediathekView.png")) - imageLabel!!.background = Color.black - imageLabel!!.isOpaque = true - progressBar!!.value = 50 - progressBar!!.preferredSize = Dimension(146, 10) - statusLabel!!.text = "Status Text Message is here" - statusLabel!!.foreground = Color.white - statusLabel!!.background = Color.black - statusLabel!!.isOpaque = true - val contentPaneLayout = GroupLayout(contentPane) - contentPane.layout = contentPaneLayout + appTitleLabel.foreground = Color.white + appTitleLabel.background = Color.black + appTitleLabel.isOpaque = true + versionLabel.text = "Version" + versionLabel.isOpaque = true + versionLabel.foreground = Color.white + versionLabel.background = Color.black + imageLabel.icon = ImageIcon(javaClass.getResource("/mediathek/res/MediathekView.png")) + imageLabel.background = Color.black + imageLabel.isOpaque = true + progressBar.value = 50 + progressBar.preferredSize = Dimension(146, 10) + statusLabel.text = "Status Text Message is here" + statusLabel.foreground = Color.white + statusLabel.background = Color.black + statusLabel.isOpaque = true + + backgroundPanel.background = Color.black + splashContent.background = Color.black + + stackPanel.add(backgroundPanel, StackLayout.BOTTOM) + stackPanel.add(splashContent, StackLayout.TOP) + contentPane = stackPanel + + val contentPaneLayout = GroupLayout(splashContent) + splashContent.layout = contentPaneLayout contentPaneLayout.setHorizontalGroup( contentPaneLayout.createParallelGroup() .addGroup(