diff --git a/libs/portal/downloads/feature/src/lib/downloads.component.html b/libs/portal/downloads/feature/src/lib/downloads.component.html
index 7a424505c..6b481bc0d 100644
--- a/libs/portal/downloads/feature/src/lib/downloads.component.html
+++ b/libs/portal/downloads/feature/src/lib/downloads.component.html
@@ -54,16 +54,19 @@
- }
- @if (hasDownloads() && !isWorkspaceLayout) {
-
+ @if (hasClearableDownloads()) {
+
+ }
}
@@ -96,22 +99,34 @@
[class.downloads__item--clickable]="
isItemNavigable(item)
"
+ [class.downloads__item--unavailable]="
+ !hasSourcePlaylist(item)
+ "
[attr.role]="
isItemNavigable(item) ? 'button' : null
"
[attr.tabindex]="isItemNavigable(item) ? 0 : null"
+ [matTooltip]="
+ !hasSourcePlaylist(item)
+ ? ('DOWNLOADS.SOURCE_PLAYLIST_MISSING'
+ | translate)
+ : null
+ "
(click)="openInLibrary(item)"
(keydown)="onItemCardKeydown($event, item)"
>
- @if (item.posterUrl) {
+ @if (hasPoster(item)) {
![]()
} @else {
-
movie
+
{{
+ getPosterPlaceholderIcon(item)
+ }}
}
diff --git a/libs/portal/downloads/feature/src/lib/downloads.component.scss b/libs/portal/downloads/feature/src/lib/downloads.component.scss
index 41116f809..bf994722b 100644
--- a/libs/portal/downloads/feature/src/lib/downloads.component.scss
+++ b/libs/portal/downloads/feature/src/lib/downloads.component.scss
@@ -178,21 +178,18 @@
gap: 1rem;
padding: 1rem;
min-width: 0;
- background: color-mix(
- in srgb,
- var(--mat-sys-surface-container-high) 90%,
- var(--mat-sys-surface) 10%
+ background: var(
+ --app-widget-bg,
+ var(--mat-sys-surface-container-high)
);
- border-radius: 0;
- border: 1px solid
- color-mix(
- in srgb,
- var(--mat-sys-outline-variant) 80%,
- var(--mat-sys-on-surface) 20%
- );
- box-shadow: inset 0 1px 0
- color-mix(in srgb, var(--mat-sys-on-surface) 7%, transparent);
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+ border-radius: 14px;
+ border: 1px solid var(--app-separator, var(--mat-sys-outline-variant));
+ box-shadow: var(--app-widget-shadow, none);
+ transition:
+ background 0.18s ease,
+ border-color 0.18s ease,
+ box-shadow 0.2s ease,
+ transform 0.2s ease;
&--clickable {
cursor: pointer;
@@ -204,26 +201,33 @@
}
&:hover {
- background: color-mix(
+ background: var(--app-selection-surface);
+ border-color: color-mix(
in srgb,
- var(--mat-sys-surface-container-high) 72%,
- var(--mat-sys-surface-container) 28%
+ var(--app-selection-border) 72%,
+ transparent
);
- border-color: var(--mat-sys-outline);
+ }
+
+ &--unavailable {
+ cursor: default;
}
&-poster {
grid-area: poster;
width: 64px;
height: 90px;
- border-radius: 8px;
+ border-radius: 10px;
overflow: hidden;
flex-shrink: 0;
- background: var(--mat-sys-surface-variant);
+ background: var(
+ --app-widget-header-bg,
+ var(--mat-sys-surface-container-high)
+ );
display: flex;
align-items: center;
justify-content: center;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
+ border: 1px solid var(--app-separator, var(--mat-sys-outline-variant));
img {
width: 100%;
@@ -277,7 +281,10 @@
font-weight: 500;
padding: 2px 8px;
border-radius: 12px;
- background: var(--mat-sys-surface-container-high);
+ background: var(
+ --app-widget-header-bg,
+ var(--mat-sys-surface-container-high)
+ );
mat-icon {
font-size: 1.1rem;
@@ -313,7 +320,7 @@
font-family:
'JetBrains Mono', 'SFMono-Regular', Menlo, Monaco, Consolas,
monospace;
- background: var(--mat-sys-surface-container-high);
+ background: transparent;
padding: 2px 8px;
border-radius: 12px;
}
@@ -338,6 +345,20 @@
justify-self: end;
opacity: 1;
transition: all 0.2s ease;
+
+ button {
+ width: 34px;
+ height: 34px;
+ border-radius: 10px;
+ transition:
+ background-color 0.15s ease,
+ transform 0.15s ease;
+
+ &:hover {
+ background: var(--app-selection-surface);
+ transform: translateY(-1px);
+ }
+ }
}
mat-progress-bar {
diff --git a/libs/portal/downloads/feature/src/lib/downloads.component.ts b/libs/portal/downloads/feature/src/lib/downloads.component.ts
index abbeb1da3..53125f350 100644
--- a/libs/portal/downloads/feature/src/lib/downloads.component.ts
+++ b/libs/portal/downloads/feature/src/lib/downloads.component.ts
@@ -4,15 +4,17 @@ import {
computed,
effect,
inject,
+ signal,
} from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { MatButtonModule } from '@angular/material/button';
import { MatIcon } from '@angular/material/icon';
import { MatProgressBarModule } from '@angular/material/progress-bar';
+import { MatSnackBar } from '@angular/material/snack-bar';
import { MatTooltip } from '@angular/material/tooltip';
import { ActivatedRoute, Router } from '@angular/router';
-import { TranslatePipe } from '@ngx-translate/core';
-import { ResizableDirective } from 'components';
+import { TranslatePipe, TranslateService } from '@ngx-translate/core';
+import { DialogService, ResizableDirective } from 'components';
import { firstValueFrom, map } from 'rxjs';
import {
DatabaseService,
@@ -31,6 +33,7 @@ import {
filterCollectionBucket,
} from '@iptvnator/portal/shared/util';
import { FavoritesContextService } from '@iptvnator/portal/shared/util';
+import { Playlist } from 'shared-interfaces';
type PortalSource = 'xtream' | 'stalker';
const DOWNLOAD_COLLECTION_LABELS = {
@@ -64,6 +67,9 @@ export class DownloadsComponent {
private readonly dbService = inject(DatabaseService);
private readonly playlistsService = inject(PlaylistsService);
private readonly favoritesCtx = inject(FavoritesContextService);
+ private readonly dialogService = inject(DialogService);
+ private readonly translate = inject(TranslateService);
+ private readonly snackBar = inject(MatSnackBar);
readonly downloadsService = inject(DownloadsService);
readonly isWorkspaceLayout = isWorkspaceLayoutRoute(this.route);
@@ -79,6 +85,9 @@ export class DownloadsComponent {
readonly searchTerm = queryParamSignal(this.route, 'q', (value) =>
(value ?? '').trim().toLowerCase()
);
+ readonly playlists = toSignal(this.playlistsService.getAllPlaylists(), {
+ initialValue: [] as Playlist[],
+ });
readonly scopedDownloads = computed(() => {
const playlistId = this.playlistId();
@@ -111,6 +120,7 @@ export class DownloadsComponent {
categories: this.categories,
});
readonly selectedCategoryId = this.collectionContext.selectedCategoryId;
+ readonly failedPosterKeys = signal