using System; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Linq; using System.Management.Automation; using System.Net; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Windows.Forms; using BSA_Browser.Classes; using BSA_Browser.Controls; using BSA_Browser.Extensions; using BSA_Browser.Properties; using SharpBSABA2; namespace BSA_Browser { public enum ArchiveFileSortOrder { FolderName, FileName, FileSize, FileType } public partial class BSABrowser : Form { private const string UpdateMarker = "(!) "; string _untouchedTitle; OpenFolderDialog _openFolderDialog = new OpenFolderDialog(); List<ArchiveEntry> _files = new List<ArchiveEntry>(); ArchiveFileSorter _filesSorter = new ArchiveFileSorter(); Timer _searchDelayTimer; /// <summary> /// Get the selected archive. /// </summary> private ArchiveNode SelectedArchiveNode { get { if (tvFolders.SelectedNode == null) return null; return this.GetRootNode(tvFolders.SelectedNode); } } public BSABrowser() { InitializeComponent(); // Show application version in title this.Text += $" ({Program.GetVersion()})"; // Store title so it can be restored later, // for example when showing the extraction progress in title _untouchedTitle = this.Text; lvFiles.ContextMenu = contextMenu1; if (Settings.Default.UpdateSettings) { Settings.Default.Upgrade(); Settings.Default.UpdateSettings = false; Settings.Default.Save(); } // Restore last path for OpenFolderDialog if (!string.IsNullOrEmpty(Settings.Default.LastUnpackPath)) _openFolderDialog.InitialFolder = Settings.Default.LastUnpackPath; // Load Recent Files list if (Settings.Default.RecentFiles != null) { foreach (string item in Settings.Default.RecentFiles) this.AddToRecentFiles(item); } // Load Quick Extract Paths if (Settings.Default.QuickExtractPaths == null) Settings.Default.QuickExtractPaths = new QuickExtractPaths(); this.LoadQuickExtractPaths(); // Set lvFiles sorter ArchiveFileSorter.SetSorter(Settings.Default.SortType, Settings.Default.SortDesc); // Enable visual styles tvFolders.EnableVisualStyles(); tvFolders.EnableAutoScroll(); lvFiles.EnableVisualStyles(); lvFiles.EnableVisualStylesSelection(); lvFiles.HideFocusRectangle(); // Set TextBox cue txtSearch.SetCue("Search term..."); } public BSABrowser(string[] args) : this() { this.OpenArchives(true, args); } private void BSABrowser_Load(object sender, EventArgs e) { // Initialize WindowStates if null if (Settings.Default.WindowStates == null) { Settings.Default.WindowStates = new WindowStates(); } // Add this form if it doesn't exists if (!Settings.Default.WindowStates.Contains(this.Name)) { Settings.Default.WindowStates.Add(this.Name); } // Restore window state Settings.Default.WindowStates[this.Name].RestoreForm(this); // Restore sorting preferences cmbSortOrder.SelectedIndex = (int)Settings.Default.SortType; cbDesc.Checked = Settings.Default.SortDesc; // Restore Regex preference cbRegex.Checked = Settings.Default.SearchUseRegex; // Show ! in main menu if update is available this.ShowUpdateNotification(); } private void BSABrowser_FormClosing(object sender, FormClosingEventArgs e) { if (tvFolders.GetNodeCount(false) > 0) this.CloseArchives(); this.SaveRecentFiles(); Settings.Default.WindowStates[this.Name].SaveForm(this); Settings.Default.LastUnpackPath = _openFolderDialog.Folder; Settings.Default.Save(); } private void File_DragOver(object sender, DragEventArgs e) { if (e.Data.GetDataPresent(DataFormats.FileDrop)) e.Effect = DragDropEffects.Link; } private void File_DragDrop(object sender, DragEventArgs e) { this.OpenArchives(true, ((string[])e.Data.GetData(DataFormats.FileDrop)) .Where(x => IsSupportedFile(x)) .ToArray()); } private void btnOpen_Click(object sender, EventArgs e) { if (OpenArchiveDialog.ShowDialog(this) == DialogResult.OK) this.OpenArchives(true, OpenArchiveDialog.FileNames); } private void btnExtractAll_Click(object sender, EventArgs e) { if (tvFolders.SelectedNode == null) return; if (_openFolderDialog.ShowDialog(this) == DialogResult.OK) { this.ExtractFiles(_openFolderDialog.Folder, false, true, this.SelectedArchiveNode.Archive.Files.ToArray()); } } private void btnExtractAllFolders_Click(object sender, EventArgs e) { if (tvFolders.SelectedNode == null) return; if (_openFolderDialog.ShowDialog(this) == DialogResult.OK) { this.ExtractFiles(_openFolderDialog.Folder, true, true, this.SelectedArchiveNode.Archive.Files.ToArray()); } } private void btnPreview_Click(object sender, EventArgs e) { this.PreviewSelected(); } private void cmbSortOrder_SelectedIndexChanged(object sender, EventArgs e) { this.SortList(); Settings.Default.SortType = (ArchiveFileSortOrder)cmbSortOrder.SelectedIndex; } private void cbDesc_CheckedChanged(object sender, EventArgs e) { this.SortList(); Settings.Default.SortDesc = cbDesc.Checked; } private void lvFiles_DoubleClick(object sender, EventArgs e) { this.PreviewSelected(); } private void lvFiles_Enter(object sender, EventArgs e) { lvFiles.HideFocusRectangle(); } private void lvFiles_ItemDrag(object sender, ItemDragEventArgs e) { if (!(lvFiles.SelectedIndices.Count >= 1)) return; var obj = new DataObject(); var sc = new StringCollection(); foreach (int index in lvFiles.SelectedIndices) { var fe = _files[index]; string dest = Program.CreateTempDirectory(); fe.Extract(dest, false); sc.Add(Path.Combine(dest, fe.FileName)); } obj.SetFileDropList(sc); lvFiles.DoDragDrop(obj, DragDropEffects.Move); } private void lvFiles_KeyDown(object sender, KeyEventArgs e) { if (e.Control && e.KeyCode == Keys.A) { lvFiles.SelectAllItems(); } } private void lvFiles_SelectedIndexChanged(object sender, EventArgs e) { lvFiles.HideFocusRectangle(); } private void lvFiles_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e) { if (_files.Count <= e.ItemIndex) return; var file = _files[e.ItemIndex]; var lvi = new ListViewItem(Path.Combine(file.Folder, file.FileName)); lvi.SubItems.Add(this.FormatBytes(file.DisplaySize)); lvi.Tag = file; e.Item = lvi; } private void txtSearch_TextChanged(object sender, EventArgs e) { if (_searchDelayTimer == null) { _searchDelayTimer = new Timer(); _searchDelayTimer.Tick += delegate { DoSearch(); }; _searchDelayTimer.Interval = 500; } _searchDelayTimer.Stop(); _searchDelayTimer.Start(); } private void cbRegex_CheckedChanged(object sender, EventArgs e) { Settings.Default.SearchUseRegex = cbRegex.Checked; this.DoSearch(); } private void tvFolders_BeforeExpand(object sender, TreeViewCancelEventArgs e) { var rootNode = this.GetRootNode(e.Node); // This event only needs to run once, so return if AllFiles is NOT null if (rootNode.AllFiles != null) return; e.Node.Nodes.Clear(); var nodes = new Dictionary<string, TreeNode>(); rootNode.AllFiles = (ArchiveEntry[])rootNode.Files.Clone(); // This builds the all TreeNodes foreach (var lvi in rootNode.AllFiles) { string path = Path.GetDirectoryName(lvi.FullPath); if (path == string.Empty || nodes.ContainsKey(path)) continue; string[] dirs = path.Split('\\'); for (int i = 0; i < dirs.Length; i++) { string newpath = string.Join("\\", dirs, 0, i + 1); if (!nodes.ContainsKey(newpath)) { var tn = new TreeNode(dirs[i]); tn.Tag = newpath; if (i == 0) e.Node.Nodes.Add(tn); else nodes[path].Nodes.Add(tn); nodes.Add(newpath, tn); } path = newpath; } } if (Settings.Default.SortArchiveDirectories) { this.SortNodes(e.Node); } } private void tvFolders_AfterSelect(object sender, TreeViewEventArgs e) { var rootNode = this.GetRootNode(e.Node); string path = (string)e.Node.Tag; // If AllFiles is null, trigger event which will populate it if (rootNode.AllFiles == null) tvFolders_BeforeExpand(null, new TreeViewCancelEventArgs(e.Node, false, TreeViewAction.Unknown)); if (path == null) // Root node is selected, so show all files rootNode.Files = rootNode.AllFiles; else { // Only show files under selected node var lvis = new List<ArchiveEntry>(rootNode.AllFiles.Length); foreach (var lvi in rootNode.AllFiles) if (lvi.FullPath.StartsWith(path)) lvis.Add(lvi); rootNode.Files = lvis.ToArray(); } lvFiles.ScrollToTop(); this.DoSearch(); } #region mainMenu1 private void openArchiveMenuItem_Click(object sender, EventArgs e) { if (OpenArchiveDialog.ShowDialog(this) == DialogResult.OK) this.OpenArchives(true, OpenArchiveDialog.FileNames); } private void closeSelectedArchiveMenuItem_Click(object sender, EventArgs e) { if (this.SelectedArchiveNode == null) return; this.CloseArchive(SelectedArchiveNode); } private void closeAllArchivesMenuItem_Click(object sender, EventArgs e) { this.CloseArchives(); } private void optionsMenuItem_Click(object sender, EventArgs e) { using (var of = new OptionsForm()) { if (of.ShowDialog(this) == DialogResult.OK) { of.SaveChanges(); Settings.Default.Save(); // Sync changes to UI this.LoadQuickExtractPaths(); } } } private void recentFilesMenuItem_Popup(object sender, EventArgs e) { emptyListMenuItem.Enabled = recentFilesMenuItem.MenuItems.Count > 2; } private void emptyListMenuItem_Click(object sender, EventArgs e) { for (int i = recentFilesMenuItem.MenuItems.Count - 1; i != 1; i--) recentFilesMenuItem.MenuItems.RemoveAt(i); } private void recentFiles_Click(object sender, EventArgs e) { var item = sender as MenuItem; string file = item.Tag.ToString(); if (!string.IsNullOrEmpty(file) && File.Exists(file)) { this.OpenArchive(file, true); } else { if (MessageBox.Show(this, $"\"{file}\" doesn't exist anymore.\n\n" + "Do you want to remove it from the recent files list?", "Lost File", MessageBoxButtons.YesNo) == DialogResult.Yes) { recentFilesMenuItem.MenuItems.Remove(item); } } } private void exitMenuItem_Click(object sender, EventArgs e) { this.Close(); } private void editMenuItem_Popup(object sender, EventArgs e) { bool hasSelectedItems = lvFiles.SelectedIndices.Count > 0; copyMenuItem.Enabled = hasSelectedItems; } private void copyPathMenuItem_Click(object sender, EventArgs e) { var builder = new StringBuilder(); foreach (int index in lvFiles.SelectedIndices) { if (!string.IsNullOrEmpty(builder.ToString())) builder.AppendLine(); builder.Append(_files[index].FullPath); } Clipboard.SetText(builder.ToString()); } private void copyFolderPathMenuItem_Click(object sender, EventArgs e) { var builder = new StringBuilder(); foreach (int index in lvFiles.SelectedIndices) { if (!string.IsNullOrEmpty(builder.ToString())) builder.AppendLine(); builder.Append(Path.GetDirectoryName(_files[index].FullPath)); } Clipboard.SetText(builder.ToString()); } private void copyFileNameMenuItem_Click(object sender, EventArgs e) { var builder = new StringBuilder(); foreach (int index in lvFiles.SelectedIndices) { if (!string.IsNullOrEmpty(builder.ToString())) builder.AppendLine(); builder.Append(Path.GetFileName(_files[index].FullPath)); } Clipboard.SetText(builder.ToString()); } private void selectAllMenuItem_Click(object sender, EventArgs e) { lvFiles.SelectAllItems(); } private void openFolderMenuItem_Click(object sender, EventArgs e) { var menuItem = sender as MenuItem; var path = menuItem.Tag as QuickExtractPath; if (!Directory.Exists(path.Path)) { MessageBox.Show(this, $"{path.Name}'s path no longer exists."); return; } Process.Start(path.Path); } private void helpMenuItem_Popup(object sender, EventArgs e) { // Remove UpdateMarker from Text if (helpMenuItem.Text.StartsWith(UpdateMarker)) helpMenuItem.Text = helpMenuItem.Text.Remove(0, UpdateMarker.Length); } private async void checkForUpdateMenuItem_Click(object sender, EventArgs e) { // Remove UpdateMarker from Text if (checkForUpdateMenuItem.Text.StartsWith(UpdateMarker)) checkForUpdateMenuItem.Text = checkForUpdateMenuItem.Text.Remove(0, UpdateMarker.Length); try { if (await this.IsUpdateAvailable()) { if (MessageBox.Show(this, "Update available!\n\n" + "Do you want to open the BSA Browser NexusMods page?", "Update available", MessageBoxButtons.YesNo) == DialogResult.Yes) { Process.Start(Program.Website); } } else { MessageBox.Show(this, "You have the latest version."); } } catch (Win32Exception) { MessageBox.Show(this, "Couldn't open the BSA Browser NexusMods page."); } catch (Exception ex) { MessageBox.Show(this, "Error checking for update.\n\n" + ex.Message); } } private void aboutMenuItem_Click(object sender, EventArgs e) { using (var ab = new AboutBox()) { ab.ShowDialog(this); } } #endregion #region contextMenu1 private void contextMenu1_Popup(object sender, EventArgs e) { bool hasSelectedItems = lvFiles.SelectedIndices.Count > 0; bool listIsEmpty = SelectedArchiveNode == null || SelectedArchiveNode.Archive.Files.Count == 0; extractMenuItem.Enabled = hasSelectedItems; extractFoldersMenuItem.Enabled = hasSelectedItems; quickExtractsMenuItem.Enabled = hasSelectedItems; copyMenuItem1.Enabled = hasSelectedItems; } private void extractMenuItem_Click(object sender, EventArgs e) { if (lvFiles.SelectedIndices.Count == 0) return; if (_openFolderDialog.ShowDialog(this) == DialogResult.OK) { var files = new List<ArchiveEntry>(); foreach (int index in lvFiles.SelectedIndices) files.Add(_files[index]); this.ExtractFiles(_openFolderDialog.Folder, false, true, files.ToArray()); } } private void extractFoldersMenuItem_Click(object sender, EventArgs e) { if (lvFiles.SelectedIndices.Count == 0) return; if (_openFolderDialog.ShowDialog(this) == DialogResult.OK) { var files = new List<ArchiveEntry>(); foreach (int index in lvFiles.SelectedIndices) files.Add(_files[index]); this.ExtractFiles(_openFolderDialog.Folder, true, true, files.ToArray()); } } private void previewMenuItem_Click(object sender, EventArgs e) { this.PreviewSelected(); } private void quickExtractsMenuItem_Click(object sender, EventArgs e) { if (quickExtractsMenuItem.MenuItems.Count > 0) return; // Open options with second tab selected using (var of = new OptionsForm(1)) { if (of.ShowDialog(this) == DialogResult.OK) { of.SaveChanges(); Settings.Default.Save(); // Sync changes to UI this.LoadQuickExtractPaths(); } } } private void quickExtractMenuItem_Click(object sender, EventArgs e) { var menuItem = sender as MenuItem; var path = menuItem.Tag as QuickExtractPath; if (!Directory.Exists(path.Path)) { DialogResult result = MessageBox.Show(this, string.Format("{0} path doesn't exists anymore. Do you want to create it?", path.Name), "Quick Extract", MessageBoxButtons.YesNo); if (result == DialogResult.No) return; Directory.CreateDirectory(path.Path); } var files = new List<ArchiveEntry>(); foreach (int index in lvFiles.SelectedIndices) files.Add(_files[index]); ExtractFiles(path.Path, path.UseFolderPath, true, files.ToArray()); } private void copyPathMenuItem1_Click(object sender, EventArgs e) { copyPathMenuItem.PerformClick(); } private void copyFolderPathMenuItem1_Click(object sender, EventArgs e) { copyFolderPathMenuItem.PerformClick(); } private void copyFileNameMenuItem1_Click(object sender, EventArgs e) { copyFileNameMenuItem.PerformClick(); } private void selectAllMenuItem1_Click(object sender, EventArgs e) { lvFiles.SelectAllItems(); } #endregion /// <summary> /// Opens the given archive, adding it to the TreeView and making it browsable. /// </summary> /// <param name="path">The archive file path.</param> /// <param name="addToRecentFiles">True if archive should be added to recent files list.</param> public void OpenArchive(string path, bool addToRecentFiles = false) { // Check if archive is already opened foreach (ArchiveNode node in tvFolders.Nodes) { if (node.Archive.FullPath.ToLower() == path.ToLower()) { MessageBox.Show(this, "This archive is already opened."); return; } } Archive archive = null; try { string extension = Path.GetExtension(path); // ToDo: Read file header to find archive type, not just extension switch (extension.ToLower()) { case ".bsa": case ".dat": if (SharpBSABA2.BSAUtil.BSA.IsSupportedVersion(path) == false) { if (MessageBox.Show(this, "Archive has an unknown version number.\n" + "Attempt to open anyway?", "Warning", MessageBoxButtons.YesNo) != DialogResult.Yes) return; } archive = new SharpBSABA2.BSAUtil.BSA(path); break; case ".ba2": archive = new SharpBSABA2.BA2Util.BA2(path) { UseATIFourCC = Settings.Default.UseATIFourCC }; break; default: throw new Exception($"Unrecognized archive file type ({extension})."); } } catch (Exception ex) { MessageBox.Show(ex.Message); return; } var newNode = new ArchiveNode( Path.GetFileNameWithoutExtension(path) + this.DetectGame(path), archive); var newMenuItem = new MenuItem("Close"); newMenuItem.Tag = newNode; newMenuItem.Click += delegate { this.CloseArchive(newNode); if (tvFolders.Nodes.Count == 0) this.ClearList(); else this.DoSearch(); }; var cm = new ContextMenu(new MenuItem[] { newMenuItem }); newNode.ContextMenu = cm; newNode.Files = archive.Files.ToArray(); newNode.Nodes.Add("empty"); tvFolders.Nodes.Add(newNode); if (newNode.IsExpanded) newNode.Collapse(); btnExtractAllFolders.Enabled = true; btnExtractAll.Enabled = true; btnPreview.Enabled = true; if (addToRecentFiles) this.AddToRecentFiles(path); tvFolders.SelectedNode = newNode; } /// <summary> /// Opens all given archives. /// </summary> /// <param name="addToRecentFiles">True if archives should be added to recent files list.</param> /// <param name="paths">Array of archive file paths.</param> public void OpenArchives(bool addToRecentFiles, params string[] paths) { foreach (string path in paths) this.OpenArchive(path, addToRecentFiles); } /// <summary> /// Adds the given file to the recent files list. If it already exists in the list, it gets bumped up to the top. /// </summary> /// <param name="file">The file to add.</param> private void AddToRecentFiles(string file) { if (string.IsNullOrEmpty(file)) return; if (this.RecentListContains(file)) { var item = this.RecentListGetItemByString(file); if (item == null) return; int index = recentFilesMenuItem.MenuItems.IndexOf(item); recentFilesMenuItem.MenuItems.Remove(item); recentFilesMenuItem.MenuItems.Add(2, item); } else { var newItem = new MenuItem(file, recentFiles_Click); newItem.Tag = file; recentFilesMenuItem.MenuItems.Add(2, newItem); } } /// <summary> /// Clears the virtual ListView. /// </summary> private void ClearList() { lvFiles.BeginUpdate(); _files.Clear(); lvFiles.VirtualListSize = 0; lvFiles.EndUpdate(); } /// <summary> /// Closes the given archive, removing it from the TreeView. /// </summary> /// <param name="archiveNode"></param> private void CloseArchive(ArchiveNode archiveNode) { if (SelectedArchiveNode == archiveNode) this.ClearList(); archiveNode.Archive.Close(); tvFolders.Nodes.Remove(archiveNode); if (tvFolders.GetNodeCount(false) == 0) { btnPreview.Enabled = false; btnExtractAllFolders.Enabled = false; btnExtractAll.Enabled = false; } } /// <summary> /// Closes all open archives, clearing the TreeView. /// </summary> private void CloseArchives() { this.ClearList(); foreach (ArchiveNode node in tvFolders.Nodes) node.Archive.Close(); tvFolders.Nodes.Clear(); // Disable buttons btnPreview.Enabled = btnExtractAllFolders.Enabled = btnExtractAll.Enabled = false; } /// <summary> /// Returns string with game name if given path is a Fallout 3 or Fallout New Vegas file /// since these two games share a lot of file names. /// </summary> private string DetectGame(string path) { // path is not a original Fallout file, no additional identifier required if (!Path.GetFileName(path).ToLower().StartsWith("fallout -")) return string.Empty; var f3 = new Regex(@"^.*(Fallout|F)\s{0,1}(3).*$", RegexOptions.IgnoreCase); var fnv = new Regex(@"^.*(Fallout|F)\s{0,1}(NV|New\s{0,1}Vegas).*$", RegexOptions.IgnoreCase); if (f3.IsMatch(path)) return " (F3)"; else if (fnv.IsMatch(path)) return " (NV)"; return string.Empty; } /// <summary> /// Searches files list, filtering out not-matching files. /// </summary> private void DoSearch() { _searchDelayTimer?.Stop(); if (tvFolders.GetNodeCount(false) < 1 || tvFolders.SelectedNode == null) return; string str = txtSearch.Text; // Reset text color txtSearch.ForeColor = System.Drawing.SystemColors.WindowText; _files.Clear(); if (str.Length == 0) _files.AddRange(this.SelectedArchiveNode.Files); else if (cbRegex.Checked) { Regex regex; try { regex = new Regex(str, RegexOptions.Compiled | RegexOptions.Singleline); } catch { // Set text color to red to indicate an error with the search pattern txtSearch.ForeColor = System.Drawing.Color.Red; return; } for (int i = 0; i < this.SelectedArchiveNode.Files.Length; i++) { var file = this.SelectedArchiveNode.Files[i]; if (regex.IsMatch(file.FullPath)) _files.Add(file); } } else { // Escape special characters, then unescape wild card characters again str = WildcardPattern.Escape(str).Replace("`*", "*"); var pattern = new WildcardPattern($"*{str}*", WildcardOptions.Compiled | WildcardOptions.IgnoreCase); try { for (int i = 0; i < this.SelectedArchiveNode.Files.Length; i++) { var file = this.SelectedArchiveNode.Files[i]; if (pattern.IsMatch(file.FullPath)) _files.Add(file); } } catch { // Set text color to red to indicate an error with the search term txtSearch.ForeColor = System.Drawing.Color.Red; return; } } _files.Sort(_filesSorter); // Refresh list items lvFiles.BeginUpdate(); lvFiles.VirtualListSize = _files.Count; lvFiles.Invalidate(); lvFiles.EndUpdate(); lFileCount.Text = string.Format("{0:n0} files", _files.Count); } /// <summary> /// Extracts the given file(s) to the given path. /// </summary> /// <param name="folder">The path to extract files to.</param> /// <param name="useFolderPath">True to use full folder path for files, false to extract straight to path.</param> /// <param name="gui">True to show a progression dialog.</param> /// <param name="files">The files in the selected archive to extract.</param> private void ExtractFiles(string folder, bool useFolderPath, bool gui, params ArchiveEntry[] files) { if (gui) { pf = new ProgressForm("Unpacking archive"); pf.EnableCancel(); pf.SetProgressRange(100); pf.Canceled += delegate { bw.CancelAsync(); }; pf.Show(this); bw = new BackgroundWorker(); bw.WorkerReportsProgress = true; bw.WorkerSupportsCancellation = true; bw.DoWork += bw_DoWork; bw.ProgressChanged += bw_ProgressChanged; bw.RunWorkerCompleted += bw_RunWorkerCompleted; bw.RunWorkerAsync(new ExtractFilesArguments() { UseFolderPath = useFolderPath, Folder = folder, Files = files }); } else { try { foreach (var fe in files) fe.Extract(folder, useFolderPath); } catch (Exception ex) { MessageBox.Show(this, ex.Message, "Error"); } } } #region ExtractFiles variables BackgroundWorker bw; ProgressForm pf; private class ExtractFilesArguments { public bool UseFolderPath { get; set; } public string Folder { get; set; } public ArchiveEntry[] Files { get; set; } } private void bw_DoWork(object sender, DoWorkEventArgs e) { var arguments = e.Argument as ExtractFilesArguments; var extracted = new Dictionary<string, int>(); try { int progress = 0; int prevProgress = 0; int count = 0; foreach (var fe in arguments.Files) { if (bw.CancellationPending) { e.Result = false; break; } // Update ProgressForm's current file bw.ReportProgress(-1, fe.FileName); if (arguments.UseFolderPath) { fe.Extract(arguments.Folder, arguments.UseFolderPath); } else { if (extracted.ContainsKey(fe.FileName)) { string filename = Path.GetFileNameWithoutExtension(fe.FileName); string extension = Path.GetExtension(fe.FileName); fe.Extract(arguments.Folder, arguments.UseFolderPath, $"{filename} ({++extracted[fe.FileName]}){extension}"); } else { fe.Extract(arguments.Folder, arguments.UseFolderPath); extracted.Add(fe.FileName, 0); } } count++; progress = (int)Math.Round(((double)count / arguments.Files.Length) * 100); if (progress > prevProgress) { prevProgress = progress; bw.ReportProgress(progress); } } } catch (Exception ex) { e.Result = ex; } } private void bw_ProgressChanged(object sender, ProgressChangedEventArgs e) { if (e.ProgressPercentage == -1) { pf.SetCurrentFile(e.UserState as string); } else { pf.UpdateProgress(e.ProgressPercentage); this.Text = $"{pf.GetProgressPercentage()}% - {_untouchedTitle}"; } } private void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { pf.Unblock(); pf.Close(); pf.Dispose(); pf = null; bw.Dispose(); bw = null; this.Text = _untouchedTitle; if (e.Result is Exception) { MessageBox.Show(this, (e.Result as Exception).Message, "Error"); } } #endregion /// <summary> /// Formats the given file size to a more readable string. /// </summary> /// <param name="bytes">The file size to format.</param> private string FormatBytes(long bytes) { const int scale = 1024; string[] orders = new string[] { "GB", "MB", "KB", "Bytes" }; long max = (long)Math.Pow(scale, orders.Length - 1); foreach (string order in orders) { if (bytes > max) return string.Format("{0:##.##} {1}", decimal.Divide(bytes, max), order); max /= scale; } return "0 Bytes"; } /// <summary> /// Returns the root node of the given TreeNode. /// </summary> /// <param name="node">The TreeNode to get root node from.</param> private ArchiveNode GetRootNode(TreeNode node) { var rootNode = node; while (rootNode.Parent != null) rootNode = rootNode.Parent; return rootNode as ArchiveNode; } /// <summary> /// Returns true if file is supported by this program. False otherwise. /// </summary> private bool IsSupportedFile(string file) { switch (Path.GetExtension(file)) { case ".bsa": case ".ba2": return true; } return false; } /// <summary> /// Returns true if update is available online. /// </summary> private async Task<bool> IsUpdateAvailable() { using (var wc = new WebClient()) { // Add tick count to disable caching. var onlineVersion = new Version(await wc.DownloadStringTaskAsync(Program.VersionUrl + $"?nocache={Environment.TickCount}")); var localVersion = new Version(Application.ProductVersion); return localVersion < onlineVersion; } } /// <summary> /// Loads quick extract paths into Quick Extract menu item. /// </summary> private void LoadQuickExtractPaths() { openFoldersMenuItem.MenuItems.Clear(); quickExtractsMenuItem.MenuItems.Clear(); foreach (QuickExtractPath path in Settings.Default.QuickExtractPaths) { openFoldersMenuItem.MenuItems.Add( new MenuItem(path.Name, openFolderMenuItem_Click) { Tag = path }); quickExtractsMenuItem.MenuItems.Add( new MenuItem(path.Name, quickExtractMenuItem_Click) { Tag = path }); } } /// <summary> /// Previews selected file in default program or built-in tool if supported. /// </summary> private void PreviewSelected() { if (lvFiles.SelectedIndices.Count == 0) return; if (lvFiles.SelectedIndices.Count == 1) { var fe = _files[lvFiles.SelectedIndices[0]]; switch (Path.GetExtension(fe.LowerPath)) { /*case ".nif": MessageBox.Show("Viewing of nif's disabled as their format differs from oblivion"); return; case ".tga": System.Diagnostics.Process.Start("obmm\\NifViewer.exe", fe.LowerName); break;*/ case ".dds": case ".bmp": case ".png": case ".jpg": try { DDSViewer.ShowDialog(this, fe); } catch (Exception ex) { MessageBox.Show(this, ex.Message); } break; case ".lst": case ".txt": case ".xml": string dest = Program.CreateTempDirectory(); fe.Extract(dest, false); Process.Start(Path.Combine(dest, fe.FileName)); break; default: MessageBox.Show(this, "Filetype not supported.\n" + "Currently only .txt, .xml, .dds and .lst files can be previewed.", "Error"); break; } } else { MessageBox.Show(this, "Can only preview one file at a time", "Error"); } } /// <summary> /// Returns true if recent files list contains the given file, false otherwise. /// </summary> /// <param name="file">The file to check.</param> private bool RecentListContains(string file) { return recentFilesMenuItem.MenuItems .Cast<MenuItem>() .Any(x => x.Tag != null && x.Tag.ToString() == file); } /// <summary> /// Returns the given file's MenuItem. /// </summary> /// <param name="file">The file to get MenuItem from.</param> private MenuItem RecentListGetItemByString(string file) { return recentFilesMenuItem.MenuItems .Cast<MenuItem>() .First(x => x.Tag != null && x.Tag.ToString() == file); } /// <summary> /// Saves the recent files list to Settings. /// </summary> private void SaveRecentFiles() { if (Settings.Default.RecentFiles == null) Settings.Default.RecentFiles = new StringCollection(); else Settings.Default.RecentFiles.Clear(); for (int i = recentFilesMenuItem.MenuItems.Count - 1; i != 1; i--) Settings.Default.RecentFiles.Add(recentFilesMenuItem.MenuItems[i].Tag.ToString()); } /// <summary> /// Adds update marker (UpdateMarker constant) to Help & Check for update menu items if there is an update available. /// </summary> private async void ShowUpdateNotification() { try { if (await this.IsUpdateAvailable()) { helpMenuItem.Text = UpdateMarker + helpMenuItem.Text; checkForUpdateMenuItem.Text = UpdateMarker + checkForUpdateMenuItem.Text; } } catch { // Do nothing } } /// <summary> /// Sorts all items in list according to user selection. /// </summary> private void SortList() { ArchiveFileSorter.SetSorter((ArchiveFileSortOrder)cmbSortOrder.SelectedIndex, cbDesc.Checked); lvFiles.BeginUpdate(); _files.Sort(_filesSorter); lvFiles.EndUpdate(); } /// <summary> /// Sorts all nodes in given TreeNode. /// </summary> /// <param name="rootNode">The TreeNode whose children is to be sorted.</param> private void SortNodes(TreeNode rootNode) { foreach (TreeNode node in rootNode.Nodes) { var nodes = new TreeNode[node.Nodes.Count]; node.Nodes.CopyTo(nodes, 0); Array.Sort(nodes, new TreeNodeSorter()); node.Nodes.Clear(); node.Nodes.AddRange(nodes); this.SortNodes(node); } } } public class ArchiveFileSorter : Comparer<ArchiveEntry> { internal static ArchiveFileSortOrder order = 0; internal static bool desc = true; public static void SetSorter(ArchiveFileSortOrder sortOrder, bool sortDesc) { order = sortOrder; desc = sortDesc; } public override int Compare(ArchiveEntry a, ArchiveEntry b) { ArchiveEntry fa = a; ArchiveEntry fb = b; switch (order) { case ArchiveFileSortOrder.FolderName: return (desc) ? string.Compare(fa.LowerPath, fb.LowerPath) : string.Compare(fb.LowerPath, fa.LowerPath); case ArchiveFileSortOrder.FileName: return (desc) ? string.Compare(fa.FileName, fb.FileName) : string.Compare(fb.FileName, fa.FileName); case ArchiveFileSortOrder.FileSize: return (desc) ? fa.DisplaySize.CompareTo(fb.DisplaySize) : fb.DisplaySize.CompareTo(fa.DisplaySize); case ArchiveFileSortOrder.FileType: return (desc) ? string.Compare(Path.GetExtension(fa.FileName), Path.GetExtension(fb.FileName)) : string.Compare(Path.GetExtension(fb.FileName), Path.GetExtension(fa.FileName)); default: return 0; } } } public class TreeNodeSorter : Comparer<TreeNode> { public override int Compare(TreeNode a, TreeNode b) { if (a == null) { return b == null ? 0 : -1; } else { return b == null ? 1 : a.Text.CompareTo(b.Text); } } } }