Most Changes for v0.1.0-beta #51

Merged
Penry merged 24 commits from v0.1.0-beta into main 2026-06-07 16:46:15 +02:00
Showing only changes of commit 2e9ea45bd9 - Show all commits
@@ -1,146 +1,279 @@
@using CouchLog.Data @using CouchLog.Data
@using Microsoft.EntityFrameworkCore @using Microsoft.EntityFrameworkCore
@using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Authorization
@using CouchLog.Data
@using Microsoft.AspNetCore.Identity @using Microsoft.AspNetCore.Identity
@using Microsoft.EntityFrameworkCore @using System.ComponentModel.DataAnnotations
@inject ApplicationDbContext CouchLogDB @inject ApplicationDbContext CouchLogDB
@inject UserManager<ApplicationUser> UserManager @inject UserManager<ApplicationUser> UserManager
@inject AuthenticationStateProvider AuthenticationStateProvider @inject AuthenticationStateProvider AuthenticationStateProvider
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject ISnackbar Snackbar
<style>
.drop-zone-hover-overlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.45);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity .2s ease;
border-radius: 6px;
}
.drop-zone-wrapper:hover .drop-zone-hover-overlay {
opacity: 1;
}
</style>
<MudDialog> <MudDialog>
<TitleContent> <TitleContent>
<MudText Typo="Typo.h6"> <MudText Typo="Typo.h6">Add Global Entity</MudText>
Add Global Entity
</MudText>
</TitleContent> </TitleContent>
<DialogContent>
<EditForm Model="@newGlobalEntity" OnSubmit="CreateNewGlobalEntity">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="mb-3"> <DialogContent>
<label for="Title" class="form-label">Title</label> <EditForm EditContext="_editContext" OnSubmit="HandleSubmit">
<InputText id="Title" class="form-control" @bind-Value="newGlobalEntity.Title"></InputText> <DataAnnotationsValidator />
<ValidationMessage For="@(() => newGlobalEntity.Title)"></ValidationMessage>
</div> <MudGrid Style="min-height: 400px;">
<div class="mb-3"> <MudItem xs="5" Style="display: flex;">
<label for="Type" class="form-label">Type</label> <MudFileUpload T="IBrowserFile" @ref="_fileUpload" FilesChanged="OnPictureUploaded" Accept="image/*" Style="width: 100%; display: flex;">
<select id="Type" class="form-select" @bind="SelectedMediaTypeId"> <ActivatorContent>
@foreach(MediaType Type in MediaTypes) <div @onclick="OpenFilePicker"
{ @ondragenter="@(() => _isDragOver = true)"
<option value="@Type.Id">@Type.Name</option> @ondragleave="@(() => _isDragOver = false)"
} @ondragover:preventDefault="true"
</select> @ondrop:preventDefault="true"
</div> @ondrop="@(() => _isDragOver = false)"
@*<div class="mb-3"> style="@GetDropZoneStyle()"
<label for="Picture" class="form-label">Picture</label> class="d-flex align-center justify-center cursor-pointer drop-zone-wrapper">
<InputFile OnChange="@LoadImage" accept="image/*" />
@if (!string.IsNullOrEmpty(ImageString)) @if (_imagePreviewUrl is not null)
{ {
<img src="@ImageString" style="max-width: 256px; max-height: 256px;" /> <div style="position: relative; width: 100%; height: 100%;">
} <img src="@_imagePreviewUrl"
</div>*@ alt="Preview"
<div class="mb-3"> style="width: 100%; height: 100%; object-fit: cover; border-radius: 6px; display: block;" />
<label for="Genres" class="form-label">Genres</label> <div class="drop-zone-hover-overlay">
@foreach(Genre Genre in Genres) <MudStack AlignItems="AlignItems.Center" Spacing="1">
{ <MudIcon Icon="@Icons.Material.Filled.Edit"
<button class="btn-primary btn me-2" value="@Genre.Id" type="button" @onclick="() => AddGenreIdToGenreIdList(Genre.Id)">@Genre.Name</button> Color="Color.Surface"
} Size="Size.Large" />
</div> <MudText Typo="Typo.caption" Style="color: white;">
<div class="mb-3"> Replace Image
<InputCheckbox DisplayName="isPrivate" @bind-Value="IsPrivate" ></InputCheckbox> </MudText>
</div> </MudStack>
<button type="submit" class="btn-success btn">Create new Global Entity</button> </div>
</div>
}
else
{
<MudStack AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.CloudUpload"
Size="Size.Large"
Color="@(_isDragOver ? Color.Primary : Color.Secondary)" />
<MudText Typo="Typo.body2"
Align="Align.Center"
Color="@(_isDragOver ? Color.Primary : Color.Secondary)">
Bild hierher ziehen<br />oder klicken zum Auswählen
</MudText>
</MudStack>
}
</div>
</ActivatorContent>
</MudFileUpload>
</MudItem>
<!-- ═══ Rechte Spalte: Formular ═══ -->
<MudItem xs="7">
<MudStack Spacing="3">
<MudTextField @bind-Value="_form.Title"
Label="Title"
For="@(() => _form.Title)"
Variant="Variant.Outlined" />
<MudSelect T="int"
@bind-Value="_selectedMediaTypeId"
Label="Type"
Variant="Variant.Outlined">
@foreach (var type in _mediaTypes)
{
<MudSelectItem T="int" Value="@type.Id">@type.Name</MudSelectItem>
}
</MudSelect>
<MudSelect T="int"
Label="Genres"
MultiSelection="true"
@bind-SelectedValues="_selectedGenreIds"
Variant="Variant.Outlined">
@foreach (var genre in _genres)
{
<MudSelectItem T="int" Value="@genre.Id">@genre.Name</MudSelectItem>
}
</MudSelect>
<MudCheckBox @bind-Value="_isPrivate" Label="Is Private" Color="Color.Primary" />
<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Success" Disabled="_isSaving" Class="mt-3">
@if (_isSaving)
{
<MudProgressCircular Indeterminate="true" Size="Size.Small" Class="mr-2" />
<span>Saving...</span>
}
else
{
<span>Create Global Entity</span>
}
</MudButton>
</MudStack>
</MudItem>
</MudGrid>
</EditForm> </EditForm>
</DialogContent> </DialogContent>
@*Dialog Footer*@
<DialogActions> <DialogActions>
<MudButton OnClick="Cancel">Cancel</MudButton> <MudButton OnClick="Cancel" Variant="Variant.Text" Disabled="_isSaving">Cancel</MudButton>
</DialogActions> </DialogActions>
</MudDialog> </MudDialog>
@code { @code {
private ApplicationUser AppUser = null!;
[CascadingParameter] [CascadingParameter]
private IMudDialogInstance MudDialog { get; set; } private IMudDialogInstance MudDialog { get; set; } = null!;
private void Cancel() => MudDialog.Cancel();
private ApplicationUser? _appUser;
private EditContext _editContext = null!;
private List<MediaType> MediaTypes = new(); private List<MediaType> _mediaTypes = new();
private List<Genre> Genres = new(); private List<Genre> _genres = new();
#region ForNewGlobalEntity private GlobalEntityFormModel _form = new();
private GlobalEntity newGlobalEntity = new(); private MudFileUpload<IBrowserFile>? _fileUpload;
private IBrowserFile? Picture; private IBrowserFile? _picture;
private int SelectedMediaTypeId; private string? _imagePreviewUrl;
private bool IsPrivate; private bool _isDragOver;
private List<int> GenreIds = new (); private int _selectedMediaTypeId;
#endregion private bool _isPrivate;
private bool _isSaving;
private IEnumerable<int> _selectedGenreIds = new HashSet<int>();
private class GlobalEntityFormModel
{
[Required(ErrorMessage = "Title is required.")]
[MaxLength(200, ErrorMessage = "Title may not exceed 200 characters.")]
public string Title { get; set; } = string.Empty;
}
private string GetDropZoneStyle() =>
"width: 100%; height: 100%; min-height: 380px; border-radius: 6px; " +
$"border: 2px dashed {(_isDragOver ? "var(--mud-palette-primary)" : "var(--mud-palette-lines-default)")}; " +
$"background-color: {(_isDragOver ? "var(--mud-palette-primary-hover)" : "transparent")}; " +
"transition: border-color .2s ease, background-color .2s ease;";
private Task OpenFilePicker() =>
_fileUpload?.OpenFilePickerAsync() ?? Task.CompletedTask;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
var AuthState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); _editContext = new EditContext(_form);
var User = AuthState.User;
AppUser = (await UserManager.GetUserAsync(User))!;
if(AppUser == null) var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
_appUser = await UserManager.GetUserAsync(authState.User);
if (_appUser is null)
{ {
throw new Exception("Not Authenticated"); Snackbar.Add("Not authenticated.", Severity.Error);
MudDialog.Cancel();
return;
} }
MediaTypes = await CouchLogDB.MediaType.OrderBy(Type => Type.Id).ToListAsync(); _mediaTypes = await CouchLogDB.MediaType.OrderBy(t => t.Id).ToListAsync();
Genres = await CouchLogDB.Genres.OrderBy(Type => Type.Id).ToListAsync(); _genres = await CouchLogDB.Genres.OrderBy(t => t.Id).ToListAsync();
if (_mediaTypes.Count > 0)
_selectedMediaTypeId = _mediaTypes.First().Id;
} }
private async Task CreateNewGlobalEntity() private async Task OnPictureUploaded(IBrowserFile file)
{ {
if (Picture is null) _picture = file;
{
throw new InvalidOperationException("No Picture selected.");
}
if (AppUser is null) // Base64-Vorschau generieren (max. 5 MB)
{ const long maxPreviewSize = 5 * 1024 * 1024;
throw new InvalidOperationException("User not loaded or not logged in."); await using var stream = file.OpenReadStream(maxPreviewSize);
} using var ms = new MemoryStream();
await stream.CopyToAsync(ms);
//Save Picture and Name it var base64 = Convert.ToBase64String(ms.ToArray());
string NewFileName = $"{newGlobalEntity.Title.Replace(" ", "-")}-{Guid.NewGuid()}{Path.GetExtension(Picture.Name)}"; _imagePreviewUrl = $"data:{file.ContentType};base64,{base64}";
string PicturePath = Path.Combine("wwwroot", "Pictures", NewFileName);
using FileStream FileStream = File.Create(PicturePath);
await Picture.OpenReadStream().CopyToAsync(FileStream);
//await Picture.OpenReadStream(maxAllowedSize: 10_000_000).CopyToAsync(FileStream);
newGlobalEntity.PicturePath = $"Pictures/{NewFileName}";
newGlobalEntity.CreatorId = AppUser.Id;
newGlobalEntity.TypeId = SelectedMediaTypeId;
newGlobalEntity.CreationTime = DateTime.Now;
newGlobalEntity.IsPrivate = IsPrivate;
CouchLogDB.Add(newGlobalEntity);
await CouchLogDB.SaveChangesAsync();
foreach(int GenreId in GenreIds)
{
LinkTableGlobalGenre LinkTableGlobalGenre = new() { GenreId = GenreId, GlobalEntityId = newGlobalEntity.Id };
CouchLogDB.Add(LinkTableGlobalGenre);
}
await CouchLogDB.SaveChangesAsync();
NavigationManager.NavigateTo(NavigationManager.Uri, true);
} }
private void AddGenreIdToGenreIdList(int GenreId) private void Cancel() => MudDialog.Cancel();
private async Task HandleSubmit(EditContext ctx)
{ {
if (!GenreIds.Contains(GenreId)) if (!ctx.Validate()) return;
if (_picture is null)
{ {
GenreIds.Add(GenreId); Snackbar.Add("Please upload a picture.", Severity.Warning);
return;
}
if (_selectedMediaTypeId == 0)
{
Snackbar.Add("Please select a media type.", Severity.Warning);
return;
}
_isSaving = true;
try
{
string safeTitle = _form.Title.Replace(" ", "-");
string fileName = $"{safeTitle}-{Guid.NewGuid()}{Path.GetExtension(_picture.Name)}";
string picturePath = Path.Combine("wwwroot", "Pictures", fileName);
await using (var fs = File.Create(picturePath))
{
await _picture.OpenReadStream(maxAllowedSize: 10_000_000).CopyToAsync(fs);
}
var entity = new GlobalEntity
{
Title = _form.Title,
PicturePath = $"Pictures/{fileName}",
CreatorId = _appUser!.Id,
TypeId = _selectedMediaTypeId,
CreationTime = DateTime.UtcNow,
IsPrivate = _isPrivate
};
CouchLogDB.Add(entity);
await CouchLogDB.SaveChangesAsync();
foreach (int genreId in _selectedGenreIds)
{
CouchLogDB.Add(new LinkTableGlobalGenre
{
GenreId = genreId,
GlobalEntityId = entity.Id
});
}
await CouchLogDB.SaveChangesAsync();
Snackbar.Add("Global Entity created!", Severity.Success);
MudDialog.Close(DialogResult.Ok(entity.Id));
}
catch (Exception ex)
{
Snackbar.Add($"Error: {ex.Message}", Severity.Error);
}
finally
{
_isSaving = false;
} }
} }
} }