feat: trusted-domains OAuth popups, og:image bookmark posters, periodic updater

- main.js: trusted-domains list with default Google/Yandex/GitHub/etc.;
  cross-domain confirmation skipped for trusted; setWindowOpenHandler
  returns action:'allow' for trusted so OAuth popups work (postMessage
  back to opener, popup self-closes). Fixes YouTube/Google login reset.
- main.js: get-page-meta IPC extracts og:image / twitter:image / JSON-LD
  image from current view; HDRezka also tries .b-sidecover img for hi-res.
- Header: bookmark button pulls og:image as poster and the page's title;
  duplicate detection switched from hostname to full URL so multiple
  movies from same site can coexist.
- BookmarksBar: site icon rendered next to source domain when distinct
  from poster; img onerror falls back to placeholder.
- Settings: trusted domains chip list with add/remove/reset.
- Updater: proper semver compare (only show if latest > current),
  direct installer URL detection per platform, hourly re-check.

Bookmark schema gains optional siteIcon; existing bookmarks remain valid.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-16 18:56:12 +03:00
parent 6c314b614d
commit 2857a40d1e
8 changed files with 358 additions and 63 deletions

View File

@@ -7,7 +7,7 @@ interface HeaderProps {
setActiveApp: (name: string) => void
onAppsChange: (apps: AppEntry[]) => void
onMovieSearch: (query: string) => void
onBookmark: (title: string, url: string, poster: string, source: string) => void
onBookmark: (title: string, url: string, poster: string, source: string, siteIcon?: string) => void
onBookmarkRemove: (index: number) => void
bookmarks: import('./Settings').Bookmark[]
openedFromSearch?: boolean
@@ -108,11 +108,11 @@ const Header: React.FC<HeaderProps> = ({ activeApp, setActiveApp, onAppsChange,
const showSearchIcon = activeApp === 'home' || activeApp === 'movie-search'
const [isKiosk, setIsKiosk] = useState(true)
const [updateInfo, setUpdateInfo] = useState<{ version: string; url: string } | null>(null)
const [updateInfo, setUpdateInfo] = useState<{ version: string; currentVersion?: string; releaseUrl: string; installerUrl: string; installerName?: string } | null>(null)
useEffect(() => {
if (!window.electron) return
const off = window.electron.on('update-available', (info: { version: string; url: string }) => {
const off = window.electron.on('update-available', (info: { version: string; currentVersion?: string; releaseUrl: string; installerUrl: string; installerName?: string }) => {
setUpdateInfo(info)
})
return off
@@ -128,33 +128,31 @@ const Header: React.FC<HeaderProps> = ({ activeApp, setActiveApp, onAppsChange,
const [isBookmarked, setIsBookmarked] = useState(false)
const handleBookmark = () => {
window.electron?.getCurrentPage().then((page: any) => {
if (!page) return
let pageHost = ''
try { pageHost = new URL(page.url).hostname } catch (_) {}
const existingIdx = bookmarks.findIndex(b => {
try { return new URL(b.url).hostname === pageHost } catch (_) { return false }
})
if (existingIdx !== -1) {
onBookmarkRemove(existingIdx)
setIsBookmarked(false)
} else {
onBookmark(page.name, page.url, page.imageUrl || '', '')
setIsBookmarked(true)
}
})
const handleBookmark = async () => {
const page = await window.electron?.getCurrentPage()
if (!page) return
let pageHost = ''
try { pageHost = new URL(page.url).hostname } catch (_) {}
// Match by full URL — different movies on same site must not collide.
const existingIdx = bookmarks.findIndex(b => b.url === page.url)
if (existingIdx !== -1) {
onBookmarkRemove(existingIdx)
setIsBookmarked(false)
return
}
// Pull og:image / JSON-LD poster from the live page (specific to this movie).
const meta = await window.electron?.getPageMeta?.().catch(() => null)
const poster = meta?.poster || ''
const title = (meta?.title || page.name || '').trim() || page.name
onBookmark(title, page.url, poster, pageHost, page.imageUrl || '')
setIsBookmarked(true)
}
useEffect(() => {
if (!appOpen) { setIsBookmarked(false); return }
window.electron?.getCurrentPage().then((page: any) => {
if (!page) return
let pageHost = ''
try { pageHost = new URL(page.url).hostname } catch (_) {}
setIsBookmarked(bookmarks.some(b => {
try { return new URL(b.url).hostname === pageHost } catch (_) { return false }
}))
setIsBookmarked(bookmarks.some(b => b.url === page.url))
})
}, [activeApp, bookmarks, appOpen])
@@ -283,10 +281,10 @@ const Header: React.FC<HeaderProps> = ({ activeApp, setActiveApp, onAppsChange,
{updateInfo && (
<div className="update-banner">
<span>Доступна версия {updateInfo.version}</span>
<a href={updateInfo.url} target="_blank" rel="noreferrer" className="update-banner-btn" onClick={() => window.electron?.createView('Обновление', updateInfo.url, '', 1.0, false)}>
Скачать
</a>
<span>Доступна версия {updateInfo.version}{updateInfo.currentVersion ? ` (текущая ${updateInfo.currentVersion})` : ''}</span>
<button className="update-banner-btn" onClick={() => window.electron?.createView('Обновление', updateInfo.installerUrl, '', 1.0, false)}>
{updateInfo.installerName ? 'Скачать установщик' : 'Открыть релиз'}
</button>
<button className="update-banner-close" onClick={() => setUpdateInfo(null)}></button>
</div>
)}