Show news on application load.

This commit is contained in:
2025-11-14 15:18:30 +00:00
parent 392c69350f
commit 5bffbc342e
7 changed files with 358 additions and 39 deletions

View File

@@ -1,4 +1,5 @@
using System.Net.Http;
using Marechai.App.Services;
using Microsoft.UI.Xaml;
using Uno.Extensions;
using Uno.Extensions.Configuration;
@@ -96,8 +97,9 @@ public partial class App : Application
})
.ConfigureServices((context, services) =>
{
// TODO: Register your services
//services.AddSingleton<IMyService, MyService>();
// Register application services
services.AddSingleton<NewsService>();
services.AddSingleton<NewsViewModel>();
})
.UseNavigation(RegisterRoutes));

14
Marechai.App/Enums.cs Normal file
View File

@@ -0,0 +1,14 @@
namespace Marechai.App;
public enum NewsType
{
NewComputerInDb = 1,
NewConsoleInDb = 2,
NewComputerInCollection = 3,
NewConsoleInCollection = 4,
UpdatedComputerInDb = 5,
UpdatedConsoleInDb = 6,
UpdatedComputerInCollection = 7,
UpdatedConsoleInCollection = 8,
NewMoneyDonation = 9
}

View File

@@ -1,29 +1,132 @@
<Page x:Class="Marechai.App.Presentation.MainPage"
<?xml version="1.0"
encoding="utf-8"?>
<Page x:Class="Marechai.App.Presentation.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Marechai.App.Presentation"
xmlns:uen="using:Uno.Extensions.Navigation.UI"
xmlns:utu="using:Uno.Toolkit.UI"
NavigationCacheMode="Required"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<ScrollViewer IsTabStop="True">
<Grid utu:SafeArea.Insets="VisibleBounds">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<utu:NavigationBar Content="{Binding Title}" />
<StackPanel Grid.Row="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="16">
<TextBox Text="{Binding Name, Mode=TwoWay}"
PlaceholderText="Enter your name:" />
<Button Content="Go to Second Page"
AutomationProperties.AutomationId="SecondPageButton"
Command="{Binding GoToSecond}" />
</StackPanel>
<Grid utu:SafeArea.Insets="VisibleBounds">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Header -->
<utu:NavigationBar Content="{Binding Title}" />
<!-- Refresh Container with Pull-to-Refresh -->
<RefreshContainer Grid.Row="1"
x:Name="RefreshContainer"
RefreshRequested="RefreshContainer_RefreshRequested">
<ScrollViewer>
<Grid Padding="16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- News Title Section -->
<StackPanel Grid.Row="0"
Margin="0,0,0,16">
<TextBlock Text="Latest News"
FontSize="32"
FontWeight="Bold"
Foreground="{ThemeResource SystemAccentColor}"
Margin="0,0,0,8" />
<TextBlock Text="Stay updated with the latest additions to the database"
FontSize="14"
Foreground="{ThemeResource SystemBaseMediumColor}"
TextWrapping="Wrap" />
</StackPanel>
<!-- Loading State -->
<StackPanel Grid.Row="2"
Visibility="{Binding NewsViewModel.IsLoading}"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Padding="32"
Spacing="16">
<ProgressRing IsActive="True"
IsIndeterminate="True"
Height="48"
Width="48"
Foreground="{ThemeResource SystemAccentColor}" />
<TextBlock Text="Loading latest news..."
FontSize="16"
TextAlignment="Center"
Foreground="{ThemeResource SystemBaseMediumColor}" />
</StackPanel>
<!-- Error State -->
<StackPanel Grid.Row="2"
Visibility="{Binding NewsViewModel.HasError}"
VerticalAlignment="Center"
Spacing="16"
Padding="32">
<InfoBar IsOpen="True"
Severity="Error"
Title="Unable to Load News"
Message="{Binding NewsViewModel.ErrorMessage}"
IsClosable="False" />
<Button Content="Retry"
Command="{Binding NewsViewModel.LoadNews}"
HorizontalAlignment="Center"
Style="{ThemeResource AccentButtonStyle}" />
</StackPanel>
<!-- News Feed -->
<ItemsControl Grid.Row="2"
ItemsSource="{Binding NewsViewModel.NewsList}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Margin="0,0,0,12"
CornerRadius="8"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
Padding="16">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Date -->
<TextBlock Grid.Row="0"
Text="{Binding News.Timestamp}"
FontSize="12"
Foreground="{ThemeResource SystemBaseMediumColor}"
Margin="0,0,0,12" />
<!-- News Title/Text (Localized) -->
<TextBlock Grid.Row="1"
Text="{Binding DisplayText}"
FontSize="16"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseHighColor}"
TextWrapping="Wrap"
Margin="0,0,0,12" />
<!-- Item Name Link -->
<HyperlinkButton Grid.Row="2"
Content="{Binding News.ItemName}"
FontSize="14"
Padding="0,4,0,4"
Foreground="{ThemeResource SystemAccentColor}" />
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"
Spacing="0" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Grid>
</ScrollViewer>
</RefreshContainer>
</Grid>
</ScrollViewer>
</Page>
</Page>

