diff --git a/source/net/sourceforge/filebot/archive/Archive.java b/source/net/sourceforge/filebot/archive/Archive.java index 2e3f6977..26097027 100644 --- a/source/net/sourceforge/filebot/archive/Archive.java +++ b/source/net/sourceforge/filebot/archive/Archive.java @@ -13,7 +13,9 @@ import java.util.ArrayList; import java.util.EnumMap; import java.util.List; import java.util.Map; +import java.util.Scanner; import java.util.logging.Logger; +import java.util.regex.Matcher; import java.util.regex.Pattern; import com.sun.jna.Platform; @@ -112,13 +114,17 @@ public class Archive implements Closeable { public static final FileFilter VOLUME_ONE_FILTER = new FileFilter() { - private Pattern exclude = Pattern.compile("[.]r[0-9]+$|[.]part[0-9]*[2-9][.]rar$|[.][0-9]*[2-9]$", Pattern.CASE_INSENSITIVE); + private Pattern volume = Pattern.compile("[.]r[0-9]+$|[.]part[0-9]+|[.]rar$|[.][0-9]+$", Pattern.CASE_INSENSITIVE); @Override public boolean accept(File path) { - if (exclude.matcher(path.getName()).find()) { - return false; + Matcher matcher = volume.matcher(path.getName()); + if (matcher.find()) { + Scanner scanner = new Scanner(matcher.group()).useDelimiter("\\D+"); + if (!scanner.hasNext() || scanner.nextInt() != 1) { + return false; + } } String ext = getExtension(path.getName()); diff --git a/source/net/sourceforge/filebot/cli/CmdlineOperations.java b/source/net/sourceforge/filebot/cli/CmdlineOperations.java index 80d651b5..2d22107a 100644 --- a/source/net/sourceforge/filebot/cli/CmdlineOperations.java +++ b/source/net/sourceforge/filebot/cli/CmdlineOperations.java @@ -922,18 +922,22 @@ public class CmdlineOperations implements CmdlineInterface { for (File file : archiveFiles) { Archive archive = new Archive(file); - File outputFolder = (output != null) ? new File(output).getAbsoluteFile() : new File(file.getParentFile(), getNameWithoutExtension(file.getName())); - - CLILogger.info(String.format("Extract archive [%s] to [%s]", file.getName(), outputFolder)); - FileMapper outputMapper = new FileMapper(outputFolder, false); - - List entries = archive.listFiles(); - for (File entry : entries) { - extractedFiles.add(outputMapper.getOutputFile(entry)); + try { + File outputFolder = (output != null) ? new File(output).getAbsoluteFile() : new File(file.getParentFile(), getNameWithoutExtension(file.getName())); + + CLILogger.info(String.format("Extract archive [%s] to [%s]", file.getName(), outputFolder)); + FileMapper outputMapper = new FileMapper(outputFolder, false); + + List entries = archive.listFiles(); + for (File entry : entries) { + extractedFiles.add(outputMapper.getOutputFile(entry)); + } + + CLILogger.finest("Extract files " + entries); + archive.extract(outputMapper); + } finally { + archive.close(); } - - CLILogger.finest("Extract files " + entries); - archive.extract(outputMapper); } return extractedFiles; diff --git a/source/net/sourceforge/filebot/ui/analyze/AnalyzePanel.java b/source/net/sourceforge/filebot/ui/analyze/AnalyzePanel.java index 2016f2c1..72675ad8 100644 --- a/source/net/sourceforge/filebot/ui/analyze/AnalyzePanel.java +++ b/source/net/sourceforge/filebot/ui/analyze/AnalyzePanel.java @@ -26,19 +26,21 @@ public class AnalyzePanel extends JComponent { add(fileTreePanel, "grow, sizegroupx column"); add(toolsPanel, "grow, sizegroupx column"); - addTool(new TypeTool()); + addTool(new ExtractTool()); addTool(new SplitTool()); + addTool(new TypeTool()); putClientProperty("transferablePolicy", fileTreePanel.getTransferablePolicy()); fileTreePanel.addPropertyChangeListener("filetree", filetreeListener); } - + private void addTool(Tool tool) { toolsPanel.addTab(tool.getName(), tool); } + private final PropertyChangeListener filetreeListener = new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent evt) { diff --git a/source/net/sourceforge/filebot/ui/analyze/ExtractTool.java b/source/net/sourceforge/filebot/ui/analyze/ExtractTool.java new file mode 100644 index 00000000..cf8c907d --- /dev/null +++ b/source/net/sourceforge/filebot/ui/analyze/ExtractTool.java @@ -0,0 +1,291 @@ + +package net.sourceforge.filebot.ui.analyze; + + +import static net.sourceforge.filebot.ui.NotificationLogging.*; +import static net.sourceforge.tuned.ExceptionUtilities.*; +import static net.sourceforge.tuned.FileUtilities.*; +import static net.sourceforge.tuned.ui.TunedUtilities.*; + +import java.awt.Color; +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.beans.PropertyChangeEvent; +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CancellationException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.Icon; +import javax.swing.JButton; +import javax.swing.JFileChooser; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.ListSelectionModel; +import javax.swing.SwingWorker; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.TableModel; + +import net.miginfocom.swing.MigLayout; +import net.sourceforge.filebot.ResourceManager; +import net.sourceforge.filebot.archive.Archive; +import net.sourceforge.filebot.archive.FileMapper; +import net.sourceforge.filebot.ui.analyze.FileTree.FolderNode; +import net.sourceforge.tuned.ui.GradientStyle; +import net.sourceforge.tuned.ui.LoadingOverlayPane; +import net.sourceforge.tuned.ui.ProgressDialog; +import net.sourceforge.tuned.ui.ProgressDialog.Cancellable; +import net.sourceforge.tuned.ui.SwingWorkerPropertyChangeAdapter; +import net.sourceforge.tuned.ui.notification.SeparatorBorder; + + +class ExtractTool extends Tool { + + private JTable table = new JTable(new ArchiveEntryModel()); + + + public ExtractTool() { + super("Archives"); + + table.setFillsViewportHeight(true); + table.setAutoCreateRowSorter(true); + table.setAutoCreateColumnsFromModel(true); + table.setAutoResizeMode(JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS); + table.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); + table.setRowHeight(20); + + JScrollPane tableScrollPane = new JScrollPane(table); + tableScrollPane.setBorder(new SeparatorBorder(2, new Color(0, 0, 0, 90), GradientStyle.TOP_TO_BOTTOM, SeparatorBorder.Position.BOTTOM)); + + setLayout(new MigLayout("insets 0, nogrid, fill", "align center", "[fill][pref!]")); + add(new LoadingOverlayPane(tableScrollPane, this, "20px", "30px"), "grow, wrap"); + add(new JButton(extractAction), "gap top rel, gap bottom unrel"); + } + + + @Override + protected void setModel(TableModel model) { + table.setModel(model); + } + + + @Override + protected TableModel createModelInBackground(FolderNode sourceModel) throws InterruptedException { + List entries = new ArrayList(); + + for (Iterator iterator = sourceModel.fileIterator(); iterator.hasNext();) { + File file = iterator.next(); + + // ignore non-archives files and trailing multi-volume parts + if (Archive.VOLUME_ONE_FILTER.accept(file)) { + try { + Archive archive = new Archive(file); + try { + for (File it : archive.listFiles()) { + entries.add(new ArchiveEntry(file, it)); + } + } finally { + archive.close(); + } + } catch (Exception e) { + // unwind thread, if we have been cancelled + if (findCause(e, InterruptedException.class) != null) { + throw findCause(e, InterruptedException.class); + } + Logger.getLogger(getClass().getName()).log(Level.SEVERE, e.getMessage(), e); + } + } + + // unwind thread, if we have been cancelled + if (Thread.interrupted()) { + throw new InterruptedException(); + } + } + + return new ArchiveEntryModel(entries); + } + + + private Action extractAction = new AbstractAction("Extract All", ResourceManager.getIcon("package.extract")) { + + @Override + public void actionPerformed(ActionEvent evt) { + final List archives = ((ArchiveEntryModel) table.getModel()).getArchiveList(); + if (archives.isEmpty()) { + return; + } + + Window window = getWindow(evt.getSource()); + JFileChooser chooser = new JFileChooser(archives.get(0).getParentFile()); + chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + chooser.setMultiSelectionEnabled(false); + if (chooser.showSaveDialog(window) != JFileChooser.APPROVE_OPTION) { + return; + } + + final ExtractJob job = new ExtractJob(archives, chooser.getSelectedFile()); + + final ProgressDialog dialog = new ProgressDialog(window, job); + dialog.setLocation(getOffsetLocation(dialog.getOwner())); + dialog.setTitle("Extracting files..."); + dialog.setIcon((Icon) getValue(SMALL_ICON)); + dialog.setIndeterminate(true); + + // close progress dialog when worker is finished + job.addPropertyChangeListener(new SwingWorkerPropertyChangeAdapter() { + + @Override + protected void event(String name, Object oldValue, Object newValue) { + if (name.equals("currentFile")) { + String note = "Extracting " + ((File) newValue).getName(); + dialog.setNote(note); + dialog.setWindowTitle(note); + } + } + + + @Override + protected void done(PropertyChangeEvent evt) { + dialog.close(); + } + }); + + job.execute(); + dialog.setVisible(true); + } + }; + + + private static class ArchiveEntry { + + public final File archive; + public final File entry; + + + public ArchiveEntry(File archive, File entry) { + this.archive = archive; + this.entry = entry; + } + } + + + private static class ArchiveEntryModel extends AbstractTableModel { + + private final ArchiveEntry[] data; + + + public ArchiveEntryModel() { + this.data = new ArchiveEntry[0]; + } + + + public ArchiveEntryModel(Collection data) { + this.data = data.toArray(new ArchiveEntry[data.size()]); + } + + + public List getArchiveList() { + Set archives = new LinkedHashSet(); + for (ArchiveEntry it : data) { + archives.add(it.archive); + } + return new ArrayList(archives); + } + + + @Override + public int getRowCount() { + return data.length; + } + + + @Override + public int getColumnCount() { + return 2; + } + + + @Override + public String getColumnName(int column) { + switch (column) { + case 0: + return "File"; + case 1: + return "Path"; + } + return null; + } + + + @Override + public Object getValueAt(int row, int column) { + switch (column) { + case 0: + return data[row].entry.getName(); + case 1: + File root = new File(data[row].archive.getName()); + File prefix = data[row].entry.getParentFile(); + File path = (prefix == null) ? root : new File(root, prefix.getPath()); + return normalizePathSeparators(path.getPath()); + } + return null; + } + + } + + + protected static class ExtractJob extends SwingWorker implements Cancellable { + + private final File[] archives; + private final File outputRoot; + + + public ExtractJob(Collection archives, File outputRoot) { + this.archives = archives.toArray(new File[archives.size()]); + this.outputRoot = outputRoot; + } + + + @Override + protected Void doInBackground() throws Exception { + for (File it : archives) { + try { + // update progress dialog + firePropertyChange("currentFile", null, it); + + Archive archive = new Archive(it); + try { + File outputFolder = (outputRoot != null) ? outputRoot : new File(it.getParentFile(), getNameWithoutExtension(it.getName())); + FileMapper outputMapper = new FileMapper(outputFolder, false); + archive.extract(outputMapper); + } finally { + archive.close(); + } + } catch (Exception e) { + UILogger.log(Level.WARNING, "Failed to extract archive: " + it.getName(), e); + } + + if (isCancelled()) { + throw new CancellationException(); + } + } + return null; + } + + + @Override + public boolean cancel() { + return cancel(true); + } + + } + +} diff --git a/source/net/sourceforge/filebot/ui/analyze/SplitTool.java b/source/net/sourceforge/filebot/ui/analyze/SplitTool.java index 4e5b3e1a..50413017 100644 --- a/source/net/sourceforge/filebot/ui/analyze/SplitTool.java +++ b/source/net/sourceforge/filebot/ui/analyze/SplitTool.java @@ -24,10 +24,11 @@ import net.sourceforge.filebot.ui.analyze.FileTree.FolderNode; import net.sourceforge.filebot.ui.transfer.DefaultTransferHandler; import net.sourceforge.tuned.FileUtilities; import net.sourceforge.tuned.ui.GradientStyle; +import net.sourceforge.tuned.ui.LoadingOverlayPane; import net.sourceforge.tuned.ui.notification.SeparatorBorder; -public class SplitTool extends Tool implements ChangeListener { +class SplitTool extends Tool implements ChangeListener { private FileTree tree = new FileTree(); @@ -35,7 +36,7 @@ public class SplitTool extends Tool implements ChangeListener { public SplitTool() { - super("Discs"); + super("Disks"); JScrollPane treeScrollPane = new JScrollPane(tree); treeScrollPane.setBorder(new SeparatorBorder(2, new Color(0, 0, 0, 90), GradientStyle.TOP_TO_BOTTOM, SeparatorBorder.Position.BOTTOM)); @@ -45,7 +46,7 @@ public class SplitTool extends Tool implements ChangeListener { setLayout(new MigLayout("insets 0, nogrid, fill", "align center", "[fill][pref!]")); - add(treeScrollPane, "grow, wrap"); + add(new LoadingOverlayPane(treeScrollPane, this), "grow, wrap"); add(new JLabel("Split every")); add(spinner, "wmax 80, gap top rel, gap bottom unrel"); @@ -104,7 +105,7 @@ public class SplitTool extends Tool implements ChangeListener { if (totalSize + fileSize > splitSize) { // part is full, add node and start with next one - root.add(createStatisticsNode(String.format("Part %d", nextPart++), currentPart)); + root.add(createStatisticsNode(String.format("Disk %d", nextPart++), currentPart)); // reset total size and file list totalSize = 0; @@ -122,7 +123,7 @@ public class SplitTool extends Tool implements ChangeListener { if (!currentPart.isEmpty()) { // add last part - root.add(createStatisticsNode(String.format("Part %d", nextPart++), currentPart)); + root.add(createStatisticsNode(String.format("Disk %d", nextPart++), currentPart)); } if (!remainder.isEmpty()) { diff --git a/source/net/sourceforge/filebot/ui/analyze/Tool.java b/source/net/sourceforge/filebot/ui/analyze/Tool.java index 27bfe979..6da0e7fc 100644 --- a/source/net/sourceforge/filebot/ui/analyze/Tool.java +++ b/source/net/sourceforge/filebot/ui/analyze/Tool.java @@ -15,6 +15,7 @@ import net.sourceforge.filebot.ui.analyze.FileTree.FileNode; import net.sourceforge.filebot.ui.analyze.FileTree.FolderNode; import net.sourceforge.tuned.ExceptionUtilities; import net.sourceforge.tuned.FileUtilities; +import net.sourceforge.tuned.ui.LoadingOverlayPane; abstract class Tool extends JComponent { @@ -26,20 +27,21 @@ abstract class Tool extends JComponent { setName(name); } - + public void setSourceModel(FolderNode sourceModel) { if (updateTask != null) { updateTask.cancel(true); } + Tool.this.firePropertyChange(LoadingOverlayPane.LOADING_PROPERTY, false, true); updateTask = new UpdateModelTask(sourceModel); updateTask.execute(); } - + protected abstract M createModelInBackground(FolderNode sourceModel) throws InterruptedException; - + protected abstract void setModel(M model); @@ -52,15 +54,19 @@ abstract class Tool extends JComponent { this.sourceModel = sourceModel; } - + @Override protected M doInBackground() throws Exception { return createModelInBackground(sourceModel); } - + @Override protected void done() { + if (this == updateTask) { + Tool.this.firePropertyChange(LoadingOverlayPane.LOADING_PROPERTY, true, false); + } + // update task will only be cancelled if a newer update task has been started if (this == updateTask && !isCancelled()) { try { diff --git a/source/net/sourceforge/filebot/ui/analyze/TypeTool.java b/source/net/sourceforge/filebot/ui/analyze/TypeTool.java index 718032f0..0d9c9768 100644 --- a/source/net/sourceforge/filebot/ui/analyze/TypeTool.java +++ b/source/net/sourceforge/filebot/ui/analyze/TypeTool.java @@ -20,9 +20,10 @@ import net.miginfocom.swing.MigLayout; import net.sourceforge.filebot.ui.analyze.FileTree.FolderNode; import net.sourceforge.filebot.ui.transfer.DefaultTransferHandler; import net.sourceforge.tuned.FileUtilities; +import net.sourceforge.tuned.ui.LoadingOverlayPane; -public class TypeTool extends Tool { +class TypeTool extends Tool { private FileTree tree = new FileTree(); @@ -35,13 +36,13 @@ public class TypeTool extends Tool { JScrollPane treeScrollPane = new JScrollPane(tree); treeScrollPane.setBorder(BorderFactory.createEmptyBorder()); - add(treeScrollPane, "grow"); + add(new LoadingOverlayPane(treeScrollPane, this), "grow"); tree.setTransferHandler(new DefaultTransferHandler(null, new FileTreeExportHandler())); tree.setDragEnabled(true); } - + @Override protected TreeModel createModelInBackground(FolderNode sourceModel) throws InterruptedException { Map> map = new HashMap>(); @@ -86,7 +87,7 @@ public class TypeTool extends Tool { return new DefaultTreeModel(root); } - + @Override protected void setModel(TreeModel model) { tree.setModel(model); diff --git a/source/net/sourceforge/tuned/ui/ProgressDialog.java b/source/net/sourceforge/tuned/ui/ProgressDialog.java index 0a7b38fd..62c394ae 100644 --- a/source/net/sourceforge/tuned/ui/ProgressDialog.java +++ b/source/net/sourceforge/tuned/ui/ProgressDialog.java @@ -37,13 +37,12 @@ public class ProgressDialog extends JDialog { headerLabel.setFont(headerLabel.getFont().deriveFont(18f)); progressBar.setIndeterminate(true); progressBar.setStringPainted(true); - JPanel c = (JPanel) getContentPane(); c.setLayout(new MigLayout("insets dialog, nogrid, fill")); c.add(iconLabel, "h pref!, w pref!"); c.add(headerLabel, "gap 3mm, wrap paragraph"); - c.add(progressBar, "grow, wrap paragraph"); + c.add(progressBar, "hmin 25px, grow, wrap paragraph"); c.add(new JButton(cancelAction), "align center");