diff --git a/GarExtract.cs b/GarExtract.cs
new file mode 100644
index 00000000..7f4f7258
--- /dev/null
+++ b/GarExtract.cs
@@ -0,0 +1,233 @@
+//! \file GarExtract.cs
+//! \date Fri Jul 25 05:52:27 2014
+//! \brief Extract archive frontend.
+//
+// Copyright (C) 2014 by morkt
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//
+
+using System;
+using System.IO;
+using System.Linq;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Windows;
+using System.Windows.Input;
+using Ookii.Dialogs.Wpf;
+using GameRes;
+using GARbro.GUI.Strings;
+
+namespace GARbro.GUI
+{
+ public partial class MainWindow : Window
+ {
+ ///
+ /// Handle "Extract item" command.
+ ///
+ private void ExtractItemExec (object sender, ExecutedRoutedEventArgs e)
+ {
+ var entry = CurrentDirectory.SelectedItem as EntryViewModel;
+ if (null == entry)
+ return;
+ try
+ {
+ if (!ViewModel.IsArchive)
+ {
+ if (!entry.IsDirectory)
+ {
+ var arc_dir = CurrentPath;
+ var source = Path.Combine (arc_dir, entry.Name);
+ string destination = arc_dir;
+ // extract into directory named after archive
+ if (!string.IsNullOrEmpty (Path.GetExtension (entry.Name)))
+ destination = Path.GetFileNameWithoutExtension (source);
+ ExtractArchive (source, destination);
+ }
+ }
+ else if (null != m_app.CurrentArchive)
+ {
+ var vm = ViewModel as ArchiveViewModel;
+ string destination = Path.GetDirectoryName (vm.Path);
+ string arc_name = Path.GetFileName (vm.Path);
+ if (entry.Name == ".." && vm.SubDir == "") // root entry
+ {
+ ExtractArchive (m_app.CurrentArchive, arc_name, destination);
+ }
+ else
+ {
+ ExtractFileFromArchive (entry, destination);
+ }
+ }
+ }
+ catch (Exception X)
+ {
+ PopupError (X.Message, guiStrings.MsgErrorExtracting);
+ }
+ }
+
+ private void ExtractArchive (string path, string destination)
+ {
+ string arc_name = Path.GetFileName (path);
+ FormatCatalog.Instance.LastError = null;
+ var arc = ArcFile.TryOpen (path);
+ if (null != arc)
+ {
+ ExtractArchive (arc, arc_name, destination);
+ }
+ else
+ {
+ string error_message;
+ if (FormatCatalog.Instance.LastError != null)
+ error_message = FormatCatalog.Instance.LastError.Message;
+ else
+ error_message = guiStrings.MsgUnknownFormat;
+ SetStatusText (string.Format ("{1}: {0}", error_message, arc_name));
+ }
+ }
+
+ private void ExtractArchive (ArcFile arc, string arc_name, string destination)
+ {
+ if (0 == arc.Dir.Count)
+ {
+ SetStatusText (string.Format ("{1}: {0}", guiStrings.MsgEmptyArchive, arc_name));
+ return;
+ }
+ var extractDialog = new ExtractArchiveDialog (arc_name, destination);
+ extractDialog.Owner = this;
+ var result = extractDialog.ShowDialog();
+ if (!result.Value)
+ return;
+
+ destination = extractDialog.DestinationDir.Text;
+ if (!string.IsNullOrEmpty (destination))
+ {
+ destination = Path.GetFullPath (destination);
+ Trace.WriteLine (destination, "Extract destination");
+ StopWatchDirectoryChanges();
+ try
+ {
+ Directory.CreateDirectory (destination);
+ Directory.SetCurrentDirectory (destination);
+ }
+ finally
+ {
+ m_watcher.EnableRaisingEvents = true;
+ }
+ }
+ IEnumerable file_list = arc.Dir;
+ bool skip_images = !extractDialog.ExtractImages.IsChecked.Value;
+ bool skip_script = !extractDialog.ExtractText.IsChecked.Value;
+ if (skip_images || skip_script)
+ file_list = file_list.Where (f => !(skip_images && f.Type == "image") && !(skip_script && f.Type == "script"));
+
+ if (!file_list.Any())
+ {
+ SetStatusText (string.Format ("{1}: {0}", guiStrings.MsgNoFiles, arc_name));
+ return;
+ }
+ ImageFormat image_format = null;
+ if (!skip_images)
+ image_format = extractDialog.GetImageFormat (extractDialog.ImageConversionFormat);
+
+ SetStatusText (string.Format(guiStrings.MsgExtractingTo, arc_name, destination));
+ ExtractFilesFromArchive (string.Format (guiStrings.MsgExtractingArchive, arc_name),
+ arc, file_list, image_format);
+ }
+
+ private void ExtractFileFromArchive (EntryViewModel entry, string destination)
+ {
+ var extractDialog = new ExtractFile (entry, destination);
+ extractDialog.Owner = this;
+ var result = extractDialog.ShowDialog();
+ if (!result.Value)
+ return;
+
+ var file_list = (ViewModel as ArchiveViewModel).GetFiles (entry);
+
+ destination = extractDialog.DestinationDir.Text;
+ if (!string.IsNullOrEmpty (destination))
+ {
+ destination = Path.GetFullPath (destination);
+ Directory.CreateDirectory (destination);
+ Directory.SetCurrentDirectory (destination);
+ }
+ ImageFormat format = null;
+ if (entry.Type == "image")
+ format = extractDialog.GetImageFormat (extractDialog.ImageConversionFormat);
+
+ string arc_name = Path.GetFileName (CurrentPath);
+ ExtractFilesFromArchive (string.Format (guiStrings.MsgExtractingFile, arc_name),
+ m_app.CurrentArchive, file_list, format);
+ }
+
+ private void ExtractFilesFromArchive (string text, ArcFile arc, IEnumerable file_list,
+ ImageFormat image_format = null)
+ {
+ file_list = file_list.OrderBy (e => e.Offset);
+ var extractProgressDialog = new ProgressDialog ()
+ {
+ WindowTitle = guiStrings.TextTitle,
+ Text = text,
+ Description = "",
+ MinimizeBox = true,
+ };
+ if (!file_list.Skip (1).Any()) // 1 == file_list.Count()
+ {
+ extractProgressDialog.Description = file_list.First().Name;
+ extractProgressDialog.ProgressBarStyle = ProgressBarStyle.MarqueeProgressBar;
+ }
+ extractProgressDialog.DoWork += (s, e) =>
+ {
+ try
+ {
+ int total = file_list.Count();
+ int i = 0;
+ foreach (var entry in file_list)
+ {
+ if (extractProgressDialog.CancellationPending)
+ break;
+ if (total > 1)
+ extractProgressDialog.ReportProgress (i*100/total, null, entry.Name);
+ if (null != image_format && entry.Type == "image")
+ ExtractImage (arc, entry, image_format);
+ else
+ arc.Extract (entry);
+ ++i;
+ }
+ SetStatusText (string.Format (guiStrings.MsgExtractCompletePlural, i,
+ Localization.Plural (i, "file")));
+ }
+ catch (Exception X)
+ {
+ SetStatusText (X.Message);
+ }
+ };
+ extractProgressDialog.RunWorkerCompleted += (s, e) => {
+ extractProgressDialog.Dispose();
+ if (!ViewModel.IsArchive)
+ {
+ arc.Dispose();
+ Dispatcher.Invoke (RefreshView);
+ }
+ };
+ extractProgressDialog.ShowDialog (this);
+ }
+ }
+}