feat(1.0.13): тематики, стриминг поиска, авто-логотипы

- Тематики: 14 курированных категорий (Зомби, Космос, Вампиры,
  Постапокалипсис, Шпионы и т.д.) поверх жанров TMDB. discover-tmdb
  принимает themeKeywords (pipe-OR по with_keywords); для musical/
  superhero/martial добавлен extraGenre AND-связкой в movie-режиме.
- Стриминг поиска по сайтам: search-movies переведён с invoke→return
  на event-emit — карточки появляются по мере ответа каждого сайта,
  не ждут самого медленного. Спиннер виден до первого результата.
- Авто-детект логотипа сайта: поле "URL иконки" убрано из формы.
  Бэк IPC detect-logo пробует manifest.json → apple-touch-icon →
  link rel=icon ≥48px → JSON-LD → og:image → msapplication-TileImage
  → /favicon.ico (с проверкой content-type=image/*). Легаси-приложения
  без иконки догоняются тихо при открытии Settings.
This commit is contained in:
2026-05-17 11:15:42 +03:00
parent 1030622e19
commit 8684eb7b67
7 changed files with 338 additions and 60 deletions

View File

@@ -26,9 +26,34 @@ contextBridge.exposeInMainWorld('electron', {
checkUpdateNow: () => ipcRenderer.invoke('check-update-now'),
readConfig: () => ipcRenderer.invoke('read-config'),
writeConfig: (data) => ipcRenderer.send('write-config', data),
searchMovies: (query, sites) => ipcRenderer.invoke('search-movies', query, sites),
// Streaming: возвращает unsubscribe(). onResult({ source, results }) вызывается по мере прихода
// ответов от каждого сайта; onDone() — когда все сайты ответили.
// Слушатели снимаются автоматически после onDone (или вручную через возвращённую функцию).
searchMoviesStream: (query, sites, onResult, onDone) => {
const searchId = `s_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
let active = true;
const cleanup = () => {
if (!active) return;
active = false;
ipcRenderer.removeListener('search-movies-result', resultListener);
ipcRenderer.removeListener('search-movies-done', doneListener);
};
const resultListener = (_event, payload) => {
if (active && payload.searchId === searchId) onResult({ source: payload.source, results: payload.results });
};
const doneListener = (_event, payload) => {
if (active && payload.searchId === searchId) {
try { onDone(); } finally { cleanup(); }
}
};
ipcRenderer.on('search-movies-result', resultListener);
ipcRenderer.on('search-movies-done', doneListener);
ipcRenderer.send('search-movies-start', searchId, query, sites);
return cleanup;
},
searchTmdb: (query, apiKey) => ipcRenderer.invoke('search-tmdb', query, apiKey),
discoverTmdb: (params) => ipcRenderer.invoke('discover-tmdb', params),
detectLogo: (siteUrl) => ipcRenderer.invoke('detect-logo', siteUrl),
toggleKiosk: () => ipcRenderer.invoke('toggle-kiosk'),
isKiosk: () => ipcRenderer.invoke('is-kiosk'),