This commit is contained in:
Poker 2024-05-19 19:18:23 +08:00
parent e10ef1a357
commit ddcb9ca9c0
No known key found for this signature in database
GPG Key ID: C65A6AD457D5C8F8
128 changed files with 2177 additions and 431 deletions

View File

@ -0,0 +1,31 @@
#region Copyright (c) Pixeval/Pixeval.Controls
// GPL v3 License
//
// Pixeval/Pixeval.Controls
// Copyright (c) 2024 Pixeval.Controls/CardControl.cs
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
#endregion
using Microsoft.UI.Xaml.Controls;
namespace Pixeval.Controls;
public class CardControl : ContentControl
{
public CardControl()
{
DefaultStyleKey = typeof(CardControl);
}
}

View File

@ -0,0 +1,28 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Pixeval.Controls">
<Style TargetType="controls:CardControl">
<Setter Property="IsTabStop" Value="False" />
<Setter Property="Background" Value="{ThemeResource CardBackground}" />
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeBrush}" />
<Setter Property="BorderThickness" Value="{StaticResource CardBorderThickness}" />
<Setter Property="CornerRadius" Value="{StaticResource CardCornerRadius}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="controls:CardControl">
<Grid x:Name="CardContainer"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<ContentPresenter HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Content="{TemplateBinding Content}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@ -53,7 +53,7 @@
</Grid.RenderTransform>
<fluent:SymbolIcon
Foreground="{StaticResource LayerFillColorDefaultBrush}"
IsFilled="True"
IconVariant="Filled"
Symbol="Heart" />
<IconSourceElement IconSource="{x:Bind Command.IconSource, Mode=OneWay}" />
<fluent:SymbolIcon Symbol="Heart" />

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net8.0-windows10.0.22621.0</TargetFramework>
<TargetPlatformMinVersion>10.0.19041.0</TargetPlatformMinVersion>
<RootNamespace>Pixeval.Controls</RootNamespace>
<RootNamespace>Pixeval.Controls</RootNamespace>
<Platforms>x86;x64;arm64</Platforms>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<UseWinUI>true</UseWinUI>
@ -12,25 +12,26 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefaultLanguage>zh-cn</DefaultLanguage>
<IsTrimmable>true</IsTrimmable>
<WindowsSdkPackageVersion>10.0.22621.38</WindowsSdkPackageVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Labs.WinUI.Shimmer" Version="0.1.240517-build.1678" />
<PackageReference Include="FluentIcons.WinUI" Version="1.1.247" />
<PackageReference Include="FluentIcons.WinUI" Version="1.1.258" />
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.2.0" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.106">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.3233" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.5.240627000" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.6.240829007" />
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.9" />
<PackageReference Include="CommunityToolkit.WinUI.Triggers" Version="8.1.240606-rc" />
<PackageReference Include="CommunityToolkit.WinUI.Collections" Version="8.1.240606-rc" />
<PackageReference Include="CommunityToolkit.WinUI.Triggers" Version="8.1.240821" />
<PackageReference Include="CommunityToolkit.WinUI.Collections" Version="8.1.240821" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.1.240606-rc" />
<PackageReference Include="CommunityToolkit.WinUI.Extensions" Version="8.1.240606-rc" />
<PackageReference Include="WinUI3Utilities" Version="1.1.7.6" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.1.240821" />
<PackageReference Include="CommunityToolkit.WinUI.Extensions" Version="8.1.240821" />
<PackageReference Include="WinUI3Utilities" Version="1.1.7.8" />
<ProjectReference Include="..\Pixeval.Utilities\Pixeval.Utilities.csproj" />
<ProjectReference Include="..\Pixeval.SourceGen\Pixeval.SourceGen.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="False" />
@ -44,9 +45,6 @@
</Target>
<ItemGroup>
<CompilerVisibleItemMetadata Include="AdditionalFiles" MetadataName="SourceItemGroup" />
</ItemGroup>
<ItemGroup>
<PRIResource Include="Strings\*\*.resjson" />
</ItemGroup>

View File

@ -1,24 +1,31 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Pixeval.Controls">
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<CornerRadius x:Key="CardCornerRadius">5</CornerRadius>
<Thickness x:Key="CardBorderThickness">1</Thickness>
<x:Double x:Key="SmallIconFontSize">16</x:Double>
<Thickness x:Key="CardControlPadding">16</Thickness>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Default">
<SolidColorBrush x:Key="InfoBarInformationalSeverityBackgroundBrush" Color="#FF34424d" />
<Color x:Key="InfoBarInformationalSeverityIconBackground">#FF5fb2f2</Color>
<SolidColorBrush x:Key="CardStrokeBrush" Color="#0F000000" />
<SolidColorBrush x:Key="CardBackground" Color="#FAFCFD" />
</ResourceDictionary>
<ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="InfoBarInformationalSeverityBackgroundBrush" Color="#FFd3e7f7" />
<Color x:Key="InfoBarInformationalSeverityIconBackground">#FF0063b1</Color>
<SolidColorBrush x:Key="CardStrokeBrush" Color="#0F000000" />
<SolidColorBrush x:Key="CardBackground" Color="#FAFCFD" />
</ResourceDictionary>
<ResourceDictionary x:Key="HighContrast">
<SolidColorBrush x:Key="InfoBarInformationalSeverityBackgroundBrush" Color="#FF34424d" />
<Color x:Key="InfoBarInformationalSeverityIconBackground">#FF5fb2f2</Color>
<SolidColorBrush x:Key="CardStrokeBrush" Color="#19000000" />
<SolidColorBrush x:Key="CardBackground" Color="#2B2A2F" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>

View File

@ -7,6 +7,8 @@
<ResourceDictionary Source="ms-appx:///Pixeval.Controls/IconButton/IconButton.xaml" />
<ResourceDictionary Source="ms-appx:///Pixeval.Controls/NotifyOnLoadedComboBox/NotifyOnLoadedComboBox.xaml" />
<ResourceDictionary Source="ms-appx:///Pixeval.Controls/NotifyOnLoadedCalendarDatePicker/NotifyOnLoadedCalendarDatePicker.xaml" />
<ResourceDictionary Source="ms-appx:///Pixeval.Controls/Timeline/TimelineUnit.xaml" />
<ResourceDictionary Source="ms-appx:///Pixeval.Controls/CardControl/CardControl.xaml" />
<!-- ReSharper restore Xaml.PathError -->
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

View File

@ -0,0 +1,41 @@
#region Copyright (c) Pixeval/Pixeval.Controls
// GPL v3 License
//
// Pixeval/Pixeval.Controls
// Copyright (c) 2024 Pixeval.Controls/TimelineAxisPlacement.cs
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
#endregion
using System;
namespace Pixeval.Controls.Timeline;
public enum TimelineAxisPlacement
{
Left, Right
}
public static class TimelineAxisPlacementExtension
{
public static TimelineAxisPlacement Inverse(this TimelineAxisPlacement placement)
{
return placement switch
{
TimelineAxisPlacement.Left => TimelineAxisPlacement.Right,
TimelineAxisPlacement.Right => TimelineAxisPlacement.Left,
_ => throw new ArgumentOutOfRangeException(nameof(placement), placement, null)
};
}
}

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<UserControl
x:Class="Pixeval.Controls.Timeline.TimelineControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Pixeval.Controls.Timeline"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<ListView></ListView>
</UserControl>

View File