View File

@@ -1,11 +1,60 @@
using Microsoft.UI.Xaml.Controls;
using System;
using Windows.Foundation;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Navigation;
namespace Marechai.App.Presentation;
public sealed partial class MainPage : Page
{
private bool _initialNewsLoaded;
public MainPage()
{
this.InitializeComponent();
InitializeComponent();
DataContextChanged += MainPage_DataContextChanged;
}
}
private void MainPage_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
if(_initialNewsLoaded) return;
if(args.NewValue is MainViewModel viewModel && viewModel.NewsViewModel is not null)
{
_initialNewsLoaded = true;
_ = viewModel.NewsViewModel.LoadNews.ExecuteAsync(null);
DataContextChanged -= MainPage_DataContextChanged;
}
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if(_initialNewsLoaded) return;
if(DataContext is MainViewModel viewModel && viewModel.NewsViewModel is not null)
{
_initialNewsLoaded = true;
_ = viewModel.NewsViewModel.LoadNews.ExecuteAsync(null);
DataContextChanged -= MainPage_DataContextChanged;
}
}
private async void RefreshContainer_RefreshRequested(RefreshContainer sender, RefreshRequestedEventArgs args)
{
// Handle pull-to-refresh
using Deferral deferral = args.GetDeferral();
try
{
if(DataContext is MainViewModel viewModel && viewModel.NewsViewModel is not null)
await viewModel.NewsViewModel.LoadNews.ExecuteAsync(null);
}
catch(Exception)
{
// Swallow to avoid process crash; NewsViewModel already logs errors.
}
}
}

View File

@@ -6,20 +6,22 @@ namespace Marechai.App.Presentation;
public partial class MainViewModel : ObservableObject
{
private INavigator _navigator;
private readonly INavigator _navigator;
[ObservableProperty] private string? name;
[ObservableProperty]
private string? name;
[ObservableProperty]
private NewsViewModel? newsViewModel;
public MainViewModel(
IStringLocalizer localizer,
IOptions<AppConfig> appInfo,
INavigator navigator)
public MainViewModel(IStringLocalizer localizer, IOptions<AppConfig> appInfo, INavigator navigator,
NewsViewModel newsViewModel)
{
_navigator = navigator;
Title = "Main";
Title += $" - {localizer["ApplicationName"]}";
Title += $" - {appInfo?.Value?.Environment}";
GoToSecond = new AsyncRelayCommand(GoToSecondView);
_navigator = navigator;
NewsViewModel = newsViewModel;
Title = "Marechai";
Title += $" - {localizer["ApplicationName"]}";
Title += $" - {appInfo?.Value?.Environment}";
GoToSecond = new AsyncRelayCommand(GoToSecondView);
}
public string? Title { get; }
@@ -30,4 +32,4 @@ public partial class MainViewModel : ObservableObject
{
await _navigator.NavigateViewModelAsync<SecondViewModel>(this, data: new Entity(Name!));
}
}
}

