From fc5024d545846b84b7e42a28c6517171b95be7bc Mon Sep 17 00:00:00 2001 From: RafaelHGOliveira Date: Sun, 19 Jul 2026 19:53:46 -0300 Subject: [PATCH 01/11] Add batch swap compatibility helper and result type --- src/Data/BatchSwapResult.cs | 38 +++++++++++++++++++++++++++++++++++++ src/Data/DLLManager.cs | 34 +++++++++++++++++++++++++++++++++ src/Data/DLLRecord.cs | 5 +++++ 3 files changed, 77 insertions(+) create mode 100644 src/Data/BatchSwapResult.cs diff --git a/src/Data/BatchSwapResult.cs b/src/Data/BatchSwapResult.cs new file mode 100644 index 00000000..230bac7b --- /dev/null +++ b/src/Data/BatchSwapResult.cs @@ -0,0 +1,38 @@ +using DLSS_Swapper.Helpers; + +namespace DLSS_Swapper.Data; + +public enum BatchSwapStatus +{ + Swapped, + Skipped, + Error, +} + +public class BatchSwapResult +{ + public string GameTitle { get; init; } = string.Empty; + public BatchSwapStatus Status { get; init; } + + // Localised, game-agnostic action label built on the UI thread before the + // worker starts (e.g. "DLSS → 3.7.0" or "Preset DLSS → D"). Never resolved + // inside the batch worker. + public string ActionLabel { get; init; } = string.Empty; + + // Resource key explaining a skip. Resolved lazily so it is read on the UI + // thread at display time, not inside the batch worker. + public string ReasonKey { get; init; } = string.Empty; + + // Raw (unlocalised) message from Game.UpdateDllAsync when Status is Error. + public string ErrorMessage { get; init; } = string.Empty; + + public bool PromptToRelaunchAsAdmin { get; init; } + + public string DisplayText => Status switch + { + BatchSwapStatus.Skipped => $"{GameTitle} — {ActionLabel} — {ResourceHelper.GetString(ReasonKey)}", + BatchSwapStatus.Error when string.IsNullOrEmpty(ErrorMessage) => $"{GameTitle} — {ActionLabel} — {ResourceHelper.GetString("GamesPage_Batch_Error_Generic")}", + BatchSwapStatus.Error => $"{GameTitle} — {ActionLabel} — {ResourceHelper.GetFormattedResourceTemplate("GamesPage_Batch_Error_PossiblyPartialTemplate", ErrorMessage)}", + _ => $"{GameTitle} — {ActionLabel}", + }; +} diff --git a/src/Data/DLLManager.cs b/src/Data/DLLManager.cs index afd8e5d0..20632f56 100644 --- a/src/Data/DLLManager.cs +++ b/src/Data/DLLManager.cs @@ -787,6 +787,40 @@ public string GetAssetTypeName(GameAssetType assetType) }; } + /// + /// Decides if a batch swap can apply dllRecord to game. ReasonKey is a resource + /// key describing why not. Mirrors the DLSS 1.x vs 2/3 rule from + /// DLLPickerControlModel, but evaluates every DLSS asset instead of only the + /// first: UpdateDllAsync overwrites every matching asset with the same dll, so + /// games mixing generations must be skipped. + /// + internal static (bool Compatible, string ReasonKey) GetBatchCompatibility(Game game, DLLRecord dllRecord) + { + var existingAssets = game.GameAssets.Where(x => x.AssetType == dllRecord.AssetType).ToList(); + if (existingAssets.Count == 0) + { + return (false, "GamesPage_Batch_Skipped_NoAsset"); + } + + if (dllRecord.AssetType == GameAssetType.DLSS) + { + var hasV1Assets = existingAssets.Any(x => x.Version.StartsWith("1.")); + var hasV2PlusAssets = existingAssets.Any(x => x.Version.StartsWith("1.") == false); + + if (hasV1Assets == true && hasV2PlusAssets == true) + { + return (false, "GamesPage_Batch_Skipped_MixedGenerations"); + } + + var recordIsV1 = dllRecord.Version.StartsWith("1."); + if (hasV1Assets != recordIsV1) + { + return (false, "GamesPage_Batch_Skipped_Incompatible"); + } + } + + return (true, string.Empty); + } public GameAssetType GetAssetBackupType(GameAssetType assetType) { diff --git a/src/Data/DLLRecord.cs b/src/Data/DLLRecord.cs index e6c5018a..53f7d77d 100644 --- a/src/Data/DLLRecord.cs +++ b/src/Data/DLLRecord.cs @@ -283,6 +283,11 @@ internal void CancelDownload() try { LocalRecord.FileDownloader = fileDownloader; + // Reset stale error state from a previous failed attempt, otherwise a + // cancelled retry is misreported as an error by observers that + // distinguish cancel from error via HasDownloadError. + LocalRecord.HasDownloadError = false; + LocalRecord.DownloadErrorMessage = string.Empty; NotifyPropertyChanged(nameof(LocalRecord)); From 6e62c2c82d29ed64a26925837c1f067e63624da5 Mon Sep 17 00:00:00 2001 From: RafaelHGOliveira Date: Sun, 19 Jul 2026 19:53:47 -0300 Subject: [PATCH 02/11] Add batch DLL picker and swap summary controls --- src/UserControls/BatchDllPickerControl.xaml | 127 +++++++++++++++ .../BatchDllPickerControl.xaml.cs | 15 ++ .../BatchDllPickerControlModel.cs | 148 ++++++++++++++++++ ...PickerControlModelTranslationProperties.cs | 26 +++ src/UserControls/BatchDllRowModel.cs | 85 ++++++++++ src/UserControls/BatchSwapSummaryControl.xaml | 45 ++++++ .../BatchSwapSummaryControl.xaml.cs | 17 ++ .../BatchSwapSummaryControlModel.cs | 52 ++++++ ...ummaryControlModelTranslationProperties.cs | 17 ++ 9 files changed, 532 insertions(+) create mode 100644 src/UserControls/BatchDllPickerControl.xaml create mode 100644 src/UserControls/BatchDllPickerControl.xaml.cs create mode 100644 src/UserControls/BatchDllPickerControlModel.cs create mode 100644 src/UserControls/BatchDllPickerControlModelTranslationProperties.cs create mode 100644 src/UserControls/BatchDllRowModel.cs create mode 100644 src/UserControls/BatchSwapSummaryControl.xaml create mode 100644 src/UserControls/BatchSwapSummaryControl.xaml.cs create mode 100644 src/UserControls/BatchSwapSummaryControlModel.cs create mode 100644 src/UserControls/BatchSwapSummaryControlModelTranslationProperties.cs diff --git a/src/UserControls/BatchDllPickerControl.xaml b/src/UserControls/BatchDllPickerControl.xaml new file mode 100644 index 00000000..7e11fd56 --- /dev/null +++ b/src/UserControls/BatchDllPickerControl.xaml @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/UserControls/BatchDllPickerControl.xaml.cs b/src/UserControls/BatchDllPickerControl.xaml.cs new file mode 100644 index 00000000..4f1712b2 --- /dev/null +++ b/src/UserControls/BatchDllPickerControl.xaml.cs @@ -0,0 +1,15 @@ +using Microsoft.UI.Xaml.Controls; + +namespace DLSS_Swapper.UserControls; + +public sealed partial class BatchDllPickerControl : UserControl +{ + internal BatchDllPickerControlModel ViewModel { get; } + + public BatchDllPickerControl(EasyContentDialog parentDialog) + { + this.InitializeComponent(); + ViewModel = new BatchDllPickerControlModel(parentDialog); + DataContext = ViewModel; + } +} diff --git a/src/UserControls/BatchDllPickerControlModel.cs b/src/UserControls/BatchDllPickerControlModel.cs new file mode 100644 index 00000000..6ff4fa6e --- /dev/null +++ b/src/UserControls/BatchDllPickerControlModel.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using CommunityToolkit.Mvvm.ComponentModel; +using DLSS_Swapper.Data; +using DLSS_Swapper.Data.DLSS; +using DLSS_Swapper.Helpers; + +namespace DLSS_Swapper.UserControls; + +public partial class BatchDllPickerControlModel : ObservableObject +{ + readonly WeakReference _parentDialogWeakReference; + + public List DlssRows { get; } + public List FsrRows { get; } + public List XeSSRows { get; } + + public bool HasFsrRows => FsrRows.Count > 0; + public bool HasXeSSRows => XeSSRows.Count > 0; + + // Drives the explanation shown when every preset combo is disabled. Without it + // the user just sees three greyed-out dropdowns and no reason why. + public bool PresetsUnsupported { get; } + + public BatchDllPickerControlModelTranslationProperties TranslationProperties { get; } = new BatchDllPickerControlModelTranslationProperties(); + + public BatchDllPickerControlModel(EasyContentDialog parentDialog) + { + _parentDialogWeakReference = new WeakReference(parentDialog); + parentDialog.IsPrimaryButtonEnabled = false; + + var noChangeText = ResourceHelper.GetString("GamesPage_Batch_NoChange"); + var presetSupported = NVAPIHelper.Instance.IsSupported; + PresetsUnsupported = presetSupported == false; + + // NOTE: DLL type + // Preset rows are always shown (a preset is useful even with no downloaded + // DLL). Each preset type uses its OWN option list. + DlssRows = new List + { + BuildPresetRow(GameAssetType.DLSS, noChangeText, NVAPIHelper.Instance.DlssPresetOptions, presetSupported), + BuildPresetRow(GameAssetType.DLSS_D, noChangeText, NVAPIHelper.Instance.DlssDPresetOptions, presetSupported), + BuildPresetRow(GameAssetType.DLSS_G, noChangeText, NVAPIHelper.Instance.DlssGPresetOptions, presetSupported), + }; + + // DLL-only rows only appear when at least one DLL version is available. + FsrRows = BuildDllOnlyRows(noChangeText, new[] { GameAssetType.FSR_31_DX12, GameAssetType.FSR_31_VK }); + XeSSRows = BuildDllOnlyRows(noChangeText, new[] { GameAssetType.XeSS, GameAssetType.XeSS_FG, GameAssetType.XeSS_DX11, GameAssetType.XeLL }); + + foreach (var row in AllRows()) + { + row.PropertyChanged += Row_PropertyChanged; + } + } + + BatchDllRowModel BuildPresetRow(GameAssetType type, string noChangeText, IReadOnlyList presetOptions, bool presetEnabled) + { + var options = BuildDllOptions(type, noChangeText); + // Sentinel value 0xFFFFFFFF is not a real preset (PresetOption.UpdateNameFromTranslation + // leaves unknown values untouched), so it never collides with Default (0). + var sentinel = new PresetOption(noChangeText, 0xFFFFFFFF); + return new BatchDllRowModel(type, DLLManager.Instance.GetAssetTypeName(type), options, sentinel, presetOptions, presetEnabled); + } + + List BuildDllOnlyRows(string noChangeText, GameAssetType[] types) + { + var rows = new List(); + foreach (var type in types) + { + var options = BuildDllOptions(type, noChangeText); + // options[0] is the sentinel; a real version exists only if count > 1. + if (options.Count <= 1) + { + continue; + } + rows.Add(new BatchDllRowModel(type, DLLManager.Instance.GetAssetTypeName(type), options, null, null, false)); + } + return rows; + } + + static List BuildDllOptions(GameAssetType type, string noChangeText) + { + // NOTE: DLL type + var records = type switch + { + GameAssetType.DLSS => DLLManager.Instance.DLSSRecords.ToList(), + GameAssetType.DLSS_G => DLLManager.Instance.DLSSGRecords.ToList(), + GameAssetType.DLSS_D => DLLManager.Instance.DLSSDRecords.ToList(), + GameAssetType.FSR_31_DX12 => DLLManager.Instance.FSR31DX12Records.ToList(), + GameAssetType.FSR_31_VK => DLLManager.Instance.FSR31VKRecords.ToList(), + GameAssetType.XeSS => DLLManager.Instance.XeSSRecords.ToList(), + GameAssetType.XeSS_DX11 => DLLManager.Instance.XeSSDX11Records.ToList(), + GameAssetType.XeSS_FG => DLLManager.Instance.XeSSFGRecords.ToList(), + GameAssetType.XeLL => DLLManager.Instance.XeLLRecords.ToList(), + _ => new List(), + }; + + if (Settings.Instance.OnlyShowDownloadedDlls == true) + { + records.RemoveAll(x => x.LocalRecord?.IsDownloaded != true); + } + + if (Settings.Instance.AllowDebugDlls == false) + { + records.RemoveAll(x => x.IsDevFile == true); + } + + // Only records with a LocalRecord can ever be applied (the executor needs + // LocalRecord before it can download). Dropping the rest here keeps the + // PlannedDllActions invariant: every planned record has a LocalRecord. + records.RemoveAll(x => x.LocalRecord is null); + + var options = new List + { + new BatchDllOption { DisplayName = noChangeText, Record = null }, + }; + options.AddRange(records.Select(r => new BatchDllOption { DisplayName = r.DisplayName, Record = r })); + return options; + } + + void Row_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(BatchDllRowModel.SelectedDllOption) || e.PropertyName == nameof(BatchDllRowModel.SelectedPreset)) + { + if (_parentDialogWeakReference.TryGetTarget(out var dialog) == true) + { + dialog.IsPrimaryButtonEnabled = AllRows().Any(r => r.HasDllAction || r.HasPresetAction); + } + } + } + + IEnumerable AllRows() => DlssRows.Concat(FsrRows).Concat(XeSSRows); + + // What the picker decides — never *who* it applies to. + public List<(GameAssetType Type, DLLRecord Record)> PlannedDllActions => + AllRows() + .Where(r => r.HasDllAction) + .Select(r => (r.Type, r.SelectedDllOption!.Record!)) + .ToList(); + + public List<(GameAssetType Type, PresetOption Preset)> PlannedPresetActions => + AllRows() + .Where(r => r.HasPresetAction) + .Select(r => (r.Type, r.SelectedPreset!)) + .ToList(); +} diff --git a/src/UserControls/BatchDllPickerControlModelTranslationProperties.cs b/src/UserControls/BatchDllPickerControlModelTranslationProperties.cs new file mode 100644 index 00000000..264ab66e --- /dev/null +++ b/src/UserControls/BatchDllPickerControlModelTranslationProperties.cs @@ -0,0 +1,26 @@ +using DLSS_Swapper.Attributes; +using DLSS_Swapper.Helpers; +using DLSS_Swapper.Interfaces; + +namespace DLSS_Swapper.UserControls; + +public class BatchDllPickerControlModelTranslationProperties : LocalizedViewModelBase +{ + [TranslationProperty] + public string DlssGroupText => ResourceHelper.GetString("GamesPage_Batch_Group_DLSS"); + + [TranslationProperty] + public string FsrGroupText => ResourceHelper.GetString("GamesPage_Batch_Group_FSR"); + + [TranslationProperty] + public string XessGroupText => ResourceHelper.GetString("GamesPage_Batch_Group_XeSS"); + + [TranslationProperty] + public string VersionColumnText => ResourceHelper.GetString("GamesPage_Batch_ColumnHeader_Version"); + + [TranslationProperty] + public string PresetColumnText => ResourceHelper.GetString("GamesPage_Batch_ColumnHeader_Preset"); + + [TranslationProperty] + public string PresetsUnsupportedText => ResourceHelper.GetString("GamesPage_Batch_Preset_UnsupportedMessage"); +} diff --git a/src/UserControls/BatchDllRowModel.cs b/src/UserControls/BatchDllRowModel.cs new file mode 100644 index 00000000..76a7dd9b --- /dev/null +++ b/src/UserControls/BatchDllRowModel.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using System.Linq; +using CommunityToolkit.Mvvm.ComponentModel; +using DLSS_Swapper.Data; +using DLSS_Swapper.Data.DLSS; +using DLSS_Swapper.Helpers; + +namespace DLSS_Swapper.UserControls; + +// One selectable DLL version inside a batch picker row. A null Record is the +// "Don't change" sentinel that leaves this row's DLL untouched. +public class BatchDllOption +{ + public string DisplayName { get; init; } = string.Empty; + public DLLRecord? Record { get; init; } +} + +// A single type row (DLSS, FSR_31_VK, ...). Holds its own DLL-version options and, +// for the three preset-capable types, its own preset options. "Don't change" is +// represented by presence of the sentinel selection, never by a uint value — so +// preset 0 ("Default") stays a real, selectable action. +public partial class BatchDllRowModel : ObservableObject +{ + public GameAssetType Type { get; } + public string TypeName { get; } + + // TypeName is only a visual label in the row Grid, so the combos would otherwise + // reach Narrator unnamed. Both combos in a row need distinct names. + public string DllAutomationName { get; } + public string PresetAutomationName { get; } + + // True for DLSS / DLSS_D / DLSS_G. + public bool HasPreset { get; } + + // HasPreset && NVAPI supported. When false the preset combo shows only the + // sentinel and is disabled. + public bool PresetEnabled { get; } + + // DllOptions[0] is always the sentinel (Record == null). + public List DllOptions { get; } + + // PresetOptions[0] is the sentinel when HasPreset; empty otherwise. + public List PresetOptions { get; } + + readonly PresetOption? _presetSentinel; + + [ObservableProperty] + public partial BatchDllOption? SelectedDllOption { get; set; } + + [ObservableProperty] + public partial PresetOption? SelectedPreset { get; set; } + + public BatchDllRowModel(GameAssetType type, string typeName, List dllOptions, PresetOption? presetSentinel, IReadOnlyList? presetOptions, bool presetEnabled) + { + Type = type; + TypeName = typeName; + DllAutomationName = ResourceHelper.GetFormattedResourceTemplate("GamesPage_Batch_VersionForTypeTemplate", typeName); + PresetAutomationName = ResourceHelper.GetFormattedResourceTemplate("GamesPage_Batch_PresetForTypeTemplate", typeName); + DllOptions = dllOptions; + SelectedDllOption = dllOptions.FirstOrDefault(); // sentinel + + HasPreset = presetSentinel is not null; + PresetEnabled = presetEnabled; + _presetSentinel = presetSentinel; + + var options = new List(); + if (presetSentinel is not null) + { + options.Add(presetSentinel); + if (presetOptions is not null) + { + options.AddRange(presetOptions); + } + } + PresetOptions = options; + SelectedPreset = _presetSentinel; + } + + public bool HasDllAction => SelectedDllOption?.Record is not null; + + public bool HasPresetAction => + HasPreset + && SelectedPreset is not null + && ReferenceEquals(SelectedPreset, _presetSentinel) == false; +} diff --git a/src/UserControls/BatchSwapSummaryControl.xaml b/src/UserControls/BatchSwapSummaryControl.xaml new file mode 100644 index 00000000..f322e2ea --- /dev/null +++ b/src/UserControls/BatchSwapSummaryControl.xaml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/UserControls/BatchSwapSummaryControl.xaml.cs b/src/UserControls/BatchSwapSummaryControl.xaml.cs new file mode 100644 index 00000000..7068c583 --- /dev/null +++ b/src/UserControls/BatchSwapSummaryControl.xaml.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using DLSS_Swapper.Data; +using Microsoft.UI.Xaml.Controls; + +namespace DLSS_Swapper.UserControls; + +public sealed partial class BatchSwapSummaryControl : UserControl +{ + BatchSwapSummaryControlModel ViewModel { get; } + + public BatchSwapSummaryControl(IReadOnlyList results) + { + this.InitializeComponent(); + ViewModel = new BatchSwapSummaryControlModel(results); + DataContext = ViewModel; + } +} diff --git a/src/UserControls/BatchSwapSummaryControlModel.cs b/src/UserControls/BatchSwapSummaryControlModel.cs new file mode 100644 index 00000000..5e01b280 --- /dev/null +++ b/src/UserControls/BatchSwapSummaryControlModel.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.Linq; +using DLSS_Swapper.Data; +using DLSS_Swapper.Helpers; + +namespace DLSS_Swapper.UserControls; + +class BatchSwapSummaryControlModel +{ + // Skips that are expected rather than problems: the user asked for a DLL type + // the game simply doesn't ship. Listing one line per game+type buries the + // results that actually need attention, so these are collapsed into a count. + static readonly HashSet NotApplicableReasonKeys = new HashSet() + { + "GamesPage_Batch_Skipped_NoAsset", + "GamesPage_Batch_Preset_Skipped_NoAsset", + "GamesPage_Batch_Preset_Skipped_Unsupported", + }; + + public string SwappedSummaryText { get; } = string.Empty; + public int SkippedCount { get; } + public int ErrorCount { get; } + public string NotApplicableMessage { get; } = string.Empty; + public string AdminMessage { get; } = string.Empty; + public IReadOnlyList Results { get; } + + public BatchSwapSummaryControlModelTranslationProperties TranslationProperties { get; } = new BatchSwapSummaryControlModelTranslationProperties(); + + public BatchSwapSummaryControlModel(IReadOnlyList results) + { + // Not-applicable skips are counted but kept out of the list. + Results = results.Where(x => x.Status != BatchSwapStatus.Skipped || NotApplicableReasonKeys.Contains(x.ReasonKey) == false).ToList(); + + var swapped = results.Where(x => x.Status == BatchSwapStatus.Swapped).ToList(); + SwappedSummaryText = ResourceHelper.GetFormattedResourceTemplate("GamesPage_Batch_Summary_SwappedTemplate", swapped.Count, swapped.Select(x => x.GameTitle).Distinct().Count()); + + SkippedCount = results.Count(x => x.Status == BatchSwapStatus.Skipped && NotApplicableReasonKeys.Contains(x.ReasonKey) == false); + ErrorCount = results.Count(x => x.Status == BatchSwapStatus.Error); + + var notApplicableCount = results.Count(x => x.Status == BatchSwapStatus.Skipped && NotApplicableReasonKeys.Contains(x.ReasonKey) == true); + if (notApplicableCount > 0) + { + NotApplicableMessage = ResourceHelper.GetFormattedResourceTemplate("GamesPage_Batch_Summary_NotApplicableTemplate", notApplicableCount); + } + + var adminCount = results.Count(x => x.PromptToRelaunchAsAdmin == true); + if (adminCount > 0) + { + AdminMessage = ResourceHelper.GetFormattedResourceTemplate("GamesPage_Batch_NeedsAdminTemplate", adminCount); + } + } +} diff --git a/src/UserControls/BatchSwapSummaryControlModelTranslationProperties.cs b/src/UserControls/BatchSwapSummaryControlModelTranslationProperties.cs new file mode 100644 index 00000000..b5b3359f --- /dev/null +++ b/src/UserControls/BatchSwapSummaryControlModelTranslationProperties.cs @@ -0,0 +1,17 @@ +using DLSS_Swapper.Attributes; +using DLSS_Swapper.Helpers; +using DLSS_Swapper.Interfaces; + +namespace DLSS_Swapper.UserControls; + +public class BatchSwapSummaryControlModelTranslationProperties : LocalizedViewModelBase +{ + [TranslationProperty] + public string SwappedText => ResourceHelper.GetString("GamesPage_Batch_Summary_Swapped"); + + [TranslationProperty] + public string SkippedText => ResourceHelper.GetString("GamesPage_Batch_Summary_Skipped"); + + [TranslationProperty] + public string ErrorsText => ResourceHelper.GetString("GamesPage_Batch_Summary_Errors"); +} From e2d12bdec85312440ad48c68c60ea6f1c91ca424 Mon Sep 17 00:00:00 2001 From: RafaelHGOliveira Date: Sun, 19 Jul 2026 19:53:47 -0300 Subject: [PATCH 03/11] Add multi-selection mode and batch swap execution to games page --- src/Pages/GameGridPage.xaml | 52 +- src/Pages/GameGridPage.xaml.cs | 139 +++++ src/Pages/GameGridPageModel.cs | 585 +++++++++++++++++- .../GameGridPageModelTranslationProperties.cs | 12 + 4 files changed, 781 insertions(+), 7 deletions(-) diff --git a/src/Pages/GameGridPage.xaml b/src/Pages/GameGridPage.xaml index 4ae4d4de..b8fcbd02 100644 --- a/src/Pages/GameGridPage.xaml +++ b/src/Pages/GameGridPage.xaml @@ -97,6 +97,20 @@ + + + + + + + + + + + + + @@ -115,6 +129,10 @@ + + + @@ -231,22 +249,27 @@ - + - + - + + + + + + - + @@ -267,6 +290,25 @@ - + + + + +