diff --git a/.claude/settings.local.json b/.claude/settings.local.json index c82d415d9..e2006512c 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -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": [] diff --git a/apps/electron-backend/src/app/events/database/content.events.ts b/apps/electron-backend/src/app/events/database/content.events.ts index d4805bb81..53baa54d2 100644 --- a/apps/electron-backend/src/app/events/database/content.events.ts +++ b/apps/electron-backend/src/app/events/database/content.events.ts @@ -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); diff --git a/apps/web/src/app/stalker/stalker-favorites/stalker-favorites.component.html b/apps/web/src/app/stalker/stalker-favorites/stalker-favorites.component.html index 73a4dff49..6f88784e7 100644 --- a/apps/web/src/app/stalker/stalker-favorites/stalker-favorites.component.html +++ b/apps/web/src/app/stalker/stalker-favorites/stalker-favorites.component.html @@ -1,13 +1,4 @@ @if (itemDetails) { - @switch (itemDetails.category_id) { @case ('vod') { } @case ('series') { - + } } } @else { diff --git a/apps/web/src/app/stalker/stalker-favorites/stalker-favorites.component.ts b/apps/web/src/app/stalker/stalker-favorites/stalker-favorites.component.ts index fd6ea17c4..aca1b8216 100644 --- a/apps/web/src/app/stalker/stalker-favorites/stalker-favorites.component.ts +++ b/apps/web/src/app/stalker/stalker-favorites/stalker-favorites.component.ts @@ -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, diff --git a/apps/web/src/app/stalker/stalker-series-view/stalker-series-view.component.ts b/apps/web/src/app/stalker/stalker-series-view/stalker-series-view.component.ts index 83a7cbc5e..9b3b0fa9a 100644 --- a/apps/web/src/app/stalker/stalker-series-view/stalker-series-view.component.ts +++ b/apps/web/src/app/stalker/stalker-series-view/stalker-series-view.component.ts @@ -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(); /** * 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(); } } \ No newline at end of file diff --git a/apps/web/src/app/xtream-tauri/category-content-view/category-content-view.component.ts b/apps/web/src/app/xtream-tauri/category-content-view/category-content-view.component.ts index fec71cee4..f2955ac1d 100644 --- a/apps/web/src/app/xtream-tauri/category-content-view/category-content-view.component.ts +++ b/apps/web/src/app/xtream-tauri/category-content-view/category-content-view.component.ts @@ -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, }); diff --git a/apps/web/src/app/xtream/vod-details/vod-details.component.ts b/apps/web/src/app/xtream/vod-details/vod-details.component.ts index 948406af3..c46ba1b2d 100644 --- a/apps/web/src/app/xtream/vod-details/vod-details.component.ts +++ b/apps/web/src/app/xtream/vod-details/vod-details.component.ts @@ -36,6 +36,7 @@ export class VodDetailsComponent implements OnInit { @Output() addToFavoritesClicked = new EventEmitter(); @Output() playClicked = new EventEmitter(); @Output() removeFromFavoritesClicked = new EventEmitter(); + @Output() backClicked = new EventEmitter(); 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(); diff --git a/libs/ui/components/src/lib/playlist-switcher/playlist-switcher.component.scss b/libs/ui/components/src/lib/playlist-switcher/playlist-switcher.component.scss index 78529df81..cb3a5cc1a 100644 --- a/libs/ui/components/src/lib/playlist-switcher/playlist-switcher.component.scss +++ b/libs/ui/components/src/lib/playlist-switcher/playlist-switcher.component.scss @@ -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 { diff --git a/libs/ui/components/src/lib/playlist-switcher/playlist-switcher.component.ts b/libs/ui/components/src/lib/playlist-switcher/playlist-switcher.component.ts index 25c080b2f..3c2d8ee15 100644 --- a/libs/ui/components/src/lib/playlist-switcher/playlist-switcher.component.ts +++ b/libs/ui/components/src/lib/playlist-switcher/playlist-switcher.component.ts @@ -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, ], }) diff --git a/libs/ui/components/src/lib/vjs-player/vjs-player.component.ts b/libs/ui/components/src/lib/vjs-player/vjs-player.component.ts index c1db22c9c..411a06cf3 100644 --- a/libs/ui/components/src/lib/vjs-player/vjs-player.component.ts +++ b/libs/ui/components/src/lib/vjs-player/vjs-player.component.ts @@ -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 diff --git a/package.json b/package.json index 90ec715bb..16f311b0c 100644 --- a/package.json +++ b/package.json @@ -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",