mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
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:
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)"
|
||||
/>
|
||||
@@ -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>();
|
||||
|
||||
@@ -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 {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user