feat: back-to-search button, retry site search, update checker, nsis installer

This commit is contained in:
2026-03-14 14:25:52 +03:00
parent 14da54f204
commit 6c314b614d
6 changed files with 150 additions and 11 deletions

25
main.js
View File

@@ -157,6 +157,27 @@ async function loadExtensions() {
}
}
// --- Updates ---
async function checkForUpdates() {
try {
const res = await getDirectSession().fetch(
'https://gitea.esh-service.ru/api/v1/repos/public/ESH-Media/releases/latest'
);
if (!res.ok) return;
const data = await res.json();
const latest = (data.tag_name || '').replace(/^v/, '');
const current = app.getVersion();
if (latest && latest !== current) {
mainWindow.webContents.send('update-available', {
version: latest,
url: data.html_url,
assets: (data.assets || []).map(a => ({ name: a.name, url: a.browser_download_url })),
});
}
} catch (_) {}
}
// --- Window ---
async function createWindow() {
@@ -862,6 +883,10 @@ app.whenReady().then(async () => {
getDirectSession();
await loadExtensions();
await createWindow();
mainWindow.webContents.once('did-finish-load', () => {
setTimeout(checkForUpdates, 4000);
});
});
app.on('window-all-closed', () => {

View File

@@ -46,14 +46,12 @@
"extensions/**/*"
],
"win": {
"target": ["zip"],
"target": ["nsis", "zip"],
"icon": "public/favicon.ico"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"installerLanguages": ["Russian", "English"],
"language": "1049"
"allowToChangeInstallationDirectory": true
},
"linux": {
"target": ["AppImage", "deb"],

View File

@@ -10,9 +10,11 @@ interface HeaderProps {
onBookmark: (title: string, url: string, poster: string, source: string) => void
onBookmarkRemove: (index: number) => void
bookmarks: import('./Settings').Bookmark[]
openedFromSearch?: boolean
onBackToSearch?: () => void
}
const Header: React.FC<HeaderProps> = ({ activeApp, setActiveApp, onAppsChange, onMovieSearch, onBookmark, onBookmarkRemove, bookmarks }) => {
const Header: React.FC<HeaderProps> = ({ activeApp, setActiveApp, onAppsChange, onMovieSearch, onBookmark, onBookmarkRemove, bookmarks, openedFromSearch, onBackToSearch }) => {
const [isCollapsed, setIsCollapsed] = useState(false)
const [isHovered, setIsHovered] = useState(false)
const [leftDisabled, setLeftDisabled] = useState(true)
@@ -106,6 +108,15 @@ 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)
useEffect(() => {
if (!window.electron) return
const off = window.electron.on('update-available', (info: { version: string; url: string }) => {
setUpdateInfo(info)
})
return off
}, [])
useEffect(() => {
window.electron?.isKiosk().then(k => setIsKiosk(k))
@@ -185,6 +196,14 @@ const Header: React.FC<HeaderProps> = ({ activeApp, setActiveApp, onAppsChange,
</div>
<div className="header-center">
{appOpen && openedFromSearch && onBackToSearch && (
<button className="header-btn nav-btn" onClick={onBackToSearch} title="Вернуться к результатам поиска">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#e53935" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<polyline points="15 18 9 12 15 6" />
</svg>
<span style={{ fontSize: '12px', color: '#e53935', marginLeft: 2 }}>Поиск</span>
</button>
)}
{appOpen && (
<>
<button
@@ -262,6 +281,16 @@ const Header: React.FC<HeaderProps> = ({ activeApp, setActiveApp, onAppsChange,
)}
</div>
{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>
<button className="update-banner-close" onClick={() => setUpdateInfo(null)}></button>
</div>
)}
{showSettings && (
<Settings onClose={closeSettings} onAppsChange={onAppsChange} />
)}

View File

@@ -468,7 +468,21 @@ const MovieSearch: React.FC<MovieSearchProps> = ({ onOpenUrl, onBookmark, initia
<div className="ms-site-results">
{sitesLoading && <p className="movie-search-message">Ищем на {sites.length} сайтах...</p>}
{!sitesLoading && !siteResults.length && !message && <p className="movie-search-message">Поиск...</p>}
{siteResults.length > 0 && <div className="ms-sites-label">Найдено на сайтах</div>}
{!sitesLoading && siteResults.length > 0 && (
<div className="ms-sites-label">
Найдено на сайтах
<button className="ms-retry-btn" onClick={() => selected && doSiteSearch(selected.title || selected.originalTitle, sites, selected.year, selected.mediaType)} title="Повторить поиск">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<polyline points="23 4 23 10 17 10" /><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
</svg>
</button>
</div>
)}
{!sitesLoading && !siteResults.length && (
<button className="ms-retry-btn ms-retry-standalone" onClick={() => selected && doSiteSearch(selected.title || selected.originalTitle, sites, selected.year, selected.mediaType)}>
Повторить поиск
</button>
)}
{siteResults.map((r, i) => (
<div key={i} className="ms-site-row" onClick={() => onOpenUrl(r.title, r.url)}>
<span className="ms-site-source">{r.source}</span>

View File

@@ -17,6 +17,8 @@ const HomePage: React.FC = () => {
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>({})
@@ -46,14 +48,22 @@ const HomePage: React.FC = () => {
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) => {
const updated = [...bookmarks, { title, url, poster, source }]
setBookmarks(updated)
@@ -89,12 +99,14 @@ const HomePage: React.FC = () => {
return (
<>
<Header activeApp={activeApp} setActiveApp={setActiveApp} onAppsChange={setAppCardList} onMovieSearch={handleMovieSearch} onBookmark={handleBookmarkAdd} onBookmarkRemove={handleBookmarkRemove} bookmarks={bookmarks} />
<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} />
{activeApp === 'movie-search'
? <MovieSearch initialQuery={movieQuery ?? ''} onOpenUrl={handleMovieSearchOpen} onBookmark={handleBookmarkAdd} />
: <AppList apps={appCardList} bookmarks={bookmarks} onBookmarkOpen={handleBookmarkOpen} onBookmarkRemove={handleBookmarkRemove} />
}
<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} />
)}
</>
)
}

View File

@@ -1323,3 +1323,64 @@ body {
.modal-btn-yes { background: #E50914; color: #fff; }
.modal-btn-no { background: rgba(255,255,255,0.1); color: #ccc; }
.modal-btn-ok { background: rgba(255,255,255,0.1); color: #ccc; }
/* ---- Update banner ---- */
.update-banner {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 9999;
background: #1a1a1a;
border: 1px solid rgba(229,9,20,0.4);
border-radius: 8px;
padding: 10px 14px;
display: flex;
align-items: center;
gap: 10px;
font-size: 13px;
color: #fff;
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
}
.update-banner-btn {
background: #E50914;
color: #fff;
border: none;
border-radius: 5px;
padding: 4px 12px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
text-decoration: none;
}
.update-banner-close {
background: none;
border: none;
color: #777;
cursor: pointer;
font-size: 13px;
padding: 0;
line-height: 1;
}
.update-banner-close:hover { color: #fff; }
/* ---- Retry btn ---- */
.ms-retry-btn {
background: none;
border: none;
color: #aaa;
cursor: pointer;
padding: 2px 6px;
display: inline-flex;
align-items: center;
gap: 4px;
border-radius: 4px;
transition: color 0.15s;
}
.ms-retry-btn:hover { color: #fff; }
.ms-retry-standalone {
margin-top: 12px;
font-size: 13px;
color: #E50914;
display: block;
}
.ms-retry-standalone:hover { color: #ff4444; }