mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 12:06:16 -08:00
feat: implement favorites channel sorting functionality and UI updates
Entire-Checkpoint: c6e522b4276c
This commit is contained in:
1 parent
21a9cdb0c6
commit
4c6e65be9b
10 files changed
+355
-145
No files matched your search
+2
-2
@@ -1,7 +1,7 @@
|
||||
<div
|
||||
class="nav-list"
|
||||
cdkDropList
|
||||
[cdkDropListDisabled]="!draggable() || hasSearchTerm()"
|
||||
[cdkDropListDisabled]="!canDragDrop()"
|
||||
(cdkDropListDropped)="onDrop($event)"
|
||||
>
|
||||
@if (enrichedChannels().length === 0) {
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
@for (ch of enrichedChannels(); track trackByUid($index, ch)) {
|
||||
<app-channel-list-item
|
||||
[isDraggable]="draggable() && !hasSearchTerm()"
|
||||
[isDraggable]="canDragDrop()"
|
||||
[logo]="ch.logo"
|
||||
[name]="ch.name"
|
||||
[selected]="activeUid() === ch.uid"
|
||||
|
||||
+23
-5
@@ -11,10 +11,15 @@ import {
|
||||
output,
|
||||
} from '@angular/core';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { ChannelListItemComponent } from 'components';
|
||||
import { EpgProgram } from 'shared-interfaces';
|
||||
import { UnifiedFavoriteChannel } from '@iptvnator/portal/shared/util';
|
||||
import {
|
||||
DEFAULT_FAVORITES_CHANNEL_SORT_MODE,
|
||||
FavoritesChannelSortMode,
|
||||
sortFavoriteChannelItems,
|
||||
UnifiedFavoriteChannel,
|
||||
} from '@iptvnator/portal/shared/util';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
export interface EnrichedUnifiedFavorite extends UnifiedFavoriteChannel {
|
||||
currentEpgProgram: EpgProgram | null;
|
||||
@@ -30,7 +35,7 @@ export interface EnrichedUnifiedFavorite extends UnifiedFavoriteChannel {
|
||||
ChannelListItemComponent,
|
||||
DragDropModule,
|
||||
MatIconModule,
|
||||
TranslatePipe,
|
||||
TranslateModule,
|
||||
],
|
||||
})
|
||||
export class GlobalFavoritesListComponent {
|
||||
@@ -40,11 +45,19 @@ export class GlobalFavoritesListComponent {
|
||||
readonly activeUid = input<string | null>(null);
|
||||
readonly searchTermInput = input('');
|
||||
readonly draggable = input(true);
|
||||
readonly sortMode = input<FavoritesChannelSortMode>(
|
||||
DEFAULT_FAVORITES_CHANNEL_SORT_MODE
|
||||
);
|
||||
|
||||
readonly channelSelected = output<UnifiedFavoriteChannel>();
|
||||
readonly channelsReordered = output<UnifiedFavoriteChannel[]>();
|
||||
readonly favoriteToggled = output<UnifiedFavoriteChannel>();
|
||||
|
||||
readonly isCustomSort = computed(() => this.sortMode() === 'custom');
|
||||
readonly canDragDrop = computed(
|
||||
() => this.draggable() && this.isCustomSort() && !this.hasSearchTerm()
|
||||
);
|
||||
|
||||
readonly hasSearchTerm = computed(
|
||||
() => this.searchTermInput().trim().length > 0
|
||||
);
|
||||
@@ -59,7 +72,12 @@ export class GlobalFavoritesListComponent {
|
||||
? channels.filter((ch) => ch.name.toLowerCase().includes(term))
|
||||
: channels;
|
||||
|
||||
return filtered.map((ch) => {
|
||||
const sorted = sortFavoriteChannelItems(filtered, this.sortMode(), {
|
||||
getName: (ch) => ch.name,
|
||||
getAddedAt: (ch) => ch.addedAt,
|
||||
});
|
||||
|
||||
return sorted.map((ch) => {
|
||||
const epgKey = ch.tvgId?.trim() || ch.name?.trim();
|
||||
const currentEpgProgram = epgKey
|
||||
? (epgMap.get(epgKey) ?? null)
|
||||
@@ -81,7 +99,7 @@ export class GlobalFavoritesListComponent {
|
||||
}
|
||||
|
||||
onDrop(event: CdkDragDrop<EnrichedUnifiedFavorite[]>): void {
|
||||
if (this.hasSearchTerm()) {
|
||||
if (!this.canDragDrop()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+43
-4
@@ -58,6 +58,36 @@
|
||||
<div class="collection-header">
|
||||
<span class="collection-title">{{ title() | translate }}</span>
|
||||
|
||||
@if (showFavSortButton()) {
|
||||
<button
|
||||
type="button"
|
||||
mat-icon-button
|
||||
class="favorites-sort-button"
|
||||
[matMenuTriggerFor]="favoritesSortMenu"
|
||||
[matTooltip]="
|
||||
('WORKSPACE.SORT_LABEL' | translate) +
|
||||
(favSortLabelKey() | translate)
|
||||
"
|
||||
[attr.aria-label]="'WORKSPACE.SORT_LABEL' | translate"
|
||||
>
|
||||
<mat-icon>sort_by_alpha</mat-icon>
|
||||
</button>
|
||||
<mat-menu #favoritesSortMenu="matMenu">
|
||||
@for (option of favSortOptions; track option.mode) {
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="setFavSortMode(option.mode)"
|
||||
>
|
||||
<mat-icon>{{ option.icon }}</mat-icon>
|
||||
<span>{{ option.translationKey | translate }}</span>
|
||||
@if (favSortMode() === option.mode) {
|
||||
<mat-icon class="sort-check">check</mat-icon>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
</mat-menu>
|
||||
}
|
||||
|
||||
<div class="collection-header-toggles">
|
||||
@if (showContentToggle()) {
|
||||
<mat-button-toggle-group
|
||||
@@ -106,18 +136,26 @@
|
||||
</mat-button-toggle-group>
|
||||
}
|
||||
|
||||
@if (mode() === 'recent' && allItems().length > 0) {
|
||||
@if (currentTypeItems().length > 0) {
|
||||
<button
|
||||
type="button"
|
||||
mat-icon-button
|
||||
(click)="clearAllRecent()"
|
||||
(click)="clearAllCurrent()"
|
||||
[matTooltip]="
|
||||
'WORKSPACE.SHELL.CLEAR_RECENTLY_VIEWED_SECTION'
|
||||
clearButtonTooltipKey()
|
||||
| translate
|
||||
: {
|
||||
type:
|
||||
currentTypeLabelKey() | translate,
|
||||
}
|
||||
"
|
||||
[attr.aria-label]="
|
||||
'WORKSPACE.SHELL.CLEAR_RECENTLY_VIEWED_SECTION_ARIA'
|
||||
clearButtonTooltipKey()
|
||||
| translate
|
||||
: {
|
||||
type:
|
||||
currentTypeLabelKey() | translate,
|
||||
}
|
||||
"
|
||||
>
|
||||
<mat-icon>delete_sweep</mat-icon>
|
||||
@@ -134,6 +172,7 @@
|
||||
[mode]="mode()"
|
||||
[searchTerm]="workspaceSearchTerm()"
|
||||
[autoOpenItem]="pendingAutoOpenLiveItem()"
|
||||
[sortMode]="favSortMode()"
|
||||
(removeItem)="onRemoveItem($event)"
|
||||
(reorderItems)="onReorder($event)"
|
||||
(itemPlayed)="onItemPlayed($event)"
|
||||
|
||||
+12
-1
@@ -36,13 +36,17 @@
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--mat-sys-on-surface);
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.favorites-sort-button {
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
.collection-header-toggles {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.toggle-placeholder {
|
||||
@@ -75,9 +79,16 @@ app-unified-grid-tab {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
app-unified-grid-tab {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
app-unified-live-tab {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
+111
-12
@@ -16,6 +16,7 @@ import {
|
||||
import { MatButtonToggleModule } from '@angular/material/button-toggle';
|
||||
import { MatIconButton } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
@@ -30,6 +31,8 @@ import {
|
||||
CollectionContentType,
|
||||
CollectionScope,
|
||||
CollectionViewState,
|
||||
FavoritesChannelSortMode,
|
||||
getFavoritesChannelSortModeTranslationKey,
|
||||
getOpenCollectionDetailItemState,
|
||||
getCollectionViewState,
|
||||
getOpenLiveCollectionItemState,
|
||||
@@ -37,7 +40,9 @@ import {
|
||||
isWorkspaceLayoutRoute,
|
||||
OPEN_COLLECTION_DETAIL_STATE_KEY,
|
||||
OPEN_LIVE_COLLECTION_ITEM_STATE_KEY,
|
||||
persistFavoritesChannelSortMode,
|
||||
queryParamSignal,
|
||||
restoreFavoritesChannelSortMode,
|
||||
routeParamSignal,
|
||||
ScopeToggleService,
|
||||
STALKER_RETURN_TO_STATE_KEY,
|
||||
@@ -63,6 +68,7 @@ import {
|
||||
MatButtonToggleModule,
|
||||
MatIconButton,
|
||||
MatIconModule,
|
||||
MatMenuModule,
|
||||
MatTooltip,
|
||||
TranslatePipe,
|
||||
UnifiedGridTabComponent,
|
||||
@@ -204,12 +210,79 @@ export class UnifiedCollectionPageComponent implements AfterContentInit {
|
||||
() => this.availableTypes().length > 1
|
||||
);
|
||||
|
||||
readonly currentTypeItems = computed(() => {
|
||||
switch (this.selectedContentType()) {
|
||||
case 'live':
|
||||
return this.liveItems();
|
||||
case 'movie':
|
||||
return this.movieItems();
|
||||
case 'series':
|
||||
return this.seriesItems();
|
||||
}
|
||||
});
|
||||
|
||||
readonly currentTypeLabelKey = computed(() => {
|
||||
switch (this.selectedContentType()) {
|
||||
case 'live':
|
||||
return 'PORTALS.LIVE_TV';
|
||||
case 'movie':
|
||||
return 'PORTALS.MOVIES';
|
||||
case 'series':
|
||||
return 'PORTALS.SERIES';
|
||||
}
|
||||
});
|
||||
|
||||
readonly clearButtonTooltipKey = computed(() =>
|
||||
this.mode() === 'favorites'
|
||||
? 'WORKSPACE.SHELL.CLEAR_FAVORITES_TYPE'
|
||||
: 'WORKSPACE.SHELL.CLEAR_RECENTLY_VIEWED_TYPE'
|
||||
);
|
||||
|
||||
readonly title = computed(() => {
|
||||
return this.mode() === 'favorites'
|
||||
? 'PORTALS.FAVORITES'
|
||||
: 'PORTALS.RECENTLY_VIEWED';
|
||||
});
|
||||
|
||||
readonly favSortMode = signal<FavoritesChannelSortMode>(
|
||||
restoreFavoritesChannelSortMode()
|
||||
);
|
||||
readonly favSortLabelKey = computed(() =>
|
||||
getFavoritesChannelSortModeTranslationKey(this.favSortMode())
|
||||
);
|
||||
readonly showFavSortButton = computed(
|
||||
() =>
|
||||
this.mode() === 'favorites' &&
|
||||
this.selectedContentType() === 'live' &&
|
||||
this.hasLive()
|
||||
);
|
||||
readonly favSortOptions: ReadonlyArray<{
|
||||
mode: FavoritesChannelSortMode;
|
||||
translationKey: string;
|
||||
icon: string;
|
||||
}> = [
|
||||
{
|
||||
mode: 'custom',
|
||||
translationKey: 'WORKSPACE.SORT_CUSTOM',
|
||||
icon: 'drag_indicator',
|
||||
},
|
||||
{
|
||||
mode: 'name-asc',
|
||||
translationKey: 'WORKSPACE.SORT_NAME_ASC',
|
||||
icon: 'sort_by_alpha',
|
||||
},
|
||||
{
|
||||
mode: 'name-desc',
|
||||
translationKey: 'WORKSPACE.SORT_NAME_DESC',
|
||||
icon: 'sort_by_alpha',
|
||||
},
|
||||
{
|
||||
mode: 'date-desc',
|
||||
translationKey: 'WORKSPACE.SORT_DATE_DESC',
|
||||
icon: 'schedule',
|
||||
},
|
||||
];
|
||||
|
||||
private readonly favoritesReloadKey = computed(() => {
|
||||
if (this.mode() !== 'favorites') {
|
||||
return 'recent';
|
||||
@@ -315,6 +388,11 @@ export class UnifiedCollectionPageComponent implements AfterContentInit {
|
||||
this.selectedContentType.set(value);
|
||||
}
|
||||
|
||||
setFavSortMode(mode: FavoritesChannelSortMode): void {
|
||||
this.favSortMode.set(mode);
|
||||
persistFavoritesChannelSortMode(mode);
|
||||
}
|
||||
|
||||
onGridItemSelected(item: UnifiedCollectionItem): void {
|
||||
this.syncCurrentCollectionViewState();
|
||||
|
||||
@@ -360,24 +438,45 @@ export class UnifiedCollectionPageComponent implements AfterContentInit {
|
||||
);
|
||||
}
|
||||
|
||||
clearAllRecent(): void {
|
||||
if (this.allItems().length === 0) {
|
||||
clearAllCurrent(): void {
|
||||
const itemsToRemove = this.currentTypeItems();
|
||||
if (itemsToRemove.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scope = this.effectiveScope();
|
||||
const playlistId = this.playlistId();
|
||||
const isFavorites = this.mode() === 'favorites';
|
||||
const type = this.translate.instant(this.currentTypeLabelKey());
|
||||
const isPlaylistScope = this.effectiveScope() === 'playlist';
|
||||
const titleKey = isFavorites
|
||||
? 'WORKSPACE.SHELL.CLEAR_FAVORITES_DIALOG_TITLE'
|
||||
: 'WORKSPACE.SHELL.CLEAR_RECENTLY_VIEWED_DIALOG_TITLE';
|
||||
const messageKey = isFavorites
|
||||
? isPlaylistScope
|
||||
? 'WORKSPACE.SHELL.CLEAR_FAVORITES_DIALOG_MESSAGE_PLAYLIST'
|
||||
: 'WORKSPACE.SHELL.CLEAR_FAVORITES_DIALOG_MESSAGE_ALL'
|
||||
: isPlaylistScope
|
||||
? 'WORKSPACE.SHELL.CLEAR_RECENTLY_VIEWED_DIALOG_MESSAGE_PLAYLIST'
|
||||
: 'WORKSPACE.SHELL.CLEAR_RECENTLY_VIEWED_DIALOG_MESSAGE_ALL';
|
||||
|
||||
this.dialogService.openConfirmDialog({
|
||||
title: this.translate.instant(
|
||||
'WORKSPACE.SHELL.CLEAR_RECENTLY_VIEWED_DIALOG_TITLE'
|
||||
),
|
||||
message: this.translate.instant(
|
||||
'WORKSPACE.SHELL.CLEAR_RECENTLY_VIEWED_DIALOG_MESSAGE'
|
||||
),
|
||||
title: this.translate.instant(titleKey, { type }),
|
||||
message: this.translate.instant(messageKey, { type }),
|
||||
onConfirm: () => {
|
||||
this.allItems.set([]);
|
||||
void this.recentData.clearRecentItems(scope, playlistId);
|
||||
const removedType = this.selectedContentType();
|
||||
this.allItems.update((items) =>
|
||||
items.filter((item) => item.contentType !== removedType)
|
||||
);
|
||||
const remaining = this.availableTypes();
|
||||
if (remaining.length > 0) {
|
||||
this.selectedContentType.set(remaining[0]);
|
||||
}
|
||||
void Promise.all(
|
||||
itemsToRemove.map((item) =>
|
||||
isFavorites
|
||||
? this.favoritesData.removeFavorite(item)
|
||||
: this.recentData.removeRecentItem(item)
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
+29
-37
@@ -1,26 +1,27 @@
|
||||
@if (activeDetail()) {
|
||||
<div [class]="layoutClass()">
|
||||
<div
|
||||
class="sidebar"
|
||||
appResizable
|
||||
[minWidth]="250"
|
||||
[maxWidth]="600"
|
||||
[defaultWidth]="360"
|
||||
storageKey="unified-live-tab-sidebar-width"
|
||||
>
|
||||
<app-global-favorites-list
|
||||
[channels]="channelsForList()"
|
||||
[epgMap]="epgMap()"
|
||||
[progressTick]="progressTick()"
|
||||
[activeUid]="activeUid()"
|
||||
[searchTermInput]="searchTerm()"
|
||||
[draggable]="mode() === 'favorites'"
|
||||
(channelSelected)="onChannelSelected($event)"
|
||||
(channelsReordered)="onReorder($event)"
|
||||
(favoriteToggled)="onFavoriteToggled($event)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="sidebar"
|
||||
appResizable
|
||||
[minWidth]="250"
|
||||
[maxWidth]="600"
|
||||
[defaultWidth]="360"
|
||||
storageKey="unified-live-tab-sidebar-width"
|
||||
>
|
||||
<app-global-favorites-list
|
||||
[channels]="channelsForList()"
|
||||
[epgMap]="epgMap()"
|
||||
[progressTick]="progressTick()"
|
||||
[activeUid]="activeUid()"
|
||||
[searchTermInput]="searchTerm()"
|
||||
[draggable]="mode() === 'favorites'"
|
||||
[sortMode]="sortMode()"
|
||||
(channelSelected)="onChannelSelected($event)"
|
||||
(channelsReordered)="onReorder($event)"
|
||||
(favoriteToggled)="onFavoriteToggled($event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="content-container">
|
||||
@if (activeDetail()) {
|
||||
@if (shouldUseInlinePlayer()) {
|
||||
<div class="video-player" [class.video-player--radio]="isRadioSelection()">
|
||||
@if (isRadioSelection()) {
|
||||
@@ -122,19 +123,10 @@
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<div class="full-list">
|
||||
<app-global-favorites-list
|
||||
[channels]="channelsForList()"
|
||||
[epgMap]="epgMap()"
|
||||
[progressTick]="progressTick()"
|
||||
[activeUid]="activeUid()"
|
||||
[searchTermInput]="searchTerm()"
|
||||
[draggable]="mode() === 'favorites'"
|
||||
(channelSelected)="onChannelSelected($event)"
|
||||
(channelsReordered)="onReorder($event)"
|
||||
(favoriteToggled)="onFavoriteToggled($event)"
|
||||
} @else {
|
||||
<app-portal-empty-state
|
||||
icon="live_tv"
|
||||
[message]="'CHANNELS.SELECT_CHANNEL_PLAYBACK' | translate"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
+33
-72
@@ -1,71 +1,42 @@
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
@use '../../styles/portal-layout' as portal;
|
||||
|
||||
.itv-container {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr 1fr;
|
||||
grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
|
||||
grid-template-areas:
|
||||
'sidebar video-player video-player'
|
||||
'sidebar epg epg';
|
||||
height: 100%;
|
||||
}
|
||||
@include portal.live-layout;
|
||||
|
||||
.itv-container--radio {
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
grid-template-rows: minmax(0, 1fr);
|
||||
grid-template-areas: 'sidebar video-player';
|
||||
}
|
||||
|
||||
.itv-container--external {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
grid-template-areas: 'sidebar epg';
|
||||
height: 100%;
|
||||
|
||||
.epg {
|
||||
border-top: none;
|
||||
.sidebar {
|
||||
> app-global-favorites-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.full-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.content-container {
|
||||
background: var(--mat-sys-surface);
|
||||
|
||||
.sidebar {
|
||||
grid-area: sidebar;
|
||||
width: 360px;
|
||||
min-width: 250px;
|
||||
max-width: 600px;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
border-right: 1px solid var(--mat-sys-outline-variant);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
&:has(> app-portal-empty-state) {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.video-player {
|
||||
grid-area: video-player;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.video-player--radio {
|
||||
position: relative;
|
||||
background: var(--mat-sys-surface);
|
||||
&--radio {
|
||||
position: relative;
|
||||
background: var(--mat-sys-surface);
|
||||
}
|
||||
|
||||
> app-vjs-player,
|
||||
> app-html-video-player,
|
||||
> app-art-player,
|
||||
> app-audio-player {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.player-toolbar {
|
||||
@@ -108,20 +79,9 @@
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.video-player > app-vjs-player,
|
||||
.video-player > app-html-video-player,
|
||||
.video-player > app-art-player,
|
||||
.video-player > app-audio-player {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.epg {
|
||||
grid-area: epg;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
border-top: 1px solid var(--mat-sys-outline-variant);
|
||||
background: var(--mat-sys-surface);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -132,9 +92,10 @@
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.epg-content > app-epg-view {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
> app-epg-view,
|
||||
> app-epg-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
+11
-12
@@ -12,8 +12,11 @@ import {
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { isM3uCatchupPlaybackSupported } from 'm3u-utils';
|
||||
import {
|
||||
DEFAULT_FAVORITES_CHANNEL_SORT_MODE,
|
||||
FavoritesChannelSortMode,
|
||||
matchesOpenLiveCollectionItem,
|
||||
OpenLiveCollectionItemState,
|
||||
PORTAL_PLAYER,
|
||||
@@ -25,6 +28,7 @@ import {
|
||||
} from '@iptvnator/portal/shared/util';
|
||||
import { EpgListComponent } from '@iptvnator/ui/epg';
|
||||
import { GlobalFavoritesListComponent } from '../global-favorites-list/global-favorites-list.component';
|
||||
import { PortalEmptyStateComponent } from '../portal-empty-state/portal-empty-state.component';
|
||||
import {
|
||||
AudioPlayerComponent,
|
||||
ArtPlayerComponent,
|
||||
@@ -51,7 +55,9 @@ import { EpgViewComponent } from 'shared-portals';
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
MatProgressSpinnerModule,
|
||||
PortalEmptyStateComponent,
|
||||
ResizableDirective,
|
||||
TranslatePipe,
|
||||
VjsPlayerComponent,
|
||||
],
|
||||
})
|
||||
@@ -60,6 +66,9 @@ export class UnifiedLiveTabComponent {
|
||||
readonly mode = input<'favorites' | 'recent'>('favorites');
|
||||
readonly searchTerm = input('');
|
||||
readonly autoOpenItem = input<OpenLiveCollectionItemState | null>(null);
|
||||
readonly sortMode = input<FavoritesChannelSortMode>(
|
||||
DEFAULT_FAVORITES_CHANNEL_SORT_MODE
|
||||
);
|
||||
|
||||
readonly removeItem = output<UnifiedCollectionItem>();
|
||||
readonly reorderItems = output<UnifiedCollectionItem[]>();
|
||||
@@ -118,15 +127,6 @@ export class UnifiedLiveTabComponent {
|
||||
readonly shouldUseInlinePlayer = computed(() => {
|
||||
return this.isRadioSelection() || this.isEmbeddedPlayer();
|
||||
});
|
||||
readonly layoutClass = computed(() => {
|
||||
if (this.isRadioSelection()) {
|
||||
return 'itv-container itv-container--radio';
|
||||
}
|
||||
|
||||
return this.shouldUseInlinePlayer()
|
||||
? 'itv-container'
|
||||
: 'itv-container--external';
|
||||
});
|
||||
|
||||
readonly activeChannelForOverlay = computed((): Channel | undefined => {
|
||||
const detail = this.activeDetail();
|
||||
@@ -232,10 +232,9 @@ export class UnifiedLiveTabComponent {
|
||||
const item = this.items().find(
|
||||
(candidate) => candidate.uid === channel.uid
|
||||
);
|
||||
if (!item) {
|
||||
return;
|
||||
if (item) {
|
||||
await this.activateItem(item);
|
||||
}
|
||||
await this.activateItem(item);
|
||||
}
|
||||
|
||||
onFavoriteToggled(channel: UnifiedFavoriteChannel): void {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './lib/logger';
|
||||
export * from './lib/channel-sort';
|
||||
export * from './lib/favorites-channel-sort';
|
||||
export * from './lib/portal-catalog-detail';
|
||||
export * from './lib/portal-catalog-facade';
|
||||
export * from './lib/portal-external-playback';
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
const FAVORITES_SORT_COLLATOR = new Intl.Collator(undefined, {
|
||||
numeric: true,
|
||||
sensitivity: 'base',
|
||||
});
|
||||
|
||||
export type FavoritesChannelSortMode =
|
||||
| 'custom'
|
||||
| 'name-asc'
|
||||
| 'name-desc'
|
||||
| 'date-desc';
|
||||
|
||||
export const DEFAULT_FAVORITES_CHANNEL_SORT_MODE: FavoritesChannelSortMode =
|
||||
'custom';
|
||||
|
||||
export const FAVORITES_CHANNEL_SORT_STORAGE_KEY =
|
||||
'favorites-channel-sort-mode';
|
||||
|
||||
export function isFavoritesChannelSortMode(
|
||||
value: unknown
|
||||
): value is FavoritesChannelSortMode {
|
||||
return (
|
||||
value === 'custom' ||
|
||||
value === 'name-asc' ||
|
||||
value === 'name-desc' ||
|
||||
value === 'date-desc'
|
||||
);
|
||||
}
|
||||
|
||||
export function restoreFavoritesChannelSortMode(
|
||||
storageKey: string = FAVORITES_CHANNEL_SORT_STORAGE_KEY,
|
||||
fallback: FavoritesChannelSortMode = DEFAULT_FAVORITES_CHANNEL_SORT_MODE
|
||||
): FavoritesChannelSortMode {
|
||||
const storedValue = localStorage.getItem(storageKey);
|
||||
return isFavoritesChannelSortMode(storedValue) ? storedValue : fallback;
|
||||
}
|
||||
|
||||
export function persistFavoritesChannelSortMode(
|
||||
mode: FavoritesChannelSortMode,
|
||||
storageKey: string = FAVORITES_CHANNEL_SORT_STORAGE_KEY
|
||||
): void {
|
||||
localStorage.setItem(storageKey, mode);
|
||||
}
|
||||
|
||||
export function getFavoritesChannelSortModeTranslationKey(
|
||||
mode: FavoritesChannelSortMode
|
||||
): string {
|
||||
switch (mode) {
|
||||
case 'name-asc':
|
||||
return 'WORKSPACE.SORT_NAME_ASC';
|
||||
case 'name-desc':
|
||||
return 'WORKSPACE.SORT_NAME_DESC';
|
||||
case 'date-desc':
|
||||
return 'WORKSPACE.SORT_DATE_DESC';
|
||||
default:
|
||||
return 'WORKSPACE.SORT_CUSTOM';
|
||||
}
|
||||
}
|
||||
|
||||
export function sortFavoriteChannelItems<T>(
|
||||
items: readonly T[],
|
||||
mode: FavoritesChannelSortMode,
|
||||
accessors: {
|
||||
getName: (item: T) => string | null | undefined;
|
||||
getAddedAt?: (item: T) => string | null | undefined;
|
||||
}
|
||||
): readonly T[] {
|
||||
if (mode === 'custom') {
|
||||
return items;
|
||||
}
|
||||
|
||||
if (mode === 'date-desc') {
|
||||
const getAddedAt = accessors.getAddedAt;
|
||||
if (!getAddedAt) {
|
||||
return items;
|
||||
}
|
||||
return [...items].sort((a, b) => {
|
||||
const timeA = Date.parse(getAddedAt(a) ?? '') || 0;
|
||||
const timeB = Date.parse(getAddedAt(b) ?? '') || 0;
|
||||
return timeB - timeA;
|
||||
});
|
||||
}
|
||||
|
||||
return [...items].sort((a, b) => {
|
||||
const result = FAVORITES_SORT_COLLATOR.compare(
|
||||
accessors.getName(a) ?? '',
|
||||
accessors.getName(b) ?? ''
|
||||
);
|
||||
return mode === 'name-asc' ? result : -result;
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user