diff --git a/README.md b/README.md index 658a7de..d605ae5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Next Gen +# Next Gen Dev the next generation of template @@ -24,6 +24,9 @@ the next generation of template - [ ] Release VirusTotal scan results for security verification. - [ ] Auto update - [ ] Https for mcp +- [ ] wallpapers app and system +- [ ] System app +- [ ] Uninstall system external app The issue is that Turborepo expects a specific structure. Here's the correct setup: @@ -179,3 +182,4 @@ npm link turbo-generators + diff --git a/apps/ui/package.json b/apps/ui/package.json index 76a83af..8b8bc6c 100644 --- a/apps/ui/package.json +++ b/apps/ui/package.json @@ -1,7 +1,7 @@ { - "name": "@nde/next-gen-tools", + "name": "next-gen-dev", "version": "1.0.1", - "description": "Electron app with floating DevTools", + "description": "Next Gen Developer Tools", "main": "./out/main/index.js", "author": "Next Dev Team ", "homepage": "https://github.com/next-dev-team/next-gen#readme", @@ -91,8 +91,8 @@ "zustand": "^5.0.9" }, "build": { - "appId": "com.float-devtools-app.app", - "productName": "next-gen-tools", + "appId": "com.next-gen-dev.app", + "productName": "Next Gen Dev", "publish": [ { "provider": "github", @@ -101,7 +101,8 @@ } ], "directories": { - "output": "dist" + "output": "dist", + "buildResources": "resources" }, "files": [ "out/**/*", @@ -150,6 +151,11 @@ ], "artifactName": "${productName}-${version}-${arch}.${ext}", "signAndEditExecutable": false + }, + "nsis": { + "oneClick": false, + "allowToChangeInstallationDirectory": true, + "deleteAppDataOnUninstall": true } } } diff --git a/apps/ui/resources/icon.svg b/apps/ui/resources/icon.svg new file mode 100644 index 0000000..786b828 --- /dev/null +++ b/apps/ui/resources/icon.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/ui/src/main/anti-detection/index.js b/apps/ui/src/main/anti-detection/index.js index 0d570c4..5cb53c2 100644 --- a/apps/ui/src/main/anti-detection/index.js +++ b/apps/ui/src/main/anti-detection/index.js @@ -183,15 +183,18 @@ async function setupSessionAntiDetection(ses, profile) { "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8"; } - // Add SEC headers for Chromium browsers if (userAgent && userAgent.includes("Chrome")) { - headers["Sec-CH-UA"] = generateSecChUa(profile); - headers["Sec-CH-UA-Mobile"] = profile.category === "mobile" ? "?1" : "?0"; - headers["Sec-CH-UA-Platform"] = getPlatformHint(profile); - headers["Sec-Fetch-Dest"] = headers["Sec-Fetch-Dest"] || "document"; - headers["Sec-Fetch-Mode"] = headers["Sec-Fetch-Mode"] || "navigate"; - headers["Sec-Fetch-Site"] = headers["Sec-Fetch-Site"] || "none"; - headers["Sec-Fetch-User"] = "?1"; + const profileWithUserAgent = { ...profile, userAgent }; + if (!headers["Sec-CH-UA"]) { + headers["Sec-CH-UA"] = generateSecChUa(profileWithUserAgent); + } + if (!headers["Sec-CH-UA-Mobile"]) { + headers["Sec-CH-UA-Mobile"] = + profile?.category === "mobile" ? "?1" : "?0"; + } + if (!headers["Sec-CH-UA-Platform"]) { + headers["Sec-CH-UA-Platform"] = getPlatformHint(profileWithUserAgent); + } } // Remove Electron-specific headers @@ -245,16 +248,21 @@ async function setupSessionAntiDetection(ses, profile) { * Generate Sec-CH-UA header value */ function generateSecChUa(profile) { - if (profile.userAgent.includes("Chrome")) { - const match = profile.userAgent.match(/Chrome\/(\d+)/); + const userAgent = + typeof profile?.userAgent === "string" ? profile.userAgent : ""; + + if (userAgent.includes("Edg")) { + const match = userAgent.match(/Edg\/(\d+)/); const version = match ? match[1] : "122"; - return `"Chromium";v="${version}", "Google Chrome";v="${version}", "Not(A:Brand";v="24"`; + return `"Chromium";v="${version}", "Microsoft Edge";v="${version}", "Not(A:Brand";v="24"`; } - if (profile.userAgent.includes("Edg")) { - const match = profile.userAgent.match(/Edg\/(\d+)/); + + if (userAgent.includes("Chrome")) { + const match = userAgent.match(/Chrome\/(\d+)/); const version = match ? match[1] : "122"; - return `"Chromium";v="${version}", "Microsoft Edge";v="${version}", "Not(A:Brand";v="24"`; + return `"Chromium";v="${version}", "Google Chrome";v="${version}", "Not(A:Brand";v="24"`; } + return `"Chromium";v="122", "Not(A:Brand";v="24"`; } @@ -262,14 +270,16 @@ function generateSecChUa(profile) { * Get platform hint from profile */ function getPlatformHint(profile) { - if (profile.platform.includes("Win")) return '"Windows"'; - if (profile.platform.includes("Mac")) return '"macOS"'; - if (profile.platform.includes("Linux")) return '"Linux"'; - if (profile.platform.includes("iPhone")) return '"iOS"'; - if ( - profile.platform.includes("armv") || - profile.userAgent.includes("Android") - ) + const platform = + typeof profile?.platform === "string" ? profile.platform : ""; + const userAgent = + typeof profile?.userAgent === "string" ? profile.userAgent : ""; + + if (platform.includes("Win")) return '"Windows"'; + if (platform.includes("Mac")) return '"macOS"'; + if (platform.includes("Linux")) return '"Linux"'; + if (platform.includes("iPhone")) return '"iOS"'; + if (platform.includes("armv") || userAgent.includes("Android")) return '"Android"'; return '"Unknown"'; } @@ -464,4 +474,6 @@ module.exports = { getAllProfiles, getProfile, getRandomProfile, + generateSecChUa, + getPlatformHint, }; diff --git a/apps/ui/src/main/index.js b/apps/ui/src/main/index.js index 6ad7d21..bff3dd7 100644 --- a/apps/ui/src/main/index.js +++ b/apps/ui/src/main/index.js @@ -21,6 +21,36 @@ const { spawn, fork } = require("child_process"); const fs = require("fs"); const Conf = require("conf"); +// Set app name explicitly for system dialogs and notifications +app.name = "Next Gen Dev"; + +const isPlaywrightRun = + process.execArgv.some((arg) => /playwright/i.test(String(arg || ""))) || + process.argv.some((arg) => /playwright/i.test(String(arg || ""))) || + process.env.PW_TEST === "1" || + process.env.PLAYWRIGHT === "1"; + +if (isPlaywrightRun || process.env.NEXTGEN_NO_SANDBOX === "1") { + try { + app.commandLine.appendSwitch("no-sandbox"); + } catch {} + try { + app.commandLine.appendSwitch("disable-setuid-sandbox"); + } catch {} +} + +// Configure About panel for macOS +if (process.platform === "darwin") { + app.setAboutPanelOptions({ + applicationName: "Next Gen Dev", + applicationVersion: app.getVersion(), + copyright: "Copyright © 2026 Next Dev Team", + version: "1.0.1", + website: "https://next-dev.team", + iconPath: path.resolve(__dirname, "../../resources/icon.svg"), + }); +} + // Anti-detection module for browser fingerprinting protection const antiDetection = require("./anti-detection"); @@ -41,14 +71,29 @@ process.on("unhandledRejection", (reason, promise) => { // Don't crash on unhandled rejections }); +let scheduleActiveBrowserViewRecovery = null; + // Handle GPU process crashes app.on("gpu-process-crashed", (event, killed) => { console.error("[GPU] GPU process crashed, killed:", killed); + if (typeof scheduleActiveBrowserViewRecovery === "function") { + scheduleActiveBrowserViewRecovery({ type: "gpu", killed }); + } }); // Handle child process crashes app.on("child-process-gone", (event, details) => { console.error("[Process] Child process gone:", details.type, details.reason); + if ( + details && + (details.type === "GPU" || details.type === "Utility") && + typeof scheduleActiveBrowserViewRecovery === "function" + ) { + scheduleActiveBrowserViewRecovery({ + type: details.type, + reason: details.reason, + }); + } }); const scrumStore = new Conf({ projectName: "next-gen-scrum" }); @@ -99,6 +144,45 @@ let trayClickTimer = null; const DEFAULT_QUICK_TOGGLE_SHORTCUT = "CommandOrControl+Shift+Space"; +// ============================================ +// SINGLE INSTANCE LOCK & DEEP LINKING +// ============================================ + +const shouldUseSingleInstanceLock = + !isPlaywrightRun && process.env.NEXTGEN_DISABLE_SINGLE_INSTANCE !== "1"; + +if (shouldUseSingleInstanceLock) { + const gotTheLock = app.requestSingleInstanceLock(); + if (!gotTheLock) { + app.quit(); + } else { + app.on("second-instance", (event, commandLine) => { + if (mainWindow) { + if (mainWindow.isMinimized()) mainWindow.restore(); + if (!mainWindow.isVisible()) mainWindow.show(); + mainWindow.focus(); + + const url = commandLine.find( + (arg) => arg.startsWith("http://") || arg.startsWith("https://") + ); + if (url) { + openUrlWithTarget(url, true).catch(console.error); + } + } + }); + + app.on("open-url", (event, url) => { + event.preventDefault(); + openUrlWithTarget(url, true).catch(console.error); + }); + } +} else { + app.on("open-url", (event, url) => { + event.preventDefault(); + openUrlWithTarget(url, true).catch(console.error); + }); +} + const browserViews = new Map(); let activeBrowserTabId = null; const browserBoundsCache = new Map(); @@ -107,6 +191,20 @@ const browserPopupStatsByTabId = new Map(); let adblockEnabledCache = null; let adblockerPromise = null; +scheduleActiveBrowserViewRecovery = () => { + const tabId = activeBrowserTabId; + if (!tabId) return; + const view = browserViews.get(tabId); + if (!view || !view.webContents || view.webContents.isDestroyed()) return; + + setTimeout(() => { + if (!view || !view.webContents || view.webContents.isDestroyed()) return; + try { + view.webContents.reload(); + } catch {} + }, 750); +}; + async function ensureAdblocker() { if (adblockerPromise) return adblockerPromise; adblockerPromise = (async () => { @@ -521,6 +619,11 @@ function safeHideWindow(windowInstance) { function createWindow({ show = true } = {}) { const shouldShow = Boolean(show); + const trayIcon = resolveAppIcon({ + size: process.platform === "darwin" ? 18 : 16, + }); + const windowIcon = resolveAppIcon({ size: 256 }); + mainWindow = new BrowserWindow({ width: 1200, height: 800, @@ -528,6 +631,7 @@ function createWindow({ show = true } = {}) { minHeight: 600, backgroundColor: "#0f172a", show: false, + icon: windowIcon, skipTaskbar: true, titleBarStyle: "hidden", titleBarOverlay: { @@ -584,39 +688,17 @@ function createWindow({ show = true } = {}) { }); } -function resolveTrayIcon() { +function resolveAppIcon({ size = 16 } = {}) { const packagedCandidates = [ - path.join( - process.resourcesPath, - "turbo", - "generators", - "templates", - "rnr-expo", - "assets", - "images", - "favicon.png" - ), - path.join( - process.resourcesPath, - "turbo", - "generators", - "templates", - "rnr-uniwind", - "assets", - "images", - "favicon.png" - ), + path.join(process.resourcesPath, "icon.png"), + path.join(process.resourcesPath, "icon.ico"), + path.join(process.resourcesPath, "resources", "icon.png"), + path.join(process.resourcesPath, "resources", "icon.svg"), ]; const devCandidates = [ - path.resolve( - __dirname, - "../../../turbo/generators/templates/rnr-expo/assets/images/favicon.png" - ), - path.resolve( - __dirname, - "../../../turbo/generators/templates/rnr-uniwind/assets/images/favicon.png" - ), + path.resolve(__dirname, "../../resources/icon.png"), + path.resolve(__dirname, "../../resources/icon.svg"), ]; const candidates = app.isPackaged @@ -628,12 +710,12 @@ function resolveTrayIcon() { if (!fs.existsSync(candidate)) continue; const image = nativeImage.createFromPath(candidate); if (image && !image.isEmpty()) { - const size = process.platform === "darwin" ? 18 : 16; return image.resize({ width: size, height: size }); } } catch {} } + // Fallback to a simple data URL if no icon found return nativeImage.createFromDataURL( "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAABmSURBVHgB7ZKxDYAwCENzJ3EFd3AEZ3AEZ3AEZ7DgQi1KM3pZVj9kqUQy4X5y0KcQ7mGdR1gHQQQyQq0v7z0yWmWcX4q8WwWQjT2QbC7a0g6y9mYp+Qb9b7QxQy2lQAAAABJRU5ErkJggg==" ); @@ -698,9 +780,11 @@ async function updateTrayMenu() { function ensureTray() { if (tray) return tray; - const icon = resolveTrayIcon(); + const icon = resolveAppIcon({ + size: process.platform === "darwin" ? 18 : 16, + }); tray = new Tray(icon); - tray.setToolTip("Next Gen"); + tray.setToolTip("Next Gen Dev"); updateTrayMenu().catch(() => {}); @@ -828,6 +912,7 @@ function notifyBrowserState(tabId) { url: view.webContents.getURL(), canGoBack: view.webContents.canGoBack(), canGoForward: view.webContents.canGoForward(), + isLoading: view.webContents.isLoading(), }); } @@ -877,6 +962,10 @@ async function ensureBrowserView(tabId, options = {}) { const view = new BrowserView({ webPreferences }); + try { + view.webContents.backgroundThrottling = false; + } catch {} + applyAdblockToSession(view.webContents.session).catch(() => {}); try { @@ -895,6 +984,9 @@ async function ensureBrowserView(tabId, options = {}) { view.webContents.on("did-navigate", () => notifyBrowserState(tabId)); view.webContents.on("did-navigate-in-page", () => notifyBrowserState(tabId)); + view.webContents.on("did-start-loading", () => notifyBrowserState(tabId)); + view.webContents.on("did-stop-loading", () => notifyBrowserState(tabId)); + view.webContents.on("did-fail-load", () => notifyBrowserState(tabId)); // Inject anti-detection stealth script after page load view.webContents.on("did-finish-load", async () => { @@ -1046,7 +1138,7 @@ async function ensureBrowserView(tabId, options = {}) { }); if (!adblockEnabledCache) { - shell.openExternal(url).catch(() => {}); + openUrlWithTarget(url).catch(() => {}); } return { action: "deny" }; } catch { @@ -2081,6 +2173,52 @@ ipcMain.handle("get-quick-toggle-shortcut", async () => { return currentStore.get("quickToggleShortcut", DEFAULT_QUICK_TOGGLE_SHORTCUT); }); +ipcMain.handle("get-external-link-target", async () => { + const currentStore = await getStore(); + const value = String( + currentStore.get("externalLinkTarget", "system") || "system" + ); + return value === "app" ? "app" : "system"; +}); + +ipcMain.handle("set-external-link-target", async (event, value) => { + const currentStore = await getStore(); + const normalized = value === "app" ? "app" : "system"; + currentStore.set("externalLinkTarget", normalized); + sendSettingsChanged("externalLinkTarget", normalized); + return true; +}); + +ipcMain.handle("is-default-browser", async () => { + // Check if both http and https are handled by this app + return ( + app.isDefaultProtocolClient("http") && app.isDefaultProtocolClient("https") + ); +}); + +ipcMain.handle("set-as-default-browser", async (event, value) => { + let success = false; + if (value) { + const successHttp = app.setAsDefaultProtocolClient("http"); + const successHttps = app.setAsDefaultProtocolClient("https"); + success = successHttp && successHttps; + } else { + const successHttp = app.removeAsDefaultProtocolClient("http"); + const successHttps = app.removeAsDefaultProtocolClient("https"); + success = successHttp && successHttps; + } + + if (success) { + // Notify renderer about the change + const isDefault = + app.isDefaultProtocolClient("http") && + app.isDefaultProtocolClient("https"); + sendSettingsChanged("isDefaultBrowser", isDefault); + } + + return success; +}); + // Select folder dialog ipcMain.handle("select-folder", async (event, { title, defaultPath }) => { const result = await dialog.showOpenDialog(mainWindow, { @@ -2112,6 +2250,48 @@ async function ensureAbsolutePath(targetPath) { return path.resolve(rootPath, targetPath); } +async function openUrlWithTarget(url, forceApp = false) { + const raw = String(url || "").trim(); + if (!raw) return false; + + let parsed; + try { + parsed = new URL(raw); + } catch { + return false; + } + + if (!/^https?:$/.test(parsed.protocol)) return false; + + const currentStore = await getStore(); + const target = String( + currentStore.get("externalLinkTarget", "system") || "system" + ); + + // If forced (e.g. from deep link) or if the app is the system default browser, + // we should open it in the app's internal browser view. + const isDefault = + app.isDefaultProtocolClient("http") && app.isDefaultProtocolClient("https"); + + if (forceApp || isDefault || target === "app") { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send("open-in-app-browser", { + url: parsed.toString(), + }); + return true; + } + return false; + } + + try { + await shell.openExternal(parsed.toString()); + return true; + } catch (err) { + console.error("Failed to open external URL:", err); + return false; + } +} + // Open folder in file explorer ipcMain.handle("open-folder", async (event, folderPath) => { if (folderPath) { @@ -2124,16 +2304,7 @@ ipcMain.handle("open-folder", async (event, folderPath) => { // Open external URL ipcMain.handle("open-external", async (event, url) => { - if (url) { - try { - await shell.openExternal(url); - return true; - } catch (err) { - console.error("Failed to open external URL:", err); - return false; - } - } - return false; + return await openUrlWithTarget(url); }); // Write text to clipboard @@ -2162,13 +2333,75 @@ ipcMain.handle("app-uninstall", async () => { if (result.response === 1) { try { + // 1. Clear stores const currentStore = await getStore(); currentStore.clear(); if (scrumStore && typeof scrumStore.clear === "function") { scrumStore.clear(); } - // Quit the application + // 2. Clear login settings (prevent auto-start after uninstall) + try { + app.setLoginItemSettings({ + openAtLogin: false, + path: app.getPath("exe"), + }); + } catch (e) { + console.warn("[Uninstall] Failed to clear login settings:", e); + } + + // 3. Clear session data (cookies, cache, etc.) + try { + const { session } = require("electron"); + await session.defaultSession.clearStorageData(); + await session.defaultSession.clearCache(); + } catch (e) { + console.warn("[Uninstall] Failed to clear session data:", e); + } + + // 4. Clear userData directory contents (best effort) + const userDataPath = app.getPath("userData"); + console.log(`[Uninstall] Clearing userData at: ${userDataPath}`); + + // We can't delete the folder itself easily while running, but we can try to delete contents + try { + const files = fs.readdirSync(userDataPath); + for (const file of files) { + const filePath = path.join(userDataPath, file); + try { + // Skip the log file or current process files if they might be locked + if (file === "logs" || file.includes("Singleton")) continue; + fs.rmSync(filePath, { recursive: true, force: true }); + } catch (e) { + console.warn(`[Uninstall] Could not remove ${file}:`, e.message); + } + } + } catch (e) { + console.error("[Uninstall] Failed to clear userData contents:", e); + } + + // 5. On macOS, try to move the app bundle to trash if packaged + if (process.platform === "darwin" && app.isPackaged) { + try { + // app.getPath('exe') is typically .../Next Gen Dev.app/Contents/MacOS/Next Gen Dev + const exePath = app.getPath("exe"); + const appBundlePath = exePath.replace( + /\.app\/Contents\/MacOS\/.*$/, + ".app" + ); + + if (appBundlePath.endsWith(".app")) { + console.log( + `[Uninstall] Moving app bundle to trash: ${appBundlePath}` + ); + await shell.trashItem(appBundlePath); + } + } catch (e) { + console.error("[Uninstall] Failed to move app to trash:", e); + } + } + + // 6. Quit the application isQuitting = true; app.quit(); return true; @@ -3912,7 +4145,9 @@ app.whenReady().then(async () => { // Initialize anti-detection IPC handlers antiDetection.initAntiDetectionIPC(); - ensureTray(); + if (!isPlaywrightRun) { + ensureTray(); + } // Auto-start MCP Server startMcpServer(); @@ -3988,14 +4223,19 @@ app.on("window-all-closed", () => { app.on("activate", () => { if (mainWindow === null) { + if (process.platform === "win32" && process.argv.length > 1) { + const url = process.argv.find( + (arg) => arg.startsWith("http://") || arg.startsWith("https://") + ); + if (url) { + openUrlWithTarget(url, true).catch(console.error); + } + } createWindow(); } }); -// Handle external links -ipcMain.on("open-external", (event, url) => { - shell.openExternal(url); -}); +// Handle external links removed - using ipcMain.handle("open-external") instead ipcMain.handle("run-e2e-test", async (event, { testFile, options = {} }) => { const { spawn } = require("child_process"); diff --git a/apps/ui/src/preload/index.js b/apps/ui/src/preload/index.js index 5f831ac..4b61fea 100644 --- a/apps/ui/src/preload/index.js +++ b/apps/ui/src/preload/index.js @@ -21,6 +21,12 @@ contextBridge.exposeInMainWorld("electronAPI", { setQuickToggleEnabled: (enabled) => ipcRenderer.invoke("set-quick-toggle-enabled", enabled), getQuickToggleShortcut: () => ipcRenderer.invoke("get-quick-toggle-shortcut"), + getExternalLinkTarget: () => ipcRenderer.invoke("get-external-link-target"), + setExternalLinkTarget: (value) => + ipcRenderer.invoke("set-external-link-target", value), + isDefaultBrowser: () => ipcRenderer.invoke("is-default-browser"), + setAsDefaultBrowser: (value) => + ipcRenderer.invoke("set-as-default-browser", value), getAppVisibility: () => ipcRenderer.invoke("get-app-visibility"), showApp: () => ipcRenderer.invoke("app-show-window"), hideApp: () => ipcRenderer.invoke("app-hide-window"), @@ -262,4 +268,9 @@ contextBridge.exposeInMainWorld("electronAPI", { // External links openExternal: (url) => ipcRenderer.invoke("open-external", url), + onOpenInAppBrowser: (callback) => { + const handler = (event, payload) => callback(payload); + ipcRenderer.on("open-in-app-browser", handler); + return () => ipcRenderer.removeListener("open-in-app-browser", handler); + }, }); diff --git a/apps/ui/src/renderer/index.html b/apps/ui/src/renderer/index.html index a11f345..4cd8983 100644 --- a/apps/ui/src/renderer/index.html +++ b/apps/ui/src/renderer/index.html @@ -3,7 +3,7 @@ - Next Gen Tools + Next Gen Dev
diff --git a/apps/ui/src/renderer/src/App.jsx b/apps/ui/src/renderer/src/App.jsx index 0838b15..e236728 100644 --- a/apps/ui/src/renderer/src/App.jsx +++ b/apps/ui/src/renderer/src/App.jsx @@ -116,6 +116,15 @@ function App() { localStorage.setItem("theme", designMode); }, [designMode, isDarkMode]); + useEffect(() => { + if (!window.electronAPI?.onOpenInAppBrowser) return; + return window.electronAPI.onOpenInAppBrowser(({ url } = {}) => { + const raw = String(url || "").trim(); + if (!raw) return; + window.location.hash = `#/browser?url=${encodeURIComponent(raw)}`; + }); + }, []); + return (
-
- +
+
-

+

{activeTab === "ui" ? "UI Builder" : activeTab === "resources" ? "Resources" : activeTab === "launchpad" ? "Launchpad" - : "Next Gen"} + : "Next Gen Dev"}

