Add project restore files and NuGet cache for GsaViewer
- Created project.nuget.cache to store NuGet package cache information. - Added project.packagespec.json to define project restore settings and dependencies. - Included rider.project.restore.info for Rider IDE integration.
This commit is contained in:
726
GsaEditor/ViewModels/MainWindowViewModel.cs
Normal file
726
GsaEditor/ViewModels/MainWindowViewModel.cs
Normal file
@ -0,0 +1,726 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Text;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Platform.Storage;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using GsaEditor.Core.Compression;
|
||||
using GsaEditor.Core.IO;
|
||||
using GsaEditor.Core.Models;
|
||||
using GsaEditor.Helpers;
|
||||
|
||||
namespace GsaEditor.ViewModels;
|
||||
|
||||
public partial class MainWindowViewModel : ViewModelBase
|
||||
{
|
||||
private static readonly HashSet<string> TextExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
".txt", ".nml", ".xml", ".json", ".csv", ".ini", ".cfg", ".log", ".nut"
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> ImageExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
".png", ".jpg", ".jpeg", ".bmp"
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> UnsupportedImageExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
".tga", ".dds"
|
||||
};
|
||||
|
||||
private static readonly FilePickerFileType GsaFileType = new("GSA Archives")
|
||||
{
|
||||
Patterns = new[] { "*.gsa" }
|
||||
};
|
||||
|
||||
private static readonly FilePickerFileType AllFilesType = new("All Files")
|
||||
{
|
||||
Patterns = new[] { "*" }
|
||||
};
|
||||
|
||||
private Window? _window;
|
||||
private GsaArchive? _archive;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(Title))]
|
||||
[NotifyPropertyChangedFor(nameof(StatusArchivePath))]
|
||||
[NotifyPropertyChangedFor(nameof(HasArchive))]
|
||||
private string? _archivePath;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(Title))]
|
||||
private bool _isDirty;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isLoading;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(HasSelectedFile))]
|
||||
[NotifyPropertyChangedFor(nameof(HasSelectedFolder))]
|
||||
[NotifyPropertyChangedFor(nameof(HasSelectedEntry))]
|
||||
private EntryTreeNodeViewModel? _selectedNode;
|
||||
|
||||
[ObservableProperty]
|
||||
private EntryViewModel? _selectedEntry;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsTextPreview))]
|
||||
[NotifyPropertyChangedFor(nameof(IsImagePreview))]
|
||||
[NotifyPropertyChangedFor(nameof(IsHexPreview))]
|
||||
[NotifyPropertyChangedFor(nameof(HasPreview))]
|
||||
private PreviewMode _previewMode = PreviewMode.None;
|
||||
|
||||
[ObservableProperty]
|
||||
private string? _previewText;
|
||||
|
||||
[ObservableProperty]
|
||||
private Bitmap? _previewImage;
|
||||
|
||||
[ObservableProperty]
|
||||
private string? _hexDumpText;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isEditingText;
|
||||
|
||||
public ObservableCollection<EntryTreeNodeViewModel> TreeRoots { get; } = new();
|
||||
|
||||
// --- Derived properties ---
|
||||
|
||||
public string Title => ArchivePath != null
|
||||
? $"GsaEditor - {Path.GetFileName(ArchivePath)}{(IsDirty ? " *" : "")}"
|
||||
: "GsaEditor";
|
||||
|
||||
public bool HasArchive => _archive != null;
|
||||
public bool HasSelectedEntry => SelectedNode?.Entry != null;
|
||||
public bool HasSelectedFile => SelectedNode != null && !SelectedNode.IsFolder;
|
||||
public bool HasSelectedFolder => SelectedNode != null && SelectedNode.IsFolder;
|
||||
public bool IsTextPreview => PreviewMode == PreviewMode.Text;
|
||||
public bool IsImagePreview => PreviewMode == PreviewMode.Image;
|
||||
public bool IsHexPreview => PreviewMode == PreviewMode.HexDump;
|
||||
public bool HasPreview => PreviewMode != PreviewMode.None;
|
||||
|
||||
// --- Status bar ---
|
||||
|
||||
public string StatusArchivePath => ArchivePath ?? "No archive loaded";
|
||||
|
||||
public string StatusEntryCount => _archive != null
|
||||
? $"{_archive.Entries.Count} entries"
|
||||
: "";
|
||||
|
||||
public string StatusSizeInfo
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_archive == null) return "";
|
||||
long totalOriginal = _archive.Entries.Sum(e => (long)e.OriginalLength);
|
||||
long totalCompressed = _archive.Entries.Sum(e => (long)e.CompressedLength);
|
||||
return $"Total: {FormatSize(totalOriginal)} / Compressed: {FormatSize(totalCompressed)}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the parent window reference for dialog display.
|
||||
/// </summary>
|
||||
public void SetWindow(Window window)
|
||||
{
|
||||
_window = window;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Selection changed
|
||||
// =========================================================================
|
||||
|
||||
partial void OnSelectedNodeChanged(EntryTreeNodeViewModel? value)
|
||||
{
|
||||
IsEditingText = false;
|
||||
|
||||
if (value?.Entry != null)
|
||||
{
|
||||
var entryVm = new EntryViewModel(value.Entry);
|
||||
entryVm.OnModified = () =>
|
||||
{
|
||||
IsDirty = true;
|
||||
NotifyStatusChanged();
|
||||
};
|
||||
SelectedEntry = entryVm;
|
||||
UpdatePreview(value.Entry);
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedEntry = null;
|
||||
ClearPreview();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdatePreview(GsaEntry entry)
|
||||
{
|
||||
var ext = Path.GetExtension(entry.Alias).ToLowerInvariant();
|
||||
|
||||
if (TextExtensions.Contains(ext))
|
||||
{
|
||||
PreviewMode = PreviewMode.Text;
|
||||
var data = entry.GetDecompressedData();
|
||||
PreviewText = Encoding.UTF8.GetString(data);
|
||||
PreviewImage?.Dispose();
|
||||
PreviewImage = null;
|
||||
HexDumpText = null;
|
||||
}
|
||||
else if (ImageExtensions.Contains(ext))
|
||||
{
|
||||
try
|
||||
{
|
||||
var data = entry.GetDecompressedData();
|
||||
using var ms = new MemoryStream(data);
|
||||
PreviewImage?.Dispose();
|
||||
PreviewImage = new Bitmap(ms);
|
||||
PreviewMode = PreviewMode.Image;
|
||||
}
|
||||
catch
|
||||
{
|
||||
PreviewImage = null;
|
||||
PreviewMode = PreviewMode.HexDump;
|
||||
HexDumpText = "Preview not available for this image.";
|
||||
}
|
||||
PreviewText = null;
|
||||
}
|
||||
else if (UnsupportedImageExtensions.Contains(ext))
|
||||
{
|
||||
PreviewMode = PreviewMode.HexDump;
|
||||
HexDumpText = $"Preview not available for {ext.ToUpperInvariant()} files.";
|
||||
PreviewText = null;
|
||||
PreviewImage?.Dispose();
|
||||
PreviewImage = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
PreviewMode = PreviewMode.HexDump;
|
||||
var data = entry.GetDecompressedData();
|
||||
HexDumpText = HexDumper.Dump(data, 512);
|
||||
PreviewText = null;
|
||||
PreviewImage?.Dispose();
|
||||
PreviewImage = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearPreview()
|
||||
{
|
||||
PreviewMode = PreviewMode.None;
|
||||
PreviewText = null;
|
||||
PreviewImage?.Dispose();
|
||||
PreviewImage = null;
|
||||
HexDumpText = null;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// File menu commands
|
||||
// =========================================================================
|
||||
|
||||
[RelayCommand]
|
||||
private async Task OpenArchive()
|
||||
{
|
||||
if (_window == null) return;
|
||||
|
||||
if (IsDirty)
|
||||
{
|
||||
var confirmed = await Dialogs.ConfirmAsync(_window, "Unsaved Changes",
|
||||
"You have unsaved changes. Open a new archive without saving?");
|
||||
if (!confirmed) return;
|
||||
}
|
||||
|
||||
var files = await _window.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||
{
|
||||
Title = "Open GSA Archive",
|
||||
FileTypeFilter = new[] { GsaFileType, AllFilesType },
|
||||
AllowMultiple = false
|
||||
});
|
||||
|
||||
if (files.Count == 0) return;
|
||||
|
||||
var path = files[0].Path.LocalPath;
|
||||
await LoadArchiveAsync(path);
|
||||
}
|
||||
|
||||
private async Task LoadArchiveAsync(string path)
|
||||
{
|
||||
IsLoading = true;
|
||||
ClearPreview();
|
||||
SelectedEntry = null;
|
||||
SelectedNode = null;
|
||||
|
||||
try
|
||||
{
|
||||
var archive = await Task.Run(() => GsaReader.Read(path));
|
||||
_archive = archive;
|
||||
ArchivePath = path;
|
||||
IsDirty = false;
|
||||
BuildTree();
|
||||
NotifyStatusChanged();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (_window != null)
|
||||
await Dialogs.ShowMessageAsync(_window, "Error", $"Failed to open archive:\n{ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task Save()
|
||||
{
|
||||
if (_archive == null || _window == null) return;
|
||||
|
||||
if (string.IsNullOrEmpty(ArchivePath))
|
||||
{
|
||||
await SaveAs();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Run(() => GsaWriter.Write(_archive, ArchivePath!));
|
||||
|
||||
// Generate .idx alongside
|
||||
var idxPath = Path.ChangeExtension(ArchivePath, ".idx");
|
||||
await Task.Run(() => GsaIndexWriter.Write(_archive, idxPath));
|
||||
|
||||
IsDirty = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await Dialogs.ShowMessageAsync(_window, "Error", $"Failed to save archive:\n{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SaveAs()
|
||||
{
|
||||
if (_archive == null || _window == null) return;
|
||||
|
||||
var file = await _window.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
||||
{
|
||||
Title = "Save GSA Archive As",
|
||||
FileTypeChoices = new[] { GsaFileType },
|
||||
DefaultExtension = "gsa",
|
||||
SuggestedFileName = ArchivePath != null ? Path.GetFileName(ArchivePath) : "archive.gsa"
|
||||
});
|
||||
|
||||
if (file == null) return;
|
||||
|
||||
var path = file.Path.LocalPath;
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Run(() => GsaWriter.Write(_archive, path));
|
||||
|
||||
var idxPath = Path.ChangeExtension(path, ".idx");
|
||||
await Task.Run(() => GsaIndexWriter.Write(_archive, idxPath));
|
||||
|
||||
ArchivePath = path;
|
||||
IsDirty = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await Dialogs.ShowMessageAsync(_window, "Error", $"Failed to save archive:\n{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task CloseArchive()
|
||||
{
|
||||
if (_window == null) return;
|
||||
|
||||
if (IsDirty)
|
||||
{
|
||||
var confirmed = await Dialogs.ConfirmAsync(_window, "Unsaved Changes",
|
||||
"You have unsaved changes. Close without saving?");
|
||||
if (!confirmed) return;
|
||||
}
|
||||
|
||||
_archive = null;
|
||||
ArchivePath = null;
|
||||
IsDirty = false;
|
||||
TreeRoots.Clear();
|
||||
SelectedEntry = null;
|
||||
SelectedNode = null;
|
||||
ClearPreview();
|
||||
NotifyStatusChanged();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Exit()
|
||||
{
|
||||
_window?.Close();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Edit menu commands
|
||||
// =========================================================================
|
||||
|
||||
[RelayCommand]
|
||||
private async Task AddFiles()
|
||||
{
|
||||
if (_archive == null || _window == null) return;
|
||||
|
||||
var files = await _window.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||
{
|
||||
Title = "Add Files to Archive",
|
||||
AllowMultiple = true,
|
||||
FileTypeFilter = new[] { AllFilesType }
|
||||
});
|
||||
|
||||
if (files.Count == 0) return;
|
||||
|
||||
string prefix = "";
|
||||
if (SelectedNode != null && SelectedNode.IsFolder)
|
||||
{
|
||||
prefix = SelectedNode.FullPath;
|
||||
if (!prefix.EndsWith("/")) prefix += "/";
|
||||
}
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
var filePath = file.Path.LocalPath;
|
||||
var fileName = Path.GetFileName(filePath);
|
||||
var alias = prefix + fileName;
|
||||
|
||||
var data = await Task.Run(() => File.ReadAllBytes(filePath));
|
||||
var entry = new GsaEntry();
|
||||
entry.Alias = alias;
|
||||
entry.SetData(data, compress: true);
|
||||
|
||||
_archive.Entries.Add(entry);
|
||||
}
|
||||
|
||||
IsDirty = true;
|
||||
BuildTree();
|
||||
NotifyStatusChanged();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ExtractAll()
|
||||
{
|
||||
if (_archive == null || _window == null) return;
|
||||
|
||||
var folders = await _window.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
||||
{
|
||||
Title = "Select Extraction Folder"
|
||||
});
|
||||
|
||||
if (folders.Count == 0) return;
|
||||
|
||||
var basePath = folders[0].Path.LocalPath;
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Run(() =>
|
||||
{
|
||||
foreach (var entry in _archive.Entries)
|
||||
{
|
||||
var outputPath = Path.Combine(basePath, entry.Alias.Replace('/', Path.DirectorySeparatorChar));
|
||||
var dir = Path.GetDirectoryName(outputPath);
|
||||
if (dir != null) Directory.CreateDirectory(dir);
|
||||
|
||||
var data = entry.GetDecompressedData();
|
||||
File.WriteAllBytes(outputPath, data);
|
||||
}
|
||||
});
|
||||
|
||||
await Dialogs.ShowMessageAsync(_window, "Extract All",
|
||||
$"Successfully extracted {_archive.Entries.Count} files to:\n{basePath}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await Dialogs.ShowMessageAsync(_window, "Error", $"Extraction failed:\n{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task Repack()
|
||||
{
|
||||
if (_archive == null || _window == null) return;
|
||||
|
||||
IsLoading = true;
|
||||
try
|
||||
{
|
||||
await Task.Run(() =>
|
||||
{
|
||||
foreach (var entry in _archive.Entries)
|
||||
{
|
||||
var decompressed = entry.GetDecompressedData();
|
||||
entry.SetData(decompressed, entry.IsCompressed);
|
||||
}
|
||||
});
|
||||
|
||||
IsDirty = true;
|
||||
BuildTree();
|
||||
NotifyStatusChanged();
|
||||
|
||||
if (SelectedNode?.Entry != null)
|
||||
{
|
||||
var entryVm = new EntryViewModel(SelectedNode.Entry);
|
||||
entryVm.OnModified = () => { IsDirty = true; NotifyStatusChanged(); };
|
||||
SelectedEntry = entryVm;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Context menu commands
|
||||
// =========================================================================
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ExtractEntry()
|
||||
{
|
||||
if (SelectedNode?.Entry == null || _window == null) return;
|
||||
|
||||
var entry = SelectedNode.Entry;
|
||||
var file = await _window.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
||||
{
|
||||
Title = "Extract File",
|
||||
SuggestedFileName = Path.GetFileName(entry.Alias),
|
||||
FileTypeChoices = new[] { AllFilesType }
|
||||
});
|
||||
|
||||
if (file == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
var data = entry.GetDecompressedData();
|
||||
var path = file.Path.LocalPath;
|
||||
await File.WriteAllBytesAsync(path, data);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await Dialogs.ShowMessageAsync(_window, "Error", $"Extraction failed:\n{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ReplaceEntry()
|
||||
{
|
||||
if (SelectedNode?.Entry == null || _archive == null || _window == null) return;
|
||||
|
||||
var files = await _window.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||
{
|
||||
Title = "Replace Entry With",
|
||||
AllowMultiple = false,
|
||||
FileTypeFilter = new[] { AllFilesType }
|
||||
});
|
||||
|
||||
if (files.Count == 0) return;
|
||||
|
||||
var filePath = files[0].Path.LocalPath;
|
||||
var data = await Task.Run(() => File.ReadAllBytes(filePath));
|
||||
|
||||
var entry = SelectedNode.Entry;
|
||||
entry.SetData(data, entry.IsCompressed);
|
||||
|
||||
IsDirty = true;
|
||||
NotifyStatusChanged();
|
||||
|
||||
// Refresh the selected entry view
|
||||
var entryVm = new EntryViewModel(entry);
|
||||
entryVm.OnModified = () => { IsDirty = true; NotifyStatusChanged(); };
|
||||
SelectedEntry = entryVm;
|
||||
UpdatePreview(entry);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void DeleteEntry()
|
||||
{
|
||||
if (SelectedNode?.Entry == null || _archive == null) return;
|
||||
|
||||
_archive.Entries.Remove(SelectedNode.Entry);
|
||||
IsDirty = true;
|
||||
SelectedNode = null;
|
||||
SelectedEntry = null;
|
||||
ClearPreview();
|
||||
BuildTree();
|
||||
NotifyStatusChanged();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task RenameEntry()
|
||||
{
|
||||
if (SelectedNode?.Entry == null || _window == null) return;
|
||||
|
||||
var entry = SelectedNode.Entry;
|
||||
var result = await Dialogs.InputAsync(_window, "Rename Entry", "New alias:", entry.Alias);
|
||||
|
||||
if (result != null && result != entry.Alias && result.Length > 0)
|
||||
{
|
||||
entry.Alias = result;
|
||||
IsDirty = true;
|
||||
BuildTree();
|
||||
NotifyStatusChanged();
|
||||
|
||||
if (SelectedEntry != null)
|
||||
{
|
||||
var entryVm = new EntryViewModel(entry);
|
||||
entryVm.OnModified = () => { IsDirty = true; NotifyStatusChanged(); };
|
||||
SelectedEntry = entryVm;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ExtractFolder()
|
||||
{
|
||||
if (SelectedNode == null || !SelectedNode.IsFolder || _archive == null || _window == null) return;
|
||||
|
||||
var folders = await _window.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
||||
{
|
||||
Title = "Select Extraction Folder"
|
||||
});
|
||||
|
||||
if (folders.Count == 0) return;
|
||||
|
||||
var basePath = folders[0].Path.LocalPath;
|
||||
var prefix = SelectedNode.FullPath;
|
||||
if (!prefix.EndsWith("/")) prefix += "/";
|
||||
|
||||
try
|
||||
{
|
||||
var matching = _archive.Entries.Where(e => e.Alias.StartsWith(prefix, StringComparison.Ordinal)).ToList();
|
||||
|
||||
await Task.Run(() =>
|
||||
{
|
||||
foreach (var entry in matching)
|
||||
{
|
||||
var relativePath = entry.Alias.Substring(prefix.Length);
|
||||
var outputPath = Path.Combine(basePath, relativePath.Replace('/', Path.DirectorySeparatorChar));
|
||||
var dir = Path.GetDirectoryName(outputPath);
|
||||
if (dir != null) Directory.CreateDirectory(dir);
|
||||
|
||||
var data = entry.GetDecompressedData();
|
||||
File.WriteAllBytes(outputPath, data);
|
||||
}
|
||||
});
|
||||
|
||||
await Dialogs.ShowMessageAsync(_window, "Extract Folder",
|
||||
$"Successfully extracted files to:\n{basePath}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await Dialogs.ShowMessageAsync(_window, "Error", $"Extraction failed:\n{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Text editing commands
|
||||
// =========================================================================
|
||||
|
||||
[RelayCommand]
|
||||
private void StartEdit()
|
||||
{
|
||||
IsEditingText = true;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void SaveEdit()
|
||||
{
|
||||
if (SelectedNode?.Entry == null || PreviewText == null) return;
|
||||
|
||||
var entry = SelectedNode.Entry;
|
||||
var newData = Encoding.UTF8.GetBytes(PreviewText);
|
||||
entry.SetData(newData, entry.IsCompressed);
|
||||
|
||||
IsDirty = true;
|
||||
IsEditingText = false;
|
||||
|
||||
var entryVm = new EntryViewModel(entry);
|
||||
entryVm.OnModified = () => { IsDirty = true; NotifyStatusChanged(); };
|
||||
SelectedEntry = entryVm;
|
||||
NotifyStatusChanged();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void CancelEdit()
|
||||
{
|
||||
IsEditingText = false;
|
||||
if (SelectedNode?.Entry != null)
|
||||
{
|
||||
PreviewText = Encoding.UTF8.GetString(SelectedNode.Entry.GetDecompressedData());
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// About
|
||||
// =========================================================================
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ShowAbout()
|
||||
{
|
||||
if (_window == null) return;
|
||||
await Dialogs.ShowMessageAsync(_window, "About GsaEditor",
|
||||
"GsaEditor v1.0\n\nGSA Archive Viewer & Editor\nBuilt with Avalonia UI");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Tree building
|
||||
// =========================================================================
|
||||
|
||||
private void BuildTree()
|
||||
{
|
||||
TreeRoots.Clear();
|
||||
if (_archive == null) return;
|
||||
|
||||
foreach (var entry in _archive.Entries)
|
||||
{
|
||||
var parts = entry.Alias.Split('/');
|
||||
var currentLevel = TreeRoots;
|
||||
var currentPath = "";
|
||||
|
||||
for (int i = 0; i < parts.Length; i++)
|
||||
{
|
||||
var part = parts[i];
|
||||
currentPath = i == 0 ? part : currentPath + "/" + part;
|
||||
bool isLeaf = (i == parts.Length - 1);
|
||||
|
||||
var existing = currentLevel.FirstOrDefault(n => n.Name == part);
|
||||
if (existing == null)
|
||||
{
|
||||
existing = new EntryTreeNodeViewModel
|
||||
{
|
||||
Name = part,
|
||||
FullPath = currentPath,
|
||||
IsFolder = !isLeaf,
|
||||
Entry = isLeaf ? entry : null
|
||||
};
|
||||
currentLevel.Add(existing);
|
||||
}
|
||||
|
||||
if (!isLeaf)
|
||||
{
|
||||
currentLevel = existing.Children;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Helpers
|
||||
// =========================================================================
|
||||
|
||||
private void NotifyStatusChanged()
|
||||
{
|
||||
OnPropertyChanged(nameof(StatusEntryCount));
|
||||
OnPropertyChanged(nameof(StatusSizeInfo));
|
||||
OnPropertyChanged(nameof(HasArchive));
|
||||
}
|
||||
|
||||
private static string FormatSize(long bytes)
|
||||
{
|
||||
if (bytes < 1024) return $"{bytes} B";
|
||||
if (bytes < 1024 * 1024) return $"{bytes / 1024.0:F1} KB";
|
||||
return $"{bytes / (1024.0 * 1024.0):F1} MB";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user