feat: enhance global favorites list and unified collection components with draggable functionality and improved layout

Entire-Checkpoint: 56d3e3141f61
This commit is contained in:
4gray committed 2026-03-26 19:07:01 +01:00
1 parent 34c2323216
commit d85ef89893
9 files changed
+91 -62

No files matched your search

@@ -1,7 +1,7 @@
<div
class="nav-list"
cdkDropList
[cdkDropListDisabled]="hasSearchTerm()"
[cdkDropListDisabled]="!draggable() || hasSearchTerm()"
(cdkDropListDropped)="onDrop($event)"
>
@if (enrichedChannels().length === 0) {
@@ -32,7 +32,7 @@
@for (ch of enrichedChannels(); track trackByUid($index, ch)) {
<app-channel-list-item
[isDraggable]="!hasSearchTerm()"
[isDraggable]="draggable() && !hasSearchTerm()"
[logo]="ch.logo"
[name]="ch.name"
[selected]="activeUid() === ch.uid"
@@ -39,6 +39,7 @@ export class GlobalFavoritesListComponent {
readonly progressTick = input<number>(0);
readonly activeUid = input<string | null>(null);
readonly searchTermInput = input('');
readonly draggable = input(true);
readonly channelSelected = output<UnifiedFavoriteChannel>();
readonly channelsReordered = output<UnifiedFavoriteChannel[]>();
@@ -1,6 +1,10 @@
@if (isLoading()) {
<div class="collection-header">
<span class="collection-title">{{ title() | translate }}</span>
<div class="collection-header-toggles">
<span class="toggle-placeholder"></span>
<span class="toggle-placeholder toggle-placeholder--scope"></span>
</div>
</div>
@if (selectedContentType() === 'live') {
<div class="skeleton-list">
@@ -43,50 +47,64 @@
<div class="collection-header">
<span class="collection-title">{{ title() | translate }}</span>
@if (showContentToggle()) {
<div class="collection-header-toggles">
@if (showContentToggle()) {
<mat-button-toggle-group
class="content-toggle"
[value]="selectedContentType()"
(change)="onContentTypeChange($event.value)"
hideSingleSelectionIndicator
>
@if (hasLive()) {
<mat-button-toggle value="live">
{{ 'PORTALS.LIVE_TV' | translate }}
</mat-button-toggle>
}
@if (hasMovies()) {
<mat-button-toggle value="movie">
{{ 'PORTALS.MOVIES' | translate }}
</mat-button-toggle>
}
@if (hasSeries()) {
<mat-button-toggle value="series">
{{ 'PORTALS.SERIES' | translate }}
</mat-button-toggle>
}
</mat-button-toggle-group>
}
<mat-button-toggle-group
class="content-toggle"
[value]="selectedContentType()"
(change)="onContentTypeChange($event.value)"
class="scope-toggle"
[value]="scope()"
(change)="onScopeChange($event.value)"
hideSingleSelectionIndicator
>
@if (hasLive()) {
<mat-button-toggle value="live">
{{ 'PORTALS.LIVE_TV' | translate }}
</mat-button-toggle>
}
@if (hasMovies()) {
<mat-button-toggle value="movie">
{{ 'PORTALS.MOVIES' | translate }}
</mat-button-toggle>
}
@if (hasSeries()) {
<mat-button-toggle value="series">
{{ 'PORTALS.SERIES' | translate }}
</mat-button-toggle>
}
<mat-button-toggle value="playlist">
<mat-icon>playlist_play</mat-icon>
<span class="toggle-label">{{
'PORTALS.THIS_PLAYLIST' | translate
}}</span>
</mat-button-toggle>
<mat-button-toggle value="all">
<mat-icon>public</mat-icon>
<span class="toggle-label">{{
'PORTALS.ALL_PLAYLISTS' | translate
}}</span>
</mat-button-toggle>
</mat-button-toggle-group>
}
<mat-button-toggle-group
class="scope-toggle"
[value]="scope()"
(change)="onScopeChange($event.value)"
hideSingleSelectionIndicator
>
<mat-button-toggle value="playlist">
<mat-icon>playlist_play</mat-icon>
<span class="toggle-label">{{
'PORTALS.THIS_PLAYLIST' | translate
}}</span>
</mat-button-toggle>
<mat-button-toggle value="all">
<mat-icon>public</mat-icon>
<span class="toggle-label">{{
'PORTALS.ALL_PLAYLISTS' | translate
}}</span>
</mat-button-toggle>
</mat-button-toggle-group>
@if (mode() === 'recent' && allItems().length > 0) {
<button
type="button"
mat-icon-button
(click)="clearAllRecent()"
[matTooltip]="'WORKSPACE.SHELL.CLEAR_RECENTLY_VIEWED_SECTION' | translate"
[attr.aria-label]="'WORKSPACE.SHELL.CLEAR_RECENTLY_VIEWED_SECTION_ARIA' | translate"
>
<mat-icon>delete_sweep</mat-icon>
</button>
}
</div>
</div>
@switch (selectedContentType()) {
@@ -39,6 +39,23 @@
margin-right: auto;
}
.collection-header-toggles {
display: flex;
align-items: center;
gap: 12px;
}
.toggle-placeholder {
height: 43px;
width: 120px;
border-radius: 18px;
background: color-mix(in srgb, var(--mat-sys-on-surface) 6%, transparent);
&--scope {
width: 200px;
}
}
.content-toggle,
.scope-toggle {
.toggle-label {
@@ -9,7 +9,9 @@ import {
signal,
} from '@angular/core';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatIconButton } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatTooltip } from '@angular/material/tooltip';
import { ActivatedRoute } from '@angular/router';
import { TranslatePipe } from '@ngx-translate/core';
import {
@@ -32,7 +34,9 @@ import { UnifiedGridTabComponent } from './unified-grid-tab.component';
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
MatButtonToggleModule,
MatIconButton,
MatIconModule,
MatTooltip,
TranslatePipe,
UnifiedGridTabComponent,
UnifiedLiveTabComponent,
@@ -151,6 +155,14 @@ export class UnifiedCollectionPageComponent implements OnInit {
);
}
async clearAllRecent(): Promise<void> {
await this.recentData.clearRecentItems(
this.scope(),
this.playlistId()
);
this.allItems.set([]);
}
async onReorder(items: UnifiedCollectionItem[]): Promise<void> {
const nonLive = this.allItems().filter(
(i) => i.contentType !== 'live'
@@ -18,6 +18,7 @@
[progressTick]="progressTick()"
[activeUid]="activeUid()"
[searchTermInput]="searchTerm()"
[draggable]="mode() === 'favorites'"
(channelSelected)="onChannelSelected($event)"
(channelsReordered)="onReorder($event)"
(favoriteToggled)="onFavoriteToggled($event)"
@@ -97,6 +98,7 @@
[progressTick]="progressTick()"
[activeUid]="activeUid()"
[searchTermInput]="searchTerm()"
[draggable]="mode() === 'favorites'"
(channelSelected)="onChannelSelected($event)"
(channelsReordered)="onReorder($event)"
(favoriteToggled)="onFavoriteToggled($event)"
@@ -101,18 +101,5 @@
<mat-icon>download</mat-icon>
</button>
}
@if (headerBulkAction(); as action) {
<span class="header-actions-divider" aria-hidden="true"></span>
<button
type="button"
mat-icon-button
(click)="onHeaderBulkActionRequested()"
[disabled]="action.disabled"
[matTooltip]="action.tooltip"
[attr.aria-label]="action.ariaLabel"
>
<mat-icon>{{ action.icon }}</mat-icon>
</button>
}
</div>
</header>
@@ -16,7 +16,6 @@ import {
PlaylistType,
} from '@iptvnator/playlist/shared/ui';
import { WorkspaceHeaderAction } from '@iptvnator/portal/shared/util';
import { WorkspaceHeaderBulkAction } from '../../services/workspace-shell.facade';
@Component({
selector: 'app-workspace-shell-header',
@@ -49,7 +48,6 @@ export class WorkspaceShellHeaderComponent {
readonly headerShortcut = input<WorkspaceHeaderAction | null>(null);
readonly isElectron = input(false);
readonly isDownloadsView = input(false);
readonly headerBulkAction = input<WorkspaceHeaderBulkAction | null>(null);
readonly searchChanged = output<string>();
readonly searchSubmitted = output<string>();
@@ -58,7 +56,6 @@ export class WorkspaceShellHeaderComponent {
readonly globalFavoritesRequested = output<void>();
readonly headerShortcutRequested = output<void>();
readonly downloadsRequested = output<void>();
readonly headerBulkActionRequested = output<void>();
readonly playlistInfoRequested = output<void>();
readonly accountInfoRequested = output<void>();
@@ -100,7 +97,4 @@ export class WorkspaceShellHeaderComponent {
this.downloadsRequested.emit();
}
onHeaderBulkActionRequested(): void {
this.headerBulkActionRequested.emit();
}
}
@@ -52,7 +52,6 @@
[headerShortcut]="facade.headerShortcut()"
[isElectron]="facade.isElectron"
[isDownloadsView]="facade.isDownloadsView()"
[headerBulkAction]="facade.headerBulkAction()"
(searchChanged)="facade.onSearchInput($event)"
(searchSubmitted)="facade.onSearchEnter($event)"
(commandPaletteRequested)="facade.openCommandPalette()"
@@ -60,7 +59,6 @@
(globalFavoritesRequested)="facade.navigateToGlobalFavorites()"
(headerShortcutRequested)="facade.runHeaderShortcut()"
(downloadsRequested)="facade.openDownloadsShortcut()"
(headerBulkActionRequested)="facade.runHeaderBulkAction()"
(playlistInfoRequested)="facade.openPlaylistInfo()"
(accountInfoRequested)="facade.openAccountInfo()"
/>