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

@@ -6,4 +6,5 @@
<app-recent-playlists
class="recent-playlists"
[searchQueryInput]="searchQuery()"
(addPlaylistClicked)="onAddPlaylist($event)"
/>
+16 -2
View File
@@ -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<string>('');
onSearchQueryChange(query: string): void {
this.searchQuery.set(query);
}
onAddPlaylist(playlistType: PlaylistType): void {
this.dialog.open<AddPlaylistDialogComponent, { type: PlaylistType }>(
AddPlaylistDialogComponent,
{
width: '600px',
data: { type: playlistType },
}
);
}
}
@@ -28,12 +28,15 @@
<button
mat-icon-button
class="add-playlist-btn"
[matMenuTriggerFor]="addPlaylistMenu"
[matMenuTriggerFor]="addPlaylistMenuComponent().menu()"
[matTooltip]="'HOME.URL_UPLOAD.ADD_PLAYLIST' | translate"
data-test-id="add-playlist"
>
<mat-icon>add</mat-icon>
</button>
<app-add-playlist-menu
(playlistTypeSelected)="opedAddPlaylistDialog($event)"
/>
<!-- Sort button -->
<button
mat-icon-button
@@ -194,53 +197,6 @@
}
</section>
</mat-menu>
<mat-menu #addPlaylistMenu="matMenu">
<button
mat-menu-item
[attr.aria-label]="'HOME.TABS.FILE_UPLOAD' | translate"
(click)="opedAddPlaylistDialog('file')"
>
<mat-icon>attach_file</mat-icon>
<span>{{ 'HOME.TABS.FILE_UPLOAD' | translate }}</span>
</button>
<button
mat-menu-item
[attr.aria-label]="'HOME.TABS.URL_UPLOAD' | translate"
(click)="opedAddPlaylistDialog('url')"
>
<mat-icon>link</mat-icon>
<span>{{ 'HOME.TABS.URL_UPLOAD' | translate }}</span>
</button>
<button
mat-menu-item
[attr.aria-label]="'HOME.TABS.TEXT_IMPORT' | translate"
(click)="opedAddPlaylistDialog('text')"
>
<mat-icon>text_format</mat-icon>
<span>{{ 'HOME.TABS.TEXT_IMPORT' | translate }}</span>
</button>
<mat-divider></mat-divider>
<button
mat-menu-item
[attr.aria-label]="'HOME.TABS.XTREME_IMPORT' | translate"
(click)="opedAddPlaylistDialog('xtream')"
>
<mat-icon>video_library</mat-icon>
<span>{{ 'HOME.TABS.XTREME_IMPORT' | translate }}</span>
</button>
<button
mat-menu-item
[attr.aria-label]="
'HOME.TABS.STALKER_PORTAL_IMPORT' | translate
"
(click)="opedAddPlaylistDialog('stalker')"
>
<mat-icon>dashboard</mat-icon>
<span>{{
'HOME.TABS.STALKER_PORTAL_IMPORT' | translate
}}</span>
</button>
</mat-menu>
</div>
}
</div>
@@ -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<string>();
readonly subtitle = input.required<string>();