mirror of
https://github.com/claunia/marechai.git
synced 2026-07-08 17:57:08 +00:00
Platforms (Xbox, PlayStation, PC, etc.) were displayed as plain text/chips
with no visual identity. This adds a single optional logo per platform,
shown as a small icon next to the platform name.
Unlike CompanyLogo (which supports multiple SVG logos per company,
versioned by year, for rebrand history), platforms only need one raster
logo with no history, so this introduces a simpler, separate model:
LogoId (Guid?) and LogoExtension (string?) stored directly on
SoftwarePlatform rather than a new child entity.
Backend:
- SoftwarePlatform gains nullable LogoId/LogoExtension columns
(migration AddSoftwarePlatformLogo).
- New POST/DELETE /software/platforms/{id}/logo endpoints (Admin only).
Upload accepts JPEG/PNG/WebP/BMP up to 5MB, validated by extension and
content type. Reuses the existing ImageMagick-CLI-backed Photos helper
(same one used for screenshots/photos) to convert the upload into
JPEG/WebP/AVIF variants under assets/photos/platform-logos/, sized for
an icon (256px full / 64px thumb) rather than the 4K/512px tiers used
for photos. Replacing or deleting a logo cleans up all rendered files.
- SoftwarePlatformDto exposes logo_id/logo_extension; GET list/single
projections include them. Platform cache is invalidated on logo
upload/delete same as other mutations.
- Photos.EnsureCreated for "platform-logos" registered at startup.
Web UI (legacy Blazor):
- SoftwarePlatformDialog (admin edit dialog) gets an upload/preview/
remove logo control, calling the new endpoints directly since there's
only one logo to manage (no separate logo-list page needed).
- Platform chips on the /software landing page and the admin platforms
grid show a small thumbnail next to the name when a logo is set.
Uno app (Marechai.App):
- AdminSoftwarePlatformsViewModel/Page get the same upload/remove logo
capability in the edit panel, using FileOpenPicker and the regenerated
Kiota client. New localized strings (LogoLabel, UploadLogoButton,
RemoveLogoButton, FailedToUploadLogo, FailedToRemoveLogo) added across
all 5 languages (en/de/es/fr/pt-BR).
API client:
- openapi.yaml and Marechai.ApiClient regenerated from a running server
instance to pick up the new endpoints and DTO fields (LogoRequestBuilder,
extended SoftwarePlatformDto). Diff is purely additive.
324 lines
11 KiB
C#
324 lines
11 KiB
C#
#nullable enable
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using Marechai.ApiClient.Models;
|
|
using Marechai.App.Services;
|
|
using Marechai.App.Services.Authentication;
|
|
using Microsoft.Extensions.Configuration;
|
|
|
|
namespace Marechai.App.Presentation.ViewModels.Admin;
|
|
|
|
public partial class AdminSoftwarePlatformsViewModel : ObservableObject, IRegionAware
|
|
{
|
|
private readonly SoftwarePlatformsService _service;
|
|
private readonly IJwtService _jwtService;
|
|
private readonly IStringLocalizer _localizer;
|
|
private readonly ILogger<AdminSoftwarePlatformsViewModel> _logger;
|
|
private readonly ITokenService _tokenService;
|
|
private readonly IConfiguration _configuration;
|
|
|
|
[ObservableProperty] private ObservableCollection<SoftwarePlatformDto> _platforms = [];
|
|
[ObservableProperty] private ObservableCollection<SoftwarePlatformDto> _filteredPlatforms = [];
|
|
[ObservableProperty] private string _filterText = string.Empty;
|
|
[ObservableProperty] private SoftwarePlatformDto? _selectedPlatform;
|
|
[ObservableProperty] private bool _isLoading;
|
|
[ObservableProperty] private bool _isDataLoaded;
|
|
[ObservableProperty] private bool _hasError;
|
|
[ObservableProperty] private string _errorMessage = string.Empty;
|
|
[ObservableProperty] private bool _isAdmin;
|
|
[ObservableProperty] private bool _isEditing;
|
|
[ObservableProperty] private string _editPanelTitle = string.Empty;
|
|
[ObservableProperty] private string _platformName = string.Empty;
|
|
[ObservableProperty] private Guid? _logoId;
|
|
[ObservableProperty] private bool _isUploadingLogo;
|
|
|
|
private int? _editingId;
|
|
private List<SoftwarePlatformDto>? _allPlatforms;
|
|
|
|
public string? LogoThumbnailUrl => LogoId.HasValue
|
|
? $"{_configuration.GetSection("ApiClient:Url").Value}/assets/photos/platform-logos/thumbs/webp/4k/{LogoId}.webp"
|
|
: null;
|
|
|
|
public AdminSoftwarePlatformsViewModel(SoftwarePlatformsService service,
|
|
IJwtService jwtService,
|
|
ITokenService tokenService,
|
|
ILogger<AdminSoftwarePlatformsViewModel> logger,
|
|
IStringLocalizer localizer,
|
|
IConfiguration configuration)
|
|
{
|
|
_service = service;
|
|
_jwtService = jwtService;
|
|
_tokenService = tokenService;
|
|
_logger = logger;
|
|
_localizer = localizer;
|
|
_configuration = configuration;
|
|
|
|
LoadCommand = new AsyncRelayCommand(LoadAsync);
|
|
OpenAddCommand = new RelayCommand(OpenAdd);
|
|
OpenEditCommand = new RelayCommand<SoftwarePlatformDto>(OpenEdit);
|
|
DeleteCommand = new AsyncRelayCommand<SoftwarePlatformDto>(DeleteAsync);
|
|
SaveCommand = new AsyncRelayCommand(SaveAsync);
|
|
CancelEditCommand = new RelayCommand(CancelEdit);
|
|
UploadLogoCommand = new AsyncRelayCommand(UploadLogoAsync);
|
|
RemoveLogoCommand = new AsyncRelayCommand(RemoveLogoAsync);
|
|
|
|
CheckAdminRole();
|
|
}
|
|
|
|
public IAsyncRelayCommand LoadCommand { get; }
|
|
public IRelayCommand OpenAddCommand { get; }
|
|
public IRelayCommand<SoftwarePlatformDto> OpenEditCommand { get; }
|
|
public IAsyncRelayCommand<SoftwarePlatformDto> DeleteCommand { get; }
|
|
public IAsyncRelayCommand SaveCommand { get; }
|
|
public IRelayCommand CancelEditCommand { get; }
|
|
public IAsyncRelayCommand UploadLogoCommand { get; }
|
|
public IAsyncRelayCommand RemoveLogoCommand { get; }
|
|
|
|
partial void OnLogoIdChanged(Guid? value) => OnPropertyChanged(nameof(LogoThumbnailUrl));
|
|
|
|
public bool IsNavigationTarget(NavigationContext navigationContext) => true;
|
|
public void OnNavigatedFrom(NavigationContext navigationContext) { }
|
|
|
|
public void OnNavigatedTo(NavigationContext navigationContext)
|
|
{
|
|
CheckAdminRole();
|
|
|
|
if(IsAdmin) _ = LoadCommand.ExecuteAsync(null);
|
|
}
|
|
|
|
private void CheckAdminRole()
|
|
{
|
|
try
|
|
{
|
|
string token = _tokenService.GetToken();
|
|
|
|
if(string.IsNullOrWhiteSpace(token)) { IsAdmin = false; return; }
|
|
|
|
IEnumerable<string> roles = _jwtService.GetRoles(token);
|
|
|
|
IsAdmin = roles.Contains("UberAdmin", StringComparer.OrdinalIgnoreCase) ||
|
|
roles.Contains("Admin", StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
catch { IsAdmin = false; }
|
|
}
|
|
|
|
private async Task LoadAsync()
|
|
{
|
|
try
|
|
{
|
|
IsLoading = true;
|
|
HasError = false;
|
|
ErrorMessage = string.Empty;
|
|
Platforms.Clear();
|
|
|
|
List<SoftwarePlatformDto> response = await _service.GetAllAsync();
|
|
_allPlatforms = response;
|
|
|
|
foreach(SoftwarePlatformDto item in response) Platforms.Add(item);
|
|
|
|
ApplyFilter();
|
|
IsDataLoaded = true;
|
|
}
|
|
catch(Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error loading software platforms");
|
|
ErrorMessage = _localizer["FailedToLoadSoftwarePlatforms"];
|
|
HasError = true;
|
|
}
|
|
finally { IsLoading = false; }
|
|
}
|
|
|
|
private void OpenAdd()
|
|
{
|
|
_editingId = null;
|
|
EditPanelTitle = _localizer["AddSoftwarePlatformDialog_Title"];
|
|
ClearForm();
|
|
IsEditing = true;
|
|
}
|
|
|
|
private void OpenEdit(SoftwarePlatformDto? item)
|
|
{
|
|
if(item == null) return;
|
|
|
|
_editingId = item.Id;
|
|
EditPanelTitle = _localizer["EditSoftwarePlatformDialog_Title"];
|
|
PlatformName = item.Name ?? string.Empty;
|
|
LogoId = item.LogoId;
|
|
HasError = false;
|
|
ErrorMessage = string.Empty;
|
|
IsEditing = true;
|
|
}
|
|
|
|
private async Task DeleteAsync(SoftwarePlatformDto? item)
|
|
{
|
|
if(item?.Id == null) return;
|
|
|
|
try
|
|
{
|
|
await _service.DeleteAsync(item.Id.Value);
|
|
await LoadAsync();
|
|
}
|
|
catch(Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error deleting software platform {Id}", item.Id);
|
|
ErrorMessage = _localizer["FailedToDeleteSoftwarePlatform"];
|
|
HasError = true;
|
|
}
|
|
}
|
|
|
|
private async Task SaveAsync()
|
|
{
|
|
try
|
|
{
|
|
if(string.IsNullOrWhiteSpace(PlatformName))
|
|
{
|
|
ErrorMessage = _localizer["NameIsRequired"];
|
|
HasError = true;
|
|
return;
|
|
}
|
|
|
|
var dto = new SoftwarePlatformDto { Name = PlatformName };
|
|
|
|
if(_editingId == null)
|
|
await _service.CreateAsync(dto);
|
|
else
|
|
{
|
|
dto.Id = _editingId;
|
|
await _service.UpdateAsync(dto);
|
|
}
|
|
|
|
IsEditing = false;
|
|
ClearForm();
|
|
await LoadAsync();
|
|
}
|
|
catch(Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error saving software platform");
|
|
ErrorMessage = _localizer["FailedToSaveSoftwarePlatform"];
|
|
HasError = true;
|
|
}
|
|
}
|
|
|
|
private void CancelEdit()
|
|
{
|
|
IsEditing = false;
|
|
_editingId = null;
|
|
ClearForm();
|
|
HasError = false;
|
|
ErrorMessage = string.Empty;
|
|
}
|
|
|
|
public void ApplyFilter()
|
|
{
|
|
FilteredPlatforms.Clear();
|
|
IEnumerable<SoftwarePlatformDto> source = (IEnumerable<SoftwarePlatformDto>?)_allPlatforms ?? Platforms;
|
|
|
|
if(!string.IsNullOrWhiteSpace(FilterText))
|
|
source = source.Where(p => p.Name != null &&
|
|
p.Name.Contains(FilterText, StringComparison.OrdinalIgnoreCase));
|
|
|
|
foreach(SoftwarePlatformDto item in source) FilteredPlatforms.Add(item);
|
|
}
|
|
|
|
private void ClearForm()
|
|
{
|
|
PlatformName = string.Empty;
|
|
LogoId = null;
|
|
HasError = false;
|
|
ErrorMessage = string.Empty;
|
|
}
|
|
|
|
private async Task UploadLogoAsync()
|
|
{
|
|
if(_editingId == null) return;
|
|
|
|
try
|
|
{
|
|
var picker = new Windows.Storage.Pickers.FileOpenPicker();
|
|
picker.FileTypeFilter.Add(".jpg");
|
|
picker.FileTypeFilter.Add(".jpeg");
|
|
picker.FileTypeFilter.Add(".png");
|
|
picker.FileTypeFilter.Add(".webp");
|
|
picker.FileTypeFilter.Add(".bmp");
|
|
|
|
#if !HAS_UNO
|
|
var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(App.MainWindow);
|
|
WinRT.Interop.InitializeWithWindow.Initialize(picker, hwnd);
|
|
#endif
|
|
|
|
Windows.Storage.StorageFile? file = await picker.PickSingleFileAsync();
|
|
|
|
if(file == null) return;
|
|
|
|
IsUploadingLogo = true;
|
|
HasError = false;
|
|
|
|
using Stream stream = await file.OpenStreamForReadAsync();
|
|
using var ms = new MemoryStream();
|
|
await stream.CopyToAsync(ms);
|
|
byte[] fileBytes = ms.ToArray();
|
|
|
|
SoftwarePlatformDto? result =
|
|
await _service.UploadLogoAsync(_editingId.Value, fileBytes, file.Name, file.ContentType);
|
|
|
|
if(result == null)
|
|
{
|
|
ErrorMessage = _localizer["FailedToUploadLogo"];
|
|
HasError = true;
|
|
|
|
return;
|
|
}
|
|
|
|
LogoId = result.LogoId;
|
|
}
|
|
catch(Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error uploading logo for software platform {Id}", _editingId);
|
|
ErrorMessage = _localizer["FailedToUploadLogo"];
|
|
HasError = true;
|
|
}
|
|
finally
|
|
{
|
|
IsUploadingLogo = false;
|
|
}
|
|
}
|
|
|
|
private async Task RemoveLogoAsync()
|
|
{
|
|
if(_editingId == null) return;
|
|
|
|
try
|
|
{
|
|
IsUploadingLogo = true;
|
|
HasError = false;
|
|
|
|
bool success = await _service.DeleteLogoAsync(_editingId.Value);
|
|
|
|
if(!success)
|
|
{
|
|
ErrorMessage = _localizer["FailedToRemoveLogo"];
|
|
HasError = true;
|
|
|
|
return;
|
|
}
|
|
|
|
LogoId = null;
|
|
}
|
|
catch(Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error removing logo for software platform {Id}", _editingId);
|
|
ErrorMessage = _localizer["FailedToRemoveLogo"];
|
|
HasError = true;
|
|
}
|
|
finally
|
|
{
|
|
IsUploadingLogo = false;
|
|
}
|
|
}
|
|
}
|