mirror of
https://github.com/4sval/FModel.git
synced 2026-03-22 09:44:37 -05:00
* fix: address remaining WPF→Avalonia gaps for phase 2 completion Resolves r6e/FModel-Linux#84 - Convert Colors.xaml & Icons.xaml to Avalonia xmlns - Migrate CommitControl to Avalonia (replace DataTriggers with IsVisible) - Migrate SearchTextBox to Avalonia (StyledProperty, native Watermark) - Migrate TreeViewItemBehavior to Avalonia attached properties - Replace ListCollectionView with DataGridCollectionView in 8 ViewModels - Add Framework/CompositeCollection as WPF CompositeCollection replacement - Remove AdonisUI.Controls refs from LoadCommand & AddEditDirectoryCommand - Replace SystemParameters with Avalonia screen API in CUE4ParseViewModel - Rewrite Resources.xaml for Avalonia Fluent theme (~2150→290 lines) - Extend CommitMessageConverter with HasDescription parameter support * fix: address review findings for PR #85 - C1: Fix ToolTip syntax (TextBlock.ToolTip → ToolTip.Tip) in tree view - C2: Add Fill binding to folder icons via ConverterParameter=Brush - C3: Convert AddEditDirectoryCommand.ShowDialog() to async with owner - M1: Convert keyed Style → ControlTheme + Theme= in all consumers - M2: Fix CommitControl badge overlap (Latest/Current mutual exclusivity) Add FallbackValue=False for null-safe Asset.IsLatest binding - M3: Add TODO(P4-004) markers for OpenFileDialog/SaveFileDialog - M4: Implement IDisposable on CompositeCollection; dispose on replace - PR: Add UrlToBitmapConverter for HTTP avatar image loading - PR: Fix ContextMenu DataContext in GameFilesTabControl ContentTemplate Restore 'Disable Alpha Channel' toggle (NoAlpha binding) - PR: Replace ElementName=TabControlName with direct DataContext bindings - PR: TreeViewItemBehavior uses GetObservable(IsSelectedProperty) with proper IDisposable subscription cleanup - S1: Clarify UpdateViewModel grouping TODO (non-DataGrid limitation) - S2: Cache compiled Regex in SearchViewModel.ItemFilter - m3: Implement TabItemFillSpace with HorizontalAlignment=Stretch Also updates FolderToGeometryConverter to support ConverterParameter for explicit brush/geometry selection. * fix: address review #2 findings on PR #85 - M1/m1/m2: UrlToBitmapConverter — sync download with ConcurrentDictionary URL cache, 4-space indent, doc comment noting async limitation - M2: Remove duplicate IsVisible attribute on Latest badge Border (MultiBinding property element takes precedence; attribute was dead code) - m3: Add TimeSpan.FromSeconds(1) timeout to user-input Regex construction - PR: ZIndex → Panel.ZIndex in SearchTextBox.xaml (Avalonia attached prop) - PR: ShadowEffect comment updated to reflect key removal (no consumers) - PR: CustomSeparator Tag TextBlock hidden when empty via StringConverters.IsNotNullOrEmpty (eliminates unwanted 10px margin) - PR: CustomVerticalSeparator BasedOn changed from default Separator to CustomSeparator (preserves tagged template inheritance from WPF) - PR: AddEditDirectoryCommand — early return when owner is null instead of silently skipping dialog - PR: CompositeCollection.Count — skip non-ICollection sources instead of O(n) enumeration fallback * Fix Avalonia parity gaps in updates, styles, and dialogs * fix: address parity review follow-ups and unresolved PR comments * Fix remaining PR review thread regressions * Address review findings: thread safety, Linux guards, indentation, and cleanup - [C1] Fix LoadCoAuthors thread race by snapshotting Commits before Task.Run - [M1] Remove IsAsync=True (WPF-only) from Avalonia bindings - [M2] Add OperatingSystem.IsWindows() guard for Win32 OpenFileDialog - [M3] Add null-safety for Application.Current in PlayPause converter - [N1] Add BrushTransition to HighlightedCheckBox for animation parity - [N2] Dispose TreeViewItem subscriptions on DetachedFromVisualTree - [N3] Fix SelectionMode converter: only Multiple uses multi-select - [S1] Simplify AddEditDirectoryCommand owner-null to early return - [S3] Remove dead WPF trigger comments from UpdateView.xaml - [S4] Add 10s timeout to UrlToBitmapConverter HttpClient - PR: Reformat UrlToBitmapConverter to 4-space indentation - PR: Catch RegexMatchTimeoutException in SearchViewModel filter * Fix review round 2: ActualWidth→Bounds, IsAsync removal, FindAncestor xmlns, misc Critical: - C1: Replace ActualHeight/ActualWidth with Bounds.Height/Bounds.Width in UpdateView, MainWindow, SettingsView, DirectorySelector (Avalonia has no ActualWidth/ActualHeight properties) - C2: Remove remaining IsAsync=True from SearchView.xaml and AudioPlayer.xaml (WPF-only, no-op in Avalonia) Major: - M1: Add xmlns:views for FModel.Views and replace dotted sub-namespace local:Views.SettingsView with views:SettingsView in all 23 FindAncestor bindings (avoids ambiguous XAML type resolution) - M2: CompositeCollection.Count now skips non-ICollection sources instead of enumerating them (O(1) for current callers) Minor: - N1: Add Application.Current null guard in FolderToGeometryConverter (matches PlaybackStateToPlayPauseConverter pattern) Suggestions: - S1: UrlToBitmapConverter.LoadAsync now cleans up _inFlight on all exit paths (defense-in-depth for exceptional failures) - S2: Add comment in CommitMessageConverter documenting interaction with LoadCoAuthors co-author cleanup * fix: merge resource dicts, regex cache, handler cleanup, null-init - Merge Colors.xaml, Icons.xaml, Resources.xaml into App.xaml so StaticResource/DynamicResource lookups resolve at runtime - Cache invalid regex state in SearchViewModel to avoid repeated recompilation on every filter invocation - Detach DetachedFromVisualTree handler in TreeViewItemBehavior when IsBroughtIntoViewWhenSelected is set to false - Fix LoadingModeToSelectionModeConverter doc to match behavior - Initialize Commit/Author backing fields with null! to satisfy non-nullable contract before JSON deserialization - Add Debug.Fail in CompositeCollection.Count for non-ICollection sources to surface unexpected usage in development * fix: load missing resource dictionaries, remove dead style refs, fix using - App.xaml: add ResourceInclude for FileContextMenu, FolderContextMenu, and TiledExplorer/Resources.xaml so StaticResource lookups resolve at runtime (C1 fix) - TiledExplorer/Resources.xaml: remove ContextMenu Setters from both TiledExplorer and AssetsListBox styles; Avalonia does not support x:Shared="False", so the shared instance would break when two ListBoxes reference the same ContextMenu. ListBoxItemBehavior already handles assignment programmatically via TryFindResource on right-click. - SettingsView.xaml: remove 11 references to undefined TextBoxDefaultStyle; HotkeyTextBox inherits FluentTheme TextBox styling by default (C2 fix) - LoadingModeToSelectionModeConverter.cs: add missing 'using FModel' for ELoadingMode resolution (M1 fix) * fix: convert Resources.xaml to Styles root, restore toggle button states - Resources.xaml: change root from <ResourceDictionary> to <Styles> so StyleInclude in App.xaml correctly loads an IStyle-implementing element. All keyed resources (ControlThemes, converters) moved into <Styles.Resources>; selector-based styles (audio controls, FoldingMargin) are direct <Styles> children. - AssetsExplorerToggleButtonStyle: restore IsChecked-dependent behavior using Avalonia selectors. :checked shows FolderIconAlt + accent background, :unchecked shows AssetIcon + default background. Tooltip shows hotkey. - MainWindow.xaml: add Classes="AssetsExplorerToggle" to the toggle button so the selector-based checked/unchecked styles match. * Fix Round 7 review findings: ControlTheme, ToolTip.Tip, dead code - C1: Convert keyed Styles to ControlTheme in TiledExplorer/Resources.xaml, update Style= to Theme= in MainWindow.xaml, convert CustomRichTextBox to auto-applying Style Selector in Resources.xaml - M1: Fix ToolTip= to ToolTip.Tip= across SearchView, SettingsView, AudioPlayer, and DirectorySelector (22 occurrences) - m1: Remove duplicate _inFlight.TryRemove in UrlToBitmapConverter.cs - m2: Remove dead Result property from CustomDir.xaml.cs - S2: Remove unused InvertBooleanConverter from Resources.xaml * Fix PR review findings: image column collapse, thread safety - Add HasImageToColumnSpanConverter so AvalonEditor spans all columns when HasImage is false, preventing unused blank space from the image column width (Resources.xaml GameFilesTabControl ContentTemplate) - Marshal LoadAssets and LoadCoAuthors collection/property mutations to Dispatcher.UIThread.InvokeAsync for explicit thread safety * fix: reformat HasImageToColumnSpanConverter to 4-space indentation * fix: map All/AllButNew/AllButModified/AllButPatched to multi-select LoadingModeToSelectionModeConverter only mapped Multiple to SelectionMode.Multiple. The original WPF DataTriggers also enabled Extended selection for All, AllButNew, AllButModified, and AllButPatched modes. Add those mappings to restore parity.
180 lines
5.2 KiB
C#
180 lines
5.2 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading.Tasks;
|
|
using Avalonia.Collections;
|
|
using CUE4Parse.FileProvider.Objects;
|
|
using CUE4Parse.UE4.VirtualFileSystem;
|
|
using FModel.Framework;
|
|
|
|
namespace FModel.ViewModels;
|
|
|
|
public class SearchViewModel : ViewModel
|
|
{
|
|
public enum ESortSizeMode
|
|
{
|
|
None,
|
|
Ascending,
|
|
Descending
|
|
}
|
|
|
|
private string _filterText = string.Empty;
|
|
public string FilterText
|
|
{
|
|
get => _filterText;
|
|
set => SetProperty(ref _filterText, value);
|
|
}
|
|
|
|
private bool _hasRegexEnabled;
|
|
public bool HasRegexEnabled
|
|
{
|
|
get => _hasRegexEnabled;
|
|
set => SetProperty(ref _hasRegexEnabled, value);
|
|
}
|
|
|
|
private bool _hasMatchCaseEnabled;
|
|
public bool HasMatchCaseEnabled
|
|
{
|
|
get => _hasMatchCaseEnabled;
|
|
set => SetProperty(ref _hasMatchCaseEnabled, value);
|
|
}
|
|
|
|
private ESortSizeMode _currentSortSizeMode = ESortSizeMode.None;
|
|
public ESortSizeMode CurrentSortSizeMode
|
|
{
|
|
get => _currentSortSizeMode;
|
|
set => SetProperty(ref _currentSortSizeMode, value);
|
|
}
|
|
|
|
private int _resultsCount = 0;
|
|
public int ResultsCount
|
|
{
|
|
get => _resultsCount;
|
|
private set => SetProperty(ref _resultsCount, value);
|
|
}
|
|
|
|
private GameFile _refFile;
|
|
public GameFile RefFile
|
|
{
|
|
get => _refFile;
|
|
private set => SetProperty(ref _refFile, value);
|
|
}
|
|
|
|
public RangeObservableCollection<GameFile> SearchResults { get; }
|
|
public DataGridCollectionView SearchResultsView { get; }
|
|
|
|
public SearchViewModel()
|
|
{
|
|
SearchResults = [];
|
|
SearchResultsView = new DataGridCollectionView(SearchResults)
|
|
{
|
|
Filter = e => ItemFilter(e, FilterText.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries)),
|
|
};
|
|
ResultsCount = SearchResultsView.Count;
|
|
}
|
|
|
|
public void RefreshFilter()
|
|
{
|
|
SearchResultsView.Refresh();
|
|
ResultsCount = SearchResultsView.Count;
|
|
}
|
|
|
|
public void ChangeCollection(IEnumerable<GameFile> files, GameFile refFile = null)
|
|
{
|
|
SearchResults.Clear();
|
|
SearchResults.AddRange(files);
|
|
RefFile = refFile;
|
|
ResultsCount = SearchResultsView.Count;
|
|
}
|
|
|
|
public async Task CycleSortSizeMode()
|
|
{
|
|
CurrentSortSizeMode = CurrentSortSizeMode switch
|
|
{
|
|
ESortSizeMode.None => ESortSizeMode.Descending,
|
|
ESortSizeMode.Descending => ESortSizeMode.Ascending,
|
|
_ => ESortSizeMode.None
|
|
};
|
|
|
|
var sorted = await Task.Run(() =>
|
|
{
|
|
var archiveDict = SearchResults
|
|
.OfType<VfsEntry>()
|
|
.Select(f => f.Vfs.Name)
|
|
.Distinct()
|
|
.Select((name, idx) => (name, idx))
|
|
.ToDictionary(x => x.name, x => x.idx);
|
|
|
|
var keyed = SearchResults.Select(f =>
|
|
{
|
|
int archiveKey = f is VfsEntry ve && archiveDict.TryGetValue(ve.Vfs.Name, out var key) ? key : -1;
|
|
return (File: f, f.Size, ArchiveKey: archiveKey);
|
|
});
|
|
|
|
return CurrentSortSizeMode switch
|
|
{
|
|
ESortSizeMode.Ascending => keyed
|
|
.OrderBy(x => x.Size).ThenBy(x => x.ArchiveKey)
|
|
.Select(x => x.File).ToList(),
|
|
ESortSizeMode.Descending => keyed
|
|
.OrderByDescending(x => x.Size).ThenBy(x => x.ArchiveKey)
|
|
.Select(x => x.File).ToList(),
|
|
_ => keyed
|
|
.OrderBy(x => x.ArchiveKey).ThenBy(x => x.File.Path, StringComparer.OrdinalIgnoreCase)
|
|
.Select(x => x.File).ToList()
|
|
};
|
|
});
|
|
|
|
SearchResults.Clear();
|
|
SearchResults.AddRange(sorted);
|
|
}
|
|
|
|
private Regex? _cachedFilterRegex;
|
|
private string? _cachedFilterText;
|
|
private bool _cachedMatchCase;
|
|
private bool _cachedRegexInvalid;
|
|
|
|
private bool ItemFilter(object item, IEnumerable<string> filters)
|
|
{
|
|
if (item is not GameFile entry)
|
|
return true;
|
|
|
|
if (!HasRegexEnabled)
|
|
return filters.All(x => entry.Path.Contains(x, HasMatchCaseEnabled ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase));
|
|
|
|
if (_cachedFilterText != FilterText || _cachedMatchCase != HasMatchCaseEnabled)
|
|
{
|
|
var o = RegexOptions.None;
|
|
if (!HasMatchCaseEnabled)
|
|
o |= RegexOptions.IgnoreCase;
|
|
|
|
try
|
|
{
|
|
_cachedFilterRegex = new Regex(FilterText, o, TimeSpan.FromSeconds(1));
|
|
_cachedRegexInvalid = false;
|
|
}
|
|
catch (ArgumentException)
|
|
{
|
|
_cachedFilterRegex = null;
|
|
_cachedRegexInvalid = true;
|
|
}
|
|
|
|
_cachedFilterText = FilterText;
|
|
_cachedMatchCase = HasMatchCaseEnabled;
|
|
}
|
|
|
|
if (_cachedRegexInvalid)
|
|
return false;
|
|
|
|
try
|
|
{
|
|
return _cachedFilterRegex?.Match(entry.Path).Success == true;
|
|
}
|
|
catch (RegexMatchTimeoutException)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|