chore: update Angular cache and TypeScript build info files
This commit is contained in:
@@ -14,114 +14,10 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Center: search (grid-centered, avoids overlap) -->
|
||||
<form class="justify-self-center mx-auto w-full max-w-2xl md:max-w-3xl lg:max-w-4xl px-4 relative group" (submit)="onSubmitSearch($event, searchInput)">
|
||||
<input #searchInput type="search" [placeholder]="('search.placeholder' | t)" aria-label="Search"
|
||||
(focus)="onSearchFocus()" (input)="onSearchInput(searchInput)" (blur)="onSearchBlur()" (keydown)="onSearchKeydown($event, searchInput)"
|
||||
(search)="onSearchCleared(searchInput)"
|
||||
[ngClass]="{
|
||||
'bg-white text-slate-900 placeholder-slate-500 ring-slate-300 hover:bg-slate-50': isLightTheme(),
|
||||
'bg-slate-700/80 text-slate-200 placeholder-slate-400 ring-slate-600/50 hover:bg-slate-700/70': isDarkTheme(),
|
||||
'bg-black text-slate-200 placeholder-slate-400 ring-slate-800/50 hover:bg-slate-900': isBlackTheme(),
|
||||
'bg-blue-950/80 text-blue-100 placeholder-blue-300 ring-blue-900/60 hover:bg-blue-900/70': isBlueTheme()
|
||||
}"
|
||||
class="w-full rounded-lg py-2.5 px-4 pl-10 pr-16 focus:outline-none ring-1 focus:ring-2 focus:ring-red-500/70 transition-all duration-200 shadow-sm">
|
||||
<svg class="w-5 h-5 absolute left-6 md:left-5 top-1/2 -translate-y-1/2"
|
||||
[ngClass]="{
|
||||
'text-slate-500': isLightTheme(),
|
||||
'text-slate-400': isDarkTheme(),
|
||||
'text-slate-300': isBlackTheme(),
|
||||
'text-blue-200': isBlueTheme()
|
||||
}"
|
||||
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
<!-- Keyboard shortcut hint (only when no provider indicator) -->
|
||||
<div *ngIf="!providerContextLabel()" class="absolute right-6 top-1/2 -translate-y-1/2 hidden md:flex items-center gap-1 text-xs select-none"
|
||||
[ngClass]="{
|
||||
'text-slate-700/80': isLightTheme(),
|
||||
'text-slate-300/80': isDarkTheme() || isBlackTheme(),
|
||||
'text-blue-200/80': isBlueTheme()
|
||||
}">
|
||||
<kbd [ngClass]="{
|
||||
'bg-slate-200/80 border-slate-300 text-slate-700': isLightTheme(),
|
||||
'bg-slate-700/80 border-slate-600/60': isDarkTheme(),
|
||||
'bg-slate-900/80 border-slate-800/60 text-slate-300': isBlackTheme(),
|
||||
'bg-blue-800/80 border-blue-700/60 text-blue-200': isBlueTheme()
|
||||
}" class="px-1.5 py-0.5 rounded border">Ctrl</kbd>
|
||||
<span>+</span>
|
||||
<kbd [ngClass]="{
|
||||
'bg-slate-200/80 border-slate-300 text-slate-700': isLightTheme(),
|
||||
'bg-slate-700/80 border-slate-600/60 text-slate-300': isDarkTheme(),
|
||||
'bg-slate-900/80 border-slate-800/60 text-slate-300': isBlackTheme(),
|
||||
'bg-blue-800/80 border-blue-700/60 text-blue-200': isBlueTheme()
|
||||
}" class="px-1.5 py-0.5 rounded border">K</kbd>
|
||||
</div>
|
||||
<!-- Suggestions dropdown -->
|
||||
@if (suggestionsOpen() && suggestionItems().length > 0) {
|
||||
<div class="absolute z-50 mt-2 left-1/2 -translate-x-1/2 w-full max-w-2xl md:max-w-3xl lg:max-w-4xl rounded-xl shadow-2xl overflow-hidden"
|
||||
[ngClass]="{
|
||||
'bg-white border border-slate-200 text-slate-900': isLightTheme(),
|
||||
'bg-slate-800/95 border border-slate-700 text-slate-200': isDarkTheme(),
|
||||
'bg-black/95 border border-slate-900 text-slate-200': isBlackTheme(),
|
||||
'bg-blue-950/90 border border-blue-800 text-blue-100': isBlueTheme(),
|
||||
'backdrop-blur-md': !isLightTheme()
|
||||
}">
|
||||
<ul class="max-h-[70vh] overflow-y-auto py-1">
|
||||
@for (item of suggestionItems(); track $index; let i = $index) {
|
||||
<!-- Divider before first generated suggestion -->
|
||||
@if (isFirstGenerated(i)) {
|
||||
<li class="px-4 py-2 text-xs uppercase tracking-wide border-t border-b"
|
||||
[ngClass]="{
|
||||
'text-slate-500 border-slate-100': isLightTheme(),
|
||||
'text-slate-400 border-slate-700': isDarkTheme(),
|
||||
'text-slate-400 border-slate-800': isBlackTheme(),
|
||||
'text-blue-300 border-blue-800': isBlueTheme()
|
||||
}">Suggestions</li>
|
||||
}
|
||||
<li (mouseenter)="highlightedIndex.set(i)">
|
||||
<button type="button" (mousedown)="pickSuggestion(item.text, searchInput)"
|
||||
class="w-full text-left px-4 py-2 flex items-center gap-3 transition-colors duration-150"
|
||||
[ngClass]="{
|
||||
'hover:bg-slate-100': isLightTheme(),
|
||||
'hover:bg-slate-700/70': isDarkTheme(),
|
||||
'hover:bg-slate-900': isBlackTheme(),
|
||||
'hover:bg-blue-800/60': isBlueTheme(),
|
||||
'bg-slate-200': isLightTheme() && highlightedIndex() === i,
|
||||
'bg-slate-700/50': isDarkTheme() && highlightedIndex() === i,
|
||||
'bg-slate-900/70': isBlackTheme() && highlightedIndex() === i,
|
||||
'bg-blue-800/50': isBlueTheme() && highlightedIndex() === i,
|
||||
'text-slate-900': isLightTheme(),
|
||||
'text-slate-200': isDarkTheme() || isBlackTheme(),
|
||||
'text-blue-100': isBlueTheme()
|
||||
}">
|
||||
<!-- Clock for history, magnifier for generated -->
|
||||
<svg *ngIf="item.source === 'history'" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5"
|
||||
[ngClass]="{
|
||||
'text-slate-500': isLightTheme(),
|
||||
'text-slate-400': isDarkTheme(),
|
||||
'text-slate-300': isBlackTheme(),
|
||||
'text-blue-200': isBlueTheme()
|
||||
}" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<svg *ngIf="item.source === 'generated'" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5"
|
||||
[ngClass]="{
|
||||
'text-slate-500': isLightTheme(),
|
||||
'text-slate-400': isDarkTheme(),
|
||||
'text-slate-300': isBlackTheme(),
|
||||
'text-blue-200': isBlueTheme()
|
||||
}" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
<span class="truncate">{{ item.text }}</span>
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
<!-- Center: search box with providers -->
|
||||
<div class="justify-self-center mx-auto w-full max-w-2xl md:max-w-3xl lg:max-w-4xl px-4 relative">
|
||||
<app-search-box [placeholder]="('search.placeholder' | t)" (submitted)="onSearchBoxSubmit($event)"></app-search-box>
|
||||
</div>
|
||||
|
||||
<!-- Right: user -->
|
||||
<div class="justify-self-end flex items-center gap-3 pr-3 md:pr-4">
|
||||
|
||||
@@ -10,6 +10,7 @@ import { TranslatePipe } from '../../pipes/translate.pipe';
|
||||
import { I18nService } from '../../services/i18n.service';
|
||||
import { ThemesService } from '../../services/themes.service';
|
||||
import { HistoryService, SearchHistoryItem } from '../../services/history.service';
|
||||
import { SearchBoxComponent } from '../search/search-box.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-header',
|
||||
@@ -20,7 +21,9 @@ import { HistoryService, SearchHistoryItem } from '../../services/history.servic
|
||||
RouterLink,
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
TranslatePipe
|
||||
TranslatePipe,
|
||||
// New unified search box with provider chips and picker
|
||||
SearchBoxComponent
|
||||
]
|
||||
})
|
||||
export class HeaderComponent {
|
||||
@@ -60,7 +63,7 @@ export class HeaderComponent {
|
||||
const lower = q.toLowerCase();
|
||||
historyList = fromHistory.filter(txt => txt.toLowerCase().includes(lower)).slice(0, 15);
|
||||
}
|
||||
|
||||
|
||||
const items: Item[] = historyList.map(text => ({ text, source: 'history' }));
|
||||
|
||||
// 2) If we have fewer than 15, fill with generated suggestions
|
||||
@@ -85,6 +88,23 @@ export class HeaderComponent {
|
||||
return items;
|
||||
});
|
||||
|
||||
// New SearchBox submit handler (navigate with providers list)
|
||||
onSearchBoxSubmit(evt: { q: string; providers: ("yt"|"dm"|"tw"|"pt"|"od"|"ru")[] | 'all' }) {
|
||||
const q = (evt?.q || '').trim();
|
||||
if (!q) return;
|
||||
const provider = this.selectedProvider();
|
||||
const theme = this.themes.activeSlug();
|
||||
const qp: any = { q, provider };
|
||||
if (Array.isArray(evt.providers)) {
|
||||
qp.providers = evt.providers.join(',');
|
||||
} else if (evt.providers === 'all') {
|
||||
// Explicitly include all providers so SearchComponent uses unified multi-provider mode
|
||||
qp.providers = 'yt,dm,tw,pt,od,ru';
|
||||
}
|
||||
if (theme) qp.theme = theme;
|
||||
this.router.navigate(['/search'], { queryParams: qp });
|
||||
}
|
||||
|
||||
// Generate query suggestions based on the current text and provider
|
||||
private generateQuerySuggestions(q: string, provider: string | null): string[] {
|
||||
const base = (q || '').trim();
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<div class="fixed inset-0 z-[9999] bg-black/60 flex justify-center items-center" (click)="close.emit()">
|
||||
<div class="fixed inset-0 grid place-items-center">
|
||||
<div #dialog
|
||||
class="w-[90%] max-w-lg bg-slate-900 rounded-xl shadow-2xl border border-slate-700"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="providersTitle"
|
||||
(click)="$event.stopPropagation()"
|
||||
tabindex="0"
|
||||
(keydown)="onDialogKeydown($event)">
|
||||
<div class="p-4 border-b border-slate-700 flex items-center justify-between">
|
||||
<h2 id="providersTitle" class="text-slate-100 font-semibold">Providers</h2>
|
||||
<button (click)="close.emit()" class="px-2 py-1 rounded bg-slate-700 hover:bg-slate-600 text-slate-100">Esc</button>
|
||||
</div>
|
||||
<div class="p-4 space-y-3">
|
||||
<input type="text" [ngModel]="filter()" (ngModelChange)="filter.set($event)" placeholder="filter providers…" class="w-full px-3 py-2 rounded bg-slate-800 text-slate-100 placeholder-slate-400 border border-slate-700"/>
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="inline-flex items-center gap-2 text-slate-200">
|
||||
<input type="checkbox" [checked]="all()" (change)="toggleAll()"/>
|
||||
<span>All</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<label *ngFor="let p of providers()" class="flex items-center gap-2 px-2 py-2 rounded border border-slate-700 bg-slate-800 text-slate-100">
|
||||
<input type="checkbox" [checked]="isChecked(p.id)" (change)="toggle(p.id)"/>
|
||||
<span class="text-xs font-semibold" [class]="p.colorClass">{{ p.shortLabel }}</span>
|
||||
<span class="text-sm">{{ p.displayName }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<label class="mt-2 inline-flex items-center gap-2 text-slate-300">
|
||||
<input type="checkbox" [ngModel]="rememberDefault()" (ngModelChange)="rememberDefault.set($event)" />
|
||||
<span>Remember as default</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="p-4 border-t border-slate-700 flex items-center justify-end gap-2">
|
||||
<button (click)="close.emit()" class="px-3 py-2 rounded bg-slate-700 hover:bg-slate-600 text-slate-100">Cancel</button>
|
||||
<button (click)="onApply()" class="px-3 py-2 rounded bg-red-600 hover:bg-red-500 text-white">Apply filters</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Component, EventEmitter, Output, Input, signal, computed, ElementRef, ViewChild, AfterViewInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ProviderId, PROVIDERS } from '../../app/core/providers/provider-registry';
|
||||
|
||||
@Component({
|
||||
selector: 'app-provider-picker',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule],
|
||||
templateUrl: './provider-picker.component.html'
|
||||
})
|
||||
export class ProviderPickerComponent implements AfterViewInit {
|
||||
@Input() open = false;
|
||||
@Input() selected: ProviderId[] | 'all' = 'all';
|
||||
@Output() close = new EventEmitter<void>();
|
||||
@Output() apply = new EventEmitter<{ selection: ProviderId[] | 'all'; rememberDefault: boolean }>();
|
||||
|
||||
filter = signal('');
|
||||
rememberDefault = signal(false);
|
||||
|
||||
@ViewChild('dialog', { static: false }) dialogRef?: ElementRef<HTMLDivElement>;
|
||||
|
||||
readonly all = computed(() => this.selected === 'all');
|
||||
readonly providers = computed(() => {
|
||||
const q = this.filter().toLowerCase();
|
||||
return PROVIDERS.filter(p => !q || p.displayName.toLowerCase().includes(q) || p.shortLabel.toLowerCase().includes(q));
|
||||
});
|
||||
|
||||
toggleAll() {
|
||||
this.apply.emit({ selection: 'all', rememberDefault: this.rememberDefault() });
|
||||
}
|
||||
|
||||
isChecked(id: ProviderId): boolean {
|
||||
if (this.selected === 'all') return true;
|
||||
return Array.isArray(this.selected) && this.selected.includes(id);
|
||||
}
|
||||
|
||||
toggle(id: ProviderId) {
|
||||
if (this.selected === 'all') {
|
||||
this.apply.emit({ selection: [id], rememberDefault: this.rememberDefault() });
|
||||
return;
|
||||
}
|
||||
const set = new Set(this.selected || []);
|
||||
if (set.has(id)) set.delete(id); else set.add(id);
|
||||
const next = Array.from(set) as ProviderId[];
|
||||
this.selected = next;
|
||||
}
|
||||
|
||||
onApply() {
|
||||
this.apply.emit({ selection: this.selected, rememberDefault: this.rememberDefault() });
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
// Focus the dialog for accessibility
|
||||
queueMicrotask(() => {
|
||||
try { this.dialogRef?.nativeElement?.focus(); } catch {}
|
||||
});
|
||||
}
|
||||
|
||||
// Basic focus trap within the dialog
|
||||
onDialogKeydown(ev: KeyboardEvent) {
|
||||
if (ev.key !== 'Tab') return;
|
||||
const root = this.dialogRef?.nativeElement;
|
||||
if (!root) return;
|
||||
const focusables = Array.from(root.querySelectorAll<HTMLElement>('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'))
|
||||
.filter(el => !el.hasAttribute('disabled'));
|
||||
if (focusables.length === 0) return;
|
||||
const first = focusables[0];
|
||||
const last = focusables[focusables.length - 1];
|
||||
const active = document.activeElement as HTMLElement | null;
|
||||
if (!ev.shiftKey && active === last) {
|
||||
ev.preventDefault();
|
||||
first.focus();
|
||||
} else if (ev.shiftKey && active === first) {
|
||||
ev.preventDefault();
|
||||
last.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<form class="w-full relative" (submit)="onSubmit($event)" (keydown)="handleKeydown($event)">
|
||||
<!-- Input -->
|
||||
<div class="flex items-center gap-2 rounded-lg ring-1 ring-slate-600/50 bg-slate-800/70 px-3 py-2">
|
||||
<!-- Provider chips -->
|
||||
<button type="button"
|
||||
(click)="toggleAll()"
|
||||
class="text-xs px-2 py-0.5 rounded border"
|
||||
[ngClass]="{
|
||||
'bg-slate-200 text-slate-800 border-slate-300': providerMode()==='all',
|
||||
'bg-slate-700 text-slate-100 border-slate-600': providerMode()!=='all'
|
||||
}"
|
||||
title="Tous les fournisseurs">
|
||||
All
|
||||
</button>
|
||||
<ng-container *ngFor="let c of chips; let i = index">
|
||||
<button type="button" (click)="toggleChip(c.id)" (keydown.space)="$event.preventDefault(); toggleChip(c.id)" (keydown.enter)="$event.preventDefault(); toggleChip(c.id)" tabindex="0" role="button"
|
||||
class="text-xs px-2 py-0.5 rounded border"
|
||||
[ngClass]="{
|
||||
'bg-slate-700 text-slate-100 border-slate-600': !(selected().includes(c.id)),
|
||||
'bg-slate-200 text-slate-800 border-slate-300': selected().includes(c.id)
|
||||
}"
|
||||
[title]="'Inclure ' + c.label">
|
||||
<span [class]="c.colorClass">{{ c.label }}</span>
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<!-- Text input -->
|
||||
<input type="search"
|
||||
class="flex-1 bg-transparent outline-none text-slate-100 placeholder-slate-400 px-2"
|
||||
[placeholder]="placeholder"
|
||||
[ngModel]="query()" (ngModelChange)="queryUpdate($event)" (keydown)="handleKeydown($event)" name="q"/>
|
||||
|
||||
<!-- Actions -->
|
||||
<button type="button" (click)="openPicker()" class="text-xs px-2 py-1 rounded bg-slate-700 hover:bg-slate-600">@</button>
|
||||
<button type="submit" class="ml-1 px-3 py-1 rounded bg-red-600 hover:bg-red-500 text-white">Search</button>
|
||||
</div>
|
||||
|
||||
<!-- Provider Picker Modal -->
|
||||
<app-provider-picker *ngIf="pickerOpen()"
|
||||
[open]="pickerOpen()"
|
||||
[selected]="providersForRequest()"
|
||||
(close)="closePicker()"
|
||||
(apply)="applyPicker($event)"></app-provider-picker>
|
||||
|
||||
<!-- Inline @autocomplete popover -->
|
||||
<div *ngIf="atOpen() && atOptions().length > 0"
|
||||
class="absolute z-50 mt-1 left-3 right-3 rounded-lg shadow-xl overflow-hidden"
|
||||
style="top: 100%">
|
||||
<ul class="max-h-64 overflow-y-auto"
|
||||
[ngClass]="{ 'bg-slate-900 border border-slate-700 text-slate-200': true }">
|
||||
<li *ngFor="let opt of atOptions(); let i = index">
|
||||
<button type="button" (click)="toggleChip(opt.id); atOpen.set(false)"
|
||||
class="w-full text-left px-3 py-2 text-sm"
|
||||
[ngClass]="{
|
||||
'bg-slate-800': i === atIndex(),
|
||||
'hover:bg-slate-800': i !== atIndex()
|
||||
}">
|
||||
@ {{ opt.label }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Quick Menu (Ctrl/Cmd+K) -->
|
||||
<div *ngIf="quickOpen()" style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 9999; background-color: rgba(0, 0, 0, 0.6);" role="dialog" aria-modal="true" (click)="closeQuickMenu()">
|
||||
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -0%); width: 90%; max-width: 56rem; max-height: 85vh; border-radius: 0.75rem; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); overflow: hidden; background-color: #0f172a; border: 1px solid #334155; display: flex; flex-direction: column;" role="document" aria-labelledby="quickMenuTitle" (click)="$event.stopPropagation()" tabindex="0">
|
||||
<div class="p-3 border-b border-slate-700 flex items-center justify-between">
|
||||
<h2 id="quickMenuTitle" class="text-slate-100 font-semibold">Quick Menu</h2>
|
||||
<button (click)="closeQuickMenu()" class="px-2 py-1 rounded bg-slate-700 hover:bg-slate-600 text-slate-100" autofocus>Esc</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-0 overflow-y-auto">
|
||||
<!-- History column -->
|
||||
<div class="p-4 border-r border-slate-700">
|
||||
<div class="text-slate-300 text-sm mb-2">History</div>
|
||||
<ul class="space-y-1 max-h-80 overflow-y-auto">
|
||||
<li *ngFor="let it of recentSearches()">
|
||||
<button type="button" class="w-full text-left px-2 py-1 rounded hover:bg-slate-800 text-slate-200"
|
||||
(click)="queryUpdate(it.query || '')">{{ it.query }}</button>
|
||||
</li>
|
||||
<li *ngIf="(recentSearches()?.length || 0) === 0" class="text-slate-500 text-sm">No recent searches.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- Providers column -->
|
||||
<div class="p-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="text-slate-300 text-sm">Providers</div>
|
||||
<button type="button" class="text-xs px-2 py-1 rounded bg-slate-700 hover:bg-slate-600 text-slate-100"
|
||||
(click)="quickToggleAll()">All</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<label *ngFor="let c of chips" class="flex items-center gap-2 px-2 py-2 rounded border border-slate-700 bg-slate-800 text-slate-100">
|
||||
<input type="checkbox" [checked]="selected().includes(c.id)" (change)="quickToggle(c.id)"/>
|
||||
<span class="text-xs font-semibold" [class]="c.colorClass">{{ c.label }}</span>
|
||||
<span class="text-sm">{{ c.label }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="mt-4 text-right">
|
||||
<button type="button" class="px-3 py-2 rounded bg-red-600 hover:bg-red-500 text-white"
|
||||
(click)="applyQuickMenu({ selection: selected().length ? selected() : 'all', rememberDefault: false })">Apply filters</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,247 @@
|
||||
import { Component, EventEmitter, Output, Input, signal, computed, effect, inject, HostListener } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ProviderId, PROVIDERS } from '../../app/core/providers/provider-registry';
|
||||
import { ProviderPickerComponent } from './provider-picker.component';
|
||||
import { SearchService } from '../../app/search/search.service';
|
||||
import { UserService, type UserPreferences } from '../../services/user.service';
|
||||
import { HistoryService, type SearchHistoryItem } from '../../services/history.service';
|
||||
// No overlays; we render fixed-position modals with high z-index
|
||||
|
||||
@Component({
|
||||
selector: 'app-search-box',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, ProviderPickerComponent],
|
||||
templateUrl: './search-box.component.html'
|
||||
})
|
||||
export class SearchBoxComponent {
|
||||
private search = inject(SearchService);
|
||||
private users = inject(UserService);
|
||||
private history = inject(HistoryService);
|
||||
|
||||
// Input/Output bindings
|
||||
@Input() placeholder: string = 'Search videos…';
|
||||
@Output() submitted = new EventEmitter<{ q: string; providers: ProviderId[] | 'all' }>();
|
||||
|
||||
// Local state signals
|
||||
query = signal('');
|
||||
providerMode = signal<'all' | 'custom'>('all');
|
||||
selected = signal<ProviderId[]>([]);
|
||||
pickerOpen = signal(false);
|
||||
suggestionsOpen = signal(false);
|
||||
// Inline @autocomplete state
|
||||
atOpen = signal(false);
|
||||
atIndex = signal(0);
|
||||
atOptions = computed(() => {
|
||||
const q = this.query();
|
||||
const m = /(^|\s)@([a-z]{1,24})$/i.exec(q);
|
||||
if (!m) return [] as { id: ProviderId; label: string }[];
|
||||
const term = (m[2] || '').toLowerCase();
|
||||
const opts = PROVIDERS.filter(p =>
|
||||
p.shortLabel.toLowerCase().includes(term) || p.displayName.toLowerCase().includes(term)
|
||||
).map(p => ({ id: p.id, label: p.displayName }));
|
||||
this.atOpen.set(opts.length > 0);
|
||||
if (this.atIndex() >= opts.length) this.atIndex.set(0);
|
||||
return opts as any;
|
||||
});
|
||||
// Quick menu modal (Ctrl/Cmd+K)
|
||||
quickOpen = signal(false);
|
||||
recentSearches = signal<SearchHistoryItem[]>([]);
|
||||
|
||||
readonly chips = PROVIDERS.map(p => ({ id: p.id, label: p.shortLabel, colorClass: p.colorClass }));
|
||||
|
||||
// Derived state
|
||||
readonly providersForRequest = computed<ProviderId[] | 'all'>(() => {
|
||||
return this.providerMode() === 'all' ? 'all' : (this.selected().length ? this.selected() : 'all');
|
||||
});
|
||||
|
||||
constructor() {
|
||||
// Reflect SearchService into local state to restore from URL/preference
|
||||
effect(() => {
|
||||
const q = this.search.q$.value;
|
||||
const prov = this.search.providers$.value;
|
||||
if (typeof q === 'string' && q !== this.query()) this.query.set(q);
|
||||
if (prov === 'all') {
|
||||
this.providerMode.set('all');
|
||||
this.selected.set([]);
|
||||
} else if (Array.isArray(prov)) {
|
||||
this.providerMode.set('custom');
|
||||
this.selected.set(prov);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// Apply default providers from user preferences if none explicitly set
|
||||
effect(() => {
|
||||
const prefs: UserPreferences | null = this.users.preferences();
|
||||
if (!prefs) return;
|
||||
const list = (prefs as any).defaultProviders as string[] | undefined;
|
||||
if (Array.isArray(list) && list.length > 0) {
|
||||
// Only apply if current mode is 'all' and nothing selected
|
||||
if (this.providerMode() === 'all' && this.selected().length === 0) {
|
||||
// Cast to ProviderId[] while trusting server-provided list
|
||||
this.providerMode.set('custom');
|
||||
this.selected.set(list as any);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update query and handle inline '@provider' prefix to open filtered picker
|
||||
queryUpdate(value: string) {
|
||||
this.query.set(value);
|
||||
const m = /(^|\s)@([a-z]{1,12})$/i.exec(value);
|
||||
if (m) {
|
||||
const term = m[2] || '';
|
||||
this.pickerOpen.set(true);
|
||||
const lower = term.toLowerCase();
|
||||
const matched = PROVIDERS.filter(p => p.shortLabel.toLowerCase().startsWith(lower) || p.displayName.toLowerCase().includes(lower)).map(p => p.id);
|
||||
if (matched.length === 1) {
|
||||
this.toggleChip(matched[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle All chip
|
||||
toggleAll() {
|
||||
if (this.providerMode() === 'all') {
|
||||
// Switch to none selected (effectively prompt to pick)
|
||||
this.providerMode.set('custom');
|
||||
this.selected.set([]);
|
||||
} else {
|
||||
this.providerMode.set('all');
|
||||
this.selected.set([]);
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle individual chip
|
||||
toggleChip(id: ProviderId) {
|
||||
if (this.providerMode() === 'all') this.providerMode.set('custom');
|
||||
const cur = new Set(this.selected());
|
||||
if (cur.has(id)) cur.delete(id); else cur.add(id);
|
||||
const next = Array.from(cur);
|
||||
this.selected.set(next);
|
||||
if (next.length === 0) {
|
||||
// Empty selection warning UX could be handled by parent; keep custom mode
|
||||
}
|
||||
}
|
||||
|
||||
// Keyboard shortcuts: Alt+1..6 map to chips by order
|
||||
handleKeydown(ev: KeyboardEvent) {
|
||||
if (ev.key === '@') {
|
||||
ev.preventDefault();
|
||||
this.pickerOpen.set(true);
|
||||
return;
|
||||
}
|
||||
// Inline @autocomplete navigation
|
||||
if (this.atOpen()) {
|
||||
const opts = this.atOptions();
|
||||
if (ev.key === 'ArrowDown') { ev.preventDefault(); this.atIndex.set(Math.min(opts.length - 1, this.atIndex() + 1)); return; }
|
||||
if (ev.key === 'ArrowUp') { ev.preventDefault(); this.atIndex.set(Math.max(0, this.atIndex() - 1)); return; }
|
||||
if (ev.key === 'Enter') {
|
||||
ev.preventDefault();
|
||||
const pick = opts[this.atIndex()];
|
||||
if (pick) this.toggleChip(pick.id);
|
||||
this.atOpen.set(false);
|
||||
return;
|
||||
}
|
||||
if (ev.key === 'Escape') { this.atOpen.set(false); }
|
||||
}
|
||||
if (ev.altKey) {
|
||||
const map: Record<string, ProviderId | undefined> = { '1': 'yt', '2': 'dm', '3': 'tw', '4': 'pt', '5': 'od', '6': 'ru' };
|
||||
const pid = map[ev.key];
|
||||
if (pid) { ev.preventDefault(); this.toggleChip(pid); }
|
||||
}
|
||||
if (ev.key === 'Escape') {
|
||||
this.pickerOpen.set(false);
|
||||
this.suggestionsOpen.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Global shortcut Ctrl/Cmd+K to open the Providers picker
|
||||
@HostListener('document:keydown', ['$event'])
|
||||
onGlobalKey(ev: KeyboardEvent) {
|
||||
if ((ev.key.toLowerCase() === 'k') && (ev.ctrlKey || ev.metaKey)) {
|
||||
ev.preventDefault();
|
||||
this.quickOpen.set(true);
|
||||
// Load last 15 searches
|
||||
try { this.history.getSearchHistory(15).subscribe({ next: (items) => this.recentSearches.set(items || []), error: () => {} }); } catch {}
|
||||
}
|
||||
// Also support Ctrl+Shift+K explicitly
|
||||
if ((ev.key.toLowerCase() === 'k') && ev.ctrlKey && ev.shiftKey) {
|
||||
ev.preventDefault();
|
||||
this.quickOpen.set(true);
|
||||
}
|
||||
if (ev.key === 'Escape') {
|
||||
if (this.quickOpen()) {
|
||||
ev.preventDefault();
|
||||
this.closeQuickMenu();
|
||||
}
|
||||
if (this.pickerOpen()) {
|
||||
ev.preventDefault();
|
||||
this.closePicker();
|
||||
}
|
||||
if (this.atOpen()) {
|
||||
ev.preventDefault();
|
||||
this.atOpen.set(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onSubmit(ev: Event) {
|
||||
ev.preventDefault();
|
||||
const q = (this.query() || '').trim();
|
||||
if (!q) return;
|
||||
const prov = this.providersForRequest();
|
||||
// Update service for global listeners
|
||||
this.search.setQuery(q);
|
||||
this.search.setProviders(prov);
|
||||
this.submitted.emit({ q, providers: prov });
|
||||
}
|
||||
|
||||
openPicker() { this.pickerOpen.set(true); }
|
||||
closePicker() { this.pickerOpen.set(false); }
|
||||
|
||||
// Accept both legacy payload (array|'all') and new object payload; use any to satisfy strict template binder
|
||||
applyPicker(payload: any) {
|
||||
const isObj = payload && typeof payload === 'object' && !Array.isArray(payload);
|
||||
const prov = (isObj ? payload.selection : payload) as ProviderId[] | 'all';
|
||||
const rememberDefault = isObj ? !!payload.rememberDefault : false;
|
||||
if (prov === 'all') {
|
||||
this.providerMode.set('all');
|
||||
this.selected.set([]);
|
||||
} else {
|
||||
this.providerMode.set('custom');
|
||||
this.selected.set(prov);
|
||||
}
|
||||
// Persist default selection if requested and user is logged-in
|
||||
if (rememberDefault) {
|
||||
const body: any = { defaultProviders: (prov === 'all') ? [] : prov };
|
||||
try { this.users.updatePreferences(body).subscribe({ next: () => {}, error: () => {} }); } catch {}
|
||||
}
|
||||
this.closePicker();
|
||||
}
|
||||
|
||||
// Quick menu close/apply (re-using same payload shape as picker)
|
||||
closeQuickMenu() { this.quickOpen.set(false); }
|
||||
applyQuickMenu(payload: any) {
|
||||
this.applyPicker(payload);
|
||||
this.closeQuickMenu();
|
||||
}
|
||||
|
||||
// Quick menu provider toggles
|
||||
quickToggleAll() { this.applyQuickMenu({ selection: 'all', rememberDefault: false }); }
|
||||
quickToggle(id: ProviderId) {
|
||||
const set = new Set(this.selected());
|
||||
if (set.has(id)) set.delete(id); else set.add(id);
|
||||
this.selected.set(Array.from(set));
|
||||
}
|
||||
|
||||
// Lock body scroll when a modal is open (a11y UX)
|
||||
private _scrollLock = effect(() => {
|
||||
const open = this.quickOpen() || this.pickerOpen();
|
||||
try {
|
||||
document.body.style.overflow = open ? 'hidden' : '';
|
||||
} catch {}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<div class="mt-3 space-y-4">
|
||||
<!-- Grouped suggestion sections by provider -->
|
||||
<div *ngFor="let entry of (groups | keyvalue)" class="border border-slate-700 rounded-lg overflow-hidden">
|
||||
<div class="px-3 py-2 bg-slate-800 text-slate-200 text-sm font-semibold">
|
||||
{{ entry.key | uppercase }}
|
||||
</div>
|
||||
<ul class="divide-y divide-slate-700">
|
||||
<li *ngFor="let s of (entry.value | slice:0:3); let i = index" class="p-3 flex items-start gap-3">
|
||||
<img *ngIf="s.thumbnail" [src]="s.thumbnail" class="h-12 w-20 object-cover rounded" alt=""/>
|
||||
<div class="min-w-0">
|
||||
<div class="text-slate-100 truncate">{{ s.title }}</div>
|
||||
<div class="text-slate-400 text-xs">{{ s.uploaderName }}</div>
|
||||
</div>
|
||||
</li>
|
||||
<li *ngIf="(entry.value?.length || 0) === 0" class="p-3 text-slate-500 text-sm">
|
||||
Aucun résultat pour ce fournisseur.
|
||||
</li>
|
||||
</ul>
|
||||
<div class="px-3 py-2 bg-slate-900 text-right">
|
||||
<a class="text-xs text-red-300 hover:text-red-200"
|
||||
[routerLink]="['/search']"
|
||||
[queryParams]="{ q: query, providers: entry.key }"
|
||||
[attr.aria-label]="'View all results on ' + (entry.key | uppercase)">
|
||||
View all on {{ entry.key | uppercase }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import type { ProviderId } from '../../app/core/providers/provider-registry';
|
||||
import type { SuggestionItem } from '../../app/search/search.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-search-suggestions',
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterLink],
|
||||
templateUrl: './search-suggestions.component.html'
|
||||
})
|
||||
export class SearchSuggestionsComponent {
|
||||
@Input() groups: Record<ProviderId, SuggestionItem[]> = {
|
||||
yt: [], dm: [], tw: [], pt: [], od: [], ru: []
|
||||
} as any;
|
||||
@Input() query: string = '';
|
||||
}
|
||||
@@ -27,6 +27,11 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Unified multi-provider grouped suggestions -->
|
||||
@if (showUnified()) {
|
||||
<app-search-suggestions [groups]="groups()" [query]="q()"></app-search-suggestions>
|
||||
}
|
||||
|
||||
<input
|
||||
#searchInput
|
||||
type="text"
|
||||
|
||||
@@ -11,13 +11,16 @@ import { Title } from '@angular/platform-browser';
|
||||
import { TranslatePipe } from '../../pipes/translate.pipe';
|
||||
import { LikeButtonComponent } from '../shared/components/like-button/like-button.component';
|
||||
import { AddToPlaylistComponent } from '../shared/components/add-to-playlist/add-to-playlist.component';
|
||||
import { SearchSuggestionsComponent } from './search-suggestions.component';
|
||||
import { SearchService, type SearchResponse } from '../../app/search/search.service';
|
||||
import type { ProviderId } from '../../app/core/providers/provider-registry';
|
||||
|
||||
@Component({
|
||||
selector: 'app-search',
|
||||
standalone: true,
|
||||
templateUrl: './search.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [CommonModule, RouterLink, InfiniteAnchorComponent, TranslatePipe, LikeButtonComponent, AddToPlaylistComponent]
|
||||
imports: [CommonModule, RouterLink, InfiniteAnchorComponent, TranslatePipe, LikeButtonComponent, AddToPlaylistComponent, SearchSuggestionsComponent]
|
||||
})
|
||||
export class SearchComponent {
|
||||
private route = inject(ActivatedRoute);
|
||||
@@ -25,6 +28,7 @@ export class SearchComponent {
|
||||
private instances = inject(InstanceService);
|
||||
private history = inject(HistoryService);
|
||||
private title = inject(Title);
|
||||
private unified = inject(SearchService);
|
||||
|
||||
q = signal<string>('');
|
||||
loading = signal<boolean>(false);
|
||||
@@ -41,6 +45,11 @@ export class SearchComponent {
|
||||
notice = signal<string | null>(null);
|
||||
providerParam = signal<Provider | null>(null);
|
||||
themeParam = signal<string | null>(null);
|
||||
providersListParam = signal<ProviderId[] | 'all' | null>(null);
|
||||
|
||||
// Unified multi-provider response (grouped suggestions)
|
||||
groups = signal<Record<ProviderId, any[]>>({} as any);
|
||||
showUnified = signal<boolean>(false);
|
||||
|
||||
hasQuery = computed(() => this.q().length > 0);
|
||||
providerLabel = computed(() => {
|
||||
@@ -63,16 +72,51 @@ export class SearchComponent {
|
||||
filteredResults = computed(() => {
|
||||
const tag = this.filterTag();
|
||||
const provider = this.selectedProviderForView();
|
||||
|
||||
// Use unified results if available
|
||||
if (this.showUnified()) {
|
||||
const groups = this.groups();
|
||||
const list: Video[] = [];
|
||||
|
||||
// Convert unified groups to Video objects
|
||||
Object.entries(groups).forEach(([providerId, items]) => {
|
||||
items.forEach((item: any) => {
|
||||
list.push({
|
||||
videoId: item.id,
|
||||
title: item.title,
|
||||
thumbnail: item.thumbnail || '',
|
||||
uploaderName: item.uploaderName || 'Unknown',
|
||||
uploaderAvatar: '',
|
||||
views: 0,
|
||||
uploaded: Date.now(),
|
||||
uploadedDate: new Date().toISOString(),
|
||||
duration: 0,
|
||||
url: item.url || '',
|
||||
type: 'video',
|
||||
provider: providerId as 'youtube'|'dailymotion'|'twitch'|'rumble'|'odysee'|'peertube'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Apply tag filtering to unified results
|
||||
if (provider === 'twitch') return list; // Not used for Twitch (separate sections)
|
||||
if (tag === 'all') return list;
|
||||
|
||||
// For now, return all results since we don't have duration/uploaded info from unified search
|
||||
return list;
|
||||
}
|
||||
|
||||
// Fallback to legacy results
|
||||
const list = this.results();
|
||||
if (provider === 'twitch') return list; // Not used for Twitch (separate sections)
|
||||
if (tag === 'all') return list;
|
||||
|
||||
|
||||
const now = Date.now();
|
||||
const oneDay = 24 * 60 * 60 * 1000;
|
||||
const sevenDays = 7 * oneDay;
|
||||
const thirtyDays = 30 * oneDay;
|
||||
const oneYear = 365 * oneDay;
|
||||
|
||||
|
||||
// Duration filters
|
||||
if (tag === 'short') return list.filter(v => {
|
||||
const d = Number(v.duration || 0);
|
||||
@@ -86,7 +130,7 @@ export class SearchComponent {
|
||||
const d = Number(v.duration || 0);
|
||||
return d >= 20 * 60;
|
||||
});
|
||||
|
||||
|
||||
// Date filters
|
||||
if (tag === 'today') return list.filter(v => {
|
||||
const uploadTime = typeof v.uploaded === 'number' ? v.uploaded : 0;
|
||||
@@ -104,7 +148,7 @@ export class SearchComponent {
|
||||
const uploadTime = typeof v.uploaded === 'number' ? v.uploaded : 0;
|
||||
return uploadTime > 0 && (now - uploadTime) <= oneYear;
|
||||
});
|
||||
|
||||
|
||||
return list;
|
||||
});
|
||||
|
||||
@@ -113,8 +157,34 @@ export class SearchComponent {
|
||||
const provider = this.selectedProviderForView();
|
||||
const tags: { key: string; label: string; show: boolean }[] = [];
|
||||
const now = Date.now();
|
||||
const list = this.results();
|
||||
|
||||
|
||||
// Use unified results if available
|
||||
const list = this.showUnified() ?
|
||||
(() => {
|
||||
const groups = this.groups();
|
||||
const results: Video[] = [];
|
||||
Object.entries(groups).forEach(([_, items]) => {
|
||||
items.forEach((item: any) => {
|
||||
results.push({
|
||||
videoId: item.id,
|
||||
title: item.title,
|
||||
thumbnail: item.thumbnail || '',
|
||||
uploaderName: item.uploaderName || 'Unknown',
|
||||
uploaderAvatar: '',
|
||||
views: 0,
|
||||
uploaded: Date.now(),
|
||||
uploadedDate: new Date().toISOString(),
|
||||
duration: 0,
|
||||
url: item.url || '',
|
||||
type: 'video',
|
||||
provider: 'youtube' as any
|
||||
});
|
||||
});
|
||||
});
|
||||
return results;
|
||||
})() :
|
||||
this.results();
|
||||
|
||||
if (provider === 'twitch') {
|
||||
const hasLive = this.twitchChannels().length > 0;
|
||||
const hasVods = this.twitchVods().length > 0;
|
||||
@@ -132,20 +202,20 @@ export class SearchComponent {
|
||||
const sevenDays = 7 * oneDay;
|
||||
const thirtyDays = 30 * oneDay;
|
||||
const oneYear = 365 * oneDay;
|
||||
|
||||
let hasShort = false, hasMedium = false, hasLong = false,
|
||||
|
||||
let hasShort = false, hasMedium = false, hasLong = false,
|
||||
hasToday = false, hasThisWeek = false, hasThisMonth = false, hasThisYear = false;
|
||||
|
||||
|
||||
for (const v of list) {
|
||||
const d = Number(v.duration || 0);
|
||||
const uploadTime = typeof v.uploaded === 'number' ? v.uploaded : 0;
|
||||
const timeDiff = now - uploadTime;
|
||||
|
||||
|
||||
// Duration filters
|
||||
if (d > 0 && d < 4 * 60) hasShort = true;
|
||||
else if (d >= 4 * 60 && d < 20 * 60) hasMedium = true;
|
||||
else if (d >= 20 * 60) hasLong = true;
|
||||
|
||||
|
||||
// Date filters
|
||||
if (uploadTime > 0) {
|
||||
if (timeDiff <= oneDay) hasToday = true;
|
||||
@@ -160,13 +230,13 @@ export class SearchComponent {
|
||||
tags.push({ key: 'short', label: 'Moins de 4 min', show: hasShort });
|
||||
tags.push({ key: 'medium', label: 'De 4 à 20 min', show: hasMedium });
|
||||
tags.push({ key: 'long', label: '20 min et plus', show: hasLong });
|
||||
|
||||
|
||||
// Add date filters
|
||||
tags.push({ key: 'today', label: 'Aujourd\'hui', show: hasToday });
|
||||
tags.push({ key: 'this_week', label: 'Cette semaine', show: hasThisWeek });
|
||||
tags.push({ key: 'this_month', label: 'Ce mois-ci', show: hasThisMonth });
|
||||
tags.push({ key: 'this_year', label: 'Cette année', show: hasThisYear });
|
||||
|
||||
|
||||
return tags.filter(t => t.show);
|
||||
});
|
||||
|
||||
@@ -176,24 +246,54 @@ export class SearchComponent {
|
||||
this.title.setTitle(this.pageHeading());
|
||||
});
|
||||
|
||||
// Subscribe once to unified multi-provider results and reflect to UI
|
||||
this.unified.request$.subscribe({
|
||||
next: (resp: SearchResponse) => {
|
||||
this.groups.set(resp.groups as any);
|
||||
this.loading.set(false);
|
||||
},
|
||||
error: () => {
|
||||
this.groups.set({} as any);
|
||||
this.loading.set(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen to query param changes (so subsequent searches update)
|
||||
this.route.queryParamMap.subscribe((pm) => {
|
||||
const q = (pm.get('q') || '').trim();
|
||||
this.q.set(q);
|
||||
const prov = (pm.get('provider') as Provider) || null;
|
||||
const theme = pm.get('theme');
|
||||
const providersParam = (pm.get('providers') || '').trim();
|
||||
// Parse providers CSV if present
|
||||
let providersList: ProviderId[] | 'all' | null = null;
|
||||
if (providersParam) {
|
||||
const arr = providersParam.split(',').map(s => s.trim()).filter(Boolean) as ProviderId[];
|
||||
providersList = (arr.length > 0) ? arr : 'all';
|
||||
}
|
||||
this.providerParam.set(prov);
|
||||
this.themeParam.set(theme);
|
||||
this.providersListParam.set(providersList);
|
||||
|
||||
if (q) {
|
||||
this.notice.set(null);
|
||||
// Reset active tag on new query
|
||||
const provider = (prov || this.instances.selectedProvider());
|
||||
const provider = prov || this.instances.selectedProvider();
|
||||
this.filterTag.set(provider === 'twitch' ? 'twitch_all' : 'all');
|
||||
this.reloadSearch();
|
||||
|
||||
// Use unified search in all cases
|
||||
const providersToUse = providersList || [provider as ProviderId];
|
||||
this.unified.setQuery(q);
|
||||
this.unified.setProviders(providersToUse);
|
||||
this.showUnified.set(true);
|
||||
this.loading.set(true);
|
||||
this.groups.set({} as any); // Clear previous results
|
||||
} else {
|
||||
this.results.set([]);
|
||||
this.nextCursor.set(null);
|
||||
this.loading.set(false);
|
||||
this.groups.set({} as any);
|
||||
this.showUnified.set(false);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -206,8 +306,18 @@ export class SearchComponent {
|
||||
untracked(() => {
|
||||
// If provider is explicitly specified in query, do not override it with global changes
|
||||
if (this.providerParam()) return;
|
||||
if (this.q()) {
|
||||
this.notice.set(null);
|
||||
if (!this.q()) return;
|
||||
this.notice.set(null);
|
||||
// If unified mode is active, re-emit providers to trigger a refresh instead of legacy reload
|
||||
if (this.showUnified()) {
|
||||
const provParam = this.providersListParam();
|
||||
const provider = this.instances.selectedProvider();
|
||||
const providersToUse = (provParam && (provParam === 'all' || (Array.isArray(provParam) && provParam.length > 0)))
|
||||
? (provParam as any)
|
||||
: [provider as unknown as ProviderId];
|
||||
this.unified.setProviders(providersToUse);
|
||||
this.loading.set(true);
|
||||
} else {
|
||||
this.reloadSearch();
|
||||
}
|
||||
});
|
||||
@@ -354,11 +464,6 @@ export class SearchComponent {
|
||||
return qp;
|
||||
}
|
||||
|
||||
// Update active filter tag from the template
|
||||
setFilterTag(key: string) {
|
||||
this.filterTag.set(key);
|
||||
}
|
||||
|
||||
@ViewChild('searchInput') searchInput?: ElementRef<HTMLInputElement>;
|
||||
|
||||
onSearchEnter(event: Event) {
|
||||
@@ -366,8 +471,16 @@ export class SearchComponent {
|
||||
keyboardEvent.preventDefault();
|
||||
const query = this.q().trim();
|
||||
if (query) {
|
||||
// Utiliser reloadSearch() pour effectuer une nouvelle recherche
|
||||
this.reloadSearch();
|
||||
// Route unified search regardless of providers param
|
||||
const provParam = this.providersListParam();
|
||||
const provider = this.providerParam() || this.instances.selectedProvider();
|
||||
const providersToUse = (provParam && (provParam === 'all' || (Array.isArray(provParam) && provParam.length > 0)))
|
||||
? (provParam as any)
|
||||
: [provider as unknown as ProviderId];
|
||||
this.unified.setQuery(query);
|
||||
this.unified.setProviders(providersToUse);
|
||||
this.showUnified.set(true);
|
||||
this.loading.set(true);
|
||||
this.closeSearchResults();
|
||||
// Retirer le focus de l'input
|
||||
if (this.searchInput) {
|
||||
@@ -389,8 +502,8 @@ export class SearchComponent {
|
||||
// Vous pouvez ajouter ici la logique de recherche au fur et à mesure si nécessaire
|
||||
}
|
||||
|
||||
performSearch(query: string) {
|
||||
// Cette méthode sera appelée pour effectuer la recherche
|
||||
// Vous pouvez ajouter ici la logique de recherche
|
||||
// Update active filter tag from the template
|
||||
setFilterTag(key: string) {
|
||||
this.filterTag.set(key);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user