- 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>
125 lines
4.8 KiB
TypeScript
125 lines
4.8 KiB
TypeScript
import React, { useState, useEffect, useRef } from 'react'
|
|
import Header from '../components/Header'
|
|
import Sidebar from '../components/Sidebar'
|
|
import AppList from '../components/AppList'
|
|
import MovieSearch from '../components/MovieSearch'
|
|
import '../styles/main.css'
|
|
import { AppEntry, Bookmark } from '../components/Settings'
|
|
|
|
interface OpenedApp {
|
|
name: string
|
|
imageUrl: string
|
|
url: string
|
|
}
|
|
|
|
const HomePage: React.FC = () => {
|
|
const [openedApps, setOpenedApps] = useState<OpenedApp[]>([])
|
|
const [activeApp, setActiveApp] = useState<string>('home')
|
|
const [appCardList, setAppCardList] = useState<AppEntry[]>([])
|
|
const [movieQuery, setMovieQuery] = useState<string | null>(null)
|
|
const [movieSearchKey, setMovieSearchKey] = useState(0)
|
|
const [openedFromSearch, setOpenedFromSearch] = useState(false)
|
|
const [bookmarks, setBookmarks] = useState<Bookmark[]>([])
|
|
const configRef = useRef<any>({})
|
|
|
|
useEffect(() => {
|
|
if (!window.electron) return
|
|
window.electron.readConfig().then((cfg: any) => {
|
|
configRef.current = cfg ?? {}
|
|
if (cfg?.apps) setAppCardList(cfg.apps)
|
|
if (cfg?.bookmarks) setBookmarks(cfg.bookmarks)
|
|
if (cfg?.proxy?.host && cfg?.proxy?.port) {
|
|
window.electron!.setProxy(cfg.proxy.host, cfg.proxy.port)
|
|
}
|
|
})
|
|
const offOpenedApps = window.electron.on('update-opened-apps', (apps: OpenedApp[], activeName: string) => {
|
|
setOpenedApps(apps)
|
|
setActiveApp(activeName ?? 'home')
|
|
})
|
|
const offAlert = window.electron.on('alert', (text: string) => alert(text))
|
|
return () => { offOpenedApps(); offAlert() }
|
|
}, [])
|
|
|
|
const handleSidebarAppClick = (name: string) => {
|
|
setActiveApp(name)
|
|
window.electron?.showView(name)
|
|
}
|
|
|
|
const handleMovieSearch = (query: string) => {
|
|
window.electron?.hideView()
|
|
setMovieQuery(query)
|
|
setMovieSearchKey(k => k + 1)
|
|
setOpenedFromSearch(false)
|
|
setActiveApp('movie-search')
|
|
}
|
|
|
|
const handleMovieSearchOpen = (name: string, url: string) => {
|
|
window.electron?.createView(name, url, '', 1.0, resolveUseProxy(url))
|
|
setOpenedFromSearch(true)
|
|
setActiveApp(name)
|
|
}
|
|
|
|
const handleBackToSearch = () => {
|
|
window.electron?.hideView()
|
|
setActiveApp('movie-search')
|
|
}
|
|
|
|
const handleBookmarkAdd = (title: string, url: string, poster: string, source: string, siteIcon?: string) => {
|
|
// If caller didn't pass a site icon (e.g. movie search), look it up from the apps config by host.
|
|
let icon = siteIcon || ''
|
|
if (!icon) {
|
|
try {
|
|
const host = new URL(url).hostname
|
|
const match = appCardList.find(a => { try { return new URL(a.url).hostname === host } catch { return false } })
|
|
if (match?.imageUrl) icon = match.imageUrl
|
|
} catch {}
|
|
}
|
|
const sourceStr = source || (() => { try { return new URL(url).hostname.replace(/^www\./, '') } catch { return '' } })()
|
|
const updated = [...bookmarks, { title, url, poster, source: sourceStr, siteIcon: icon }]
|
|
setBookmarks(updated)
|
|
configRef.current = { ...configRef.current, bookmarks: updated }
|
|
window.electron?.writeConfig(configRef.current)
|
|
}
|
|
|
|
const handleBookmarkRemove = (index: number) => {
|
|
const updated = bookmarks.filter((_, i) => i !== index)
|
|
setBookmarks(updated)
|
|
configRef.current = { ...configRef.current, bookmarks: updated }
|
|
window.electron?.writeConfig(configRef.current)
|
|
}
|
|
|
|
const resolveUseProxy = (url: string) => {
|
|
try {
|
|
const host = new URL(url).hostname
|
|
const match = appCardList.find(a => { try { return new URL(a.url).hostname === host } catch { return false } })
|
|
return match ? match.useProxy : true
|
|
} catch { return true }
|
|
}
|
|
|
|
const handleBookmarkOpen = (b: Bookmark) => {
|
|
window.electron?.createView(b.title, b.url, b.poster || '', 1.0, resolveUseProxy(b.url))
|
|
setActiveApp(b.title)
|
|
}
|
|
|
|
const sidebarApps = openedApps.map(app => ({
|
|
...app,
|
|
isActive: activeApp === app.name,
|
|
onClick: () => handleSidebarAppClick(app.name),
|
|
}))
|
|
|
|
return (
|
|
<>
|
|
<Header activeApp={activeApp} setActiveApp={setActiveApp} onAppsChange={setAppCardList} onMovieSearch={handleMovieSearch} onBookmark={handleBookmarkAdd} onBookmarkRemove={handleBookmarkRemove} bookmarks={bookmarks} openedFromSearch={openedFromSearch} onBackToSearch={handleBackToSearch} />
|
|
<Sidebar openedApps={sidebarApps} activeApp={activeApp} setActiveApp={setActiveApp} />
|
|
<div style={{ display: activeApp === 'movie-search' ? undefined : 'none' }}>
|
|
<MovieSearch key={movieSearchKey} initialQuery={movieQuery ?? ''} onOpenUrl={handleMovieSearchOpen} onBookmark={handleBookmarkAdd} />
|
|
</div>
|
|
{activeApp !== 'movie-search' && (
|
|
<AppList apps={appCardList} bookmarks={bookmarks} onBookmarkOpen={handleBookmarkOpen} onBookmarkRemove={handleBookmarkRemove} />
|
|
)}
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default HomePage
|