5 Commits

Author SHA1 Message Date
4d30530ef0 Add logo to company detail page. 2025-11-15 16:00:19 +00:00
e0689684e1 Add logo cache. 2025-11-15 15:47:31 +00:00
cfdef93787 Show country flag in company detail page. 2025-11-15 15:26:47 +00:00
c6cac9e04a Add flag cache. 2025-11-15 14:40:27 +00:00
e2f86b76db Serve assets. 2025-11-15 14:11:48 +00:00
10 changed files with 364 additions and 83 deletions

View File

@@ -2,6 +2,7 @@ using System.Net.Http;
using Marechai.App.Presentation.ViewModels; using Marechai.App.Presentation.ViewModels;
using Marechai.App.Presentation.Views; using Marechai.App.Presentation.Views;
using Marechai.App.Services; using Marechai.App.Services;
using Marechai.App.Services.Caching;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Uno.Extensions; using Uno.Extensions;
using Uno.Extensions.Configuration; using Uno.Extensions.Configuration;
@@ -106,6 +107,8 @@ public partial class App : Application
.ConfigureServices((context, services) => .ConfigureServices((context, services) =>
{ {
// Register application services // Register application services
services.AddSingleton<FlagCache>();
services.AddSingleton<CompanyLogoCache>();
services.AddSingleton<NewsService>(); services.AddSingleton<NewsService>();
services.AddSingleton<NewsViewModel>(); services.AddSingleton<NewsViewModel>();
services.AddSingleton<ComputersService>(); services.AddSingleton<ComputersService>();

View File

@@ -36,6 +36,7 @@
Navigation; Navigation;
ThemeService; ThemeService;
SkiaRenderer; SkiaRenderer;
Svg;
</UnoFeatures> </UnoFeatures>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -3,13 +3,16 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Input; using System.Windows.Input;
using Marechai.App.Helpers; using Marechai.App.Helpers;
using Marechai.App.Presentation.Models; using Marechai.App.Presentation.Models;
using Marechai.App.Services; using Marechai.App.Services;
using Marechai.App.Services.Caching;
using Marechai.Data; using Marechai.Data;
using Microsoft.UI.Xaml.Media.Imaging;
using Uno.Extensions.Navigation; using Uno.Extensions.Navigation;
namespace Marechai.App.Presentation.ViewModels; namespace Marechai.App.Presentation.ViewModels;
@@ -17,8 +20,10 @@ namespace Marechai.App.Presentation.ViewModels;
public partial class CompanyDetailViewModel : ObservableObject public partial class CompanyDetailViewModel : ObservableObject
{ {
private readonly CompanyDetailService _companyDetailService; private readonly CompanyDetailService _companyDetailService;
private readonly FlagCache _flagCache;
private readonly IStringLocalizer _localizer; private readonly IStringLocalizer _localizer;
private readonly ILogger<CompanyDetailViewModel> _logger; private readonly ILogger<CompanyDetailViewModel> _logger;
private readonly CompanyLogoCache _logoCache;
private readonly INavigator _navigator; private readonly INavigator _navigator;
[ObservableProperty] [ObservableProperty]
@@ -48,6 +53,9 @@ public partial class CompanyDetailViewModel : ObservableObject
[ObservableProperty] [ObservableProperty]
private ObservableCollection<CompanyDetailMachine> _filteredConsoles = []; private ObservableCollection<CompanyDetailMachine> _filteredConsoles = [];
[ObservableProperty]
private SvgImageSource? _flagImageSource;
[ObservableProperty] [ObservableProperty]
private bool _hasError; private bool _hasError;
@@ -57,13 +65,19 @@ public partial class CompanyDetailViewModel : ObservableObject
[ObservableProperty] [ObservableProperty]
private bool _isLoading; private bool _isLoading;
[ObservableProperty]
private SvgImageSource? _logoImageSource;
[ObservableProperty] [ObservableProperty]
private CompanyDto? _soldToCompany; private CompanyDto? _soldToCompany;
public CompanyDetailViewModel(CompanyDetailService companyDetailService, IStringLocalizer localizer, public CompanyDetailViewModel(CompanyDetailService companyDetailService, FlagCache flagCache,
CompanyLogoCache logoCache, IStringLocalizer localizer,
ILogger<CompanyDetailViewModel> logger, INavigator navigator) ILogger<CompanyDetailViewModel> logger, INavigator navigator)
{ {
_companyDetailService = companyDetailService; _companyDetailService = companyDetailService;
_flagCache = flagCache;
_logoCache = logoCache;
_localizer = localizer; _localizer = localizer;
_logger = logger; _logger = logger;
_navigator = navigator; _navigator = navigator;
@@ -82,6 +96,16 @@ public partial class CompanyDetailViewModel : ObservableObject
/// </summary> /// </summary>
public string CompanyFoundedDateDisplay => Company != null ? GetFoundedDateDisplay(Company) : string.Empty; public string CompanyFoundedDateDisplay => Company != null ? GetFoundedDateDisplay(Company) : string.Empty;
/// <summary>
/// Gets whether flag content is available
/// </summary>
public bool HasFlagContent => FlagImageSource != null;
/// <summary>
/// Gets whether logo content is available
/// </summary>
public bool HasLogoContent => LogoImageSource != null;
public IAsyncRelayCommand LoadData { get; } public IAsyncRelayCommand LoadData { get; }
public ICommand GoBackCommand { get; } public ICommand GoBackCommand { get; }
public IAsyncRelayCommand<CompanyDetailMachine> NavigateToMachineCommand { get; } public IAsyncRelayCommand<CompanyDetailMachine> NavigateToMachineCommand { get; }
@@ -94,6 +118,18 @@ public partial class CompanyDetailViewModel : ObservableObject
OnPropertyChanged(nameof(CompanyFoundedDateDisplay)); OnPropertyChanged(nameof(CompanyFoundedDateDisplay));
} }
partial void OnFlagImageSourceChanged(SvgImageSource? oldValue, SvgImageSource? newValue)
{
// Notify that HasFlagContent has changed
OnPropertyChanged(nameof(HasFlagContent));
}
partial void OnLogoImageSourceChanged(SvgImageSource? oldValue, SvgImageSource? newValue)
{
// Notify that HasLogoContent has changed
OnPropertyChanged(nameof(HasLogoContent));
}
partial void OnComputersFilterTextChanged(string value) partial void OnComputersFilterTextChanged(string value)
{ {
FilterComputers(value); FilterComputers(value);
@@ -282,10 +318,12 @@ public partial class CompanyDetailViewModel : ObservableObject
{ {
try try
{ {
IsLoading = true; IsLoading = true;
ErrorMessage = string.Empty; ErrorMessage = string.Empty;
HasError = false; HasError = false;
IsDataLoaded = false; IsDataLoaded = false;
FlagImageSource = null;
LogoImageSource = null;
if(CompanyId <= 0) if(CompanyId <= 0)
{ {
@@ -306,13 +344,57 @@ public partial class CompanyDetailViewModel : ObservableObject
return; return;
} }
// Load sold-to company if applicable // Load flag if country is available
if(Company.CountryId is not null)
{
try
{
var countryCode = (short)UntypedNodeExtractor.ExtractInt(Company.CountryId);
Stream? flagStream = await _flagCache.GetFlagAsync(countryCode);
var flagSource = new SvgImageSource();
await flagSource.SetSourceAsync(flagStream.AsRandomAccessStream());
FlagImageSource = flagSource;
_logger.LogInformation("Successfully loaded flag for country code {CountryCode}", countryCode);
}
catch(Exception ex)
{
_logger.LogError("Failed to load flag for country {CountryId}: {Exception}",
Company.CountryId,
ex.Message);
// Continue without flag if loading fails
}
}
if(Company.SoldToId != null) if(Company.SoldToId != null)
{ {
int soldToId = UntypedNodeExtractor.ExtractInt(Company.SoldToId); int soldToId = UntypedNodeExtractor.ExtractInt(Company.SoldToId);
if(soldToId > 0) SoldToCompany = await _companyDetailService.GetSoldToCompanyAsync(soldToId); if(soldToId > 0) SoldToCompany = await _companyDetailService.GetSoldToCompanyAsync(soldToId);
} }
// Load logo if available
if(Company.LastLogo.HasValue)
{
try
{
Stream? logoStream = await _logoCache.GetFlagAsync(Company.LastLogo.Value);
var logoSource = new SvgImageSource();
await logoSource.SetSourceAsync(logoStream.AsRandomAccessStream());
LogoImageSource = logoSource;
_logger.LogInformation("Successfully loaded logo for company {CompanyId}", CompanyId);
}
catch(Exception ex)
{
_logger.LogError("Failed to load logo for company {CompanyId}: {Exception}", CompanyId, ex.Message);
// Continue without logo if loading fails
}
}
// Load computers and consoles made by this company // Load computers and consoles made by this company
List<MachineDto> machines = await _companyDetailService.GetComputersByCompanyAsync(CompanyId); List<MachineDto> machines = await _companyDetailService.GetComputersByCompanyAsync(CompanyId);
Computers.Clear(); Computers.Clear();

View File

@@ -7,6 +7,9 @@
NavigationCacheMode="Required" NavigationCacheMode="Required"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Page.Resources>
</Page.Resources>
<Grid utu:SafeArea.Insets="VisibleBounds"> <Grid utu:SafeArea.Insets="VisibleBounds">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
@@ -29,6 +32,16 @@
<StackPanel Padding="16" <StackPanel Padding="16"
Spacing="16"> Spacing="16">
<!-- Logo Display (Top Center) -->
<Image MaxWidth="96"
MaxHeight="96"
Stretch="Uniform"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Margin="0,0,0,8"
Source="{Binding LogoImageSource}"
Visibility="{Binding HasLogoContent, Converter={StaticResource BoolToVisibilityConverter}}" />
<!-- Loading State --> <!-- Loading State -->
<StackPanel Visibility="{Binding IsLoading}" <StackPanel Visibility="{Binding IsLoading}"
VerticalAlignment="Center" VerticalAlignment="Center"
@@ -113,14 +126,35 @@
<!-- Country --> <!-- Country -->
<StackPanel Background="{ThemeResource CardBackgroundFillColorDefaultBrush}" <StackPanel Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8" CornerRadius="8"
Padding="12" Padding="12">
Spacing="8"> <Grid ColumnSpacing="12">
<TextBlock Text="Country" <Grid.ColumnDefinitions>
FontSize="14" <ColumnDefinition Width="*" />
FontWeight="SemiBold" <ColumnDefinition Width="Auto" />
Foreground="{ThemeResource SystemBaseMediumColor}" /> </Grid.ColumnDefinitions>
<TextBlock Text="{Binding Company.Country}"
FontSize="14" /> <!-- Country Name and Label -->
<StackPanel Grid.Column="0"
Spacing="4">
<TextBlock Text="Country"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding Company.Country}"
FontSize="14"
VerticalAlignment="Center" />
</StackPanel>
<!-- Country Flag -->
<Image Grid.Column="1"
Width="48"
Height="32"
Stretch="UniformToFill"
VerticalAlignment="Center"
HorizontalAlignment="Right"
Source="{Binding FlagImageSource}"
Visibility="{Binding HasFlagContent, Converter={StaticResource BoolToVisibilityConverter}}" />
</Grid>
</StackPanel> </StackPanel>
<!-- Address --> <!-- Address -->

View File

@@ -0,0 +1,64 @@
using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using Windows.Storage;
using Microsoft.Extensions.Configuration;
namespace Marechai.App.Services.Caching;
public sealed class FlagCache
{
readonly IConfiguration _configuration;
StorageFolder _flagsFolder;
public FlagCache(IConfiguration configuration)
{
_configuration = configuration;
_ = EnsureFolderExistAsync();
}
async Task EnsureFolderExistAsync()
{
StorageFolder localFolder = ApplicationData.Current.LocalCacheFolder;
_flagsFolder = await localFolder.CreateFolderAsync("flags", CreationCollisionOption.OpenIfExists);
}
public async Task<Stream> GetFlagAsync(short countryCode)
{
var filename = $"{countryCode:D3}.svg";
Stream retStream;
if(await _flagsFolder.TryGetItemAsync(filename) is StorageFile file)
{
retStream = await file.OpenStreamForReadAsync();
return retStream;
}
await CacheFlagAsync(countryCode);
file = await _flagsFolder.GetFileAsync(filename);
retStream = await file.OpenStreamForReadAsync();
return retStream;
}
async Task CacheFlagAsync(short countryCode)
{
var filename = $"{countryCode:D3}.svg";
string baseUrl = _configuration.GetSection("ApiClient:Url").Value;
string flagUrl = baseUrl + $"/assets/flags/countries/{filename}";
using var httpClient = new HttpClient();
using HttpResponseMessage response = await httpClient.GetAsync(flagUrl);
response.EnsureSuccessStatusCode();
using Stream stream = await response.Content.ReadAsStreamAsync();
StorageFile file = await _flagsFolder.CreateFileAsync(filename, CreationCollisionOption.ReplaceExisting);
using Stream fileStream = await file.OpenStreamForWriteAsync();
await stream.CopyToAsync(fileStream);
}
}

View File

@@ -0,0 +1,64 @@
using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using Windows.Storage;
using Microsoft.Extensions.Configuration;
namespace Marechai.App.Services.Caching;
public sealed class CompanyLogoCache
{
readonly IConfiguration _configuration;
StorageFolder _flagsFolder;
public CompanyLogoCache(IConfiguration configuration)
{
_configuration = configuration;
_ = EnsureFolderExistAsync();
}
async Task EnsureFolderExistAsync()
{
StorageFolder localFolder = ApplicationData.Current.LocalCacheFolder;
_flagsFolder = await localFolder.CreateFolderAsync("logos", CreationCollisionOption.OpenIfExists);
}
public async Task<Stream> GetFlagAsync(Guid companyLogoId)
{
var filename = $"{companyLogoId}.svg";
Stream retStream;
if(await _flagsFolder.TryGetItemAsync(filename) is StorageFile file)
{
retStream = await file.OpenStreamForReadAsync();
return retStream;
}
await CacheFlagAsync(companyLogoId);
file = await _flagsFolder.GetFileAsync(filename);
retStream = await file.OpenStreamForReadAsync();
return retStream;
}
async Task CacheFlagAsync(Guid companyLogoId)
{
var filename = $"{companyLogoId}.svg";
string baseUrl = _configuration.GetSection("ApiClient:Url").Value;
string flagUrl = baseUrl + $"/assets/logos/{filename}";
using var httpClient = new HttpClient();
using HttpResponseMessage response = await httpClient.GetAsync(flagUrl);
response.EnsureSuccessStatusCode();
using Stream stream = await response.Content.ReadAsStreamAsync();
StorageFile file = await _flagsFolder.CreateFileAsync(filename, CreationCollisionOption.ReplaceExisting);
using Stream fileStream = await file.OpenStreamForWriteAsync();
await stream.CopyToAsync(fileStream);
}
}

View File

@@ -36,11 +36,11 @@ public class Photos
{ {
public delegate Task ConversionFinished(bool result); public delegate Task ConversionFinished(bool result);
public static void EnsureCreated(string webRootPath, bool scan, string item) public static void EnsureCreated(string assetRootPath, bool scan, string item)
{ {
List<string> paths = []; List<string> paths = [];
string photosRoot = Path.Combine(webRootPath, "assets", scan ? "scan" : "photos"); string photosRoot = Path.Combine(assetRootPath, scan ? "scan" : "photos");
string itemPhotosRoot = Path.Combine(photosRoot, item); string itemPhotosRoot = Path.Combine(photosRoot, item);
string itemThumbsRoot = Path.Combine(itemPhotosRoot, "thumbs"); string itemThumbsRoot = Path.Combine(itemPhotosRoot, "thumbs");
string itemOriginalPhotosRoot = Path.Combine(itemPhotosRoot, "originals"); string itemOriginalPhotosRoot = Path.Combine(itemPhotosRoot, "originals");
@@ -88,14 +88,14 @@ public class Photos
foreach(string path in paths.Where(path => !Directory.Exists(path))) Directory.CreateDirectory(path); foreach(string path in paths.Where(path => !Directory.Exists(path))) Directory.CreateDirectory(path);
} }
public static bool Convert(string webRootPath, Guid id, string originalPath, string sourceFormat, public static bool Convert(string assetRootPath, Guid id, string originalPath, string sourceFormat,
string outputFormat, string resolution, bool thumbnail, bool scan, string item) string outputFormat, string resolution, bool thumbnail, bool scan, string item)
{ {
outputFormat = outputFormat.ToLowerInvariant(); outputFormat = outputFormat.ToLowerInvariant();
resolution = resolution.ToLowerInvariant(); resolution = resolution.ToLowerInvariant();
sourceFormat = sourceFormat.ToLowerInvariant(); sourceFormat = sourceFormat.ToLowerInvariant();
string outputPath = Path.Combine(webRootPath, "assets", scan ? "scans" : "photos", item); string outputPath = Path.Combine(assetRootPath, scan ? "scans" : "photos", item);
int width, height; int width, height;
if(thumbnail) outputPath = Path.Combine(outputPath, "thumbs"); if(thumbnail) outputPath = Path.Combine(outputPath, "thumbs");
@@ -268,12 +268,12 @@ public class Photos
} }
} }
public void ConversionWorker(string webRootPath, Guid id, string originalFilePath, string sourceFormat, bool scan, public void ConversionWorker(string assetRootPath, Guid id, string originalFilePath, string sourceFormat, bool scan,
string item) string item)
{ {
List<Task> pool = List<Task> pool =
[ [
new(() => FinishedRenderingJpeg4kThumbnail?.Invoke(Convert(webRootPath, new(() => FinishedRenderingJpeg4kThumbnail?.Invoke(Convert(assetRootPath,
id, id,
originalFilePath, originalFilePath,
sourceFormat, sourceFormat,
@@ -282,7 +282,7 @@ public class Photos
true, true,
scan, scan,
item))), item))),
new(() => FinishedRenderingJpeg1440Thumbnail?.Invoke(Convert(webRootPath, new(() => FinishedRenderingJpeg1440Thumbnail?.Invoke(Convert(assetRootPath,
id, id,
originalFilePath, originalFilePath,
sourceFormat, sourceFormat,
@@ -291,7 +291,7 @@ public class Photos
true, true,
scan, scan,
item))), item))),
new(() => FinishedRenderingJpegHdThumbnail?.Invoke(Convert(webRootPath, new(() => FinishedRenderingJpegHdThumbnail?.Invoke(Convert(assetRootPath,
id, id,
originalFilePath, originalFilePath,
sourceFormat, sourceFormat,
@@ -300,7 +300,7 @@ public class Photos
true, true,
scan, scan,
item))), item))),
new(() => FinishedRenderingJpeg4K?.Invoke(Convert(webRootPath, new(() => FinishedRenderingJpeg4K?.Invoke(Convert(assetRootPath,
id, id,
originalFilePath, originalFilePath,
sourceFormat, sourceFormat,
@@ -309,7 +309,7 @@ public class Photos
false, false,
scan, scan,
item))), item))),
new(() => FinishedRenderingJpeg1440?.Invoke(Convert(webRootPath, new(() => FinishedRenderingJpeg1440?.Invoke(Convert(assetRootPath,
id, id,
originalFilePath, originalFilePath,
sourceFormat, sourceFormat,
@@ -318,7 +318,7 @@ public class Photos
false, false,
scan, scan,
item))), item))),
new(() => FinishedRenderingJpegHd?.Invoke(Convert(webRootPath, new(() => FinishedRenderingJpegHd?.Invoke(Convert(assetRootPath,
id, id,
originalFilePath, originalFilePath,
sourceFormat, sourceFormat,
@@ -327,7 +327,7 @@ public class Photos
false, false,
scan, scan,
item))), item))),
new(() => FinishedRenderingJp2k4kThumbnail?.Invoke(Convert(webRootPath, new(() => FinishedRenderingJp2k4kThumbnail?.Invoke(Convert(assetRootPath,
id, id,
originalFilePath, originalFilePath,
sourceFormat, sourceFormat,
@@ -336,7 +336,7 @@ public class Photos
true, true,
scan, scan,
item))), item))),
new(() => FinishedRenderingJp2k1440Thumbnail?.Invoke(Convert(webRootPath, new(() => FinishedRenderingJp2k1440Thumbnail?.Invoke(Convert(assetRootPath,
id, id,
originalFilePath, originalFilePath,
sourceFormat, sourceFormat,
@@ -345,7 +345,7 @@ public class Photos
true, true,
scan, scan,
item))), item))),
new(() => FinishedRenderingJp2kHdThumbnail?.Invoke(Convert(webRootPath, new(() => FinishedRenderingJp2kHdThumbnail?.Invoke(Convert(assetRootPath,
id, id,
originalFilePath, originalFilePath,
sourceFormat, sourceFormat,
@@ -354,7 +354,7 @@ public class Photos
true, true,
scan, scan,
item))), item))),
new(() => FinishedRenderingJp2k4k?.Invoke(Convert(webRootPath, new(() => FinishedRenderingJp2k4k?.Invoke(Convert(assetRootPath,
id, id,
originalFilePath, originalFilePath,
sourceFormat, sourceFormat,
@@ -363,7 +363,7 @@ public class Photos
false, false,
scan, scan,
item))), item))),
new(() => FinishedRenderingJp2k1440?.Invoke(Convert(webRootPath, new(() => FinishedRenderingJp2k1440?.Invoke(Convert(assetRootPath,
id, id,
originalFilePath, originalFilePath,
sourceFormat, sourceFormat,
@@ -372,7 +372,7 @@ public class Photos
false, false,
scan, scan,
item))), item))),
new(() => FinishedRenderingJp2kHd?.Invoke(Convert(webRootPath, new(() => FinishedRenderingJp2kHd?.Invoke(Convert(assetRootPath,
id, id,
originalFilePath, originalFilePath,
sourceFormat, sourceFormat,
@@ -381,7 +381,7 @@ public class Photos
false, false,
scan, scan,
item))), item))),
new(() => FinishedRenderingWebp4kThumbnail?.Invoke(Convert(webRootPath, new(() => FinishedRenderingWebp4kThumbnail?.Invoke(Convert(assetRootPath,
id, id,
originalFilePath, originalFilePath,
sourceFormat, sourceFormat,
@@ -390,7 +390,7 @@ public class Photos
true, true,
scan, scan,
item))), item))),
new(() => FinishedRenderingWebp1440Thumbnail?.Invoke(Convert(webRootPath, new(() => FinishedRenderingWebp1440Thumbnail?.Invoke(Convert(assetRootPath,
id, id,
originalFilePath, originalFilePath,
sourceFormat, sourceFormat,
@@ -399,7 +399,7 @@ public class Photos
true, true,
scan, scan,
item))), item))),
new(() => FinishedRenderingWebpHdThumbnail?.Invoke(Convert(webRootPath, new(() => FinishedRenderingWebpHdThumbnail?.Invoke(Convert(assetRootPath,
id, id,
originalFilePath, originalFilePath,
sourceFormat, sourceFormat,
@@ -408,7 +408,7 @@ public class Photos
true, true,
scan, scan,
item))), item))),
new(() => FinishedRenderingWebp4k?.Invoke(Convert(webRootPath, new(() => FinishedRenderingWebp4k?.Invoke(Convert(assetRootPath,
id, id,
originalFilePath, originalFilePath,
sourceFormat, sourceFormat,
@@ -417,7 +417,7 @@ public class Photos
false, false,
scan, scan,
item))), item))),
new(() => FinishedRenderingWebp1440?.Invoke(Convert(webRootPath, new(() => FinishedRenderingWebp1440?.Invoke(Convert(assetRootPath,
id, id,
originalFilePath, originalFilePath,
sourceFormat, sourceFormat,
@@ -426,7 +426,7 @@ public class Photos
false, false,
scan, scan,
item))), item))),
new(() => FinishedRenderingWebpHd?.Invoke(Convert(webRootPath, new(() => FinishedRenderingWebpHd?.Invoke(Convert(assetRootPath,
id, id,
originalFilePath, originalFilePath,
sourceFormat, sourceFormat,
@@ -435,7 +435,7 @@ public class Photos
false, false,
scan, scan,
item))), item))),
new(() => FinishedRenderingHeif4kThumbnail?.Invoke(Convert(webRootPath, new(() => FinishedRenderingHeif4kThumbnail?.Invoke(Convert(assetRootPath,
id, id,
originalFilePath, originalFilePath,
sourceFormat, sourceFormat,
@@ -444,7 +444,7 @@ public class Photos
true, true,
scan, scan,
item))), item))),
new(() => FinishedRenderingHeif1440Thumbnail?.Invoke(Convert(webRootPath, new(() => FinishedRenderingHeif1440Thumbnail?.Invoke(Convert(assetRootPath,
id, id,
originalFilePath, originalFilePath,
sourceFormat, sourceFormat,
@@ -453,7 +453,7 @@ public class Photos
true, true,
scan, scan,
item))), item))),
new(() => FinishedRenderingHeifHdThumbnail?.Invoke(Convert(webRootPath, new(() => FinishedRenderingHeifHdThumbnail?.Invoke(Convert(assetRootPath,
id, id,
originalFilePath, originalFilePath,
sourceFormat, sourceFormat,
@@ -462,7 +462,7 @@ public class Photos
true, true,
scan, scan,
item))), item))),
new(() => FinishedRenderingHeif4K?.Invoke(Convert(webRootPath, new(() => FinishedRenderingHeif4K?.Invoke(Convert(assetRootPath,
id, id,
originalFilePath, originalFilePath,
sourceFormat, sourceFormat,
@@ -471,7 +471,7 @@ public class Photos
false, false,
scan, scan,
item))), item))),
new(() => FinishedRenderingHeif1440?.Invoke(Convert(webRootPath, new(() => FinishedRenderingHeif1440?.Invoke(Convert(assetRootPath,
id, id,
originalFilePath, originalFilePath,
sourceFormat, sourceFormat,
@@ -480,7 +480,7 @@ public class Photos
false, false,
scan, scan,
item))), item))),
new(() => FinishedRenderingHeifHd?.Invoke(Convert(webRootPath, new(() => FinishedRenderingHeifHd?.Invoke(Convert(assetRootPath,
id, id,
originalFilePath, originalFilePath,
sourceFormat, sourceFormat,
@@ -489,7 +489,7 @@ public class Photos
false, false,
scan, scan,
item))), item))),
new(() => FinishedRenderingAvif4kThumbnail?.Invoke(Convert(webRootPath, new(() => FinishedRenderingAvif4kThumbnail?.Invoke(Convert(assetRootPath,
id, id,
originalFilePath, originalFilePath,
sourceFormat, sourceFormat,
@@ -498,7 +498,7 @@ public class Photos
true, true,
scan, scan,
item))), item))),
new(() => FinishedRenderingAvif1440Thumbnail?.Invoke(Convert(webRootPath, new(() => FinishedRenderingAvif1440Thumbnail?.Invoke(Convert(assetRootPath,
id, id,
originalFilePath, originalFilePath,
sourceFormat, sourceFormat,
@@ -507,7 +507,7 @@ public class Photos
true, true,
scan, scan,
item))), item))),
new(() => FinishedRenderingAvifHdThumbnail?.Invoke(Convert(webRootPath, new(() => FinishedRenderingAvifHdThumbnail?.Invoke(Convert(assetRootPath,
id, id,
originalFilePath, originalFilePath,
sourceFormat, sourceFormat,
@@ -516,7 +516,7 @@ public class Photos
true, true,
scan, scan,
item))), item))),
new(() => FinishedRenderingAvif4K?.Invoke(Convert(webRootPath, new(() => FinishedRenderingAvif4K?.Invoke(Convert(assetRootPath,
id, id,
originalFilePath, originalFilePath,
sourceFormat, sourceFormat,
@@ -525,7 +525,7 @@ public class Photos
false, false,
scan, scan,
item))), item))),
new(() => FinishedRenderingAvif1440?.Invoke(Convert(webRootPath, new(() => FinishedRenderingAvif1440?.Invoke(Convert(assetRootPath,
id, id,
originalFilePath, originalFilePath,
sourceFormat, sourceFormat,
@@ -534,7 +534,7 @@ public class Photos
false, false,
scan, scan,
item))), item))),
new(() => FinishedRenderingAvifHd?.Invoke(Convert(webRootPath, new(() => FinishedRenderingAvifHd?.Invoke(Convert(assetRootPath,
id, id,
originalFilePath, originalFilePath,
sourceFormat, sourceFormat,

View File

@@ -33,11 +33,11 @@ namespace Marechai.Server.Helpers;
public static class SvgRender public static class SvgRender
{ {
public static void RenderCountries() public static void RenderCountries(string assetRootPath)
{ {
if(!Directory.Exists("wwwroot/assets/flags/countries")) return; if(!Directory.Exists($"{assetRootPath}/flags/countries")) return;
foreach(string file in Directory.GetFiles("wwwroot/assets/flags/countries/", foreach(string file in Directory.GetFiles($"{assetRootPath}/flags/countries/",
"*.svg", "*.svg",
SearchOption.TopDirectoryOnly)) SearchOption.TopDirectoryOnly))
{ {
@@ -50,8 +50,8 @@ public static class SvgRender
"png", "webp" "png", "webp"
}) })
{ {
if(!Directory.Exists(Path.Combine("wwwroot/assets/flags/countries", format))) if(!Directory.Exists(Path.Combine($"{assetRootPath}/flags/countries", format)))
Directory.CreateDirectory(Path.Combine("wwwroot/assets/flags/countries", format)); Directory.CreateDirectory(Path.Combine($"{assetRootPath}/flags/countries", format));
SKEncodedImageFormat skFormat; SKEncodedImageFormat skFormat;
@@ -72,14 +72,14 @@ public static class SvgRender
1, 2, 3 1, 2, 3
}) })
{ {
if(!Directory.Exists(Path.Combine("wwwroot/assets/flags/countries", format, $"{multiplier}x"))) if(!Directory.Exists(Path.Combine($"{assetRootPath}/flags/countries", format, $"{multiplier}x")))
{ {
Directory.CreateDirectory(Path.Combine("wwwroot/assets/flags/countries", Directory.CreateDirectory(Path.Combine($"{assetRootPath}/flags/countries",
format, format,
$"{multiplier}x")); $"{multiplier}x"));
} }
string rendered = Path.Combine("wwwroot/assets/flags/countries", string rendered = Path.Combine($"{assetRootPath}/flags/countries",
format, format,
$"{multiplier}x", $"{multiplier}x",
flagName + $".{format}"); flagName + $".{format}");
@@ -102,11 +102,11 @@ public static class SvgRender
} }
} }
public static void ImportCompanyLogos(MarechaiContext context) public static void ImportCompanyLogos(string assetRootPath, MarechaiContext context)
{ {
if(!Directory.Exists("wwwroot/assets/incoming")) return; if(!Directory.Exists($"{assetRootPath}/incoming")) return;
foreach(string file in Directory.GetFiles("wwwroot/assets/incoming", foreach(string file in Directory.GetFiles($"{assetRootPath}/incoming",
"company_*.svg", "company_*.svg",
SearchOption.TopDirectoryOnly)) SearchOption.TopDirectoryOnly))
{ {
@@ -153,8 +153,8 @@ public static class SvgRender
}) })
{ {
string outDir = minSize == 32 string outDir = minSize == 32
? Path.Combine("wwwroot/assets/logos/thumbs", format) ? Path.Combine($"{assetRootPath}/logos/thumbs", format)
: Path.Combine("wwwroot/assets/logos", format); : Path.Combine($"{assetRootPath}/logos", format);
if(!Directory.Exists(outDir)) Directory.CreateDirectory(outDir); if(!Directory.Exists(outDir)) Directory.CreateDirectory(outDir);
@@ -200,7 +200,7 @@ public static class SvgRender
} }
} }
File.Move(file, $"wwwroot/assets/logos/{guid}.svg"); File.Move(file, $"{assetRootPath}/logos/{guid}.svg");
} }
} }

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.IO;
using System.Linq; using System.Linq;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
@@ -12,9 +13,11 @@ using Markdig;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using Version = Marechai.Server.Interop.Version; using Version = Marechai.Server.Interop.Version;
@@ -140,14 +143,36 @@ file class Program
} }
} }
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
string assetRootPath = builder.Configuration["AssetRootPath"];
if(string.IsNullOrEmpty(assetRootPath))
{
Console.WriteLine("\e[31;1mAsset root path not set, cannot continue, check configuration...\e[0m");
return;
}
Directory.CreateDirectory(assetRootPath);
DateTime start = DateTime.Now; DateTime start = DateTime.Now;
Console.WriteLine("\e[31;1mRendering new country flags...\e[0m"); Console.WriteLine("\e[31;1mRendering new country flags...\e[0m");
SvgRender.RenderCountries(); SvgRender.RenderCountries(assetRootPath);
DateTime end = DateTime.Now; DateTime end = DateTime.Now;
Console.WriteLine("\e[31;1mTook \e[32;1m{0} seconds\e[31;1m...\e[0m", (end - start).TotalSeconds); Console.WriteLine("\e[31;1mTook \e[32;1m{0} seconds\e[31;1m...\e[0m", (end - start).TotalSeconds);
WebApplicationBuilder builder = WebApplication.CreateBuilder(args); start = DateTime.Now;
Console.WriteLine("\e[31;1mEnsuring photo folders exist...\e[0m");
Photos.EnsureCreated(assetRootPath, false, "machines");
Console.WriteLine("\e[31;1mEnsuring scan folders exist...\e[0m");
Photos.EnsureCreated(assetRootPath, true, "books");
Photos.EnsureCreated(assetRootPath, true, "documents");
Photos.EnsureCreated(assetRootPath, true, "magazines");
end = DateTime.Now;
Console.WriteLine("\e[31;1mTook \e[32;1m{0} seconds\e[31;1m...\e[0m", (end - start).TotalSeconds);
// Add services to the container. // Add services to the container.
builder.Services.AddControllers() builder.Services.AddControllers()
@@ -210,8 +235,7 @@ file class Program
builder.Services.AddScoped<TokenService, TokenService>(); builder.Services.AddScoped<TokenService, TokenService>();
// Read allowed CORS origins from configuration // Read allowed CORS origins from configuration
string[] allowedOrigins = builder.Configuration.GetSection("CORS:AllowedOrigins").Get<string[]>() ?? string[] allowedOrigins = builder.Configuration.GetSection("CORS:AllowedOrigins").Get<string[]>() ?? [];
Array.Empty<string>();
builder.Services.AddCors(options => builder.Services.AddCors(options =>
{ {
@@ -241,6 +265,28 @@ file class Program
app.MapControllers(); app.MapControllers();
// Set up custom content types - associating file extension to MIME type
var provider = new FileExtensionContentTypeProvider
{
Mappings =
{
// Add new mappings
[".avif"] = "image/avif", // AVIF image format
[".heic"] = "image/heic", // HEIC image format
[".heif"] = "image/heif", // HEIF image format
[".jxl"] = "image/jxl", // JPEG-XL image format
[".webp"] = "image/webp", // WebP image format
[".svg"] = "image/svg+xml" // SVG image format
}
};
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(assetRootPath),
RequestPath = "/assets",
ContentTypeProvider = provider
});
using(IServiceScope scope = app.Services.CreateScope()) using(IServiceScope scope = app.Services.CreateScope())
{ {
IServiceProvider services = scope.ServiceProvider; IServiceProvider services = scope.ServiceProvider;
@@ -257,7 +303,7 @@ file class Program
start = DateTime.Now; start = DateTime.Now;
Console.WriteLine("\e[31;1mImporting company logos...\e[0m"); Console.WriteLine("\e[31;1mImporting company logos...\e[0m");
SvgRender.ImportCompanyLogos(context); SvgRender.ImportCompanyLogos(assetRootPath, context);
end = DateTime.Now; end = DateTime.Now;
Console.WriteLine("\e[31;1mTook \e[32;1m{0} seconds\e[31;1m...\e[0m", (end - start).TotalSeconds); Console.WriteLine("\e[31;1mTook \e[32;1m{0} seconds\e[31;1m...\e[0m", (end - start).TotalSeconds);
@@ -278,20 +324,6 @@ file class Program
end = DateTime.Now; end = DateTime.Now;
Console.WriteLine("\e[31;1mTook \e[32;1m{0} seconds\e[31;1m...\e[0m", (end - start).TotalSeconds); Console.WriteLine("\e[31;1mTook \e[32;1m{0} seconds\e[31;1m...\e[0m", (end - start).TotalSeconds);
start = DateTime.Now;
Console.WriteLine("\e[31;1mEnsuring photo folders exist...\e[0m");
Photos.EnsureCreated("wwwroot", false, "machines");
end = DateTime.Now;
start = DateTime.Now;
Console.WriteLine("\e[31;1mEnsuring scan folders exist...\e[0m");
Photos.EnsureCreated("wwwroot", true, "books");
Photos.EnsureCreated("wwwroot", true, "documents");
Photos.EnsureCreated("wwwroot", true, "magazines");
end = DateTime.Now;
Console.WriteLine("\e[31;1mTook \e[32;1m{0} seconds\e[31;1m...\e[0m", (end - start).TotalSeconds);
} }
catch(Exception ex) catch(Exception ex)
{ {

View File

@@ -64,5 +64,6 @@
"Name": "NormalUser", "Name": "NormalUser",
"Description": "A normal user role." "Description": "A normal user role."
} }
] ],
"AssetRootPath": "/var/marechai/assets"
} }