fix: kiosk toggle sync, load more animation, remove nsis target

This commit is contained in:
2026-03-14 12:48:29 +03:00
parent c31e4a304d
commit 14da54f204
77 changed files with 485 additions and 1424 deletions

View File

@@ -19,7 +19,7 @@ npm run dev
## Сборка
```bash
# Windows (zip)
# Windows (installer + zip)
npm run build:win
# Linux (AppImage + deb)
@@ -32,18 +32,56 @@ npm run build:linux
## Настройка
В настройках приложения (шестерёнка):
Настройки открываются кнопкой в левом верхнем углу приложения.
- **Список приложений** — сайты, которые отображаются на главном экране
- **TMDB API Key** — для поиска и обзора фильмов, получить на [themoviedb.org](https://www.themoviedb.org/settings/api)
### Приложения
Список сайтов, которые отображаются на главном экране в виде карточек. Для каждого можно указать:
- **Название** — отображается под иконкой
- **URL** — адрес сайта, открывается в отдельном WebContentsView
- **URL иконки** — картинка для карточки (необязательно)
- **Прокси** — использовать ли прокси для этого сайта (переключатель включается отдельно для каждого)
### Прокси
Приложение поддерживает HTTP/HTTPS/SOCKS5 прокси. Настраивается в разделе "Прокси" — указываешь хост и порт. Прокси применяется не глобально, а поприложенно: для каждого сайта в списке есть отдельный переключатель. Это позволяет, например, открывать заблокированные сайты через прокси, а остальные — напрямую.
Конфигурация прокси сохраняется в файл `~/.ESH-Media.json` и применяется при следующем запуске автоматически.
### Поиск фильмов
- **TMDB API Key** — ключ для поиска метаданных, постеров и обзора по фильтрам. Получить бесплатно на [themoviedb.org](https://www.themoviedb.org/settings/api). Поддерживаются как обычные API-ключи, так и Bearer-токены.
- **Сайты** — список фильмовых сайтов, на которых будет производиться поиск после выбора фильма из TMDB. Поддерживаются движки DLE (kinogo, lordfilm и зеркала), HDRezka, Filmix. Тип определяется автоматически по домену.
Если раздел "Сайты" пустой, приложение попробует использовать подходящие сайты из раздела "Приложения".
## Конфиг
Хранится в домашней директории пользователя: `~/.ESH-Media.json`.
```json
{
"apps": [...],
"proxy": { "host": "127.0.0.1", "port": "7890" },
"movieSites": [...],
"tmdbApiKey": "...",
"bookmarks": [...]
}
```
## Структура
```
main.js — main process
preload.js — preload / IPC bridge
main.js — main process
preload.js — preload / IPC bridge
index.html — точка входа основного UI
loader.html — экран загрузки
dialog-error.html — диалог ошибки
dialog-confirm.html — диалог подтверждения
src/
components/ — React components
pages/ pages
assets/ — styles
entries/ — entry points для Vite (loader, dialogs)
components/ — React компоненты
pages/ — страницы
styles/ — стили
```

12
dialog-confirm.html Normal file
View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Confirm</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/entries/dialog-confirm.tsx"></script>
</body>
</html>

12
dialog-error.html Normal file
View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Error</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/entries/dialog-error.tsx"></script>
</body>
</html>

12
loader.html Normal file
View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Loading</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/entries/loader.tsx"></script>
</body>
</html>

107
main.js
View File

@@ -163,6 +163,8 @@ async function createWindow() {
mainWindow = new BrowserWindow({
width: 1280,
height: 800,
kiosk: true,
autoHideMenuBar: true,
webPreferences: {
preload: PRELOAD_PATH,
contextIsolation: true,
@@ -214,17 +216,11 @@ function setLoader() {
loaderView = new WebContentsView({ webPreferences: { contextIsolation: true, nodeIntegration: false } });
addChild(loaderView);
loaderView.setBounds({ x: 0, y: HEADER_H, width, height: height - HEADER_H });
const html = `<html><head><style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#111;display:flex;align-items:center;justify-content:center;height:100vh;transition:opacity 0.25s ease;opacity:0}
body.visible{opacity:1}
.spinner{width:36px;height:36px;border:3px solid rgba(255,255,255,0.1);border-top-color:#E50914;border-radius:50%;animation:spin 0.7s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
</style></head><body>
<div class="spinner"></div>
<script>requestAnimationFrame(()=>requestAnimationFrame(()=>document.body.classList.add('visible')))</script>
</body></html>`;
loaderView.webContents.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent(html));
if (isDev) {
loaderView.webContents.loadURL(`${RENDERER_URL}/loader.html`);
} else {
loaderView.webContents.loadFile(path.join(__dirname, 'dist', 'loader.html'));
}
}
function removeLoader() {
@@ -237,41 +233,6 @@ function removeLoader() {
// --- Dialogs (WebContentsView overlays) ---
const DIALOG_STYLES = `
*{margin:0;padding:0;box-sizing:border-box}
body{
background:rgba(0,0,0,0);
display:flex;align-items:center;justify-content:center;
height:100vh;
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
transition:background 0.22s ease;
}
body.visible{background:rgba(0,0,0,0.78)}
body.hiding{background:rgba(0,0,0,0)}
.card{
background:#1c1c1c;border:1px solid rgba(255,255,255,0.1);
border-radius:12px;padding:32px 36px;text-align:center;
min-width:300px;max-width:420px;box-shadow:0 24px 64px rgba(0,0,0,0.8);
opacity:0;transform:scale(0.92) translateY(10px);
transition:opacity 0.22s ease,transform 0.22s cubic-bezier(0.34,1.56,0.64,1);
}
body.visible .card{opacity:1;transform:scale(1) translateY(0)}
body.hiding .card{opacity:0;transform:scale(0.95) translateY(6px)}
.title{font-size:17px;font-weight:700;color:#fff;margin-bottom:10px}
.msg{font-size:13px;color:#999;line-height:1.5;margin-bottom:26px}
.btns{display:flex;gap:10px;justify-content:center}
button{padding:10px 26px;border:none;border-radius:7px;font-size:13px;font-weight:600;cursor:pointer;transition:opacity 0.15s,transform 0.1s}
button:hover{opacity:0.85} button:active{transform:scale(0.97)}
.btn-yes{background:#E50914;color:#fff}
.btn-no,.btn-ok{background:rgba(255,255,255,0.1);color:#ccc}
`;
function dialogFadeIn(view) {
view.webContents.executeJavaScript(
`requestAnimationFrame(()=>requestAnimationFrame(()=>document.body.classList.add('visible')))`
).catch(() => {});
}
function dialogFadeOut(view, cb) {
view.webContents.executeJavaScript(
`document.body.classList.remove('visible');document.body.classList.add('hiding')`
@@ -292,15 +253,13 @@ function makeDialogView() {
function setError(title, text) {
const view = makeDialogView();
errorViews.push(view);
const html = `<html><head><style>${DIALOG_STYLES}</style></head><body>
<div class="card">
<div class="title">${title}</div>
<div class="msg">${text}</div>
<div class="btns"><button class="btn-ok" onclick="window.electron&&window.electron.handleAction('error')">Закрыть</button></div>
</div>
</body></html>`;
view.webContents.once('did-finish-load', () => { addChild(view); dialogFadeIn(view); });
view.webContents.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent(html));
const query = new URLSearchParams({ title: title || '', text: text || '' }).toString();
view.webContents.once('did-finish-load', () => { addChild(view); });
if (isDev) {
view.webContents.loadURL(`${RENDERER_URL}/dialog-error.html?${query}`);
} else {
view.webContents.loadFile(path.join(__dirname, 'dist', 'dialog-error.html'), { query: { title: title || '', text: text || '' } });
}
}
function removeError() {
@@ -312,17 +271,13 @@ function removeError() {
function setConfirm(text, actionOnYes) {
const view = makeDialogView();
confirmViews.push({ view, actionOnYes });
const html = `<html><head><style>${DIALOG_STYLES}</style></head><body>
<div class="card">
<div class="msg">${text}</div>
<div class="btns">
<button class="btn-yes" onclick="window.electron&&window.electron.handleAction('confirmYes')">Да</button>
<button class="btn-no" onclick="window.electron&&window.electron.handleAction('confirmNo')">Нет</button>
</div>
</div>
</body></html>`;
view.webContents.once('did-finish-load', () => { addChild(view); dialogFadeIn(view); });
view.webContents.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent(html));
const query = new URLSearchParams({ text: text || '' }).toString();
view.webContents.once('did-finish-load', () => { addChild(view); });
if (isDev) {
view.webContents.loadURL(`${RENDERER_URL}/dialog-confirm.html?${query}`);
} else {
view.webContents.loadFile(path.join(__dirname, 'dist', 'dialog-confirm.html'), { query: { text: text || '' } });
}
}
function removeConfirm() {
@@ -835,7 +790,21 @@ ipcMain.on('write-config', (_event, data) => {
}
});
ipcMain.on('confirm', (_event, text, actionOnYes) => setConfirm(text, actionOnYes));
ipcMain.handle('is-kiosk', () => mainWindow.isKiosk());
ipcMain.handle('toggle-kiosk', () => {
if (mainWindow.isKiosk()) {
mainWindow.setKiosk(false);
mainWindow.maximize();
mainWindow.setMenuBarVisibility(false);
return false;
} else {
mainWindow.setKiosk(true);
return true;
}
});
ipcMain.on('confirm',(_event, text, actionOnYes) => setConfirm(text, actionOnYes));
ipcMain.on('action', (_event, action) => {
if (action === 'error') {
@@ -889,8 +858,8 @@ app.whenReady().then(async () => {
} catch (_) {}
enableBlockingInSession(session.defaultSession);
enableBlockingInSession(session.fromPartition('persist:proxy'));
enableBlockingInSession(session.fromPartition('persist:direct'));
getProxySession();
getDirectSession();
await loadExtensions();
await createWindow();
});

View File

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

View File

@@ -26,4 +26,7 @@ contextBridge.exposeInMainWorld('electron', {
searchMovies: (query, sites) => ipcRenderer.invoke('search-movies', query, sites),
searchTmdb: (query, apiKey) => ipcRenderer.invoke('search-tmdb', query, apiKey),
discoverTmdb: (params) => ipcRenderer.invoke('discover-tmdb', params),
toggleKiosk: () => ipcRenderer.invoke('toggle-kiosk'),
isKiosk: () => ipcRenderer.invoke('is-kiosk'),
});

View File

@@ -1,13 +0,0 @@
{
"files": {
"main.css": "/static/css/main.e6c6eccb.css",
"main.js": "/static/js/main.e2d60cfb.js",
"index.html": "/index.html",
"main.e6c6eccb.css.map": "/static/css/main.e6c6eccb.css.map",
"main.e2d60cfb.js.map": "/static/js/main.e2d60cfb.js.map"
},
"entrypoints": [
"static/css/main.e6c6eccb.css",
"static/js/main.e2d60cfb.js"
]
}

View File

@@ -1 +0,0 @@
<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Loader Animation</title><script defer="defer" src="/static/js/main.e2d60cfb.js"></script><link href="/static/css/main.e6c6eccb.css" rel="stylesheet"></head><body><div id="root"></div></body></html>

View File

@@ -1,2 +0,0 @@
.confirm{position:relative}.confirm div{border-radius:50%;opacity:1;position:absolute}h1,h2{color:#fff}.confirm-container{background-color:#626262;border:0 solid #000;border-radius:20px;display:flex;left:50%;max-height:600px;min-height:200px;min-width:400px;padding:15px;position:fixed;top:50%;transform:translate(-50%,-50%);z-index:9999}.confirm-button-container{bottom:40px;color:#fff;font-size:medium;position:absolute;right:40px;width:100}.confirm-button-container button{background:#0f00;border:none;border-radius:4px;color:#fff;cursor:pointer;font-size:20px;height:50px;margin-right:20px;transition:background .3s ease;width:70px}.confirm-button:hover{background:#717171}body{align-items:center;animation:fadeIn .5s forwards;display:flex;height:100vh;justify-content:center;margin:0;opacity:0;overflow:hidden;padding:0}@keyframes fadeIn{0%{opacity:0}to{background:#000000b3;opacity:.9}}
/*# sourceMappingURL=main.e6c6eccb.css.map*/

View File

@@ -1 +0,0 @@
{"version":3,"file":"static/css/main.e6c6eccb.css","mappings":"AACA,SACI,iBACF,CAEA,aAGE,iBAAkB,CADlB,SAAU,CADV,iBAIF,CACA,MACE,UAEF,CCbF,mBAKI,wBAAiC,CACjC,mBAAuB,CACvB,kBAAmB,CACnB,YAAa,CALb,QAAS,CAUT,gBAAiB,CADjB,gBAAiB,CADjB,eAAgB,CAFhB,YAAa,CARb,cAAc,CACd,OAAQ,CAER,8BAAgC,CAMhC,YAIF,CAEA,0BAKE,WAAY,CAHZ,UAAY,CACZ,gBAAiB,CACjB,iBAAkB,CAElB,UAAW,CALX,SAMF,CAEA,iCAGE,gBAA8B,CAC9B,WAAY,CAGZ,iBAAkB,CAElB,UAAY,CAHZ,cAAe,CADf,cAAe,CAHf,WAAY,CAQZ,iBAAkB,CAFlB,8BAAgC,CAPhC,UAUF,CAEA,sBACE,kBACF,CCxCF,KAOI,kBAAmB,CAEnB,6BAA+B,CAJ/B,YAAa,CADb,YAAa,CAEb,sBAAuB,CALvB,QAAS,CAOT,SAAU,CALV,eAAgB,CADhB,SAQF,CAEA,kBACE,GACE,SACF,CACA,GAEE,oBAA8B,CAD9B,UAEF,CACF","sources":["components/Confirm.css","components/ConfirmContainer.css","App.css"],"sourcesContent":["/* src/components/Loader.css */\r\n.confirm {\r\n position: relative;\r\n }\r\n \r\n .confirm div {\r\n position: absolute;\r\n opacity: 1;\r\n border-radius: 50%;\r\n /* animation: loader-animation 1.5s infinite ease-in-out; */\r\n }\r\n h1, h2 {\r\n color: white;\r\n /* animation: loader-animation 1.5s infinite ease-in-out; */\r\n }\r\n /* .error div:nth-child(2) {\r\n animation-delay: -1.2s;\r\n }\r\n \r\n @keyframes error-animation {\r\n 0%,\r\n 100% {\r\n width: 0;\r\n height: 0;\r\n top: 50px;\r\n left: 50px;\r\n opacity: 0.5;\r\n }\r\n 50% {\r\n width: 100px;\r\n height: 100px;\r\n top: 0;\r\n left: 0;\r\n opacity: 0;\r\n }\r\n }\r\n */","/* src/components/LoaderContainer.css */\r\n.confirm-container {\r\n position:fixed; /* Fixed position to keep it centered relative to the viewport */\r\n top: 50%; /* Center vertically */\r\n left: 50%; /* Center horizontally */\r\n transform: translate(-50%, -50%); /* Adjust for the element's size */\r\n background-color: rgb(98, 98, 98);\r\n border: 0px solid black;\r\n border-radius: 20px;\r\n display: flex;\r\n padding: 15px;\r\n z-index: 9999; /* Ensure it's on top of other content */\r\n min-width: 400px; /* Width of the loader */\r\n min-height: 200px; /* Height of the loader */\r\n max-height: 600px; /* Height of the loader */\r\n }\r\n \r\n .confirm-button-container {\r\n width: 100;\r\n color: white;\r\n font-size: medium;\r\n position: absolute;\r\n bottom: 40px; \r\n right: 40px;\r\n }\r\n \r\n .confirm-button-container button {\r\n width: 70px;\r\n height: 50px;\r\n background: rgba(0, 255, 0, 0); /* Transparent background */\r\n border: none;\r\n font-size: 20px;\r\n cursor: pointer;\r\n border-radius: 4px;\r\n transition: background 0.3s ease;\r\n color: white;\r\n margin-right: 20px;\r\n }\r\n \r\n .confirm-button:hover {\r\n background: rgb(113, 113, 113); /* Light green with opacity 0.5 */\r\n } \r\n ","/* src/App.css */\r\nbody {\r\n margin: 0;\r\n padding: 0;\r\n overflow: hidden;\r\n height: 100vh;\r\n display: flex;\r\n justify-content: center;\r\n align-items: center;\r\n opacity: 0;\r\n animation: fadeIn 0.5s forwards;\r\n }\r\n \r\n @keyframes fadeIn {\r\n from {\r\n opacity: 0;\r\n }\r\n to {\r\n opacity: 0.9;\r\n background: rgba(0, 0, 0, 0.7);\r\n }\r\n }\r\n "],"names":[],"sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@@ -1,39 +0,0 @@
/**
* @license React
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

File diff suppressed because one or more lines are too long

View File

@@ -1,101 +0,0 @@
<html lang="en" class="focus-outline-visible">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Loader Animation</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
animation: fadeIn 0.5s forwards;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 0.9;
background: rgba(0, 0, 0, 0.7);
}
}
.loader-container {
position: absolute;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.loader {
position: relative;
width: 100px;
height: 100px;
}
.loader div {
position: absolute;
border: 4px solid gray;
opacity: 1;
border-radius: 50%;
animation: loader-animation 1.5s infinite ease-in-out;
}
.loader div:nth-child(2) {
animation-delay: -1.2s;
}
@keyframes loader-animation {
0%,
100% {
width: 0;
height: 0;
top: 50px;
left: 50px;
opacity: 0.5;
}
50% {
width: 100px;
height: 100px;
top: 0;
left: 0;
opacity: 0;
}
}
</style>
</head>
<body>
<div class="loader-container">
<div class="loader">
<div></div>
<div></div>
</div>
</div>
<div class="loader-container">
<div class="loader">
<div></div>
<div></div>
</div>
<div class="loader">
<div></div>
<div></div>
</div>
</div>
</body>
</html>

View File

@@ -1,40 +0,0 @@
{
"name": "torrent_client",
"version": "1.0.0",
"private": true,
"proxy": "http://localhost:3001/",
"dependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-scripts": "5.0.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"serv": "node ./serv.js"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"main": "index.js",
"author": "",
"license": "ISC",
"description": ""
}

View File

@@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Loader Animation</title>
</head>
<body>
<!-- This is where your React app will be injected -->
<div id="root"></div>
</body>
</html>

View File

@@ -1,16 +0,0 @@
const express = require('express');
const path = require('path');
// const __filename = fileURLToPath(import.meta.url);
// const __dirname = dirname(__filename);
const app = express();
app.use(express.static(path.join(__dirname, 'build')));
app.use(express.static(path.join(__dirname, 'build')));
app.use(express.static(path.join(__dirname, 'build')));
app.get('/*', function (req, res) {
res.sendFile(path.join(__dirname, 'build', 'index.html'));
});
app.listen(3001, () => console.log('Example app is listening on port 3001.'));

View File

@@ -1,23 +0,0 @@
/* src/App.css */
body {
margin: 0;
padding: 0;
overflow: hidden;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
animation: fadeIn 0.5s forwards;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 0.9;
background: rgba(0, 0, 0, 0.7);
}
}

View File

@@ -1,22 +0,0 @@
// src/App.tsx
import React from "react";
import ConfirmContainer from "./components/ConfirmContainer";
import "./App.css";
declare global {
interface Window {
electron?: {
handleAction: (action: string) => void;
};
}
}
const App: React.FC = () => {
return (
<div>
<ConfirmContainer />
</div>
);
};
export default App;

View File

@@ -1,37 +0,0 @@
/* src/components/Loader.css */
.confirm {
position: relative;
}
.confirm div {
position: absolute;
opacity: 1;
border-radius: 50%;
/* animation: loader-animation 1.5s infinite ease-in-out; */
}
h1, h2 {
color: white;
/* animation: loader-animation 1.5s infinite ease-in-out; */
}
/* .error div:nth-child(2) {
animation-delay: -1.2s;
}
@keyframes error-animation {
0%,
100% {
width: 0;
height: 0;
top: 50px;
left: 50px;
opacity: 0.5;
}
50% {
width: 100px;
height: 100px;
top: 0;
left: 0;
opacity: 0;
}
}
*/

View File

@@ -1,31 +0,0 @@
// src/components/Loader.tsx
import React, { useEffect, useState } from "react";
import "./Confirm.css";
interface ErrorDataProps {
title: string;
text: string;
}
interface ErrorProps {
errorData: ErrorDataProps;
setCustomData: (errorData: ErrorDataProps) => void;
}
const Error: React.FC = () => {
const [errorData, setCustomData] = useState<ErrorDataProps>();
useEffect(() => {
fetch('http://localhost:3001/api/custom-data')
.then(response => response.json())
.then(errorData => setCustomData(errorData ?? {title: "Uncaught", text: "error"}))
.catch(error => console.error('Error fetching custom text:', error));
}, []);
return (
<div className="confirm">
<h2>{errorData?.text}</h2>
</div>
);
};
export default Error;

View File

@@ -1,43 +0,0 @@
/* src/components/LoaderContainer.css */
.confirm-container {
position:fixed; /* Fixed position to keep it centered relative to the viewport */
top: 50%; /* Center vertically */
left: 50%; /* Center horizontally */
transform: translate(-50%, -50%); /* Adjust for the element's size */
background-color: rgb(98, 98, 98);
border: 0px solid black;
border-radius: 20px;
display: flex;
padding: 15px;
z-index: 9999; /* Ensure it's on top of other content */
min-width: 400px; /* Width of the loader */
min-height: 200px; /* Height of the loader */
max-height: 600px; /* Height of the loader */
}
.confirm-button-container {
width: 100;
color: white;
font-size: medium;
position: absolute;
bottom: 40px;
right: 40px;
}
.confirm-button-container button {
width: 70px;
height: 50px;
background: rgba(0, 255, 0, 0); /* Transparent background */
border: none;
font-size: 20px;
cursor: pointer;
border-radius: 4px;
transition: background 0.3s ease;
color: white;
margin-right: 20px;
}
.confirm-button:hover {
background: rgb(113, 113, 113); /* Light green with opacity 0.5 */
}

View File

@@ -1,39 +0,0 @@
// src/components/LoaderContainer.tsx
import React, { useEffect, useState } from "react";
import Error from "./Confirm";
import "./ConfirmContainer.css";
const ConfirmContainer: React.FC = () => {
return (
<div className="confirm-container">
<Error />
<div className="confirm-button-container">
<button
className="confirm-button"
onClick={() =>
{
window?.electron &&
window.electron.handleAction("confirmNo");
}
}
>
<strong>Нет</strong>
</button>
<button
className="confirm-button"
onClick={() =>
{
window?.electron &&
window.electron.handleAction("confirmYes");
}
}
>
<strong>Да</strong>
</button>
</div>
</div>
);
};
export default ConfirmContainer;

View File

@@ -1,22 +0,0 @@
// src/index.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
// Get the root element from the DOM
const rootElement = document.getElementById("root");
if (rootElement) {
// Create a root and render the app
const root = ReactDOM.createRoot(rootElement);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
} else {
console.error("Root element not found");
}

View File

@@ -1,21 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": false,
"jsx": "react-jsx",
"noEmit": true
},
"include": ["src"]
}

View File

@@ -1,13 +0,0 @@
{
"files": {
"main.css": "/static/css/main.a5182e31.css",
"main.js": "/static/js/main.ab661a74.js",
"index.html": "/index.html",
"main.a5182e31.css.map": "/static/css/main.a5182e31.css.map",
"main.ab661a74.js.map": "/static/js/main.ab661a74.js.map"
},
"entrypoints": [
"static/css/main.a5182e31.css",
"static/js/main.ab661a74.js"
]
}

View File

@@ -1 +0,0 @@
<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Loader Animation</title><script defer="defer" src="/static/js/main.ab661a74.js"></script><link href="/static/css/main.a5182e31.css" rel="stylesheet"></head><body><div id="root"></div></body></html>

View File

@@ -1,2 +0,0 @@
.error{position:relative}.error div{border-radius:50%;opacity:1;position:absolute}h1,h2{color:#fff}.error-container{background-color:#626262;border:0 solid #000;border-radius:20px;display:flex;left:50%;max-height:600px;min-height:200px;min-width:400px;padding:15px;position:fixed;top:50%;transform:translate(-50%,-50%);z-index:9999}.error-button{background:#0f00;border:none;border-radius:4px;bottom:40px;color:#fff;cursor:pointer;font-size:medium;font-size:16px;padding:10px 20px;position:absolute;right:40px;transition:background .3s ease}.error-button strong{color:#fff}.error-button:hover{background:#717171}body{align-items:center;animation:fadeIn .5s forwards;display:flex;height:100vh;justify-content:center;margin:0;opacity:0;overflow:hidden;padding:0}@keyframes fadeIn{0%{opacity:0}to{background:#000000b3;opacity:.9}}
/*# sourceMappingURL=main.a5182e31.css.map*/

View File

@@ -1 +0,0 @@
{"version":3,"file":"static/css/main.a5182e31.css","mappings":"AACA,OACI,iBACF,CAEA,WAGE,iBAAkB,CADlB,SAAU,CADV,iBAIF,CACA,MACE,UAEF,CCbF,iBAKI,wBAAiC,CACjC,mBAAuB,CACvB,kBAAmB,CACnB,YAAa,CALb,QAAS,CAUT,gBAAiB,CADjB,gBAAiB,CADjB,eAAgB,CAFhB,YAAa,CARb,cAAc,CACd,OAAQ,CAER,8BAAgC,CAMhC,YAIF,CAEA,cAME,gBAA8B,CAC9B,WAAY,CAIZ,iBAAkB,CAPlB,WAAY,CAHZ,UAAY,CASZ,cAAe,CARf,gBAAiB,CAOjB,cAAe,CADf,iBAAkB,CALlB,iBAAkB,CAElB,UAAW,CAOX,8BACF,CAEA,qBACE,UACF,CAEA,oBACE,kBACF,CCrCF,KAOI,kBAAmB,CAEnB,6BAA+B,CAJ/B,YAAa,CADb,YAAa,CAEb,sBAAuB,CALvB,QAAS,CAOT,SAAU,CALV,eAAgB,CADhB,SAQF,CAEA,kBACE,GACE,SACF,CACA,GAEE,oBAA8B,CAD9B,UAEF,CACF","sources":["components/Error.css","components/ErrorContainer.css","App.css"],"sourcesContent":["/* src/components/Loader.css */\r\n.error {\r\n position: relative;\r\n }\r\n \r\n .error div {\r\n position: absolute;\r\n opacity: 1;\r\n border-radius: 50%;\r\n /* animation: loader-animation 1.5s infinite ease-in-out; */\r\n }\r\n h1, h2 {\r\n color: white;\r\n /* animation: loader-animation 1.5s infinite ease-in-out; */\r\n }\r\n /* .error div:nth-child(2) {\r\n animation-delay: -1.2s;\r\n }\r\n \r\n @keyframes error-animation {\r\n 0%,\r\n 100% {\r\n width: 0;\r\n height: 0;\r\n top: 50px;\r\n left: 50px;\r\n opacity: 0.5;\r\n }\r\n 50% {\r\n width: 100px;\r\n height: 100px;\r\n top: 0;\r\n left: 0;\r\n opacity: 0;\r\n }\r\n }\r\n */","/* src/components/LoaderContainer.css */\r\n.error-container {\r\n position:fixed; /* Fixed position to keep it centered relative to the viewport */\r\n top: 50%; /* Center vertically */\r\n left: 50%; /* Center horizontally */\r\n transform: translate(-50%, -50%); /* Adjust for the element's size */\r\n background-color: rgb(98, 98, 98);\r\n border: 0px solid black;\r\n border-radius: 20px;\r\n display: flex;\r\n padding: 15px;\r\n z-index: 9999; /* Ensure it's on top of other content */\r\n min-width: 400px; /* Width of the loader */\r\n min-height: 200px; /* Height of the loader */\r\n max-height: 600px; /* Height of the loader */\r\n }\r\n \r\n .error-button {\r\n color: white;\r\n font-size: medium;\r\n position: absolute;\r\n bottom: 40px; \r\n right: 40px;\r\n background: rgba(0, 255, 0, 0); /* Transparent background */\r\n border: none;\r\n padding: 10px 20px;\r\n font-size: 16px;\r\n cursor: pointer;\r\n border-radius: 4px;\r\n transition: background 0.3s ease;\r\n }\r\n \r\n .error-button strong {\r\n color: white;\r\n }\r\n \r\n .error-button:hover {\r\n background: rgb(113, 113, 113); /* Light green with opacity 0.5 */\r\n } \r\n ","/* src/App.css */\r\nbody {\r\n margin: 0;\r\n padding: 0;\r\n overflow: hidden;\r\n height: 100vh;\r\n display: flex;\r\n justify-content: center;\r\n align-items: center;\r\n opacity: 0;\r\n animation: fadeIn 0.5s forwards;\r\n }\r\n \r\n @keyframes fadeIn {\r\n from {\r\n opacity: 0;\r\n }\r\n to {\r\n opacity: 0.9;\r\n background: rgba(0, 0, 0, 0.7);\r\n }\r\n }\r\n "],"names":[],"sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@@ -1,39 +0,0 @@
/**
* @license React
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

File diff suppressed because one or more lines are too long

View File

@@ -1,101 +0,0 @@
<html lang="en" class="focus-outline-visible">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Loader Animation</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
animation: fadeIn 0.5s forwards;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 0.9;
background: rgba(0, 0, 0, 0.7);
}
}
.loader-container {
position: absolute;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.loader {
position: relative;
width: 100px;
height: 100px;
}
.loader div {
position: absolute;
border: 4px solid gray;
opacity: 1;
border-radius: 50%;
animation: loader-animation 1.5s infinite ease-in-out;
}
.loader div:nth-child(2) {
animation-delay: -1.2s;
}
@keyframes loader-animation {
0%,
100% {
width: 0;
height: 0;
top: 50px;
left: 50px;
opacity: 0.5;
}
50% {
width: 100px;
height: 100px;
top: 0;
left: 0;
opacity: 0;
}
}
</style>
</head>
<body>
<div class="loader-container">
<div class="loader">
<div></div>
<div></div>
</div>
</div>
<div class="loader-container">
<div class="loader">
<div></div>
<div></div>
</div>
<div class="loader">
<div></div>
<div></div>
</div>
</div>
</body>
</html>

View File

@@ -1,40 +0,0 @@
{
"name": "torrentClient",
"version": "1.0.0",
"private": true,
"proxy": "http://localhost:3001/",
"dependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-scripts": "5.0.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"serv": "node ./serv.js"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"main": "index.js",
"author": "",
"license": "ISC",
"description": ""
}

View File

@@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Loader Animation</title>
</head>
<body>
<!-- This is where your React app will be injected -->
<div id="root"></div>
</body>
</html>

View File

@@ -1,16 +0,0 @@
const express = require('express');
const path = require('path');
// const __filename = fileURLToPath(import.meta.url);
// const __dirname = dirname(__filename);
const app = express();
app.use(express.static(path.join(__dirname, 'build')));
app.use(express.static(path.join(__dirname, 'build')));
app.use(express.static(path.join(__dirname, 'build')));
app.get('/*', function (req, res) {
res.sendFile(path.join(__dirname, 'build', 'index.html'));
});
app.listen(3001, () => console.log('Example app is listening on port 3001.'));

View File

@@ -1,23 +0,0 @@
/* src/App.css */
body {
margin: 0;
padding: 0;
overflow: hidden;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
animation: fadeIn 0.5s forwards;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 0.9;
background: rgba(0, 0, 0, 0.7);
}
}

View File

@@ -1,22 +0,0 @@
// src/App.tsx
import React from "react";
import ErrorContainer from "./components/ErrorContainer";
import "./App.css";
declare global {
interface Window {
electron?: {
handleAction: (action: string, isCancel?: boolean) => void;
};
}
}
const App: React.FC = () => {
return (
<div>
<ErrorContainer />
</div>
);
};
export default App;

View File

@@ -1,37 +0,0 @@
/* src/components/Loader.css */
.error {
position: relative;
}
.error div {
position: absolute;
opacity: 1;
border-radius: 50%;
/* animation: loader-animation 1.5s infinite ease-in-out; */
}
h1, h2 {
color: white;
/* animation: loader-animation 1.5s infinite ease-in-out; */
}
/* .error div:nth-child(2) {
animation-delay: -1.2s;
}
@keyframes error-animation {
0%,
100% {
width: 0;
height: 0;
top: 50px;
left: 50px;
opacity: 0.5;
}
50% {
width: 100px;
height: 100px;
top: 0;
left: 0;
opacity: 0;
}
}
*/

View File

@@ -1,32 +0,0 @@
// src/components/Loader.tsx
import React, { useEffect, useState } from "react";
import "./Error.css";
interface ErrorDataProps {
title: string;
text: string;
}
interface ErrorProps {
errorData: ErrorDataProps;
setCustomData: (errorData: ErrorDataProps) => void;
}
const Error: React.FC = () => {
const [errorData, setCustomData] = useState<ErrorDataProps>();
useEffect(() => {
fetch('http://localhost:3001/api/custom-data')
.then(response => response.json())
.then(errorData => setCustomData(errorData ?? {title: "Uncaught", text: "error"}))
.catch(error => console.error('Error fetching custom text:', error));
}, []);
return (
<div className="error">
<h1>{errorData?.title}</h1>
<h2>{errorData?.text}</h2>
</div>
);
};
export default Error;

View File

@@ -1,40 +0,0 @@
/* src/components/LoaderContainer.css */
.error-container {
position:fixed; /* Fixed position to keep it centered relative to the viewport */
top: 50%; /* Center vertically */
left: 50%; /* Center horizontally */
transform: translate(-50%, -50%); /* Adjust for the element's size */
background-color: rgb(98, 98, 98);
border: 0px solid black;
border-radius: 20px;
display: flex;
padding: 15px;
z-index: 9999; /* Ensure it's on top of other content */
min-width: 400px; /* Width of the loader */
min-height: 200px; /* Height of the loader */
max-height: 600px; /* Height of the loader */
}
.error-button {
color: white;
font-size: medium;
position: absolute;
bottom: 40px;
right: 40px;
background: rgba(0, 255, 0, 0); /* Transparent background */
border: none;
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
border-radius: 4px;
transition: background 0.3s ease;
}
.error-button strong {
color: white;
}
.error-button:hover {
background: rgb(113, 113, 113); /* Light green with opacity 0.5 */
}

View File

@@ -1,27 +0,0 @@
// src/components/LoaderContainer.tsx
import React, { useEffect, useState } from "react";
import Error from "./Error";
import "./ErrorContainer.css";
const ErrorContainer: React.FC = () => {
return (
<div className="error-container">
<Error />
<button
className="error-button"
onClick={() =>
{
window?.electron &&
window.electron.handleAction("error");
}
}
>
<strong>OK</strong>
</button>
</div>
);
};
export default ErrorContainer;

View File

@@ -1,22 +0,0 @@
// src/index.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
// Get the root element from the DOM
const rootElement = document.getElementById("root");
if (rootElement) {
// Create a root and render the app
const root = ReactDOM.createRoot(rootElement);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
} else {
console.error("Root element not found");
}

View File

@@ -1,21 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": false,
"jsx": "react-jsx",
"noEmit": true
},
"include": ["src"]
}

View File

@@ -1,13 +0,0 @@
{
"files": {
"main.css": "/static/css/main.4d16bcf2.css",
"main.js": "/static/js/main.bc0cdbdb.js",
"index.html": "/index.html",
"main.4d16bcf2.css.map": "/static/css/main.4d16bcf2.css.map",
"main.bc0cdbdb.js.map": "/static/js/main.bc0cdbdb.js.map"
},
"entrypoints": [
"static/css/main.4d16bcf2.css",
"static/js/main.bc0cdbdb.js"
]
}

View File

@@ -1 +0,0 @@
<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Loader Animation</title><script defer="defer" src="/static/js/main.bc0cdbdb.js"></script><link href="/static/css/main.4d16bcf2.css" rel="stylesheet"></head><body><div id="root"></div></body></html>

View File

@@ -1,2 +0,0 @@
.loader{height:100px;position:relative;width:100px}.loader div{animation:loader-animation 1.5s ease-in-out infinite;border:4px solid gray;border-radius:50%;opacity:1;position:absolute}.loader div:nth-child(2){animation-delay:-1.2s}@keyframes loader-animation{0%,to{height:0;left:50px;opacity:.5;top:50px;width:0}50%{height:100px;left:0;opacity:0;top:0;width:100px}}.loader-container{height:100px;left:50%;position:fixed;top:50%;transform:translate(-50%,-50%);width:100px;z-index:9999}.loader-container,body{align-items:center;display:flex;justify-content:center}body{animation:fadeIn .5s forwards;height:100vh;margin:0;opacity:0;overflow:hidden;padding:0}@keyframes fadeIn{0%{opacity:0}to{background:#000000b3;opacity:.9}}
/*# sourceMappingURL=main.4d16bcf2.css.map*/

View File

@@ -1 +0,0 @@
{"version":3,"file":"static/css/main.4d16bcf2.css","mappings":"AACA,QAGI,YAAa,CAFb,iBAAkB,CAClB,WAEF,CAEA,YAKE,oDAAqD,CAHrD,qBAAsB,CAEtB,iBAAkB,CADlB,SAAU,CAFV,iBAKF,CAEA,yBACE,qBACF,CAEA,4BACE,MAGE,QAAS,CAET,SAAU,CACV,UAAY,CAFZ,QAAS,CAFT,OAKF,CACA,IAEE,YAAa,CAEb,MAAO,CACP,SAAU,CAFV,KAAM,CAFN,WAKF,CACF,CClCF,kBAUI,YAAa,CAPb,QAAS,CAFT,cAAe,CACf,OAAQ,CAER,8BAAgC,CAKhC,WAAY,CADZ,YAGF,CCXF,uBDOI,kBAAmB,CAFnB,YAAa,CACb,sBCIF,CAVF,KASI,6BAA+B,CAL/B,YAAa,CAHb,QAAS,CAOT,SAAU,CALV,eAAgB,CADhB,SAQF,CAEA,kBACE,GACE,SACF,CACA,GAEE,oBAA8B,CAD9B,UAEF,CACF","sources":["components/Loader.css","components/LoaderContainer.css","App.css"],"sourcesContent":["/* src/components/Loader.css */\r\n.loader {\r\n position: relative;\r\n width: 100px;\r\n height: 100px;\r\n }\r\n \r\n .loader div {\r\n position: absolute;\r\n border: 4px solid gray;\r\n opacity: 1;\r\n border-radius: 50%;\r\n animation: loader-animation 1.5s infinite ease-in-out;\r\n }\r\n \r\n .loader div:nth-child(2) {\r\n animation-delay: -1.2s;\r\n }\r\n \r\n @keyframes loader-animation {\r\n 0%,\r\n 100% {\r\n width: 0;\r\n height: 0;\r\n top: 50px;\r\n left: 50px;\r\n opacity: 0.5;\r\n }\r\n 50% {\r\n width: 100px;\r\n height: 100px;\r\n top: 0;\r\n left: 0;\r\n opacity: 0;\r\n }\r\n }\r\n ","/* src/components/LoaderContainer.css */\r\n.loader-container {\r\n position: fixed; /* Fixed position to keep it centered relative to the viewport */\r\n top: 50%; /* Center vertically */\r\n left: 50%; /* Center horizontally */\r\n transform: translate(-50%, -50%); /* Adjust for the element's size */\r\n display: flex;\r\n justify-content: center;\r\n align-items: center;\r\n z-index: 9999; /* Ensure it's on top of other content */\r\n width: 100px; /* Width of the loader */\r\n height: 100px; /* Height of the loader */\r\n }\r\n ","/* src/App.css */\r\nbody {\r\n margin: 0;\r\n padding: 0;\r\n overflow: hidden;\r\n height: 100vh;\r\n display: flex;\r\n justify-content: center;\r\n align-items: center;\r\n opacity: 0;\r\n animation: fadeIn 0.5s forwards;\r\n }\r\n \r\n @keyframes fadeIn {\r\n from {\r\n opacity: 0;\r\n }\r\n to {\r\n opacity: 0.9;\r\n background: rgba(0, 0, 0, 0.7);\r\n }\r\n }\r\n "],"names":[],"sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@@ -1,39 +0,0 @@
/**
* @license React
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

File diff suppressed because one or more lines are too long

View File

@@ -1,101 +0,0 @@
<html lang="en" class="focus-outline-visible">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Loader Animation</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
animation: fadeIn 0.5s forwards;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 0.9;
background: rgba(0, 0, 0, 0.7);
}
}
.loader-container {
position: absolute;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.loader {
position: relative;
width: 100px;
height: 100px;
}
.loader div {
position: absolute;
border: 4px solid gray;
opacity: 1;
border-radius: 50%;
animation: loader-animation 1.5s infinite ease-in-out;
}
.loader div:nth-child(2) {
animation-delay: -1.2s;
}
@keyframes loader-animation {
0%,
100% {
width: 0;
height: 0;
top: 50px;
left: 50px;
opacity: 0.5;
}
50% {
width: 100px;
height: 100px;
top: 0;
left: 0;
opacity: 0;
}
}
</style>
</head>
<body>
<div class="loader-container">
<div class="loader">
<div></div>
<div></div>
</div>
</div>
<div class="loader-container">
<div class="loader">
<div></div>
<div></div>
</div>
<div class="loader">
<div></div>
<div></div>
</div>
</div>
</body>
</html>

View File

@@ -1,41 +0,0 @@
{
"name": "loader",
"version": "1.0.0",
"private": true,
"proxy": "http://localhost:3001/",
"dependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-scripts": "5.0.1"
},
"scripts": {
"d": "react-scripts build && react-scripts start",
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"serv": "node ./serv.js"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"main": "index.js",
"author": "",
"license": "ISC",
"description": ""
}

View File

@@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Loader Animation</title>
</head>
<body>
<!-- This is where your React app will be injected -->
<div id="root"></div>
</body>
</html>

View File

@@ -1,16 +0,0 @@
const express = require('express');
const path = require('path');
// const __filename = fileURLToPath(import.meta.url);
// const __dirname = dirname(__filename);
const app = express();
app.use(express.static(path.join(__dirname, 'build')));
app.use(express.static(path.join(__dirname, 'build')));
app.use(express.static(path.join(__dirname, 'build')));
app.get('/*', function (req, res) {
res.sendFile(path.join(__dirname, 'build', 'index.html'));
});
app.listen(3001, () => console.log('Example app is listening on port 3001.'));

View File

@@ -1,23 +0,0 @@
/* src/App.css */
body {
margin: 0;
padding: 0;
overflow: hidden;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
animation: fadeIn 0.5s forwards;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 0.9;
background: rgba(0, 0, 0, 0.7);
}
}

View File

@@ -1,22 +0,0 @@
// src/App.tsx
import React from "react";
import LoaderContainer from "./components/LoaderContainer";
import "./App.css";
declare global {
interface Window {
electron?: {
handleAction: (action: string, isCancel?: boolean) => void;
};
}
}
const App: React.FC = () => {
return (
<div>
<LoaderContainer />
<LoaderContainer />
</div>
);
};
export default App;

View File

@@ -1,37 +0,0 @@
/* src/components/Loader.css */
.loader {
position: relative;
width: 100px;
height: 100px;
}
.loader div {
position: absolute;
border: 4px solid gray;
opacity: 1;
border-radius: 50%;
animation: loader-animation 1.5s infinite ease-in-out;
}
.loader div:nth-child(2) {
animation-delay: -1.2s;
}
@keyframes loader-animation {
0%,
100% {
width: 0;
height: 0;
top: 50px;
left: 50px;
opacity: 0.5;
}
50% {
width: 100px;
height: 100px;
top: 0;
left: 0;
opacity: 0;
}
}

View File

@@ -1,14 +0,0 @@
// src/components/Loader.tsx
import React from "react";
import "./Loader.css";
const Loader: React.FC = () => {
return (
<div className="loader">
<div></div>
<div></div>
</div>
);
};
export default Loader;

View File

@@ -1,14 +0,0 @@
/* src/components/LoaderContainer.css */
.loader-container {
position: fixed; /* Fixed position to keep it centered relative to the viewport */
top: 50%; /* Center vertically */
left: 50%; /* Center horizontally */
transform: translate(-50%, -50%); /* Adjust for the element's size */
display: flex;
justify-content: center;
align-items: center;
z-index: 9999; /* Ensure it's on top of other content */
width: 100px; /* Width of the loader */
height: 100px; /* Height of the loader */
}

View File

@@ -1,14 +0,0 @@
// src/components/LoaderContainer.tsx
import React from "react";
import Loader from "./Loader";
import "./LoaderContainer.css";
const LoaderContainer: React.FC = () => {
return (
<div className="loader-container">
<Loader />
</div>
);
};
export default LoaderContainer;

View File

@@ -1,22 +0,0 @@
// src/index.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
// Get the root element from the DOM
const rootElement = document.getElementById("root");
if (rootElement) {
// Create a root and render the app
const root = ReactDOM.createRoot(rootElement);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
} else {
console.error("Root element not found");
}

View File

@@ -1,21 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": false,
"jsx": "react-jsx",
"noEmit": true
},
"include": ["src"]
}

12
sidebar.html Normal file
View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sidebar</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/entries/sidebar.tsx"></script>
</body>
</html>

View File

@@ -105,6 +105,16 @@ const Header: React.FC<HeaderProps> = ({ activeApp, setActiveApp, onAppsChange,
const appOpen = activeApp !== 'home' && activeApp !== 'movie-search'
const showSearchIcon = activeApp === 'home' || activeApp === 'movie-search'
const [isKiosk, setIsKiosk] = useState(true)
useEffect(() => {
window.electron?.isKiosk().then(k => setIsKiosk(k))
}, [])
const toggleKiosk = () => {
window.electron?.toggleKiosk().then((newState: boolean) => setIsKiosk(newState))
}
const [isBookmarked, setIsBookmarked] = useState(false)
const handleBookmark = () => {
@@ -147,6 +157,25 @@ const Header: React.FC<HeaderProps> = ({ activeApp, setActiveApp, onAppsChange,
{(!isCollapsed || isHovered) && (
<>
<div className="header-left">
<div className="header-btn" onClick={toggleKiosk} title={isKiosk ? 'Выйти из режима киоска' : 'Режим киоска'}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#aaa" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
{isKiosk ? (
<>
<polyline points="4 14 10 14 10 20" />
<polyline points="20 10 14 10 14 4" />
<line x1="10" y1="14" x2="3" y2="21" />
<line x1="21" y1="3" x2="14" y2="10" />
</>
) : (
<>
<polyline points="15 3 21 3 21 9" />
<polyline points="9 21 3 21 3 15" />
<line x1="21" y1="3" x2="14" y2="10" />
<line x1="3" y1="21" x2="10" y2="14" />
</>
)}
</svg>
</div>
<div className="header-btn" onClick={openSettings} title="Настройки">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#aaa" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="3" />

View File

@@ -129,6 +129,32 @@ const StarIcon = () => (
</svg>
)
const MovieCard: React.FC<{ movie: TmdbMovie; idx: number; baseIdx: number; onSelect: (m: TmdbMovie) => void }> = ({ movie, idx, baseIdx, onSelect }) => (
<div
className="movie-result-card"
style={{ animationDelay: `${Math.max(0, idx - baseIdx) * 30}ms` }}
onClick={() => onSelect(movie)}
>
<div className="movie-result-poster">
{movie.poster
? <img src={movie.poster} alt={movie.title} onError={e => { (e.target as HTMLImageElement).style.display = 'none'; (e.target as HTMLImageElement).nextElementSibling?.removeAttribute('style') }} />
: null
}
<div className="movie-result-poster-placeholder" style={movie.poster ? { display: 'none' } : undefined}>
{movie.title.charAt(0).toUpperCase()}
</div>
</div>
<div className="movie-result-info">
<span className="movie-result-title">{movie.title}</span>
<div className="ms-card-meta">
{movie.year && <span className="movie-result-year">{movie.year}</span>}
{movie.rating && <span className="ms-card-rating"><StarIcon /> {movie.rating}</span>}
</div>
{movie.mediaType === 'tv' && <span className="ms-card-type">Сериал</span>}
</div>
</div>
)
const MovieSearch: React.FC<MovieSearchProps> = ({ onOpenUrl, onBookmark, initialQuery = '' }) => {
const [query, setQuery] = useState(initialQuery)
const [mediaType, setMediaType] = useState<'movie' | 'tv'>('movie')
@@ -147,6 +173,7 @@ const MovieSearch: React.FC<MovieSearchProps> = ({ onOpenUrl, onBookmark, initia
const [tmdbLoading, setTmdbLoading] = useState(false)
const [sitesLoading, setSitesLoading] = useState(false)
const [loadingMore, setLoadingMore] = useState(false)
const [cardBase, setCardBase] = useState(0)
const [message, setMessage] = useState('')
const [apiKey, setApiKey] = useState('')
const [sites, setSites] = useState<MovieSite[]>([])
@@ -295,6 +322,7 @@ const MovieSearch: React.FC<MovieSearchProps> = ({ onOpenUrl, onBookmark, initia
const handleLoadMore = () => {
const nextPage = page + 1
setPage(nextPage)
setCardBase(tmdbResults.length)
doDiscover(apiKey, nextPage, true)
}
@@ -313,31 +341,6 @@ const MovieSearch: React.FC<MovieSearchProps> = ({ onOpenUrl, onBookmark, initia
const loading = tmdbLoading || sitesLoading
const MovieCard = ({ movie, idx }: { movie: TmdbMovie; idx: number }) => (
<div
className="movie-result-card"
style={{ animationDelay: `${idx * 30}ms` }}
onClick={() => handleSelectMovie(movie)}
>
<div className="movie-result-poster">
{movie.poster
? <img src={movie.poster} alt={movie.title} onError={e => { (e.target as HTMLImageElement).style.display = 'none'; (e.target as HTMLImageElement).nextElementSibling?.removeAttribute('style') }} />
: null
}
<div className="movie-result-poster-placeholder" style={movie.poster ? { display: 'none' } : undefined}>
{movie.title.charAt(0).toUpperCase()}
</div>
</div>
<div className="movie-result-info">
<span className="movie-result-title">{movie.title}</span>
<div className="ms-card-meta">
{movie.year && <span className="movie-result-year">{movie.year}</span>}
{movie.rating && <span className="ms-card-rating"><StarIcon /> {movie.rating}</span>}
</div>
{movie.mediaType === 'tv' && <span className="ms-card-type">Сериал</span>}
</div>
</div>
)
return (
<div className="movie-search">
@@ -495,7 +498,7 @@ const MovieSearch: React.FC<MovieSearchProps> = ({ onOpenUrl, onBookmark, initia
{tmdbResults.length > 0 && (
<>
<div className="movie-results">
{tmdbResults.map((movie, i) => <MovieCard key={`${movie.id}-${i}`} movie={movie} idx={i} />)}
{tmdbResults.map((movie, i) => <MovieCard key={movie.id} movie={movie} idx={i} baseIdx={cardBase} onSelect={handleSelectMovie} />)}
</div>
{!isSearchMode && page < totalPages && (
<button className="ms-load-more" onClick={handleLoadMore} disabled={loadingMore}>

View File

@@ -0,0 +1,40 @@
import React, { useEffect, useState } from 'react'
import ReactDOM from 'react-dom/client'
import '../styles/dialogs.css'
declare global {
interface Window {
electron?: { handleAction: (action: string) => void }
__dialogData?: { text?: string }
}
}
const ConfirmDialog = () => {
const [visible, setVisible] = useState(false)
const params = new URLSearchParams(window.location.search)
const text = params.get('text') || window.__dialogData?.text || ''
useEffect(() => {
requestAnimationFrame(() => requestAnimationFrame(() => setVisible(true)))
}, [])
useEffect(() => {
if (visible) document.body.classList.add('visible')
}, [visible])
return (
<div className="card">
{text && <div className="msg">{text}</div>}
<div className="btns">
<button className="btn-yes" onClick={() => window.electron?.handleAction('confirmYes')}>
Да
</button>
<button className="btn-no" onClick={() => window.electron?.handleAction('confirmNo')}>
Нет
</button>
</div>
</div>
)
}
ReactDOM.createRoot(document.getElementById('root')!).render(<ConfirmDialog />)

View File

@@ -0,0 +1,39 @@
import React, { useEffect, useState } from 'react'
import ReactDOM from 'react-dom/client'
import '../styles/dialogs.css'
declare global {
interface Window {
electron?: { handleAction: (action: string) => void }
__dialogData?: { title?: string; text?: string }
}
}
const ErrorDialog = () => {
const [visible, setVisible] = useState(false)
const params = new URLSearchParams(window.location.search)
const title = params.get('title') || window.__dialogData?.title || 'Ошибка'
const text = params.get('text') || window.__dialogData?.text || ''
useEffect(() => {
requestAnimationFrame(() => requestAnimationFrame(() => setVisible(true)))
}, [])
useEffect(() => {
if (visible) document.body.classList.add('visible')
}, [visible])
return (
<div className="card">
{title && <div className="title">{title}</div>}
{text && <div className="msg">{text}</div>}
<div className="btns">
<button className="btn-ok" onClick={() => window.electron?.handleAction('error')}>
Закрыть
</button>
</div>
</div>
)
}
ReactDOM.createRoot(document.getElementById('root')!).render(<ErrorDialog />)

40
src/entries/loader.tsx Normal file
View File

@@ -0,0 +1,40 @@
import React, { useEffect } from 'react'
import ReactDOM from 'react-dom/client'
const css = `
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #111;
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
transition: opacity 0.25s ease;
opacity: 0;
}
body.visible { opacity: 1; }
.spinner {
width: 36px;
height: 36px;
border: 3px solid rgba(255,255,255,0.1);
border-top-color: #E50914;
border-radius: 50%;
animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
`
const Loader = () => {
useEffect(() => {
const style = document.createElement('style')
style.textContent = css
document.head.appendChild(style)
requestAnimationFrame(() => requestAnimationFrame(() => {
document.body.classList.add('visible')
}))
}, [])
return <div className="spinner" />
}
ReactDOM.createRoot(document.getElementById('root')!).render(<Loader />)

99
src/entries/sidebar.tsx Normal file
View File

@@ -0,0 +1,99 @@
import React, { useState, useEffect, useRef } from 'react'
import ReactDOM from 'react-dom/client'
import '../styles/main.css'
interface OpenedApp {
name: string
imageUrl: string
url: string
}
declare global {
interface Window {
electron?: {
on: (channel: string, fn: (...args: any[]) => void) => () => void
hideView: () => void
showView: (name: string) => void
adjustView: (expanded: boolean) => void
getSidebarState: () => Promise<{ openedApps: OpenedApp[]; activeApp: string }>
}
}
}
const SidebarApp = () => {
const [openedApps, setOpenedApps] = useState<OpenedApp[]>([])
const [activeApp, setActiveApp] = useState('home')
const [expanded, setExpanded] = useState(false)
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
useEffect(() => {
window.electron?.getSidebarState().then(data => {
if (!data) return
setOpenedApps(data.openedApps || [])
setActiveApp(data.activeApp || 'home')
})
const off = window.electron?.on('sidebar-update', (data: { openedApps: OpenedApp[]; activeApp: string }) => {
setOpenedApps(data.openedApps || [])
setActiveApp(data.activeApp || 'home')
})
return () => off?.()
}, [])
const handleMouseEnter = () => {
if (timeoutRef.current) clearTimeout(timeoutRef.current)
timeoutRef.current = setTimeout(() => {
setExpanded(true)
window.electron?.adjustView(true)
}, 150)
}
const handleMouseLeave = () => {
if (timeoutRef.current) clearTimeout(timeoutRef.current)
timeoutRef.current = setTimeout(() => {
setExpanded(false)
window.electron?.adjustView(false)
}, 150)
}
const goHome = () => {
window.electron?.hideView()
}
return (
<div
className={`sidebar ${expanded ? 'expanded' : 'collapsed'}`}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<div
className={`sidebar-item ${activeApp === 'home' ? 'active' : ''}`}
onClick={goHome}
>
<svg className="sidebar-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
<polyline points="9 22 9 12 15 12 15 22" />
</svg>
<span>Домой</span>
</div>
{openedApps.map(app => (
<div
key={app.name}
className={`sidebar-item ${activeApp === app.name ? 'active' : ''}`}
onClick={() => window.electron?.showView(app.name)}
>
{app.imageUrl ? (
<img src={app.imageUrl} alt={app.name} className="sidebar-app-icon" />
) : (
<div className="sidebar-app-icon sidebar-app-icon-placeholder">
{app.name.charAt(0).toUpperCase()}
</div>
)}
<span>{app.name}</span>
</div>
))}
</div>
)
}
ReactDOM.createRoot(document.getElementById('root')!).render(<SidebarApp />)

View File

@@ -25,6 +25,8 @@ declare global {
searchMovies: (query: string, sites: any[]) => Promise<any[]>
searchTmdb: (query: string, apiKey: string) => Promise<{ results: any[]; error?: string }>
discoverTmdb: (params: any) => Promise<{ results: any[]; totalPages: number; error?: string }>
toggleKiosk: () => void
isKiosk: () => Promise<boolean>
}
}
}

52
src/styles/dialogs.css Normal file
View File

@@ -0,0 +1,52 @@
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: rgba(0, 0, 0, 0);
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
transition: background 0.22s ease;
}
body.visible { background: rgba(0, 0, 0, 0.78); }
body.hiding { background: rgba(0, 0, 0, 0); }
.card {
background: #1c1c1c;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 32px 36px;
text-align: center;
min-width: 300px;
max-width: 420px;
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.8);
opacity: 0;
transform: scale(0.92) translateY(10px);
transition: opacity 0.22s ease, transform 0.22s cubic-bezier(0.34, 1.56, 0.64, 1);
}
body.visible .card { opacity: 1; transform: scale(1) translateY(0); }
body.hiding .card { opacity: 0; transform: scale(0.95) translateY(6px); }
.title { font-size: 17px; font-weight: 700; color: #fff; margin-bottom: 10px; }
.msg { font-size: 13px; color: #999; line-height: 1.5; margin-bottom: 26px; }
.btns { display: flex; gap: 10px; justify-content: center; }
button {
padding: 10px 26px;
border: none;
border-radius: 7px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: opacity 0.15s, transform 0.1s;
}
button:hover { opacity: 0.85; }
button:active { transform: scale(0.97); }
.btn-yes { background: #E50914; color: #fff; }
.btn-no,
.btn-ok { background: rgba(255, 255, 255, 0.1); color: #ccc; }

View File

@@ -1,19 +1,29 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { resolve } from 'path'
export default defineConfig({
plugins: [react()],
base: './',
build: {
outDir: 'dist',
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html'),
loader: resolve(__dirname, 'loader.html'),
'dialog-error': resolve(__dirname, 'dialog-error.html'),
'dialog-confirm': resolve(__dirname, 'dialog-confirm.html'),
sidebar: resolve(__dirname, 'sidebar.html'),
},
},
},
server: {
port: 5173,
watch: {
ignored: ['**/extensions/**', '**/node_modules/**'],
ignored: ['**/extensions/**', '**/node_modules/**', '**/release/**', '**/dist/**'],
},
},
optimizeDeps: {
entries: ['index.html', 'src/**/*.tsx', 'src/**/*.ts'],
entries: ['index.html', 'loader.html', 'dialog-error.html', 'dialog-confirm.html', 'src/**/*.tsx', 'src/**/*.ts'],
},
})