v1.0 @@ -1131,7 +1132,7 @@ export default function MainLayout({
diff --git a/apps/ui/src/renderer/src/lib/securityScore.js b/apps/ui/src/renderer/src/lib/securityScore.js new file mode 100644 index 0000000..d306f2d --- /dev/null +++ b/apps/ui/src/renderer/src/lib/securityScore.js @@ -0,0 +1,156 @@ +const PROTECTION_CHECK_DEFINITIONS = [ + { + id: "webrtc", + label: "WebRTC leak protection", + settingKey: "blockWebRTC", + }, + { + id: "canvas", + label: "Canvas fingerprint masking", + settingKey: "blockCanvasFingerprint", + }, + { + id: "audio", + label: "Audio fingerprint masking", + settingKey: "blockAudioFingerprint", + }, + { + id: "webgl", + label: "WebGL shielding", + settingKey: "blockWebGL", + }, + { + id: "fonts", + label: "Font enumeration blocking", + settingKey: "blockFonts", + }, + { + id: "geo", + label: "Geolocation guarding", + settingKey: "blockGeolocation", + }, +]; + +const buildProtectionChecks = (profile) => + PROTECTION_CHECK_DEFINITIONS.map((check) => ({ + id: check.id, + label: check.label, + enabled: Boolean(profile?.settings?.[check.settingKey]), + })); + +const getScoreTone = (normalizedScore) => { + if (normalizedScore >= 80) return "bg-emerald-500"; + if (normalizedScore >= 60) return "bg-amber-500"; + return "bg-rose-500"; +}; + +const getScoreLabel = (normalizedScore) => { + if (normalizedScore >= 80) return "High"; + if (normalizedScore >= 60) return "Medium"; + return "Low"; +}; + +export const getSecurityScore = ({ + profiles = [], + proxies = [], + activeProfileId = null, +} = {}) => { + const stats = { + proxies: proxies.length, + activeProxies: proxies.filter((proxy) => proxy.status === "active").length, + profiles: profiles.length, + runningProfiles: profiles.filter((profile) => profile.status === "running") + .length, + }; + + const activeProfile = profiles.find( + (profile) => profile.id === activeProfileId + ); + const activeProxy = proxies.find( + (proxy) => proxy.id === activeProfile?.proxyId + ); + + const protectionChecks = buildProtectionChecks(activeProfile); + const protectionScore = protectionChecks.filter((item) => item.enabled).length; + const rawScore = + 30 + + (stats.profiles > 0 ? 10 : 0) + + (activeProfileId ? 10 : 0) + + (stats.runningProfiles > 0 ? 10 : 0) + + (activeProxy?.status === "active" ? 15 : 0) + + protectionScore * 5; + const normalizedScore = Math.min(Math.max(rawScore, 0), 100); + const checklistItems = [ + { + id: "profiles", + label: "At least one browser profile is configured.", + done: stats.profiles > 0, + doneLabel: "Done", + pendingLabel: "Pending", + }, + { + id: "proxy", + label: "Attach a live proxy for session routing.", + done: stats.activeProxies > 0, + doneLabel: "Active", + pendingLabel: "Pending", + }, + { + id: "running", + label: "Start a profile to activate protections.", + done: stats.runningProfiles > 0, + doneLabel: "Running", + pendingLabel: "Pending", + }, + { + id: "active-profile", + label: "Confirm an active profile is selected.", + done: Boolean(activeProfileId), + doneLabel: "Selected", + pendingLabel: "Pending", + }, + ]; + const improvementItems = [ + { + id: "activate-profile", + label: "Activate a browser profile for protection.", + done: Boolean(activeProfileId), + doneLabel: "Done", + pendingLabel: "Pending", + }, + { + id: "active-proxy", + label: "Assign a working proxy to the active profile.", + done: activeProxy?.status === "active", + doneLabel: "Active", + pendingLabel: "Pending", + }, + { + id: "run-profile", + label: "Start at least one profile session.", + done: stats.runningProfiles > 0, + doneLabel: "Running", + pendingLabel: "Pending", + }, + { + id: "enable-protections", + label: "Enable all fingerprint protections.", + done: protectionScore === protectionChecks.length, + doneLabel: "Complete", + pendingLabel: "Pending", + }, + ]; + + return { + stats, + activeProfile, + activeProxy, + protectionChecks, + protectionScore, + normalizedScore, + checklistItems, + improvementItems, + scoreLabel: getScoreLabel(normalizedScore), + scoreTone: getScoreTone(normalizedScore), + }; +}; diff --git a/apps/ui/src/renderer/src/stores/browserTabsStore.js b/apps/ui/src/renderer/src/stores/browserTabsStore.js index 85c5543..f802281 100644 --- a/apps/ui/src/renderer/src/stores/browserTabsStore.js +++ b/apps/ui/src/renderer/src/stores/browserTabsStore.js @@ -106,6 +106,7 @@ export const useBrowserTabsStore = create( url: trimmed, canGoBack: false, canGoForward: false, + isLoading: true, }, }, })); diff --git a/apps/ui/src/renderer/src/views/AntiBrowserView.jsx b/apps/ui/src/renderer/src/views/AntiBrowserView.jsx index c58af85..c06262f 100644 --- a/apps/ui/src/renderer/src/views/AntiBrowserView.jsx +++ b/apps/ui/src/renderer/src/views/AntiBrowserView.jsx @@ -1,9 +1,17 @@ -import { Globe, Network, Settings2, Shield, Users } from "lucide-react"; -import React, { useCallback, useState } from "react"; +import { Globe, Network, Settings2, Shield, ShieldCheck, Users } from "lucide-react"; +import React, { useCallback, useEffect, useState } from "react"; import { toast } from "sonner"; import BrowserProfilesPanel from "../components/BrowserProfilesPanel"; import ProxyManagementPanel from "../components/ProxyManagementPanel"; +import { Badge } from "../components/ui/badge"; import { Button } from "../components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "../components/ui/card"; import { Tabs, TabsContent, @@ -17,6 +25,7 @@ import { TooltipTrigger, } from "../components/ui/tooltip"; import { cn } from "../lib/utils"; +import { getSecurityScore } from "../lib/securityScore"; import { useBrowserProfileStore } from "../stores/browserProfileStore"; import { useBrowserTabsStore } from "../stores/browserTabsStore"; import { useProxyStore } from "../stores/proxyStore"; @@ -34,6 +43,13 @@ export function AntiBrowserView({ children }) { const proxies = useProxyStore((s) => s.proxies); const openUrlTab = useBrowserTabsStore((s) => s.openUrlTab); + useEffect(() => { + if (activeMainTab === "browser") return; + if (window.electronAPI?.browserView?.hideAll) { + window.electronAPI.browserView.hideAll().catch(() => {}); + } + }, [activeMainTab]); + const handleProxySelect = useCallback((id) => { setSelectedProxyId(id); }, []); @@ -62,12 +78,17 @@ export function AntiBrowserView({ children }) { [openUrlTab, profiles, setActiveProfile] ); - const stats = { - proxies: proxies.length, - activeProxies: proxies.filter((p) => p.status === "active").length, - profiles: profiles.length, - runningProfiles: profiles.filter((p) => p.status === "running").length, - }; + const { + stats, + activeProxy, + protectionChecks, + protectionScore, + checklistItems, + improvementItems, + normalizedScore, + scoreLabel, + scoreTone, + } = getSecurityScore({ profiles, proxies, activeProfileId }); return (
@@ -133,6 +154,14 @@ export function AntiBrowserView({ children }) { )} + + + Security Score + Score + @@ -203,6 +232,201 @@ export function AntiBrowserView({ children }) { {activeMainTab === "browser" && (
{children}
)} + + {activeMainTab === "score" && ( +
+
+ + +
+
+ + Anti-Browser Security Score + + + Confidence indicator for your current protection setup. + +
+ + {scoreLabel} Trust + +
+
+ +
+
+

+ {normalizedScore} +

+

+ of 100 secure points +

+
+
+

Based on proxy routing and profile protections.

+

Enable more safeguards to improve trust.

+
+
+
+
+
+
+
+ Profiles configured + + {stats.profiles} + +
+
+ Active proxies + + {stats.activeProxies} + +
+
+ Running profiles + + {stats.runningProfiles} + +
+
+ Active profile + + {activeProfileId ? "Selected" : "None"} + +
+
+ Protection checks enabled + + {protectionScore}/{protectionChecks.length} + +
+
+ Active proxy status + + {activeProxy?.status || "Not set"} + +
+
+ + +
+ + + Network identity + + Basic IP and routing details for the active profile. + + + +
+ Exit IP / Host + + {activeProxy?.host || "Not set"} + +
+
+ Proxy type + + {activeProxy?.type || "Not set"} + +
+
+ Location + + {activeProxy?.country || activeProxy?.city + ? `${activeProxy?.city || "Unknown"}, ${ + activeProxy?.country || "Unknown" + }` + : "Unknown"} + +
+
+ ISP + + {activeProxy?.isp || "Unknown"} + +
+
+
+ + + Protection coverage + + Shows which anti-fingerprint checks are active. + + + +
    + {protectionChecks.map((check) => ( +
  • + {check.label} + + {check.enabled ? "Enabled" : "Disabled"} + +
  • + ))} +
+
+
+
+
+
+ + + Next improvements + + Follow these steps to raise your security score. + + + +
    + {improvementItems.map((item) => ( +
  • + {item.label} + + {item.done ? item.doneLabel : item.pendingLabel} + +
  • + ))} +
+
+
+ + + Score checklist + + Improve trust by completing these safeguards. + + + +
    + {checklistItems.map((item) => ( +
  • + {item.label} + + {item.done ? item.doneLabel : item.pendingLabel} + +
  • + ))} +
+
+
+
+
+ )}
); diff --git a/apps/ui/src/renderer/src/views/BrowserToolView.jsx b/apps/ui/src/renderer/src/views/BrowserToolView.jsx index 83b7feb..9e7480a 100644 --- a/apps/ui/src/renderer/src/views/BrowserToolView.jsx +++ b/apps/ui/src/renderer/src/views/BrowserToolView.jsx @@ -8,6 +8,7 @@ import { FileJson, Globe, Image, + Loader2, Maximize2, Minimize2, Monitor, @@ -69,8 +70,8 @@ import { TooltipTrigger, } from "../components/ui/tooltip"; import { cn } from "../lib/utils"; -import { useBrowserTabsStore } from "../stores/browserTabsStore"; import { useBrowserProfileStore } from "../stores/browserProfileStore"; +import { useBrowserTabsStore } from "../stores/browserTabsStore"; import { useProxyStore } from "../stores/proxyStore"; import { useResourceStore } from "../stores/resourceStore"; import { copyToClipboard, generateElementCode } from "../utils/codeGenerator"; @@ -549,6 +550,56 @@ function Dashboard({ onOpenUrl }) { group: "Docs", }, { title: "Lucide Icons", url: "https://lucide.dev", group: "Docs" }, + { + title: "ChatGPT", + url: "https://chatgpt.com", + group: "AI", + }, + { + title: "Claude", + url: "https://claude.ai", + group: "AI", + }, + { + title: "Gemini", + url: "https://gemini.google.com", + group: "AI", + }, + { + title: "Perplexity", + url: "https://www.perplexity.ai", + group: "AI", + }, + { + title: "Grok", + url: "https://grok.com", + group: "AI", + }, + { + title: "DeepSeek", + url: "https://chat.deepseek.com", + group: "AI", + }, + { + title: "Qwen", + url: "https://chat.qwen.ai", + group: "AI", + }, + { + title: "Kimi K2", + url: "https://kimi.moonshot.cn", + group: "AI", + }, + { + title: "Mistral", + url: "https://chat.mistral.ai", + group: "AI", + }, + { + title: "Doubao", + url: "https://www.doubao.com", + group: "AI", + }, { title: "Shadcn Chat", url: "https://context7.com/websites/ui_shadcn?tab=chat", @@ -646,9 +697,10 @@ function Dashboard({ onOpenUrl }) { (it) => it.group === "Entertainment" ); const musicItems = filtered.filter((it) => it.group === "Music"); + const aiItems = filtered.filter((it) => it.group === "AI"); const docsItems = filtered.filter((it) => it.group === "Docs"); const moreItems = filtered.filter( - (it) => !["Entertainment", "Music", "Docs"].includes(it.group) + (it) => !["Entertainment", "Music", "AI", "Docs"].includes(it.group) ); return ( @@ -682,6 +734,7 @@ function Dashboard({ onOpenUrl }) { Docs Entertainment Music + AI More @@ -718,6 +771,23 @@ function Dashboard({ onOpenUrl }) { ))}
+ +
+ {aiItems.map((it) => ( + + ))} +
+
{docsItems.map((it) => ( @@ -2326,6 +2396,7 @@ export default function BrowserToolView() { url: payload.url, canGoBack: payload.canGoBack, canGoForward: payload.canGoForward, + isLoading: payload.isLoading, }); if (payload.url) addHistoryEntry(payload.url, payload.url); }); @@ -2457,7 +2528,7 @@ export default function BrowserToolView() { if (!activeIsBrowser) return; try { - updateTabState(resolvedActiveTabId, { url }); + updateTabState(resolvedActiveTabId, { url, isLoading: true }); addHistoryEntry(url, url); if (hasElectronView) { @@ -2503,6 +2574,9 @@ export default function BrowserToolView() { > {tabs.map((t, idx) => { const isActive = t.id === resolvedActiveTabId; + const tState = tabStateById[t.id] || {}; + const isLoading = tState.isLoading; + return (
+ {isLoading && ( + + )} {t.title} @@ -2636,12 +2713,18 @@ export default function BrowserToolView() { return; } if (iframeRef.current) { + updateTabState(resolvedActiveTabId, { isLoading: true }); iframeRef.current.contentWindow?.location?.reload?.(); } }} aria-label="Reload" > - +
@@ -2742,12 +2825,25 @@ export default function BrowserToolView() { {activeTab?.kind === "browser" ? (
+ {activeTabState.isLoading && ( +
+
+ + + Loading... + +
+
+ )} {!hasElectronView ? (