Proyecto App Windows
This commit is contained in:
@@ -0,0 +1,839 @@
|
||||
import {
|
||||
Injectable,
|
||||
NgZone,
|
||||
computed,
|
||||
effect,
|
||||
inject,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import {
|
||||
PlaylistActions,
|
||||
selectAllPlaylistsMeta,
|
||||
selectPlaylistsLoadingFlag,
|
||||
} from 'm3u-state';
|
||||
import { firstValueFrom, startWith } from 'rxjs';
|
||||
import {
|
||||
DatabaseService,
|
||||
GlobalRecentlyAddedKind,
|
||||
PlaylistsService,
|
||||
} from 'services';
|
||||
import {
|
||||
buildPlaylistRecentItems,
|
||||
Channel,
|
||||
Playlist,
|
||||
PortalAddedItem,
|
||||
PortalActivityItem,
|
||||
PlaylistMeta,
|
||||
PortalActivityType,
|
||||
PortalFavoriteItem,
|
||||
PortalRecentItem,
|
||||
stalkerItemMatchesId,
|
||||
} from 'shared-interfaces';
|
||||
import {
|
||||
buildStalkerFavoriteItems,
|
||||
getActivityTypeLabelKey,
|
||||
mapDbFavoriteToItem,
|
||||
mapDbRecentlyAddedToItem,
|
||||
mapDbRecentToItem,
|
||||
toDateTimestamp,
|
||||
toTimestamp,
|
||||
} from './dashboard-mappers';
|
||||
import {
|
||||
buildStalkerDetailNavigationTarget,
|
||||
buildStalkerStateItem,
|
||||
buildXtreamNavigationTarget,
|
||||
getGlobalFavoriteNavigation,
|
||||
getRecentItemNavigation,
|
||||
WorkspaceNavigationTarget,
|
||||
} from '@iptvnator/portal/shared/util';
|
||||
|
||||
export type DashboardContentKind = 'all' | 'channels' | 'vod' | 'series';
|
||||
|
||||
/** @deprecated Use {@link PortalRecentItem} from `shared-interfaces` instead. */
|
||||
export type GlobalRecentItem = PortalRecentItem;
|
||||
/** @deprecated Use {@link PortalFavoriteItem} from `shared-interfaces` instead. */
|
||||
export type DashboardFavoriteItem = PortalFavoriteItem;
|
||||
/** @deprecated Use {@link PortalAddedItem} from `shared-interfaces` instead. */
|
||||
export type DashboardRecentlyAddedItem = PortalAddedItem;
|
||||
export type DashboardRecentlyAddedFilterKind = GlobalRecentlyAddedKind;
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class DashboardDataService {
|
||||
private readonly store = inject(Store);
|
||||
private readonly dbService = inject(DatabaseService);
|
||||
private readonly playlistsService = inject(PlaylistsService);
|
||||
private readonly ngZone = inject(NgZone);
|
||||
private readonly translate = inject(TranslateService);
|
||||
private favoritesAutoRefreshEnabled = false;
|
||||
private readonly languageTick = toSignal(
|
||||
this.translate.onLangChange.pipe(startWith(null)),
|
||||
{ initialValue: null }
|
||||
);
|
||||
|
||||
private readonly xtreamGlobalRecentItems = signal<GlobalRecentItem[]>([]);
|
||||
private readonly xtreamRecentlyAddedItemsState = signal<
|
||||
DashboardRecentlyAddedItem[]
|
||||
>([]);
|
||||
private readonly xtreamGlobalFavorites = signal<DashboardFavoriteItem[]>(
|
||||
[]
|
||||
);
|
||||
private readonly m3uGlobalFavorites = signal<DashboardFavoriteItem[]>([]);
|
||||
private readonly globalRecentLoadingState = signal(true);
|
||||
private readonly globalRecentLoadedState = signal(false);
|
||||
private readonly globalRecentDbLoadedState = signal(!window.electron);
|
||||
private readonly globalFavoritesLoadingState = signal(true);
|
||||
private readonly globalFavoritesLoadedState = signal(false);
|
||||
private readonly xtreamRecentlyAddedLoadingState = signal(true);
|
||||
private readonly xtreamRecentlyAddedLoadedState = signal(false);
|
||||
readonly playlists = this.store.selectSignal(selectAllPlaylistsMeta);
|
||||
readonly playlistsLoaded = this.store.selectSignal(
|
||||
selectPlaylistsLoadingFlag
|
||||
);
|
||||
readonly xtreamPlaylistCount = computed(
|
||||
() => this.playlists().filter((playlist) => !!playlist.serverUrl).length
|
||||
);
|
||||
readonly hasXtreamPlaylists = computed(() => this.xtreamPlaylistCount() > 0);
|
||||
readonly globalRecentLoading = this.globalRecentLoadingState.asReadonly();
|
||||
readonly globalRecentLoaded = this.globalRecentLoadedState.asReadonly();
|
||||
readonly globalFavoritesLoading =
|
||||
this.globalFavoritesLoadingState.asReadonly();
|
||||
readonly globalFavoritesLoaded =
|
||||
this.globalFavoritesLoadedState.asReadonly();
|
||||
readonly xtreamRecentlyAddedLoading =
|
||||
this.xtreamRecentlyAddedLoadingState.asReadonly();
|
||||
readonly xtreamRecentlyAddedLoaded =
|
||||
this.xtreamRecentlyAddedLoadedState.asReadonly();
|
||||
readonly xtreamRecentlyAddedItems =
|
||||
this.xtreamRecentlyAddedItemsState.asReadonly();
|
||||
readonly dashboardReady = computed(
|
||||
() =>
|
||||
this.playlistsLoaded() &&
|
||||
this.globalRecentLoaded() &&
|
||||
this.globalFavoritesLoaded() &&
|
||||
(!this.hasXtreamPlaylists() || this.xtreamRecentlyAddedLoaded())
|
||||
);
|
||||
|
||||
private readonly playlistFavoritesReloadKey = computed(() => {
|
||||
if (!this.playlistsLoaded()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.playlists()
|
||||
.map((playlist) =>
|
||||
[
|
||||
playlist._id,
|
||||
playlist.serverUrl
|
||||
? 'xtream'
|
||||
: playlist.macAddress
|
||||
? 'stalker'
|
||||
: 'm3u',
|
||||
JSON.stringify(playlist.favorites ?? []),
|
||||
].join('::')
|
||||
)
|
||||
.join('|');
|
||||
});
|
||||
|
||||
readonly playlistBackedGlobalRecentItems = computed<GlobalRecentItem[]>(
|
||||
() => {
|
||||
this.languageTick();
|
||||
return buildPlaylistRecentItems(this.playlists(), {
|
||||
stalker: this.translateText(
|
||||
'WORKSPACE.DASHBOARD.STALKER_PORTAL'
|
||||
),
|
||||
m3u: this.translateText('WORKSPACE.DASHBOARD.M3U'),
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
readonly globalRecentItems = computed<GlobalRecentItem[]>(() =>
|
||||
[
|
||||
...this.xtreamGlobalRecentItems(),
|
||||
...this.playlistBackedGlobalRecentItems(),
|
||||
]
|
||||
.sort(
|
||||
(a, b) =>
|
||||
toDateTimestamp(b.viewed_at) - toDateTimestamp(a.viewed_at)
|
||||
)
|
||||
.slice(0, 200)
|
||||
);
|
||||
|
||||
readonly stalkerGlobalFavorites = computed<DashboardFavoriteItem[]>(() => {
|
||||
this.languageTick();
|
||||
return buildStalkerFavoriteItems(
|
||||
this.playlists(),
|
||||
this.translateText('WORKSPACE.DASHBOARD.STALKER_PORTAL')
|
||||
);
|
||||
});
|
||||
|
||||
readonly globalFavoriteItems = computed(() =>
|
||||
[
|
||||
...this.xtreamGlobalFavorites(),
|
||||
...this.m3uGlobalFavorites(),
|
||||
...this.stalkerGlobalFavorites(),
|
||||
]
|
||||
.sort((a, b) => toTimestamp(b.added_at) - toTimestamp(a.added_at))
|
||||
.slice(0, 200)
|
||||
);
|
||||
|
||||
private readonly recentPlaylistActivityTimestamps = computed(() => {
|
||||
const timestamps = new Map<string, number>();
|
||||
|
||||
for (const item of this.globalRecentItems()) {
|
||||
const viewedAt = toDateTimestamp(item.viewed_at);
|
||||
const current = timestamps.get(item.playlist_id) ?? 0;
|
||||
|
||||
if (viewedAt > current) {
|
||||
timestamps.set(item.playlist_id, viewedAt);
|
||||
}
|
||||
}
|
||||
|
||||
return timestamps;
|
||||
});
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
this.playlistFavoritesReloadKey();
|
||||
if (!this.playlistsLoaded() || !this.favoritesAutoRefreshEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
void this.refreshPlaylistBackedGlobalFavorites();
|
||||
});
|
||||
|
||||
effect(() => {
|
||||
this.playlistsLoaded();
|
||||
this.finishInitialGlobalRecentLoadIfReady();
|
||||
this.finishInitialGlobalFavoritesLoadIfReady();
|
||||
});
|
||||
|
||||
effect(() => {
|
||||
if (this.hasXtreamPlaylists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.ngZone.run(() => {
|
||||
this.xtreamRecentlyAddedItemsState.set([]);
|
||||
this.xtreamRecentlyAddedLoadingState.set(false);
|
||||
this.xtreamRecentlyAddedLoadedState.set(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
readonly stats = computed(() => {
|
||||
const items = this.playlists();
|
||||
return {
|
||||
total: items.length,
|
||||
xtream: items.filter((item) => !!item.serverUrl).length,
|
||||
stalker: items.filter((item) => !!item.macAddress).length,
|
||||
m3u: items.filter((item) => !item.serverUrl && !item.macAddress)
|
||||
.length,
|
||||
};
|
||||
});
|
||||
|
||||
readonly recentPlaylists = computed(() =>
|
||||
[...this.playlists()]
|
||||
.sort((a, b) => {
|
||||
const activityDelta =
|
||||
this.getPlaylistActivityTimestamp(b) -
|
||||
this.getPlaylistActivityTimestamp(a);
|
||||
if (activityDelta !== 0) {
|
||||
return activityDelta;
|
||||
}
|
||||
|
||||
const metadataDelta =
|
||||
this.getRecentTimestamp(b) - this.getRecentTimestamp(a);
|
||||
if (metadataDelta !== 0) {
|
||||
return metadataDelta;
|
||||
}
|
||||
|
||||
return a._id.localeCompare(b._id);
|
||||
})
|
||||
.slice(0, 12)
|
||||
);
|
||||
|
||||
readonly quickRecent = computed(() => this.recentPlaylists().slice(0, 4));
|
||||
|
||||
async reloadGlobalRecentItems(): Promise<void> {
|
||||
if (!this.globalRecentLoaded()) {
|
||||
this.globalRecentLoadingState.set(true);
|
||||
}
|
||||
|
||||
if (!window.electron) {
|
||||
this.xtreamGlobalRecentItems.set([]);
|
||||
this.globalRecentDbLoadedState.set(true);
|
||||
this.finishInitialGlobalRecentLoadIfReady();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const recentItems = await this.dbService.getGlobalRecentlyViewed();
|
||||
const normalized = recentItems.map((item) =>
|
||||
mapDbRecentToItem(item)
|
||||
);
|
||||
this.ngZone.run(() => this.xtreamGlobalRecentItems.set(normalized));
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
'[DashboardData] Failed to reload global recent items',
|
||||
err
|
||||
);
|
||||
this.ngZone.run(() => this.xtreamGlobalRecentItems.set([]));
|
||||
} finally {
|
||||
this.globalRecentDbLoadedState.set(true);
|
||||
this.finishInitialGlobalRecentLoadIfReady();
|
||||
}
|
||||
}
|
||||
|
||||
async reloadGlobalFavorites(): Promise<void> {
|
||||
if (!this.globalFavoritesLoaded()) {
|
||||
this.globalFavoritesLoadingState.set(true);
|
||||
}
|
||||
|
||||
const xtreamReload = this.reloadXtreamGlobalFavorites();
|
||||
const m3uReload = this.refreshPlaylistBackedGlobalFavorites();
|
||||
|
||||
await Promise.all([xtreamReload, m3uReload]);
|
||||
this.favoritesAutoRefreshEnabled = true;
|
||||
}
|
||||
|
||||
async getGlobalRecentlyAddedItems(
|
||||
kind: DashboardRecentlyAddedFilterKind,
|
||||
limit = 200
|
||||
): Promise<DashboardRecentlyAddedItem[]> {
|
||||
if (!window.electron) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const items = await this.dbService.getGlobalRecentlyAdded(kind, limit);
|
||||
return items
|
||||
.map((item) => mapDbRecentlyAddedToItem(item))
|
||||
.sort((a, b) => toTimestamp(b.added_at) - toTimestamp(a.added_at));
|
||||
}
|
||||
|
||||
async getXtreamRecentlyAddedItems(
|
||||
limit = 20
|
||||
): Promise<DashboardRecentlyAddedItem[]> {
|
||||
if (!window.electron) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const items = await this.dbService.getGlobalRecentlyAdded(
|
||||
'all',
|
||||
limit,
|
||||
'xtream'
|
||||
);
|
||||
return items
|
||||
.map((item) => mapDbRecentlyAddedToItem(item))
|
||||
.sort((a, b) => toTimestamp(b.added_at) - toTimestamp(a.added_at));
|
||||
}
|
||||
|
||||
async reloadXtreamRecentlyAddedItems(limit = 20): Promise<void> {
|
||||
if (!this.xtreamRecentlyAddedLoaded()) {
|
||||
this.xtreamRecentlyAddedLoadingState.set(true);
|
||||
}
|
||||
|
||||
if (!window.electron) {
|
||||
this.ngZone.run(() => {
|
||||
this.xtreamRecentlyAddedItemsState.set([]);
|
||||
this.xtreamRecentlyAddedLoadingState.set(false);
|
||||
this.xtreamRecentlyAddedLoadedState.set(true);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const items = await this.getXtreamRecentlyAddedItems(limit);
|
||||
this.ngZone.run(() =>
|
||||
this.xtreamRecentlyAddedItemsState.set(items)
|
||||
);
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
'[DashboardData] Failed to reload Xtream recently added items',
|
||||
err
|
||||
);
|
||||
this.ngZone.run(() => this.xtreamRecentlyAddedItemsState.set([]));
|
||||
} finally {
|
||||
this.ngZone.run(() => {
|
||||
this.xtreamRecentlyAddedLoadingState.set(false);
|
||||
this.xtreamRecentlyAddedLoadedState.set(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async reloadXtreamGlobalFavorites(): Promise<void> {
|
||||
if (!window.electron) {
|
||||
this.xtreamGlobalFavorites.set([]);
|
||||
this.finishInitialGlobalFavoritesLoadIfReady();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const favorites = await this.dbService.getAllGlobalFavorites();
|
||||
const normalized = favorites.map((item) =>
|
||||
mapDbFavoriteToItem(item)
|
||||
);
|
||||
this.ngZone.run(() => this.xtreamGlobalFavorites.set(normalized));
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
'[DashboardData] Failed to reload global favorites',
|
||||
err
|
||||
);
|
||||
this.ngZone.run(() => this.xtreamGlobalFavorites.set([]));
|
||||
} finally {
|
||||
this.finishInitialGlobalFavoritesLoadIfReady();
|
||||
}
|
||||
}
|
||||
|
||||
private async refreshPlaylistBackedGlobalFavorites(): Promise<void> {
|
||||
await this.reloadM3uGlobalFavorites();
|
||||
this.finishInitialGlobalFavoritesLoadIfReady();
|
||||
}
|
||||
|
||||
private async reloadM3uGlobalFavorites(): Promise<void> {
|
||||
const m3uPlaylists = this.playlists().filter(
|
||||
(playlist) =>
|
||||
!playlist.serverUrl &&
|
||||
!playlist.macAddress &&
|
||||
Array.isArray(playlist.favorites) &&
|
||||
playlist.favorites.some(
|
||||
(favorite): favorite is string =>
|
||||
typeof favorite === 'string' &&
|
||||
favorite.trim().length > 0
|
||||
)
|
||||
);
|
||||
|
||||
if (m3uPlaylists.length === 0) {
|
||||
this.ngZone.run(() => this.m3uGlobalFavorites.set([]));
|
||||
return;
|
||||
}
|
||||
|
||||
const favoriteItems = await Promise.all(
|
||||
m3uPlaylists.map((playlist) =>
|
||||
this.loadM3uPlaylistFavorites(playlist).catch(
|
||||
(): DashboardFavoriteItem[] => []
|
||||
)
|
||||
)
|
||||
);
|
||||
const flattenedFavorites = favoriteItems.reduce(
|
||||
(acc, items) => [...acc, ...items],
|
||||
[] as DashboardFavoriteItem[]
|
||||
);
|
||||
|
||||
this.ngZone.run(() => this.m3uGlobalFavorites.set(flattenedFavorites));
|
||||
}
|
||||
|
||||
private finishInitialGlobalFavoritesLoadIfReady(): void {
|
||||
if (!this.playlistsLoaded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.ngZone.run(() => {
|
||||
this.globalFavoritesLoadedState.set(true);
|
||||
this.globalFavoritesLoadingState.set(false);
|
||||
});
|
||||
}
|
||||
|
||||
private finishInitialGlobalRecentLoadIfReady(): void {
|
||||
if (!this.playlistsLoaded() || !this.globalRecentDbLoadedState()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.ngZone.run(() => {
|
||||
this.globalRecentLoadedState.set(true);
|
||||
this.globalRecentLoadingState.set(false);
|
||||
});
|
||||
}
|
||||
|
||||
isTypeInKind(
|
||||
type: PortalActivityType,
|
||||
kind: DashboardContentKind
|
||||
): boolean {
|
||||
if (kind === 'all') {
|
||||
return true;
|
||||
}
|
||||
if (kind === 'channels') {
|
||||
return type === 'live';
|
||||
}
|
||||
if (kind === 'vod') {
|
||||
return type === 'movie';
|
||||
}
|
||||
return type === 'series';
|
||||
}
|
||||
|
||||
getPlaylistLink(playlist: PlaylistMeta): string[] {
|
||||
if (playlist.serverUrl) {
|
||||
return ['/workspace', 'xtreams', playlist._id, 'vod'];
|
||||
}
|
||||
|
||||
if (playlist.macAddress) {
|
||||
return ['/workspace', 'stalker', playlist._id, 'vod'];
|
||||
}
|
||||
|
||||
return ['/workspace', 'playlists', playlist._id];
|
||||
}
|
||||
|
||||
getPlaylistProvider(playlist: PlaylistMeta): string {
|
||||
this.languageTick();
|
||||
|
||||
if (playlist.serverUrl) {
|
||||
return this.translateText('WORKSPACE.DASHBOARD.XTREAM');
|
||||
}
|
||||
|
||||
if (playlist.macAddress) {
|
||||
return this.translateText('WORKSPACE.DASHBOARD.STALKER');
|
||||
}
|
||||
|
||||
return this.translateText('WORKSPACE.DASHBOARD.M3U');
|
||||
}
|
||||
|
||||
getRecentItemProviderLabel(item: GlobalRecentItem): string {
|
||||
this.languageTick();
|
||||
|
||||
if (item.source === 'stalker') {
|
||||
return this.translateText('WORKSPACE.DASHBOARD.STALKER');
|
||||
}
|
||||
if (item.source === 'xtream') {
|
||||
return this.translateText('WORKSPACE.DASHBOARD.XTREAM');
|
||||
}
|
||||
if (item.source === 'm3u') {
|
||||
return this.translateText('WORKSPACE.DASHBOARD.M3U');
|
||||
}
|
||||
return this.translateText('WORKSPACE.DASHBOARD.PROVIDER');
|
||||
}
|
||||
|
||||
getRecentItemTypeLabel(item: GlobalRecentItem): string {
|
||||
this.languageTick();
|
||||
return this.translateText(getActivityTypeLabelKey(item.type));
|
||||
}
|
||||
|
||||
getRecentItemLink(item: GlobalRecentItem): string[] {
|
||||
return getRecentItemNavigation(item).link;
|
||||
}
|
||||
|
||||
getRecentItemNavigationState(
|
||||
item: GlobalRecentItem
|
||||
): WorkspaceNavigationTarget['state'] {
|
||||
return getRecentItemNavigation(item).state;
|
||||
}
|
||||
|
||||
async removeGlobalRecentItem(item: GlobalRecentItem): Promise<void> {
|
||||
if (item.source === 'xtream') {
|
||||
await this.dbService.removeRecentItem(
|
||||
item.id as number,
|
||||
item.playlist_id
|
||||
);
|
||||
await this.reloadGlobalRecentItems();
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.source === 'stalker') {
|
||||
const updatedPlaylist = await firstValueFrom(
|
||||
this.playlistsService.removeFromPortalRecentlyViewed(
|
||||
item.playlist_id,
|
||||
item.id
|
||||
)
|
||||
);
|
||||
|
||||
this.store.dispatch(
|
||||
PlaylistActions.updatePlaylistMeta({
|
||||
playlist: {
|
||||
_id: item.playlist_id,
|
||||
recentlyViewed: updatedPlaylist?.recentlyViewed ?? [],
|
||||
} as unknown as PlaylistMeta,
|
||||
}) as any
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.source === 'm3u') {
|
||||
const updatedPlaylist = await firstValueFrom(
|
||||
this.playlistsService.removeFromM3uRecentlyViewed(
|
||||
item.playlist_id,
|
||||
String(item.xtream_id ?? item.id)
|
||||
)
|
||||
);
|
||||
|
||||
this.store.dispatch(
|
||||
PlaylistActions.updatePlaylistMeta({
|
||||
playlist: {
|
||||
_id: item.playlist_id,
|
||||
recentlyViewed: updatedPlaylist?.recentlyViewed ?? [],
|
||||
} as unknown as PlaylistMeta,
|
||||
}) as any
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
getFavoriteItemProviderLabel(item: DashboardFavoriteItem): string {
|
||||
return this.getActivityItemProviderLabel(item);
|
||||
}
|
||||
|
||||
getRecentlyAddedItemProviderLabel(
|
||||
item: DashboardRecentlyAddedItem
|
||||
): string {
|
||||
return this.getActivityItemProviderLabel(item);
|
||||
}
|
||||
|
||||
private getActivityItemProviderLabel(
|
||||
item: Pick<PortalActivityItem, 'source'>
|
||||
): string {
|
||||
this.languageTick();
|
||||
|
||||
if (item.source === 'stalker') {
|
||||
return this.translateText('WORKSPACE.DASHBOARD.STALKER');
|
||||
}
|
||||
if (item.source === 'xtream') {
|
||||
return this.translateText('WORKSPACE.DASHBOARD.XTREAM');
|
||||
}
|
||||
if (item.source === 'm3u') {
|
||||
return this.translateText('WORKSPACE.DASHBOARD.M3U');
|
||||
}
|
||||
return this.translateText('WORKSPACE.DASHBOARD.PROVIDER');
|
||||
}
|
||||
|
||||
getFavoriteItemTypeLabel(item: DashboardFavoriteItem): string {
|
||||
return this.getActivityItemTypeLabel(item);
|
||||
}
|
||||
|
||||
getRecentlyAddedItemTypeLabel(item: DashboardRecentlyAddedItem): string {
|
||||
return this.getActivityItemTypeLabel(item);
|
||||
}
|
||||
|
||||
private getActivityItemTypeLabel(
|
||||
item: Pick<PortalActivityItem, 'type'>
|
||||
): string {
|
||||
this.languageTick();
|
||||
return this.translateText(getActivityTypeLabelKey(item.type));
|
||||
}
|
||||
|
||||
getGlobalFavoriteLink(item: DashboardFavoriteItem): string[] {
|
||||
return getGlobalFavoriteNavigation(item).link;
|
||||
}
|
||||
|
||||
getGlobalFavoriteNavigationState(
|
||||
item: DashboardFavoriteItem
|
||||
): WorkspaceNavigationTarget['state'] {
|
||||
return getGlobalFavoriteNavigation(item).state;
|
||||
}
|
||||
|
||||
getRecentlyAddedLink(item: DashboardRecentlyAddedItem): string[] {
|
||||
if (item.source === 'stalker' && item.type !== 'live') {
|
||||
return buildStalkerDetailNavigationTarget({
|
||||
playlistId: item.playlist_id,
|
||||
type: item.type,
|
||||
categoryId: item.category_id,
|
||||
item: buildStalkerStateItem(item.stalker_item, {
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
type: item.type,
|
||||
category_id: item.category_id,
|
||||
poster_url: item.poster_url,
|
||||
}),
|
||||
}).link;
|
||||
}
|
||||
|
||||
return buildXtreamNavigationTarget({
|
||||
playlistId: item.playlist_id,
|
||||
type: item.type,
|
||||
categoryId: item.category_id,
|
||||
itemId: item.xtream_id,
|
||||
title: item.title,
|
||||
imageUrl: item.poster_url,
|
||||
}).link;
|
||||
}
|
||||
|
||||
getRecentlyAddedNavigationState(
|
||||
item: DashboardRecentlyAddedItem
|
||||
): WorkspaceNavigationTarget['state'] {
|
||||
if (item.source === 'stalker' && item.type !== 'live') {
|
||||
return buildStalkerDetailNavigationTarget({
|
||||
playlistId: item.playlist_id,
|
||||
type: item.type,
|
||||
categoryId: item.category_id,
|
||||
item: buildStalkerStateItem(item.stalker_item, {
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
type: item.type,
|
||||
category_id: item.category_id,
|
||||
poster_url: item.poster_url,
|
||||
}),
|
||||
}).state;
|
||||
}
|
||||
|
||||
return buildXtreamNavigationTarget({
|
||||
playlistId: item.playlist_id,
|
||||
type: item.type,
|
||||
categoryId: item.category_id,
|
||||
itemId: item.xtream_id,
|
||||
title: item.title,
|
||||
imageUrl: item.poster_url,
|
||||
}).state;
|
||||
}
|
||||
|
||||
async removeGlobalFavorite(item: DashboardFavoriteItem): Promise<void> {
|
||||
if (item.source === 'xtream') {
|
||||
await this.dbService.removeFromFavorites(
|
||||
item.id as number,
|
||||
item.playlist_id
|
||||
);
|
||||
await this.reloadGlobalFavorites();
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.source === 'stalker') {
|
||||
const playlist = this.playlists().find(
|
||||
(p) => p._id === item.playlist_id
|
||||
);
|
||||
if (!playlist) return;
|
||||
|
||||
const currentFavorites = Array.isArray(playlist.favorites)
|
||||
? [...playlist.favorites]
|
||||
: [];
|
||||
const itemMatchStr = String(item.id);
|
||||
|
||||
const filteredFavorites = currentFavorites.filter(
|
||||
(raw, index) =>
|
||||
!stalkerItemMatchesId(
|
||||
raw,
|
||||
itemMatchStr,
|
||||
playlist._id,
|
||||
index
|
||||
)
|
||||
);
|
||||
|
||||
this.store.dispatch(
|
||||
PlaylistActions.updatePlaylistMeta({
|
||||
playlist: {
|
||||
_id: item.playlist_id,
|
||||
favorites: filteredFavorites,
|
||||
} as unknown as PlaylistMeta,
|
||||
}) as any
|
||||
);
|
||||
}
|
||||
|
||||
if (item.source === 'm3u') {
|
||||
const playlist = await firstValueFrom(
|
||||
this.playlistsService.getPlaylistById(item.playlist_id)
|
||||
);
|
||||
const currentFavorites = Array.isArray(playlist?.favorites)
|
||||
? playlist.favorites.filter(
|
||||
(favorite): favorite is string =>
|
||||
typeof favorite === 'string'
|
||||
)
|
||||
: [];
|
||||
const filteredFavorites = currentFavorites.filter(
|
||||
(favorite) => favorite !== String(item.id)
|
||||
);
|
||||
|
||||
await firstValueFrom(
|
||||
this.playlistsService.setFavorites(
|
||||
item.playlist_id,
|
||||
filteredFavorites
|
||||
)
|
||||
);
|
||||
await this.reloadGlobalFavorites();
|
||||
}
|
||||
}
|
||||
|
||||
formatTimestamp(value?: string | number): string {
|
||||
this.languageTick();
|
||||
|
||||
const timestamp = toTimestamp(value);
|
||||
if (!timestamp) {
|
||||
return this.translateText('WORKSPACE.DASHBOARD.NOT_YET_SYNCED');
|
||||
}
|
||||
|
||||
return new Date(timestamp).toLocaleString(this.getLocale());
|
||||
}
|
||||
|
||||
private async loadM3uPlaylistFavorites(
|
||||
playlistMeta: PlaylistMeta
|
||||
): Promise<DashboardFavoriteItem[]> {
|
||||
const playlist = (await firstValueFrom(
|
||||
this.playlistsService.getPlaylistById(playlistMeta._id)
|
||||
)) as Playlist & {
|
||||
playlist?: {
|
||||
items?: Channel[];
|
||||
};
|
||||
};
|
||||
const favorites = Array.isArray(playlist?.favorites)
|
||||
? playlist.favorites.filter(
|
||||
(favorite): favorite is string =>
|
||||
typeof favorite === 'string' && favorite.trim().length > 0
|
||||
)
|
||||
: [];
|
||||
|
||||
if (favorites.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const items: Channel[] = Array.isArray(playlist?.playlist?.items)
|
||||
? (playlist.playlist.items as Channel[])
|
||||
: [];
|
||||
const fallbackTimestamp =
|
||||
this.getM3uFavoriteTimestamp(playlistMeta) ??
|
||||
new Date(0).toISOString();
|
||||
|
||||
return items.reduce<DashboardFavoriteItem[]>((acc, channel) => {
|
||||
const channelId = String(channel.id ?? '').trim();
|
||||
const channelUrl = String(channel.url ?? '').trim();
|
||||
const matchedFavoriteId = favorites.find(
|
||||
(favorite) => favorite === channelId || favorite === channelUrl
|
||||
);
|
||||
|
||||
if (!matchedFavoriteId) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
acc.push({
|
||||
id: matchedFavoriteId,
|
||||
title: channel.name?.trim() || channel.tvg?.name || channelId,
|
||||
type: 'live',
|
||||
playlist_id: playlistMeta._id,
|
||||
playlist_name:
|
||||
playlistMeta.title || playlistMeta.filename || 'M3U',
|
||||
added_at: fallbackTimestamp,
|
||||
category_id: 'live',
|
||||
xtream_id: matchedFavoriteId,
|
||||
poster_url: channel.tvg?.logo || undefined,
|
||||
source: 'm3u',
|
||||
});
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
private getM3uFavoriteTimestamp(playlist: PlaylistMeta): string | null {
|
||||
if (playlist.updateDate) {
|
||||
return new Date(playlist.updateDate).toISOString();
|
||||
}
|
||||
|
||||
return typeof playlist.importDate === 'string'
|
||||
? playlist.importDate
|
||||
: null;
|
||||
}
|
||||
|
||||
private getPlaylistActivityTimestamp(item: PlaylistMeta): number {
|
||||
return this.recentPlaylistActivityTimestamps().get(item._id) ?? 0;
|
||||
}
|
||||
|
||||
private getRecentTimestamp(item: PlaylistMeta): number {
|
||||
return toTimestamp(item.updateDate) || toTimestamp(item.importDate);
|
||||
}
|
||||
|
||||
private getLocale(): string | undefined {
|
||||
return (
|
||||
this.translate.currentLang ||
|
||||
this.translate.defaultLang ||
|
||||
undefined
|
||||
);
|
||||
}
|
||||
|
||||
private translateText(
|
||||
key: string,
|
||||
params?: Record<string, string | number>
|
||||
): string {
|
||||
return this.translate.instant(key, params);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user