Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/Data/BatchSwapResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using DLSS_Swapper.Helpers;

namespace DLSS_Swapper.Data;

public enum BatchSwapStatus
{
// Covers successfully applied changes: DLL swaps, DLL restores, and preset updates.
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 the game operation 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}",
};
}
34 changes: 34 additions & 0 deletions src/Data/DLLManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,40 @@ public string GetAssetTypeName(GameAssetType assetType)
};
}

/// <summary>
/// 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.
/// </summary>
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)
{
Expand Down
5 changes: 5 additions & 0 deletions src/Data/DLLRecord.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));


Expand Down
52 changes: 47 additions & 5 deletions src/Pages/GameGridPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,20 @@

<GridView PointerWheelChanged="MainGridView_PointerWheelChanged" ManipulationMode="None" x:Name="MainGridView" ItemsSource="{x:Bind CurrentCollectionView, Mode=OneWay}" SelectionMode="None" IsItemClickEnabled="True" ItemClick="GridAndListView_ItemClick" Padding="20" >

<GridView.Resources>
<!-- White selection checkmark backdrop for contrast against varied cover art (default uses the accent color). -->
<SolidColorBrush x:Key="GridViewItemCheckBoxSelectedBrush" Color="White" />
<SolidColorBrush x:Key="GridViewItemCheckBoxSelectedPointerOverBrush" Color="White" />
<SolidColorBrush x:Key="GridViewItemCheckBoxSelectedPressedBrush" Color="#DDDDDD" />
<SolidColorBrush x:Key="GridViewItemCheckBrush" Color="Black" />

<!-- The item container draws behind the cover art, so its selected background only
shows through the padding added below, reading as an accent frame. -->
<StaticResource x:Key="GridViewItemBackgroundSelected" ResourceKey="AccentFillColorDefaultBrush" />
<StaticResource x:Key="GridViewItemBackgroundSelectedPointerOver" ResourceKey="AccentFillColorSecondaryBrush" />
<StaticResource x:Key="GridViewItemBackgroundSelectedPressed" ResourceKey="AccentFillColorTertiaryBrush" />
</GridView.Resources>

<GridView.GroupStyle>
<GroupStyle HidesIfEmpty="True">
<GroupStyle.HeaderTemplate>
Expand All @@ -115,6 +129,10 @@
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
<Setter Property="Margin" Value="8"/>
<Setter Property="BorderThickness" Value="0"/>
<!-- Padding + matching corner radius are what make the selected background
visible as a frame around the 8px-rounded card content. -->
<Setter Property="Padding" Value="3"/>
<Setter Property="CornerRadius" Value="11"/>
</Style>
</GridView.ItemContainerStyle>

Expand Down Expand Up @@ -231,22 +249,27 @@
<FontIcon Glyph="&#xE946;" />
</AppBarButton.Content>
</AppBarButton>
<AppBarButton Label="{x:Bind ViewModel.TranslationProperties.AddGameText, Mode=OneWay}" Command="{x:Bind ViewModel.AddManualGameButtonCommand}" IsEnabled="{x:Bind ViewModel.IsGameListLoading, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}">
<AppBarButton Label="{x:Bind ViewModel.TranslationProperties.AddGameText, Mode=OneWay}" Command="{x:Bind ViewModel.AddManualGameButtonCommand}" IsEnabled="{x:Bind ViewModel.CanAddGameAndChangeView, Mode=OneWay}">
<AppBarButton.Content>
<FontIcon Glyph="&#xE710;" />
</AppBarButton.Content>
</AppBarButton>
<AppBarButton Label="{x:Bind ViewModel.TranslationProperties.RefreshText, Mode=OneWay}" Command="{x:Bind ViewModel.RefreshGamesButtonCommand}" IsEnabled="{x:Bind ViewModel.IsLoading, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}">
<AppBarButton Label="{x:Bind ViewModel.TranslationProperties.RefreshText, Mode=OneWay}" Command="{x:Bind ViewModel.RefreshGamesButtonCommand}" IsEnabled="{x:Bind ViewModel.CanRefresh, Mode=OneWay}">
<AppBarButton.Content>
<FontIcon Glyph="&#xE72C;" />
</AppBarButton.Content>
</AppBarButton>
<AppBarButton Icon="Filter" Label="{x:Bind ViewModel.TranslationProperties.FilterText, Mode=OneWay}" Command="{x:Bind ViewModel.FilterGamesButtonCommand}" IsEnabled="{x:Bind ViewModel.IsGameListLoading, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}">
<AppBarButton Label="{x:Bind ViewModel.TranslationProperties.BulkSwapText, Mode=OneWay}" Command="{x:Bind ViewModel.ToggleSelectionModeCommand}" IsEnabled="{x:Bind ViewModel.IsGameListLoading, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}">
<AppBarButton.Content>
<FontIcon Glyph="&#xE762;" />
</AppBarButton.Content>
</AppBarButton>
<AppBarButton Icon="Filter" Label="{x:Bind ViewModel.TranslationProperties.FilterText, Mode=OneWay}" Command="{x:Bind ViewModel.FilterGamesButtonCommand}" IsEnabled="{x:Bind ViewModel.CanUseFilter, Mode=OneWay}">
<AppBarButton.Content>
<FontIcon Glyph="&#xE71C;" />
</AppBarButton.Content>
</AppBarButton>
<AppBarButton Label="{x:Bind ViewModel.TranslationProperties.ViewTypeText, Mode=OneWay}" Content="{x:Bind ViewModel.GameGridViewIcon, Mode=OneWay}">
<AppBarButton Label="{x:Bind ViewModel.TranslationProperties.ViewTypeText, Mode=OneWay}" Content="{x:Bind ViewModel.GameGridViewIcon, Mode=OneWay}" IsEnabled="{x:Bind ViewModel.CanAddGameAndChangeView, Mode=OneWay}">
<AppBarButton.Flyout>
<MenuFlyout>
<MenuFlyoutItem Text="{x:Bind ViewModel.TranslationProperties.GridViewText, Mode=OneWay}" Command="{x:Bind ViewModel.ChangeGameGridViewCommand}" CommandParameter="{x:Bind pages:GameGridViewType.GridView}" />
Expand All @@ -267,6 +290,25 @@
</Grid>

