gif fullscreen v1

This commit is contained in:
beo3000 2026-01-11 11:59:44 +01:00
parent c4dcd2af13
commit c65b006bb6
4 changed files with 491 additions and 52 deletions

View File

@ -22,8 +22,8 @@
Label="Ziel-Punkte"
Variant="Variant.Outlined"
Min="12"
Max="50"
HelperText="Vorsprung bei dem Fuchs entkommen erfolgreich ist (12-50)" />
Max="60"
HelperText="Vorsprung bei dem Fuchs entkommen erfolgreich ist (12-60)" />
</MudStack>
@ -76,7 +76,7 @@
{
public int LeadingThrows { get; set; } = 2;
public int PinCountGoal { get; set; } = 21;
public int PinCountGoal { get; set; } = 41;
public ParticipantsMode ParticipantsMode { get; set; } = ParticipantsMode.GameLogic;
}

View File

@ -1,23 +1,23 @@
@inherits Fluxor.Blazor.Web.Components.FluxorComponent
@using Fluxor
@using Koogle.Application.DTOs
@using Koogle.Application.Interfaces
@using Koogle.Domain.Enums
@using Koogle.Web.Store.GifState
@inject IClubTerminologyService Terms
@inject IState<GifPlaybackState> GifState
@inject IDispatcher Dispatcher
@if (GifState.Value.IsPlaying && GifState.Value.CurrentGif != null)
{
<MudOverlay Visible="true" DarkBackground="true" ZIndex="9999" OnClick="DismissGif">
<MudOverlay Visible="true" DarkBackground="true" ZIndex="9999" OnClick="DismissGif" Class="fullscreen-overlay">
<div class="gif-player-container" @onclick:stopPropagation="true">
@* Video/Image Bereich: Nimmt den verfügbaren Platz ein *@
<div class="gif-content">
@if (IsVideo)
{
<video autoplay muted loop class="gif-display" @ref="_videoElement">
<video autoplay muted loop playsinline class="gif-display" @ref="_videoElement">
<source src="@GifState.Value.CurrentGif.Url" type="@GifState.Value.CurrentGif.ContentType" />
</video>
}
@ -25,17 +25,19 @@
{
<img src="@GifState.Value.CurrentGif.Url" class="gif-display" alt="@GifState.Value.CurrentGif.Name" />
}
@* Overlay-Texte direkt auf dem Medium für maximale Platzersparnis *@
<div class="gif-overlay-text">
<MudText Typo="Typo.h2" Class="gif-event-name">
@GetEventDisplayName(GifState.Value.TriggerEvent)
</MudText>
<MudText Typo="Typo.h6" Class="gif-name">
@GifState.Value.CurrentGif.Name
</MudText>
</div>
</div>
<div class="gif-info">
<MudText Typo="Typo.h3" Class="gif-event-name">
@GetEventDisplayName(GifState.Value.TriggerEvent)
</MudText>
<MudText Typo="Typo.subtitle1" Class="gif-name">
@GifState.Value.CurrentGif.Name
</MudText>
</div>
@* Controls *@
<MudIconButton Icon="@Icons.Material.Filled.Close"
Color="Color.Default"
Size="Size.Large"
@ -45,14 +47,8 @@
@if (ShowRating)
{
<div class="gif-rating">
<MudIconButton Icon="@Icons.Material.Filled.ThumbUp"
Color="Color.Success"
Size="Size.Medium"
OnClick="@(() => RateGif(1))" />
<MudIconButton Icon="@Icons.Material.Filled.ThumbDown"
Color="Color.Error"
Size="Size.Medium"
OnClick="@(() => RateGif(-1))" />
<MudIconButton Icon="@Icons.Material.Filled.ThumbUp" Color="Color.Success" OnClick="@(() => RateGif(1))" />
<MudIconButton Icon="@Icons.Material.Filled.ThumbDown" Color="Color.Error" OnClick="@(() => RateGif(-1))" />
</div>
}
</div>
@ -60,64 +56,86 @@
}
<style>
.gif-player-container {
position: relative;
display: flex;
flex-direction: column;
/* Das Overlay muss den gesamten Screen füllen */
.fullscreen-overlay {
display: flex !important;
align-items: center;
justify-content: center;
max-width: 90vw;
max-height: 90vh;
padding: 0 !important;
}
.gif-player-container {
position: relative;
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
overflow: hidden;
}
.gif-content {
position: relative;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
/* Wichtig: object-fit: contain sorgt für Beibehaltung des Seitenverhältnisses */
.gif-display {
max-width: 80vw;
max-height: 60vh;
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
width: 100%;
height: 100%;
object-fit: contain;
max-width: 100vw;
max-height: 100vh;
}
.gif-info {
margin-top: 16px;
/* Texte über das Video legen, um keinen Platz zu verschwenden */
.gif-overlay-text {
position: absolute;
bottom: 10%;
left: 0;
right: 0;
text-align: center;
color: white;
pointer-events: none; /* Klicks gehen durch auf das Video/Overlay */
padding: 20px;
background: linear-gradient(transparent, rgba(0,0,0,0.7));
}
.gif-event-name {
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
margin-bottom: 8px;
font-weight: 900;
text-shadow: 0 0 20px rgba(0,0,0,1), 2px 2px 5px rgba(0,0,0,0.8);
color: white;
text-transform: uppercase;
}
.gif-name {
opacity: 0.8;
color: rgba(255, 255, 255, 0.7);
text-shadow: 1px 1px 3px rgba(0,0,0,0.8);
}
.gif-close-button {
position: absolute;
top: -40px;
right: -40px;
background: rgba(255, 255, 255, 0.2);
top: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.4) !important;
color: white !important;
}
.gif-close-button:hover {
background: rgba(255, 255, 255, 0.3);
z-index: 100;
}
.gif-rating {
position: absolute;
bottom: -60px;
bottom: 20px;
right: 20px;
display: flex;
gap: 16px;
background: rgba(0, 0, 0, 0.5);
padding: 8px 16px;
border-radius: 24px;
gap: 10px;
background: rgba(0, 0, 0, 0.6);
padding: 4px 12px;
border-radius: 50px;
z-index: 100;
}
</style>

View File

@ -0,0 +1,220 @@
@inherits Fluxor.Blazor.Web.Components.FluxorComponent
@using Fluxor
@using Koogle.Application.DTOs
@using Koogle.Application.Interfaces
@using Koogle.Domain.Enums
@using Koogle.Web.Store.GifState
@inject IClubTerminologyService Terms
@inject IState<GifPlaybackState> GifState
@inject IDispatcher Dispatcher
@if (GifState.Value.IsPlaying && GifState.Value.CurrentGif != null)
{
<MudOverlay Visible="true" DarkBackground="true" ZIndex="9999" OnClick="DismissGif">
<!-- Vollbild-Container, Klicks innerhalb nicht zum Overlay propagieren -->
<div class="gif-player-container" @onclick:stopPropagation="true">
<div class="gif-content">
@if (IsVideo)
{
<video autoplay muted loop playsinline class="gif-display" @ref="_videoElement">
<source src="@GifState.Value.CurrentGif.Url" type="@GifState.Value.CurrentGif.ContentType" />
</video>
}
else
{
<img src="@GifState.Value.CurrentGif.Url" class="gif-display" alt="@GifState.Value.CurrentGif.Name" />
}
</div>
<div class="gif-info">
<MudText Typo="Typo.h3" Class="gif-event-name">
@GetEventDisplayName(GifState.Value.TriggerEvent)
</MudText>
<MudText Typo="Typo.subtitle1" Class="gif-name">
@GifState.Value.CurrentGif.Name
</MudText>
</div>
<MudIconButton Icon="@Icons.Material.Filled.Close"
Color="Color.Default"
Size="Size.Large"
Class="gif-close-button"
OnClick="DismissGif" />
@if (ShowRating)
{
<div class="gif-rating">
<MudIconButton Icon="@Icons.Material.Filled.ThumbUp"
Color="Color.Success"
Size="Size.Medium"
OnClick="@(() => RateGif(1))" />
<MudIconButton Icon="@Icons.Material.Filled.ThumbDown"
Color="Color.Error"
Size="Size.Medium"
OnClick="@(() => RateGif(-1))" />
</div>
}
</div>
</MudOverlay>
}
<style>
/* Vollbild-Layout */
.gif-player-container {
position: fixed; /* unabhängig vom Dokumentfluss: immer viewportfüllend */
inset: 0; /* top/right/bottom/left = 0 */
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
/* Abstand zu Rändern und Notch berücksichtigen */
padding: calc(16px + env(safe-area-inset-top)) calc(16px + env(safe-area-inset-right)) calc(24px + env(safe-area-inset-bottom)) calc(16px + env(safe-area-inset-left));
width: 100vw;
height: 100vh;
box-sizing: border-box;
}
.gif-content {
/* Fläche, in der das Medium maximal wachsen darf */
flex: 1 1 auto;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
min-height: 0; /* verhindert Flex-Überlauf in manchen Browsern */
}
.gif-display {
/* Maximale Größe ohne Verzerrung; Seitenverhältnis bleibt erhalten */
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
object-fit: contain; /* entscheidend für korrekte Skalierung */
border-radius: 12px;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.65);
background: #000; /* falls Letterboxing, wirkt das sauberer */
}
.gif-info {
margin-top: 12px;
text-align: center;
color: white;
/* leichte Lesbarkeit über Video/GIF */
text-shadow: 0 2px 6px rgba(0, 0, 0, 0.75);
}
.gif-event-name {
margin-bottom: 6px;
font-weight: 700;
}
.gif-name {
opacity: 0.9;
}
/* Schließen-Button: im Viewport oben rechts, sicherer Abstand */
.gif-close-button {
position: fixed;
top: calc(16px + env(safe-area-inset-top));
right: calc(16px + env(safe-area-inset-right));
background: rgba(255, 255, 255, 0.18);
color: white !important;
backdrop-filter: blur(6px);
border-radius: 999px;
}
.gif-close-button:hover {
background: rgba(255, 255, 255, 0.28);
}
/* Rating: unten mittig, gut erreichbar */
.gif-rating {
position: fixed;
bottom: calc(24px + env(safe-area-inset-bottom));
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 16px;
background: rgba(0, 0, 0, 0.55);
padding: 10px 18px;
border-radius: 28px;
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.35);
}
/* Optional: bei sehr kleinen Höhen etwas enger layouten */
@@media (max-height: 480px) {
.gif-info
{
display: none;
}
/* Platz sparen */
.gif-display {
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0,0,0,.6);
}
}
</style>
@code {
private ElementReference _videoElement;
[Parameter]
public bool ShowRating { get; set; } = true;
[Parameter]
public Guid? UserProfileId { get; set; }
private bool IsVideo => GifState.Value.CurrentGif?.ContentType.StartsWith("video/") ?? false;
private void DismissGif()
{
Dispatcher.Dispatch(new EndGifPlaybackAction());
}
private void RateGif(int value)
{
if (UserProfileId.HasValue && GifState.Value.CurrentGif != null)
{
Dispatcher.Dispatch(new RateCurrentGifAction(UserProfileId.Value, value));
}
DismissGif();
}
private string _termNoWood = "PUDEL!";
private string _termGutter = "RINNE!";
private string _termBell = "GLOCKE!";
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
var noWood = await Terms.GetTermAsync(TermKey.NoWood);
_termNoWood = $"{noWood.ToUpper()}!";
var gutter = await Terms.GetTermAsync(TermKey.Gutter);
_termGutter = $"{gutter.ToUpper()}!";
var bell = await Terms.GetTermAsync(TermKey.Gutter);
_termBell = $"{bell.ToUpper()}!";
StateHasChanged();
}
}
private string GetEventDisplayName(ThrowEventType? eventType) => eventType switch
{
ThrowEventType.Strike => "ALLE NEUNE!",
ThrowEventType.Circle => "KRANZ!",
ThrowEventType.Bell => _termBell,
ThrowEventType.Gutter => _termGutter,
ThrowEventType.NoWood => _termNoWood,
ThrowEventType.Cleared => "ABGERÄUMT!",
_ => ""
};
}

