feat: back-to-search button, retry site search, update checker, nsis installer
This commit is contained in:
25
main.js
25
main.js
@@ -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 ---
|
// --- Window ---
|
||||||
|
|
||||||
async function createWindow() {
|
async function createWindow() {
|
||||||
@@ -862,6 +883,10 @@ app.whenReady().then(async () => {
|
|||||||
getDirectSession();
|
getDirectSession();
|
||||||
await loadExtensions();
|
await loadExtensions();
|
||||||
await createWindow();
|
await createWindow();
|
||||||
|
|
||||||
|
mainWindow.webContents.once('did-finish-load', () => {
|
||||||
|
setTimeout(checkForUpdates, 4000);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
app.on('window-all-closed', () => {
|
||||||
|
|||||||
@@ -46,14 +46,12 @@
|
|||||||
"extensions/**/*"
|
"extensions/**/*"
|
||||||
],
|
],
|
||||||
"win": {
|
"win": {
|
||||||
"target": ["zip"],
|
"target": ["nsis", "zip"],
|
||||||
"icon": "public/favicon.ico"
|
"icon": "public/favicon.ico"
|
||||||
},
|
},
|
||||||
"nsis": {
|
"nsis": {
|
||||||
"oneClick": false,
|
"oneClick": false,
|
||||||
"allowToChangeInstallationDirectory": true,
|
"allowToChangeInstallationDirectory": true
|
||||||
"installerLanguages": ["Russian", "English"],
|
|
||||||
"language": "1049"
|
|
||||||
},
|
},
|
||||||
"linux": {
|
"linux": {
|
||||||
"target": ["AppImage", "deb"],
|
"target": ["AppImage", "deb"],
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ interface HeaderProps {
|
|||||||
onBookmark: (title: string, url: string, poster: string, source: string) => void
|
onBookmark: (title: string, url: string, poster: string, source: string) => void
|
||||||
onBookmarkRemove: (index: number) => void
|
onBookmarkRemove: (index: number) => void
|
||||||
bookmarks: import('./Settings').Bookmark[]
|
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 [isCollapsed, setIsCollapsed] = useState(false)
|
||||||
const [isHovered, setIsHovered] = useState(false)
|
const [isHovered, setIsHovered] = useState(false)
|
||||||
const [leftDisabled, setLeftDisabled] = useState(true)
|
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 showSearchIcon = activeApp === 'home' || activeApp === 'movie-search'
|
||||||
|
|
||||||
const [isKiosk, setIsKiosk] = useState(true)
|
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(() => {
|
useEffect(() => {
|
||||||
window.electron?.isKiosk().then(k => setIsKiosk(k))
|
window.electron?.isKiosk().then(k => setIsKiosk(k))
|
||||||
@@ -185,6 +196,14 @@ const Header: React.FC<HeaderProps> = ({ activeApp, setActiveApp, onAppsChange,
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="header-center">
|
<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 && (
|
{appOpen && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
@@ -262,6 +281,16 @@ const Header: React.FC<HeaderProps> = ({ activeApp, setActiveApp, onAppsChange,
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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 && (
|
{showSettings && (
|
||||||
<Settings onClose={closeSettings} onAppsChange={onAppsChange} />
|
<Settings onClose={closeSettings} onAppsChange={onAppsChange} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -468,7 +468,21 @@ const MovieSearch: React.FC<MovieSearchProps> = ({ onOpenUrl, onBookmark, initia
|
|||||||
<div className="ms-site-results">
|
<div className="ms-site-results">
|
||||||
{sitesLoading && <p className="movie-search-message">Ищем на {sites.length} сайтах...</p>}
|
{sitesLoading && <p className="movie-search-message">Ищем на {sites.length} сайтах...</p>}
|
||||||
{!sitesLoading && !siteResults.length && !message && <p className="movie-search-message">Поиск...</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) => (
|
{siteResults.map((r, i) => (
|
||||||
<div key={i} className="ms-site-row" onClick={() => onOpenUrl(r.title, r.url)}>
|
<div key={i} className="ms-site-row" onClick={() => onOpenUrl(r.title, r.url)}>
|
||||||
<span className="ms-site-source">{r.source}</span>
|
<span className="ms-site-source">{r.source}</span>
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ const HomePage: React.FC = () => {
|
|||||||
const [activeApp, setActiveApp] = useState<string>('home')
|
const [activeApp, setActiveApp] = useState<string>('home')
|
||||||
const [appCardList, setAppCardList] = useState<AppEntry[]>([])
|
const [appCardList, setAppCardList] = useState<AppEntry[]>([])
|
||||||
const [movieQuery, setMovieQuery] = useState<string | null>(null)
|
const [movieQuery, setMovieQuery] = useState<string | null>(null)
|
||||||
|
const [movieSearchKey, setMovieSearchKey] = useState(0)
|
||||||
|
const [openedFromSearch, setOpenedFromSearch] = useState(false)
|
||||||
const [bookmarks, setBookmarks] = useState<Bookmark[]>([])
|
const [bookmarks, setBookmarks] = useState<Bookmark[]>([])
|
||||||
const configRef = useRef<any>({})
|
const configRef = useRef<any>({})
|
||||||
|
|
||||||
@@ -46,14 +48,22 @@ const HomePage: React.FC = () => {
|
|||||||
const handleMovieSearch = (query: string) => {
|
const handleMovieSearch = (query: string) => {
|
||||||
window.electron?.hideView()
|
window.electron?.hideView()
|
||||||
setMovieQuery(query)
|
setMovieQuery(query)
|
||||||
|
setMovieSearchKey(k => k + 1)
|
||||||
|
setOpenedFromSearch(false)
|
||||||
setActiveApp('movie-search')
|
setActiveApp('movie-search')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMovieSearchOpen = (name: string, url: string) => {
|
const handleMovieSearchOpen = (name: string, url: string) => {
|
||||||
window.electron?.createView(name, url, '', 1.0, resolveUseProxy(url))
|
window.electron?.createView(name, url, '', 1.0, resolveUseProxy(url))
|
||||||
|
setOpenedFromSearch(true)
|
||||||
setActiveApp(name)
|
setActiveApp(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleBackToSearch = () => {
|
||||||
|
window.electron?.hideView()
|
||||||
|
setActiveApp('movie-search')
|
||||||
|
}
|
||||||
|
|
||||||
const handleBookmarkAdd = (title: string, url: string, poster: string, source: string) => {
|
const handleBookmarkAdd = (title: string, url: string, poster: string, source: string) => {
|
||||||
const updated = [...bookmarks, { title, url, poster, source }]
|
const updated = [...bookmarks, { title, url, poster, source }]
|
||||||
setBookmarks(updated)
|
setBookmarks(updated)
|
||||||
@@ -89,12 +99,14 @@ const HomePage: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
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} />
|
<Sidebar openedApps={sidebarApps} activeApp={activeApp} setActiveApp={setActiveApp} />
|
||||||
{activeApp === 'movie-search'
|
<div style={{ display: activeApp === 'movie-search' ? undefined : 'none' }}>
|
||||||
? <MovieSearch initialQuery={movieQuery ?? ''} onOpenUrl={handleMovieSearchOpen} onBookmark={handleBookmarkAdd} />
|
<MovieSearch key={movieSearchKey} initialQuery={movieQuery ?? ''} onOpenUrl={handleMovieSearchOpen} onBookmark={handleBookmarkAdd} />
|
||||||
: <AppList apps={appCardList} bookmarks={bookmarks} onBookmarkOpen={handleBookmarkOpen} onBookmarkRemove={handleBookmarkRemove} />
|
</div>
|
||||||
}
|
{activeApp !== 'movie-search' && (
|
||||||
|
<AppList apps={appCardList} bookmarks={bookmarks} onBookmarkOpen={handleBookmarkOpen} onBookmarkRemove={handleBookmarkRemove} />
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1323,3 +1323,64 @@ body {
|
|||||||
.modal-btn-yes { background: #E50914; color: #fff; }
|
.modal-btn-yes { background: #E50914; color: #fff; }
|
||||||
.modal-btn-no { background: rgba(255,255,255,0.1); color: #ccc; }
|
.modal-btn-no { background: rgba(255,255,255,0.1); color: #ccc; }
|
||||||
.modal-btn-ok { 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; }
|
||||||
|
|||||||
Reference in New Issue
Block a user