<ContentControl x:Name="MainContentControl" Grid.Row="1" HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch" VerticalAlignment="Stretch" Content="{x:Bind ViewModel, Mode=OneWay}" ContentTemplateSelector="{StaticResource TemplateSelector}" />


<Border Grid.Row="1"
VerticalAlignment="Bottom"
HorizontalAlignment="Center"
Margin="0,0,0,24"
Padding="16,8"
CornerRadius="8"
BorderThickness="1"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
Visibility="{x:Bind ViewModel.IsSelectionMode, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<StackPanel Orientation="Horizontal" Spacing="12">
Comment thread
beeradmoore marked this conversation as resolved.
<TextBlock Text="{x:Bind ViewModel.SelectedGamesCountText, Mode=OneWay}" VerticalAlignment="Center" />
<Button Content="{x:Bind ViewModel.TranslationProperties.SelectAllText, Mode=OneWay}" Command="{x:Bind ViewModel.SelectAllVisibleCommand}" />
<Button Content="{x:Bind ViewModel.TranslationProperties.SelectNoneText, Mode=OneWay}" Command="{x:Bind ViewModel.ClearSelectionCommand}" />
<Button Style="{StaticResource AccentButtonStyle}" Content="{x:Bind ViewModel.TranslationProperties.SelectDllsText, Mode=OneWay}" Command="{x:Bind ViewModel.ApplyBatchDllCommand}" />
<Button Content="{x:Bind ViewModel.TranslationProperties.CloseText, Mode=OneWay}" Command="{x:Bind ViewModel.ToggleSelectionModeCommand}" />
</StackPanel>
</Border>

</Grid>
</Page>
133 changes: 133 additions & 0 deletions src/Pages/GameGridPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
using DLSS_Swapper.UserControls;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Windows.System;
using AsyncAwaitBestPractices;
Expand Down Expand Up @@ -192,4 +195,134 @@ private void ClearSearchBox_Click(object sender, RoutedEventArgs e)
{
SearchBox.Text = string.Empty;
}

internal string SearchText => SearchBox.Text;

bool _isSyncingSelection;
int _viewSyncGeneration;

ListViewBase? GetActiveListControl()
{
return MainContentControl.ContentTemplateRoot as ListViewBase;
}

internal void EnterSelectionMode()
{
var listControl = GetActiveListControl();
if (listControl is null)
{
return;
}

listControl.SelectionMode = ListViewSelectionMode.Multiple;
listControl.IsItemClickEnabled = false;
listControl.SelectionChanged += ListControl_SelectionChanged;
ResyncVisualSelection();
}

internal void ExitSelectionMode()
{
var listControl = GetActiveListControl();
if (listControl is null)
{
return;
}

listControl.SelectionChanged -= ListControl_SelectionChanged;
_isSyncingSelection = true;
try
{
if (listControl.Items.Count > 0)
{
listControl.DeselectRange(new ItemIndexRange(0, (uint)listControl.Items.Count));
}
}
finally
{
_isSyncingSelection = false;
}
listControl.SelectionMode = ListViewSelectionMode.None;
listControl.IsItemClickEnabled = true;
}

internal IEnumerable<Game> GetDistinctVisibleGames()
{
var list = GetActiveListControl();
return list is null ? Enumerable.Empty<Game>() : list.Items.OfType<Game>().Distinct().ToList();
}

void ListControl_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_isSyncingSelection == true)
{
return;
}

ViewModel.UpdateSelection(e.AddedItems, e.RemovedItems);
ResyncVisualSelection();
}

internal void BeginSuppressSelectionEvents()
{
_isSyncingSelection = true;
_viewSyncGeneration++;
}

// Called after CurrentCollectionView changes while selection mode is active.
// Swapping the ItemsSource clears the list's visual selection, so it is
// re-applied from the view model's SelectedGames afterwards.
internal void ResyncVisualSelectionAfterViewChange()
{
var generation = _viewSyncGeneration;
var enqueued = DispatcherQueue.TryEnqueue(() =>
{
if (generation != _viewSyncGeneration)
{
// A newer view change has already been requested; ignore this stale callback.
return;
}

ResyncVisualSelection();
ViewModel.NotifySelectionChanged();
});

// BeginSuppressSelectionEvents set _isSyncingSelection before the ItemsSource
// swap; if the resync could not be queued, nothing else would ever reset it
// and all selection events would be ignored from here on.
if (enqueued == false)
{
_isSyncingSelection = false;
}
}

internal void ResyncVisualSelection()
{
var listControl = GetActiveListControl();
if (listControl is null)
{
_isSyncingSelection = false;
return;
}

_isSyncingSelection = true;
try
{
if (listControl.Items.Count > 0)
{
listControl.DeselectRange(new ItemIndexRange(0, (uint)listControl.Items.Count));
}

for (var index = 0; index < listControl.Items.Count; index++)
{
if (listControl.Items[index] is Game game && ViewModel.SelectedGames.Contains(game))
{
listControl.SelectRange(new ItemIndexRange(index, 1));
}
}
}
finally
{
_isSyncingSelection = false;
}
}
}
Loading