fix: kiosk toggle sync, load more animation, remove nsis target
This commit is contained in:
52
README.md
52
README.md
@@ -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
|
||||
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
12
dialog-confirm.html
Normal 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
12
dialog-error.html
Normal 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
12
loader.html
Normal 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
107
main.js
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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'),
|
||||
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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*/
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
@@ -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": ""
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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.'));
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -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;
|
||||
@@ -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 */
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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*/
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
@@ -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": ""
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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.'));
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -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;
|
||||
@@ -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 */
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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*/
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
@@ -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": ""
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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.'));
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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 */
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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
12
sidebar.html
Normal 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>
|
||||
@@ -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" />
|
||||
|
||||
@@ -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}>
|
||||
|
||||
40
src/entries/dialog-confirm.tsx
Normal file
40
src/entries/dialog-confirm.tsx
Normal 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 />)
|
||||
39
src/entries/dialog-error.tsx
Normal file
39
src/entries/dialog-error.tsx
Normal 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
40
src/entries/loader.tsx
Normal 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
99
src/entries/sidebar.tsx
Normal 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 />)
|
||||
@@ -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
52
src/styles/dialogs.css
Normal 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; }
|
||||
@@ -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'],
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user