This commit is contained in:
2026-03-14 05:04:51 +03:00
commit c31e4a304d
120 changed files with 11802 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
node_modules/
dist/
dist-electron/
release/
*.log
.DS_Store

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"DockerRun.DisableDockerrc": true
}

49
README.md Normal file
View 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
View File

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

904
main.js Normal file
View 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

File diff suppressed because it is too large Load Diff

58
package.json Normal file
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
public/images/RuTube.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
public/images/VPN.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 972 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 B

BIN
public/images/collapse.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 607 B

BIN
public/images/expand.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 B

BIN
public/images/home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

1
public/images/home.svg Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

BIN
public/images/kinogo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
public/images/kinopoisk.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 B

BIN
public/images/left.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
public/images/refresh.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 848 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 B

BIN
public/images/right.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 B

BIN
public/images/tv.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

View 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
View 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

View 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

View 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

View 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

View 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

View 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

View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

43
public/index.html Normal file
View 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>

View 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"
]
}

View 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>

View File

@@ -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*/

View File

@@ -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":""}

File diff suppressed because one or more lines are too long

View 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.
*/

File diff suppressed because one or more lines are too long

View 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>

View 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": ""
}

View 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>

View 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.'));

View 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);
}
}

View 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;

View 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;
}
}
*/

View 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;

View File

@@ -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 */
}

View File

@@ -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;

View File

View 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");
}

View 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"]
}

View 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"
]
}

View 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>

View File

@@ -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*/

View File

@@ -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":""}

File diff suppressed because one or more lines are too long

View 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.
*/

File diff suppressed because one or more lines are too long

View 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>

View 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": ""
}

View 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>

View 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.'));

View 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);
}
}

View 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;

View 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;
}
}
*/

View 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;

View 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 */
}

View 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;

View File

View 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");
}

View 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"]
}

View 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"
]
}

View 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>

View File

@@ -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*/

View File

@@ -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":""}

File diff suppressed because one or more lines are too long

View 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.
*/

File diff suppressed because one or more lines are too long

View 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>

View 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": ""
}

View 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>

View 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.'));

View 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);
}
}

View 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;

View 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;
}
}

View 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;

View 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 */
}

View 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;

View File

View 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");
}

View 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"]
}

Some files were not shown because too many files have changed in this diff Show More