View File

@@ -0,0 +1,107 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using Marechai.App.Services;
namespace Marechai.App.Presentation;
/// <summary>
/// Wrapper for NewsDto with generated display text
/// </summary>
public class NewsItemViewModel
{
public required NewsDto News { get; init; }
public required string DisplayText { get; init; }
}
public partial class NewsViewModel : ObservableObject
{
private readonly IStringLocalizer _localizer;
private readonly ILogger<NewsViewModel> _logger;
private readonly NewsService _newsService;
[ObservableProperty]
private string errorMessage = string.Empty;
[ObservableProperty]
private bool hasError;
[ObservableProperty]
private bool isLoading;
[ObservableProperty]
private ObservableCollection<NewsItemViewModel> newsList = new();
public NewsViewModel(NewsService newsService, IStringLocalizer localizer, ILogger<NewsViewModel> logger)
{
_newsService = newsService;
_localizer = localizer;
_logger = logger;
LoadNews = new AsyncRelayCommand(LoadNewsAsync);
}
public IAsyncRelayCommand LoadNews { get; }
/// <summary>
/// Generates localized text based on NewsType
/// </summary>
private string GetLocalizedTextForNewsType(NewsType type)
{
return type switch
{
NewsType.NewComputerInDb => _localizer["New computer in database"].Value,
NewsType.NewConsoleInDb => _localizer["New console in database"].Value,
NewsType.NewComputerInCollection => _localizer["New computer in collection"].Value,
NewsType.NewConsoleInCollection => _localizer["New console in collection"].Value,
NewsType.UpdatedComputerInDb => _localizer["Updated computer in database"].Value,
NewsType.UpdatedConsoleInDb => _localizer["Updated console in database"].Value,
NewsType.UpdatedComputerInCollection => _localizer["Updated computer in collection"].Value,
NewsType.UpdatedConsoleInCollection => _localizer["Updated console in collection"].Value,
_ => string.Empty
};
}
/// <summary>
/// Loads the latest news from the API
/// </summary>
private async Task LoadNewsAsync()
{
try
{
IsLoading = true;
ErrorMessage = string.Empty;
HasError = false;
NewsList.Clear();
List<NewsDto> news = await _newsService.GetLatestNewsAsync();
if(news.Count == 0)
{
ErrorMessage = _localizer["No news available"].Value;
HasError = true;
}
else
{
foreach(NewsDto item in news)
{
NewsList.Add(new NewsItemViewModel
{
News = item,
DisplayText = GetLocalizedTextForNewsType((NewsType)(item.Type ?? 0))
});
}
}
}
catch(Exception ex)
{
_logger.LogError("Error loading news: {Exception}", ex.Message);
ErrorMessage = _localizer["Failed to load news. Please try again later."].Value;
HasError = true;
}
finally
{
IsLoading = false;
}
}
}

View File

@@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Marechai.App.Services;
/// <summary>
/// Service for fetching and managing news from the Marechai API
/// </summary>
public class NewsService
{
private readonly ApiClient _apiClient;
private readonly ILogger<NewsService> _logger;
public NewsService(ApiClient apiClient, ILogger<NewsService> logger)
{
_apiClient = apiClient;
_logger = logger;
}
/// <summary>
/// Fetches the latest news from the API
/// </summary>
/// <returns>List of latest news items, or empty list if API call fails</returns>
public async Task<List<NewsDto>> GetLatestNewsAsync()
{
try
{
_logger.LogInformation("Fetching latest news from API");
List<NewsDto> news = await _apiClient.News.Latest.GetAsync();
_logger.LogInformation("Successfully fetched {Count} news items", news?.Count ?? 0);
return news ?? new List<NewsDto>();
}
catch(Exception ex)
{
_logger.LogError(ex, "Error fetching latest news from API");
return new List<NewsDto>();
}
}
}