mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
Merge pull request #725 from 4gray/fix/stalker-favorites
fix/stalker-favorites
This commit is contained in:
11 files changed
+96
-41
No files matched your search
@@ -11,7 +11,13 @@
|
||||
"Bash(npm run build:backend:*)",
|
||||
"Bash(pnpm add:*)",
|
||||
"WebFetch(domain:github.com)",
|
||||
"WebFetch(domain:www.npmjs.com)"
|
||||
"WebFetch(domain:www.npmjs.com)",
|
||||
"Skill(example-skills:frontend-design)",
|
||||
"Bash(npm run test:frontend:*)",
|
||||
"Bash(nx lint:*)",
|
||||
"Bash(npx tsc:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(xargs sed:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
@@ -243,7 +243,13 @@ ipcMain.handle(
|
||||
async (event, playlistId: string, searchTerm: string, types: string[]) => {
|
||||
try {
|
||||
const db = await getDatabase();
|
||||
const result = await db
|
||||
// Note: SQLite's LOWER() only handles ASCII characters, not Unicode/Cyrillic.
|
||||
// We use a two-step approach:
|
||||
// 1. SQL filters by playlist and type
|
||||
// 2. JavaScript filters by title using proper Unicode toLowerCase()
|
||||
const searchTermLower = searchTerm.toLowerCase();
|
||||
|
||||
const candidates = await db
|
||||
.select({
|
||||
id: schema.content.id,
|
||||
category_id: schema.content.categoryId,
|
||||
@@ -262,14 +268,18 @@ ipcMain.handle(
|
||||
.where(
|
||||
and(
|
||||
eq(schema.categories.playlistId, playlistId),
|
||||
sql`LOWER(${schema.content.title}) LIKE ${'%' + escapeLikePattern(searchTerm.toLowerCase()) + '%'} ESCAPE '\\'`,
|
||||
inArray(
|
||||
schema.content.type,
|
||||
types as Array<'live' | 'movie' | 'series'>
|
||||
)
|
||||
)
|
||||
)
|
||||
.limit(50);
|
||||
);
|
||||
|
||||
// Filter in JavaScript for proper Unicode case-insensitive search
|
||||
const result = candidates
|
||||
.filter(item => item.title?.toLowerCase().includes(searchTermLower))
|
||||
.slice(0, 50);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error searching content:', error);
|
||||
@@ -286,7 +296,13 @@ ipcMain.handle(
|
||||
async (event, searchTerm: string, types: string[]) => {
|
||||
try {
|
||||
const db = await getDatabase();
|
||||
const result = await db
|
||||
// Note: SQLite's LOWER() only handles ASCII characters, not Unicode/Cyrillic.
|
||||
// We use a two-step approach:
|
||||
// 1. SQL filters by type only
|
||||
// 2. JavaScript filters by title using proper Unicode toLowerCase()
|
||||
const searchTermLower = searchTerm.toLowerCase();
|
||||
|
||||
const candidates = await db
|
||||
.select({
|
||||
id: schema.content.id,
|
||||
category_id: schema.content.categoryId,
|
||||
@@ -309,16 +325,18 @@ ipcMain.handle(
|
||||
eq(schema.categories.playlistId, schema.playlists.id)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
sql`LOWER(${schema.content.title}) LIKE ${'%' + escapeLikePattern(searchTerm.toLowerCase()) + '%'} ESCAPE '\\'`,
|
||||
inArray(
|
||||
schema.content.type,
|
||||
types as Array<'live' | 'movie' | 'series'>
|
||||
)
|
||||
inArray(
|
||||
schema.content.type,
|
||||
types as Array<'live' | 'movie' | 'series'>
|
||||
)
|
||||
)
|
||||
.orderBy(schema.content.title)
|
||||
.limit(50);
|
||||
);
|
||||
|
||||
// Filter in JavaScript for proper Unicode case-insensitive search
|
||||
const result = candidates
|
||||
.filter(item => item.title?.toLowerCase().includes(searchTermLower))
|
||||
.sort((a, b) => a.title.localeCompare(b.title))
|
||||
.slice(0, 50);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error in global search:', error);
|
||||
|
||||
@@ -1,13 +1,4 @@
|
||||
@if (itemDetails) {
|
||||
<button
|
||||
mat-button
|
||||
(click)="itemDetails = null"
|
||||
[attr.aria-label]="'SETTINGS.BACK_TO_HOME' | translate"
|
||||
class="back-button"
|
||||
>
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
{{ 'SETTINGS.BACK_TO_HOME' | translate }}
|
||||
</button>
|
||||
@switch (itemDetails.category_id) {
|
||||
@case ('vod') {
|
||||
<app-vod-details
|
||||
@@ -19,10 +10,11 @@
|
||||
itemDetails.details.info.movie_image
|
||||
)
|
||||
"
|
||||
(backClicked)="itemDetails = null"
|
||||
/>
|
||||
}
|
||||
@case ('series') {
|
||||
<app-stalker-series-view />
|
||||
<app-stalker-series-view (backClicked)="itemDetails = null" />
|
||||
}
|
||||
}
|
||||
} @else {
|
||||
|
||||
@@ -14,10 +14,7 @@ import { StalkerStore } from '../stalker.store';
|
||||
templateUrl: './stalker-favorites.component.html',
|
||||
imports: [
|
||||
FavoritesLayoutComponent,
|
||||
MatButton,
|
||||
MatIcon,
|
||||
StalkerSeriesViewComponent,
|
||||
TranslatePipe,
|
||||
VodDetailsComponent,
|
||||
],
|
||||
styles: [
|
||||
@@ -128,20 +125,21 @@ export class StalkerFavoritesComponent {
|
||||
|
||||
openItem(item: any) {
|
||||
console.debug('Open item', item);
|
||||
const normalizedItem = this.normalizeFavoriteItem(item);
|
||||
|
||||
switch (item.category_id) {
|
||||
case 'itv':
|
||||
this.stalkerStore.setSelectedContentType('itv');
|
||||
this.createLinkToPlayVodItv(item.cmd, item.name, item.logo);
|
||||
break;
|
||||
case 'vod':
|
||||
this.itemDetails = item;
|
||||
this.stalkerStore.setSelectedItem(item);
|
||||
this.itemDetails = normalizedItem;
|
||||
this.stalkerStore.setSelectedItem(normalizedItem.details);
|
||||
this.stalkerStore.setSelectedContentType('vod');
|
||||
break;
|
||||
case 'series':
|
||||
this.itemDetails = item;
|
||||
|
||||
this.stalkerStore.setSelectedItem(item);
|
||||
this.itemDetails = normalizedItem;
|
||||
this.stalkerStore.setSelectedItem(normalizedItem.details);
|
||||
this.stalkerStore.setSelectedContentType('series');
|
||||
break;
|
||||
default:
|
||||
@@ -149,6 +147,41 @@ export class StalkerFavoritesComponent {
|
||||
}
|
||||
}
|
||||
|
||||
private normalizeFavoriteItem(item: any): any {
|
||||
if (item.details && item.details.info) {
|
||||
return item;
|
||||
}
|
||||
|
||||
const source = item.details || item;
|
||||
const info = source.info || {
|
||||
name: item.name || item.title || 'Unknown',
|
||||
movie_image: item.cover || item.screenshot_uri || item.logo,
|
||||
description: item.description,
|
||||
releasedate: item.releasedate || item.year,
|
||||
genre: item.genre || item.genres_str,
|
||||
duration: item.duration,
|
||||
rating_imdb: item.rating_imdb || item.rating,
|
||||
country: item.country,
|
||||
actors: item.actors,
|
||||
director: item.director,
|
||||
backdrop_path:
|
||||
item.backdrop_path ||
|
||||
[item.cover, item.screenshot_uri].filter(Boolean),
|
||||
};
|
||||
|
||||
return {
|
||||
...item,
|
||||
details: {
|
||||
...source,
|
||||
info: info,
|
||||
cmd: item.cmd || source.cmd,
|
||||
id: item.stream_id || item.id || source.id,
|
||||
series: item.series || source.series,
|
||||
is_series: item.is_series || source.is_series,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async createLinkToPlayVodItv(
|
||||
cmd?: string,
|
||||
title?: string,
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
effect,
|
||||
inject,
|
||||
input,
|
||||
output,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
@@ -56,6 +57,7 @@ interface StalkerSeriesSeason {
|
||||
})
|
||||
export class StalkerSeriesViewComponent {
|
||||
readonly stalkerStore = inject(StalkerStore);
|
||||
readonly backClicked = output<void>();
|
||||
|
||||
/**
|
||||
* Optional input for VOD items with embedded series array (vclub mode)
|
||||
@@ -349,6 +351,7 @@ export class StalkerSeriesViewComponent {
|
||||
}
|
||||
|
||||
goBack() {
|
||||
this.backClicked.emit();
|
||||
this.stalkerStore.clearSelectedItem();
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -96,8 +96,9 @@ export class CategoryContentViewComponent implements OnInit {
|
||||
},
|
||||
};
|
||||
|
||||
this.store.setSelectedItem(selectedItem);
|
||||
if (!this.isStalker) {
|
||||
if (this.isStalker) {
|
||||
this.store.setSelectedItem(selectedItem);
|
||||
} else {
|
||||
this.router.navigate([item.xtream_id], {
|
||||
relativeTo: this.activatedRoute,
|
||||
});
|
||||
|
||||
@@ -36,6 +36,7 @@ export class VodDetailsComponent implements OnInit {
|
||||
@Output() addToFavoritesClicked = new EventEmitter<any>();
|
||||
@Output() playClicked = new EventEmitter<XtreamVodDetails>();
|
||||
@Output() removeFromFavoritesClicked = new EventEmitter<number>();
|
||||
@Output() backClicked = new EventEmitter<void>();
|
||||
|
||||
private location = inject(Location);
|
||||
private playlistService = inject(PlaylistsService);
|
||||
@@ -114,6 +115,7 @@ export class VodDetailsComponent implements OnInit {
|
||||
}
|
||||
|
||||
goBack() {
|
||||
this.backClicked.emit();
|
||||
if (this.isStalker) {
|
||||
// Stalker mode: clear selectedItem to return to category view
|
||||
this.stalkerStore.clearSelectedItem();
|
||||
|
||||
@@ -9,10 +9,12 @@
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
app-region: no-drag;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
background: rgba(255, 255, 255, 0.13);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
&:active {
|
||||
|
||||
@@ -16,7 +16,6 @@ import { MatIcon } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatMenu, MatMenuModule, MatMenuTrigger } from '@angular/material/menu';
|
||||
import { MatRippleModule } from '@angular/material/core';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { NavigationEnd, Router } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
@@ -37,7 +36,6 @@ import { PlaylistMeta } from 'shared-interfaces';
|
||||
MatInputModule,
|
||||
MatMenuModule,
|
||||
MatRippleModule,
|
||||
MatTooltip,
|
||||
TranslatePipe,
|
||||
],
|
||||
})
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import '@yangkghjh/videojs-aspect-ratio-panel';
|
||||
import videoJs from 'video.js';
|
||||
import 'videojs-contrib-quality-levels';
|
||||
//import 'videojs-hls-quality-selector';
|
||||
import 'videojs-hls-quality-selector';
|
||||
|
||||
/**
|
||||
* This component contains the implementation of video player that is based on video.js library
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "iptvnator",
|
||||
"version": "0.17.1",
|
||||
"version": "0.18.0",
|
||||
"license": "MIT",
|
||||
"description": "IPTV player application.",
|
||||
"homepage": "https://github.com/4gray/iptvnator",
|
||||
|
||||
Reference in new issue
Block a user