diff --git a/ui/src/app/app.component.html b/ui/src/app/app.component.html
index 54ea2bd..764eca4 100644
--- a/ui/src/app/app.component.html
+++ b/ui/src/app/app.component.html
@@ -76,7 +76,8 @@
(clearCompleted)="clearCompletedDownloads()"
(clearFailed)="clearFailedDownloads()"
(retryFailed)="retryFailedDownloads()"
- (downloadSelected)="downloadSelectedFiles($event)">
+ (downloadSelected)="downloadSelectedFiles($event)"
+ (play)="openPlayer($event)">
@@ -146,4 +147,16 @@
+
+
+
+
+ {{ playerTitle }}
+
+
+
+
+
diff --git a/ui/src/app/app.component.sass b/ui/src/app/app.component.sass
index 50fe880..49c04d8 100644
--- a/ui/src/app/app.component.sass
+++ b/ui/src/app/app.component.sass
@@ -62,6 +62,45 @@
font-weight: 600
color: var(--fg)
+// In-page video player
+.player
+ position: fixed
+ top: 50%
+ left: 50%
+ transform: translate(-50%, -50%)
+ width: min(960px, 92vw)
+ background: var(--surface)
+ border: 1px solid var(--border)
+ border-radius: 12px
+ box-shadow: 0 24px 64px oklch(20% 0.02 240 / 0.35)
+ z-index: 50
+ overflow: hidden
+ display: flex
+ flex-direction: column
+
+.player-head
+ display: flex
+ align-items: center
+ gap: 12px
+ padding: 10px 14px
+ border-bottom: 1px solid var(--border)
+ flex: none
+
+ .player-title
+ flex: 1
+ min-width: 0
+ font-size: 14px
+ font-weight: 600
+ color: var(--fg)
+ white-space: nowrap
+ overflow: hidden
+ text-overflow: ellipsis
+
+.player-video
+ width: 100%
+ max-height: 78vh
+ background: #000
+
.icon-btn
margin-left: auto
diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts
index 600dff6..355948c 100644
--- a/ui/src/app/app.component.ts
+++ b/ui/src/app/app.component.ts
@@ -1,4 +1,4 @@
-import { Component, OnInit } from '@angular/core';
+import { Component, HostListener, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, distinctUntilChanged, map } from 'rxjs';
@@ -45,6 +45,10 @@ export class AppComponent implements OnInit {
sidebarOpen = false;
activityOpen = false;
+ // In-page video player (completed items)
+ playerUrl: string | null = null;
+ playerTitle = '';
+
// Metrics
activeDownloads = 0;
queuedDownloads = 0;
@@ -136,6 +140,23 @@ export class AppComponent implements OnInit {
this.isAdvancedOpen = open === undefined ? !this.isAdvancedOpen : open;
}
+ // ---------- In-page player ----------
+
+ openPlayer(row: KeyedDownload): void {
+ this.playerUrl = this.buildDownloadLink(row.value);
+ this.playerTitle = row.value.title;
+ }
+
+ closePlayer(): void {
+ this.playerUrl = null;
+ this.playerTitle = '';
+ }
+
+ @HostListener('document:keydown.escape')
+ onEscape(): void {
+ if (this.playerUrl) this.closePlayer();
+ }
+
// ---------- Data getters ----------
statusEntries(tab: DownloadTab): KeyedDownload[] {
diff --git a/ui/src/app/download-card.component.html b/ui/src/app/download-card.component.html
index 7aa2058..a5a3be7 100644
--- a/ui/src/app/download-card.component.html
+++ b/ui/src/app/download-card.component.html
@@ -19,7 +19,10 @@
{{ row.value.size ? (row.value.size | fileSize) : '—' }}
-
+
+
@@ -74,7 +75,8 @@
(selectChange)="toggleSelect(row.key, $event)"
(del)="del.emit([$event])"
(retry)="retry.emit($event)"
- (folderChange)="folderEdit.emit($event)">
+ (folderChange)="folderEdit.emit($event)"
+ (play)="play.emit(row)">
diff --git a/ui/src/app/download-list.component.ts b/ui/src/app/download-list.component.ts
index 434153e..c526b54 100644
--- a/ui/src/app/download-list.component.ts
+++ b/ui/src/app/download-list.component.ts
@@ -37,6 +37,7 @@ export class DownloadListComponent implements OnChanges {
@Output() clearFailed = new EventEmitter();
@Output() retryFailed = new EventEmitter();
@Output() downloadSelected = new EventEmitter();
+ @Output() play = new EventEmitter();
faList = faList;
faThLarge = faThLarge;
diff --git a/ui/src/app/download-row.component.html b/ui/src/app/download-row.component.html
index 5589e99..c97d652 100644
--- a/ui/src/app/download-row.component.html
+++ b/ui/src/app/download-row.component.html
@@ -39,7 +39,10 @@
-
+
+