mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 10:06:15 -08:00
Merge pull request #761 from 4gray/feat/search-in-visible-categories
feat: add excludeHidden option to search functions for filtering results
This commit is contained in:
14 files changed
+97
-51
No files matched your search
@@ -184,10 +184,11 @@ contextBridge.exposeInMainWorld('electron', {
|
||||
dbSearchContent: (
|
||||
playlistId: string,
|
||||
searchTerm: string,
|
||||
types: string[]
|
||||
) => ipcRenderer.invoke('DB_SEARCH_CONTENT', playlistId, searchTerm, types),
|
||||
dbGlobalSearch: (searchTerm: string, types: string[]) =>
|
||||
ipcRenderer.invoke('DB_GLOBAL_SEARCH', searchTerm, types),
|
||||
types: string[],
|
||||
excludeHidden?: boolean
|
||||
) => ipcRenderer.invoke('DB_SEARCH_CONTENT', playlistId, searchTerm, types, excludeHidden),
|
||||
dbGlobalSearch: (searchTerm: string, types: string[], excludeHidden?: boolean) =>
|
||||
ipcRenderer.invoke('DB_GLOBAL_SEARCH', searchTerm, types, excludeHidden),
|
||||
dbGetRecentlyViewed: () => ipcRenderer.invoke('DB_GET_RECENTLY_VIEWED'),
|
||||
dbClearRecentlyViewed: () => ipcRenderer.invoke('DB_CLEAR_RECENTLY_VIEWED'),
|
||||
// Favorites
|
||||
|
||||
@@ -259,8 +259,9 @@ ipcMain.handle(
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'DB_SEARCH_CONTENT',
|
||||
async (event, playlistId: string, searchTerm: string, types: string[]) => {
|
||||
async (event, playlistId: string, searchTerm: string, types: string[], excludeHidden = false) => {
|
||||
try {
|
||||
if (!types || types.length === 0) return [];
|
||||
const db = await getDatabase();
|
||||
const searchTermLower = searchTerm.toLocaleLowerCase();
|
||||
const likePatterns = buildLikePatterns(searchTerm);
|
||||
@@ -270,6 +271,19 @@ ipcMain.handle(
|
||||
|
||||
// Pre-filter with SQL LIKE (case-insensitive for ASCII, handled in native C code).
|
||||
// This avoids loading the entire content table into JS memory.
|
||||
const conditions = [
|
||||
eq(schema.categories.playlistId, playlistId),
|
||||
inArray(
|
||||
schema.content.type,
|
||||
types as Array<'live' | 'movie' | 'series'>
|
||||
),
|
||||
or(...likeConditions),
|
||||
];
|
||||
|
||||
if (excludeHidden) {
|
||||
conditions.push(eq(schema.categories.hidden, false));
|
||||
}
|
||||
|
||||
const candidates = await db
|
||||
.select({
|
||||
id: schema.content.id,
|
||||
@@ -286,16 +300,7 @@ ipcMain.handle(
|
||||
schema.categories,
|
||||
eq(schema.content.categoryId, schema.categories.id)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.categories.playlistId, playlistId),
|
||||
inArray(
|
||||
schema.content.type,
|
||||
types as Array<'live' | 'movie' | 'series'>
|
||||
),
|
||||
or(...likeConditions)
|
||||
)
|
||||
)
|
||||
.where(and(...conditions))
|
||||
.limit(200);
|
||||
|
||||
// Post-filter for proper Unicode case-insensitive matching
|
||||
@@ -319,8 +324,9 @@ ipcMain.handle(
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'DB_GLOBAL_SEARCH',
|
||||
async (event, searchTerm: string, types: string[]) => {
|
||||
async (event, searchTerm: string, types: string[], excludeHidden = false) => {
|
||||
try {
|
||||
if (!types || types.length === 0) return [];
|
||||
const db = await getDatabase();
|
||||
const searchTermLower = searchTerm.toLocaleLowerCase();
|
||||
const likePatterns = buildLikePatterns(searchTerm);
|
||||
@@ -332,6 +338,18 @@ ipcMain.handle(
|
||||
// This avoids loading the entire content table into JS memory.
|
||||
// Previously this query loaded ALL content rows (potentially 100k+),
|
||||
// created JS objects for each, then filtered in JS — blocking the main process.
|
||||
const conditions = [
|
||||
inArray(
|
||||
schema.content.type,
|
||||
types as Array<'live' | 'movie' | 'series'>
|
||||
),
|
||||
or(...likeConditions),
|
||||
];
|
||||
|
||||
if (excludeHidden) {
|
||||
conditions.push(eq(schema.categories.hidden, false));
|
||||
}
|
||||
|
||||
const candidates = await db
|
||||
.select({
|
||||
id: schema.content.id,
|
||||
@@ -354,15 +372,7 @@ ipcMain.handle(
|
||||
schema.playlists,
|
||||
eq(schema.categories.playlistId, schema.playlists.id)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
inArray(
|
||||
schema.content.type,
|
||||
types as Array<'live' | 'movie' | 'series'>
|
||||
),
|
||||
or(...likeConditions)
|
||||
)
|
||||
)
|
||||
.where(and(...conditions))
|
||||
.orderBy(schema.content.title)
|
||||
.limit(200);
|
||||
|
||||
|
||||
@@ -318,10 +318,6 @@ export default class EpgEvents {
|
||||
|
||||
case 'EPG_PROGRESS':
|
||||
if (message.stats) {
|
||||
console.log(
|
||||
this.loggerLabel,
|
||||
`Progress: ${message.stats.totalChannels} channels, ${message.stats.totalPrograms} programs`
|
||||
);
|
||||
// Forward progress to renderer
|
||||
this.sendProgressToRenderer(
|
||||
url,
|
||||
|
||||
@@ -240,9 +240,10 @@ export class ElectronXtreamDataSource implements IXtreamDataSource {
|
||||
async searchContent(
|
||||
playlistId: string,
|
||||
searchTerm: string,
|
||||
types: string[]
|
||||
types: string[],
|
||||
excludeHidden?: boolean
|
||||
): Promise<XtreamContentItem[]> {
|
||||
return this.dbService.searchXtreamContent(playlistId, searchTerm, types);
|
||||
return this.dbService.searchXtreamContent(playlistId, searchTerm, types, excludeHidden);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
|
||||
@@ -221,7 +221,8 @@ export class PwaXtreamDataSource implements IXtreamDataSource {
|
||||
async searchContent(
|
||||
playlistId: string,
|
||||
searchTerm: string,
|
||||
types: string[]
|
||||
types: string[],
|
||||
_excludeHidden?: boolean
|
||||
): Promise<XtreamContentItem[]> {
|
||||
const results: any[] = [];
|
||||
const searchLower = searchTerm.toLowerCase();
|
||||
|
||||
@@ -234,7 +234,8 @@ export interface IXtreamDataSource {
|
||||
searchContent(
|
||||
playlistId: string,
|
||||
searchTerm: string,
|
||||
types: string[]
|
||||
types: string[],
|
||||
excludeHidden?: boolean
|
||||
): Promise<XtreamContentItem[]>;
|
||||
|
||||
// =========================================================================
|
||||
|
||||
@@ -19,6 +19,15 @@
|
||||
}
|
||||
</ng-container>
|
||||
|
||||
<ng-container header-actions>
|
||||
<mat-checkbox
|
||||
[ngModel]="excludeHidden()"
|
||||
(ngModelChange)="toggleExcludeHidden($event)"
|
||||
>
|
||||
{{ 'PORTALS.SEARCH_VIEW.VISIBLE_CATEGORIES_ONLY' | translate }}
|
||||
</mat-checkbox>
|
||||
</ng-container>
|
||||
|
||||
<ng-container subheader>
|
||||
@if (isGlobalSearch) {
|
||||
<div class="global-search-subheader">
|
||||
|
||||
@@ -63,6 +63,7 @@ export class SearchResultsComponent implements AfterViewInit {
|
||||
readonly filters = this.xtreamStore.searchFilters;
|
||||
|
||||
private static readonly GROUP_BY_STORAGE_KEY = 'global-search-group-by-playlist';
|
||||
private static readonly EXCLUDE_HIDDEN_STORAGE_KEY = 'xtream-search-exclude-hidden';
|
||||
|
||||
isGlobalSearch = false;
|
||||
|
||||
@@ -71,6 +72,11 @@ export class SearchResultsComponent implements AfterViewInit {
|
||||
localStorage.getItem(SearchResultsComponent.GROUP_BY_STORAGE_KEY) !== 'false'
|
||||
);
|
||||
|
||||
/** Whether to exclude content from hidden categories */
|
||||
readonly excludeHidden = signal(
|
||||
localStorage.getItem(SearchResultsComponent.EXCLUDE_HIDDEN_STORAGE_KEY) === 'true'
|
||||
);
|
||||
|
||||
readonly filterConfig = [
|
||||
{
|
||||
key: 'live' as keyof SearchFilters,
|
||||
@@ -125,13 +131,15 @@ export class SearchResultsComponent implements AfterViewInit {
|
||||
const types = Object.entries(filters)
|
||||
.filter(([_, enabled]) => enabled)
|
||||
.map(([type]) => type);
|
||||
const excludeHidden = this.excludeHidden();
|
||||
|
||||
if (this.isGlobalSearch) {
|
||||
this.searchGlobal(this.searchTerm(), types);
|
||||
this.searchGlobal(this.searchTerm(), types, excludeHidden);
|
||||
} else {
|
||||
this.xtreamStore.searchContent({
|
||||
term: this.searchTerm(),
|
||||
types,
|
||||
excludeHidden,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -160,12 +168,13 @@ export class SearchResultsComponent implements AfterViewInit {
|
||||
this.xtreamStore.setGlobalSearchResults([]);
|
||||
}
|
||||
|
||||
async searchGlobal(term: string, types: string[]) {
|
||||
async searchGlobal(term: string, types: string[], excludeHidden?: boolean) {
|
||||
this.xtreamStore.setIsSearching(true);
|
||||
try {
|
||||
const results = await this.databaseService.globalSearchContent(
|
||||
term,
|
||||
types
|
||||
types,
|
||||
excludeHidden
|
||||
);
|
||||
if (results && Array.isArray(results)) {
|
||||
this.xtreamStore.setGlobalSearchResults(results);
|
||||
@@ -211,6 +220,14 @@ export class SearchResultsComponent implements AfterViewInit {
|
||||
localStorage.setItem(SearchResultsComponent.GROUP_BY_STORAGE_KEY, String(value));
|
||||
}
|
||||
|
||||
toggleExcludeHidden(value: boolean) {
|
||||
this.excludeHidden.set(value);
|
||||
localStorage.setItem(SearchResultsComponent.EXCLUDE_HIDDEN_STORAGE_KEY, String(value));
|
||||
if (this.searchTerm().length >= 3) {
|
||||
this.executeSearch();
|
||||
}
|
||||
}
|
||||
|
||||
getGroupedResults() {
|
||||
return this.groupedResults();
|
||||
}
|
||||
|
||||
@@ -70,7 +70,8 @@ export function withSearch() {
|
||||
*/
|
||||
async searchContent(
|
||||
searchTerm: string,
|
||||
types: string[]
|
||||
types: string[],
|
||||
excludeHidden?: boolean
|
||||
): Promise<XtreamContentItem[]> {
|
||||
// Access parent store's playlistId (from withPortal)
|
||||
const storeAny = store as any;
|
||||
@@ -87,7 +88,8 @@ export function withSearch() {
|
||||
const results = await dataSource.searchContent(
|
||||
playlistId,
|
||||
searchTerm,
|
||||
types
|
||||
types,
|
||||
excludeHidden
|
||||
);
|
||||
|
||||
patchState(store, {
|
||||
|
||||
@@ -81,7 +81,7 @@ export const XtreamStore = signalStore(
|
||||
const xtreamApiService = inject(XtreamApiService);
|
||||
const ngrxStore = inject(Store);
|
||||
const searchContent = (store as any)
|
||||
.searchContent as (term: string, types: string[]) => Promise<unknown>;
|
||||
.searchContent as (term: string, types: string[], excludeHidden?: boolean) => Promise<unknown>;
|
||||
|
||||
return {
|
||||
/**
|
||||
@@ -218,11 +218,11 @@ export const XtreamStore = signalStore(
|
||||
|
||||
/**
|
||||
* Search content wrapper for rxMethod compatibility
|
||||
* Can be called with object { term, types } or direct params
|
||||
* Can be called with object { term, types, excludeHidden } or direct params
|
||||
*/
|
||||
searchContent(params: { term: string; types: string[] }): void {
|
||||
searchContent(params: { term: string; types: string[]; excludeHidden?: boolean }): void {
|
||||
// Call the underlying search method from withSearch
|
||||
void searchContent(params.term, params.types);
|
||||
void searchContent(params.term, params.types, params.excludeHidden);
|
||||
},
|
||||
};
|
||||
})
|
||||
|
||||
@@ -446,7 +446,8 @@
|
||||
"RESULTS_FOUND": "Found {{count}} results",
|
||||
"NO_RESULTS_FOR": "No results found for \"{{term}}\"",
|
||||
"GROUP_BY_PLAYLIST": "Group results by playlist",
|
||||
"XTREAM_ONLY_NOTE": "Global search is currently available for Xtream playlists only."
|
||||
"XTREAM_ONLY_NOTE": "Global search is currently available for Xtream playlists only.",
|
||||
"VISIBLE_CATEGORIES_ONLY": "Visible categories only"
|
||||
},
|
||||
"STREAM_URL_COPIED": "Stream URL copied to clipboard",
|
||||
"COPY_STREAM_URL": "Copy Stream URL",
|
||||
|
||||
Vendored
+4
-2
@@ -150,11 +150,13 @@ declare global {
|
||||
dbSearchContent: (
|
||||
playlistId: string,
|
||||
searchTerm: string,
|
||||
types: string[]
|
||||
types: string[],
|
||||
excludeHidden?: boolean
|
||||
) => Promise<any[]>;
|
||||
dbGlobalSearch: (
|
||||
searchTerm: string,
|
||||
types: string[]
|
||||
types: string[],
|
||||
excludeHidden?: boolean
|
||||
) => Promise<any[]>;
|
||||
dbGetRecentlyViewed: () => Promise<any[]>;
|
||||
dbClearRecentlyViewed: () => Promise<{ success: boolean }>;
|
||||
|
||||
Vendored
+4
-2
@@ -153,11 +153,13 @@ declare global {
|
||||
dbSearchContent: (
|
||||
playlistId: string,
|
||||
searchTerm: string,
|
||||
types: string[]
|
||||
types: string[],
|
||||
excludeHidden?: boolean
|
||||
) => Promise<any[]>;
|
||||
dbGlobalSearch: (
|
||||
searchTerm: string,
|
||||
types: string[]
|
||||
types: string[],
|
||||
excludeHidden?: boolean
|
||||
) => Promise<any[]>;
|
||||
dbGetRecentlyViewed: () => Promise<any[]>;
|
||||
dbClearRecentlyViewed: () => Promise<{ success: boolean }>;
|
||||
|
||||
@@ -261,12 +261,14 @@ export class DatabaseService {
|
||||
async searchXtreamContent(
|
||||
playlistId: string,
|
||||
searchTerm: string,
|
||||
types: string[]
|
||||
types: string[],
|
||||
excludeHidden?: boolean
|
||||
): Promise<XtreamContent[]> {
|
||||
return await window.electron.dbSearchContent(
|
||||
playlistId,
|
||||
searchTerm,
|
||||
types
|
||||
types,
|
||||
excludeHidden
|
||||
);
|
||||
}
|
||||
|
||||
@@ -275,9 +277,10 @@ export class DatabaseService {
|
||||
*/
|
||||
async globalSearchContent(
|
||||
searchTerm: string,
|
||||
types: string[]
|
||||
types: string[],
|
||||
excludeHidden?: boolean
|
||||
): Promise<GlobalSearchResult[]> {
|
||||
return await window.electron.dbGlobalSearch(searchTerm, types);
|
||||
return await window.electron.dbGlobalSearch(searchTerm, types, excludeHidden);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user