feat: seamless auto-update via electron-updater, multi-select filters, session restore

- electron-updater wired with Gitea API discovery: setFeedURL dynamically
  per release (Gitea 1.24.7 lacks /releases/latest/download/ shortcut).
  Differential download via .blockmap saves ~70 MB per patch. Renderer
  banner shows states: available → downloading X% → ready. User clicks
  "Установить и перезапустить" → quitAndInstall replaces files + relaunches.
- Multi-select filters per user spec: genres AND (TMDB with_genres comma-
  joined), countries OR (pipe-joined into with_origin_country /
  with_original_language), years OR (fan-out one request per year, merge
  by id since TMDB has no discrete-year OR). Rating stays single threshold.
- Session persistence: openedSession {tabs, activeName} saved to config
  on tab create/show/hide/remove/in-app navigation, plus before-quit.
  Restored after did-finish-load via ipcMain.emit('create-view',...) per
  tab. Survives auto-update relaunch — bring user back to the same page.
- electron-builder publish config (generic provider) so latest.yml is
  generated during build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-16 20:42:25 +03:00
parent 10361cf3c0
commit a171f62629
8 changed files with 565 additions and 93 deletions

View File

@@ -110,12 +110,24 @@ 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; currentVersion?: string; releaseUrl: string; installerUrl: string; installerName?: string } | null>(null)
type UpdateStatus =
| { state: 'available'; version: string; currentVersion?: string }
| { state: 'downloading'; percent: number; bytesPerSecond?: number; transferred?: number; total?: number; version?: string; currentVersion?: string }
| { state: 'ready'; version: string; currentVersion?: string }
| { state: 'manual'; version: string; currentVersion?: string; installerUrl: string; installerName?: string }
| { state: 'error'; message: string }
const [updateStatus, setUpdateStatus] = useState<UpdateStatus | null>(null)
useEffect(() => {
if (!window.electron) return
const off = window.electron.on('update-available', (info: { version: string; currentVersion?: string; releaseUrl: string; installerUrl: string; installerName?: string }) => {
setUpdateInfo(info)
const off = window.electron.on('update-status', (info: UpdateStatus) => {
setUpdateStatus(prev => {
// Preserve version across state transitions when payload omits it (download-progress)
if (info.state === 'downloading' && prev && 'version' in prev && prev.version) {
return { ...info, version: info.version || prev.version, currentVersion: info.currentVersion || ('currentVersion' in prev ? prev.currentVersion : undefined) }
}
return info
})
})
return off
}, [])
@@ -278,13 +290,39 @@ const Header: React.FC<HeaderProps> = ({ activeApp, setActiveApp, onAppsChange,
)}
</div>
{updateInfo && (
{updateStatus && updateStatus.state !== 'error' && (
<div className="update-banner">
<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>
{updateStatus.state === 'available' && (
<>
<span className="update-banner-spinner" />
<span>Загружается обновление {updateStatus.version}{updateStatus.currentVersion ? ` (текущая ${updateStatus.currentVersion})` : ''}</span>
</>
)}
{updateStatus.state === 'downloading' && (
<>
<span>Скачивается {updateStatus.version || 'обновление'}: {updateStatus.percent}%</span>
<div className="update-banner-progress">
<div className="update-banner-progress-bar" style={{ width: `${updateStatus.percent}%` }} />
</div>
</>
)}
{updateStatus.state === 'ready' && (
<>
<span>Версия {updateStatus.version} готова к установке</span>
<button className="update-banner-btn" onClick={() => window.electron?.installUpdate?.()}>
Установить и перезапустить
</button>
</>
)}
{updateStatus.state === 'manual' && (
<>
<span>Доступна {updateStatus.version}{updateStatus.currentVersion ? ` (текущая ${updateStatus.currentVersion})` : ''}</span>
<button className="update-banner-btn" onClick={() => window.electron?.createView('Обновление', updateStatus.installerUrl, '', 1.0, false)}>
Скачать установщик
</button>
</>
)}
<button className="update-banner-close" onClick={() => setUpdateStatus(null)}></button>
</div>
)}