From aa2d5b95080f5d1e1b715baf818eb5fa87c7efb1 Mon Sep 17 00:00:00 2001 From: 4gray Date: Thu, 4 Dec 2025 21:03:08 +0100 Subject: [PATCH] feat(ui): add empty-state and expose addPlaylist event Introduce explicit empty-state handling in RecentPlaylists: - Replace previous direct rendering of an empty mat-list item with app-empty-state for two cases: a global "welcome" state when no playlists exist at all, and a "no-results" state when filters/search return nothing. This clarifies UX and provides a CTA for adding playlists in the welcome state. - Change playlist data binding from playlists$ to playlistsData$ and adapt loops and accessors to data.playlists / data.totalCount. - Keep existing global favorites item and playlist-item rendering. Wire up addPlaylist event and components: - Add addPlaylistClicked output to RecentPlaylists component and forward the event from the home component so the parent can open the add-playlist flow. - Import EmptyStateComponent and PlaylistType types used by the new output. Minor cleanup and imports: - Reformat some imports and add tap to RxJS imports. - Adjust template structure and placeholders for deferred loading. --- apps/web/src/app/home/home.component.html | 1 + apps/web/src/app/home/home.component.ts | 18 ++++++- .../components/header/header.component.html | 52 ++----------------- .../components/header/header.component.ts | 11 ++-- .../recent-playlists.component.html | 40 ++++++++------ .../recent-playlists.component.ts | 28 ++++++---- 6 files changed, 68 insertions(+), 82 deletions(-) diff --git a/apps/web/src/app/home/home.component.html b/apps/web/src/app/home/home.component.html index 5566a0b05..9a86208fb 100644 --- a/apps/web/src/app/home/home.component.html +++ b/apps/web/src/app/home/home.component.html @@ -6,4 +6,5 @@ diff --git a/apps/web/src/app/home/home.component.ts b/apps/web/src/app/home/home.component.ts index e71b073dc..0244682de 100644 --- a/apps/web/src/app/home/home.component.ts +++ b/apps/web/src/app/home/home.component.ts @@ -1,6 +1,8 @@ -import { Component, signal } from '@angular/core'; +import { Component, inject, signal } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; import { TranslatePipe } from '@ngx-translate/core'; -import { RecentPlaylistsComponent } from 'components'; +import { PlaylistType, RecentPlaylistsComponent } from 'components'; +import { AddPlaylistDialogComponent } from '../shared/components/add-playlist/add-playlist-dialog.component'; import { HeaderComponent } from '../shared/components/header/header.component'; @Component({ @@ -10,9 +12,21 @@ import { HeaderComponent } from '../shared/components/header/header.component'; imports: [HeaderComponent, RecentPlaylistsComponent, TranslatePipe], }) export class HomeComponent { + private readonly dialog = inject(MatDialog); + searchQuery = signal(''); onSearchQueryChange(query: string): void { this.searchQuery.set(query); } + + onAddPlaylist(playlistType: PlaylistType): void { + this.dialog.open( + AddPlaylistDialogComponent, + { + width: '600px', + data: { type: playlistType }, + } + ); + } } diff --git a/apps/web/src/app/shared/components/header/header.component.html b/apps/web/src/app/shared/components/header/header.component.html index bf9697502..2608029e5 100644 --- a/apps/web/src/app/shared/components/header/header.component.html +++ b/apps/web/src/app/shared/components/header/header.component.html @@ -28,12 +28,15 @@ + - - - - - - } diff --git a/apps/web/src/app/shared/components/header/header.component.ts b/apps/web/src/app/shared/components/header/header.component.ts index e99af2211..9340e8504 100644 --- a/apps/web/src/app/shared/components/header/header.component.ts +++ b/apps/web/src/app/shared/components/header/header.component.ts @@ -1,4 +1,4 @@ -import { Component, HostBinding, OnInit, effect, inject, input, output } from '@angular/core'; +import { Component, HostBinding, OnInit, effect, inject, input, output, viewChild } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCheckboxModule } from '@angular/material/checkbox'; @@ -13,18 +13,17 @@ import { TranslateModule } from '@ngx-translate/core'; import { DataService, SortBy, SortOrder, SortService } from 'services'; //import { shell } from 'electron'; import { selectActiveTypeFilters, setSelectedFilters } from 'm3u-state'; +import { AddPlaylistMenuComponent, PlaylistType } from 'components'; import { HomeComponent } from '../../../home/home.component'; import { AboutDialogComponent } from '../about-dialog/about-dialog.component'; -import { - AddPlaylistDialogComponent, - PlaylistType, -} from '../add-playlist/add-playlist-dialog.component'; +import { AddPlaylistDialogComponent } from '../add-playlist/add-playlist-dialog.component'; @Component({ selector: 'app-header', templateUrl: './header.component.html', styleUrls: ['./header.component.scss'], imports: [ + AddPlaylistMenuComponent, FormsModule, MatButtonModule, MatCheckboxModule, @@ -40,6 +39,7 @@ export class HeaderComponent implements OnInit { @HostBinding('class.home-header') get isHomeHeader() { return this.isHome; } + private activatedRoute = inject(ActivatedRoute); private dialog = inject(MatDialog); private dataService = inject(DataService); @@ -47,6 +47,7 @@ export class HeaderComponent implements OnInit { private store = inject(Store); private sortService = inject(SortService); + readonly addPlaylistMenuComponent = viewChild.required(AddPlaylistMenuComponent); readonly isDesktop = !!window.electron; readonly title = input.required(); readonly subtitle = input.required(); diff --git a/libs/ui/components/src/lib/recent-playlists/recent-playlists.component.html b/libs/ui/components/src/lib/recent-playlists/recent-playlists.component.html index db9f73b0b..b087f7f7d 100644 --- a/libs/ui/components/src/lib/recent-playlists/recent-playlists.component.html +++ b/libs/ui/components/src/lib/recent-playlists/recent-playlists.component.html @@ -1,24 +1,28 @@ @if (allPlaylistsLoaded()) { @defer { - @if (playlists$ | async; as playlists) { - - @if (playlists.length === 0) { - - search -
- {{ 'HOME.PLAYLISTS.NO_PLAYLISTS' | translate }} -
-
- {{ 'HOME.PLAYLISTS.ADD_FIRST' | translate }} -
-
+ @if (playlistsData$ | async; as data) { + @if (data.playlists.length === 0) { + @if (data.totalCount === 0) { + + } @else { + + + } + } @else { + star
- {{ 'HOME.PLAYLISTS.GLOBAL_FAVORITES' | translate }} + {{ + 'HOME.PLAYLISTS.GLOBAL_FAVORITES' + | translate + }}
{{ @@ -27,7 +31,11 @@ }}
- @for (item of playlists; track item._id; let last = $last) { + @for ( + item of data.playlists; + track item._id; + let last = $last + ) { } - } -
+
+ } } } @placeholder {
diff --git a/libs/ui/components/src/lib/recent-playlists/recent-playlists.component.ts b/libs/ui/components/src/lib/recent-playlists/recent-playlists.component.ts index d856ed674..408c98a13 100644 --- a/libs/ui/components/src/lib/recent-playlists/recent-playlists.component.ts +++ b/libs/ui/components/src/lib/recent-playlists/recent-playlists.component.ts @@ -1,12 +1,6 @@ import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; import { AsyncPipe } from '@angular/common'; -import { - Component, - effect, - inject, - input, - output, -} from '@angular/core'; +import { Component, effect, inject, input, output } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { MatIcon } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; @@ -22,7 +16,7 @@ import { selectPlaylistsLoadingFlag, } from 'm3u-state'; import { NgxSkeletonLoaderComponent } from 'ngx-skeleton-loader'; -import { BehaviorSubject, combineLatest, map } from 'rxjs'; +import { BehaviorSubject, combineLatest, map, tap } from 'rxjs'; import { DatabaseService, DataService, SortService } from 'services'; import { GLOBAL_FAVORITES_PLAYLIST_ID, @@ -30,6 +24,8 @@ import { PlaylistMeta, } from 'shared-interfaces'; import { DialogService } from '../confirm-dialog/dialog.service'; +import { PlaylistType } from '../add-playlist-menu/add-playlist-menu.component'; +import { EmptyStateComponent } from './empty-state/empty-state.component'; import { PlaylistInfoComponent } from './playlist-info/playlist-info.component'; import { PlaylistItemComponent } from './playlist-item/playlist-item.component'; @@ -39,6 +35,7 @@ import { PlaylistItemComponent } from './playlist-item/playlist-item.component'; styleUrls: ['./recent-playlists.component.scss'], imports: [ AsyncPipe, + EmptyStateComponent, MatIcon, MatInputModule, MatListModule, @@ -61,6 +58,7 @@ export class RecentPlaylistsComponent { readonly sidebarMode = input(false); readonly searchQueryInput = input(''); readonly playlistClicked = output(); + readonly addPlaylistClicked = output(); readonly allPlaylistsLoaded = this.store.selectSignal( selectPlaylistsLoadingFlag @@ -77,7 +75,7 @@ export class RecentPlaylistsComponent { }); } - readonly playlists$ = combineLatest([ + readonly playlistsData$ = combineLatest([ this.store.select(selectAllPlaylistsMeta), this.searchQuery, this.store.select(selectActiveTypeFilters), @@ -111,10 +109,15 @@ export class RecentPlaylistsComponent { ); // Apply sorting using the SortService - return this.sortService.sortPlaylists( + const sortedPlaylists = this.sortService.sortPlaylists( filteredPlaylists, sortOptions ); + + return { + playlists: sortedPlaylists, + totalCount: playlists.length + }; }) ); @@ -149,6 +152,10 @@ export class RecentPlaylistsComponent { this.playlistClicked.emit(GLOBAL_FAVORITES_PLAYLIST_ID); } + onAddPlaylist(playlistType: PlaylistType) { + this.addPlaylistClicked.emit(playlistType); + } + getPlaylist(playlistMeta: PlaylistMeta): void { if (playlistMeta.serverUrl) { this.router.navigate(['xtreams', playlistMeta._id]); @@ -281,5 +288,4 @@ export class RecentPlaylistsComponent { }, }); } - }