@ -0,0 +1,17 @@
using Microsoft.UI.Xaml;
using WinUI3Utilities.Attributes;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace Pixeval.Controls.Timeline
{
[DependencyProperty<DataTemplate>("ItemTemplate")]
public sealed partial class TimelineControl
{
public TimelineControl()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,186 @@
using Windows.UI;
using Microsoft.UI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using WinUI3Utilities.Attributes;
namespace Pixeval.Controls.Timeline;
[DependencyProperty<TimelineAxisPlacement>("FoldedDefaultPlacement", "Pixeval.Controls.Timeline.TimelineAxisPlacement.Left")]
[DependencyProperty<double>("FoldThreshold", "-1.0")]
[DependencyProperty<TimelineAxisPlacement>("TimelineAxisPlacement", "Pixeval.Controls.Timeline.TimelineAxisPlacement.Left", nameof(TimelineAxisPlacementPropertyChangedCallback))]
[DependencyProperty<IconSource>("TitleIcon")]
[DependencyProperty<SolidColorBrush>("TitleIconBackground", DependencyPropertyDefaultValue.Default)]
public sealed partial class TimelineUnit : ContentControl
{
public TimelineUnit()
{
DefaultStyleKey = typeof(TimelineUnit);
}
private bool _folded;
private bool _differentDefaultAxisPlacement;
private Grid _leftIndicatorAxis = null!;
private Grid _rightIndicatorAxis = null!;
private Grid _leftIconContainer = null!;
private Grid _rightIconContainer = null!;
private ColumnDefinition _rightColumn = null!;
private ColumnDefinition _leftColumn = null!;
private ContentControl _contentPresenter = null!;
private double _containerHeightFixed;
private static void TimelineAxisPlacementPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is TimelineUnit { IsLoaded: true } unit && e.NewValue is TimelineAxisPlacement placement)
{
unit._folded = !unit._folded;
unit.AdjustAxisPlacement(placement, true); // unit._folded;
}
}
private double GetTimelineAxisHeight(bool isAuxAxis)
{
if (ActualHeight is not 0 && _containerHeightFixed is 0)
{
_containerHeightFixed = ActualHeight;
return isAuxAxis ? _containerHeightFixed : _containerHeightFixed - 45 is >= 0 and var value ? value : 0;
}
else
return isAuxAxis ? _containerHeightFixed : _containerHeightFixed - 45 is >= 0 and var value ? value : 0;
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
_leftIndicatorAxis = (GetTemplateChild("LeftIndicatorAxis") as Grid)!;
_rightIndicatorAxis = (GetTemplateChild("RightIndicatorAxis") as Grid)!;
_leftIconContainer = (GetTemplateChild("LeftIconContainer") as Grid)!;
_rightIconContainer = (GetTemplateChild("RightIconContainer") as Grid)!;
_contentPresenter = (GetTemplateChild("ContentPresenter") as ContentControl)!;
_leftColumn = (GetTemplateChild("LeftColumn") as ColumnDefinition)!;
_rightColumn = (GetTemplateChild("RightColumn") as ColumnDefinition)!;
_contentPresenter.Loaded += ContentContainerOnLoaded;
SizeChanged += OnSizeChanged;
}
private bool FoldCriteria()
{
return FoldThreshold is not -1 && ActualWidth < FoldThreshold;
}
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
if (FoldCriteria())
{
if (TimelineAxisPlacement == FoldedDefaultPlacement)
{
AdjustAxisPlacement(TimelineAxisPlacement, true);
}
else
{
_differentDefaultAxisPlacement = true;
TimelineAxisPlacement = FoldedDefaultPlacement;
}
}
if (FoldThreshold is not -1 &&
ActualWidth > FoldThreshold &&
TimelineAxisPlacement != FoldedDefaultPlacement.Inverse())
{
if (!_differentDefaultAxisPlacement)
{
AdjustAxisPlacement(TimelineAxisPlacement, true);
}
else
{
TimelineAxisPlacement = FoldedDefaultPlacement.Inverse();
}
}
}
private void AdjustAxisPlacement(TimelineAxisPlacement placement, bool isFolding)
{
switch (placement)
{
case TimelineAxisPlacement.Left:
_rightColumn.Width = new GridLength(0);
_leftIconContainer.Visibility = Visibility.Visible;
_rightIconContainer.Visibility = Visibility.Collapsed;
_leftIndicatorAxis.Margin = isFolding
? new Thickness(0, 5, 0, 5)
: new Thickness(0, 5, 0, 0);
_leftIndicatorAxis.CornerRadius = isFolding
? new CornerRadius(2)
: new CornerRadius(2, 2, 0, 0);
if (isFolding)
{
_rightIndicatorAxis.Visibility = Visibility.Collapsed;
}
else
{
_leftIndicatorAxis.Visibility = Visibility.Visible;
_rightIndicatorAxis.Visibility = Visibility.Visible;
_rightIndicatorAxis.Margin = new Thickness(0, 0, 0, 5);
_rightIndicatorAxis.CornerRadius = new CornerRadius(0, 0, 2, 2);
}
_contentPresenter.HorizontalAlignment = HorizontalAlignment.Left;
AdjustIndicatorAxisSize();
break;
case TimelineAxisPlacement.Right:
_leftColumn.Width = new GridLength(0);
_leftIconContainer.Visibility = Visibility.Collapsed;
_rightIconContainer.Visibility = Visibility.Visible;
_rightIndicatorAxis.Margin = isFolding
? new Thickness(0, 5, 0, 5)
: new Thickness(0, 5, 0, 0);
_rightIndicatorAxis.CornerRadius = isFolding
? new CornerRadius(2)
: new CornerRadius(2, 2, 0, 0);
if (isFolding)
{
_leftIndicatorAxis.Visibility = Visibility.Collapsed;
}
else
{
_rightIndicatorAxis.Visibility = Visibility.Visible;
_leftIndicatorAxis.Visibility = Visibility.Visible;
_leftIndicatorAxis.Margin = new Thickness(0, 0, 0, 5);
_leftIndicatorAxis.CornerRadius = new CornerRadius(0, 0, 2, 2);
}
_contentPresenter.HorizontalAlignment = HorizontalAlignment.Right;
AdjustIndicatorAxisSize();
break;
}
}
private void ContentContainerOnLoaded(object sender, RoutedEventArgs e)
{
AdjustIndicatorAxisSize();
AdjustAxisPlacement(TimelineAxisPlacement, true);
_leftIconContainer.Background = TitleIconBackground;
_rightIconContainer.Background = TitleIconBackground;
}
public void Reset()
{
AdjustIndicatorAxisSize();
AdjustAxisPlacement(TimelineAxisPlacement, true);
}
private void AdjustIndicatorAxisSize()
{
_leftIndicatorAxis.Height = GetTimelineAxisHeight(TimelineAxisPlacement is TimelineAxisPlacement.Right);
_rightIndicatorAxis.Height = GetTimelineAxisHeight(TimelineAxisPlacement is TimelineAxisPlacement.Left);
}
}

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:timeline="using:Pixeval.Controls.Timeline">
<Style TargetType="timeline:TimelineUnit" >
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="timeline:TimelineUnit">
<Grid x:Name="TimelineSectionBackground" HorizontalAlignment="Stretch" VerticalAlignment="Center" Background="{TemplateBinding Background}">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="LeftColumn" Width="50" />
<ColumnDefinition Width="*"/>
<ColumnDefinition x:Name="RightColumn" Width="50" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" VerticalAlignment="Stretch">
<Grid x:Name="LeftIconContainer" Background="#F4F6F8" HorizontalAlignment="Center" VerticalAlignment="Center" CornerRadius="25" Width="45" Height="45">
<IconSourceElement IconSource="{TemplateBinding TitleIcon}" Width="45" Height="45" HorizontalAlignment="Center" />
</Grid>
<Grid x:Name="LeftIndicatorAxis" Background="#E3E3E3" Width="4" />
</StackPanel>
<ContentControl
x:Name="ContentPresenter"
Grid.Column="1"
Padding="15,0"
Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}"
HorizontalAlignment="Left" VerticalAlignment="Top"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentTemplateSelector="{TemplateBinding ContentTemplateSelector}"
Content="{TemplateBinding Content}"
DataContext="{TemplateBinding DataContext}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" />
<StackPanel Grid.Column="2" VerticalAlignment="Stretch">
<Grid x:Name="RightIconContainer" Background="#F4F6F8" HorizontalAlignment="Center" VerticalAlignment="Center" CornerRadius="25" Width="45" Height="45">
<IconSourceElement IconSource="{TemplateBinding TitleIcon}" Width="45" Height="45" HorizontalAlignment="Center" />
</Grid>
<Grid x:Name="RightIndicatorAxis" Background="#E3E3E3" Width="4" />
</StackPanel>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@ -26,6 +26,8 @@ namespace Pixeval.CoreApi.Engine;
public class EngineHandle : ICancellable, INotifyCompletion, ICompletionCallback<EngineHandle>
#pragma warning restore 660,661
{
public static readonly EngineHandle Default = new(Guid.Empty);
private readonly Action<EngineHandle>? _onCompletion;
public bool Equals(EngineHandle other)

View File

@ -31,8 +31,8 @@ namespace Pixeval.CoreApi.Engine;
/// deserialize its content into a list of result entries or stops and reports the iteration is over
/// </para>
/// </summary>
/// <typeparam name="TE">The type of the results of the <see cref="IFetchEngine{TE}" /></typeparam>
public interface IFetchEngine<out TE> : IAsyncEnumerable<TE>, IMakoClientSupport, IEngineHandleSource
/// <typeparam name="TResult">The type of the results of the <see cref="IFetchEngine{TE}" /></typeparam>
public interface IFetchEngine<out TResult> : IAsyncEnumerable<TResult>, IMakoClientSupport, IEngineHandleSource
{
/// <summary>
/// How many pages have been fetches

View File

@ -179,14 +179,14 @@ internal partial class FeedEngine(MakoClient makoClient, EngineHandle? engineHan
FeedType? feedType = status.Value.GetPropertyString("type") switch
{
"add_bookmark" => FeedType.AddBookmark,
"add_illust" => FeedType.AddIllust,
"add_illust" => FeedType.PostIllust,
"add_novel_bookmark" => FeedType.AddNovelBookmark,
"add_favorite" => FeedType.AddFavorite,
_ => null
};
var feedTargetId = feedType switch
{
FeedType.AddBookmark or FeedType.AddIllust => status.Value.GetProperty("ref_illust").GetPropertyString("id"),
FeedType.AddBookmark or FeedType.PostIllust => status.Value.GetProperty("ref_illust").GetPropertyString("id"),
FeedType.AddFavorite => status.Value.GetProperty("ref_user").GetPropertyLong("id").ToString(), // long & string in two objects with almost the same properties? fuck pixiv
FeedType.AddNovelBookmark => status.Value.GetProperty("ref_novel").GetPropertyString("id"),
_ => null
@ -198,7 +198,7 @@ internal partial class FeedEngine(MakoClient makoClient, EngineHandle? engineHan
var feedTargetThumbnail = feedType switch
{
FeedType.AddBookmark or FeedType.AddIllust => illusts.FirstOrNull(i => i.Name == feedTargetId)
FeedType.AddBookmark or FeedType.PostIllust => illusts.FirstOrNull(i => i.Name == feedTargetId)
?.GetPropertyOrNull("url")
?.GetPropertyOrNull("m")
?.GetString(),
@ -231,20 +231,26 @@ internal partial class FeedEngine(MakoClient makoClient, EngineHandle? engineHan
?.GetPropertyOrNull("url")
?.GetPropertyOrNull("m")
?.GetString();
if (!long.TryParse(feedTargetId, out var feedIdLong))
{
return null;
}
var feedObject = new Feed
{
FeedId = feedTargetId,
Id = feedIdLong,
FeedThumbnail = feedTargetThumbnail,
Type = feedType,
PostDate = postDate,
PostUserId = postUserId,
PostUserName = postUserName,
PostUsername = postUserName,
PostUserThumbnail = postUserThumbnail
};
switch (feedType)
{
case FeedType.AddBookmark or FeedType.AddIllust:
case FeedType.AddBookmark or FeedType.PostIllust:
{
var illustration = illusts.FirstOrNull(i => i.Name == feedTargetId);
feedObject.ArtistName = users.FirstOrNull(u => u.Name == (illustration?.GetPropertyOrNull("post_user")?.GetPropertyOrNull("id")?.GetString() ?? string.Empty))?.GetPropertyOrNull("name")?.GetString();

View File

@ -286,7 +286,7 @@ public partial class MakoClient
/// <returns>
/// The <see cref="FeedEngine" /> containing the feeds.
/// </returns>
public IFetchEngine<Feed> Feeds()
public IFetchEngine<Feed?> Feeds()
{
EnsureNotCancelled();
return new FeedEngine(this, new EngineHandle(CancelInstance));

View File

@ -51,8 +51,8 @@ public partial class MakoClient : ICancellable, IDisposable, IAsyncDisposable
{
Logger = logger;
Session = session;
Provider = BuildServiceProvider(Services);
Configuration = configuration;
Provider = BuildServiceProvider(Services);
IsCancelled = false;
}
@ -91,11 +91,11 @@ public partial class MakoClient : ICancellable, IDisposable, IAsyncDisposable
{
BaseAddress = new Uri(MakoHttpOptions.AppApiBaseUrl)
})
.AddKeyedSingleton<HttpClient, MakoHttpClient>(MakoApiKind.WebApi,
(s, _) => new(s.GetRequiredKeyedService<HttpMessageHandler>(typeof(PixivApiHttpMessageHandler)))
{
BaseAddress = new Uri(MakoHttpOptions.WebApiBaseUrl)
})
.AddKeyedSingleton<HttpClient, MakoHttpClient>(MakoApiKind.WebApi,
(s, _) => new MakoHttpClient(s.GetRequiredKeyedService<HttpMessageHandler>(typeof(PixivApiHttpMessageHandler)))
{
BaseAddress = new Uri(MakoHttpOptions.WebApiBaseUrl),
})
.AddKeyedSingleton<HttpClient, MakoHttpClient>(MakoApiKind.AuthApi,
(s, _) => new(s.GetRequiredKeyedService<HttpMessageHandler>(typeof(PixivApiHttpMessageHandler)))
{

View File

@ -22,12 +22,12 @@ using System;
namespace Pixeval.CoreApi.Model;
public record Feed : IEntry
public record Feed : IIdEntry
{
/// <summary>
/// May points to user, illustration or novel
/// </summary>
public string? FeedId { get; set; }
public long Id { get; set; }
/// <summary>
/// The name of the target of this feed if it has one
@ -45,7 +45,7 @@ public record Feed : IEntry
/// </summary>
public string? PostUserId { get; set; }
public string? PostUserName { get; set; }
public string? PostUsername { get; set; }
/// <summary>
/// The creator's name of the illustration/novel if possible
@ -62,7 +62,7 @@ public record Feed : IEntry
public string? PostUserThumbnail { get; set; }
/// <summary>
/// Is this feed's target pointing to an user
/// Is this feed's target pointing to a user
/// </summary>
public bool IsTargetRefersToUser { get; set; }
}
@ -77,7 +77,7 @@ public enum FeedType
/// <summary>
/// User posted a new illust
/// </summary>
AddIllust,
PostIllust,
/// <summary>
/// User followed an artist

View File

@ -41,8 +41,15 @@ internal class PixivApiHttpMessageHandler(MakoClient makoClient) : MakoClientSup
headers.UserAgent.AddRange(MakoClient.Configuration.UserAgent);
headers.AcceptLanguage.Add(new StringWithQualityHeaderValue(MakoClient.Configuration.CultureInfo.Name));
if (host is MakoHttpOptions.AppApiHost)
headers.Authorization = new AuthenticationHeaderValue("Bearer", MakoClient.Session.AccessToken);
switch (host)
{
case MakoHttpOptions.AppApiHost:
headers.Authorization = new AuthenticationHeaderValue("Bearer", MakoClient.Session.AccessToken);
break;
case MakoHttpOptions.WebApiHost:
headers.TryAddWithoutValidation("Cookie", makoClient.Configuration.Cookie);
break;
}
return GetHttpMessageInvoker(domainFronting)
.SendAsync(request, cancellationToken);

View File

@ -31,10 +31,11 @@ public record MakoClientConfiguration(
int ConnectionTimeout,
bool DomainFronting,
string? Proxy,
string? Cookie,
string? MirrorHost,
CultureInfo CultureInfo)
{
public MakoClientConfiguration() : this(5000, false, "", "", CultureInfo.CurrentCulture) { }
public MakoClientConfiguration() : this(5000, false, "", "", "", CultureInfo.CurrentCulture) { }
[JsonIgnore] public CultureInfo CultureInfo { get; set; } = CultureInfo;
@ -59,6 +60,9 @@ public record MakoClientConfiguration(
[JsonPropertyName("proxy")]
public string? Proxy { get; set; } = Proxy;
[JsonPropertyName("cookie")]
public string? Cookie { get; set; } = Cookie;
/// <summary>
/// Mirror server's host of image downloading
/// </summary>

View File

@ -13,7 +13,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" PrivateAssets="all" />
<PackageReference Include="PolySharp" Version="1.14.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@ -0,0 +1,201 @@
#region Copyright (c) Pixeval/Pixeval.Utilities
// GPL v3 License
//
// Pixeval/Pixeval.Utilities
// Copyright (c) 2024 Pixeval.Utilities/Debounce.cs
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
namespace Pixeval.Utilities;
public interface IDebouncedTask<T, TResult> where T : struct, IEquatable<T>
{
T Id { get; }
T? Dependency { get; }
Task<TResult> ExecuteAsync();
bool IsFinalizer { get; }
bool IsHead { get; }
}
/// <summary>
/// This class is used to debounce tasks that are dependent on each other, a common <c>debounce()</c> function often
/// debouncing per task basis, however, sometimes tasks can be grouped, this class debounces specifically the tasks
/// that are dependent on each other.
///
/// To use this class, a tag of type <see cref="T"/> is required, the tag identifies each task and its dependency,
/// the class debounces tasks in the following way:
///
/// 1. If a task has a dependency, it will be executed only if the dependency has been executed, otherwise it will
/// be debounced (disregarded) edit: check the todo 9/12/2024
/// 2. If a task has already been executed, it will be debounced (disregarded)
/// 3. If a task is a finalizer, then it finalizes the task group, the whole chain of dependency will be removed from
/// the executed tasks list, allows this group of task to be executed once again.
/// 4. Pre-debouncing:
/// 1. If a task is the same as the last task, it will be debounced (disregarded)
/// 2. If a task group is the same as the last task group (executing), it will be debounced (disregarded)
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="TResult"></typeparam>
public class Debounce<T, TResult> : IDisposable where T : struct, IEquatable<T>
{
private record DebounceTaskWrapper(bool Disregarded, IDebouncedTask<T, TResult> Task, TaskCompletionSource<TResult> Completion)
{
public bool Disregarded { get; set; } = Disregarded;
}
private bool _started;
private readonly LinkedList<DebounceTaskWrapper> _executedTasks = [];
private readonly Channel<DebounceTaskWrapper> _taskQueue = Channel.CreateUnbounded<DebounceTaskWrapper>();
private readonly List<DebounceTaskWrapper> _auxQueue = [];
public async Task<TResult> ExecuteAsync(IDebouncedTask<T, TResult> task)
{
if (_taskQueue.Reader.Completion.IsCompleted)
{
throw new InvalidOperationException("The debounce queue has been disposed");
}
if (!_started)
_ = StartLoopAsync();
var wrapper = new DebounceTaskWrapper(false, task, new TaskCompletionSource<TResult>());
if (_auxQueue.LastOrDefault()?.Task.Id.Equals(task.Id) ?? false)
{
return await Task.FromCanceled<TResult>(new CancellationToken(true));
}
_auxQueue.Add(wrapper);
if (task.IsFinalizer)
{
// debouncing a head-finalizer-head-finalizer pattern
var dependencyChain = FindDependencyChainFrom(new LinkedList<DebounceTaskWrapper>(_auxQueue), wrapper);
var truncated = new LinkedList<DebounceTaskWrapper>(_auxQueue[..^dependencyChain.Count]);
var oldDependencyChain = FindDependencyChainOf(truncated, task.Id);
if (dependencyChain.SequenceEquals(oldDependencyChain, ts => ts.Task.Id))
{
foreach (var debouncedTaskWrapper in dependencyChain)
{
debouncedTaskWrapper.Disregarded = true;
}
}
}
// debouncing a finalizer-head-finalizer pattern
if (_executedTasks.LastOrDefault()?.Task.IsFinalizer ?? false && task.IsFinalizer)
{
var dependencyChain = FindDependencyChainFrom(new LinkedList<DebounceTaskWrapper>(_auxQueue), wrapper);
if (dependencyChain.FirstOrDefault()?.Task?.IsHead ?? false)
{
foreach (var debounceTaskWrapper in dependencyChain)
{
debounceTaskWrapper.Disregarded = true;
}
}
}
// TODO debouncing op2-finalizer-head-op2-finalizer pattern
await _taskQueue.Writer.WriteAsync(wrapper);
return await wrapper.Completion.Task;
}
private async Task StartLoopAsync()
{
if (_started)
return;
_started = true;
while (await _taskQueue.Reader.WaitToReadAsync())
{
var wrapper = await _taskQueue.Reader.ReadAsync();
var (disregarded, task, completion) = wrapper;
if (disregarded)
{
completion.SetCanceled();
}
// TODO comment this temporarily for some tasks can be both grouped or singleton
// if (task.Dependency is { } dep && _executedTasks.All(t => !t.Task.Id.Equals(dep)))
// {
// completion.SetCanceled();
// continue;
// }
if (_executedTasks.Any(t => t.Task.Id.Equals(task.Id)))
{
completion.SetCanceled();
continue;
}
// it's important to add task to list before executing it
_executedTasks.AddLast(wrapper);
var result = await task.ExecuteAsync();
_auxQueue.RemoveAt(0);
if (task.IsFinalizer)
{
var dependencyChain = FindDependencyChainFrom(_executedTasks, wrapper);
foreach (var t in dependencyChain)
{
_executedTasks.Remove(t);
}
}
completion.SetResult(result);
}
}
private static List<DebounceTaskWrapper> FindDependencyChainOf(LinkedList<DebounceTaskWrapper> executedTasks, T id)
{
var current = executedTasks.LastOrDefault(t => t.Task.Id.Equals(id));
return current is null ? [] : FindDependencyChainFrom(executedTasks, current);
}
private static List<DebounceTaskWrapper> FindDependencyChainFrom(LinkedList<DebounceTaskWrapper> executedTasks, DebounceTaskWrapper current)
{
List<DebounceTaskWrapper> list = [current];
for (var node = executedTasks.Last; node is { Value: { Task: var val } wrapper }; node = node.Previous)
{
if (val.Id.Equals(current.Task.Dependency))
{
list.Add(wrapper);
current = wrapper;
}
}
return list;
}
public void Dispose()
{
_taskQueue.Writer.Complete();
GC.SuppressFinalize(this);
}
}

View File

@ -21,6 +21,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
@ -62,6 +63,37 @@ public static class Enumerates
return enumerable as IList<T> ?? enumerable.ToList();
}
private class KeyedEqualityComparer<T, TKey>(Func<T, TKey> selector) : IEqualityComparer<T> where TKey : IEquatable<TKey>
{
public bool Equals(T? x, T? y)
{
if (x is null && y is null)
return true;
if (x is null || y is null)
return false;
return selector(x).Equals(selector(y));
}
public int GetHashCode([DisallowNull] T obj)
{
return selector(obj).GetHashCode();
}
}
public static bool SequenceEquals<T, TKey>(this IEnumerable<T> @this,
IEnumerable<T> another,
Func<T, TKey> keySelector,
SequenceComparison comparison = SequenceComparison.Sequential)
where TKey : IEquatable<TKey>
{
return comparison switch
{
SequenceComparison.Sequential => @this.SequenceEqual(another, new KeyedEqualityComparer<T,TKey>(keySelector)),
SequenceComparison.Unordered => @this.Order().SequenceEqual(another.Order(), new KeyedEqualityComparer<T, TKey>(keySelector)), // not the fastest way, but still enough
_ => ThrowUtils.ArgumentOutOfRange<SequenceComparison, bool>(comparison)
};
}
public static bool SequenceEquals<T>(this IEnumerable<T> @this,
IEnumerable<T> another,
SequenceComparison comparison = SequenceComparison.Sequential,

View File

@ -45,13 +45,6 @@ public static class Functions
block(obj);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T LetChain<T>(this T obj, Action<T> block)
{
block(obj);
return obj;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T Apply<T>(this T obj, Action<T> block)
{

View File

@ -24,5 +24,5 @@ namespace Pixeval.Controls;
public interface IFactory<in T, out TSelf>
{
static abstract TSelf CreateInstance(T entry);
static abstract TSelf CreateInstance(T entry, int index);
}

View File

@ -8,6 +8,6 @@
<IsTrimmable>true</IsTrimmable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.HighPerformance" Version="8.2.2" />
<PackageReference Include="CommunityToolkit.HighPerformance" Version="8.3.1" />
</ItemGroup>
</Project>

View File

@ -43,7 +43,7 @@ namespace Pixeval.AppManagement;
/// <summary>
/// Provide miscellaneous information about the app
/// </summary>
[AppContext<AppSettings>(ConfigKey = "Config", Type = ApplicationDataContainerType.Roaming, MethodName = "Config")]
[AppContext<AppSettings>(ConfigKey = "Config", MethodName = "Config")]
[AppContext<LoginContext>(ConfigKey = "LoginContext", MethodName = "LoginContext")]
[AppContext<AppDebugTrace>(ConfigKey = "DebugTrace", MethodName = "DebugTrace")]
public static partial class AppInfo
@ -91,6 +91,9 @@ public static partial class AppInfo
public static string IconAbsolutePath => ApplicationUriToPath(new Uri(IconApplicationUri));
public static Uri NavigationIconUri(string name) => new Uri($"ms-appx:///Assets/Images/Icons/{name}.png");
public static string ApplicationUriToPath(Uri uri)
{
if (uri.Scheme is not "ms-appx")

View File

@ -35,8 +35,8 @@ using Pixeval.Options;
using Pixeval.Util.UI;
using WinUI3Utilities;
using WinUI3Utilities.Attributes;
using Windows.Globalization;
using FluentIcons.Common;
using Microsoft.Windows.Globalization;
using Pixeval.Utilities;
using static Pixeval.SettingsPageResources;
@ -129,6 +129,9 @@ public partial record AppSettings() : IWindowSettings
[SettingsEntry(Symbol.Key, nameof(ReverseSearchApiKeyEntryHeader), nameof(ReverseSearchApiKeyEntryDescriptionHyperlinkButtonContent))]
public string ReverseSearchApiKey { get; set; } = "";
[SettingsEntry(Symbol.Cookies, nameof(WebCookieEntryHeader), nameof(WebCookieEntryDescription))]
public string? WebCookie { get; set; }
[SettingsEntry(Symbol.TargetArrow, nameof(ReverseSearchResultSimilarityThresholdEntryHeader), nameof(ReverseSearchResultSimilarityThresholdEntryDescription))]
public int ReverseSearchResultSimilarityThreshold { get; set; } = 80;
@ -288,7 +291,7 @@ public partial record AppSettings() : IWindowSettings
public MakoClientConfiguration ToMakoClientConfiguration()
{
return new MakoClientConfiguration(5000, EnableDomainFronting, Proxy, MirrorHost, CurrentCulture);
return new MakoClientConfiguration(5000, EnableDomainFronting, Proxy, WebCookie, MirrorHost, CurrentCulture);
}
private static string GetSpecialFolder()

View File

@ -12,7 +12,7 @@ namespace Pixeval.AppManagement;
public class Versioning
{
public Version CurrentVersion { get; } = Version.Parse(GitVersionInformation.AssemblySemVer);
public Version CurrentVersion { get; } = Version.Parse("4.1.1"/*TODO:GitVersionInformation.AssemblySemVer*/);
public Version? NewestVersion => NewestAppReleaseModel?.Version;

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 KiB

View File

@ -42,7 +42,7 @@ using QuestPDF.Infrastructure;
namespace Pixeval.Controls;
public partial class DocumentViewerViewModel(NovelContent novelContent) : ObservableObject, IDisposable, INovelParserViewModel<SoftwareBitmapSource>, INovelParserViewModel<Stream>
public partial class DocumentViewerViewModel(NovelContent novelContent) : ObservableObject, INovelParserViewModel<SoftwareBitmapSource>, INovelParserViewModel<Stream>
{
/// <summary>
/// 需要从外部Invoke

View File

@ -4,8 +4,23 @@ using WinUI3Utilities.Attributes;
namespace Pixeval.Controls;
/// <summary>
/// <see cref="IsLoadingMore"/>将覆盖<see cref="HasNoItem"/>的效果
/// This view is provided as a holder for, normally a list of items with optionally incremental loading functionality
/// It features four additional properties in order to give a better UI experience
/// </summary>
///
/// <remarks>
/// <list type="bullet">
/// <item>
/// <see cref="HasNoItem"/> determines whether the list is empty, that is, not only it currently has no items presented, but also that no more item will be loaded. The correct image will be displayed to inform user when this property is <c>true</c>
/// </item>
/// <item>
/// <see cref="IsLoadingMore"/> determines whether the list is under loading, the correct image will be displayed to inform user when this property is <c>true</c>
/// </item>
/// <item>
/// <see cref="IsLoadingMore"/> takes higher priority than <see cref="HasNoItem"/>, i.e. If both of them are <c>true</c>, the effect of <see cref="IsLoadingMore"/> overwrites that of <see cref="HasNoItem"/>
/// </item>
/// </list>
/// </remarks>
[DependencyProperty<bool>("HasNoItem", "true", nameof(OnHasNoItemChanged))]
[DependencyProperty<bool>("IsLoadingMore", "false", nameof(OnHasNoItemChanged))]
[DependencyProperty<object>("Content")]

View File

@ -33,6 +33,7 @@ using Symbol = FluentIcons.Common.Symbol;
namespace Pixeval.Controls;
[DebuggerDisplay("{Entry}")]
public abstract class EntryViewModel<T>(T entry) : ObservableObject, IDisposable where T : IEntry
{

View File

@ -58,7 +58,7 @@ public class FetchEngineIncrementalSource<T, TViewModel>(IAsyncEnumerable<T?> as
{
if (_asyncEnumerator.Current is { } obj && !_yieldedItems.Contains(Identifier(obj)))
{
result.Add(Select(obj));
result.Add(Select(obj, _yieldedCounter));
_ = _yieldedItems.Add(Identifier(obj));
++i;
_yieldedCounter++;
@ -75,5 +75,5 @@ public class FetchEngineIncrementalSource<T, TViewModel>(IAsyncEnumerable<T?> as
protected virtual long Identifier(T entity) => entity.Id;
protected virtual TViewModel Select(T entity) => TViewModel.CreateInstance(entity);
protected TViewModel Select(T entity, int index) => TViewModel.CreateInstance(entity, index);
}

View File

@ -96,7 +96,7 @@
<Grid controls:DockPanel.Dock="Right">
<fluent:SymbolIcon
Foreground="{StaticResource LayerFillColorDefaultBrush}"
IsFilled="True"
IconVariant="Filled"
Symbol="SquareMultiple"
Visibility="{x:Bind local:C.ToVisibility(ViewModel.IsManga), Mode=OneWay}" />
<fluent:SymbolIcon Symbol="SquareMultiple" Visibility="{x:Bind local:C.ToVisibility(ViewModel.IsManga), Mode=OneWay}" />

View File

@ -49,7 +49,7 @@ public partial class IllustrationItemViewModel
protected override void SaveCommandOnExecuteRequested(XamlUICommand sender, ExecuteRequestedEventArgs args)
{
var hWnd = null as ulong?;
var getImageStream = null as GetImageStreams;
GetImageStream? getImageStreamAsync = null;
switch (args.Parameter)
{
case (ulong h, GetImageStreams f):
@ -71,7 +71,7 @@ public partial class IllustrationItemViewModel
protected override async void SaveAsCommandOnExecuteRequested(XamlUICommand sender, ExecuteRequestedEventArgs args)
{
ulong hWnd;
var getImageStream = null as GetImageStreams;
GetImageStream? getImageStreamAsync = null;
switch (args.Parameter)
{
case (ulong h, GetImageStreams f):
@ -108,6 +108,7 @@ public partial class IllustrationItemViewModel
private void SaveUtility(ulong? hWnd, GetImageStreams? getImageStream, string path)
{
var ib = hWnd?.InfoGrowlReturn("");
Progress<double>? progress = null;
if (ib is not null)
ib.Title = EntryItemResources.ImageProcessing;
@ -146,7 +147,7 @@ public partial class IllustrationItemViewModel
var ib = hWnd?.InfoGrowlReturn("");
var progress = null as Progress<double>;
Progress<double>? progress = null;
if (ib is not null)
if (!IsUgoira)
progress = new Progress<double>(d => ib.Title = EntryItemResources.UgoiraProcessing.Format(d));

View File

@ -35,7 +35,7 @@ namespace Pixeval.Controls;
/// </summary>
public partial class IllustrationItemViewModel : WorkEntryViewModel<Illustration>, IFactory<Illustration, IllustrationItemViewModel>
{
public static IllustrationItemViewModel CreateInstance(Illustration entry) => new(entry);
public static IllustrationItemViewModel CreateInstance(Illustration entry, int _) => new(entry);
public IllustrationItemViewModel(Illustration illustration) : base(illustration)
{

View File

@ -14,14 +14,14 @@ public partial class IllustratorItemViewModel
{
InitializeCommandsBase();
FollowCommand.GetFollowCommand(IsFollowed);
FollowCommand.RefreshFollowCommand(IsFollowed);
FollowCommand.ExecuteRequested += FollowCommandExecuteRequested;
}
private async void FollowCommandExecuteRequested(XamlUICommand sender, ExecuteRequestedEventArgs args)
{
IsFollowed = await MakoHelper.SetFollowAsync(UserId, !IsFollowed);
FollowCommand.GetFollowCommand(IsFollowed);
FollowCommand.RefreshFollowCommand(IsFollowed);
}
public override Uri AppUri => MakoHelper.GenerateUserAppUri(UserId);

View File

@ -26,7 +26,7 @@ namespace Pixeval.Controls;
public sealed partial class IllustratorItemViewModel : EntryViewModel<User>, IFactory<User, IllustratorItemViewModel>
{
public static IllustratorItemViewModel CreateInstance(User entry) => new(entry);
public static IllustratorItemViewModel CreateInstance(User entry, int _) => new(entry);
[ObservableProperty]
private bool _isFollowed;
@ -36,7 +36,7 @@ public sealed partial class IllustratorItemViewModel : EntryViewModel<User>, IFa
IsFollowed = Entry.UserInfo.IsFollowed;
InitializeCommands();
FollowCommand.GetFollowCommand(IsFollowed);
FollowCommand.RefreshFollowCommand(IsFollowed);
}
public string Username => Entry.UserInfo.Name;

View File

@ -45,49 +45,54 @@ public sealed partial class NovelItem
var old = _isPointerOver;
_isPointerOver = value;
var currentView = ConnectedAnimationService.GetForCurrentView();
if (IsPointerOver > 0 && old <= 0)
switch (IsPointerOver)
{
var anim1 = currentView.PrepareToAnimate("ForwardConnectedAnimation1", this);
var anim2 = currentView.PrepareToAnimate("ForwardConnectedAnimation2", Image);
var anim3 = currentView.PrepareToAnimate("ForwardConnectedAnimation3", HeartButton);
var anim4 = currentView.PrepareToAnimate("ForwardConnectedAnimation4", TitleTextBlock);
var anim5 = currentView.PrepareToAnimate("ForwardConnectedAnimation5", AuthorTextBlock);
var anim6 = currentView.PrepareToAnimate("ForwardConnectedAnimation6", TagsList);
anim1.Configuration = anim2.Configuration = anim3.Configuration =
anim4.Configuration = anim5.Configuration = anim6.Configuration =
new BasicConnectedAnimationConfiguration();
_ = anim1.TryStart(NovelItemPopup);
_ = anim2.TryStart(PopupImage);
_ = anim3.TryStart(PopupHeartButton);
_ = anim4.TryStart(PopupTitleTextBlock);
_ = anim5.TryStart(PopupAuthorButton);
_ = anim6.TryStart(PopupTagsList);
NovelItemPopup.Child.To<FrameworkElement>().Width = ActualWidth + 10;
NovelItemPopup.IsOpen = true;
}
else if (IsPointerOver <= 0 && old > 0)
{
var anim1 = currentView.PrepareToAnimate("BackwardConnectedAnimation1", NovelItemPopup);
var anim2 = currentView.PrepareToAnimate("BackwardConnectedAnimation2", PopupImage);
var anim3 = currentView.PrepareToAnimate("BackwardConnectedAnimation3", PopupHeartButton);
var anim4 = currentView.PrepareToAnimate("BackwardConnectedAnimation4", PopupTitleTextBlock);
var anim5 = currentView.PrepareToAnimate("BackwardConnectedAnimation5", PopupAuthorButton);
var anim6 = currentView.PrepareToAnimate("BackwardConnectedAnimation6", PopupTagsList);
anim1.Configuration = anim2.Configuration = anim3.Configuration =
anim4.Configuration = anim5.Configuration = anim6.Configuration =
new BasicConnectedAnimationConfiguration();
anim1.Completed += (_, _) =>
case > 0 when old <= 0:
{
NovelItemPopup.IsOpen = false;
NovelItemPopup.Visibility = Visibility.Visible;
};
_ = anim1.TryStart(this);
_ = anim2.TryStart(Image);
_ = anim3.TryStart(HeartButton);
_ = anim4.TryStart(TitleTextBlock);
_ = anim5.TryStart(AuthorTextBlock);
_ = anim6.TryStart(TagsList);
_ = Task.Delay(100).ContinueWith(_ => NovelItemPopup.Visibility = Visibility.Collapsed, TaskScheduler.FromCurrentSynchronizationContext());
var anim1 = currentView.PrepareToAnimate("ForwardConnectedAnimation1", this);
var anim2 = currentView.PrepareToAnimate("ForwardConnectedAnimation2", Image);
var anim3 = currentView.PrepareToAnimate("ForwardConnectedAnimation3", HeartButton);
var anim4 = currentView.PrepareToAnimate("ForwardConnectedAnimation4", TitleTextBlock);
var anim5 = currentView.PrepareToAnimate("ForwardConnectedAnimation5", AuthorTextBlock);
var anim6 = currentView.PrepareToAnimate("ForwardConnectedAnimation6", TagsList);
anim1.Configuration = anim2.Configuration = anim3.Configuration =
anim4.Configuration = anim5.Configuration = anim6.Configuration =
new BasicConnectedAnimationConfiguration();
_ = anim1.TryStart(NovelItemPopup);
_ = anim2.TryStart(PopupImage);
_ = anim3.TryStart(PopupHeartButton);
_ = anim4.TryStart(PopupTitleTextBlock);
_ = anim5.TryStart(PopupAuthorTextBlock);
_ = anim6.TryStart(PopupTagsList);
NovelItemPopup.Child.To<FrameworkElement>().Width = ActualWidth + 10;
NovelItemPopup.IsOpen = true;
break;
}
case <= 0 when old > 0:
{
var anim1 = currentView.PrepareToAnimate("BackwardConnectedAnimation1", NovelItemPopup);
var anim2 = currentView.PrepareToAnimate("BackwardConnectedAnimation2", PopupImage);
var anim3 = currentView.PrepareToAnimate("BackwardConnectedAnimation3", PopupHeartButton);
var anim4 = currentView.PrepareToAnimate("BackwardConnectedAnimation4", PopupTitleTextBlock);
var anim5 = currentView.PrepareToAnimate("BackwardConnectedAnimation5", PopupAuthorTextBlock);
var anim6 = currentView.PrepareToAnimate("BackwardConnectedAnimation6", PopupTagsList);
anim1.Configuration = anim2.Configuration = anim3.Configuration =
anim4.Configuration = anim5.Configuration = anim6.Configuration =
new BasicConnectedAnimationConfiguration();
anim1.Completed += (_, _) =>
{
NovelItemPopup.IsOpen = false;
NovelItemPopup.Visibility = Visibility.Visible;
};
_ = anim1.TryStart(this);
_ = anim2.TryStart(Image);
_ = anim3.TryStart(HeartButton);
_ = anim4.TryStart(TitleTextBlock);
_ = anim5.TryStart(AuthorTextBlock);
_ = anim6.TryStart(TagsList);
_ = Task.Delay(100).ContinueWith(_ => NovelItemPopup.Visibility = Visibility.Collapsed, TaskScheduler.FromCurrentSynchronizationContext());
break;
}
}
}
}

View File

@ -40,7 +40,7 @@ public partial class NovelItemViewModel
protected override void SaveCommandOnExecuteRequested(XamlUICommand sender, ExecuteRequestedEventArgs args)
{
var hWnd = null as ulong?;
var documentViewerViewModel = null as DocumentViewerViewModel;
DocumentViewerViewModel? documentViewerViewModel = null;
switch (args.Parameter)
{
case (ulong h, DocumentViewerViewModel vm):
@ -62,7 +62,7 @@ public partial class NovelItemViewModel
protected override async void SaveAsCommandOnExecuteRequested(XamlUICommand sender, ExecuteRequestedEventArgs args)
{
ulong hWnd;
var documentViewerViewModel = null as DocumentViewerViewModel;
DocumentViewerViewModel? documentViewerViewModel = null;
switch (args.Parameter)
{
case (ulong h, DocumentViewerViewModel vm):

View File

@ -27,7 +27,7 @@ namespace Pixeval.Controls;
public partial class NovelItemViewModel(Novel novel) : WorkEntryViewModel<Novel>(novel), IFactory<Novel, NovelItemViewModel>
{
public static NovelItemViewModel CreateInstance(Novel entry) => new(entry);
public static NovelItemViewModel CreateInstance(Novel entry, int _) => new(entry);
public int TextLength => Entry.TextLength;

View File

@ -167,7 +167,7 @@ public sealed partial class DownloadMacroSettingsExpander
ITransducer when isNot => MacroParserResources.NegationNotAllowedFormatted.Format(name),
ITransducer when optionalParams is not null => MacroParserResources.NonParameterizedMacroBearingParameterFormatted.Format(name),
MangaIndexMacro m when !(context.TryGetValue(IsMangaMacro.NameConst, out var v) && v) => MacroParserResources.MacroShouldBeContainedFormatted.Format(m.Name, IsMangaMacro.NameConst),
ILastSegment l => (null as string).LetChain(_ => lastSegmentContexts.Add((l.Name, context))),
ILastSegment l => (null as string).Apply(_ => lastSegmentContexts.Add((l.Name, context))),
ITransducer => null,
// IPredicate
IPredicate when optionalParams is null => MacroParserResources.ParameterizedMacroMissingParameterFormatted.Format(name),

View File

@ -26,7 +26,7 @@ namespace Pixeval.Controls;
public partial class SpotlightItemViewModel : ThumbnailEntryViewModel<Spotlight>, IFactory<Spotlight, SpotlightItemViewModel>
{
public static SpotlightItemViewModel CreateInstance(Spotlight entry) => new(entry);
public static SpotlightItemViewModel CreateInstance(Spotlight entry, int _) => new(entry);
public SpotlightItemViewModel(Spotlight spotlight) : base(spotlight) => InitializeCommandsBase();

View File

@ -23,14 +23,18 @@ using Windows.System;
using Microsoft.UI.Xaml.Input;
using Pixeval.Util.UI;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using FluentIcons.Common;
using Pixeval.CoreApi.Model;
using Pixeval.Utilities;
namespace Pixeval.Controls;
public partial class WorkEntryViewModel<T>
{
/// <summary>
/// Parameter: <see cref="ValueTuple{T1, T2, T3}"/>
/// <list type="bullet">
@ -99,7 +103,7 @@ public partial class WorkEntryViewModel<T>
AddToBookmarkCommand.ExecuteRequested += AddToBookmarkCommandOnExecuteRequested;
BookmarkCommand.GetBookmarkCommand(IsBookmarked);
BookmarkCommand.RefreshBookmarkCommand(IsBookmarked);
BookmarkCommand.ExecuteRequested += BookmarkCommandOnExecuteRequested;
SaveCommand.ExecuteRequested += SaveCommandOnExecuteRequested;
@ -109,23 +113,24 @@ public partial class WorkEntryViewModel<T>
CopyCommand.ExecuteRequested += CopyCommandOnExecuteRequested;
}
private async void BookmarkCommandOnExecuteRequested(XamlUICommand sender, ExecuteRequestedEventArgs args)
private void BookmarkCommandOnExecuteRequested(XamlUICommand sender, ExecuteRequestedEventArgs args)
{
IsBookmarked = await SetBookmarkAsync(Id, !IsBookmarked);
BookmarkCommand.GetBookmarkCommand(IsBookmarked);
IsBookmarked = !IsBookmarked; // pre-update
BookmarkCommand.RefreshBookmarkCommand(IsBookmarked); // pre-update
_ = _bookmarkDebounce.ExecuteAsync(IsBookmarked ? new BookmarkDebounceTask(this, false, null) : new RemoveBookmarkDebounceTask(this, false, null));
if (App.AppViewModel.AppSettings.DownloadWhenBookmarked && IsBookmarked)
SaveCommand.Execute(args.Parameter);
}
private async void AddToBookmarkCommandOnExecuteRequested(XamlUICommand sender, ExecuteRequestedEventArgs args)
private void AddToBookmarkCommandOnExecuteRequested(XamlUICommand sender, ExecuteRequestedEventArgs args)
{
if (args.Parameter is not (IEnumerable<string> userTags, bool isPrivate, var parameter))
return;
var success = await SetBookmarkAsync(Id, true, isPrivate, userTags);
if (!success)
return;
IsBookmarked = true;
BookmarkCommand.GetBookmarkCommand(IsBookmarked);
BookmarkCommand.RefreshBookmarkCommand(IsBookmarked);
_ = _bookmarkDebounce.ExecuteAsync(IsBookmarked ? new BookmarkDebounceTask(this, isPrivate, userTags) : new RemoveBookmarkDebounceTask(this, isPrivate, userTags));
if (App.AppViewModel.AppSettings.DownloadWhenBookmarked)
SaveCommand.Execute(parameter);
}

View File

@ -0,0 +1,75 @@
#region Copyright (c) Pixeval/Pixeval
// GPL v3 License
//
// Pixeval/Pixeval
// Copyright (c) 2024 Pixeval/WorkEntryViewModel.Debounce.cs
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
#endregion
using Pixeval.Utilities;
using System.Threading.Tasks;
using System;
using System.Collections.Generic;
namespace Pixeval.Controls;
public partial class WorkEntryViewModel<T>
{
private readonly Debounce<BookmarkDebounceTag, bool> _bookmarkDebounce = new();
public enum BookmarkDebounceGroupPhase
{
Bookmark, RemoveBookmark
}
private record struct BookmarkDebounceTag(long IllustId, BookmarkDebounceGroupPhase Phase);
private class BookmarkDebounceTask(WorkEntryViewModel<T> vm, bool isPrivate, IEnumerable<string>? userTags) : IDebouncedTask<BookmarkDebounceTag, bool>
{
public BookmarkDebounceTag Id => new(vm.Id, BookmarkDebounceGroupPhase.Bookmark);
public BookmarkDebounceTag? Dependency => null;
public Task<bool> ExecuteAsync()
{
return vm.SetBookmarkAsync(vm.Id, true, isPrivate, userTags);
}
public bool IsFinalizer => false;
public bool IsHead => true;
}
private class RemoveBookmarkDebounceTask(WorkEntryViewModel<T> vm, bool isPrivate, IEnumerable<string>? userTags) : IDebouncedTask<BookmarkDebounceTag, bool>
{
public BookmarkDebounceTag Id => new(vm.Id, BookmarkDebounceGroupPhase.RemoveBookmark);
public BookmarkDebounceTag? Dependency => new(vm.Id, BookmarkDebounceGroupPhase.Bookmark);
public Task<bool> ExecuteAsync()
{
return vm.SetBookmarkAsync(vm.Id, false, isPrivate, userTags);
}
public bool IsFinalizer => true;
public bool IsHead => false;
}
protected override void DisposeOverride()
{
_bookmarkDebounce.Dispose();
}
}

View File

@ -149,7 +149,7 @@ public partial class DownloadManager : IDisposable
}
/// <summary>
/// Tries to redownload a task only if its already queued and not running
/// Attempts to re-download a task only if its already queued and not running
/// </summary>
/// <remarks>
/// Execute the task only if it's already queued

View File

@ -32,7 +32,7 @@ using WinUI3Utilities;
namespace Pixeval.Pages.Capability;
public partial class BookmarkPageViewModel(long userId) : ObservableObject, IDisposable
public class BookmarksPageViewModel(long userId) : ObservableObject, IDisposable
{
public long UserId { get; } = userId;

View File

@ -4,7 +4,6 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Pixeval.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Pixeval.Pages.Capability"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:model="using:Pixeval.CoreApi.Model"
xmlns:pixeval="using:Pixeval"

View File

@ -31,7 +31,7 @@ namespace Pixeval.Pages.Capability;
public sealed partial class BookmarksPage : IScrollViewHost
{
private BookmarkPageViewModel _viewModel = null!;
private BookmarksPageViewModel _viewModel = null!;
public BookmarksPage() => InitializeComponent();
@ -39,7 +39,7 @@ public sealed partial class BookmarksPage : IScrollViewHost
{
if (e.Parameter is not long uid)
uid = App.AppViewModel.PixivUid;
_viewModel = new BookmarkPageViewModel(uid);
_viewModel = new BookmarksPageViewModel(uid);
_viewModel.TagBookmarksIncrementallyLoaded += ViewModelOnTagBookmarksIncrementallyLoaded;
}

View File

@ -0,0 +1,19 @@
<controls:EnhancedPage
x:Class="Pixeval.Pages.Capability.Feeds.CondensedFeedPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Pixeval.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
mc:Ignorable="d">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<controls:WorkView
x:Name="WorkView"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
x:FieldModifier="public" />
</Grid>
</controls:EnhancedPage>

View File

@ -0,0 +1,29 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.UI.Xaml.Navigation;
using Pixeval.CoreApi.Model;
namespace Pixeval.Pages.Capability.Feeds;
public sealed partial class CondensedFeedPage
{
public CondensedFeedPage()
{
InitializeComponent();
WorkView.LayoutType = App.AppViewModel.AppSettings.ItemsViewLayoutType;
WorkView.ThumbnailDirection = App.AppViewModel.AppSettings.ThumbnailDirection;
}
public override void OnPageActivated(NavigationEventArgs e)
{
switch (e.Parameter)
{
case IEnumerable<Illustration> works:
WorkView.ResetEngine(App.AppViewModel.MakoClient.Computed(works.ToAsyncEnumerable()));
break;
case IEnumerable<Novel> works2:
WorkView.ResetEngine(App.AppViewModel.MakoClient.Computed(works2.ToAsyncEnumerable()));
break;
}
}
}

View File

@ -0,0 +1,93 @@
#region Copyright (c) Pixeval/Pixeval
// GPL v3 License
//
// Pixeval/Pixeval
// Copyright (c) 2024 Pixeval/FeedItemCondensedViewModel.cs
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
#endregion
using Microsoft.UI.Xaml.Media;
using Pixeval.CoreApi.Model;
using Pixeval.Util.IO;
using System.Collections.Generic;
using System.Threading.Tasks;
using System;
using System.Linq;
using Microsoft.UI;
using Pixeval.AppManagement;
namespace Pixeval.Pages.Capability.Feeds;
public class FeedItemCondensedViewModel(List<Feed?> entries) : AbstractFeedItemViewModel(new IFeedEntry.CondensedFeedEntry(entries))
{
public override void Dispose()
{
GC.SuppressFinalize(this);
}
public override Uri AppUri => throw new NotSupportedException("AppUri is not supported for condensed feeds");
public override Uri WebUri => throw new NotSupportedException("WebUri is not supported for condensed feeds");
public override Uri PixEzUri => throw new NotSupportedException("PixEzUri is not supported for condensed feeds");
private ImageSource? _userAvatar;
// It's impossible to use [ObservableProperty] here, for that generated properties lack the `override` modifier
// same for the ItemBackground property
public override ImageSource UserAvatar
{
get => _userAvatar!;
protected set => SetProperty(ref _userAvatar, value);
}
private SolidColorBrush _itemBackground = new(Colors.Transparent);
public override SolidColorBrush ItemBackground
{
get => _itemBackground;
set => SetProperty(ref _itemBackground, value);
}
public override string PostUsername => entries[0]?.PostUsername ?? string.Empty;
public override string PostDateFormatted => $"{FormatDate(entries[^1]?.PostDate ?? default)} ~ {FormatDate(entries[0]?.PostDate ?? default)}";
private static string FormatDate(DateTimeOffset postDate)
{
return (DateTime.Now - postDate) < TimeSpan.FromDays(1)
? postDate.ToString("hh:mm tt")
: postDate.ToString("M");
}
public override async Task LoadAsync()
{
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
#pragma warning disable MVVMTK0034
if (_userAvatar is not null)
#pragma warning restore MVVMTK0034
return;
if (entries[0]?.PostUserThumbnail is { Length: > 0 } url)
{
var image = (await App.AppViewModel.MakoClient.DownloadBitmapImageAsync(url, 35)).UnwrapOrElse(await AppInfo.ImageNotAvailable.ValueAsync)!;
UserAvatar = image;
}
else
{
UserAvatar = await AppInfo.ImageNotAvailable.ValueAsync;
}
}
}

View File

@ -0,0 +1,124 @@
#region Copyright (c) Pixeval/Pixeval
// GPL v3 License
//
// Pixeval/Pixeval
// Copyright (c) 2024 Pixeval/FeedItemSparseViewModel.cs
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
#endregion
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml.Media;
using Pixeval.Controls.Timeline;
using Pixeval.Controls;
using Pixeval.CoreApi.Model;
using Pixeval.Util.IO;
using Pixeval.Util;
using System.Threading.Tasks;
using System;
using Microsoft.UI;
using Pixeval.AppManagement;
namespace Pixeval.Pages.Capability.Feeds;
public partial class FeedItemSparseViewModel(Feed entry) : AbstractFeedItemViewModel(new IFeedEntry.SparseFeedEntry(entry)), IViewModelFactory<Feed, FeedItemSparseViewModel>
{
[ObservableProperty]
private TimelineAxisPlacement _placement;
public override string PostUsername => entry.PostUsername ?? string.Empty;
// If the post date is within one day, show the precise moment, otherwise shows the date
// we make an optimistic assumption that user rarely view feeds over one year ago, so
// we don't show the year here.
public override string PostDateFormatted =>
(DateTime.Now - entry.PostDate) < TimeSpan.FromDays(1)
? entry.PostDate.ToString("hh:mm tt")
: entry.PostDate.ToString("M");
private ImageSource? _userAvatar;
// It's impossible to use [ObservableProperty] here, for that generated properties lack the `override` modifier
// same for the ItemBackground property
public override ImageSource UserAvatar
{
get => _userAvatar!;
protected set => SetProperty(ref _userAvatar, value);
}
private SolidColorBrush _itemBackground = new(Colors.Transparent);
public override SolidColorBrush ItemBackground
{
get => _itemBackground;
set => SetProperty(ref _itemBackground, value);
}
public static FeedItemSparseViewModel CreateInstance(Feed entry, int index)
{
return new FeedItemSparseViewModel(entry);
}
public override async Task LoadAsync()
{
Placement = TimelineAxisPlacement.Left; // index % 2 == 0 ? TimelineAxisPlacement.Left : TimelineAxisPlacement.Right;
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
#pragma warning disable MVVMTK0034
if (_userAvatar is not null)
#pragma warning restore MVVMTK0034
return;
if (entry.PostUserThumbnail is { } url)
{
var image = (await App.AppViewModel.MakoClient.DownloadBitmapImageAsync(url, 35)).UnwrapOrElse(await AppInfo.ImageNotAvailable.ValueAsync)!;
UserAvatar = image;
}
else
{
UserAvatar = await AppInfo.ImageNotAvailable.ValueAsync;
}
}
public override void Dispose()
{
GC.SuppressFinalize(this);
}
public override Uri AppUri => entry.Type switch
{
FeedType.AddBookmark or FeedType.PostIllust => MakoHelper.GenerateIllustrationAppUri(entry.Id),
FeedType.AddFavorite => MakoHelper.GenerateUserAppUri(entry.Id),
FeedType.AddNovelBookmark => MakoHelper.GenerateNovelAppUri(entry.Id),
_ => throw new ArgumentOutOfRangeException()
};
public override Uri WebUri => entry.Type switch
{
FeedType.AddBookmark or FeedType.PostIllust => MakoHelper.GenerateIllustrationWebUri(entry.Id),
FeedType.AddFavorite => MakoHelper.GenerateUserWebUri(entry.Id),
FeedType.AddNovelBookmark => MakoHelper.GenerateNovelWebUri(entry.Id),
_ => throw new ArgumentOutOfRangeException()
};
public override Uri PixEzUri => entry.Type switch
{
FeedType.AddBookmark or FeedType.PostIllust => MakoHelper.GenerateIllustrationPixEzUri(entry.Id),
FeedType.AddFavorite => MakoHelper.GenerateUserPixEzUri(entry.Id),
FeedType.AddNovelBookmark => MakoHelper.GenerateNovelPixEzUri(entry.Id),
_ => throw new ArgumentOutOfRangeException()
};
}

View File

@ -0,0 +1,109 @@
#region Copyright (c) Pixeval/Pixeval
// GPL v3 License
//
// Pixeval/Pixeval
// Copyright (c) 2024 Pixeval/FeedItemViewModel.cs
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Windows.UI.ViewManagement;
using Microsoft.UI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using Pixeval.Controls;
using Pixeval.CoreApi.Model;
using Pixeval.Util.UI;
#pragma warning disable CS9107 // Parameter is captured into the state of the enclosing type and its value is also passed to the base constructor. The value might be captured by the base class as well.
namespace Pixeval.Pages.Capability.Feeds;
static file class FeedItemColors
{
public static readonly SolidColorBrush AddBookmark = new(UiHelper.ParseHexColor("#FF5449"));
public static readonly SolidColorBrush AddFavorite = new(UiHelper.ParseHexColor("#85976E"));
public static readonly SolidColorBrush PostIllust = new(UiHelper.ParseHexColor("#769CDF"));
public static readonly SolidColorBrush AddNovelBookmark = new(UiHelper.ParseHexColor("#9B9168"));
}
public interface IFeedEntry : IIdEntry
{
public record SparseFeedEntry(Feed Entry) : IFeedEntry
{
public long Id => Entry.Id;
}
public record CondensedFeedEntry(List<Feed?> Entries) : IFeedEntry
{
public long Id => Entries.First()?.Id ?? 0;
}
}
public abstract class AbstractFeedItemViewModel(IFeedEntry entry) : EntryViewModel<IFeedEntry>(entry), IViewModelFactory<IFeedEntry, AbstractFeedItemViewModel>
{
public SolidColorBrush FeedBrush => GetMostSignificantEntry()!.Type switch
{
FeedType.AddBookmark => FeedItemColors.AddBookmark,
FeedType.AddFavorite => FeedItemColors.AddFavorite,
FeedType.PostIllust => FeedItemColors.PostIllust,
FeedType.AddNovelBookmark => FeedItemColors.AddNovelBookmark,
_ => throw new ArgumentOutOfRangeException()
};
public abstract ImageSource UserAvatar { get; protected set; }
public abstract SolidColorBrush ItemBackground { get; set; }
public abstract string PostUsername { get; }
public abstract string PostDateFormatted { get; }
public bool IsCondensed => Entry is IFeedEntry.CondensedFeedEntry;
public abstract Task LoadAsync();
public void Select(bool value)
{
var selectedBackground = App.AppViewModel.AppSettings.ActualTheme is ElementTheme.Dark
? new UISettings().GetColorValue(UIColorType.AccentDark3)
: new UISettings().GetColorValue(UIColorType.AccentLight3);
ItemBackground = value ? new SolidColorBrush(selectedBackground) : new SolidColorBrush(Colors.Transparent);
}
public static AbstractFeedItemViewModel CreateInstance(IFeedEntry entry, int index)
{
return entry switch
{
IFeedEntry.SparseFeedEntry(var feed) => new FeedItemSparseViewModel(feed),
IFeedEntry.CondensedFeedEntry condensed => new FeedItemCondensedViewModel(condensed.Entries),
_ => throw new ArgumentOutOfRangeException()
};
}
// Reify the entry from IFeedEntry.
public Feed? GetMostSignificantEntry()
{
return Entry switch
{
IFeedEntry.SparseFeedEntry(var feed) => feed,
IFeedEntry.CondensedFeedEntry condensed => condensed.Entries.First(),
_ => throw new ArgumentOutOfRangeException()
};
}
}

View File

@ -0,0 +1,168 @@
<Page
x:Class="Pixeval.Pages.Capability.Feeds.FeedPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Pixeval.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:feeds="using:Pixeval.Pages.Capability.Feeds"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:media="using:CommunityToolkit.WinUI.Media"
xmlns:ui="using:CommunityToolkit.WinUI"
Background="Transparent"
Loaded="FeedPage_OnLoaded"
mc:Ignorable="d">
<Page.Resources>
<media:AttachedCardShadow x:Key="ContentPanelShadow" Offset="4" />
<!-- See https://github.com/microsoft/microsoft-ui-xaml/blob/35c590bb28841eb9d466624bb828c78b939d4312/src/controls/dev/ItemContainer/ItemContainer_themeresources.xaml#L59 -->
<SolidColorBrush x:Key="ItemContainerSelectedInnerBorderBrush" Color="Transparent" />
<SolidColorBrush x:Key="ItemContainerBorderBrush" Color="Transparent" />
<SolidColorBrush x:Key="ItemContainerSelectedBackground" Color="Transparent" />
<SolidColorBrush x:Key="ItemContainerSelectedPointerOverBackground" Color="Transparent" />
<SolidColorBrush x:Key="ItemContainerSelectedPressedBackground" Color="Transparent" />
<SolidColorBrush x:Key="ItemContainerSelectionVisualBackground" Color="Transparent" />
<SolidColorBrush x:Key="ItemContainerSelectionVisualPointerOverBackground" Color="Transparent" />
<SolidColorBrush x:Key="ItemContainerSelectionVisualPressedBackground" Color="Transparent" />
<media:AttachedCardShadow x:Key="TimelineBlockShadow" Offset="4" />
</Page.Resources>
<SplitView
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
DisplayMode="Inline"
IsPaneOpen="True"
OpenPaneLength="350"
PaneBackground="Transparent"
PanePlacement="Left">
<SplitView.Pane>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock
x:Uid="/FeedPage/FeedListTextBlock"
Grid.Row="0"
Margin="40,30,20,10"
Style="{StaticResource TitleTextBlockStyle}" />
<Grid Grid.Row="1">
<controls:EntryView
Padding="0,10,0,0"
VerticalAlignment="Stretch"
Background="Transparent"
HasNoItem="{x:Bind _viewModel.HasNoItem, Mode=OneWay}"
IsLoadingMore="{x:Bind ItemsView.IsLoadingMore, Mode=OneWay}">
<controls:EntryView.Content>
<controls:AdvancedItemsView
x:Name="ItemsView"
HorizontalAlignment="Stretch"
IsItemInvokedEnabled="False"
ItemsSource="{x:Bind _viewModel.DataProvider.View, Mode=OneWay}"
SelectionChanged="ItemsView_OnSelectionChanged"
SelectionMode="Single">
<controls:AdvancedItemsView.ItemTemplate>
<DataTemplate x:DataType="feeds:AbstractFeedItemViewModel">
<ItemContainer x:Name="FeedItemContainer" Width="320">
<Grid Width="320">
<Grid
Width="320"
Height="80"
Padding="10,0"
ui:Effects.Shadow="{StaticResource ContentPanelShadow}"
Background="{x:Bind ItemBackground, Mode=OneWay}"
CornerRadius="4"
DataContext="{x:Bind Mode=OneWay}"
EffectiveViewportChanged="TimelineBlock_OnEffectiveViewportChanged"
Loaded="TimelineUnit_OnLoaded">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="60" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid
Grid.Column="0"
Width="35"
Height="35"
HorizontalAlignment="Center"
VerticalAlignment="Center"
CornerRadius="50">
<Grid.Background>
<ImageBrush ImageSource="{x:Bind UserAvatar, Mode=OneWay}" Stretch="UniformToFill" />
</Grid.Background>
</Grid>
<Grid
Grid.Column="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="5" />
<RowDefinition Height="2*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<TextBlock
MaxWidth="155"
HorizontalAlignment="Left"
VerticalAlignment="Bottom"
FontSize="13"
FontWeight="Bold"
Text="{x:Bind PostUsername}"
TextTrimming="CharacterEllipsis"
TextWrapping="WrapWholeWords" />
<TextBlock
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
FontSize="10"
Text="{x:Bind PostDateFormatted}" />
</Grid>
<Grid Grid.Row="2">
<TextBlock
x:Name="FeedContentTextBlock"
Margin="0,0,60,0"
FontSize="12"
LineHeight="20"
TextTrimming="CharacterEllipsis"
TextWrapping="WrapWholeWords" />
<SymbolIcon
HorizontalAlignment="Right"
Symbol="Pictures"
Visibility="{x:Bind IsCondensed}" />
</Grid>
</Grid>
</Grid>
</Grid>
</ItemContainer>
</DataTemplate>
</controls:AdvancedItemsView.ItemTemplate>
<controls:AdvancedItemsView.ItemTransitionProvider>
<LinedFlowLayoutItemCollectionTransitionProvider />
</controls:AdvancedItemsView.ItemTransitionProvider>
</controls:AdvancedItemsView>
</controls:EntryView.Content>
</controls:EntryView>
</Grid>
</Grid>
</SplitView.Pane>
<Grid
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ui:Effects.Shadow="{StaticResource ContentPanelShadow}"
CornerRadius="8,0,0,8">
<Frame
x:Name="FeedPageFrame"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" />
<Grid
Width="100"
Height="80"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="{ThemeResource PixevalAppAcrylicBrush}"
CornerRadius="4"
Visibility="{x:Bind _viewModel.IsLoading, Mode=OneWay}">
<ProgressRing
Width="30"
Height="30"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Grid>
</Grid>
</SplitView>
</Page>

View File

@ -0,0 +1,165 @@
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Windows.UI.Text;
using CommunityToolkit.WinUI;
using Microsoft.UI.Xaml.Documents;
using Microsoft.UI.Xaml.Media.Animation;
using Pixeval.Controls;
using Pixeval.Controls.Windowing;
using Pixeval.CoreApi.Model;
using Pixeval.Pages.IllustrationViewer;
using Pixeval.Pages.IllustratorViewer;
using Pixeval.Pages.NovelViewer;
using Pixeval.Utilities;
using WinUI3Utilities;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace Pixeval.Pages.Capability.Feeds
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class FeedPage
{
private AbstractFeedItemViewModel? _lastSelected;
public FeedPage()
{
InitializeComponent();
_viewModel = new FeedPageViewModel();
}
private readonly FeedPageViewModel _viewModel;
private void FeedPage_OnLoaded(object sender, RoutedEventArgs e)
{
_viewModel.DataProvider.ResetEngine(new FeedProxyFetchEngine(App.AppViewModel.MakoClient.Feeds())!);
_viewModel.DataProvider.View.CollectionChanged += (o, args) =>
{
if (args.Action is NotifyCollectionChangedAction.Add)
{
ItemsView.SelectedIndex = 0;
}
};
}
private async void TimelineUnit_OnLoaded(object sender, RoutedEventArgs e)
{
await LoadFeedItemViewModelAsync((Grid) sender);
}
private async void TimelineBlock_OnEffectiveViewportChanged(FrameworkElement sender, EffectiveViewportChangedEventArgs args)
{
if (args.BringIntoViewDistanceY <= 100)
{
await LoadFeedItemViewModelAsync((Grid) sender);
}
}
private async Task LoadFeedItemViewModelAsync(FrameworkElement root)
{
var contentTextBlock = root.FindDescendant("FeedContentTextBlock") as TextBlock;
var vm = root.GetDataContext<AbstractFeedItemViewModel>();
var isSparse = vm is FeedItemSparseViewModel;
var entry = vm.GetMostSignificantEntry()!;
var feedNameString = vm is FeedItemCondensedViewModel { Entry: IFeedEntry.CondensedFeedEntry(var entries) }
? GetVmEmphasizedRun(string.Join(", ", entries.Take(2).Select(e => e?.FeedName)))
: GetVmEmphasizedRun(entry.FeedName ?? string.Empty);
var entriesLengthIfCondensed = vm is FeedItemCondensedViewModel { Entry: IFeedEntry.CondensedFeedEntry({ Count: var count }) }
? count
: 0;
contentTextBlock ?.Inlines.Clear();
switch (entry.Type)
{
case FeedType.AddBookmark or FeedType.AddNovelBookmark:
contentTextBlock?.Inlines.Add(new Run { Text = isSparse ? FeedPageResources.SparseBookmarkPrefix : FeedPageResources.CondensedBookmarkPrefix });
contentTextBlock?.Inlines.Add(feedNameString);
contentTextBlock?.Inlines.Add(new Run { Text = isSparse ? FeedPageResources.SparseBookmarkSuffix : FeedPageResources.CondensedBookmarkFormattedSuffix.Format(entriesLengthIfCondensed) });
break;
case FeedType.PostIllust:
contentTextBlock?.Inlines.Add(new Run { Text = isSparse ? FeedPageResources.SparsePostIllustPrefix : FeedPageResources.CondensedPostIllustPrefix });
contentTextBlock?.Inlines.Add(feedNameString);
if (!isSparse)
contentTextBlock?.Inlines.Add(new Run { Text = FeedPageResources.CondensedPostIllustFormattedSuffix.Format(entriesLengthIfCondensed) });
break;
case FeedType.AddFavorite:
contentTextBlock?.Inlines.Add(new Run { Text = FeedPageResources.SparseFollowUserPrefix });
contentTextBlock?.Inlines.Add(feedNameString);
if (!isSparse)
contentTextBlock?.Inlines.Add(new Run { Text = FeedPageResources.CondensedFollowUserFormattedSuffix.Format(entriesLengthIfCondensed) });
break;
}
await vm.LoadAsync();
return;
Run GetVmEmphasizedRun(string text) => new()
{
Text = text,
FontFamily = new FontFamily("Bahnschrift"),
Foreground = vm.FeedBrush,
FontWeight = new FontWeight(700)
};
}
private async void ItemsView_OnSelectionChanged(ItemsView sender, ItemsViewSelectionChangedEventArgs args)
{
_lastSelected?.Select(false);
var vm = sender.SelectedItem as AbstractFeedItemViewModel;
vm?.Select(true);
_lastSelected = vm;
_viewModel.CancelLoad();
switch (vm)
{
case FeedItemSparseViewModel { Entry: IFeedEntry.SparseFeedEntry svmEntry }:
switch (svmEntry.Entry.Type)
{
case FeedType.AddBookmark or FeedType.PostIllust:
var illustration = await _viewModel.PerformLoadAsync(() => App.AppViewModel.MakoClient.GetIllustrationFromIdAsync(svmEntry.Id));
FeedPageFrame.NavigateTo<IllustrationViewerPage>(WindowFactory.RootWindow.HWnd, (new List<IllustrationItemViewModel> { new(illustration) }, 0), new CommonNavigationTransitionInfo());
break;
case FeedType.AddNovelBookmark:
var novel = await _viewModel.PerformLoadAsync(() => App.AppViewModel.MakoClient.GetNovelFromIdAsync(svmEntry.Id));
FeedPageFrame.NavigateTo<NovelViewerPage>(WindowFactory.RootWindow.HWnd, (new List<NovelItemViewModel> { new(novel) }, 0), new CommonNavigationTransitionInfo());
break;
case FeedType.AddFavorite:
var user = await _viewModel.PerformLoadAsync(() => App.AppViewModel.MakoClient.GetUserFromIdAsync(svmEntry.Id, App.AppViewModel.AppSettings.TargetFilter));
FeedPageFrame.NavigateTo<IllustratorViewerPage>(WindowFactory.RootWindow.HWnd, user, new CommonNavigationTransitionInfo());
break;
}
break;
case FeedItemCondensedViewModel { Entry: IFeedEntry.CondensedFeedEntry(var entries) }:
switch (vm.GetMostSignificantEntry()!.Type)
{
case FeedType.AddBookmark or FeedType.PostIllust:
IEnumerable<IWorkEntry> illustrations = await _viewModel.PerformLoadAsync(() =>
Task.WhenAll(entries.Select(entry => App.AppViewModel.MakoClient.GetIllustrationFromIdAsync(entry!.Id))));
FeedPageFrame.Navigate(typeof(CondensedFeedPage), illustrations, new CommonNavigationTransitionInfo());
break;
case FeedType.AddNovelBookmark:
IEnumerable<Novel> novels = await _viewModel.PerformLoadAsync(() =>
Task.WhenAll(entries.Select(entry => App.AppViewModel.MakoClient.GetNovelFromIdAsync(entry!.Id))));
FeedPageFrame.Navigate(typeof(CondensedFeedPage), novels, new CommonNavigationTransitionInfo());
break;
case FeedType.AddFavorite:
break;
}
break;
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More