221 lines
8.2 KiB
HTML
221 lines
8.2 KiB
HTML
<div
|
|
#tileRoot
|
|
class="group relative flex h-full min-h-0 flex-col overflow-hidden bg-black/85 transition duration-200"
|
|
tabindex="0"
|
|
role="button"
|
|
[attr.aria-label]="mini() ? 'Focus ' + displayName() + ' stream' : 'Open ' + displayName() + ' stream in widescreen mode'"
|
|
[attr.title]="canToggleFullscreen() ? (isFullscreen() ? 'Double-click to exit fullscreen' : 'Double-click for fullscreen') : null"
|
|
[ngClass]="{
|
|
'ring-2 ring-primary/70': focused() && !immersive() && !mini() && !isFullscreen(),
|
|
'min-h-[24rem] rounded-[1.75rem] border border-white/10 shadow-2xl': featured() && !compact() && !immersive() && !mini() && !isFullscreen(),
|
|
'rounded-[1.75rem] border border-white/10 shadow-2xl': !featured() && !compact() && !immersive() && !mini() && !isFullscreen(),
|
|
'rounded-2xl border border-white/10 shadow-2xl': compact() && !immersive() && !mini() && !isFullscreen(),
|
|
'rounded-2xl border border-white/10 shadow-xl': mini() && !isFullscreen(),
|
|
'shadow-none': immersive() || isFullscreen()
|
|
}"
|
|
(click)="requestFocus()"
|
|
(dblclick)="onTileDoubleClick($event)"
|
|
(mousemove)="onTilePointerMove()"
|
|
(keydown.enter)="requestFocus()"
|
|
(keydown.space)="requestFocus(); $event.preventDefault()"
|
|
>
|
|
<video
|
|
#streamVideo
|
|
autoplay
|
|
playsinline
|
|
class="absolute inset-0 h-full w-full bg-black object-contain"
|
|
></video>
|
|
|
|
<div class="pointer-events-none absolute inset-0 bg-gradient-to-b from-black/70 via-black/10 to-black/80"></div>
|
|
|
|
@if (isFullscreen()) {
|
|
<div
|
|
class="pointer-events-none absolute inset-x-3 top-3 z-20 transition-all duration-300 sm:inset-x-4 sm:top-4"
|
|
[class.opacity-0]="!showFullscreenHeader()"
|
|
[class.translate-y-[-12px]]="!showFullscreenHeader()"
|
|
>
|
|
<div
|
|
class="pointer-events-auto flex items-center gap-3 rounded-2xl border border-white/10 bg-black/45 px-4 py-3 backdrop-blur-lg"
|
|
[class.pointer-events-none]="!showFullscreenHeader()"
|
|
>
|
|
<div class="flex min-w-0 flex-1 items-center gap-3">
|
|
<app-user-avatar
|
|
[name]="displayName()"
|
|
[avatarUrl]="item().user.avatarUrl"
|
|
size="xs"
|
|
/>
|
|
|
|
<div class="min-w-0 flex-1">
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<p class="truncate text-sm font-semibold text-white sm:text-base">{{ displayName() }}</p>
|
|
<span class="rounded-full bg-primary/10 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.18em] text-primary"> Live </span>
|
|
</div>
|
|
|
|
<p class="mt-1 text-xs text-white/60">
|
|
{{ item().isLocal ? 'Local preview in fullscreen' : 'Fullscreen stream view' }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
@if (!item().isLocal) {
|
|
<button
|
|
type="button"
|
|
class="inline-flex h-9 w-9 items-center justify-center rounded-full border border-white/10 bg-black/45 text-white/75 transition hover:bg-black/60 hover:text-white"
|
|
[title]="muted() ? 'Unmute stream audio' : 'Mute stream audio'"
|
|
(click)="toggleMuted(); $event.stopPropagation()"
|
|
>
|
|
<ng-icon
|
|
[name]="muted() ? 'lucideVolumeX' : 'lucideVolume2'"
|
|
class="h-4 w-4"
|
|
/>
|
|
</button>
|
|
}
|
|
|
|
<button
|
|
type="button"
|
|
class="inline-flex h-9 w-9 items-center justify-center rounded-full border border-white/10 bg-black/45 text-white/75 transition hover:bg-black/60 hover:text-white"
|
|
title="Exit fullscreen"
|
|
(click)="exitFullscreen($event)"
|
|
>
|
|
<ng-icon
|
|
name="lucideMinimize"
|
|
class="h-4 w-4"
|
|
/>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@if (mini()) {
|
|
<div class="absolute inset-x-0 bottom-0 p-2">
|
|
<div class="rounded-xl border border-white/10 bg-black/55 px-2.5 py-2 backdrop-blur-md">
|
|
<div class="flex items-center gap-2">
|
|
<app-user-avatar
|
|
[name]="displayName()"
|
|
[avatarUrl]="item().user.avatarUrl"
|
|
size="xs"
|
|
/>
|
|
<div class="min-w-0 flex-1">
|
|
<p class="truncate text-xs font-semibold text-white">{{ displayName() }}</p>
|
|
<p class="text-[10px] uppercase tracking-[0.16em] text-white/60">Live stream</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
} @else if (!immersive()) {
|
|
<div
|
|
class="absolute left-4 top-4 flex items-center gap-3 bg-black/50 backdrop-blur-md"
|
|
[ngClass]="compact() ? 'max-w-[calc(100%-5rem)] rounded-xl px-2.5 py-2' : 'max-w-[calc(100%-8rem)] rounded-full px-3 py-2'"
|
|
>
|
|
<app-user-avatar
|
|
[name]="displayName()"
|
|
[avatarUrl]="item().user.avatarUrl"
|
|
size="xs"
|
|
/>
|
|
<div class="min-w-0">
|
|
<p
|
|
class="truncate font-semibold text-white"
|
|
[class.text-xs]="compact()"
|
|
[class.text-sm]="!compact()"
|
|
>
|
|
{{ displayName() }}
|
|
</p>
|
|
<p
|
|
class="flex items-center gap-1 uppercase text-white/65"
|
|
[class.text-[10px]]="compact()"
|
|
[class.text-[11px]]="!compact()"
|
|
[class.tracking-[0.18em]]="compact()"
|
|
[class.tracking-[0.24em]]="!compact()"
|
|
>
|
|
<ng-icon
|
|
name="lucideMonitor"
|
|
class="h-3 w-3"
|
|
/>
|
|
Live
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="absolute right-4 top-4 flex items-center gap-2 opacity-100 transition md:opacity-0 md:group-hover:opacity-100">
|
|
<button
|
|
type="button"
|
|
class="inline-flex items-center justify-center rounded-full border border-white/15 bg-black/45 text-white/90 backdrop-blur-md transition hover:bg-black/65"
|
|
[class.h-8]="compact()"
|
|
[class.w-8]="compact()"
|
|
[class.h-10]="!compact()"
|
|
[class.w-10]="!compact()"
|
|
[title]="focused() ? 'Viewing in widescreen' : 'View in widescreen'"
|
|
(click)="requestFocus(); $event.stopPropagation()"
|
|
>
|
|
<ng-icon
|
|
name="lucideMaximize"
|
|
[class.h-3.5]="compact()"
|
|
[class.w-3.5]="compact()"
|
|
[class.h-4]="!compact()"
|
|
[class.w-4]="!compact()"
|
|
/>
|
|
</button>
|
|
|
|
@if (!item().isLocal) {
|
|
<button
|
|
type="button"
|
|
class="inline-flex items-center justify-center rounded-full border border-white/15 bg-black/45 text-white/90 backdrop-blur-md transition hover:bg-black/65"
|
|
[class.h-8]="compact()"
|
|
[class.w-8]="compact()"
|
|
[class.h-10]="!compact()"
|
|
[class.w-10]="!compact()"
|
|
[title]="muted() ? 'Unmute stream audio' : 'Mute stream audio'"
|
|
(click)="toggleMuted(); $event.stopPropagation()"
|
|
>
|
|
<ng-icon
|
|
[name]="muted() ? 'lucideVolumeX' : 'lucideVolume2'"
|
|
[class.h-3.5]="compact()"
|
|
[class.w-3.5]="compact()"
|
|
[class.h-4]="!compact()"
|
|
[class.w-4]="!compact()"
|
|
/>
|
|
</button>
|
|
}
|
|
</div>
|
|
|
|
<div class="absolute inset-x-0 bottom-0 p-4">
|
|
@if (item().isLocal) {
|
|
@if (!compact()) {
|
|
<div class="rounded-2xl bg-black/50 px-4 py-3 text-xs text-white/75 backdrop-blur-md">
|
|
Your preview stays muted locally to avoid audio feedback.
|
|
</div>
|
|
}
|
|
} @else {
|
|
@if (compact()) {
|
|
<div class="rounded-xl bg-black/50 px-3 py-2 text-[11px] text-white/80 backdrop-blur-md">
|
|
{{ muted() ? 'Muted' : volume() + '% audio' }}
|
|
</div>
|
|
} @else {
|
|
<div class="rounded-2xl bg-black/50 px-4 py-3 backdrop-blur-md">
|
|
<div class="mb-2 flex items-center justify-between text-xs text-white/80">
|
|
<span class="flex items-center gap-2 font-medium">
|
|
<ng-icon
|
|
[name]="muted() ? 'lucideVolumeX' : 'lucideVolume2'"
|
|
class="h-3.5 w-3.5"
|
|
/>
|
|
Stream audio
|
|
</span>
|
|
<span>{{ muted() ? 'Muted' : volume() + '%' }}</span>
|
|
</div>
|
|
|
|
<input
|
|
type="range"
|
|
min="0"
|
|
max="100"
|
|
[value]="volume()"
|
|
class="w-full accent-primary"
|
|
(click)="$event.stopPropagation()"
|
|
(input)="updateVolume($event)"
|
|
/>
|
|
</div>
|
|
}
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|