feat: trusted-domains OAuth popups, og:image bookmark posters, periodic updater
- main.js: trusted-domains list with default Google/Yandex/GitHub/etc.; cross-domain confirmation skipped for trusted; setWindowOpenHandler returns action:'allow' for trusted so OAuth popups work (postMessage back to opener, popup self-closes). Fixes YouTube/Google login reset. - main.js: get-page-meta IPC extracts og:image / twitter:image / JSON-LD image from current view; HDRezka also tries .b-sidecover img for hi-res. - Header: bookmark button pulls og:image as poster and the page's title; duplicate detection switched from hostname to full URL so multiple movies from same site can coexist. - BookmarksBar: site icon rendered next to source domain when distinct from poster; img onerror falls back to placeholder. - Settings: trusted domains chip list with add/remove/reset. - Updater: proper semver compare (only show if latest > current), direct installer URL detection per platform, hourly re-check. Bookmark schema gains optional siteIcon; existing bookmarks remain valid. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
148
main.js
148
main.js
@@ -7,9 +7,47 @@ const { ElectronBlocker, adsAndTrackingLists } = require('@cliqz/adblocker-elect
|
||||
|
||||
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' } };
|
||||
const DEFAULT_TRUSTED_DOMAINS = [
|
||||
// Google ecosystem (OAuth)
|
||||
'google.com', 'accounts.google.com', 'googleapis.com', 'googleusercontent.com',
|
||||
'gstatic.com', 'youtube.com', 'ytimg.com', 'googlevideo.com',
|
||||
// Yandex
|
||||
'yandex.ru', 'yandex.com', 'passport.yandex.ru', 'passport.yandex.com', 'yastatic.net',
|
||||
// GitHub
|
||||
'github.com', 'github.io', 'githubassets.com', 'githubusercontent.com',
|
||||
// VK / Mail.ru
|
||||
'vk.com', 'vk.ru', 'vkuser.net', 'mail.ru', 'my.mail.ru',
|
||||
// Microsoft (login.live.com etc., некоторые сайты через них)
|
||||
'live.com', 'microsoft.com', 'microsoftonline.com', 'office.com',
|
||||
// Apple
|
||||
'apple.com', 'icloud.com',
|
||||
// Facebook (для соцлогина)
|
||||
'facebook.com', 'fb.com',
|
||||
];
|
||||
const DEFAULT_CONFIG = { apps: [], proxy: { host: '127.0.0.1', port: '7890' }, trustedDomains: DEFAULT_TRUSTED_DOMAINS };
|
||||
|
||||
let blockerPromise = null;
|
||||
let cachedTrustedDomains = DEFAULT_TRUSTED_DOMAINS;
|
||||
|
||||
function loadTrustedDomainsFromDisk() {
|
||||
try {
|
||||
if (fs.existsSync(CONFIG_PATH)) {
|
||||
const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
||||
if (Array.isArray(cfg.trustedDomains) && cfg.trustedDomains.length) {
|
||||
cachedTrustedDomains = cfg.trustedDomains;
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function isTrustedDomain(hostname) {
|
||||
if (!hostname) return false;
|
||||
const h = hostname.toLowerCase();
|
||||
return cachedTrustedDomains.some(d => {
|
||||
const dom = d.toLowerCase().replace(/^\./, '');
|
||||
return h === dom || h.endsWith('.' + dom);
|
||||
});
|
||||
}
|
||||
|
||||
function getBlocker() {
|
||||
if (blockerPromise) return blockerPromise;
|
||||
@@ -159,6 +197,18 @@ async function loadExtensions() {
|
||||
|
||||
// --- Updates ---
|
||||
|
||||
function compareSemver(a, b) {
|
||||
// Returns 1 if a > b, -1 if a < b, 0 if equal. Numeric per-segment, missing → 0.
|
||||
const pa = a.split('.').map(n => parseInt(n, 10) || 0);
|
||||
const pb = b.split('.').map(n => parseInt(n, 10) || 0);
|
||||
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
||||
const x = pa[i] || 0, y = pb[i] || 0;
|
||||
if (x > y) return 1;
|
||||
if (x < y) return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
async function checkForUpdates() {
|
||||
try {
|
||||
const res = await getDirectSession().fetch(
|
||||
@@ -168,13 +218,25 @@ async function checkForUpdates() {
|
||||
const data = await res.json();
|
||||
const latest = (data.tag_name || '').replace(/^v/, '');
|
||||
const current = app.getVersion();
|
||||
if (latest && latest !== current) {
|
||||
mainWindow.webContents.send('update-available', {
|
||||
version: latest,
|
||||
url: data.html_url,
|
||||
assets: (data.assets || []).map(a => ({ name: a.name, url: a.browser_download_url })),
|
||||
});
|
||||
}
|
||||
if (!latest || compareSemver(latest, current) <= 0) return;
|
||||
const assets = (data.assets || []).map(a => ({ name: a.name, url: a.browser_download_url }));
|
||||
// Prefer Windows installer (.exe) on Windows, AppImage/deb on Linux. Fall back to zip.
|
||||
const isWin = process.platform === 'win32';
|
||||
const isLinux = process.platform === 'linux';
|
||||
const installer = assets.find(a => {
|
||||
const n = a.name.toLowerCase();
|
||||
if (isWin) return n.endsWith('.exe');
|
||||
if (isLinux) return n.endsWith('.appimage') || n.endsWith('.deb');
|
||||
return false;
|
||||
}) || assets.find(a => a.name.toLowerCase().endsWith('.zip'));
|
||||
mainWindow.webContents.send('update-available', {
|
||||
version: latest,
|
||||
currentVersion: current,
|
||||
releaseUrl: data.html_url,
|
||||
installerUrl: installer?.url || data.html_url,
|
||||
installerName: installer?.name || '',
|
||||
assets,
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
@@ -392,7 +454,7 @@ ipcMain.on('create-view', async (_event, name, url, imageUrl, _zoom, useProxy) =
|
||||
if (newUrl.startsWith('data:')) { trackNavigation(newUrl); return; }
|
||||
let newHostname = '';
|
||||
try { newHostname = new URL(newUrl).hostname; } catch (_) { trackNavigation(newUrl); return; }
|
||||
if (origHostname && newHostname && newHostname !== origHostname) {
|
||||
if (origHostname && newHostname && newHostname !== origHostname && !isTrustedDomain(newHostname)) {
|
||||
e.preventDefault();
|
||||
pendingNavigate = { view, url: newUrl };
|
||||
setConfirm(`Перейти на "${newHostname}"?`, 'navigate-confirmed');
|
||||
@@ -401,14 +463,37 @@ ipcMain.on('create-view', async (_event, name, url, imageUrl, _zoom, useProxy) =
|
||||
trackNavigation(newUrl);
|
||||
});
|
||||
view.webContents.on('will-redirect', (_e, u) => trackNavigation(u));
|
||||
view.webContents.setWindowOpenHandler(({ url: newUrl }) => {
|
||||
view.webContents.setWindowOpenHandler(({ url: newUrl, frameName, features }) => {
|
||||
let newHostname = '';
|
||||
try { newHostname = new URL(newUrl).hostname; } catch (_) {}
|
||||
|
||||
// Trusted domain → open as real popup BrowserWindow with same session.
|
||||
// This is what OAuth flows need: window.opener.postMessage() works,
|
||||
// popup can close itself when done, parent stays on the original page.
|
||||
if (newHostname && isTrustedDomain(newHostname)) {
|
||||
return {
|
||||
action: 'allow',
|
||||
overrideBrowserWindowOptions: {
|
||||
width: 520, height: 640,
|
||||
parent: mainWindow,
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
session: view.webContents.session,
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Untrusted cross-domain → ask the user (original behavior).
|
||||
if (origHostname && newHostname && newHostname !== origHostname) {
|
||||
pendingNavigate = { view, url: newUrl };
|
||||
setConfirm(`Перейти на "${newHostname}"?`, 'navigate-confirmed');
|
||||
return { action: 'deny' };
|
||||
}
|
||||
|
||||
// Same-origin popup → just navigate the current view.
|
||||
trackNavigation(newUrl);
|
||||
view.webContents.loadURL(newUrl);
|
||||
return { action: 'deny' };
|
||||
@@ -806,11 +891,52 @@ ipcMain.handle('read-config', () => {
|
||||
ipcMain.on('write-config', (_event, data) => {
|
||||
try {
|
||||
fs.writeFileSync(CONFIG_PATH, JSON.stringify(data, null, 2), 'utf8');
|
||||
if (Array.isArray(data?.trustedDomains)) cachedTrustedDomains = data.trustedDomains;
|
||||
} catch (e) {
|
||||
console.warn('Failed to write config:', e.message);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-page-meta', async () => {
|
||||
if (!currentView || !currentView.view || currentView.view.webContents.isDestroyed()) return null;
|
||||
try {
|
||||
return await currentView.view.webContents.executeJavaScript(`
|
||||
(function() {
|
||||
const get = (sel, attr) => { const el = document.querySelector(sel); return el ? (el.getAttribute(attr) || '') : ''; };
|
||||
const abs = (u) => { try { return new URL(u, location.href).href } catch (_) { return u } };
|
||||
let poster = get('meta[property="og:image:secure_url"]', 'content')
|
||||
|| get('meta[property="og:image"]', 'content')
|
||||
|| get('meta[name="twitter:image"]', 'content')
|
||||
|| get('meta[name="twitter:image:src"]', 'content');
|
||||
// HDRezka has a higher-res poster in the sidebar
|
||||
if (!poster || /hdrezka|rezka/i.test(location.hostname)) {
|
||||
const side = document.querySelector('.b-sidecover img');
|
||||
if (side && side.src) poster = side.src;
|
||||
}
|
||||
// JSON-LD fallback
|
||||
if (!poster) {
|
||||
for (const s of document.querySelectorAll('script[type="application/ld+json"]')) {
|
||||
try {
|
||||
const d = JSON.parse(s.textContent || 'null');
|
||||
const arr = Array.isArray(d) ? d : [d];
|
||||
for (const it of arr) {
|
||||
if (!it) continue;
|
||||
const img = it.image || it.poster || (it.thumbnailUrl);
|
||||
if (img) { poster = Array.isArray(img) ? img[0] : (typeof img === 'string' ? img : img.url); if (poster) break; }
|
||||
}
|
||||
if (poster) break;
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
const title = (get('meta[property="og:title"]', 'content') || document.title || '').trim();
|
||||
return { poster: poster ? abs(poster) : '', title, url: location.href };
|
||||
})()
|
||||
`);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('is-kiosk', () => mainWindow.isKiosk());
|
||||
|
||||
ipcMain.handle('toggle-kiosk', () => {
|
||||
@@ -871,6 +997,7 @@ app.whenReady().then(async () => {
|
||||
);
|
||||
|
||||
// Apply proxy from config before blocker tries to download filter lists
|
||||
loadTrustedDomainsFromDisk();
|
||||
try {
|
||||
if (fs.existsSync(CONFIG_PATH)) {
|
||||
const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
||||
@@ -886,6 +1013,7 @@ app.whenReady().then(async () => {
|
||||
|
||||
mainWindow.webContents.once('did-finish-load', () => {
|
||||
setTimeout(checkForUpdates, 4000);
|
||||
setInterval(checkForUpdates, 60 * 60 * 1000); // re-check hourly for long-running kiosk sessions
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user