Files
ESH-Media/src/components/Settings.tsx
eshmeshek 2857a40d1e 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>
2026-05-16 18:56:12 +03:00

355 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react'
export interface AppEntry {
name: string
imageUrl: string
url: string
useProxy: boolean
}
export interface Bookmark {
title: string
url: string
poster?: string // movie poster (og:image, or fallback site icon)
source?: string // domain shown under title
siteIcon?: string // small site icon shown alongside source
}
export interface MovieSite {
domain: string
type: 'dle' | 'hdrezka' | 'filmix'
enabled: boolean
}
interface ProxyConfig {
host: string
port: string
}
interface SettingsData {
apps: AppEntry[]
proxy: ProxyConfig
movieSites: MovieSite[]
tmdbApiKey: string
bookmarks: Bookmark[]
trustedDomains?: string[]
}
interface SettingsProps {
onClose: () => void
onAppsChange: (apps: AppEntry[]) => void
}
const DEFAULT_TRUSTED_DOMAINS = [
'google.com', 'accounts.google.com', 'googleapis.com', 'googleusercontent.com',
'gstatic.com', 'youtube.com', 'ytimg.com', 'googlevideo.com',
'yandex.ru', 'yandex.com', 'passport.yandex.ru', 'passport.yandex.com', 'yastatic.net',
'github.com', 'github.io', 'githubassets.com', 'githubusercontent.com',
'vk.com', 'vk.ru', 'vkuser.net', 'mail.ru', 'my.mail.ru',
'live.com', 'microsoft.com', 'microsoftonline.com', 'office.com',
'apple.com', 'icloud.com', 'facebook.com', 'fb.com',
]
const DEFAULT_SETTINGS: SettingsData = { apps: [], proxy: { host: '127.0.0.1', port: '7890' }, movieSites: [], tmdbApiKey: '', bookmarks: [], trustedDomains: DEFAULT_TRUSTED_DOMAINS }
function guessMovieSiteType(domain: string): MovieSite['type'] {
if (/rezka/.test(domain)) return 'hdrezka'
if (/filmix/.test(domain)) return 'filmix'
return 'dle'
}
function saveSettings(data: SettingsData) {
window.electron?.writeConfig(data)
}
const Settings: React.FC<SettingsProps> = ({ onClose, onAppsChange }) => {
const [settings, setSettings] = useState<SettingsData>(DEFAULT_SETTINGS)
const [newApp, setNewApp] = useState<AppEntry>({ name: '', imageUrl: '', url: '', useProxy: false })
useEffect(() => {
window.electron?.readConfig().then((cfg: SettingsData | null) => {
if (cfg?.apps) setSettings(cfg)
})
}, [])
const updateProxy = (field: keyof ProxyConfig, value: string) => {
const updated = { ...settings, proxy: { ...settings.proxy, [field]: value } }
setSettings(updated)
saveSettings(updated)
window.electron?.setProxy(updated.proxy.host, updated.proxy.port)
}
const toggleAppProxy = (index: number) => {
const apps = settings.apps.map((app, i) =>
i === index ? { ...app, useProxy: !app.useProxy } : app
)
const updated = { ...settings, apps }
setSettings(updated)
saveSettings(updated)
onAppsChange(apps)
}
const addApp = () => {
if (!newApp.name || !newApp.url) return
const apps = [...settings.apps, newApp]
const updated = { ...settings, apps }
setSettings(updated)
saveSettings(updated)
onAppsChange(apps)
setNewApp({ name: '', imageUrl: '', url: '', useProxy: false })
}
const removeApp = (index: number) => {
const apps = settings.apps.filter((_, i) => i !== index)
const updated = { ...settings, apps }
setSettings(updated)
saveSettings(updated)
onAppsChange(apps)
}
const [newSite, setNewSite] = useState<MovieSite>({ domain: '', type: 'dle', enabled: true })
const [newTrusted, setNewTrusted] = useState('')
const trustedDomains = settings.trustedDomains ?? DEFAULT_TRUSTED_DOMAINS
const addTrustedDomain = () => {
const d = newTrusted.trim().toLowerCase().replace(/^https?:\/\//, '').replace(/\/.*$/, '').replace(/^\./, '')
if (!d || trustedDomains.includes(d)) { setNewTrusted(''); return }
const updated = { ...settings, trustedDomains: [...trustedDomains, d] }
setSettings(updated); saveSettings(updated); setNewTrusted('')
}
const removeTrustedDomain = (index: number) => {
const updated = { ...settings, trustedDomains: trustedDomains.filter((_, i) => i !== index) }
setSettings(updated); saveSettings(updated)
}
const resetTrustedDomains = () => {
const updated = { ...settings, trustedDomains: DEFAULT_TRUSTED_DOMAINS }
setSettings(updated); saveSettings(updated)
}
const addMovieSite = () => {
if (!newSite.domain.trim()) return
let domain = newSite.domain.trim()
if (domain.startsWith('https://')) domain = domain.slice(8)
else if (domain.startsWith('http://')) domain = domain.slice(7)
if (domain.endsWith('/')) domain = domain.slice(0, -1)
const movieSites = [...(settings.movieSites ?? []), { ...newSite, domain }]
const updated = { ...settings, movieSites }
setSettings(updated)
saveSettings(updated)
setNewSite({ domain: '', type: 'dle', enabled: true })
}
const removeMovieSite = (index: number) => {
const movieSites = (settings.movieSites ?? []).filter((_, i) => i !== index)
const updated = { ...settings, movieSites }
setSettings(updated)
saveSettings(updated)
}
const toggleMovieSite = (index: number) => {
const movieSites = (settings.movieSites ?? []).map((s, i) =>
i === index ? { ...s, enabled: !s.enabled } : s
)
const updated = { ...settings, movieSites }
setSettings(updated)
saveSettings(updated)
}
return (
<div className="settings-overlay" onClick={onClose}>
<div className="settings-panel" onClick={e => e.stopPropagation()}>
<div className="settings-header">
<h2>Настройки</h2>
<button className="settings-close" onClick={onClose}></button>
</div>
<div className="settings-section">
<h3>Прокси</h3>
<div className="proxy-row">
<input
className="settings-input"
placeholder="Хост"
value={settings.proxy.host}
onChange={e => updateProxy('host', e.target.value)}
/>
<span className="proxy-colon">:</span>
<input
className="settings-input proxy-port"
placeholder="Порт"
value={settings.proxy.port}
onChange={e => updateProxy('port', e.target.value)}
/>
</div>
<div className="proxy-info">
<code>http_proxy=http://{settings.proxy.host}:{settings.proxy.port}</code>
<code>https_proxy=http://{settings.proxy.host}:{settings.proxy.port}</code>
<code>socks5://{settings.proxy.host}:{settings.proxy.port}</code>
</div>
<p className="proxy-hint">
Прокси применяется к каждому сайту индивидуально переключатель рядом с каждым приложением.
</p>
</div>
<div className="settings-section">
<div className="settings-section-head-row">
<h3>Доверенные домены</h3>
<button className="settings-reset-btn" onClick={resetTrustedDomains} title="Сбросить к стандартному списку">Сбросить</button>
</div>
<p className="proxy-hint">
Переходы и popup'ы на эти домены открываются без подтверждения — нужно для входа через Google, Яндекс, GitHub и т.п.
Совпадение по суффиксу: <code>google.com</code> разрешит и <code>accounts.google.com</code>, и <code>www.google.com</code>.
</p>
<div className="trusted-domains-list">
{trustedDomains.map((d, i) => (
<span key={i} className="trusted-domain-chip">
{d}
<button className="trusted-domain-remove" onClick={() => removeTrustedDomain(i)} title="Удалить">✕</button>
</span>
))}
{trustedDomains.length === 0 && <p className="settings-empty">Список пуст.</p>}
</div>
<div className="trusted-domain-add">
<input
className="settings-input"
placeholder="example.com"
value={newTrusted}
onChange={e => setNewTrusted(e.target.value)}
onKeyDown={e => e.key === 'Enter' && addTrustedDomain()}
/>
<button className="settings-add-btn" onClick={addTrustedDomain}>Добавить</button>
</div>
</div>
<div className="settings-section">
<h3>Приложения</h3>
<div className="settings-apps-list">
{settings.apps.map((app, i) => (
<div key={i} className="settings-app-row">
<div className="settings-app-info">
{app.imageUrl && (
<img src={app.imageUrl} alt={app.name} className="settings-app-icon" />
)}
<div className="settings-app-text">
<span className="settings-app-name">{app.name}</span>
<span className="settings-app-url">{app.url}</span>
</div>
</div>
<div className="settings-app-actions">
<label className="proxy-switch-label">
<span>Прокси</span>
<div
className={`proxy-switch ${app.useProxy ? 'on' : 'off'}`}
onClick={() => toggleAppProxy(i)}
/>
</label>
<button className="settings-remove-btn" onClick={() => removeApp(i)}>✕</button>
</div>
</div>
))}
{settings.apps.length === 0 && (
<p className="settings-empty">Нет приложений. Добавьте ниже.</p>
)}
</div>
<div className="add-app-form">
<h4>Добавить приложение</h4>
<input
className="settings-input"
placeholder="Название"
value={newApp.name}
onChange={e => setNewApp({ ...newApp, name: e.target.value })}
/>
<input
className="settings-input"
placeholder="URL сайта"
value={newApp.url}
onChange={e => setNewApp({ ...newApp, url: e.target.value })}
/>
<input
className="settings-input"
placeholder="URL иконки (необязательно)"
value={newApp.imageUrl}
onChange={e => setNewApp({ ...newApp, imageUrl: e.target.value })}
/>
<label className="proxy-switch-label add-proxy-label">
<span>Использовать прокси</span>
<div
className={`proxy-switch ${newApp.useProxy ? 'on' : 'off'}`}
onClick={() => setNewApp({ ...newApp, useProxy: !newApp.useProxy })}
/>
</label>
<button className="settings-add-btn" onClick={addApp}>Добавить</button>
</div>
</div>
<div className="settings-section">
<h3>Поиск фильмов</h3>
<h4>TMDB API ключ</h4>
<input
className="settings-input"
placeholder="Получить бесплатно на themoviedb.org"
value={settings.tmdbApiKey ?? ''}
onChange={e => {
const updated = { ...settings, tmdbApiKey: e.target.value }
setSettings(updated)
saveSettings(updated)
}}
/>
<p className="proxy-hint">Нужен для постеров и метаданных. Без ключа поиск работает напрямую по сайтам.</p>
<h4>Сайты</h4>
<p className="proxy-hint">Тип определяется автоматически. Поддерживаются: kinogo, lordfilm, gidonline, hdrezka, filmix и их зеркала. Домен без https://</p>
<div className="settings-apps-list">
{(settings.movieSites ?? []).map((site, i) => (
<div key={i} className="settings-app-row">
<div className="settings-app-info">
<div className="settings-app-text">
<span className="settings-app-name">{site.domain}</span>
<span className="settings-app-url">{site.type}</span>
</div>
</div>
<div className="settings-app-actions">
<label className="proxy-switch-label">
<span>Вкл</span>
<div className={`proxy-switch ${site.enabled ? 'on' : 'off'}`} onClick={() => toggleMovieSite(i)} />
</label>
<button className="settings-remove-btn" onClick={() => removeMovieSite(i)}>✕</button>
</div>
</div>
))}
{!(settings.movieSites ?? []).length && (
<p className="settings-empty">Нет сайтов. Добавьте ниже.</p>
)}
</div>
<div className="add-app-form">
<h4>Добавить сайт</h4>
<input
className="settings-input"
placeholder="Домен (например: kinogo.cc)"
value={newSite.domain}
onChange={e => {
const domain = e.target.value
setNewSite({ ...newSite, domain, type: guessMovieSiteType(domain) })
}}
/>
<select
className="settings-select"
value={newSite.type}
onChange={e => setNewSite({ ...newSite, type: e.target.value as MovieSite['type'] })}
>
<option value="dle">DLE (kinogo, lordfilm и зеркала)</option>
<option value="hdrezka">HDRezka</option>
<option value="filmix">Filmix</option>
</select>
<button className="settings-add-btn" onClick={addMovieSite}>Добавить</button>
</div>
</div>
</div>
</div>
)
}
export default Settings