Some checks failed
Deploy Web Apps / deploy (push) Successful in 5m52s
Build Android APK / build-android-apk (push) Failing after 23m15s
Queue Release Build / prepare (push) Successful in 1m42s
Queue Release Build / build-linux (push) Failing after 9m33s
Queue Release Build / build-windows (push) Successful in 26m5s
Queue Release Build / finalize (push) Has been skipped
373 lines
16 KiB
HTML
373 lines
16 KiB
HTML
<!-- eslint-disable @angular-eslint/template/cyclomatic-complexity -->
|
|
<div class="absolute inset-0">
|
|
@if (showExpanded()) {
|
|
<section
|
|
appThemeNode="voiceWorkspace"
|
|
class="pointer-events-auto absolute inset-0 bg-background/95 backdrop-blur-xl"
|
|
(mouseenter)="onWorkspacePointerMove()"
|
|
(mousemove)="onWorkspacePointerMove()"
|
|
>
|
|
<div class="flex h-full min-h-0 flex-col">
|
|
<div class="relative flex-1 min-h-0 overflow-hidden">
|
|
<div
|
|
class="pointer-events-none absolute inset-x-3 top-3 z-10 transition-all duration-300 sm:inset-x-4 sm:top-4"
|
|
[class.opacity-0]="!showWorkspaceHeader()"
|
|
[class.translate-y-[-12px]]="!showWorkspaceHeader()"
|
|
>
|
|
<div
|
|
class="pointer-events-auto flex flex-wrap items-center gap-3 rounded-2xl border border-white/10 bg-black/45 px-4 py-3 backdrop-blur-lg"
|
|
[class.pointer-events-none]="!showWorkspaceHeader()"
|
|
>
|
|
<div class="flex min-w-0 flex-1 items-center gap-3">
|
|
<div class="flex h-10 w-10 items-center justify-center rounded-2xl bg-primary/10 text-primary">
|
|
<ng-icon
|
|
name="lucideMonitor"
|
|
class="h-5 w-5"
|
|
/>
|
|
</div>
|
|
|
|
<div class="min-w-0 flex-1">
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<h2 class="truncate text-sm font-semibold text-white sm:text-base">{{ connectedVoiceChannelName() }}</h2>
|
|
<span class="rounded-full bg-primary/10 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.18em] text-primary">
|
|
{{ 'voice.workspace.streams' | translate }}
|
|
</span>
|
|
</div>
|
|
|
|
<div class="mt-1 flex flex-wrap items-center gap-2 text-xs text-white/65">
|
|
<span>{{ serverName() }}</span>
|
|
<span class="h-1 w-1 rounded-full bg-white/25"></span>
|
|
<span>{{ 'voice.workspace.inVoice' | translate: { count: connectedVoiceUsers().length } }}</span>
|
|
<span class="h-1 w-1 rounded-full bg-white/25"></span>
|
|
<span>{{ liveStreamCountLabel(liveShareCount()) }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@if (connectedVoiceUsers().length > 0) {
|
|
<div class="hidden items-center gap-2 lg:flex">
|
|
@for (participant of connectedVoiceUsers().slice(0, 4); track trackUser($index, participant)) {
|
|
<app-user-avatar
|
|
[name]="participant.displayName"
|
|
[avatarUrl]="participant.avatarUrl"
|
|
size="xs"
|
|
[ringClass]="'ring-2 ring-white/10'"
|
|
/>
|
|
}
|
|
|
|
@if (connectedVoiceUsers().length > 4) {
|
|
<div class="rounded-full bg-white/10 px-2.5 py-1 text-[11px] font-medium text-white/70">
|
|
+{{ connectedVoiceUsers().length - 4 }}
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
@if (isWidescreenMode() && widescreenShare()) {
|
|
<div class="flex min-w-0 items-center gap-2 rounded-2xl border border-white/10 bg-black/35 px-2.5 py-2 text-white/85">
|
|
<app-user-avatar
|
|
[name]="focusedShareTitle()"
|
|
[avatarUrl]="widescreenShare()!.user.avatarUrl"
|
|
size="xs"
|
|
/>
|
|
|
|
<div class="min-w-0">
|
|
<p class="truncate text-xs font-semibold text-white">{{ focusedShareTitle() }}</p>
|
|
<p class="text-[10px] uppercase tracking-[0.18em] text-white/55">
|
|
{{ widescreenShare()!.isLocal ? ('voice.workspace.localPreview' | translate) : ('voice.workspace.focusedStream' | translate) }}
|
|
</p>
|
|
</div>
|
|
|
|
@if (focusedAudioShare()) {
|
|
<div class="mx-1 hidden h-6 w-px bg-white/10 sm:block"></div>
|
|
|
|
<div class="flex items-center gap-2">
|
|
<button
|
|
type="button"
|
|
class="inline-flex h-8 w-8 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]="
|
|
focusedShareMuted() ? ('voice.workspace.unmuteStreamAudio' | translate) : ('voice.workspace.muteStreamAudio' | translate)
|
|
"
|
|
(click)="toggleFocusedShareMuted()"
|
|
>
|
|
<ng-icon
|
|
[name]="focusedShareMuted() ? 'lucideVolumeX' : 'lucideVolume2'"
|
|
class="h-3.5 w-3.5"
|
|
/>
|
|
</button>
|
|
|
|
<input
|
|
type="range"
|
|
min="0"
|
|
max="100"
|
|
[value]="focusedShareVolume()"
|
|
class="h-1.5 w-20 accent-primary sm:w-24"
|
|
(input)="updateFocusedShareVolume($event)"
|
|
/>
|
|
|
|
<span class="w-10 text-right text-[11px] text-white/65">
|
|
{{ focusedShareMuted() ? ('common.muted' | translate) : focusedShareVolume() + '%' }}
|
|
</span>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
<div class="ml-auto flex items-center gap-2">
|
|
@if (isWidescreenMode() && hasMultipleShares()) {
|
|
<button
|
|
type="button"
|
|
class="inline-flex items-center gap-2 rounded-full border border-white/10 bg-black/35 px-3 py-2 text-xs font-medium text-white/80 transition hover:bg-black/55 hover:text-white"
|
|
[title]="'voice.workspace.showAllStreams' | translate"
|
|
(click)="showAllStreams()"
|
|
>
|
|
<ng-icon
|
|
name="lucideUsers"
|
|
class="h-3.5 w-3.5"
|
|
/>
|
|
{{ 'voice.workspace.allStreams' | translate }}
|
|
</button>
|
|
}
|
|
|
|
<button
|
|
type="button"
|
|
class="inline-flex h-10 w-10 items-center justify-center rounded-full border border-white/10 bg-black/35 text-white/70 transition hover:bg-black/55 hover:text-white"
|
|
[title]="'voice.workspace.minimizeWorkspace' | translate"
|
|
(click)="minimizeWorkspace()"
|
|
>
|
|
<ng-icon
|
|
name="lucideMinimize"
|
|
class="h-4 w-4"
|
|
/>
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
class="inline-flex h-10 w-10 items-center justify-center rounded-full border border-white/10 bg-black/35 text-white/70 transition hover:bg-black/55 hover:text-white"
|
|
[title]="'voice.workspace.returnToChat' | translate"
|
|
(click)="closeWorkspace()"
|
|
>
|
|
<ng-icon
|
|
name="lucideX"
|
|
class="h-4 w-4"
|
|
/>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@if (isWidescreenMode() && thumbnailShares().length > 0) {
|
|
<div
|
|
class="pointer-events-none absolute inset-x-3 bottom-3 z-10 transition-all duration-300 sm:inset-x-4 sm:bottom-4"
|
|
[class.opacity-0]="!showWorkspaceHeader()"
|
|
[class.translate-y-[12px]]="!showWorkspaceHeader()"
|
|
>
|
|
<div
|
|
class="pointer-events-auto rounded-2xl border border-white/10 bg-black/45 p-2.5 backdrop-blur-lg"
|
|
[class.pointer-events-none]="!showWorkspaceHeader()"
|
|
>
|
|
<div class="mb-2 flex items-center justify-between px-1">
|
|
<span class="text-[10px] font-semibold uppercase tracking-[0.18em] text-white/55">{{
|
|
'voice.workspace.otherLiveStreams' | translate
|
|
}}</span>
|
|
<span class="text-[10px] text-white/40">{{ thumbnailShares().length }}</span>
|
|
</div>
|
|
|
|
<div class="flex gap-2 overflow-x-auto pb-1">
|
|
@for (share of thumbnailShares(); track trackShare($index, share)) {
|
|
<div class="h-[5.25rem] w-[9.5rem] shrink-0 sm:h-[5.5rem] sm:w-[10rem]">
|
|
<app-voice-workspace-stream-tile
|
|
[item]="share"
|
|
[mini]="true"
|
|
[focused]="false"
|
|
(focusRequested)="focusShare($event)"
|
|
/>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
<div
|
|
class="h-full min-h-0"
|
|
[ngClass]="isWidescreenMode() ? 'p-0' : 'p-3 pt-20 sm:p-4 sm:pt-24'"
|
|
>
|
|
@if (activeShares().length > 0) {
|
|
@if (isWidescreenMode() && widescreenShare()) {
|
|
<div class="h-full min-h-0">
|
|
<app-voice-workspace-stream-tile
|
|
[item]="widescreenShare()!"
|
|
[featured]="true"
|
|
[focused]="true"
|
|
[immersive]="true"
|
|
(focusRequested)="focusShare($event)"
|
|
/>
|
|
</div>
|
|
} @else {
|
|
<div
|
|
class="grid h-full min-h-0 auto-rows-[minmax(15rem,1fr)] grid-cols-1 gap-3 overflow-auto sm:grid-cols-2 sm:gap-4"
|
|
[ngClass]="{ '2xl:grid-cols-3': activeShares().length > 2 }"
|
|
>
|
|
@for (share of activeShares(); track trackShare($index, share)) {
|
|
<div class="min-h-[15rem]">
|
|
<app-voice-workspace-stream-tile
|
|
[item]="share"
|
|
[focused]="false"
|
|
(focusRequested)="focusShare($event)"
|
|
/>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
} @else {
|
|
<div class="flex h-full items-center justify-center">
|
|
<div class="w-full max-w-3xl rounded-[2rem] border border-dashed border-white/10 bg-card/60 p-8 text-center shadow-2xl sm:p-10">
|
|
<div class="mx-auto mb-5 flex h-16 w-16 items-center justify-center rounded-3xl bg-primary/10 text-primary">
|
|
<ng-icon
|
|
name="lucideMonitor"
|
|
class="h-8 w-8"
|
|
/>
|
|
</div>
|
|
|
|
<h2 class="text-2xl font-semibold text-foreground">{{ 'voice.workspace.noLiveStreams' | translate }}</h2>
|
|
<p class="mx-auto mt-3 max-w-2xl text-sm leading-6 text-muted-foreground">
|
|
{{ 'voice.workspace.noLiveStreamsDescription' | translate: { channel: connectedVoiceChannelName() } }}
|
|
</p>
|
|
|
|
@if (connectedVoiceUsers().length > 0) {
|
|
<div class="mt-6 flex flex-wrap items-center justify-center gap-3">
|
|
@for (participant of connectedVoiceUsers().slice(0, 4); track trackUser($index, participant)) {
|
|
<div class="flex items-center gap-2 rounded-full border border-white/10 bg-black/30 px-3 py-2">
|
|
<app-user-avatar
|
|
[name]="participant.displayName"
|
|
[avatarUrl]="participant.avatarUrl"
|
|
size="xs"
|
|
/>
|
|
<span class="text-sm text-foreground">{{ participant.displayName }}</span>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
<div class="mt-8 flex flex-wrap items-center justify-center gap-3 text-sm text-muted-foreground">
|
|
<span class="inline-flex items-center gap-2 rounded-full bg-secondary/70 px-4 py-2">
|
|
<ng-icon
|
|
name="lucideUsers"
|
|
class="h-4 w-4"
|
|
/>
|
|
{{ 'voice.workspace.participantsReady' | translate: { count: connectedVoiceUsers().length } }}
|
|
</span>
|
|
<button
|
|
type="button"
|
|
class="inline-flex items-center gap-2 rounded-full bg-primary px-5 py-2.5 font-medium text-primary-foreground transition hover:bg-primary/90"
|
|
[class.hidden]="isMobile()"
|
|
(click)="toggleScreenShare()"
|
|
>
|
|
<ng-icon
|
|
name="lucideMonitor"
|
|
class="h-4 w-4"
|
|
/>
|
|
{{ 'voice.workspace.startScreenShare' | translate }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
}
|
|
|
|
@if (showMiniWindow()) {
|
|
<div
|
|
appThemeNode="voiceWorkspace"
|
|
class="pointer-events-auto absolute z-20 w-[20rem] select-none overflow-hidden rounded-[1.75rem] border border-white/10 bg-card/95 shadow-2xl backdrop-blur-xl"
|
|
[style.left.px]="miniPosition().left"
|
|
[style.top.px]="miniPosition().top"
|
|
(dblclick)="restoreWorkspace()"
|
|
>
|
|
<div
|
|
class="flex cursor-move items-center gap-3 border-b border-white/10 bg-black/25 px-4 py-3"
|
|
(mousedown)="startMiniWindowDrag($event)"
|
|
>
|
|
<div class="flex h-9 w-9 items-center justify-center rounded-2xl bg-primary/10 text-primary">
|
|
<ng-icon
|
|
name="lucideMonitor"
|
|
class="h-4 w-4"
|
|
/>
|
|
</div>
|
|
|
|
<div class="min-w-0 flex-1">
|
|
<p class="truncate text-sm font-semibold text-foreground">{{ connectedVoiceChannelName() }}</p>
|
|
<p class="truncate text-xs text-muted-foreground">
|
|
{{ miniWindowStreamHint(liveShareCount()) }}
|
|
</p>
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
class="inline-flex h-8 w-8 items-center justify-center rounded-full text-muted-foreground transition hover:bg-black/30 hover:text-foreground"
|
|
[title]="'voice.workspace.expand' | translate"
|
|
(click)="restoreWorkspace()"
|
|
>
|
|
<ng-icon
|
|
name="lucideMaximize"
|
|
class="h-4 w-4"
|
|
/>
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
class="inline-flex h-8 w-8 items-center justify-center rounded-full text-muted-foreground transition hover:bg-black/30 hover:text-foreground"
|
|
[title]="'voice.workspace.close' | translate"
|
|
(click)="closeWorkspace()"
|
|
>
|
|
<ng-icon
|
|
name="lucideX"
|
|
class="h-4 w-4"
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="relative aspect-video bg-black">
|
|
@if (miniPreviewShare()) {
|
|
<video
|
|
#miniPreview
|
|
autoplay
|
|
playsinline
|
|
class="h-full w-full bg-black object-cover"
|
|
></video>
|
|
} @else {
|
|
<div class="flex h-full items-center justify-center text-muted-foreground">
|
|
<div class="text-center">
|
|
<ng-icon
|
|
name="lucideMonitor"
|
|
class="mx-auto h-8 w-8 opacity-50"
|
|
/>
|
|
<p class="mt-2 text-sm">{{ 'voice.workspace.waitingForStream' | translate }}</p>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
<div class="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/85 via-black/50 to-transparent px-4 py-3 text-white">
|
|
<p class="truncate text-sm font-semibold">
|
|
{{ miniPreviewTitle() }}
|
|
</p>
|
|
<p class="truncate text-xs text-white/75">{{ 'voice.workspace.connectedTo' | translate: { server: serverName() } }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@if (showScreenShareQualityDialog()) {
|
|
<app-screen-share-quality-dialog
|
|
[selectedQuality]="screenShareQuality()"
|
|
[includeSystemAudio]="includeSystemAudio()"
|
|
(cancelled)="onScreenShareQualityCancelled()"
|
|
(confirmed)="onScreenShareQualityConfirmed($event)"
|
|
/>
|
|
}
|
|
</div>
|