mirror of
https://github.com/claunia/marechai.git
synced 2025-12-16 19:14:25 +00:00
Show news on application load.
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using Marechai.App.Services;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Uno.Extensions;
|
using Uno.Extensions;
|
||||||
using Uno.Extensions.Configuration;
|
using Uno.Extensions.Configuration;
|
||||||
@@ -96,8 +97,9 @@ public partial class App : Application
|
|||||||
})
|
})
|
||||||
.ConfigureServices((context, services) =>
|
.ConfigureServices((context, services) =>
|
||||||
{
|
{
|
||||||
// TODO: Register your services
|
// Register application services
|
||||||
//services.AddSingleton<IMyService, MyService>();
|
services.AddSingleton<NewsService>();
|
||||||
|
services.AddSingleton<NewsViewModel>();
|
||||||
})
|
})
|
||||||
.UseNavigation(RegisterRoutes));
|
.UseNavigation(RegisterRoutes));
|
||||||
|
|
||||||
|
|||||||
14
Marechai.App/Enums.cs
Normal file
14
Marechai.App/Enums.cs
Normal 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
|
||||||
|
}
|
||||||
@@ -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="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
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"
|
xmlns:utu="using:Uno.Toolkit.UI"
|
||||||
NavigationCacheMode="Required"
|
NavigationCacheMode="Required"
|
||||||
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
|
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"
|
<Grid utu:SafeArea.Insets="VisibleBounds">
|
||||||
HorizontalAlignment="Center"
|
<Grid.RowDefinitions>
|
||||||
VerticalAlignment="Center"
|
<RowDefinition Height="Auto" /> <RowDefinition Height="*" />
|
||||||
Spacing="16">
|
</Grid.RowDefinitions>
|
||||||
<TextBox Text="{Binding Name, Mode=TwoWay}"
|
|
||||||
PlaceholderText="Enter your name:" />
|
<!-- Header -->
|
||||||
<Button Content="Go to Second Page"
|
<utu:NavigationBar Content="{Binding Title}" />
|
||||||
AutomationProperties.AutomationId="SecondPageButton"
|
|
||||||
Command="{Binding GoToSecond}" />
|
<!-- Refresh Container with Pull-to-Refresh -->
|
||||||
</StackPanel>
|
<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>
|
</Grid>
|
||||||
</ScrollViewer>
|
</Page>
|
||||||
</Page>
|
|
||||||
@@ -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;
|
namespace Marechai.App.Presentation;
|
||||||
|
|
||||||
public sealed partial class MainPage : Page
|
public sealed partial class MainPage : Page
|
||||||
{
|
{
|
||||||
|
private bool _initialNewsLoaded;
|
||||||
|
|
||||||
public MainPage()
|
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.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,20 +6,22 @@ namespace Marechai.App.Presentation;
|
|||||||
|
|
||||||
public partial class MainViewModel : ObservableObject
|
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(
|
public MainViewModel(IStringLocalizer localizer, IOptions<AppConfig> appInfo, INavigator navigator,
|
||||||
IStringLocalizer localizer,
|
NewsViewModel newsViewModel)
|
||||||
IOptions<AppConfig> appInfo,
|
|
||||||
INavigator navigator)
|
|
||||||
{
|
{
|
||||||
_navigator = navigator;
|
_navigator = navigator;
|
||||||
Title = "Main";
|
NewsViewModel = newsViewModel;
|
||||||
Title += $" - {localizer["ApplicationName"]}";
|
Title = "Marechai";
|
||||||
Title += $" - {appInfo?.Value?.Environment}";
|
Title += $" - {localizer["ApplicationName"]}";
|
||||||
GoToSecond = new AsyncRelayCommand(GoToSecondView);
|
Title += $" - {appInfo?.Value?.Environment}";
|
||||||
|
GoToSecond = new AsyncRelayCommand(GoToSecondView);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string? Title { get; }
|
public string? Title { get; }
|
||||||
@@ -30,4 +32,4 @@ public partial class MainViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
await _navigator.NavigateViewModelAsync<SecondViewModel>(this, data: new Entity(Name!));
|
await _navigator.NavigateViewModelAsync<SecondViewModel>(this, data: new Entity(Name!));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
107
Marechai.App/Presentation/NewsViewModel.cs
Normal file
107
Marechai.App/Presentation/NewsViewModel.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
Marechai.App/Services/NewsService.cs
Normal file
42
Marechai.App/Services/NewsService.cs
Normal 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>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user