Fix WebP images not loading on Windows installations without WebP extensions installed

This commit is contained in:
trawzified 2023-12-28 17:15:19 +01:00
parent 0be1081142
commit 1c609a3d6a
8 changed files with 2239 additions and 206 deletions

View File

@ -19,6 +19,9 @@ using Wabbajack.Extensions;
using Wabbajack.Models;
using Wabbajack.Paths;
using Wabbajack.Paths.IO;
using System.Drawing;
using Catel.IO;
using System.Drawing.Imaging;
namespace Wabbajack
{
@ -37,6 +40,32 @@ namespace Wabbajack
return img;
}
public static BitmapImage BitmapImageFromWebp(byte[] bytes, bool getThumbnail = false)
{
using(WebP webp = new())
{
Bitmap bitmap;
if (getThumbnail)
bitmap = webp.GetThumbnailFast(bytes, 640, 360);
else
bitmap = webp.Decode(bytes);
using(var ms = new MemoryStream())
{
bitmap.Save(ms, ImageFormat.Png);
ms.Position = 0;
var img = new BitmapImage();
img.BeginInit();
img.CacheOption = BitmapCacheOption.OnLoad;
img.StreamSource = ms;
img.EndInit();
img.Freeze();
return img;
}
}
}
public static bool TryGetBitmapImageFromFile(AbsolutePath path, out BitmapImage bitmapImage)
{
try
@ -95,7 +124,7 @@ namespace Wabbajack
try
{
var (found, mstream) = await FindCachedImage(url);
if (found) return (ll, mstream);
if (found) return (ll, mstream, url);
var ret = new MemoryStream();
using (var client = new HttpClient())
@ -107,21 +136,21 @@ namespace Wabbajack
ret.Seek(0, SeekOrigin.Begin);
await WriteCachedImage(url, ret.ToArray());
return (ll, ret);
return (ll, ret, url);
}
catch (Exception ex)
{
exceptionHandler(ex);
return (ll, default);
return (ll, default, url);
}
})
.Select(x =>
{
var (ll, memStream) = x;
var (ll, memStream, url) = x;
if (memStream == null) return default;
try
{
return BitmapImageFromStream(memStream);
return url.EndsWith("webp", StringComparison.InvariantCultureIgnoreCase) ? BitmapImageFromWebp(memStream.ToArray(), true) : BitmapImageFromStream(memStream);
}
catch (Exception ex)
{

File diff suppressed because it is too large Load Diff

View File

@ -133,7 +133,8 @@ namespace Wabbajack
{
if (string.IsNullOrWhiteSpace(txt)) return _ => true;
return item => item.Metadata.Title.ContainsCaseInsensitive(txt) ||
item.Metadata.Description.ContainsCaseInsensitive(txt);
item.Metadata.Description.ContainsCaseInsensitive(txt) ||
item.Metadata.Tags.Contains(txt);
});
var onlyInstalledGamesFilter = this.ObservableForProperty(vm => vm.OnlyInstalled)
@ -169,12 +170,12 @@ namespace Wabbajack
})
.StartWith(_ => true);
var searchSorter = this.WhenValueChanged(x => x.Search)
.Where(x => !string.IsNullOrWhiteSpace(x))
var searchSorter = this.WhenValueChanged(vm => vm.Search)
.Where(s => !string.IsNullOrWhiteSpace(s))
.Throttle(searchThrottle, RxApp.MainThreadScheduler)
.Select(s => SortExpressionComparer<ModListMetadataVM>
.Descending(modlist => modlist.Metadata.Title.StartsWith(s, StringComparison.InvariantCultureIgnoreCase))
.ThenByDescending(modlist => modlist.Metadata.Title.Contains(s, StringComparison.InvariantCultureIgnoreCase)));
.Descending(m => m.Metadata.Title.StartsWith(s, StringComparison.InvariantCultureIgnoreCase))
.ThenByDescending(m => m.Metadata.Title.Contains(s, StringComparison.InvariantCultureIgnoreCase)));
_modLists.Connect()
.ObserveOn(RxApp.MainThreadScheduler)
.Filter(searchTextPredicates)
@ -183,6 +184,7 @@ namespace Wabbajack
.Filter(showNSFWFilter)
.Filter(gameFilter)
.Sort(searchSorter)
.Sort(SortExpressionComparer<ModListMetadataVM>.Descending(modlist => !modlist.IsBroken))
.TreatMovesAsRemoveAdd()
.Bind(out _filteredModLists)
.Subscribe((_) =>

View File

@ -176,7 +176,7 @@ namespace Wabbajack
.ToGuiProperty(this, nameof(Exists));
var imageObs = Observable.Return(Metadata.Links.ImageUri)
.DownloadBitmapImage((ex) => _logger.LogError("Error downloading modlist image {Title}", Metadata.Title), LoadingImageLock);
.DownloadBitmapImage((ex) => _logger.LogError("Error downloading modlist image {Title} from {ImageUri}: {Exception}", Metadata.Title, Metadata.Links.ImageUri, ex.Message), LoadingImageLock);
_Image = imageObs
.ToGuiProperty(this, nameof(Image));

View File

@ -17,7 +17,7 @@
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="3*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border Grid.Column="1"
BorderBrush="Transparent"
@ -137,25 +137,8 @@
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<Grid Grid.Row="2" Margin="0, 10, 0, 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<CheckBox
x:Name="ShowNSFW"
FontSize="7"
VerticalAlignment="Center"
Foreground="{StaticResource ForegroundBrush}"
ToolTip="Only show Not Safe For Work (NSFW) modlists">
<CheckBox.LayoutTransform>
<ScaleTransform ScaleX="1.6" ScaleY="1.6"/>
</CheckBox.LayoutTransform>
</CheckBox>
<TextBlock Grid.Column="1" Margin="5, 0, 0, 0" Text="Show NSFW modlists" FontSize="14" VerticalAlignment="Center" />
</Grid>
<Grid Grid.Row="3" Margin="0, 10, 0, 0">
<Grid Grid.Row="2" Margin="0, 10, 0, 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
@ -172,6 +155,24 @@
<TextBlock Grid.Column="1" Margin="5, 0, 0, 0" Text="Show unofficial modlists" FontSize="14" VerticalAlignment="Center" />
</Grid>
<Grid Grid.Row="3" Margin="0, 10, 0, 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<CheckBox
x:Name="ShowNSFW"
FontSize="7"
VerticalAlignment="Center"
Foreground="{StaticResource ForegroundBrush}"
ToolTip="Only show Not Safe For Work (NSFW) modlists">
<CheckBox.LayoutTransform>
<ScaleTransform ScaleX="1.6" ScaleY="1.6"/>
</CheckBox.LayoutTransform>
</CheckBox>
<TextBlock Grid.Column="1" Margin="5, 0, 0, 0" Text="Show only NSFW modlists" FontSize="14" VerticalAlignment="Center" />
</Grid>
<Grid Grid.Row="4" Margin="0, 10, 0, 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>

View File

@ -48,14 +48,15 @@
</UserControl.Resources>
<Border
x:Name="ModListTile"
Margin="10"
Margin="8"
Background="Transparent"
BorderThickness="1">
CornerRadius="8"
BorderThickness="0">
<Border.Effect>
<DropShadowEffect
BlurRadius="25"
Opacity="0.5"
ShadowDepth="5" />
Opacity="0.25"
ShadowDepth="3" />
</Border.Effect>
<Border.Style>
<Style TargetType="Border">
@ -68,227 +69,216 @@
</Style>
</Border.Style>
<Grid
Width="570"
Height="480"
Background="{StaticResource DarkBackgroundBrush}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="40" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Border Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
BorderBrush="{StaticResource ButtonNormalBorder}"
BorderThickness="0,0,0,1">
<Grid ClipToBounds="True">
<mahapps:ProgressRing x:Name="LoadingProgress" />
<Viewbox
Height="340"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Stretch="UniformToFill">
<Image x:Name="ModListImage">
<Image.Style>
<Style TargetType="Image">
<Style.Triggers>
<DataTrigger Binding="{Binding IsBroken}" Value="True">
<Setter Property="Effect">
<Setter.Value>
<BlurEffect Radius="35" />
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</Image.Style>
</Image>
</Viewbox>
<Ellipse
Width="327"
Height="184"
Background="Transparent">
<Grid ClipToBounds="True">
<mahapps:ProgressRing x:Name="LoadingProgress" />
<TextBlock Text="{Binding Metadata.Title}" VerticalAlignment="Bottom" FontSize="14" Margin="10, 0, 0, 10" Panel.ZIndex="1"/>
<Border
BorderThickness="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Border.Background>
<ImageBrush x:Name="ModlistImage" Stretch="UniformToFill"/>
</Border.Background>
<Border.Style>
<Style TargetType="Border">
<Style.Triggers>
<DataTrigger Binding="{Binding IsBroken}" Value="True">
<Setter Property="Effect">
<Setter.Value>
<BlurEffect Radius="35" />
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
</Border>
<Ellipse
Height="120"
Margin="-40,0,-40,-60"
VerticalAlignment="Bottom"
Fill="Black"
Opacity="0.5">
<Ellipse.Effect>
<BlurEffect Radius="55" />
</Ellipse.Effect>
<Ellipse.Style>
<Style TargetType="Ellipse">
<Style.Triggers>
<DataTrigger Binding="{Binding IsMouseOver, ElementName=ModListTile}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Margin="-80,-100,-80,0"
VerticalAlignment="Top"
Fill="White"
Opacity="0.15">
<Ellipse.Effect>
<BlurEffect Radius="75" />
</Ellipse.Effect>
<Ellipse.Style>
<Style TargetType="Ellipse">
<Style.Triggers>
<DataTrigger Binding="{Binding IsMouseOver, ElementName=ModListTile}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="Opacity"
To="0.75"
To="0.25"
Duration="0:0:0.08" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="Opacity"
To="0.5"
To="0.15"
Duration="0:0:0.08" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Ellipse.Style>
</Ellipse>
<Label
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Ellipse.Style>
</Ellipse>
<Label
Margin="10,242,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Content="{Binding VersionText}"
Opacity="0">
<Label.Style>
<Style TargetType="Label">
<Style.Triggers>
<DataTrigger Binding="{Binding IsMouseOver, ElementName=ModListTile}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
<Label.Style>
<Style TargetType="Label">
<Style.Triggers>
<DataTrigger Binding="{Binding IsMouseOver, ElementName=ModListTile}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="Opacity"
To="1"
Duration="0:0:0.08" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="Opacity"
To="0"
Duration="0:0:0.08" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Label.Style>
</Label>
<Label
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Label.Style>
</Label>
<Label
Margin="10,257,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Content="{Binding DownloadSizeText}"
Opacity="0">
<Label.Style>
<Style TargetType="Label">
<Style.Triggers>
<DataTrigger Binding="{Binding IsMouseOver, ElementName=ModListTile}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
<Label.Style>
<Style TargetType="Label">
<Style.Triggers>
<DataTrigger Binding="{Binding IsMouseOver, ElementName=ModListTile}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="Opacity"
To="1"
Duration="0:0:0.08" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="Opacity"
To="0"
Duration="0:0:0.08" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Label.Style>
</Label>
<Label
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Label.Style>
</Label>
<Label
Margin="10,272,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Content="{Binding InstallSizeText}"
Opacity="0">
<Label.Style>
<Style TargetType="Label">
<Style.Triggers>
<DataTrigger Binding="{Binding IsMouseOver, ElementName=ModListTile}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
<Label.Style>
<Style TargetType="Label">
<Style.Triggers>
<DataTrigger Binding="{Binding IsMouseOver, ElementName=ModListTile}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="Opacity"
To="1"
Duration="0:0:0.08" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="Opacity"
To="0"
Duration="0:0:0.08" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Label.Style>
</Label>
<Label
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Label.Style>
</Label>
<Label
Margin="10,287,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Content="{Binding TotalSizeRequirementText}"
Opacity="0">
<Label.Style>
<Style TargetType="Label">
<Style.Triggers>
<DataTrigger Binding="{Binding IsMouseOver, ElementName=ModListTile}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
<Label.Style>
<Style TargetType="Label">
<Style.Triggers>
<DataTrigger Binding="{Binding IsMouseOver, ElementName=ModListTile}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="Opacity"
To="1"
Duration="0:0:0.08" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="Opacity"
To="0"
Duration="0:0:0.08" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Label.Style>
</Label>
</Grid>
</Border>
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Label.Style>
</Label>
</Grid>
<local:UnderMaintenanceOverlay Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
x:Name="Overlay"
Visibility="Collapsed" />
<!--
<TextBlock Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
x:Name="ModListTitleShadow"
Margin="5"
@ -402,8 +392,8 @@
Kind="Exclamation" />
</StackPanel>
</Button>
</Grid>
-->
</Grid>
</Border>
</rxui:ReactiveUserControl>

View File

@ -2,6 +2,8 @@
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Media.Media3D;
using MahApps.Metro.IconPacks;
using ReactiveUI;
@ -19,14 +21,39 @@ namespace Wabbajack
this.WhenActivated(disposables =>
{
ViewModel.WhenAnyValue(vm => vm.Image)
.BindToStrict(this, view => view.ModListImage.Source)
.BindToStrict(this, v => v.ModlistImage.ImageSource)
.DisposeWith(disposables);
/*
this.WhenAny(x => x.ViewModel.Metadata.Links.ImageUri)
.Select(x => new BitmapImage() { UriSource = new Uri(x) })
.BindToStrict(this, v => v.ModlistImage.ImageSource)
.DisposeWith(disposables);
*/
/*
ViewModel.WhenAnyValue(x => x.Metadata.Links.ImageUri)
.Select(x => {
var img = new BitmapImage();
img.BeginInit();
img.CacheOption = BitmapCacheOption.OnDemand;
img.DecodePixelWidth = 327;
var uri = new Uri(x, UriKind.Absolute);
img.UriSource = uri;
img.EndInit();
return img;
})
.BindToStrict(this, v => v.ModlistImage.ImageSource)
.DisposeWith(disposables);
*/
var textXformed = ViewModel.WhenAnyValue(vm => vm.Metadata.Title)
.CombineLatest(ViewModel.WhenAnyValue(vm => vm.Metadata.ImageContainsTitle),
ViewModel.WhenAnyValue(vm => vm.IsBroken))
.Select(x => x.Second && !x.Third ? "" : x.First);
/*
textXformed
.BindToStrict(this, view => view.ModListTitle.Text)
.DisposeWith(disposables);
@ -42,6 +69,7 @@ namespace Wabbajack
ViewModel.WhenAnyValue(x => x.ModListTagList)
.BindToStrict(this, x => x.TagsList.ItemsSource)
.DisposeWith(disposables);
*/
ViewModel.WhenAnyValue(x => x.LoadingImageLock.IsLoading)
.Select(x => x ? Visibility.Visible : Visibility.Collapsed)
@ -53,6 +81,7 @@ namespace Wabbajack
.BindToStrict(this, view => view.Overlay.Visibility)
.DisposeWith(disposables);
/*
ViewModel.WhenAnyValue(x => x.OpenWebsiteCommand)
.BindToStrict(this, x => x.OpenWebsiteButton.Command)
.DisposeWith(disposables);
@ -64,14 +93,18 @@ namespace Wabbajack
ViewModel.WhenAnyValue(x => x.ExecuteCommand)
.BindToStrict(this, x => x.ExecuteButton.Command)
.DisposeWith(disposables);
*/
/*
ViewModel.WhenAnyValue(x => x.ProgressPercent)
.ObserveOnDispatcher()
.ObserveOn(RxApp.MainThreadScheduler)
.Select(p => p.Value)
.BindTo(this, x => x.DownloadProgressBar.Value)
.DisposeWith(disposables);
*/
/*
ViewModel.WhenAnyValue(x => x.Status)
.ObserveOnGuiThread()
.Subscribe(x =>
@ -91,6 +124,7 @@ namespace Wabbajack
});
})
.DisposeWith(disposables);
*/
/*
this.MarkAsNeeded<ModListTileView, ModListMetadataVM, bool>(this.ViewModel, x => x.IsBroken);

View File

@ -96,6 +96,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Imazen.WebP" Version="10.0.1" />
<PackageReference Include="MahApps.Metro" Version="2.4.10" />
<PackageReference Include="MahApps.Metro.IconPacks" Version="4.11.0" />
<PackageReference Include="Microsoft-WindowsAPICodePack-Shell" Version="1.1.5" />