init
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
dist-electron/
|
||||||
|
release/
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"DockerRun.DisableDockerrc": true
|
||||||
|
}
|
||||||
49
README.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# ESH-Media
|
||||||
|
|
||||||
|
Десктопное приложение на Electron + React. Запускает веб-сервисы в отдельных WebContentsView, поиск и обзор фильмов через TMDB, встроенная блокировка рекламы.
|
||||||
|
|
||||||
|
## Стек
|
||||||
|
|
||||||
|
- Electron 32
|
||||||
|
- React 18 + TypeScript
|
||||||
|
- Vite
|
||||||
|
- @cliqz/adblocker-electron
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Сборка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Windows (zip)
|
||||||
|
npm run build:win
|
||||||
|
|
||||||
|
# Linux (AppImage + deb)
|
||||||
|
npm run build:linux
|
||||||
|
```
|
||||||
|
|
||||||
|
Артефакты в папке `release/`.
|
||||||
|
|
||||||
|
> Linux-сборку нужно запускать на Linux-машине.
|
||||||
|
|
||||||
|
## Настройка
|
||||||
|
|
||||||
|
В настройках приложения (шестерёнка):
|
||||||
|
|
||||||
|
- **Список приложений** — сайты, которые отображаются на главном экране
|
||||||
|
- **TMDB API Key** — для поиска и обзора фильмов, получить на [themoviedb.org](https://www.themoviedb.org/settings/api)
|
||||||
|
|
||||||
|
## Структура
|
||||||
|
|
||||||
|
```
|
||||||
|
main.js — main process
|
||||||
|
preload.js — preload / IPC bridge
|
||||||
|
src/
|
||||||
|
components/ — React components
|
||||||
|
pages/ — pages
|
||||||
|
assets/ — styles
|
||||||
|
```
|
||||||
12
index.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>ESH-Media</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
904
main.js
Normal file
@@ -0,0 +1,904 @@
|
|||||||
|
const { app, BrowserWindow, WebContentsView, ipcMain, session } = require('electron');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const os = require('os');
|
||||||
|
const cheerio = require('cheerio');
|
||||||
|
const { ElectronBlocker, adsAndTrackingLists } = require('@cliqz/adblocker-electron');
|
||||||
|
|
||||||
|
const CONFIG_PATH = path.join(os.homedir(), '.ESH-Media.json');
|
||||||
|
const BLOCKER_CACHE_PATH = path.join(os.homedir(), '.ESH-Media-adblock-v2.bin');
|
||||||
|
const DEFAULT_CONFIG = { apps: [], proxy: { host: '127.0.0.1', port: '7890' } };
|
||||||
|
|
||||||
|
let blockerPromise = null;
|
||||||
|
|
||||||
|
function getBlocker() {
|
||||||
|
if (blockerPromise) return blockerPromise;
|
||||||
|
blockerPromise = (async () => {
|
||||||
|
// Load from cache first (avoids re-downloading on every startup)
|
||||||
|
if (fs.existsSync(BLOCKER_CACHE_PATH)) {
|
||||||
|
try {
|
||||||
|
const data = fs.readFileSync(BLOCKER_CACHE_PATH);
|
||||||
|
const b = ElectronBlocker.deserialize(new Uint8Array(data));
|
||||||
|
console.log('[adblock] loaded from cache');
|
||||||
|
return b;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[adblock] cache invalid, re-downloading:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Download filter lists (EasyList + EasyPrivacy + uBlock Origin + Russian ad networks)
|
||||||
|
console.log('[adblock] downloading filter lists...');
|
||||||
|
const fetchFn = (url, opts) => getProxySession().fetch(url, opts);
|
||||||
|
const russianLists = [
|
||||||
|
'https://filters.adtidy.org/extension/ublock/filters/1.txt', // AdGuard Russian
|
||||||
|
'https://easylist-downloads.adblockplus.org/ruadlist+easylist.txt', // RuAdList
|
||||||
|
];
|
||||||
|
const b = await ElectronBlocker.fromLists(fetchFn, [...adsAndTrackingLists, ...russianLists]);
|
||||||
|
// Whitelist TMDB so the movie search API is not blocked
|
||||||
|
b.addFilters(['@@||api.themoviedb.org^', '@@||image.tmdb.org^', '@@||themoviedb.org^']);
|
||||||
|
fs.writeFileSync(BLOCKER_CACHE_PATH, Buffer.from(b.serialize()));
|
||||||
|
console.log('[adblock] filter lists downloaded and cached');
|
||||||
|
return b;
|
||||||
|
})();
|
||||||
|
return blockerPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableBlockingInSession(sess) {
|
||||||
|
getBlocker()
|
||||||
|
.then(b => { b.enableBlockingInSession(sess); console.log('[adblock] enabled for session'); })
|
||||||
|
.catch(e => console.warn('[adblock] failed to enable:', e.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDev = !app.isPackaged;
|
||||||
|
const RENDERER_URL = 'http://localhost:5173';
|
||||||
|
const PRELOAD_PATH = path.join(__dirname, 'preload.js');
|
||||||
|
const EXTENSIONS_PATH = path.join(__dirname, 'extensions');
|
||||||
|
|
||||||
|
const HEADER_H = 50;
|
||||||
|
const SIDEBAR_COLLAPSED_W = 75;
|
||||||
|
|
||||||
|
|
||||||
|
let mainWindow = null;
|
||||||
|
let currentView = null;
|
||||||
|
let loaderView = null;
|
||||||
|
let openedApps = [];
|
||||||
|
const errorViews = [];
|
||||||
|
const confirmViews = [];
|
||||||
|
let proxySession = null;
|
||||||
|
let directSession = null;
|
||||||
|
let pendingNavigate = null; // { view, url } — cross-domain redirect awaiting confirmation
|
||||||
|
|
||||||
|
// --- Sessions ---
|
||||||
|
|
||||||
|
function getProxySession() {
|
||||||
|
if (!proxySession) {
|
||||||
|
proxySession = session.fromPartition('persist:proxy');
|
||||||
|
enableBlockingInSession(proxySession);
|
||||||
|
}
|
||||||
|
return proxySession;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDirectSession() {
|
||||||
|
if (!directSession) {
|
||||||
|
directSession = session.fromPartition('persist:direct');
|
||||||
|
directSession.setProxy({ proxyRules: 'direct://' });
|
||||||
|
enableBlockingInSession(directSession);
|
||||||
|
}
|
||||||
|
return directSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyProxy(host, port) {
|
||||||
|
const proxyRules = `http=${host}:${port};https=${host}:${port};socks=socks5://${host}:${port}`;
|
||||||
|
await getProxySession().setProxy({ proxyRules });
|
||||||
|
await session.defaultSession.setProxy({ proxyRules });
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Extensions ---
|
||||||
|
|
||||||
|
async function loadExtensions() {
|
||||||
|
if (!fs.existsSync(EXTENSIONS_PATH)) return;
|
||||||
|
const entries = fs.readdirSync(EXTENSIONS_PATH, { withFileTypes: true });
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry.isDirectory()) continue;
|
||||||
|
const extPath = path.join(EXTENSIONS_PATH, entry.name);
|
||||||
|
// Fix Windows-style backslash paths in declarative_net_request manifest entries
|
||||||
|
const manifestPath = path.join(extPath, 'manifest.json');
|
||||||
|
if (fs.existsSync(manifestPath)) {
|
||||||
|
try {
|
||||||
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||||||
|
let changed = false;
|
||||||
|
// Remove rule_resources entries whose files don't exist on disk
|
||||||
|
if (manifest.declarative_net_request?.rule_resources) {
|
||||||
|
const valid = manifest.declarative_net_request.rule_resources.filter(r => {
|
||||||
|
const rPath = r.path.replace(/^\//, '').split('/').join(path.sep);
|
||||||
|
return fs.existsSync(path.join(extPath, rPath));
|
||||||
|
});
|
||||||
|
if (valid.length !== manifest.declarative_net_request.rule_resources.length) {
|
||||||
|
manifest.declarative_net_request.rule_resources = valid;
|
||||||
|
changed = true;
|
||||||
|
console.log(`Removed ${manifest.declarative_net_request.rule_resources.length === 0 ? 'all' : 'missing'} DNR rule_resources for: ${entry.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Remove service_worker — Electron doesn't support MV3 service workers for extensions
|
||||||
|
if (manifest.background?.service_worker) {
|
||||||
|
delete manifest.background.service_worker;
|
||||||
|
if (!Object.keys(manifest.background).length) delete manifest.background;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
// Remove permissions unsupported by Electron to suppress warnings
|
||||||
|
const UNSUPPORTED_PERMS = new Set(['contextMenus', 'notifications', 'webNavigation', 'management']);
|
||||||
|
if (manifest.permissions) {
|
||||||
|
const filtered = manifest.permissions.filter(p => !UNSUPPORTED_PERMS.has(p));
|
||||||
|
if (filtered.length !== manifest.permissions.length) {
|
||||||
|
manifest.permissions = filtered;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changed) fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf8');
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to patch manifest for', entry.name, e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Load into all sessions so content scripts run in WebContentsViews too
|
||||||
|
const sessionsToLoad = [
|
||||||
|
session.defaultSession,
|
||||||
|
session.fromPartition('persist:proxy'),
|
||||||
|
session.fromPartition('persist:direct'),
|
||||||
|
];
|
||||||
|
for (const sess of sessionsToLoad) {
|
||||||
|
try {
|
||||||
|
await sess.loadExtension(extPath, { allowFileAccess: true });
|
||||||
|
} catch (e) {
|
||||||
|
// only log once (defaultSession gives the meaningful error)
|
||||||
|
if (sess === session.defaultSession)
|
||||||
|
console.warn('Failed to load extension', entry.name, e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('Loaded extension:', entry.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Window ---
|
||||||
|
|
||||||
|
async function createWindow() {
|
||||||
|
mainWindow = new BrowserWindow({
|
||||||
|
width: 1280,
|
||||||
|
height: 800,
|
||||||
|
webPreferences: {
|
||||||
|
preload: PRELOAD_PATH,
|
||||||
|
contextIsolation: true,
|
||||||
|
nodeIntegration: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isDev) {
|
||||||
|
mainWindow.loadURL(RENDERER_URL);
|
||||||
|
} else {
|
||||||
|
mainWindow.loadFile(path.join(__dirname, 'dist', 'index.html'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- View helpers ---
|
||||||
|
|
||||||
|
function getViewBounds(sidebarWidth) {
|
||||||
|
const w = sidebarWidth !== undefined ? sidebarWidth : SIDEBAR_COLLAPSED_W;
|
||||||
|
const { width, height } = mainWindow.getBounds();
|
||||||
|
return { x: w, y: HEADER_H, width: width - w, height: height - HEADER_H };
|
||||||
|
}
|
||||||
|
|
||||||
|
function addChild(view) {
|
||||||
|
mainWindow.contentView.addChildView(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeChild(view) {
|
||||||
|
try { mainWindow.contentView.removeChildView(view); } catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bringOverlaysToTop() {
|
||||||
|
confirmViews.forEach(c => { removeChild(c.view); addChild(c.view); });
|
||||||
|
errorViews.forEach(v => { removeChild(v); addChild(v); });
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendOpenedApps(activeName) {
|
||||||
|
mainWindow.webContents.send(
|
||||||
|
'update-opened-apps',
|
||||||
|
openedApps.map(a => ({ name: a.name, url: a.url, imageUrl: a.imageUrl })),
|
||||||
|
activeName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Loader ---
|
||||||
|
|
||||||
|
function setLoader() {
|
||||||
|
if (loaderView) return;
|
||||||
|
const { width, height } = mainWindow.getBounds();
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeLoader() {
|
||||||
|
if (!loaderView) return;
|
||||||
|
const lv = loaderView;
|
||||||
|
loaderView = null;
|
||||||
|
lv.webContents.executeJavaScript(`document.body.style.opacity='0'`).catch(() => {});
|
||||||
|
setTimeout(() => { try { removeChild(lv); lv.webContents.destroy(); } catch (_) {} }, 260);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 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')`
|
||||||
|
).catch(() => {});
|
||||||
|
setTimeout(cb, 230);
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeDialogView() {
|
||||||
|
const { width, height } = mainWindow.getBounds();
|
||||||
|
const view = new WebContentsView({
|
||||||
|
webPreferences: { contextIsolation: true, nodeIntegration: false, preload: PRELOAD_PATH },
|
||||||
|
});
|
||||||
|
view.setBackgroundColor('#00000000');
|
||||||
|
view.setBounds({ x: 0, y: 0, width, height });
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeError() {
|
||||||
|
if (!errorViews.length) return;
|
||||||
|
const view = errorViews.pop();
|
||||||
|
dialogFadeOut(view, () => { try { removeChild(view); view.webContents.destroy(); } catch (_) {} });
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeConfirm() {
|
||||||
|
if (!confirmViews.length) return;
|
||||||
|
const { view } = confirmViews.pop();
|
||||||
|
dialogFadeOut(view, () => { try { removeChild(view); view.webContents.destroy(); } catch (_) {} });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function removeView(name) {
|
||||||
|
const idx = openedApps.findIndex(a => a.name === name);
|
||||||
|
if (idx === -1) return;
|
||||||
|
const [app] = openedApps.splice(idx, 1);
|
||||||
|
removeChild(app.view);
|
||||||
|
app.view.webContents.destroy();
|
||||||
|
if (currentView && currentView.name === name) currentView = null;
|
||||||
|
sendOpenedApps('home');
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- IPC ---
|
||||||
|
|
||||||
|
ipcMain.on('create-view', async (_event, name, url, imageUrl, _zoom, useProxy) => {
|
||||||
|
if (!url || !name) return;
|
||||||
|
|
||||||
|
const existing = openedApps.find(a => a.name === name);
|
||||||
|
if (existing) {
|
||||||
|
if (currentView && currentView.view) removeChild(currentView.view);
|
||||||
|
addChild(existing.view);
|
||||||
|
bringOverlaysToTop();
|
||||||
|
currentView = existing;
|
||||||
|
currentView.view.setBounds(getViewBounds());
|
||||||
|
sendOpenedApps(name);
|
||||||
|
mainWindow.webContents.send('updateWebButtons', { history: existing.history, historyPosition: existing.historyPosition });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentView && currentView.view) removeChild(currentView.view);
|
||||||
|
setLoader();
|
||||||
|
|
||||||
|
const view = new WebContentsView({
|
||||||
|
webPreferences: {
|
||||||
|
contextIsolation: true,
|
||||||
|
nodeIntegration: false,
|
||||||
|
session: useProxy ? getProxySession() : getDirectSession(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const appEntry = { name, url, imageUrl, view, history: [url], historyPosition: 0 };
|
||||||
|
openedApps.push(appEntry);
|
||||||
|
currentView = appEntry;
|
||||||
|
view.setBounds(getViewBounds());
|
||||||
|
|
||||||
|
view.webContents.on('did-finish-load', () => {
|
||||||
|
removeLoader();
|
||||||
|
addChild(view);
|
||||||
|
bringOverlaysToTop();
|
||||||
|
// Inject fade-in overlay so the page appears smoothly instead of blinking
|
||||||
|
view.webContents.executeJavaScript(`
|
||||||
|
(function(){
|
||||||
|
if(document.__nfFade)return; document.__nfFade=true;
|
||||||
|
const o=document.createElement('div');
|
||||||
|
o.style.cssText='position:fixed;inset:0;background:#111;z-index:2147483647;pointer-events:none;transition:opacity 0.35s ease;';
|
||||||
|
document.documentElement.appendChild(o);
|
||||||
|
requestAnimationFrame(()=>requestAnimationFrame(()=>{
|
||||||
|
o.style.opacity='0';
|
||||||
|
setTimeout(()=>o.remove(),370);
|
||||||
|
}));
|
||||||
|
})()
|
||||||
|
`).catch(()=>{});
|
||||||
|
});
|
||||||
|
|
||||||
|
const trackNavigation = (navigatingUrl) => {
|
||||||
|
const app = openedApps.find(a => a.name === name);
|
||||||
|
if (!app) return;
|
||||||
|
if (navigatingUrl === app.history[app.historyPosition]) {
|
||||||
|
mainWindow.webContents.send('updateWebButtons', { history: app.history, historyPosition: app.historyPosition });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (app.historyPosition < app.history.length - 1) {
|
||||||
|
app.history = app.history.slice(0, app.historyPosition + 1);
|
||||||
|
}
|
||||||
|
app.history.push(navigatingUrl);
|
||||||
|
app.historyPosition = app.history.length - 1;
|
||||||
|
mainWindow.webContents.send('updateWebButtons', { history: app.history, historyPosition: app.historyPosition });
|
||||||
|
};
|
||||||
|
|
||||||
|
let origHostname = '';
|
||||||
|
try { origHostname = new URL(url).hostname; } catch (_) {}
|
||||||
|
|
||||||
|
view.webContents.on('will-navigate', (e, newUrl) => {
|
||||||
|
if (newUrl.startsWith('data:')) { trackNavigation(newUrl); return; }
|
||||||
|
let newHostname = '';
|
||||||
|
try { newHostname = new URL(newUrl).hostname; } catch (_) { trackNavigation(newUrl); return; }
|
||||||
|
if (origHostname && newHostname && newHostname !== origHostname) {
|
||||||
|
e.preventDefault();
|
||||||
|
pendingNavigate = { view, url: newUrl };
|
||||||
|
setConfirm(`Перейти на "${newHostname}"?`, 'navigate-confirmed');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
trackNavigation(newUrl);
|
||||||
|
});
|
||||||
|
view.webContents.on('will-redirect', (_e, u) => trackNavigation(u));
|
||||||
|
view.webContents.setWindowOpenHandler(({ url: newUrl }) => {
|
||||||
|
let newHostname = '';
|
||||||
|
try { newHostname = new URL(newUrl).hostname; } catch (_) {}
|
||||||
|
if (origHostname && newHostname && newHostname !== origHostname) {
|
||||||
|
pendingNavigate = { view, url: newUrl };
|
||||||
|
setConfirm(`Перейти на "${newHostname}"?`, 'navigate-confirmed');
|
||||||
|
return { action: 'deny' };
|
||||||
|
}
|
||||||
|
trackNavigation(newUrl);
|
||||||
|
view.webContents.loadURL(newUrl);
|
||||||
|
return { action: 'deny' };
|
||||||
|
});
|
||||||
|
|
||||||
|
sendOpenedApps(name);
|
||||||
|
mainWindow.webContents.send('updateWebButtons', { history: appEntry.history, historyPosition: appEntry.historyPosition });
|
||||||
|
|
||||||
|
view.webContents.loadURL(url).catch(() => {
|
||||||
|
removeView(name);
|
||||||
|
removeLoader();
|
||||||
|
setError('Ошибка', `Не удалось загрузить: ${name}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('remove-view', (_event, name) => removeView(name || (currentView && currentView.name)));
|
||||||
|
|
||||||
|
ipcMain.on('hide-view', () => {
|
||||||
|
if (currentView && currentView.view) removeChild(currentView.view);
|
||||||
|
currentView = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('show-view', (_event, name) => {
|
||||||
|
const app = openedApps.find(a => a.name === name);
|
||||||
|
if (!app) return;
|
||||||
|
if (currentView && currentView.view) removeChild(currentView.view);
|
||||||
|
currentView = app;
|
||||||
|
addChild(app.view);
|
||||||
|
bringOverlaysToTop();
|
||||||
|
mainWindow.webContents.send('updateWebButtons', { history: app.history, historyPosition: app.historyPosition });
|
||||||
|
});
|
||||||
|
|
||||||
|
let sidebarAnim = null;
|
||||||
|
const SIDEBAR_ANIM_MS = 250;
|
||||||
|
|
||||||
|
function animateSidebarResize(targetX) {
|
||||||
|
if (!currentView || !currentView.view) return;
|
||||||
|
if (sidebarAnim) { clearInterval(sidebarAnim); sidebarAnim = null; }
|
||||||
|
|
||||||
|
const fromX = currentView.view.getBounds().x;
|
||||||
|
if (fromX === targetX) return;
|
||||||
|
|
||||||
|
const startTime = Date.now();
|
||||||
|
// easeInOut
|
||||||
|
const ease = t => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
|
||||||
|
|
||||||
|
sidebarAnim = setInterval(() => {
|
||||||
|
const t = Math.min((Date.now() - startTime) / SIDEBAR_ANIM_MS, 1);
|
||||||
|
const x = Math.round(fromX + (targetX - fromX) * ease(t));
|
||||||
|
if (currentView && currentView.view) {
|
||||||
|
const { width, height } = mainWindow.getBounds();
|
||||||
|
currentView.view.setBounds({ x, y: HEADER_H, width: width - x, height: height - HEADER_H });
|
||||||
|
}
|
||||||
|
if (t >= 1) { clearInterval(sidebarAnim); sidebarAnim = null; }
|
||||||
|
}, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.on('adjust-view', (_event, expanded) => {
|
||||||
|
animateSidebarResize(expanded ? 200 : SIDEBAR_COLLAPSED_W);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('backwardPage', () => {
|
||||||
|
const app = openedApps.find(a => a.name === (currentView && currentView.name));
|
||||||
|
if (!app || app.historyPosition <= 0) return;
|
||||||
|
app.historyPosition--;
|
||||||
|
currentView.view.webContents.loadURL(app.history[app.historyPosition])
|
||||||
|
.catch(() => setError('Ошибка', 'Страница не найдена'));
|
||||||
|
mainWindow.webContents.send('updateWebButtons', { history: app.history, historyPosition: app.historyPosition });
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('forwardPage', () => {
|
||||||
|
const app = openedApps.find(a => a.name === (currentView && currentView.name));
|
||||||
|
if (!app || app.historyPosition >= app.history.length - 1) return;
|
||||||
|
app.historyPosition++;
|
||||||
|
currentView.view.webContents.loadURL(app.history[app.historyPosition])
|
||||||
|
.catch(() => setError('Ошибка', 'Страница не найдена'));
|
||||||
|
mainWindow.webContents.send('updateWebButtons', { history: app.history, historyPosition: app.historyPosition });
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('refreshPage', () => {
|
||||||
|
const app = openedApps.find(a => a.name === (currentView && currentView.name));
|
||||||
|
if (!app) return;
|
||||||
|
currentView.view.webContents.loadURL(app.history[app.historyPosition])
|
||||||
|
.catch(() => setError('Ошибка', 'Страница не найдена'));
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('collapseWithHeader', () => {
|
||||||
|
if (!currentView || !currentView.view) return;
|
||||||
|
const { width, height } = mainWindow.getBounds();
|
||||||
|
currentView.view.setBounds({ x: 0, y: 1, width, height: height - 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('expandWithHeader', () => {
|
||||||
|
if (!currentView || !currentView.view) return;
|
||||||
|
currentView.view.setBounds(getViewBounds());
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('set-proxy', async (_event, host, port) => applyProxy(host, port));
|
||||||
|
|
||||||
|
// --- Movie Search ---
|
||||||
|
|
||||||
|
const MOVIE_PARSERS = {
|
||||||
|
dle: {
|
||||||
|
buildUrl: (domain, query) =>
|
||||||
|
`https://${domain}/?do=search&subaction=search&story=${encodeURIComponent(query)}`,
|
||||||
|
parse: (html, domain) => {
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
const results = [];
|
||||||
|
const toAbs = (src) => {
|
||||||
|
if (!src) return '';
|
||||||
|
if (src.startsWith('http')) return src;
|
||||||
|
return src.startsWith('/') ? `https://${domain}${src}` : `https://${domain}/${src}`;
|
||||||
|
};
|
||||||
|
$('.short, .movie-item, .th-item, .card, article.item, .shortstory, article.shortStory, .shortStory').each((_, el) => {
|
||||||
|
const $el = $(el);
|
||||||
|
const $link = $el.find('h2 a, .th-title a, .title a, .short-title a, .card-title a, .name a, .hTitle a').first();
|
||||||
|
const title = $link.text().trim();
|
||||||
|
let href = $link.attr('href') || '';
|
||||||
|
if (href && !href.startsWith('http')) href = `https://${domain}${href}`;
|
||||||
|
const rawSrc = $el.find('img[data-src]').first().attr('data-src') || $el.find('img[src]').first().attr('src') || '';
|
||||||
|
const poster = rawSrc.startsWith('data:') ? '' : toAbs(rawSrc);
|
||||||
|
const year = $el.text().match(/\b(19|20)\d{2}\b/)?.[0];
|
||||||
|
if (title && href) results.push({ title, url: href, poster, year, source: domain });
|
||||||
|
});
|
||||||
|
return results;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hdrezka: {
|
||||||
|
buildUrl: (domain, query) =>
|
||||||
|
`https://${domain}/?do=search&subaction=search&story=${encodeURIComponent(query)}`,
|
||||||
|
parse: (html, domain) => {
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
const results = [];
|
||||||
|
const toAbs = (src) => {
|
||||||
|
if (!src) return '';
|
||||||
|
if (src.startsWith('http')) return src;
|
||||||
|
return src.startsWith('/') ? `https://${domain}${src}` : `https://${domain}/${src}`;
|
||||||
|
};
|
||||||
|
$('.b-content__inline_item').each((_, el) => {
|
||||||
|
const $el = $(el);
|
||||||
|
const $link = $el.find('.b-content__inline_item-link a').first();
|
||||||
|
const title = $link.text().trim();
|
||||||
|
const href = $link.attr('href') || '';
|
||||||
|
const poster = toAbs($el.find('img').first().attr('src') || '');
|
||||||
|
const year = $el.find('.b-content__inline_item-link div').text().match(/\b(19|20)\d{2}\b/)?.[0];
|
||||||
|
if (title && href) results.push({ title, url: href, poster, year, source: domain });
|
||||||
|
});
|
||||||
|
return results;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
filmix: {
|
||||||
|
buildUrl: (domain, query) =>
|
||||||
|
`https://${domain}/search/${encodeURIComponent(query)}/`,
|
||||||
|
parse: (html, domain) => {
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
const results = [];
|
||||||
|
const toAbs = (src) => {
|
||||||
|
if (!src) return '';
|
||||||
|
if (src.startsWith('http')) return src;
|
||||||
|
return src.startsWith('/') ? `https://${domain}${src}` : `https://${domain}/${src}`;
|
||||||
|
};
|
||||||
|
$('.post-item, .movie-item, .item').each((_, el) => {
|
||||||
|
const $el = $(el);
|
||||||
|
const $link = $el.find('a.title, h2 a, .name a').first();
|
||||||
|
const title = $link.text().trim();
|
||||||
|
let href = $link.attr('href') || '';
|
||||||
|
if (href && !href.startsWith('http')) href = `https://${domain}${href}`;
|
||||||
|
const poster = toAbs($el.find('img').first().attr('src') || '');
|
||||||
|
const year = $el.text().match(/\b(19|20)\d{2}\b/)?.[0];
|
||||||
|
if (title && href) results.push({ title, url: href, poster, year, source: domain });
|
||||||
|
});
|
||||||
|
return results;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const SEARCH_HEADERS = {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||||
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||||
|
'Accept-Language': 'ru-RU,ru;q=0.9,en;q=0.8',
|
||||||
|
};
|
||||||
|
|
||||||
|
async function searchHdrezkaAjax(site, query) {
|
||||||
|
try {
|
||||||
|
const url = `https://${site.domain}/engine/ajax/search.php`;
|
||||||
|
console.log(`[search] ${site.domain} -> AJAX`);
|
||||||
|
const resp = await getProxySession().fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
body: `q=${encodeURIComponent(query)}`,
|
||||||
|
headers: {
|
||||||
|
...SEARCH_HEADERS,
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'Referer': `https://${site.domain}/`,
|
||||||
|
},
|
||||||
|
signal: AbortSignal.timeout(10000),
|
||||||
|
});
|
||||||
|
if (!resp.ok) return [];
|
||||||
|
const html = await resp.text();
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
const results = [];
|
||||||
|
$('ul li a').each((_, el) => {
|
||||||
|
const $el = $(el);
|
||||||
|
const href = $el.attr('href') || '';
|
||||||
|
const title = $el.find('.enty').text().trim();
|
||||||
|
if (title && href) results.push({ title, url: href, poster: '', year: '', source: site.domain });
|
||||||
|
});
|
||||||
|
console.log(`[search] ${site.domain} AJAX found: ${results.length}`);
|
||||||
|
return results;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`[search] ${site.domain} AJAX error:`, e.message);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchWithView(site, query) {
|
||||||
|
const parser = MOVIE_PARSERS[site.type];
|
||||||
|
if (!parser) return Promise.resolve([]);
|
||||||
|
const url = parser.buildUrl(site.domain, query);
|
||||||
|
console.log(`[search] ${site.domain} -> ${url} (browser)`);
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const view = new WebContentsView({
|
||||||
|
webPreferences: { contextIsolation: true, nodeIntegration: false, session: getProxySession() },
|
||||||
|
});
|
||||||
|
|
||||||
|
let extractTimer = null;
|
||||||
|
const cleanup = (results) => {
|
||||||
|
if (extractTimer) clearTimeout(extractTimer);
|
||||||
|
try { view.webContents.destroy(); } catch (_) {}
|
||||||
|
resolve(results);
|
||||||
|
};
|
||||||
|
|
||||||
|
const globalTimer = setTimeout(() => {
|
||||||
|
console.warn(`[search] ${site.domain} timeout`);
|
||||||
|
cleanup([]);
|
||||||
|
}, 25000);
|
||||||
|
|
||||||
|
const tryExtract = () => {
|
||||||
|
if (extractTimer) clearTimeout(extractTimer);
|
||||||
|
extractTimer = setTimeout(() => {
|
||||||
|
clearTimeout(globalTimer);
|
||||||
|
// Poll every 400ms until results found or 5s elapsed (handles AJAX-loaded content)
|
||||||
|
const deadline = Date.now() + 5000;
|
||||||
|
const poll = async () => {
|
||||||
|
try {
|
||||||
|
const html = await view.webContents.executeJavaScript('document.documentElement.outerHTML');
|
||||||
|
const results = parser.parse(html, site.domain);
|
||||||
|
if (results.length > 0 || Date.now() >= deadline) {
|
||||||
|
console.log(`[search] ${site.domain} browser found: ${results.length}`);
|
||||||
|
cleanup(results);
|
||||||
|
} else {
|
||||||
|
setTimeout(poll, 400);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`[search] ${site.domain} extract error:`, e.message);
|
||||||
|
cleanup([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
poll();
|
||||||
|
}, 800); // initial wait for JS redirects / challenge pages
|
||||||
|
};
|
||||||
|
|
||||||
|
view.webContents.on('did-finish-load', tryExtract);
|
||||||
|
view.webContents.on('did-fail-load', (_e, code, desc) => {
|
||||||
|
clearTimeout(globalTimer);
|
||||||
|
console.warn(`[search] ${site.domain} load failed: ${code} ${desc}`);
|
||||||
|
cleanup([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
view.webContents.loadURL(url, { userAgent: SEARCH_HEADERS['User-Agent'] }).catch(e => {
|
||||||
|
clearTimeout(globalTimer);
|
||||||
|
console.warn(`[search] ${site.domain} loadURL error:`, e.message);
|
||||||
|
cleanup([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function searchOneSite(site, query) {
|
||||||
|
if (site.type === 'hdrezka') return searchHdrezkaAjax(site, query);
|
||||||
|
return searchWithView(site, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.handle('search-tmdb', async (_event, query, apiKey) => {
|
||||||
|
console.log(`[tmdb] searching: "${query}", key type: ${apiKey ? (apiKey.startsWith('eyJ') ? 'bearer' : 'api_key') : 'none'}`);
|
||||||
|
try {
|
||||||
|
const isBearer = apiKey.startsWith('eyJ');
|
||||||
|
const url = isBearer
|
||||||
|
? `https://api.themoviedb.org/3/search/multi?query=${encodeURIComponent(query)}&language=ru-RU&include_adult=false`
|
||||||
|
: `https://api.themoviedb.org/3/search/multi?api_key=${apiKey}&query=${encodeURIComponent(query)}&language=ru-RU&include_adult=false`;
|
||||||
|
const headers = isBearer
|
||||||
|
? { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }
|
||||||
|
: {};
|
||||||
|
const resp = await getProxySession().fetch(url, { headers, signal: AbortSignal.timeout(8000) });
|
||||||
|
console.log(`[tmdb] status: ${resp.status}`);
|
||||||
|
if (!resp.ok) {
|
||||||
|
const body = await resp.text().catch(() => '');
|
||||||
|
console.warn(`[tmdb] error body:`, body.slice(0, 200));
|
||||||
|
return { error: `TMDB error ${resp.status}`, results: [] };
|
||||||
|
}
|
||||||
|
const data = await resp.json();
|
||||||
|
const results = (data.results || [])
|
||||||
|
.filter(r => r.media_type === 'movie' || r.media_type === 'tv')
|
||||||
|
.slice(0, 20)
|
||||||
|
.map(r => ({
|
||||||
|
id: r.id,
|
||||||
|
mediaType: r.media_type,
|
||||||
|
title: r.title || r.name || '',
|
||||||
|
originalTitle: r.original_title || r.original_name || '',
|
||||||
|
year: (r.release_date || r.first_air_date || '').slice(0, 4),
|
||||||
|
poster: r.poster_path ? `https://image.tmdb.org/t/p/w300${r.poster_path}` : '',
|
||||||
|
overview: r.overview || '',
|
||||||
|
rating: r.vote_average ? r.vote_average.toFixed(1) : '',
|
||||||
|
}));
|
||||||
|
console.log(`[tmdb] results: ${results.length}`);
|
||||||
|
return { results };
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[tmdb] exception:`, e.message);
|
||||||
|
return { error: e.message, results: [] };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('discover-tmdb', async (_event, { apiKey, mediaType, sortBy, genreId, year, minRating, country, page }) => {
|
||||||
|
try {
|
||||||
|
const isBearer = apiKey.startsWith('eyJ');
|
||||||
|
const type = mediaType === 'tv' ? 'tv' : 'movie';
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
language: 'ru-RU',
|
||||||
|
sort_by: sortBy || 'popularity.desc',
|
||||||
|
page: String(page || 1),
|
||||||
|
include_adult: 'false',
|
||||||
|
});
|
||||||
|
if (!isBearer) params.set('api_key', apiKey);
|
||||||
|
if (genreId) params.set('with_genres', String(genreId));
|
||||||
|
if (year) {
|
||||||
|
if (type === 'movie') params.set('primary_release_year', year);
|
||||||
|
else params.set('first_air_date_year', year);
|
||||||
|
}
|
||||||
|
if (minRating) params.set('vote_average.gte', minRating);
|
||||||
|
if (country) {
|
||||||
|
const COUNTRY_LANG = {
|
||||||
|
RU: 'ru', JP: 'ja', KR: 'ko', CN: 'zh', FR: 'fr',
|
||||||
|
DE: 'de', IT: 'it', ES: 'es', SE: 'sv', DK: 'da', TR: 'tr', IN: 'hi',
|
||||||
|
};
|
||||||
|
const lang = COUNTRY_LANG[country];
|
||||||
|
if (lang) params.set('with_original_language', lang);
|
||||||
|
else params.set('with_origin_country', country);
|
||||||
|
}
|
||||||
|
const url = `https://api.themoviedb.org/3/discover/${type}?${params}`;
|
||||||
|
const headers = isBearer ? { 'Authorization': `Bearer ${apiKey}` } : {};
|
||||||
|
const resp = await getProxySession().fetch(url, { headers, signal: AbortSignal.timeout(8000) });
|
||||||
|
if (!resp.ok) return { error: `TMDB ${resp.status}`, results: [], totalPages: 1 };
|
||||||
|
const data = await resp.json();
|
||||||
|
const results = (data.results || []).map(r => ({
|
||||||
|
id: r.id,
|
||||||
|
mediaType: type,
|
||||||
|
title: (type === 'movie' ? r.title : r.name) || '',
|
||||||
|
originalTitle: (type === 'movie' ? r.original_title : r.original_name) || '',
|
||||||
|
year: ((type === 'movie' ? r.release_date : r.first_air_date) || '').slice(0, 4),
|
||||||
|
poster: r.poster_path ? `https://image.tmdb.org/t/p/w300${r.poster_path}` : '',
|
||||||
|
overview: r.overview || '',
|
||||||
|
rating: r.vote_average ? r.vote_average.toFixed(1) : '',
|
||||||
|
}));
|
||||||
|
return { results, totalPages: Math.min(data.total_pages || 1, 500) };
|
||||||
|
} catch (e) {
|
||||||
|
return { error: e.message, results: [], totalPages: 1 };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('search-movies', async (_event, query, sites) => {
|
||||||
|
const settled = await Promise.allSettled(sites.map(s => searchOneSite(s, query)));
|
||||||
|
return settled.flatMap(r => r.status === 'fulfilled' ? r.value : []);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('get-current-page', () => {
|
||||||
|
if (!currentView) return null;
|
||||||
|
return {
|
||||||
|
name: currentView.name,
|
||||||
|
url: currentView.history[currentView.historyPosition],
|
||||||
|
imageUrl: currentView.imageUrl || '',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('read-config', () => {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(CONFIG_PATH)) {
|
||||||
|
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to read config:', e.message);
|
||||||
|
}
|
||||||
|
return DEFAULT_CONFIG;
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('write-config', (_event, data) => {
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(CONFIG_PATH, JSON.stringify(data, null, 2), 'utf8');
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to write config:', e.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('confirm', (_event, text, actionOnYes) => setConfirm(text, actionOnYes));
|
||||||
|
|
||||||
|
ipcMain.on('action', (_event, action) => {
|
||||||
|
if (action === 'error') {
|
||||||
|
removeError();
|
||||||
|
} else if (action === 'confirmYes') {
|
||||||
|
const last = confirmViews[confirmViews.length - 1];
|
||||||
|
if (last) {
|
||||||
|
if (last.actionOnYes === 'navigate-confirmed' && pendingNavigate) {
|
||||||
|
const { view: pView, url: pUrl } = pendingNavigate;
|
||||||
|
pendingNavigate = null;
|
||||||
|
if (!pView.webContents.isDestroyed()) {
|
||||||
|
pView.webContents.loadURL(pUrl).catch(() => setError('Ошибка', `Не удалось загрузить: ${pUrl}`));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mainWindow.webContents.send(last.actionOnYes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
removeConfirm();
|
||||||
|
} else if (action === 'confirmNo') {
|
||||||
|
if (confirmViews.length && confirmViews[confirmViews.length - 1].actionOnYes === 'navigate-confirmed') {
|
||||||
|
pendingNavigate = null;
|
||||||
|
}
|
||||||
|
removeConfirm();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- App lifecycle ---
|
||||||
|
|
||||||
|
app.whenReady().then(async () => {
|
||||||
|
// Add Referer to image requests so hotlink protection doesn't block them
|
||||||
|
session.defaultSession.webRequest.onBeforeSendHeaders(
|
||||||
|
{ urls: ['https://*/*', 'http://*/*'] },
|
||||||
|
(details, callback) => {
|
||||||
|
const headers = details.requestHeaders;
|
||||||
|
if (details.resourceType === 'image' && !headers['Referer'] && !headers['referer']) {
|
||||||
|
try {
|
||||||
|
const u = new URL(details.url);
|
||||||
|
headers['Referer'] = `${u.protocol}//${u.hostname}/`;
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
callback({ requestHeaders: headers });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Apply proxy from config before blocker tries to download filter lists
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(CONFIG_PATH)) {
|
||||||
|
const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
||||||
|
if (cfg?.proxy?.host && cfg?.proxy?.port) await applyProxy(cfg.proxy.host, cfg.proxy.port);
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
enableBlockingInSession(session.defaultSession);
|
||||||
|
enableBlockingInSession(session.fromPartition('persist:proxy'));
|
||||||
|
enableBlockingInSession(session.fromPartition('persist:direct'));
|
||||||
|
await loadExtensions();
|
||||||
|
await createWindow();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
if (process.platform !== 'darwin') app.quit();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('activate', async () => {
|
||||||
|
if (BrowserWindow.getAllWindows().length === 0) await createWindow();
|
||||||
|
});
|
||||||
6430
package-lock.json
generated
Normal file
58
package.json
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"name": "ESH-Media",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"main": "main.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev:renderer": "vite",
|
||||||
|
"dev:electron": "node scripts/start-electron.js",
|
||||||
|
"dev": "concurrently \"npm:dev:renderer\" \"npm:dev:electron\"",
|
||||||
|
"build:renderer": "vite build",
|
||||||
|
"build:win": "npm run build:renderer && electron-builder --win",
|
||||||
|
"build:linux": "npm run build:renderer && electron-builder --linux",
|
||||||
|
"build": "npm run build:renderer && electron-builder --win --linux",
|
||||||
|
"dist": "electron-builder"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@cliqz/adblocker-electron": "^1.34.0",
|
||||||
|
"cheerio": "^1.2.0",
|
||||||
|
"node-fetch": "^2.7.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.3.3",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
|
"concurrently": "^8.2.2",
|
||||||
|
"electron": "^32.0.0",
|
||||||
|
"electron-builder": "^24.13.3",
|
||||||
|
"typescript": "^5.5.4",
|
||||||
|
"vite": "^5.4.2",
|
||||||
|
"wait-on": "^8.0.0"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"appId": "com.ESH-Media",
|
||||||
|
"productName": "ESH-Media",
|
||||||
|
"asar": true,
|
||||||
|
"directories": {
|
||||||
|
"output": "release"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist/**/*",
|
||||||
|
"main.js",
|
||||||
|
"preload.js",
|
||||||
|
"package.json",
|
||||||
|
"extensions/**/*"
|
||||||
|
],
|
||||||
|
"win": {
|
||||||
|
"target": "zip",
|
||||||
|
"icon": "public/favicon.ico"
|
||||||
|
},
|
||||||
|
"linux": {
|
||||||
|
"target": ["AppImage", "deb"],
|
||||||
|
"icon": "public/logo.png",
|
||||||
|
"category": "Utility"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
preload.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
const { contextBridge, ipcRenderer } = require('electron');
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('electron', {
|
||||||
|
createView: (name, url, imageUrl, zoom, useProxy) =>
|
||||||
|
ipcRenderer.send('create-view', name, url, imageUrl, zoom, useProxy),
|
||||||
|
confirm: (text, funcName) => ipcRenderer.send('confirm', text, funcName),
|
||||||
|
removeView: (name) => ipcRenderer.send('remove-view', name),
|
||||||
|
hideView: () => ipcRenderer.send('hide-view'),
|
||||||
|
showView: (name) => ipcRenderer.send('show-view', name),
|
||||||
|
adjustView: (expanded) => ipcRenderer.send('adjust-view', expanded),
|
||||||
|
on: (channel, func) => {
|
||||||
|
const listener = (_event, ...args) => func(...args);
|
||||||
|
ipcRenderer.on(channel, listener);
|
||||||
|
return () => ipcRenderer.removeListener(channel, listener);
|
||||||
|
},
|
||||||
|
handleAction: (action) => ipcRenderer.send('action', action),
|
||||||
|
setProxy: (host, port) => ipcRenderer.send('set-proxy', host, port),
|
||||||
|
expandWithHeader: () => ipcRenderer.send('expandWithHeader'),
|
||||||
|
collapseWithHeader: () => ipcRenderer.send('collapseWithHeader'),
|
||||||
|
backwardPage: () => ipcRenderer.send('backwardPage'),
|
||||||
|
forwardPage: () => ipcRenderer.send('forwardPage'),
|
||||||
|
refreshPage: () => ipcRenderer.send('refreshPage'),
|
||||||
|
getCurrentPage: () => ipcRenderer.invoke('get-current-page'),
|
||||||
|
readConfig: () => ipcRenderer.invoke('read-config'),
|
||||||
|
writeConfig: (data) => ipcRenderer.send('write-config', data),
|
||||||
|
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),
|
||||||
|
});
|
||||||
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
public/images/RuTube.jpg
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
public/images/VPN.png
Normal file
|
After Width: | Height: | Size: 972 B |
BIN
public/images/Youtube_logo.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
14
public/images/bluetooth-on.svg
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
viewBox="0 0 217.499 217.499" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<path d="M123.264,108.749l45.597-44.488c1.736-1.693,2.715-4.016,2.715-6.441s-0.979-4.748-2.715-6.441l-50.038-48.82
|
||||||
|
c-2.591-2.528-6.444-3.255-9.78-1.853c-3.336,1.406-5.505,4.674-5.505,8.294v80.504l-42.331-41.3
|
||||||
|
c-3.558-3.471-9.255-3.402-12.727,0.156c-3.471,3.558-3.401,9.256,0.157,12.727l48.851,47.663l-48.851,47.663
|
||||||
|
c-3.558,3.471-3.628,9.169-0.157,12.727s9.17,3.628,12.727,0.156l42.331-41.3v80.504c0,3.62,2.169,6.888,5.505,8.294
|
||||||
|
c1.128,0.476,2.315,0.706,3.493,0.706c2.305,0,4.572-0.886,6.287-2.559l50.038-48.82c1.736-1.693,2.715-4.016,2.715-6.441
|
||||||
|
s-0.979-4.748-2.715-6.441L123.264,108.749z M121.539,30.354l28.15,27.465l-28.15,27.465V30.354z M121.539,187.143v-54.93
|
||||||
|
l28.15,27.465L121.539,187.143z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
14
public/images/bluetooth.svg
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
viewBox="0 0 217.499 217.499" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<path d="M123.264,108.749l45.597-44.488c1.736-1.693,2.715-4.016,2.715-6.441s-0.979-4.748-2.715-6.441l-50.038-48.82
|
||||||
|
c-2.591-2.528-6.444-3.255-9.78-1.853c-3.336,1.406-5.505,4.674-5.505,8.294v80.504l-42.331-41.3
|
||||||
|
c-3.558-3.471-9.255-3.402-12.727,0.156c-3.471,3.558-3.401,9.256,0.157,12.727l48.851,47.663l-48.851,47.663
|
||||||
|
c-3.558,3.471-3.628,9.169-0.157,12.727s9.17,3.628,12.727,0.156l42.331-41.3v80.504c0,3.62,2.169,6.888,5.505,8.294
|
||||||
|
c1.128,0.476,2.315,0.706,3.493,0.706c2.305,0,4.572-0.886,6.287-2.559l50.038-48.82c1.736-1.693,2.715-4.016,2.715-6.441
|
||||||
|
s-0.979-4.748-2.715-6.441L123.264,108.749z M121.539,30.354l28.15,27.465l-28.15,27.465V30.354z M121.539,187.143v-54.93
|
||||||
|
l28.15,27.465L121.539,187.143z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
BIN
public/images/church.png
Normal file
|
After Width: | Height: | Size: 427 B |
BIN
public/images/collapse.png
Normal file
|
After Width: | Height: | Size: 607 B |
BIN
public/images/expand.png
Normal file
|
After Width: | Height: | Size: 600 B |
BIN
public/images/home.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
1
public/images/home.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg id="Слой_1" data-name="Слой 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 61.53 61.51"><defs><style>.cls-1{fill:#fff;}</style></defs><title>home</title><polygon class="cls-1" points="58.74 25.2 58.74 61.51 37.57 61.51 37.57 39.63 23.97 39.63 23.97 61.51 2.8 61.51 2.8 25.2 30.77 5.57 58.74 25.2"/><polyline class="cls-1" points="30.77 4.43 0 26.04 0 21.6 30.77 0"/><polyline class="cls-1" points="30.77 4.43 61.53 26.04 61.53 21.6 30.77 0"/></svg>
|
||||||
|
After Width: | Height: | Size: 465 B |
BIN
public/images/ivi.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
public/images/kinogo.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
public/images/kinopoisk.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
public/images/left-disabled.png
Normal file
|
After Width: | Height: | Size: 342 B |
BIN
public/images/left.png
Normal file
|
After Width: | Height: | Size: 341 B |
BIN
public/images/refresh-disabled.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
public/images/refresh.png
Normal file
|
After Width: | Height: | Size: 848 B |
BIN
public/images/right-disabled.png
Normal file
|
After Width: | Height: | Size: 363 B |
BIN
public/images/right.png
Normal file
|
After Width: | Height: | Size: 350 B |
BIN
public/images/tv.png
Normal file
|
After Width: | Height: | Size: 152 KiB |
9
public/images/volume-high.svg
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 512 512"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<title>ionicons-v5-g</title>
|
||||||
|
<path d="M232,416a23.88,23.88,0,0,1-14.2-4.68,8.27,8.27,0,0,1-.66-.51L125.76,336H56a24,24,0,0,1-24-24V200a24,24,0,0,1,24-24h69.75l91.37-74.81a8.27,8.27,0,0,1,.66-.51A24,24,0,0,1,256,120V392a24,24,0,0,1-24,24ZM125.82,336Zm-.27-159.86Z"/>
|
||||||
|
<path d="M320,336a16,16,0,0,1-14.29-23.19c9.49-18.87,14.3-38,14.3-56.81,0-19.38-4.66-37.94-14.25-56.73a16,16,0,0,1,28.5-14.54C346.19,208.12,352,231.44,352,256c0,23.86-6,47.81-17.7,71.19A16,16,0,0,1,320,336Z"/>
|
||||||
|
<path d="M368,384a16,16,0,0,1-13.86-24C373.05,327.09,384,299.51,384,256c0-44.17-10.93-71.56-29.82-103.94a16,16,0,0,1,27.64-16.12C402.92,172.11,416,204.81,416,256c0,50.43-13.06,83.29-34.13,120A16,16,0,0,1,368,384Z"/>
|
||||||
|
<path d="M416,432a16,16,0,0,1-13.39-24.74C429.85,365.47,448,323.76,448,256c0-66.5-18.18-108.62-45.49-151.39a16,16,0,1,1,27-17.22C459.81,134.89,480,181.74,480,256c0,64.75-14.66,113.63-50.6,168.74A16,16,0,0,1,416,432Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
9
public/images/volume.svg
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 512 512"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<title>ionicons-v5-g</title>
|
||||||
|
<path d="M232,416a23.88,23.88,0,0,1-14.2-4.68,8.27,8.27,0,0,1-.66-.51L125.76,336H56a24,24,0,0,1-24-24V200a24,24,0,0,1,24-24h69.75l91.37-74.81a8.27,8.27,0,0,1,.66-.51A24,24,0,0,1,256,120V392a24,24,0,0,1-24,24ZM125.82,336Zm-.27-159.86Z"/>
|
||||||
|
<path d="M320,336a16,16,0,0,1-14.29-23.19c9.49-18.87,14.3-38,14.3-56.81,0-19.38-4.66-37.94-14.25-56.73a16,16,0,0,1,28.5-14.54C346.19,208.12,352,231.44,352,256c0,23.86-6,47.81-17.7,71.19A16,16,0,0,1,320,336Z"/>
|
||||||
|
<path d="M368,384a16,16,0,0,1-13.86-24C373.05,327.09,384,299.51,384,256c0-44.17-10.93-71.56-29.82-103.94a16,16,0,0,1,27.64-16.12C402.92,172.11,416,204.81,416,256c0,50.43-13.06,83.29-34.13,120A16,16,0,0,1,368,384Z"/>
|
||||||
|
<path d="M416,432a16,16,0,0,1-13.39-24.74C429.85,365.47,448,323.76,448,256c0-66.5-18.18-108.62-45.49-151.39a16,16,0,1,1,27-17.22C459.81,134.89,480,181.74,480,256c0,64.75-14.66,113.63-50.6,168.74A16,16,0,0,1,416,432Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
20
public/images/wifi-connected.svg
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Capa_1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 365.892 365.892" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<circle cx="182.945" cy="286.681" r="41.494"/>
|
||||||
|
<path d="M182.946,176.029c-35.658,0-69.337,17.345-90.09,46.398c-5.921,8.288-4.001,19.806,4.286,25.726
|
||||||
|
c3.249,2.321,6.994,3.438,10.704,3.438c5.754,0,11.423-2.686,15.021-7.724c13.846-19.383,36.305-30.954,60.078-30.954
|
||||||
|
c23.775,0,46.233,11.571,60.077,30.953c5.919,8.286,17.437,10.209,25.726,4.288c8.288-5.92,10.208-17.438,4.288-25.726
|
||||||
|
C252.285,193.373,218.606,176.029,182.946,176.029z"/>
|
||||||
|
<path d="M182.946,106.873c-50.938,0-99.694,21.749-133.77,59.67c-6.807,7.576-6.185,19.236,1.392,26.044
|
||||||
|
c3.523,3.166,7.929,4.725,12.32,4.725c5.051-0.001,10.082-2.063,13.723-6.116c27.091-30.148,65.849-47.439,106.336-47.439
|
||||||
|
s79.246,17.291,106.338,47.438c6.808,7.576,18.468,8.198,26.043,1.391c7.576-6.808,8.198-18.468,1.391-26.043
|
||||||
|
C282.641,128.621,233.883,106.873,182.946,106.873z"/>
|
||||||
|
<path d="M360.611,112.293c-47.209-48.092-110.305-74.577-177.665-74.577c-67.357,0-130.453,26.485-177.664,74.579
|
||||||
|
c-7.135,7.269-7.027,18.944,0.241,26.079c3.59,3.524,8.255,5.282,12.918,5.281c4.776,0,9.551-1.845,13.161-5.522
|
||||||
|
c40.22-40.971,93.968-63.534,151.344-63.534c57.379,0,111.127,22.563,151.343,63.532c7.136,7.269,18.812,7.376,26.08,0.242
|
||||||
|
C367.637,131.238,367.745,119.562,360.611,112.293z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
1
public/images/wifi-high.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg id="Слой_1" data-name="Слой 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 75.14 67.5"><defs><style>.cls-1{fill:#fff;}</style></defs><title>wifi-high</title><circle class="cls-1" cx="37.57" cy="60.64" r="6.87"/><path class="cls-1" d="M12.51,25.76" transform="translate(-12.43 -14.26)"/><path class="cls-1" d="M44.24,78.62" transform="translate(-12.43 -14.26)"/><path class="cls-1" d="M87.6,25.76" transform="translate(-12.43 -14.26)"/><path class="cls-1" d="M55.87,78.62" transform="translate(-12.43 -14.26)"/><path class="cls-1" d="M87.52,38.32c-37.49-37.49-75.09,0-75.09,0l.05-7.4s37.59-37.49,75.09,0Z" transform="translate(-12.43 -14.26)"/><path class="cls-1" d="M77.39,50.26c-27.37-27.37-54.82,0-54.82,0l0-7.4s27.45-27.37,54.82,0Z" transform="translate(-12.43 -14.26)"/><path class="cls-1" d="M68.76,64.29c-18.75-18.75-37.54,0-37.54,0l0-7.7s18.8-18.75,37.54,0Z" transform="translate(-12.43 -14.26)"/></svg>
|
||||||
|
After Width: | Height: | Size: 927 B |
1
public/images/wifi-low.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg id="Слой_1" data-name="Слой 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 75.14 67.5"><defs><style>.cls-1{fill:#fff;}.cls-2{fill:#666;}</style></defs><title>wifi-low</title><circle class="cls-1" cx="37.57" cy="60.64" r="6.87"/><path class="cls-1" d="M12.51,25.76" transform="translate(-12.43 -14.26)"/><path class="cls-1" d="M44.24,78.62" transform="translate(-12.43 -14.26)"/><path class="cls-1" d="M87.6,25.76" transform="translate(-12.43 -14.26)"/><path class="cls-1" d="M55.87,78.62" transform="translate(-12.43 -14.26)"/><path class="cls-2" d="M87.52,38.32c-37.49-37.49-75.09,0-75.09,0l.05-7.4s37.59-37.49,75.09,0Z" transform="translate(-12.43 -14.26)"/><path class="cls-2" d="M77.39,50.26c-27.37-27.37-54.82,0-54.82,0l0-7.4s27.45-27.37,54.82,0Z" transform="translate(-12.43 -14.26)"/><path class="cls-1" d="M68.76,64.29c-18.75-18.75-37.54,0-37.54,0l0-7.7s18.8-18.75,37.54,0Z" transform="translate(-12.43 -14.26)"/></svg>
|
||||||
|
After Width: | Height: | Size: 944 B |
1
public/images/wifi-medium.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg id="Слой_1" data-name="Слой 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 75.14 67.5"><defs><style>.cls-1{fill:#fff;}.cls-2{fill:#666;}</style></defs><title>wifi-medium</title><circle class="cls-1" cx="37.57" cy="60.64" r="6.87"/><path class="cls-1" d="M12.51,25.76" transform="translate(-12.43 -14.26)"/><path class="cls-1" d="M44.24,78.62" transform="translate(-12.43 -14.26)"/><path class="cls-1" d="M87.6,25.76" transform="translate(-12.43 -14.26)"/><path class="cls-1" d="M55.87,78.62" transform="translate(-12.43 -14.26)"/><path class="cls-2" d="M87.52,38.32c-37.49-37.49-75.09,0-75.09,0l.05-7.4s37.59-37.49,75.09,0Z" transform="translate(-12.43 -14.26)"/><path class="cls-1" d="M77.39,50.26c-27.37-27.37-54.82,0-54.82,0l0-7.4s27.45-27.37,54.82,0Z" transform="translate(-12.43 -14.26)"/><path class="cls-1" d="M68.76,64.29c-18.75-18.75-37.54,0-37.54,0l0-7.7s18.8-18.75,37.54,0Z" transform="translate(-12.43 -14.26)"/></svg>
|
||||||
|
After Width: | Height: | Size: 947 B |
1
public/images/wifi-off.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg id="Слой_1" data-name="Слой 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 75.14 75.6"><defs><style>.cls-1{fill:#666;}.cls-2{fill:#fff;}</style></defs><title>wifi-off</title><circle class="cls-1" cx="37.57" cy="68.74" r="6.87"/><path class="cls-2" d="M12.51,25.76" transform="translate(-12.43 -6.16)"/><path class="cls-2" d="M44.24,78.62" transform="translate(-12.43 -6.16)"/><path class="cls-2" d="M87.6,25.76" transform="translate(-12.43 -6.16)"/><path class="cls-2" d="M55.87,78.62" transform="translate(-12.43 -6.16)"/><path class="cls-1" d="M87.52,38.32c-37.49-37.49-75.09,0-75.09,0l.05-7.4s37.59-37.49,75.09,0Z" transform="translate(-12.43 -6.16)"/><path class="cls-1" d="M77.39,50.26c-27.37-27.37-54.82,0-54.82,0l0-7.4s27.45-27.37,54.82,0Z" transform="translate(-12.43 -6.16)"/><path class="cls-1" d="M68.76,64.29c-18.75-18.75-37.54,0-37.54,0l0-7.7s18.8-18.75,37.54,0Z" transform="translate(-12.43 -6.16)"/><rect class="cls-2" x="4.79" y="38.18" width="90.41" height="4.58" transform="translate(32.39 -29.8) rotate(46.4)"/><rect class="cls-2" x="4.79" y="38.18" width="90.41" height="4.58" transform="translate(101.36 26.02) rotate(133.6)"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
1
public/images/wifi-vary-low.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg id="Слой_1" data-name="Слой 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 75.14 67.5"><defs><style>.cls-1{fill:#fff;}.cls-2{fill:#666;}</style></defs><title>wifi-vary-low</title><circle class="cls-1" cx="37.57" cy="60.64" r="6.87"/><path class="cls-1" d="M12.51,25.76" transform="translate(-12.43 -14.26)"/><path class="cls-1" d="M44.24,78.62" transform="translate(-12.43 -14.26)"/><path class="cls-1" d="M87.6,25.76" transform="translate(-12.43 -14.26)"/><path class="cls-1" d="M55.87,78.62" transform="translate(-12.43 -14.26)"/><path class="cls-2" d="M87.52,38.32c-37.49-37.49-75.09,0-75.09,0l.05-7.4s37.59-37.49,75.09,0Z" transform="translate(-12.43 -14.26)"/><path class="cls-2" d="M77.39,50.26c-27.37-27.37-54.82,0-54.82,0l0-7.4s27.45-27.37,54.82,0Z" transform="translate(-12.43 -14.26)"/><path class="cls-2" d="M68.76,64.29c-18.75-18.75-37.54,0-37.54,0l0-7.7s18.8-18.75,37.54,0Z" transform="translate(-12.43 -14.26)"/></svg>
|
||||||
|
After Width: | Height: | Size: 949 B |
20
public/images/wifi.svg
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Capa_1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 365.892 365.892" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<circle cx="182.945" cy="286.681" r="41.494"/>
|
||||||
|
<path d="M182.946,176.029c-35.658,0-69.337,17.345-90.09,46.398c-5.921,8.288-4.001,19.806,4.286,25.726
|
||||||
|
c3.249,2.321,6.994,3.438,10.704,3.438c5.754,0,11.423-2.686,15.021-7.724c13.846-19.383,36.305-30.954,60.078-30.954
|
||||||
|
c23.775,0,46.233,11.571,60.077,30.953c5.919,8.286,17.437,10.209,25.726,4.288c8.288-5.92,10.208-17.438,4.288-25.726
|
||||||
|
C252.285,193.373,218.606,176.029,182.946,176.029z"/>
|
||||||
|
<path d="M182.946,106.873c-50.938,0-99.694,21.749-133.77,59.67c-6.807,7.576-6.185,19.236,1.392,26.044
|
||||||
|
c3.523,3.166,7.929,4.725,12.32,4.725c5.051-0.001,10.082-2.063,13.723-6.116c27.091-30.148,65.849-47.439,106.336-47.439
|
||||||
|
s79.246,17.291,106.338,47.438c6.808,7.576,18.468,8.198,26.043,1.391c7.576-6.808,8.198-18.468,1.391-26.043
|
||||||
|
C282.641,128.621,233.883,106.873,182.946,106.873z"/>
|
||||||
|
<path d="M360.611,112.293c-47.209-48.092-110.305-74.577-177.665-74.577c-67.357,0-130.453,26.485-177.664,74.579
|
||||||
|
c-7.135,7.269-7.027,18.944,0.241,26.079c3.59,3.524,8.255,5.282,12.918,5.281c4.776,0,9.551-1.845,13.161-5.522
|
||||||
|
c40.22-40.971,93.968-63.534,151.344-63.534c57.379,0,111.127,22.563,151.343,63.532c7.136,7.269,18.812,7.376,26.08,0.242
|
||||||
|
C367.637,131.238,367.745,119.562,360.611,112.293z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
BIN
public/images/yandexMusic.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
43
public/index.html
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Web site created using create-react-app"
|
||||||
|
/>
|
||||||
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
|
<!--
|
||||||
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
|
-->
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
<!--
|
||||||
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
|
Only files inside the `public` folder can be referenced from the HTML.
|
||||||
|
|
||||||
|
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||||
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
|
-->
|
||||||
|
<title>React App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
<!--
|
||||||
|
This HTML file is a template.
|
||||||
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|
||||||
|
You can add webfonts, meta tags, or analytics to this file.
|
||||||
|
The build step will place the bundled scripts into the <body> tag.
|
||||||
|
|
||||||
|
To begin the development, run `npm start` or `yarn start`.
|
||||||
|
To create a production bundle, use `npm run build` or `yarn build`.
|
||||||
|
-->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
13
public/localPages/confirm/build/asset-manifest.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"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
public/localPages/confirm/build/index.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<!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>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
.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*/
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"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":""}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* @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.
|
||||||
|
*/
|
||||||
101
public/localPages/confirm/loader.html
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<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>
|
||||||
40
public/localPages/confirm/package.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"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": ""
|
||||||
|
}
|
||||||
12
public/localPages/confirm/public/index.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!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>
|
||||||
16
public/localPages/confirm/serv.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
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.'));
|
||||||
23
public/localPages/confirm/src/App.css
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
22
public/localPages/confirm/src/App.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// 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;
|
||||||
37
public/localPages/confirm/src/components/Confirm.css
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
31
public/localPages/confirm/src/components/Confirm.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// 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;
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/* 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 */
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// 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;
|
||||||
0
public/localPages/confirm/src/index.css
Normal file
22
public/localPages/confirm/src/index.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// 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");
|
||||||
|
}
|
||||||
21
public/localPages/confirm/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"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"]
|
||||||
|
}
|
||||||
|
|
||||||
13
public/localPages/error/build/asset-manifest.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"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
public/localPages/error/build/index.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<!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>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
.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*/
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"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":""}
|
||||||
3
public/localPages/error/build/static/js/main.ab661a74.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* @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.
|
||||||
|
*/
|
||||||
101
public/localPages/error/loader.html
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<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>
|
||||||
40
public/localPages/error/package.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"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": ""
|
||||||
|
}
|
||||||
12
public/localPages/error/public/index.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!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>
|
||||||
16
public/localPages/error/serv.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
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.'));
|
||||||
23
public/localPages/error/src/App.css
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
22
public/localPages/error/src/App.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// 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;
|
||||||
37
public/localPages/error/src/components/Error.css
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
32
public/localPages/error/src/components/Error.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// 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;
|
||||||
40
public/localPages/error/src/components/ErrorContainer.css
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
/* 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 */
|
||||||
|
}
|
||||||
|
|
||||||
27
public/localPages/error/src/components/ErrorContainer.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// 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;
|
||||||
0
public/localPages/error/src/index.css
Normal file
22
public/localPages/error/src/index.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// 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");
|
||||||
|
}
|
||||||
21
public/localPages/error/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"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"]
|
||||||
|
}
|
||||||
|
|
||||||
13
public/localPages/loader/build/asset-manifest.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"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
public/localPages/loader/build/index.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<!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>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
.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*/
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"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":""}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* @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.
|
||||||
|
*/
|
||||||
101
public/localPages/loader/loader.html
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<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>
|
||||||
41
public/localPages/loader/package.json
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"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": ""
|
||||||
|
}
|
||||||
12
public/localPages/loader/public/index.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!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>
|
||||||
16
public/localPages/loader/serv.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
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.'));
|
||||||
23
public/localPages/loader/src/App.css
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
22
public/localPages/loader/src/App.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// 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;
|
||||||
37
public/localPages/loader/src/components/Loader.css
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
14
public/localPages/loader/src/components/Loader.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// 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;
|
||||||
14
public/localPages/loader/src/components/LoaderContainer.css
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/* 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 */
|
||||||
|
}
|
||||||
|
|
||||||
14
public/localPages/loader/src/components/LoaderContainer.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// 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;
|
||||||
0
public/localPages/loader/src/index.css
Normal file
22
public/localPages/loader/src/index.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// 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");
|
||||||
|
}
|
||||||
21
public/localPages/loader/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"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"]
|
||||||
|
}
|
||||||
|
|
||||||