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.
This commit is contained in:
4gray committed 2025-12-06 21:44:25 +01:00
1 parent b82d47f6b6
commit aa2d5b9508
6 files changed
+68 -82

No files matched your search

@@ -1,24 +1,28 @@
@if (allPlaylistsLoaded()) {
@defer {
@if (playlists$ | async; as playlists) {
<mat-nav-list class="playlists-list">
@if (playlists.length === 0) {
<mat-list-item>
<mat-icon matListItemIcon>search</mat-icon>
<div matListItemTitle>
{{ 'HOME.PLAYLISTS.NO_PLAYLISTS' | translate }}
</div>
<div matListItemLine class="meta">
{{ 'HOME.PLAYLISTS.ADD_FIRST' | translate }}
</div>
</mat-list-item>
@if (playlistsData$ | async; as data) {
@if (data.playlists.length === 0) {
@if (data.totalCount === 0) {
<!-- Welcome state: no playlists at all -->
<app-empty-state
[type]="'welcome'"
(addPlaylistClicked)="onAddPlaylist($event)"
/>
} @else {
<!-- No results state: search/filter returned nothing -->
<app-empty-state [type]="'no-results'" />
}
} @else {
<mat-nav-list class="playlists-list">
<mat-list-item (click)="navigateToGlobalFavorites()">
<mat-icon class="favorites-icon" matListItemIcon
>star</mat-icon
>
<div matListItemTitle>
{{ 'HOME.PLAYLISTS.GLOBAL_FAVORITES' | translate }}
{{
'HOME.PLAYLISTS.GLOBAL_FAVORITES'
| translate
}}
</div>
<div matListItemLine class="meta">
{{
@@ -27,7 +31,11 @@
}}
</div>
</mat-list-item>
@for (item of playlists; track item._id; let last = $last) {
@for (
item of data.playlists;
track item._id;
let last = $last
) {
<app-playlist-item
[item]="item"
[showActions]="!sidebarMode()"
@@ -37,8 +45,8 @@
(removeClicked)="removeClicked($event)"
/>
}
}
</mat-nav-list>
</mat-nav-list>
}
}
} @placeholder {
<div id="skeleton-container">
@@ -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<string>('');
readonly playlistClicked = output<string>();
readonly addPlaylistClicked = output<PlaylistType>();
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 {
},
});
}
}