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",