View File

@ -0,0 +1,201 @@
@inherits Fluxor.Blazor.Web.Components.FluxorComponent
@using Fluxor
@using Koogle.Application.DTOs
@using Koogle.Application.Interfaces
@using Koogle.Domain.Enums
@using Koogle.Web.Store.GifState
@inject IClubTerminologyService Terms
@inject IState<GifPlaybackState> GifState
@inject IDispatcher Dispatcher
@if (GifState.Value.IsPlaying && GifState.Value.CurrentGif != null)
{
<MudOverlay Visible="true" DarkBackground="true" ZIndex="9999" OnClick="DismissGif" Class="fullscreen-overlay">
<div class="gif-player-container" @onclick:stopPropagation="true">
@* Video/Image Bereich: Nimmt den verfügbaren Platz ein *@
<div class="gif-content">
@if (IsVideo)
{
<video autoplay muted loop playsinline class="gif-display" @ref="_videoElement">
<source src="@GifState.Value.CurrentGif.Url" type="@GifState.Value.CurrentGif.ContentType" />
</video>
}
else
{
<img src="@GifState.Value.CurrentGif.Url" class="gif-display" alt="@GifState.Value.CurrentGif.Name" />
}
@* Overlay-Texte direkt auf dem Medium für maximale Platzersparnis *@
<div class="gif-overlay-text">
<MudText Typo="Typo.h2" Class="gif-event-name">
@GetEventDisplayName(GifState.Value.TriggerEvent)
</MudText>
<MudText Typo="Typo.h6" Class="gif-name">
@GifState.Value.CurrentGif.Name
</MudText>
</div>
</div>
@* Controls *@
<MudIconButton Icon="@Icons.Material.Filled.Close"
Color="Color.Default"
Size="Size.Large"
Class="gif-close-button"
OnClick="DismissGif" />
@if (ShowRating)
{
<div class="gif-rating">
<MudIconButton Icon="@Icons.Material.Filled.ThumbUp" Color="Color.Success" OnClick="@(() => RateGif(1))" />
<MudIconButton Icon="@Icons.Material.Filled.ThumbDown" Color="Color.Error" OnClick="@(() => RateGif(-1))" />
</div>
}
</div>
</MudOverlay>
}
<style>
/* Das Overlay muss den gesamten Screen füllen */
.fullscreen-overlay {
display: flex !important;
align-items: center;
justify-content: center;
padding: 0 !important;
}
.gif-player-container {
position: relative;
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
overflow: hidden;
}
.gif-content {
position: relative;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
/* Wichtig: object-fit: contain sorgt für Beibehaltung des Seitenverhältnisses */
.gif-display {
width: 100%;
height: 100%;
object-fit: contain;
max-width: 100vw;
max-height: 100vh;
}
/* Texte über das Video legen, um keinen Platz zu verschwenden */
.gif-overlay-text {
position: absolute;
bottom: 10%;
left: 0;
right: 0;
text-align: center;
pointer-events: none; /* Klicks gehen durch auf das Video/Overlay */
padding: 20px;
background: linear-gradient(transparent, rgba(0,0,0,0.7));
}
.gif-event-name {
font-weight: 900;
text-shadow: 0 0 20px rgba(0,0,0,1), 2px 2px 5px rgba(0,0,0,0.8);
color: white;
text-transform: uppercase;
}
.gif-name {
color: rgba(255, 255, 255, 0.7);
text-shadow: 1px 1px 3px rgba(0,0,0,0.8);
}
.gif-close-button {
position: absolute;
top: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.4) !important;
color: white !important;
z-index: 100;
}
.gif-rating {
position: absolute;
bottom: 20px;
right: 20px;
display: flex;
gap: 10px;
background: rgba(0, 0, 0, 0.6);
padding: 4px 12px;
border-radius: 50px;
z-index: 100;
}
</style>
@code {
private ElementReference _videoElement;
[Parameter]
public bool ShowRating { get; set; } = true;
[Parameter]
public Guid? UserProfileId { get; set; }
private bool IsVideo => GifState.Value.CurrentGif?.ContentType.StartsWith("video/") ?? false;
private void DismissGif()
{
Dispatcher.Dispatch(new EndGifPlaybackAction());
}
private void RateGif(int value)
{
if (UserProfileId.HasValue && GifState.Value.CurrentGif != null)
{
Dispatcher.Dispatch(new RateCurrentGifAction(UserProfileId.Value, value));
}
DismissGif();
}
private string _termNoWood = "PUDEL!";
private string _termGutter = "RINNE!";
private string _termBell = "GLOCKE!";
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
var noWood = await Terms.GetTermAsync(TermKey.NoWood);
_termNoWood = $"{noWood.ToUpper()}!";
var gutter = await Terms.GetTermAsync(TermKey.Gutter);
_termGutter = $"{gutter.ToUpper()}!";
var bell = await Terms.GetTermAsync(TermKey.Gutter);
_termBell = $"{bell.ToUpper()}!";
StateHasChanged();
}
}
private string GetEventDisplayName(ThrowEventType? eventType) => eventType switch
{
ThrowEventType.Strike => "ALLE NEUNE!",
ThrowEventType.Circle => "KRANZ!",
ThrowEventType.Bell => _termBell,
ThrowEventType.Gutter => _termGutter,
ThrowEventType.NoWood => _termNoWood,
ThrowEventType.Cleared => "ABGERÄUMT!",
_ => ""
};
}