feat: add HextechDocs posts carousel

This commit is contained in:
estrogen elf
2025-06-18 01:40:07 -05:00
parent f9285a2bef
commit f7882392fd
10 changed files with 183 additions and 4 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

View File

@@ -0,0 +1,13 @@
namespace Needlework.Net.Models
{
public class HextechDocsPost
{
public required string Path { get; init; }
public required string Title { get; init; }
public required string Excerpt { get; init; }
public string Url => $"https://hextechdocs.dev{Path}";
}
}

View File

@@ -17,6 +17,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AngleSharp" Version="1.3.0" />
<PackageReference Include="Avalonia" Version="11.2.8" /> <PackageReference Include="Avalonia" Version="11.2.8" />
<PackageReference Include="Avalonia.AvaloniaEdit" Version="11.3.0" /> <PackageReference Include="Avalonia.AvaloniaEdit" Version="11.3.0" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.2.8" /> <PackageReference Include="Avalonia.Controls.DataGrid" Version="11.2.8" />

View File

@@ -87,6 +87,7 @@ class Program
// HOME // HOME
locator.Register<HomeViewModel>(() => new HomeView()); locator.Register<HomeViewModel>(() => new HomeView());
locator.Register<LibraryViewModel>(() => new LibraryView()); locator.Register<LibraryViewModel>(() => new LibraryView());
locator.Register<HextechDocsPostViewModel>(() => new HextechDocsPostView());
// SCHEMAS // SCHEMAS
locator.Register<SchemasViewModel>(() => new SchemasView()); locator.Register<SchemasViewModel>(() => new SchemasView());
locator.Register<ViewModels.Pages.Schemas.SchemaSearchDetailsViewModel>(() => new Views.Pages.Schemas.SchemaSearchDetailsView()); locator.Register<ViewModels.Pages.Schemas.SchemaSearchDetailsViewModel>(() => new Views.Pages.Schemas.SchemaSearchDetailsView());
@@ -103,6 +104,7 @@ class Program
builder.AddSingleton<DocumentService>(); builder.AddSingleton<DocumentService>();
builder.AddSingleton<NotificationService>(); builder.AddSingleton<NotificationService>();
builder.AddSingleton<SchemaPaneService>(); builder.AddSingleton<SchemaPaneService>();
builder.AddSingleton<HextechDocsPostService>();
builder.AddSingleton<IFlurlClientCache>(new FlurlClientCache() builder.AddSingleton<IFlurlClientCache>(new FlurlClientCache()
.Add("GithubClient", "https://api.github.com") .Add("GithubClient", "https://api.github.com")
.Add("GithubUserContentClient", "https://raw.githubusercontent.com") .Add("GithubUserContentClient", "https://raw.githubusercontent.com")

View File

@@ -0,0 +1,41 @@
using AngleSharp;
using FastCache;
using Needlework.Net.Models;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Needlework.Net.Services
{
public class HextechDocsPostService
{
private readonly IBrowsingContext _context = BrowsingContext.New(Configuration.Default.WithDefaultLoader());
public async Task<List<HextechDocsPost>> GetPostsAsync()
{
if (Cached<List<HextechDocsPost>>.TryGet(nameof(GetPostsAsync), out var cached))
{
return cached;
}
var document = await _context.OpenAsync("https://hextechdocs.dev/tag/lcu/");
var elements = document.QuerySelectorAll("article.post-card");
var posts = new List<HextechDocsPost>();
foreach (var element in elements)
{
var path = element.QuerySelector("a.post-card-content-link")!.GetAttribute("href")!;
var title = element.QuerySelector(".post-card-title")!.TextContent;
var excerpt = element.QuerySelector(".post-card-excerpt > p")!.TextContent;
var post = new HextechDocsPost()
{
Path = path,
Title = title,
Excerpt = excerpt,
};
posts.Add(post);
}
return cached.Save(posts, TimeSpan.FromMinutes(60));
}
}
}

View File

@@ -0,0 +1,15 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Needlework.Net.Models;
namespace Needlework.Net.ViewModels.Pages.Home
{
public partial class HextechDocsPostViewModel : ObservableObject
{
public HextechDocsPostViewModel(HextechDocsPost hextechDocsPost)
{
HextechDocsPost = hextechDocsPost;
}
public HextechDocsPost HextechDocsPost { get; }
}
}

View File

@@ -1,18 +1,44 @@
using Avalonia; using Avalonia;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using Needlework.Net.Extensions;
using Needlework.Net.Models; using Needlework.Net.Models;
using Needlework.Net.Services;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Needlework.Net.ViewModels.Pages.Home; namespace Needlework.Net.ViewModels.Pages.Home;
public partial class HomeViewModel : PageBase public partial class HomeViewModel : PageBase, IEnableLogger
{ {
public HomeViewModel() : base("Home", "fa-solid fa-house", int.MinValue) { } private readonly HextechDocsPostService _hextechDocsPostService;
private readonly IDisposable _carouselNextDisposable;
public HomeViewModel(HextechDocsPostService hextechDocsPostService) : base("Home", "fa-solid fa-house", int.MinValue)
{
_hextechDocsPostService = hextechDocsPostService;
_carouselNextDisposable = Observable.Timer(TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5))
.Select(time => Unit.Default)
.Subscribe(_ =>
{
if (SelectedHextechDocsPostsIndex == HextechDocsPosts.Count - 1)
{
SelectedHextechDocsPostsIndex = 0;
}
else
{
SelectedHextechDocsPostsIndex += 1;
}
});
}
public List<LibraryViewModel> Libraries { get; } = JsonSerializer.Deserialize<List<Library>>(AssetLoader.Open(new Uri($"avares://NeedleworkDotNet/Assets/libraries.json"))) public List<LibraryViewModel> Libraries { get; } = JsonSerializer.Deserialize<List<Library>>(AssetLoader.Open(new Uri($"avares://NeedleworkDotNet/Assets/libraries.json")))
!.Where(library => library.Tags.Contains("lcu") || library.Tags.Contains("ingame")) !.Where(library => library.Tags.Contains("lcu") || library.Tags.Contains("ingame"))
@@ -22,8 +48,27 @@ public partial class HomeViewModel : PageBase
[ObservableProperty] [ObservableProperty]
private Vector _librariesOffset = new(); private Vector _librariesOffset = new();
public override Task InitializeAsync() [ObservableProperty]
private List<HextechDocsPostViewModel> _hextechDocsPosts = [];
[ObservableProperty]
private int _selectedHextechDocsPostsIndex;
public override async Task InitializeAsync()
{ {
return Task.CompletedTask; try
{
var posts = await _hextechDocsPostService.GetPostsAsync();
var hextechDocsPosts = posts.Select(post => new HextechDocsPostViewModel(post)).ToList();
Dispatcher.UIThread.Invoke(() =>
{
HextechDocsPosts = hextechDocsPosts;
});
}
catch (Exception ex)
{
this.Log()
.Error(ex, "Failed to get posts from HextechDocs.");
}
} }
} }

View File

@@ -0,0 +1,37 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:Needlework.Net.ViewModels.Pages.Home"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Views.Pages.Home.HextechDocsPostView"
x:DataType="vm:HextechDocsPostViewModel">
<Border BorderBrush="{DynamicResource ControlStrokeColorOnAccentTertiaryBrush}"
BorderThickness="1"
CornerRadius="4"
Height="80">
<Grid>
<Border CornerRadius="4"
ClipToBounds="True">
<Image Source="/Assets/bg-event-pass.jpg"
Stretch="UniformToFill"/>
</Border>
<Border Padding="8">
<Grid RowDefinitions="*,*,*">
<TextBlock Text="{Binding HextechDocsPost.Title}"
Theme="{StaticResource BodyStrongTextBlockStyle}"
TextTrimming="CharacterEllipsis"
Grid.Row="0"/>
<TextBlock Text="{Binding HextechDocsPost.Excerpt}"
TextTrimming="CharacterEllipsis"
Grid.Row="1"/>
<HyperlinkButton Padding="0"
Content="{Binding HextechDocsPost.Url}"
NavigateUri="{Binding HextechDocsPost.Url}"
Grid.Row="2"/>
</Grid>
</Border>
</Grid>
</Border>
</UserControl>

View File

@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace Needlework.Net.Views.Pages.Home;
public partial class HextechDocsPostView : UserControl
{
public HextechDocsPostView()
{
InitializeComponent();
}
}

View File

@@ -40,6 +40,20 @@
<TextBlock>Get started with LCU or Game Client development by clicking on the endpoints tab in the left panel.</TextBlock> <TextBlock>Get started with LCU or Game Client development by clicking on the endpoints tab in the left panel.</TextBlock>
</StackPanel> </StackPanel>
</Border> </Border>
<!-- HEXTECHDOCS POSTS -->
<Carousel ItemsSource="{Binding HextechDocsPosts}"
SelectedIndex="{Binding SelectedHextechDocsPostsIndex}"
Margin="12"
IsVisible="{Binding SelectedHextechDocsPostsIndex, Converter={StaticResource EnumerableToVisibilityConverter}}">
<Carousel.PageTransition>
<PageSlide Orientation="Horizontal" Duration="0.25"/>
</Carousel.PageTransition>
<Carousel.ItemTemplate>
<DataTemplate>
<ContentControl Content="{Binding}"/>
</DataTemplate>
</Carousel.ItemTemplate>
</Carousel>
<controls:Card Margin="12"> <controls:Card Margin="12">
<TextBlock TextWrapping="Wrap">THE PROGRAM IS PROVIDED “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF MERCHANTABILITY, NONINFRINGMENT, OR OF FITNESS FOR A PARTICULAR PURPOSE. LICENSOR DOES NOT WARRANT THAT THE FUNCTIONS CONTAINED IN THE PROGRAM WILL MEET YOUR REQUIREMENTS OR THAT OPERATION WILL BE UNINTERRUPTED OR ERROR FREE. LICENSOR MAKES NO WARRANTIES RESPECTING ANY HARM THAT MAY BE CAUSED BY MALICIOUS USE OF THIS SOFTWARE. LICENSOR FURTHER EXPRESSLY DISCLAIMS ANY WARRANTY OR REPRESENTATION TO AUTHORIZED USERS OR TO ANY THIRD PARTY.</TextBlock> <TextBlock TextWrapping="Wrap">THE PROGRAM IS PROVIDED “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF MERCHANTABILITY, NONINFRINGMENT, OR OF FITNESS FOR A PARTICULAR PURPOSE. LICENSOR DOES NOT WARRANT THAT THE FUNCTIONS CONTAINED IN THE PROGRAM WILL MEET YOUR REQUIREMENTS OR THAT OPERATION WILL BE UNINTERRUPTED OR ERROR FREE. LICENSOR MAKES NO WARRANTIES RESPECTING ANY HARM THAT MAY BE CAUSED BY MALICIOUS USE OF THIS SOFTWARE. LICENSOR FURTHER EXPRESSLY DISCLAIMS ANY WARRANTY OR REPRESENTATION TO AUTHORIZED USERS OR TO ANY THIRD PARTY.</TextBlock>
</controls:Card> </controls:Card>