feed (#522)
31
src/Pixeval.Controls/CardControl/CardControl.cs
Normal 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);
|
||||
}
|
||||
}
|
28
src/Pixeval.Controls/CardControl/CardControl.xaml
Normal 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>
|
@ -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" />
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
41
src/Pixeval.Controls/Timeline/TimelineAxisPlacement.cs
Normal 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)
|
||||
};
|
||||
}
|
||||
}
|
12
src/Pixeval.Controls/Timeline/TimelineControl.xaml
Normal 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>
|
17
src/Pixeval.Controls/Timeline/TimelineControl.xaml.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
186
src/Pixeval.Controls/Timeline/TimelineUnit.cs
Normal 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);
|
||||
}
|
||||
}
|
48
src/Pixeval.Controls/Timeline/TimelineUnit.xaml
Normal 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>
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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));
|
||||
|
@ -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)))
|
||||
{
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
201
src/Pixeval.Utilities/Debounce.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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")
|
||||
|
@ -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()
|
||||
|
@ -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;
|
||||
|
||||
|
BIN
src/Pixeval/Assets/Images/Icons/about-128x128.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
src/Pixeval/Assets/Images/Icons/about-64x64.png
Normal file
After Width: | Height: | Size: 7.5 KiB |
BIN
src/Pixeval/Assets/Images/Icons/about.png
Normal file
After Width: | Height: | Size: 502 KiB |
BIN
src/Pixeval/Assets/Images/Icons/bookmarks-128x128.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
src/Pixeval/Assets/Images/Icons/bookmarks-64x64.png
Normal file
After Width: | Height: | Size: 5.6 KiB |
BIN
src/Pixeval/Assets/Images/Icons/bookmarks.png
Normal file
After Width: | Height: | Size: 300 KiB |
BIN
src/Pixeval/Assets/Images/Icons/download-list-128x128.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
src/Pixeval/Assets/Images/Icons/download-list-64x64.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
src/Pixeval/Assets/Images/Icons/download-list.png
Normal file
After Width: | Height: | Size: 490 KiB |
BIN
src/Pixeval/Assets/Images/Icons/feed-128x128.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
src/Pixeval/Assets/Images/Icons/feed-64x64.png
Normal file
After Width: | Height: | Size: 7.4 KiB |
BIN
src/Pixeval/Assets/Images/Icons/feed.png
Normal file
After Width: | Height: | Size: 448 KiB |
BIN
src/Pixeval/Assets/Images/Icons/followings-128x128.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src/Pixeval/Assets/Images/Icons/followings-64x64.png
Normal file
After Width: | Height: | Size: 5.6 KiB |
BIN
src/Pixeval/Assets/Images/Icons/followings.png
Normal file
After Width: | Height: | Size: 408 KiB |
BIN
src/Pixeval/Assets/Images/Icons/help-128x128.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
src/Pixeval/Assets/Images/Icons/help-64x64.png
Normal file
After Width: | Height: | Size: 6.1 KiB |
BIN
src/Pixeval/Assets/Images/Icons/help.png
Normal file
After Width: | Height: | Size: 448 KiB |
BIN
src/Pixeval/Assets/Images/Icons/history-128x128.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
src/Pixeval/Assets/Images/Icons/history-64x64.png
Normal file
After Width: | Height: | Size: 7.0 KiB |
BIN
src/Pixeval/Assets/Images/Icons/history.png
Normal file
After Width: | Height: | Size: 507 KiB |
BIN
src/Pixeval/Assets/Images/Icons/new-works-128x128.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
src/Pixeval/Assets/Images/Icons/new-works-64x64.png
Normal file
After Width: | Height: | Size: 7.3 KiB |
BIN
src/Pixeval/Assets/Images/Icons/new-works.png
Normal file
After Width: | Height: | Size: 544 KiB |
BIN
src/Pixeval/Assets/Images/Icons/ranking-128x128.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src/Pixeval/Assets/Images/Icons/ranking-64x64.png
Normal file
After Width: | Height: | Size: 6.5 KiB |
BIN
src/Pixeval/Assets/Images/Icons/ranking.png
Normal file
After Width: | Height: | Size: 551 KiB |
BIN
src/Pixeval/Assets/Images/Icons/recent-posts-128x128.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
src/Pixeval/Assets/Images/Icons/recent-posts-64x64.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
BIN
src/Pixeval/Assets/Images/Icons/recent-posts.png
Normal file
After Width: | Height: | Size: 477 KiB |
BIN
src/Pixeval/Assets/Images/Icons/recommend-user-128x128.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
src/Pixeval/Assets/Images/Icons/recommend-user-64x64.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
BIN
src/Pixeval/Assets/Images/Icons/recommend-user.png
Normal file
After Width: | Height: | Size: 254 KiB |
BIN
src/Pixeval/Assets/Images/Icons/recommendations-128x128.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src/Pixeval/Assets/Images/Icons/recommendations-64x64.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
src/Pixeval/Assets/Images/Icons/recommendations.png
Normal file
After Width: | Height: | Size: 348 KiB |
BIN
src/Pixeval/Assets/Images/Icons/settings-128x128.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
src/Pixeval/Assets/Images/Icons/settings-64x64.png
Normal file
After Width: | Height: | Size: 6.5 KiB |
BIN
src/Pixeval/Assets/Images/Icons/settings.png
Normal file
After Width: | Height: | Size: 151 KiB |
BIN
src/Pixeval/Assets/Images/Icons/spotlight-128x128.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src/Pixeval/Assets/Images/Icons/spotlight-64x64.png
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
src/Pixeval/Assets/Images/Icons/spotlight.png
Normal file
After Width: | Height: | Size: 423 KiB |
BIN
src/Pixeval/Assets/Images/Icons/tag-128x128.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
src/Pixeval/Assets/Images/Icons/tag-64x64.png
Normal file
After Width: | Height: | Size: 7.1 KiB |
BIN
src/Pixeval/Assets/Images/Icons/tag.png
Normal file
After Width: | Height: | Size: 365 KiB |
@ -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
|
||||
|
@ -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")]
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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}" />
|
||||
|
@ -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));
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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):
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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),
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
75
src/Pixeval/Controls/Work/WorkEntryViewModel.Debounce.cs
Normal 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();
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
19
src/Pixeval/Pages/Capability/Feeds/CondensedFeedPage.xaml
Normal 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>
|
29
src/Pixeval/Pages/Capability/Feeds/CondensedFeedPage.xaml.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
124
src/Pixeval/Pages/Capability/Feeds/FeedItemViewModel.Sparse.cs
Normal 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()
|
||||
};
|
||||
}
|
109
src/Pixeval/Pages/Capability/Feeds/FeedItemViewModel.cs
Normal 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()
|
||||
};
|
||||
}
|
||||
}
|
168
src/Pixeval/Pages/Capability/Feeds/FeedPage.xaml
Normal 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>
|
165
src/Pixeval/Pages/Capability/Feeds/FeedPage.xaml.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|