diff --git a/fw/database.ok.png b/fw/database.ok.png new file mode 100644 index 00000000..2f8a7218 Binary files /dev/null and b/fw/database.ok.png differ diff --git a/fw/subtitle.exact.download.png b/fw/subtitle.exact.download.png new file mode 100644 index 00000000..1887340d Binary files /dev/null and b/fw/subtitle.exact.download.png differ diff --git a/fw/subtitle.exact.upload.png b/fw/subtitle.exact.upload.png new file mode 100644 index 00000000..1405bb79 Binary files /dev/null and b/fw/subtitle.exact.upload.png differ diff --git a/source/net/sourceforge/filebot/resources/arrow.down.png b/source/net/sourceforge/filebot/resources/arrow.down.png new file mode 100644 index 00000000..3bc77345 Binary files /dev/null and b/source/net/sourceforge/filebot/resources/arrow.down.png differ diff --git a/source/net/sourceforge/filebot/resources/database.error.png b/source/net/sourceforge/filebot/resources/database.error.png new file mode 100644 index 00000000..578221aa Binary files /dev/null and b/source/net/sourceforge/filebot/resources/database.error.png differ diff --git a/source/net/sourceforge/filebot/resources/database.go.png b/source/net/sourceforge/filebot/resources/database.go.png new file mode 100644 index 00000000..61a8556c Binary files /dev/null and b/source/net/sourceforge/filebot/resources/database.go.png differ diff --git a/source/net/sourceforge/filebot/resources/database.ok.png b/source/net/sourceforge/filebot/resources/database.ok.png new file mode 100644 index 00000000..3188a5c3 Binary files /dev/null and b/source/net/sourceforge/filebot/resources/database.ok.png differ diff --git a/source/net/sourceforge/filebot/resources/subtitle.exact.download.png b/source/net/sourceforge/filebot/resources/subtitle.exact.download.png new file mode 100644 index 00000000..eb76b42c Binary files /dev/null and b/source/net/sourceforge/filebot/resources/subtitle.exact.download.png differ diff --git a/source/net/sourceforge/filebot/resources/subtitle.exact.upload.png b/source/net/sourceforge/filebot/resources/subtitle.exact.upload.png new file mode 100644 index 00000000..bb64b931 Binary files /dev/null and b/source/net/sourceforge/filebot/resources/subtitle.exact.upload.png differ diff --git a/source/net/sourceforge/filebot/ui/AbstractSearchPanel.java b/source/net/sourceforge/filebot/ui/AbstractSearchPanel.java index 81886c0a..52f05b62 100644 --- a/source/net/sourceforge/filebot/ui/AbstractSearchPanel.java +++ b/source/net/sourceforge/filebot/ui/AbstractSearchPanel.java @@ -75,8 +75,8 @@ public abstract class AbstractSearchPanel extends JComponent { searchTextField.getEditor().setAction(searchAction); - searchTextField.getSelectButton().setModel(Arrays.asList(createSearchEngines())); - searchTextField.getSelectButton().setLabelProvider(createSearchEngineLabelProvider()); + searchTextField.getSelectButton().setModel(Arrays.asList(getSearchEngines())); + searchTextField.getSelectButton().setLabelProvider(getSearchEngineLabelProvider()); try { // restore selected subtitle client @@ -101,10 +101,10 @@ public abstract class AbstractSearchPanel extends JComponent { } - protected abstract S[] createSearchEngines(); + protected abstract S[] getSearchEngines(); - protected abstract LabelProvider createSearchEngineLabelProvider(); + protected abstract LabelProvider getSearchEngineLabelProvider(); protected abstract Settings getSettings(); diff --git a/source/net/sourceforge/filebot/ui/panel/episodelist/EpisodeListPanel.java b/source/net/sourceforge/filebot/ui/panel/episodelist/EpisodeListPanel.java index 465f4abe..0d6089a2 100644 --- a/source/net/sourceforge/filebot/ui/panel/episodelist/EpisodeListPanel.java +++ b/source/net/sourceforge/filebot/ui/panel/episodelist/EpisodeListPanel.java @@ -79,7 +79,7 @@ public class EpisodeListPanel extends AbstractSearchPanel createSearchEngineLabelProvider() { + protected LabelProvider getSearchEngineLabelProvider() { return SimpleLabelProvider.forClass(EpisodeListProvider.class); } diff --git a/source/net/sourceforge/filebot/ui/panel/rename/HistoryDialog.java b/source/net/sourceforge/filebot/ui/panel/rename/HistoryDialog.java index 61eb2081..1cd3b138 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/HistoryDialog.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/HistoryDialog.java @@ -449,7 +449,7 @@ class HistoryDialog extends JDialog { type = QUESTION_MESSAGE; options = EnumSet.of(Option.Rename, Option.ChangeDirectory, Option.Cancel); } else { - String text = String.format("Some files are missing. Please select a different directory."); + String text = "Some files are missing. Please select a different directory."; JList missingFilesComponent = new JList(missingFiles.toArray()) { @Override diff --git a/source/net/sourceforge/filebot/ui/panel/subtitle/SimpleComboBox.java b/source/net/sourceforge/filebot/ui/panel/subtitle/SimpleComboBox.java new file mode 100644 index 00000000..170dc354 --- /dev/null +++ b/source/net/sourceforge/filebot/ui/panel/subtitle/SimpleComboBox.java @@ -0,0 +1,70 @@ + +package net.sourceforge.filebot.ui.panel.subtitle; + + +import static javax.swing.BorderFactory.*; + +import java.awt.Color; +import java.awt.Rectangle; + +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.plaf.basic.BasicComboBoxUI; +import javax.swing.plaf.basic.BasicComboPopup; +import javax.swing.plaf.basic.ComboPopup; + +import net.sourceforge.filebot.ResourceManager; + + +public class SimpleComboBox extends JComboBox { + + public SimpleComboBox() { + setUI(new SimpleComboBoxUI()); + setBorder(createEmptyBorder()); + } + + + private static class SimpleComboBoxUI extends BasicComboBoxUI { + + @Override + protected JButton createArrowButton() { + JButton button = new JButton(ResourceManager.getIcon("arrow.down")); + button.setContentAreaFilled(false); + button.setBorderPainted(false); + button.setFocusPainted(false); + button.setOpaque(false); + + return button; + } + + + @Override + protected ComboPopup createPopup() { + return new BasicComboPopup(comboBox) { + + @Override + protected Rectangle computePopupBounds(int px, int py, int pw, int ph) { + Rectangle bounds = super.computePopupBounds(px, py, pw, ph); + + // allow combobox popup to be wider than the combobox itself + bounds.width = Math.max(bounds.width, list.getPreferredSize().width); + + return bounds; + } + + + @Override + protected void configurePopup() { + super.configurePopup(); + + setOpaque(true); + setBackground(list.getBackground()); + + // use gray instead of black border for combobox popup + setBorder(createCompoundBorder(createLineBorder(Color.gray, 1), createEmptyBorder(1, 1, 1, 1))); + } + }; + } + } + +} diff --git a/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitleDropTarget.java b/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitleDropTarget.java new file mode 100644 index 00000000..26205df9 --- /dev/null +++ b/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitleDropTarget.java @@ -0,0 +1,238 @@ + +package net.sourceforge.filebot.ui.panel.subtitle; + + +import static net.sourceforge.filebot.MediaTypes.*; +import static net.sourceforge.filebot.ui.transfer.FileTransferable.*; +import static net.sourceforge.tuned.FileUtilities.*; +import static net.sourceforge.tuned.ui.TunedUtilities.*; + +import java.awt.Color; +import java.awt.Cursor; +import java.awt.dnd.DnDConstants; +import java.awt.dnd.DropTarget; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.dnd.DropTargetEvent; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.swing.Icon; +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JFileChooser; +import javax.swing.filechooser.FileNameExtensionFilter; + +import net.sourceforge.filebot.ResourceManager; +import net.sourceforge.filebot.web.VideoHashSubtitleService; + + +abstract class SubtitleDropTarget extends JButton { + + private enum DropAction { + Download, + Upload, + Cancel + } + + + public SubtitleDropTarget() { + setHorizontalAlignment(CENTER); + + setHideActionText(true); + setBorderPainted(false); + setContentAreaFilled(false); + setFocusPainted(false); + + setBackground(Color.white); + setOpaque(false); + + // initialize with default mode + setDropAction(DropAction.Download); + setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + + // install mouse listener + addActionListener(clickHandler); + + // install drop target + new DropTarget(this, dropHandler); + } + + + private void setDropAction(DropAction dropAction) { + setIcon(getIcon(dropAction)); + } + + + private Icon getIcon(DropAction dropAction) { + switch (dropAction) { + case Download: + return ResourceManager.getIcon("subtitle.exact.download"); + case Upload: + return ResourceManager.getIcon("subtitle.exact.upload"); + default: + return ResourceManager.getIcon("message.error"); + } + } + + + public abstract VideoHashSubtitleService[] getServices(); + + + public abstract String getQueryLanguage(); + + + private boolean handleDownload(List videoFiles) { + VideoHashSubtitleDownloadDialog dialog = new VideoHashSubtitleDownloadDialog(getWindow(this)); + + // initialize download parameters + dialog.setVideoFiles(videoFiles.toArray(new File[0])); + + for (VideoHashSubtitleService service : getServices()) { + dialog.addSubtitleService(service); + } + + // start looking for subtitles + dialog.startQuery(getQueryLanguage()); + + // initialize window properties + dialog.setIconImage(getImage(getIcon(DropAction.Download))); + dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); + dialog.pack(); + + // show dialog + dialog.setLocation(getOffsetLocation(dialog.getOwner())); + dialog.setVisible(true); + + // now it's up to the user + return true; + } + + + private boolean handleUpload(Map videosMappedBySubtitle) { + // TODO implement upload + throw new UnsupportedOperationException("Not implemented yet"); + } + + + private boolean handleDrop(List files) { + // perform a drop action depending on the given files + if (containsOnly(files, VIDEO_FILES)) { + return handleDownload(files); + } + + if (containsOnly(files, FOLDERS)) { + // collect all video files from the dropped folders + return handleDownload(filter(listFiles(files, 0), VIDEO_FILES)); + } + + if (containsOnly(files, SUBTITLE_FILES)) { + // TODO implement upload + throw new UnsupportedOperationException("Not implemented yet"); + } + + if (filter(files, VIDEO_FILES).size() == filter(files, SUBTITLE_FILES).size()) { + // TODO implement upload + throw new UnsupportedOperationException("Not implemented yet"); + } + + return false; + } + + + private DropAction getDropAction(List files) { + // video files only, or any folder, containing video files + if (containsOnly(files, VIDEO_FILES) || (containsOnly(files, FOLDERS) && filter(listFiles(files, 0), VIDEO_FILES).size() > 0)) { + return DropAction.Download; + } + + // subtitle files only, or video/subtitle pairs + if (containsOnly(files, SUBTITLE_FILES) || filter(files, VIDEO_FILES).size() == filter(files, SUBTITLE_FILES).size()) { + return DropAction.Upload; + } + + // unknown input + return DropAction.Cancel; + } + + + private final DropTargetAdapter dropHandler = new DropTargetAdapter() { + + @Override + public void dragEnter(DropTargetDragEvent dtde) { + DropAction dropAction = DropAction.Cancel; + + try { + dropAction = getDropAction(getFilesFromTransferable(dtde.getTransferable())); + } catch (Exception e) { + // ignore + } + + // update visual representation + setDropAction(dropAction); + + // accept or reject + if (dropAction != DropAction.Cancel) { + dtde.acceptDrag(DnDConstants.ACTION_COPY); + } else { + dtde.rejectDrag(); + } + } + + + public void dragExit(DropTargetEvent dte) { + // reset to default state + setDropAction(DropAction.Download); + }; + + + @Override + public void drop(DropTargetDropEvent dtde) { + dtde.acceptDrop(DnDConstants.ACTION_REFERENCE); + + try { + dtde.dropComplete(handleDrop(getFilesFromTransferable(dtde.getTransferable()))); + } catch (Exception e) { + Logger.getLogger("ui").log(Level.WARNING, e.getMessage(), e); + } + + // reset to default state + dragExit(dtde); + } + + }; + + private final ActionListener clickHandler = new ActionListener() { + + @Override + public void actionPerformed(ActionEvent evt) { + JFileChooser chooser = new JFileChooser(); + chooser.setMultiSelectionEnabled(true); + + // collect media file extensions (video and subtitle files) + List extensions = new ArrayList(); + Collections.addAll(extensions, VIDEO_FILES.extensions()); + Collections.addAll(extensions, SUBTITLE_FILES.extensions()); + + chooser.setFileFilter(new FileNameExtensionFilter("Media files", extensions.toArray(new String[0]))); + + if (chooser.showOpenDialog(getWindow(evt.getSource())) == JFileChooser.APPROVE_OPTION) { + List files = Arrays.asList(chooser.getSelectedFiles()); + + if (getDropAction(files) != DropAction.Cancel) { + handleDrop(Arrays.asList(chooser.getSelectedFiles())); + } + } + } + }; + +} diff --git a/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitlePanel.java b/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitlePanel.java index 6066ba51..a9511fe6 100644 --- a/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitlePanel.java +++ b/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitlePanel.java @@ -2,10 +2,14 @@ package net.sourceforge.filebot.ui.panel.subtitle; -import static net.sourceforge.filebot.Settings.*; import static net.sourceforge.filebot.ui.panel.subtitle.LanguageComboBoxModel.*; +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; import java.awt.event.ItemEvent; +import java.awt.geom.Path2D; import java.net.URI; import java.util.AbstractList; import java.util.ArrayList; @@ -19,13 +23,10 @@ import javax.swing.JComboBox; import net.sourceforge.filebot.Settings; import net.sourceforge.filebot.ui.AbstractSearchPanel; import net.sourceforge.filebot.ui.SelectDialog; -import net.sourceforge.filebot.web.OpenSubtitlesClient; import net.sourceforge.filebot.web.SearchResult; -import net.sourceforge.filebot.web.SublightSubtitleClient; -import net.sourceforge.filebot.web.SubsceneSubtitleClient; import net.sourceforge.filebot.web.SubtitleDescriptor; import net.sourceforge.filebot.web.SubtitleProvider; -import net.sourceforge.filebot.web.SubtitleSourceClient; +import net.sourceforge.filebot.web.VideoHashSubtitleService; import net.sourceforge.tuned.PreferencesList; import net.sourceforge.tuned.PreferencesMap.PreferencesEntry; import net.sourceforge.tuned.ui.LabelProvider; @@ -92,22 +93,60 @@ public class SubtitlePanel extends AbstractSearchPanel createSearchEngineLabelProvider() { + protected LabelProvider getSearchEngineLabelProvider() { return SimpleLabelProvider.forClass(SubtitleProvider.class); } diff --git a/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitleServices.java b/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitleServices.java new file mode 100644 index 00000000..5ca7ca1f --- /dev/null +++ b/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitleServices.java @@ -0,0 +1,41 @@ + +package net.sourceforge.filebot.ui.panel.subtitle; + + +import static net.sourceforge.filebot.Settings.*; + +import net.sourceforge.filebot.web.OpenSubtitlesClient; +import net.sourceforge.filebot.web.SublightSubtitleClient; +import net.sourceforge.filebot.web.SubsceneSubtitleClient; +import net.sourceforge.filebot.web.SubtitleProvider; +import net.sourceforge.filebot.web.SubtitleSourceClient; +import net.sourceforge.filebot.web.VideoHashSubtitleService; + + +final class SubtitleServices { + + public static final OpenSubtitlesClient openSubtitlesClient = new OpenSubtitlesClient(String.format("%s %s", getApplicationName(), getApplicationVersion())); + public static final SublightSubtitleClient sublightSubtitleClient = new SublightSubtitleClient(getApplicationName(), getApplicationProperty("sublight.apikey")); + + public static final SubsceneSubtitleClient subsceneSubtitleClient = new SubsceneSubtitleClient(); + public static final SubtitleSourceClient subtitleSourceClient = new SubtitleSourceClient(); + + + public static SubtitleProvider[] getSubtitleProviders() { + return new SubtitleProvider[] { openSubtitlesClient, subsceneSubtitleClient, sublightSubtitleClient, subtitleSourceClient }; + } + + + public static VideoHashSubtitleService[] getVideoHashSubtitleServices() { + return new VideoHashSubtitleService[] { openSubtitlesClient, sublightSubtitleClient }; + } + + + /** + * Dummy constructor to prevent instantiation. + */ + private SubtitleServices() { + throw new UnsupportedOperationException(); + } + +} diff --git a/source/net/sourceforge/filebot/ui/panel/subtitle/VideoHashSubtitleDownloadDialog.java b/source/net/sourceforge/filebot/ui/panel/subtitle/VideoHashSubtitleDownloadDialog.java new file mode 100644 index 00000000..73c78a65 --- /dev/null +++ b/source/net/sourceforge/filebot/ui/panel/subtitle/VideoHashSubtitleDownloadDialog.java @@ -0,0 +1,804 @@ + +package net.sourceforge.filebot.ui.panel.subtitle; + + +import static javax.swing.BorderFactory.*; +import static javax.swing.JOptionPane.*; +import static net.sourceforge.filebot.ui.panel.subtitle.SubtitleUtilities.*; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.io.File; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.DefaultCellEditor; +import javax.swing.DefaultComboBoxModel; +import javax.swing.DefaultListCellRenderer; +import javax.swing.DefaultListSelectionModel; +import javax.swing.Icon; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JList; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.SwingWorker; +import javax.swing.SwingWorker.StateValue; +import javax.swing.border.Border; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.DefaultTableCellRenderer; + +import net.miginfocom.swing.MigLayout; +import net.sourceforge.filebot.ResourceManager; +import net.sourceforge.filebot.web.SubtitleDescriptor; +import net.sourceforge.filebot.web.VideoHashSubtitleService; +import net.sourceforge.tuned.FileUtilities; +import net.sourceforge.tuned.ui.AbstractBean; +import net.sourceforge.tuned.ui.LinkButton; +import net.sourceforge.tuned.ui.RoundBorder; + + +class VideoHashSubtitleDownloadDialog extends JDialog { + + private final JPanel servicePanel = new JPanel(new MigLayout()); + private final List services = new ArrayList(); + + private final JTable subtitleMappingTable = createTable(); + + private ExecutorService downloadService; + + + public VideoHashSubtitleDownloadDialog(Window owner) { + super(owner, "Download Subtitles", ModalityType.MODELESS); + + JComponent content = (JComponent) getContentPane(); + content.setLayout(new MigLayout("fill, insets dialog, nogrid", "", "[fill][pref!]")); + + servicePanel.setBorder(new RoundBorder()); + servicePanel.setOpaque(false); + servicePanel.setBackground(new Color(0xFAFAD2)); // LightGoldenRodYellow + + content.add(new JScrollPane(subtitleMappingTable), "grow, wrap"); + content.add(servicePanel, "gap after indent*2"); + + content.add(new JButton(downloadAction), "tag ok"); + content.add(new JButton(finishAction), "tag cancel"); + } + + + protected JTable createTable() { + JTable table = new JTable(new SubtitleMappingTableModel()); + table.setDefaultRenderer(SubtitleMapping.class, new SubtitleMappingOptionRenderer()); + + table.setRowHeight(24); + table.setIntercellSpacing(new Dimension(5, 5)); + + table.setBackground(Color.white); + table.setAutoCreateRowSorter(true); + table.setFillsViewportHeight(true); + + JComboBox editor = new SimpleComboBox(); + editor.setRenderer(new SubtitleOptionRenderer()); + editor.setFocusable(false); + + table.setDefaultEditor(SubtitleMapping.class, new DefaultCellEditor(editor) { + + @Override + public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { + JComboBox editor = (JComboBox) super.getTableCellEditorComponent(table, value, isSelected, row, column); + + SubtitleMapping mapping = (SubtitleMapping) value; + editor.setModel(new DefaultComboBoxModel(mapping.getOptions())); + editor.setSelectedItem(mapping.getSelectedOption()); + + return editor; + } + }); + + // disable selection + table.setSelectionModel(new DefaultListSelectionModel() { + + @Override + public void addSelectionInterval(int from, int to) { + // ignore + } + + + @Override + public void setSelectionInterval(int from, int to) { + // ignore + } + + + @Override + public void setAnchorSelectionIndex(int anchorIndex) { + // ignore + } + + + @Override + public void setLeadSelectionIndex(int leadIndex) { + // ignore + } + }); + + return table; + } + + + public void setVideoFiles(File[] videoFiles) { + subtitleMappingTable.setModel(new SubtitleMappingTableModel(videoFiles)); + } + + + public void addSubtitleService(final VideoHashSubtitleService service) { + final VideoHashSubtitleServiceBean serviceBean = new VideoHashSubtitleServiceBean(service); + final LinkButton component = new LinkButton(serviceBean.getName(), ResourceManager.getIcon("database.go"), serviceBean.getLink()); + + serviceBean.addPropertyChangeListener(new PropertyChangeListener() { + + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (serviceBean.getState() == StateValue.STARTED) { + component.setIcon(ResourceManager.getIcon("database.go")); + } else { + component.setIcon(ResourceManager.getIcon(serviceBean.getError() == null ? "database.ok" : "database.error")); + } + + component.setToolTipText(serviceBean.getError() == null ? null : serviceBean.getError().getMessage()); + } + }); + + services.add(serviceBean); + servicePanel.add(component); + } + + + public void startQuery(String languageName) { + final SubtitleMappingTableModel mappingModel = (SubtitleMappingTableModel) subtitleMappingTable.getModel(); + + // query services concurrently + for (VideoHashSubtitleServiceBean service : services) { + QueryTask task = new QueryTask(service, mappingModel.getVideoFiles(), languageName) { + + @Override + protected void done() { + try { + Map> subtitles = get(); + + // update subtitle options + for (SubtitleMapping subtitleMapping : mappingModel) { + List options = subtitles.get(subtitleMapping.getVideoFile()); + + if (options != null && options.size() > 0) { + subtitleMapping.addOptions(options); + } + } + + // make subtitle column visible + mappingModel.setOptionColumnVisible(true); + } catch (Exception e) { + Logger.getLogger(getClass().getName()).log(Level.WARNING, e.getMessage(), e); + } + } + }; + + // start background worker + task.execute(); + } + } + + + private Boolean showConfirmReplaceDialog(List files) { + JList existingFilesComponent = new JList(files.toArray()) { + + @Override + public Dimension getPreferredScrollableViewportSize() { + // adjust component size + return new Dimension(80, 50); + } + }; + + Object[] message = new Object[] { "Replace existing subtitle files?", new JScrollPane(existingFilesComponent) }; + Object[] options = new Object[] { "Replace All", "Skip All", "Cancel" }; + JOptionPane optionPane = new JOptionPane(message, WARNING_MESSAGE, YES_NO_CANCEL_OPTION, null, options); + + // display option dialog + optionPane.createDialog(VideoHashSubtitleDownloadDialog.this, "Replace").setVisible(true); + + // replace all + if (options[0] == optionPane.getValue()) + return true; + + // skip all + if (options[1] == optionPane.getValue()) + return false; + + // cancel + return null; + } + + + private final Action downloadAction = new AbstractAction("Download", ResourceManager.getIcon("dialog.continue")) { + + @Override + public void actionPerformed(ActionEvent evt) { + // don't allow restart of download as long as there are still unfinished download tasks + if (downloadService != null && !downloadService.isTerminated()) { + return; + } + + final SubtitleMappingTableModel mappingModel = (SubtitleMappingTableModel) subtitleMappingTable.getModel(); + + // collect the subtitles that will be fetched + List downloadQueue = new ArrayList(); + + for (SubtitleMapping mapping : mappingModel) { + SubtitleDescriptorBean subtitleBean = mapping.getSelectedOption(); + + if (subtitleBean != null && subtitleBean.getState() == null) { + downloadQueue.add(new DownloadTask(subtitleBean, mapping.getSubtitleFile())); + } + } + + // collect downloads that will override a file + List confirmReplaceDownloadQueue = new ArrayList(); + List existingFiles = new ArrayList(); + + for (DownloadTask download : downloadQueue) { + if (download.getDestination().exists()) { + confirmReplaceDownloadQueue.add(download); + existingFiles.add(download.getDestination().getName()); + } + } + + // confirm replace + if (confirmReplaceDownloadQueue.size() > 0) { + Boolean option = showConfirmReplaceDialog(existingFiles); + + // abort the operation altogether + if (option == null) { + return; + } + + // don't replace any files + if (option == false) { + downloadQueue.removeAll(confirmReplaceDownloadQueue); + } + } + + // start download + if (downloadQueue.size() > 0) { + downloadService = Executors.newSingleThreadExecutor(); + + for (DownloadTask downloadTask : downloadQueue) { + downloadTask.getSubtitleBean().setState(StateValue.PENDING); + downloadService.execute(downloadTask); + } + + // terminate after all downloads have been completed + downloadService.shutdown(); + } + } + }; + + private final Action finishAction = new AbstractAction("Close", ResourceManager.getIcon("dialog.cancel")) { + + @Override + public void actionPerformed(ActionEvent evt) { + if (downloadService != null) { + downloadService.shutdownNow(); + } + + setVisible(false); + dispose(); + } + }; + + + private static class SubtitleMappingOptionRenderer extends DefaultTableCellRenderer { + + private final JComboBox optionComboBox = new SimpleComboBox(); + + + public SubtitleMappingOptionRenderer() { + optionComboBox.setRenderer(new SubtitleOptionRenderer()); + } + + + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { + SubtitleMapping mapping = (SubtitleMapping) value; + SubtitleDescriptorBean subtitleBean = mapping.getSelectedOption(); + + // render combobox for subtitle options + if (mapping.isEditable()) { + optionComboBox.setModel(new DefaultComboBoxModel(new Object[] { subtitleBean })); + return optionComboBox; + } + + // render status label + super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + setForeground(table.getForeground()); + + if (subtitleBean == null) { + // no subtitles found + setText("No subtitles found"); + setIcon(null); + setForeground(Color.gray); + } else if (subtitleBean.getState() == StateValue.PENDING) { + // download in the queue + setText(subtitleBean.getText()); + setIcon(ResourceManager.getIcon("worker.pending")); + } else if (subtitleBean.getState() == StateValue.STARTED) { + // download in progress + setText(subtitleBean.getText()); + setIcon(ResourceManager.getIcon("action.fetch")); + } else { + // download complete + setText(mapping.getSubtitleFile().getName()); + setIcon(ResourceManager.getIcon("status.ok")); + } + + return this; + } + } + + + private static class SubtitleOptionRenderer extends DefaultListCellRenderer { + + private final Border padding = createEmptyBorder(3, 3, 3, 3); + + + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { + super.getListCellRendererComponent(list, null, index, isSelected, cellHasFocus); + setBorder(padding); + + SubtitleDescriptorBean subtitleBean = (SubtitleDescriptorBean) value; + setText(subtitleBean.getText()); + setIcon(subtitleBean.getError() == null ? subtitleBean.getIcon() : ResourceManager.getIcon("status.warning")); + + return this; + } + } + + + private static class SubtitleMappingTableModel extends AbstractTableModel implements Iterable { + + private final SubtitleMapping[] data; + + private boolean optionColumnVisible = false; + + + public SubtitleMappingTableModel(File... videoFiles) { + data = new SubtitleMapping[videoFiles.length]; + + for (int i = 0; i < videoFiles.length; i++) { + data[i] = new SubtitleMapping(videoFiles[i]); + data[i].addPropertyChangeListener(new SubtitleMappingListener(i)); + } + } + + + public List getVideoFiles() { + return new AbstractList() { + + @Override + public File get(int index) { + return data[index].getVideoFile(); + } + + + @Override + public int size() { + return data.length; + } + }; + } + + + @Override + public Iterator iterator() { + return Arrays.asList(data).iterator(); + } + + + public void setOptionColumnVisible(boolean optionColumnVisible) { + this.optionColumnVisible = optionColumnVisible; + + // update columns + fireTableStructureChanged(); + } + + + @Override + public int getColumnCount() { + return optionColumnVisible ? 2 : 1; + } + + + @Override + public String getColumnName(int column) { + switch (column) { + case 0: + return "Video"; + case 1: + return "Subtitle"; + } + + return null; + } + + + @Override + public int getRowCount() { + return data.length; + } + + + @Override + public Object getValueAt(int row, int column) { + switch (column) { + case 0: + return data[row].getVideoFile().getName(); + case 1: + return data[row]; + } + + return null; + } + + + @Override + public void setValueAt(Object value, int row, int column) { + data[row].setSelectedOption((SubtitleDescriptorBean) value); + } + + + @Override + public boolean isCellEditable(int row, int column) { + return column == 1 && data[row].isEditable(); + } + + + @Override + public Class getColumnClass(int column) { + switch (column) { + case 0: + return String.class; + case 1: + return SubtitleMapping.class; + } + + return null; + } + + + private class SubtitleMappingListener implements PropertyChangeListener { + + private final int index; + + + public SubtitleMappingListener(int index) { + this.index = index; + } + + + @Override + public void propertyChange(PropertyChangeEvent evt) { + // update state and subtitle options + if (optionColumnVisible) { + fireTableCellUpdated(index, 1); + } + } + } + } + + + private static class SubtitleMapping extends AbstractBean { + + private final File videoFile; + + private SubtitleDescriptorBean selectedOption; + private List options = new ArrayList(); + + + public SubtitleMapping(File videoFile) { + this.videoFile = videoFile; + } + + + public File getVideoFile() { + return videoFile; + } + + + public File getSubtitleFile() { + return new File(videoFile.getParentFile(), FileUtilities.getName(videoFile) + '.' + selectedOption.getType()); + } + + + public boolean isEditable() { + return selectedOption != null && (selectedOption.getState() == null || selectedOption.getError() != null); + } + + + public SubtitleDescriptorBean getSelectedOption() { + return selectedOption; + } + + + public void setSelectedOption(SubtitleDescriptorBean selectedOption) { + if (this.selectedOption != null) { + this.selectedOption.removePropertyChangeListener(selectedOptionListener); + } + + this.selectedOption = selectedOption; + this.selectedOption.addPropertyChangeListener(selectedOptionListener); + + firePropertyChange("selectedOption", null, this.selectedOption); + } + + + public SubtitleDescriptorBean[] getOptions() { + return options.toArray(new SubtitleDescriptorBean[0]); + } + + + public void addOptions(List options) { + this.options.addAll(options); + + if (selectedOption == null && options.size() > 0) { + setSelectedOption(options.get(0)); + } + } + + + private final PropertyChangeListener selectedOptionListener = new PropertyChangeListener() { + + @Override + public void propertyChange(PropertyChangeEvent evt) { + firePropertyChange("selectedOption", null, selectedOption); + } + }; + } + + + private static class SubtitleDescriptorBean extends AbstractBean { + + private final SubtitleDescriptor subtitle; + private final VideoHashSubtitleServiceBean source; + + private StateValue state; + private Exception error; + + + public SubtitleDescriptorBean(SubtitleDescriptor subtitle, VideoHashSubtitleServiceBean source) { + this.subtitle = subtitle; + this.source = source; + } + + + public String getText() { + return subtitle.getName() + '.' + subtitle.getType(); + } + + + public Icon getIcon() { + return source.getIcon(); + } + + + public String getType() { + return subtitle.getType(); + } + + + public ByteBuffer fetch() throws Exception { + setState(StateValue.STARTED); + + try { + return subtitle.fetch(); + } catch (Exception e) { + // remember exception + error = e; + + // rethrow exception + throw e; + } finally { + setState(StateValue.DONE); + } + } + + + public Exception getError() { + return error; + } + + + public StateValue getState() { + return state; + } + + + public void setState(StateValue state) { + this.state = state; + firePropertyChange("state", null, this.state); + } + + + @Override + public String toString() { + return getText(); + } + } + + + private static class QueryTask extends SwingWorker>, Void> { + + private final VideoHashSubtitleServiceBean service; + + private final File[] videoFiles; + private final String languageName; + + + public QueryTask(VideoHashSubtitleServiceBean service, Collection videoFiles, String languageName) { + this.service = service; + this.videoFiles = videoFiles.toArray(new File[0]); + this.languageName = languageName; + } + + + @Override + protected Map> doInBackground() throws Exception { + Map> subtitleSet = new HashMap>(); + + for (final Entry> result : service.getSubtitleList(videoFiles, languageName).entrySet()) { + List subtitles = new ArrayList(); + + // associate subtitles with services + for (SubtitleDescriptor subtitleDescriptor : result.getValue()) { + subtitles.add(new SubtitleDescriptorBean(subtitleDescriptor, service)); + } + + subtitleSet.put(result.getKey(), subtitles); + } + + return subtitleSet; + } + } + + + private static class DownloadTask extends SwingWorker { + + private final SubtitleDescriptorBean subtitle; + private final File destination; + + + public DownloadTask(SubtitleDescriptorBean subtitle, File destination) { + this.subtitle = subtitle; + this.destination = destination; + } + + + public SubtitleDescriptorBean getSubtitleBean() { + return subtitle; + } + + + public File getDestination() { + return destination; + } + + + @Override + protected File doInBackground() { + try { + // fetch subtitle + ByteBuffer data = subtitle.fetch(); + + if (isCancelled()) + return null; + + // save to file + write(data, destination); + + return destination; + } catch (Exception e) { + Logger.getLogger("ui").log(Level.WARNING, e.getMessage(), e); + } + + return null; + } + } + + + private static class VideoHashSubtitleServiceBean extends AbstractBean { + + private final VideoHashSubtitleService service; + + private StateValue state; + private Throwable error; + + + public VideoHashSubtitleServiceBean(VideoHashSubtitleService service) { + this.service = service; + this.state = StateValue.PENDING; + } + + + public String getName() { + return service.getName(); + } + + + public Icon getIcon() { + return service.getIcon(); + } + + + public URI getLink() { + return service.getLink(); + } + + + public Map> getSubtitleList(File[] files, String languageName) throws Exception { + setState(StateValue.STARTED); + + try { + return service.getSubtitleList(files, languageName); + } catch (Exception e) { + // remember error + error = e; + + // rethrow error + throw e; + } finally { + setState(StateValue.DONE); + } + } + + + private void setState(StateValue state) { + this.state = state; + firePropertyChange("state", null, this.state); + } + + + public StateValue getState() { + return state; + } + + + public Throwable getError() { + return error; + } + + } + +} diff --git a/source/net/sourceforge/filebot/web/OpenSubtitlesClient.java b/source/net/sourceforge/filebot/web/OpenSubtitlesClient.java index 9e95a8a0..28070dd8 100644 --- a/source/net/sourceforge/filebot/web/OpenSubtitlesClient.java +++ b/source/net/sourceforge/filebot/web/OpenSubtitlesClient.java @@ -44,6 +44,12 @@ public class OpenSubtitlesClient implements SubtitleProvider, VideoHashSubtitleS } + @Override + public URI getLink() { + return URI.create("http://www.opensubtitles.org"); + } + + @Override public Icon getIcon() { return ResourceManager.getIcon("search.opensubtitles"); diff --git a/source/net/sourceforge/filebot/web/SublightSubtitleClient.java b/source/net/sourceforge/filebot/web/SublightSubtitleClient.java index 3140c599..5a5ddd0b 100644 --- a/source/net/sourceforge/filebot/web/SublightSubtitleClient.java +++ b/source/net/sourceforge/filebot/web/SublightSubtitleClient.java @@ -58,6 +58,12 @@ public class SublightSubtitleClient implements SubtitleProvider, VideoHashSubtit } + @Override + public URI getLink() { + return URI.create("http://www.sublight.si"); + } + + @Override public Icon getIcon() { return ResourceManager.getIcon("search.sublight"); @@ -131,7 +137,7 @@ public class SublightSubtitleClient implements SubtitleProvider, VideoHashSubtit } } catch (LinkageError e) { // MediaInfo native lib not available - throw new UnsupportedOperationException(e); + throw new UnsupportedOperationException(e.getMessage(), e); } return subtitles; @@ -292,7 +298,8 @@ public class SublightSubtitleClient implements SubtitleProvider, VideoHashSubtit @Override public URI getSubtitleListLink(SearchResult searchResult, String languageName) { - return null; + // note that sublight can only be accessed via the soap API + return URI.create("http://www.sublight.si/SearchSubtitles.aspx"); } diff --git a/source/net/sourceforge/filebot/web/SubsceneSubtitleClient.java b/source/net/sourceforge/filebot/web/SubsceneSubtitleClient.java index 3033adfc..e74bfecf 100644 --- a/source/net/sourceforge/filebot/web/SubsceneSubtitleClient.java +++ b/source/net/sourceforge/filebot/web/SubsceneSubtitleClient.java @@ -44,6 +44,12 @@ public class SubsceneSubtitleClient implements SubtitleProvider { } + @Override + public URI getLink() { + return URI.create("http://subscene.com"); + } + + @Override public Icon getIcon() { return ResourceManager.getIcon("search.subscene"); diff --git a/source/net/sourceforge/filebot/web/SubtitleProvider.java b/source/net/sourceforge/filebot/web/SubtitleProvider.java index 94ddf8bf..2a3b61dc 100644 --- a/source/net/sourceforge/filebot/web/SubtitleProvider.java +++ b/source/net/sourceforge/filebot/web/SubtitleProvider.java @@ -22,6 +22,9 @@ public interface SubtitleProvider { public String getName(); + public URI getLink(); + + public Icon getIcon(); } diff --git a/source/net/sourceforge/filebot/web/SubtitleSourceClient.java b/source/net/sourceforge/filebot/web/SubtitleSourceClient.java index f3134f71..9b6bf83d 100644 --- a/source/net/sourceforge/filebot/web/SubtitleSourceClient.java +++ b/source/net/sourceforge/filebot/web/SubtitleSourceClient.java @@ -15,11 +15,11 @@ import java.util.Map; import javax.swing.Icon; -import net.sourceforge.filebot.ResourceManager; - import org.w3c.dom.Document; import org.w3c.dom.Node; +import net.sourceforge.filebot.ResourceManager; + public class SubtitleSourceClient implements SubtitleProvider { @@ -34,6 +34,12 @@ public class SubtitleSourceClient implements SubtitleProvider { } + @Override + public URI getLink() { + return URI.create("http://www.subtitlesource.org"); + } + + @Override public Icon getIcon() { return ResourceManager.getIcon("search.subtitlesource"); diff --git a/source/net/sourceforge/filebot/web/VideoHashSubtitleService.java b/source/net/sourceforge/filebot/web/VideoHashSubtitleService.java index 98402cea..fa906b5f 100644 --- a/source/net/sourceforge/filebot/web/VideoHashSubtitleService.java +++ b/source/net/sourceforge/filebot/web/VideoHashSubtitleService.java @@ -3,9 +3,12 @@ package net.sourceforge.filebot.web; import java.io.File; +import java.net.URI; import java.util.List; import java.util.Map; +import javax.swing.Icon; + public interface VideoHashSubtitleService { @@ -14,4 +17,13 @@ public interface VideoHashSubtitleService { public boolean publishSubtitle(int imdbid, String languageName, File videoFile, File subtitleFile) throws Exception; + + public String getName(); + + + public URI getLink(); + + + public Icon getIcon(); + } diff --git a/source/net/sourceforge/tuned/FileUtilities.java b/source/net/sourceforge/tuned/FileUtilities.java index 4c2ec31f..dd7f83d8 100644 --- a/source/net/sourceforge/tuned/FileUtilities.java +++ b/source/net/sourceforge/tuned/FileUtilities.java @@ -140,6 +140,32 @@ public final class FileUtilities { } + public static List listFiles(Iterable folders, int maxDepth) { + List files = new ArrayList(); + + // collect files from directory tree + for (File folder : folders) { + listFiles(folder, 0, files, maxDepth); + } + + return files; + } + + + private static void listFiles(File folder, int depth, List files, int maxDepth) { + if (depth > maxDepth) + return; + + for (File file : folder.listFiles()) { + if (file.isDirectory()) { + listFiles(file, depth + 1, files, maxDepth); + } else { + files.add(file); + } + } + } + + /** * Invalid filename characters: \, /, :, *, ?, ", <, >, |, \r and \n */ diff --git a/source/net/sourceforge/tuned/ui/AbstractBean.java b/source/net/sourceforge/tuned/ui/AbstractBean.java new file mode 100644 index 00000000..04fc9c67 --- /dev/null +++ b/source/net/sourceforge/tuned/ui/AbstractBean.java @@ -0,0 +1,46 @@ + +package net.sourceforge.tuned.ui; + + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; + +import javax.swing.event.SwingPropertyChangeSupport; + + +public abstract class AbstractBean { + + private final PropertyChangeSupport pcs; + + + public AbstractBean() { + pcs = new SwingPropertyChangeSupport(this, true); + } + + + protected void firePropertyChange(String propertyName, Object oldValue, Object newValue) { + pcs.firePropertyChange(propertyName, oldValue, newValue); + } + + + protected void firePropertyChange(PropertyChangeEvent e) { + pcs.firePropertyChange(e); + } + + + public void addPropertyChangeListener(PropertyChangeListener listener) { + pcs.addPropertyChangeListener(listener); + } + + + public void removePropertyChangeListener(PropertyChangeListener listener) { + pcs.removePropertyChangeListener(listener); + } + + + public PropertyChangeListener[] getPropertyChangeListeners() { + return pcs.getPropertyChangeListeners(); + } + +} diff --git a/source/net/sourceforge/tuned/ui/RoundBorder.java b/source/net/sourceforge/tuned/ui/RoundBorder.java new file mode 100644 index 00000000..fd663a38 --- /dev/null +++ b/source/net/sourceforge/tuned/ui/RoundBorder.java @@ -0,0 +1,73 @@ + +package net.sourceforge.tuned.ui; + + +import java.awt.Color; +import java.awt.Component; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Insets; +import java.awt.RenderingHints; + +import javax.swing.border.AbstractBorder; + + +public class RoundBorder extends AbstractBorder { + + private final Color color; + private final Insets insets; + private final int arc; + + + public RoundBorder() { + this.color = new Color(0xACACAC); + this.arc = 12; + this.insets = new Insets(1, 1, 1, 1); + } + + + public RoundBorder(Color color, int arc, Insets insets) { + this.color = color; + this.arc = arc; + this.insets = insets; + } + + + @Override + public boolean isBorderOpaque() { + return false; + } + + + @Override + public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) { + Graphics2D g2d = (Graphics2D) g.create(); + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + g2d.setPaint(c.getBackground()); + g2d.fillRoundRect(x, y, width - 1, height - 1, arc, arc); + + g2d.setPaint(color); + g2d.drawRoundRect(x, y, width - 1, height - 1, arc, arc); + + g2d.dispose(); + } + + + @Override + public Insets getBorderInsets(Component c) { + return new Insets(insets.top, insets.left, insets.bottom, insets.right); + } + + + @Override + public Insets getBorderInsets(Component c, Insets insets) { + insets.top = this.insets.top; + insets.left = this.insets.left; + insets.bottom = this.insets.bottom; + insets.right = this.insets.right; + + return insets; + } + +}