├── .gitignore ├── chat.png ├── .DS_Store ├── assets ├── icon.ico ├── icon.png └── icon.icns ├── settings.json ├── .github └── workflows │ ├── main.yml │ ├── flow1 │ └── static.yml ├── preload.js ├── README.md ├── Index.html ├── package.json ├── forge.config.js ├── main.js ├── home.html ├── styles.css └── script.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | 3 | /out -------------------------------------------------------------------------------- /chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TopMyster/IsleBrowser/HEAD/chat.png -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TopMyster/IsleBrowser/HEAD/.DS_Store -------------------------------------------------------------------------------- /assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TopMyster/IsleBrowser/HEAD/assets/icon.ico -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TopMyster/IsleBrowser/HEAD/assets/icon.png -------------------------------------------------------------------------------- /assets/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TopMyster/IsleBrowser/HEAD/assets/icon.icns -------------------------------------------------------------------------------- /settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "searchEngine": "Bing", 3 | "theme": "light", 4 | "homepage": "home.html", 5 | "chatbot": "default", 6 | "version": "1.0.0" 7 | } 8 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | workflow_dispatch: 7 | jobs: 8 | publish: 9 | permissions: 10 | contents: write 11 | strategy: 12 | matrix: 13 | os: [windows-latest, macos-latest] 14 | runs-on: ${{ matrix.os }} 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: 18 20 | - name: Generate short SHA 21 | id: short-sha 22 | run: echo "sha=${GITHUB_SHA:0:5}" >> $GITHUB_OUTPUT 23 | - name: Install dependencies 24 | run: npm install 25 | - name: Publish with Electron Forge 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | run: npm run forge-publish 29 | - name: Create GitHub Release 30 | uses: actions/create-release@v1 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | with: 34 | tag_name: ${{ steps.short-sha.outputs.sha }} 35 | release_name: Release ${{ github.ref_name }} 36 | draft: false 37 | -------------------------------------------------------------------------------- /preload.js: -------------------------------------------------------------------------------- 1 | const { contextBridge, ipcRenderer } = require('electron'); 2 | 3 | contextBridge.exposeInMainWorld('electronAPI', { 4 | navigateTo: (url) => ipcRenderer.invoke('navigate-to', url), 5 | goBack: () => ipcRenderer.invoke('go-back'), 6 | goForward: () => ipcRenderer.invoke('go-forward'), 7 | showBrowserView: (show) => ipcRenderer.invoke('show-browserview', show), 8 | // Context menu functions 9 | openContextMenu: (x, y, options) => ipcRenderer.invoke('show-context-menu', x, y, options), 10 | // Engine selection communication 11 | setEngine: (engineName) => { 12 | localStorage.setItem('selectedEngine', engineName); 13 | // Communicate with parent window 14 | if (window.parent && window.parent !== window) { 15 | window.parent.postMessage({ type: 'ENGINE_CHANGED', engine: engineName }, '*'); 16 | } 17 | }, 18 | getEngine: () => { 19 | return localStorage.getItem('selectedEngine') || 'Bing'; 20 | }, 21 | // Session persistence 22 | saveBrowserState: (state) => ipcRenderer.invoke('save-browser-state', state), 23 | loadBrowserState: () => ipcRenderer.invoke('load-browser-state'), 24 | // Webview reload 25 | reloadWebview: () => ipcRenderer.send('reload-webview') 26 | }); 27 | -------------------------------------------------------------------------------- /.github/workflows/flow1: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the "main" branch 8 | push: 9 | branches: [ "main" ] 10 | pull_request: 11 | branches: [ "main" ] 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 17 | jobs: 18 | # This workflow contains a single job called "build" 19 | build: 20 | # The type of runner that the job will run on 21 | runs-on: ubuntu-latest 22 | 23 | # Steps represent a sequence of tasks that will be executed as part of the job 24 | steps: 25 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 26 | - uses: actions/checkout@v4 27 | 28 | # Runs a single command using the runners shell 29 | - name: Run a one-line script 30 | run: echo Hello, world! 31 | 32 | # Runs a set of commands using the runners shell 33 | - name: Run a multi-line script 34 | run: | 35 | echo Add other actions to build, 36 | echo test, and deploy your project. 37 | -------------------------------------------------------------------------------- /.github/workflows/static.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["main"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: false 23 | 24 | jobs: 25 | # Single deploy job since we're just deploying 26 | deploy: 27 | environment: 28 | name: github-pages 29 | url: ${{ steps.deployment.outputs.page_url }} 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | - name: Setup Pages 35 | uses: actions/configure-pages@v5 36 | - name: Upload artifact 37 | uses: actions/upload-pages-artifact@v3 38 | with: 39 | # Upload entire repository 40 | path: '.' 41 | - name: Deploy to GitHub Pages 42 | id: deployment 43 | uses: actions/deploy-pages@v4 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Isle Browser prioritizes the websites you visit over controls and tab viewing, while still keeping those features accessible. Keep in mind, Isle Browser Nova is still a work in progress and is nearly twice as fast as the original Isle Browser. ENJOY!😁 2 | 3 | Screenshots (For a more news go to https://www.reddit.com/r/IsleBrowser/ and subsribe to https://www.youtube.com/@Islebrowser) 4 | 5 | Screenshot 2025-07-21 at 11 24 15 AM 6 | 7 | Screenshot 2025-08-01 at 10 24 59 PM 8 | 9 | Screenshot 2025-08-01 at 10 26 09 PM 10 | 11 | Screenshot 2025-07-21 at 11 27 54 AM 12 | 13 | Isle Browser Nova is the non vibe coded version of the browser. It is better designed and more well maintained. Please do not shame or mock my creation in the issues bar. The bar is only to be used for an issue with the software. Isle browser Nova is created by me alone and I would prefer if people won't mock it. I only vibe coded the original isle browser as an experimental browser to see how efficient and well ai could code. 14 | -------------------------------------------------------------------------------- /Index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Isle Browser 5 | 6 | 7 | 8 | 9 |
10 |
11 |
12 | + 13 | < 14 | > 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |
23 | 24 | 25 | 26 |
27 | 28 |
29 | 31 | 32 |
33 | 34 |
35 |
36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "isle-nova", 3 | "version": "1.0.0", 4 | "description": "A new innovative browser", 5 | "author": "TopMyster", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/TopMyster/IsleBrowser" 9 | }, 10 | "main": "main.js", 11 | "scripts": { 12 | "start": "electron-forge start", 13 | "package": "electron-forge package", 14 | "make": "electron-forge make", 15 | "forge-publish": "electron-forge publish", 16 | "build-all": "node build-all.js", 17 | "build-optimized": "node build-optimized.js", 18 | "build-windows-200mb": "node build-windows-200mb.cjs", 19 | "build-windows-simple": "node build-windows-simple.cjs", 20 | "build-debian-200mb": "node build-debian-200mb.cjs", 21 | "build-macos-200mb": "node build-macos-200mb.cjs", 22 | "build-all-functional": "node build-all-functional.cjs", 23 | "build-all-200mb": "node build-all-optimized.js", 24 | "build-minimal": "node build-minimal.js", 25 | "build-cross-platform": "node build-cross-platform.js", 26 | "build-simple": "node build-simple.js", 27 | "build-all-platforms-200mb": "node build-all-platforms.js", 28 | "clean": "rm -rf out dist temp-build temp-optimize dist-minimal dist-optimized temp-cross-build temp-simple dist-simple electron-builder.json" 29 | }, 30 | "devDependencies": { 31 | "@electron-forge/cli": "^7.8.1", 32 | "@electron-forge/maker-deb": "^7.8.1", 33 | "@electron-forge/maker-rpm": "^7.8.1", 34 | "@electron-forge/maker-squirrel": "^7.8.1", 35 | "@electron-forge/maker-zip": "^7.8.1", 36 | "@electron-forge/plugin-auto-unpack-natives": "^7.8.1", 37 | "@electron-forge/plugin-fuses": "^7.8.1", 38 | "@electron-forge/publisher-electron-release-server": "^7.8.1", 39 | "@electron/fuses": "^1.8.0", 40 | "electron": "^30.0.0", 41 | "electron-builder": "^26.0.12", 42 | "electron-packager": "^17.1.2" 43 | }, 44 | "dependencies": { 45 | "electron-is-dev": "^3.0.1", 46 | "update-electron-app": "^3.1.1" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /forge.config.js: -------------------------------------------------------------------------------- 1 | const { FusesPlugin } = require('@electron-forge/plugin-fuses'); 2 | const { FuseV1Options, FuseVersion } = require('@electron/fuses'); 3 | 4 | // Get the target platform from command line args or use current platform 5 | const getTargetPlatform = () => { 6 | const args = process.argv; 7 | const platformIndex = args.findIndex(arg => arg === '--platform'); 8 | return platformIndex !== -1 && args[platformIndex + 1] ? args[platformIndex + 1] : process.platform; 9 | }; 10 | 11 | const targetPlatform = getTargetPlatform(); 12 | 13 | module.exports = { 14 | packagerConfig: { 15 | asar: true, 16 | name: 'Isle Nova', 17 | executableName: 'Isle Nova', 18 | icon: './assets/icon', 19 | appBundleId: 'com.topmyster.islenovabrowser', 20 | appCategoryType: 'public.app-category.productivity', 21 | osxSign: false, 22 | osxNotarize: false, 23 | // Size optimization settings - being more conservative to avoid breaking dependencies 24 | ignore: [ 25 | // Test directories and files 26 | /\/node_modules\/.*\/test\//, 27 | /\/node_modules\/.*\/tests\//, 28 | /\/node_modules\/.*\/__tests__\//, 29 | /\/node_modules\/.*\/.*\.spec\.(js|ts|jsx|tsx)$/, 30 | /\/node_modules\/.*\/.*\.test\.(js|ts|jsx|tsx)$/, 31 | 32 | // Documentation and examples 33 | /\/node_modules\/.*\/example\//, 34 | /\/node_modules\/.*\/examples\//, 35 | /\/node_modules\/.*\/docs?\//, 36 | /\/node_modules\/.*\/demo\//, 37 | /\/node_modules\/.*\/demos\//, 38 | 39 | // VCS files 40 | /\/node_modules\/.*\/.git\//, 41 | /\/node_modules\/.*\/\.travis\.yml$/, 42 | 43 | // Documentation files 44 | /\/node_modules\/.*\/README\.(md|txt|rst)$/i, 45 | /\/node_modules\/.*\/CHANGELOG\.(md|txt|rst)$/i, 46 | /\/node_modules\/.*\/LICENSE/i, 47 | 48 | // Source maps 49 | /\/node_modules\/.*\/.*\.map$/ 50 | ], 51 | // Include only English locale to save space 52 | afterCopy: [(buildPath, electronVersion, platform, arch, callback) => { 53 | const fs = require('fs'); 54 | const path = require('path'); 55 | 56 | const localesPath = path.join(buildPath, 'locales'); 57 | if (fs.existsSync(localesPath)) { 58 | const files = fs.readdirSync(localesPath); 59 | files.forEach(file => { 60 | if (!file.startsWith('en-US') && file.endsWith('.pak')) { 61 | try { 62 | fs.unlinkSync(path.join(localesPath, file)); 63 | } catch (e) { 64 | // Ignore errors 65 | } 66 | } 67 | }); 68 | } 69 | callback(); 70 | }], 71 | win32metadata: { 72 | CompanyName: 'TopMyster', 73 | FileDescription: 'Isle Nova Browser', 74 | OriginalFilename: 'Isle Nova.exe', 75 | ProductName: 'Isle Nova', 76 | InternalName: 'Isle Nova' 77 | } 78 | }, 79 | rebuildConfig: {}, 80 | makers: [ 81 | // Windows x64 82 | { 83 | name: '@electron-forge/maker-zip', 84 | config: { 85 | name: 'Isle.Nova-win32-x64' 86 | }, 87 | platforms: ['win32'], 88 | arch: ['x64'] 89 | }, 90 | // Windows ARM64 91 | { 92 | name: '@electron-forge/maker-zip', 93 | config: { 94 | name: 'Isle.Nova-win32-arm64' 95 | }, 96 | platforms: ['win32'], 97 | arch: ['arm64'] 98 | }, 99 | // macOS Intel (x64) 100 | { 101 | name: '@electron-forge/maker-zip', 102 | config: { 103 | name: 'Isle.Nova-macOS-intel' 104 | }, 105 | platforms: ['darwin'], 106 | arch: ['x64'] 107 | }, 108 | // macOS ARM64 109 | { 110 | name: '@electron-forge/maker-zip', 111 | config: { 112 | name: 'Isle.Nova-macOS-arm64' 113 | }, 114 | platforms: ['darwin'], 115 | arch: ['arm64'] 116 | }, 117 | // Linux DEB package 118 | { 119 | name: '@electron-forge/maker-deb', 120 | config: { 121 | name: 'Isle-nova-1.0.0-amd64.deb', 122 | options: { 123 | bin: 'Isle Nova', 124 | maintainer: 'TopMyster', 125 | homepage: 'https://github.com/TopMyster/IsleBrowser', 126 | description: 'A modern, innovative browser built with Electron', 127 | categories: ['Network', 'WebBrowser'] 128 | } 129 | }, 130 | platforms: ['linux'], 131 | arch: ['x64'] 132 | }, 133 | // Linux ZIP 134 | { 135 | name: '@electron-forge/maker-zip', 136 | config: { 137 | name: 'Isle-nova-1.0.0-linux-x64' 138 | }, 139 | platforms: ['linux'], 140 | arch: ['x64'] 141 | } 142 | ], 143 | plugins: [ 144 | { 145 | name: '@electron-forge/plugin-auto-unpack-natives', 146 | config: {}, 147 | }, 148 | 149 | new FusesPlugin({ 150 | version: FuseVersion.V1, 151 | [FuseV1Options.RunAsNode]: false, 152 | [FuseV1Options.EnableCookieEncryption]: true, 153 | [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false, 154 | [FuseV1Options.EnableNodeCliInspectArguments]: false, 155 | [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true, 156 | [FuseV1Options.OnlyLoadAppFromAsar]: true, 157 | }), 158 | ], 159 | }; 160 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow, ipcMain, globalShortcut, Menu, dialog, shell } = require('electron'); 2 | const { join } = require('path'); 3 | const { writeFileSync, readFileSync } = require('fs'); 4 | const { updateElectronApp } = require('update-electron-app'); 5 | 6 | // __dirname is available in CommonJS 7 | // Check if we're in development mode 8 | const isDev = process.env.NODE_ENV === 'development' || process.defaultApp || /[\\|/]electron-prebuilt[\\|/]/.test(process.execPath) || /[\\|/]electron[\\|/]/.test(process.execPath); 9 | 10 | if (!isDev) { 11 | updateElectronApp(); 12 | } 13 | 14 | // Handle Squirrel startup events on Windows 15 | if (process.platform === 'win32') { 16 | const squirrelCommand = process.argv[1]; 17 | if (squirrelCommand) { 18 | switch (squirrelCommand) { 19 | case '--squirrel-install': 20 | case '--squirrel-updated': 21 | case '--squirrel-uninstall': 22 | case '--squirrel-obsolete': 23 | app.quit(); 24 | return; 25 | } 26 | } 27 | } 28 | 29 | function getIconPath() { 30 | // Use appropriate icon format for each platform 31 | if (process.platform === 'win32') { 32 | return join(__dirname, 'assets', 'icon.ico'); // Windows prefers .ico 33 | } else if (process.platform === 'darwin') { 34 | return join(__dirname, 'assets', 'icon.icns'); // macOS uses .icns 35 | } else { 36 | return join(__dirname, 'assets', 'icon.png'); // Linux uses .png 37 | } 38 | } 39 | 40 | function createWindow () { 41 | const win = new BrowserWindow({ 42 | width: 1400, 43 | height: 850, 44 | transparent: true, // Temporarily disabled for testing 45 | frame: false, // Temporarily enabled for testing 46 | show: false, // Don't show until ready 47 | icon: getIconPath(), 48 | vibrancy: 'ultra-dark', 49 | blur: 50, 50 | backgroundColor: 'rgba(211, 211, 211, 0.65)', 51 | webPreferences: { 52 | nodeIntegration: true, 53 | contextIsolation: false, 54 | webviewTag: true, 55 | preload: join(__dirname, 'preload.js') 56 | } 57 | }); 58 | 59 | // Show window when ready to prevent white flash 60 | win.once('ready-to-show', () => { 61 | win.show(); 62 | }); 63 | 64 | win.loadFile('index.html'); 65 | } 66 | 67 | app.whenReady().then(() => { 68 | createWindow(); 69 | 70 | // Register global shortcuts 71 | // Cmd+L: Toggle chat panel 72 | globalShortcut.register('CommandOrControl+L', () => { 73 | const focusedWindow = BrowserWindow.getFocusedWindow(); 74 | if (focusedWindow) { 75 | console.log('Global Cmd+L shortcut triggered'); 76 | focusedWindow.webContents.send('toggle-chat-panel'); 77 | } 78 | }); 79 | 80 | // Cmd+K: Toggle tabbar 81 | globalShortcut.register('CommandOrControl+K', () => { 82 | const focusedWindow = BrowserWindow.getFocusedWindow(); 83 | if (focusedWindow) { 84 | console.log('Global Cmd+K shortcut triggered'); 85 | focusedWindow.webContents.send('toggle-tabbar'); 86 | } 87 | }); 88 | }); 89 | 90 | // IPC handlers for browser functionality 91 | ipcMain.handle('navigate-to', async (event, url) => { 92 | // Handle navigation logic here 93 | console.log('Navigate to:', url); 94 | }); 95 | 96 | ipcMain.handle('go-back', async (event) => { 97 | // Handle back navigation 98 | console.log('Go back'); 99 | }); 100 | 101 | ipcMain.handle('go-forward', async (event) => { 102 | // Handle forward navigation 103 | console.log('Go forward'); 104 | }); 105 | 106 | ipcMain.handle('show-browserview', async (event, show) => { 107 | console.log('Show browser view:', show); 108 | }); 109 | 110 | // Global variable to store context menu info 111 | let contextMenuInfo = { linkURL: '', srcURL: '', selectionText: '' }; 112 | 113 | // Session persistence handlers 114 | ipcMain.handle('save-browser-state', async (event, state) => { 115 | try { 116 | const sessionPath = join(__dirname, 'session.json'); 117 | writeFileSync(sessionPath, JSON.stringify(state, null, 2)); 118 | return { success: true }; 119 | } catch (error) { 120 | console.error('Failed to save session:', error); 121 | return { success: false, error: error.message }; 122 | } 123 | }); 124 | 125 | ipcMain.handle('load-browser-state', async (event) => { 126 | try { 127 | const sessionPath = join(__dirname, 'session.json'); 128 | const data = readFileSync(sessionPath, 'utf8'); 129 | return { success: true, data: JSON.parse(data) }; 130 | } catch (error) { 131 | console.log('No session file found or failed to load:', error.message); 132 | return { success: false, error: error.message }; 133 | } 134 | }); 135 | 136 | // Webview reload handler 137 | ipcMain.on('reload-webview', (event) => { 138 | event.sender.send('reload-webview-response'); 139 | }); 140 | 141 | // Function to create new browser window 142 | function createNewBrowserWindow(url) { 143 | const newWin = new BrowserWindow({ 144 | width: 1200, 145 | height: 800, 146 | icon: getIconPath(), 147 | webPreferences: { 148 | nodeIntegration: true, 149 | contextIsolation: false, 150 | webviewTag: true, 151 | preload: join(__dirname, 'preload.js') 152 | } 153 | }); 154 | 155 | newWin.loadFile('index.html'); 156 | 157 | if (url) { 158 | newWin.webContents.once('dom-ready', () => { 159 | newWin.webContents.send('navigate-to-url', url); 160 | }); 161 | } 162 | } 163 | 164 | app.on('window-all-closed', function () { 165 | if (process.platform !== 'darwin') app.quit(); 166 | }); 167 | 168 | app.on('will-quit', () => { 169 | // Unregister all shortcuts 170 | globalShortcut.unregisterAll(); 171 | }); 172 | -------------------------------------------------------------------------------- /home.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 |
6 | 7 |

Settings

8 |

______________________________________________

9 | 10 | 18 |
19 | 20 | 24 |

25 | 26 | 32 |
33 |
34 |

Isle

35 |

The browser you deserve

36 | ⚙︎ 37 | 38 |
39 |
40 | 41 | 130 | 131 | 132 |
133 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | :root { 2 | /* Light theme variables */ 3 | --bg-primary: rgba(250, 250, 250, 0.412); 4 | --bg-secondary: rgba(255, 255, 255, 0.655); 5 | --bg-tertiary: rgba(255, 255, 255, 0.649); 6 | --bg-input: rgba(237, 237, 237, 0.411); 7 | --bg-tab-item: rgba(255, 255, 255, 0.6); 8 | --bg-tab-hover: rgba(255, 255, 255, 0.8); 9 | --bg-button: rgba(255, 255, 255, 0.558); 10 | --bg-button-hover: rgba(22, 177, 255, 0.402); 11 | --bg-settings: rgba(255, 255, 255, 0.793); 12 | --bg-chat: rgba(255, 255, 255, 0.95); 13 | 14 | --text-primary: #000000; 15 | --text-secondary: #333333; 16 | --text-tertiary: #666666; 17 | --text-link: #515151; 18 | --text-link-hover: #333333; 19 | 20 | --border-light: rgba(206, 206, 206, 0.632); 21 | --border-medium: rgba(206, 206, 206, 0.884); 22 | --border-chat: rgba(0, 0, 0, 0.1); 23 | 24 | --shadow-light: 2px 2px 20px rgba(206, 206, 206, 0.632); 25 | --shadow-medium: 2px 2px 20px rgba(206, 206, 206, 0.884); 26 | --shadow-chat: -2px 0 20px rgba(0, 0, 0, 0.1); 27 | 28 | --accent-primary: rgba(22, 177, 255, 0.3); 29 | --accent-border: rgba(22, 177, 255, 0.5); 30 | --error-bg: rgba(255, 0, 0, 0.1); 31 | --error-bg-hover: rgba(255, 0, 0, 0.2); 32 | --error-text: #ff4444; 33 | } 34 | 35 | .dark-theme { 36 | /* Dark theme variables */ 37 | --bg-primary: rgba(18, 18, 18, 0.95); 38 | --bg-secondary: rgba(32, 32, 32, 0.9); 39 | --bg-tertiary: rgba(28, 28, 28, 0.92); 40 | --bg-input: rgba(45, 45, 45, 0.9); 41 | --bg-tab-item: rgba(40, 40, 40, 0.8); 42 | --bg-tab-hover: rgba(50, 50, 50, 0.9); 43 | --bg-button: rgba(45, 45, 45, 0.8); 44 | --bg-button-hover: rgba(22, 177, 255, 0.3); 45 | --bg-settings: rgba(25, 25, 25, 0.95); 46 | --bg-chat: rgba(20, 20, 20, 0.95); 47 | 48 | --text-primary: #ffffff; 49 | --text-secondary: #e0e0e0; 50 | --text-tertiary: #b0b0b0; 51 | --text-link: #c0c0c0; 52 | --text-link-hover: #ffffff; 53 | 54 | --border-light: rgba(80, 80, 80, 0.5); 55 | --border-medium: rgba(60, 60, 60, 0.7); 56 | --border-chat: rgba(255, 255, 255, 0.1); 57 | 58 | --shadow-light: 2px 2px 20px rgba(0, 0, 0, 0.5); 59 | --shadow-medium: 2px 2px 20px rgba(0, 0, 0, 0.7); 60 | --shadow-chat: -2px 0 20px rgba(0, 0, 0, 0.8); 61 | 62 | --accent-primary: rgba(22, 177, 255, 0.4); 63 | --accent-border: rgba(22, 177, 255, 0.6); 64 | --error-bg: rgba(255, 50, 50, 0.2); 65 | --error-bg-hover: rgba(255, 50, 50, 0.3); 66 | --error-text: #ff6666; 67 | } 68 | 69 | body { 70 | margin: 0; 71 | text-align: center; 72 | position: relative; 73 | transition: all 0.3s ease; 74 | overflow: hidden; 75 | text-decoration: none; 76 | background-color: var(--bg-primary); 77 | backdrop-filter: blur(450px); 78 | color: var(--text-primary); 79 | } 80 | 81 | 82 | #tabbar { 83 | width: 470px; 84 | height: 45px; 85 | background-color: var(--bg-secondary); 86 | backdrop-filter: blur(2.5px); 87 | text-align: center; 88 | z-index: 200; 89 | position: fixed; 90 | border-radius: 20px; 91 | margin-bottom: 5px; 92 | bottom: 5px; 93 | box-shadow: var(--shadow-light); 94 | transition: all 0.3s ease; 95 | transform: translateX(-50%); 96 | left: 50%; 97 | display: block; 98 | user-select: none; 99 | color: var(--text-primary); 100 | } 101 | 102 | #searchbar { 103 | background-color: var(--bg-input); 104 | backdrop-filter: blur(50px); 105 | height: 33px; 106 | width: 200px; 107 | border-radius: 14px; 108 | padding: 0px 10px; 109 | position: relative; 110 | transition: all 0.3s ease; 111 | cursor: text; 112 | border: none; 113 | top: 5px; 114 | color: var(--text-primary); 115 | } 116 | 117 | #searchbar::placeholder { 118 | color: var(--text-tertiary); 119 | } 120 | 121 | #tabs{ 122 | display: none; 123 | width: 460px; 124 | height: 400px; 125 | max-height: 80vh; 126 | background-color: var(--bg-tertiary); 127 | backdrop-filter: blur(3.5px); 128 | z-index: 150; 129 | border-radius: 20px; 130 | position: absolute; 131 | bottom: 100px; 132 | right: 100px; 133 | box-shadow: var(--shadow-medium); 134 | transition: all 0.3s ease; 135 | cursor: default; 136 | overflow-y: auto; 137 | padding: 20px; 138 | box-sizing: border-box; 139 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 140 | color: var(--text-primary); 141 | } 142 | 143 | .tab-item { 144 | display: flex; 145 | align-items: center; 146 | justify-content: space-between; 147 | background-color: var(--bg-tab-item); 148 | margin: 8px 0; 149 | padding: 12px 15px; 150 | border-radius: 12px; 151 | transition: all 0.2s ease; 152 | cursor: pointer; 153 | border: 2px solid transparent; 154 | } 155 | 156 | .tab-item:hover { 157 | background-color: var(--bg-tab-hover); 158 | transform: translateX(5px); 159 | } 160 | 161 | .tab-item.active-tab { 162 | background-color: var(--accent-primary); 163 | border-color: var(--accent-border); 164 | } 165 | 166 | .tab-content { 167 | flex: 1; 168 | text-align: left; 169 | cursor: pointer; 170 | } 171 | 172 | .tab-title { 173 | display: block; 174 | font-weight: bold; 175 | font-size: 14px; 176 | margin-bottom: 4px; 177 | color: var(--text-secondary); 178 | } 179 | 180 | .tab-url { 181 | display: block; 182 | font-size: 11px; 183 | color: var(--text-tertiary); 184 | opacity: 0.8; 185 | overflow: hidden; 186 | text-overflow: ellipsis; 187 | white-space: nowrap; 188 | max-width: 350px; 189 | } 190 | 191 | .tab-close { 192 | background: var(--error-bg); 193 | border: none; 194 | width: 24px; 195 | height: 24px; 196 | border-radius: 50%; 197 | cursor: pointer; 198 | font-size: 16px; 199 | font-weight: bold; 200 | color: var(--error-text); 201 | display: flex; 202 | align-items: center; 203 | justify-content: center; 204 | transition: all 0.2s ease; 205 | margin-left: 10px; 206 | } 207 | 208 | .tab-close:hover:not(:disabled) { 209 | background: var(--error-bg-hover); 210 | transform: scale(1.1); 211 | } 212 | 213 | .tab-close:disabled { 214 | opacity: 0.3; 215 | cursor: not-allowed; 216 | } 217 | 218 | #tabbutton { 219 | border: none; 220 | background-color: var(--bg-button); 221 | font-weight: bold; 222 | font-size: 15px; 223 | padding: 5px 10px; 224 | border-radius: 10px; 225 | transition: all 0.3s ease; 226 | box-shadow: var(--shadow-medium); 227 | cursor: pointer; 228 | top: 5px; 229 | color: var(--text-primary); 230 | } 231 | 232 | #tabbutton:hover{ 233 | background-color: var(--bg-button-hover); 234 | transition: all 0.3s ease; 235 | } 236 | 237 | .navstuff { 238 | top: 3px; 239 | position: relative; 240 | } 241 | 242 | 243 | #Browser { 244 | position: relative; 245 | top: 0; 246 | left: 0; 247 | width: 100vw; 248 | height: 100vh; 249 | border: none; 250 | z-index: 1; 251 | transition: width 0.3s ease; 252 | } 253 | 254 | 255 | input:focus { 256 | outline: none; 257 | } 258 | 259 | #settings { 260 | display: none; 261 | width: 500px; 262 | height: 300px; 263 | background-color: var(--bg-settings); 264 | backdrop-filter: blur(3.5px); 265 | z-index: 150; 266 | border-radius: 20px; 267 | position: relative; 268 | text-align: center; 269 | box-shadow: var(--shadow-medium); 270 | transition: all 0.3s ease; 271 | cursor: pointer; 272 | color: var(--text-primary); 273 | } 274 | 275 | #drag { 276 | background-color: transparent; 277 | width: 200px; 278 | height: 10px; 279 | left: 100%; 280 | transform: translateX(-90%); 281 | -webkit-app-region: drag; 282 | z-index: 3000; 283 | position: absolute; 284 | } 285 | 286 | #bookmarks{ 287 | display: none; 288 | background-color: var(--bg-secondary); 289 | color: var(--text-primary); 290 | border-radius: 10px; 291 | padding: 5px; 292 | box-shadow: var(--shadow-medium); 293 | } 294 | 295 | a { 296 | text-decoration: none; 297 | color: var(--text-link); 298 | font-weight: bold; 299 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 300 | transition: color 0.2s ease; 301 | } 302 | 303 | a:hover { 304 | color: var(--text-link-hover); 305 | } 306 | 307 | #bmtext { 308 | background-color: var(--bg-input); 309 | border: 2px solid var(--text-tertiary); 310 | backdrop-filter: blur(5px); 311 | border-radius: 5px; 312 | width: 70px; 313 | color: var(--text-primary); 314 | transition: all 0.2s ease; 315 | } 316 | 317 | #bmlink { 318 | background-color: var(--bg-input); 319 | backdrop-filter: blur(5px); 320 | border: 2px solid var(--text-tertiary); 321 | border-radius: 5px; 322 | width: 120px; 323 | color: var(--text-primary); 324 | transition: all 0.2s ease; 325 | } 326 | 327 | #addbmsection { 328 | text-align: right; 329 | position: absolute; 330 | z-index: 200; 331 | } 332 | 333 | #addbmsection button { 334 | background-color: var(--bg-button-hover); 335 | color: var(--text-primary); 336 | border: none; 337 | border-radius: 5px; 338 | padding: 2px 7px; 339 | cursor: pointer; 340 | transition: all 0.2s ease; 341 | } 342 | 343 | #addbmsection button:hover { 344 | background-color: var(--accent-primary); 345 | } 346 | 347 | #chatContainer { 348 | position: absolute; 349 | width: 400px; 350 | height: 100vh; 351 | right: 0px; 352 | top: 0; 353 | background-color: var(--bg-chat); 354 | backdrop-filter: blur(10px); 355 | box-shadow: var(--shadow-chat); 356 | border-left: 1px solid var(--border-chat); 357 | transform: translateX(100%); 358 | opacity: 0; 359 | visibility: hidden; 360 | transition: transform 0.4s cubic-bezier(0.25, 0.8, 0.25, 1), 361 | opacity 0.4s cubic-bezier(0.25, 0.8, 0.25, 1), 362 | visibility 0.4s, 363 | background-color 0.3s ease, 364 | box-shadow 0.3s ease, 365 | border-color 0.3s ease; 366 | z-index: 300; 367 | } 368 | 369 | #chatContainer.show { 370 | transform: translateX(0); 371 | opacity: 1; 372 | visibility: visible; 373 | } 374 | 375 | #chatContainer iframe { 376 | transition: opacity 0.3s ease 0.2s; 377 | } 378 | 379 | #chatContainer:not(.show) iframe { 380 | opacity: 0; 381 | } 382 | -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | let currentEngine = "Bing"; 2 | let searchUrl = "https://www.bing.com/search?q="; 3 | let browserSettings = { 4 | searchEngine: "Bing", 5 | theme: "light", 6 | homepage: "home.html", 7 | chatbot: "default", 8 | version: "1.0.0" 9 | }; 10 | 11 | function loadUserSettings() { 12 | try { 13 | const savedSettings = localStorage.getItem('browserSettings'); 14 | if (savedSettings) { 15 | browserSettings = { ...browserSettings, ...JSON.parse(savedSettings) }; 16 | } else { 17 | fetch('settings.json') 18 | .then(response => response.json()) 19 | .then(data => { 20 | browserSettings = { ...browserSettings, ...data }; 21 | applyUserSettings(); 22 | }) 23 | .catch(() => applyUserSettings()); 24 | } 25 | applyUserSettings(); 26 | } catch (error) { 27 | applyUserSettings(); 28 | } 29 | } 30 | 31 | function saveUserSettings() { 32 | localStorage.setItem('browserSettings', JSON.stringify(browserSettings)); 33 | } 34 | 35 | function applyUserSettings() { 36 | currentEngine = browserSettings.searchEngine; 37 | updateSearchEngine(); 38 | switchTheme(browserSettings.theme); 39 | applyChatbotSettings(); 40 | } 41 | 42 | function switchTheme(theme) { 43 | const body = document.body; 44 | 45 | if (theme === 'dark') { 46 | // Add dark theme class to body 47 | body.classList.add('dark-theme'); 48 | body.classList.remove('light-theme'); 49 | } else { 50 | // Add light theme class to body (or remove dark theme class) 51 | body.classList.remove('dark-theme'); 52 | body.classList.add('light-theme'); 53 | } 54 | 55 | // Handle special case for search bar placeholder and chat command color 56 | const searchbar = document.getElementById('searchbar'); 57 | if (searchbar) { 58 | const currentValue = searchbar.value; 59 | if (currentValue.includes('/ch')) { 60 | // Keep the special chat command color override 61 | if (theme === 'dark') { 62 | searchbar.style.color = 'rgba(0, 200, 120, 1)'; 63 | } else { 64 | searchbar.style.color = 'rgba(0, 124, 82, 1)'; 65 | } 66 | } else { 67 | // Remove any inline color to let CSS variables take over 68 | searchbar.style.color = ''; 69 | } 70 | } 71 | } 72 | 73 | function updateSearchEngine() { 74 | const engines = { 75 | "Google": "https://www.google.com/search?q=", 76 | "Perplexity": "https://www.perplexity.ai/search?q=", 77 | "Bing": "https://www.bing.com/search?q=", 78 | "Brave": "https://search.brave.com/search?q=", 79 | "Yahoo": "https://search.yahoo.com/search?p=", 80 | "Duckduckgo": "https://duckduckgo.com/?q=" 81 | }; 82 | searchUrl = engines[currentEngine] || engines["Bing"]; 83 | } 84 | 85 | function changeSearchEngine(engineName) { 86 | browserSettings.searchEngine = engineName; 87 | currentEngine = engineName; 88 | updateSearchEngine(); 89 | saveUserSettings(); 90 | } 91 | 92 | function changeTheme(theme) { 93 | browserSettings.theme = theme; 94 | switchTheme(theme); 95 | saveUserSettings(); 96 | } 97 | 98 | function setupEngineSelector() { 99 | const engineSelect = document.getElementById('engineSelect'); 100 | if (engineSelect) { 101 | engineSelect.value = currentEngine; 102 | engineSelect.addEventListener('change', function() { 103 | changeSearchEngine(this.value); 104 | }); 105 | } 106 | } 107 | 108 | function setupThemeSelector() { 109 | const themeSelect = document.getElementById('themeSelect'); 110 | if (themeSelect) { 111 | themeSelect.value = browserSettings.theme; 112 | themeSelect.addEventListener('change', function() { 113 | changeTheme(this.value); 114 | }); 115 | } 116 | } 117 | 118 | function changeChatbot(chatbotType) { 119 | browserSettings.chatbot = chatbotType; 120 | applyChatbotSettings(); 121 | saveUserSettings(); 122 | } 123 | 124 | function applyChatbotSettings() { 125 | const chatContainer = document.getElementById('chatContainer'); 126 | if (!chatContainer) return; 127 | 128 | chatContainer.innerHTML = ''; 129 | 130 | const chatWebview = document.createElement('webview'); 131 | chatWebview.style.width = '100%'; 132 | chatWebview.style.height = '100%'; 133 | chatWebview.style.border = 'none'; 134 | 135 | if (browserSettings.chatbot === 'chatgpt') { 136 | 137 | chatWebview.src = 'https://chatgpt.com'; 138 | } else if (browserSettings.chatbot === 'gemini') { 139 | 140 | chatWebview.src = 'https://gemini.google.com'; 141 | } else if (browserSettings.chatbot === 'perplexity') { 142 | 143 | chatWebview.src = 'https://www.perplexity.ai'; 144 | } else { 145 | 146 | chatWebview.src = 'https://cdn.botpress.cloud/webchat/v3.2/shareable.html?configUrl=https://files.bpcontent.cloud/2025/07/21/19/20250721195356-NX7X7607.json'; 147 | } 148 | 149 | chatContainer.appendChild(chatWebview); 150 | } 151 | 152 | function setupChatbotSelector() { 153 | const chatbotSelect = document.getElementById('chatbotSelect'); 154 | if (chatbotSelect) { 155 | chatbotSelect.value = browserSettings.chatbot; 156 | chatbotSelect.addEventListener('change', function() { 157 | changeChatbot(this.value); 158 | }); 159 | } 160 | } 161 | 162 | let tabs = []; 163 | let activeTabIndex = 0; 164 | let tabCounter = 0; 165 | 166 | function initTabs() { 167 | createNewTab('home.html', 'Home'); 168 | } 169 | 170 | function createNewTab(url = 'home.html', title = 'New Tab') { 171 | const tabId = `tab-${++tabCounter}`; 172 | const tab = { 173 | id: tabId, 174 | url: url, 175 | title: title, 176 | history: [url] 177 | }; 178 | 179 | tabs.push(tab); 180 | activeTabIndex = tabs.length - 1; 181 | 182 | updateTabsUI(); 183 | loadTabContent(activeTabIndex); 184 | return tab; 185 | } 186 | 187 | function closeTab(index) { 188 | if (tabs.length <= 1) return; 189 | 190 | tabs.splice(index, 1); 191 | 192 | if (activeTabIndex >= tabs.length) { 193 | activeTabIndex = tabs.length - 1; 194 | } else if (activeTabIndex > index) { 195 | activeTabIndex--; 196 | } 197 | 198 | updateTabsUI(); 199 | loadTabContent(activeTabIndex); 200 | } 201 | 202 | function switchToTab(index) { 203 | if (index >= 0 && index < tabs.length) { 204 | activeTabIndex = index; 205 | loadTabContent(activeTabIndex); 206 | updateTabsUI(); 207 | hidTabs(); 208 | } 209 | } 210 | 211 | function loadTabContent(index) { 212 | if (tabs[index]) { 213 | const webview = document.getElementById('Browser'); 214 | webview.loadURL(tabs[index].url); 215 | document.getElementById('searchbar').value = tabs[index].url === 'home.html' ? '' : tabs[index].url; 216 | } 217 | } 218 | 219 | function updateTabsUI() { 220 | const tabsContainer = document.getElementById('tabs'); 221 | 222 | // Clear existing tabs 223 | while (tabsContainer.firstChild) { 224 | tabsContainer.removeChild(tabsContainer.firstChild); 225 | } 226 | 227 | // Render the current tabs 228 | tabs.forEach((tab, index) => { 229 | const tabElement = document.createElement('div'); 230 | tabElement.className = 'tab-item'; 231 | tabElement.innerHTML = ` 232 |
233 | ${tab.title} 234 | ${tab.url} 235 |
236 | 237 | `; 238 | 239 | if (index === activeTabIndex) { 240 | tabElement.classList.add('active-tab'); 241 | } 242 | 243 | tabsContainer.appendChild(tabElement); 244 | }); 245 | } 246 | 247 | function updateCurrentTabUrl(url) { 248 | if (tabs[activeTabIndex]) { 249 | tabs[activeTabIndex].url = url; 250 | tabs[activeTabIndex].title = getPageTitle(url); 251 | updateTabsUI(); 252 | } 253 | } 254 | 255 | function getPageTitle(url) { 256 | if (url === 'home.html') return 'Home'; 257 | if (url.includes('google.com')) return 'Google'; 258 | if (url.includes('bing.com')) return 'Bing'; 259 | if (url.includes('chatgpt.com')) return 'ChatGPT'; 260 | 261 | try { 262 | const urlObj = new URL(url.startsWith('http') ? url : 'https://' + url); 263 | return urlObj.hostname; 264 | } catch { 265 | return 'New Tab'; 266 | } 267 | } 268 | 269 | document.getElementById('searchbar').addEventListener('input', function(){ 270 | let search = document.getElementById('searchbar').value; 271 | const isDark = document.body.classList.contains('dark-theme'); 272 | 273 | if (search.includes('/ch')) { 274 | // Special color for chat command 275 | if (isDark) { 276 | document.getElementById('searchbar').style.color = 'rgba(0, 200, 120, 1)'; 277 | } else { 278 | document.getElementById('searchbar').style.color = 'rgba(0, 124, 82, 1)'; 279 | } 280 | } else { 281 | // Remove inline style to let CSS variables take over 282 | document.getElementById('searchbar').style.color = ''; 283 | } 284 | }); 285 | 286 | 287 | function back() { 288 | document.getElementById('Browser').goBack(); 289 | } 290 | function forward() { 291 | document.getElementById('Browser').goForward(); 292 | } 293 | 294 | function openlink() { 295 | const searchQuery = document.getElementById('searchbar').value.trim(); 296 | let url; 297 | let engine = searchUrl; 298 | 299 | if (searchQuery.startsWith('/ch')) { 300 | engine = 'https://chatgpt.com/?q='; 301 | const query = searchQuery.slice(3).trim(); 302 | url = engine + encodeURIComponent(query); 303 | } else if (searchQuery.includes('.')) { 304 | url = searchQuery.startsWith('http') ? searchQuery : 'https://' + searchQuery; 305 | } else { 306 | url = engine + encodeURIComponent(searchQuery); 307 | } 308 | 309 | document.getElementById('Browser').loadURL(url); 310 | updateCurrentTabUrl(url); 311 | } 312 | 313 | 314 | function showTabs() { 315 | document.getElementById('tabs').style.display = 'block'; 316 | updateTabsUI(); 317 | } 318 | 319 | function hidTabs() { 320 | document.getElementById('tabs').style.display = 'none'; 321 | } 322 | 323 | function toggleTabs() { 324 | const tabsElement = document.getElementById('tabs'); 325 | if (tabsElement.style.display === 'none' || tabsElement.style.display === '') { 326 | showTabs(); 327 | } else { 328 | hidTabs(); 329 | } 330 | } 331 | 332 | document.getElementById('searchbar').addEventListener('keydown', function(event) { 333 | if (event.key === 'Enter') { 334 | openlink(); 335 | } 336 | }); 337 | 338 | function showSettings(){ 339 | document.getElementById('settings').style.display = 'block'; 340 | } 341 | function hidSettings(){ 342 | document.getElementById('settings').style.display = 'none'; 343 | } 344 | 345 | function newtab() { 346 | const url = browserSettings.homepage || 'home.html'; 347 | createNewTab(url, 'New Tab'); 348 | document.getElementById('searchbar').value = ''; 349 | } 350 | 351 | document.addEventListener('keydown', function(event) { 352 | const isCmd = event.metaKey || event.ctrlKey; 353 | 354 | if (isCmd && event.key === 't') { 355 | event.preventDefault(); 356 | newtab(); 357 | } 358 | 359 | if (isCmd && event.key === 'w' && tabs.length > 1) { 360 | event.preventDefault(); 361 | closeTab(activeTabIndex); 362 | } 363 | 364 | if (isCmd && event.shiftKey && event.key === 'T') { 365 | event.preventDefault(); 366 | toggleTabs(); 367 | } 368 | 369 | if (isCmd && event.key >= '1' && event.key <= '9') { 370 | event.preventDefault(); 371 | const tabIndex = parseInt(event.key) - 1; 372 | if (tabIndex < tabs.length) switchToTab(tabIndex); 373 | } 374 | 375 | // Add F5 and Cmd+R for webview reload 376 | if (event.key === 'F5' || (isCmd && event.key === 'r')) { 377 | event.preventDefault(); 378 | reloadWebview(); 379 | } 380 | 381 | // Add Cmd+, to go to home 382 | if (isCmd && event.key === ',') { 383 | event.preventDefault(); 384 | goToHome(); 385 | } 386 | 387 | // Add Cmd+Shift+C to test context menu 388 | if (isCmd && event.shiftKey && event.key === 'C') { 389 | event.preventDefault(); 390 | showSimpleContextMenu(window.innerWidth / 2, window.innerHeight / 2); 391 | } 392 | 393 | if (event.key === 'Escape') hidTabs(); 394 | }); 395 | 396 | let lastSettings = JSON.stringify(browserSettings); 397 | setInterval(function() { 398 | const saved = localStorage.getItem('browserSettings'); 399 | if (saved && saved !== lastSettings) { 400 | try { 401 | const newSettings = JSON.parse(saved); 402 | if (newSettings.searchEngine !== browserSettings.searchEngine) { 403 | changeSearchEngine(newSettings.searchEngine); 404 | } 405 | if (newSettings.theme !== browserSettings.theme) { 406 | changeTheme(newSettings.theme); 407 | } 408 | if (newSettings.chatbot !== browserSettings.chatbot) { 409 | changeChatbot(newSettings.chatbot); 410 | } 411 | lastSettings = saved; 412 | } catch (error) { 413 | console.log('Settings sync error:', error); 414 | } 415 | } 416 | }, 1000); 417 | 418 | window.addEventListener('message', function(event) { 419 | if (event.data?.type === 'ENGINE_CHANGED') { 420 | changeSearchEngine(event.data.engine); 421 | } 422 | if (event.data?.type === 'THEME_CHANGED') { 423 | changeTheme(event.data.theme); 424 | } 425 | if (event.data?.type === 'CHATBOT_CHANGED') { 426 | changeChatbot(event.data.chatbot); 427 | } 428 | // Handle context menu actions from webview 429 | if (event.data?.type === 'CONTEXT_MENU_ACTION') { 430 | const { action, url } = event.data; 431 | switch (action) { 432 | case 'new-tab': 433 | if (url) { 434 | createNewTab(url, getPageTitle(url)); 435 | } 436 | break; 437 | case 'new-window': 438 | if (url && window.electronAPI?.openNewWindow) { 439 | window.electronAPI.openNewWindow(url); 440 | } else if (url) { 441 | // Fallback: open in new tab if new window API not available 442 | createNewTab(url, getPageTitle(url)); 443 | } 444 | break; 445 | case 'inspect': 446 | // Open dev tools for the webview 447 | const webview = document.getElementById('Browser'); 448 | if (webview && webview.openDevTools) { 449 | webview.openDevTools(); 450 | } 451 | break; 452 | case 'save-as': 453 | if (url) { 454 | // Create a temporary link to trigger download 455 | const a = document.createElement('a'); 456 | a.href = url; 457 | a.download = url.split('/').pop() || 'download'; 458 | document.body.appendChild(a); 459 | a.click(); 460 | document.body.removeChild(a); 461 | } 462 | break; 463 | } 464 | } 465 | }); 466 | 467 | window.addEventListener('DOMContentLoaded', function() { 468 | loadUserSettings(); 469 | 470 | // Load saved session or create initial tab 471 | setTimeout(() => { 472 | loadBrowserSession(); 473 | // If no session was loaded, create initial tab 474 | if (tabs.length === 0) { 475 | initTabs(); 476 | } 477 | }, 100); 478 | 479 | setupEngineSelector(); 480 | setupThemeSelector(); 481 | setupChatbotSelector(); 482 | 483 | const webview = document.getElementById('Browser'); 484 | webview.addEventListener('did-navigate', (event) => { 485 | updateCurrentTabUrl(event.url); 486 | // Re-setup context menu after navigation 487 | setTimeout(() => setupWebviewContextMenu(), 1000); 488 | }); 489 | webview.addEventListener('did-navigate-in-page', (event) => { 490 | updateCurrentTabUrl(event.url); 491 | // Re-setup context menu after in-page navigation 492 | setTimeout(() => setupWebviewContextMenu(), 1000); 493 | }); 494 | 495 | webview.addEventListener('dom-ready', function() { 496 | setTimeout(() => { 497 | setupEngineSelector(); 498 | setupWebviewContextMenu(); // Setup context menu when DOM is ready 499 | }, 500); 500 | }); 501 | 502 | // Setup context menu for webview 503 | setupWebviewContextMenu(); 504 | 505 | document.addEventListener('click', function(event) { 506 | const tabsElement = document.getElementById('tabs'); 507 | const tabButton = document.getElementById('tabbutton'); 508 | 509 | if (!tabsElement.contains(event.target) && !tabButton.contains(event.target)) { 510 | if (tabsElement.style.display === 'block') { 511 | hidTabs(); 512 | } 513 | } 514 | }); 515 | 516 | // Add global right-click listener 517 | document.addEventListener('contextmenu', function(event) { 518 | console.log('Global right-click detected at:', event.clientX, event.clientY); 519 | // Check if the right-click is on the webview or inside the browser area 520 | const webview = document.getElementById('Browser'); 521 | const browserContainer = document.getElementById('browserContainer'); 522 | 523 | if (webview && (event.target === webview || browserContainer.contains(event.target))) { 524 | event.preventDefault(); 525 | console.log('Showing context menu for webview area at:', event.clientX, event.clientY); 526 | 527 | // Use the exact mouse position 528 | showWorkingContextMenu(event.clientX, event.clientY, webview); 529 | } 530 | }); 531 | }); 532 | 533 | document.getElementById('bmbtn').addEventListener('click', function() { 534 | var bookmark = document.getElementById('bookmarks'); 535 | if (bookmark.style.display === 'none' || bookmark.style.display === '') { 536 | bookmark.style.display = 'block'; 537 | } else { 538 | bookmark.style.display = 'none'; 539 | } 540 | }); 541 | function addbm() { 542 | 543 | if (!document.getElementById('bmlink') || !document.getElementById('bmtext')) { 544 | alert('Please enter both a name and a URL for the bookmark.'); 545 | } 546 | const bmText = document.getElementById('bmtext').value; 547 | const bmLink = document.getElementById('bmlink').value; 548 | const url = 'https://' + bmLink; 549 | let newEl = document.createElement('a'); 550 | let btnText = document.createTextNode(" " + bmText + " "); 551 | newEl.appendChild(btnText); 552 | let bmlink = document.createAttribute('href'); 553 | bmlink.value = url; 554 | newEl.setAttributeNode(bmlink); 555 | newEl.onclick = function(e) { 556 | e.preventDefault(); 557 | document.getElementById('Browser').src = url; 558 | }; 559 | document.getElementById('bookmarks').appendChild(newEl); 560 | 561 | } 562 | 563 | document.getElementById('chatbtn').addEventListener('click', function() { 564 | toggleChatPanel(); 565 | }); 566 | 567 | function toggleChatPanel() { 568 | var chatContainer = document.getElementById('chatContainer'); 569 | var webview = document.getElementById('Browser'); 570 | 571 | if (chatContainer.classList.contains('show')) { 572 | // Close chat panel 573 | chatContainer.classList.remove('show'); 574 | webview.style.width = '100%'; 575 | } else { 576 | // Open chat panel 577 | chatContainer.classList.add('show'); 578 | webview.style.width = 'calc(100% - 400px)'; 579 | } 580 | } 581 | 582 | // Handle IPC messages from the main process for global keyboard shortcuts 583 | const { ipcRenderer } = require('electron'); 584 | 585 | ipcRenderer.on('toggle-chat-panel', () => { 586 | console.log('Chat panel toggled via global shortcut'); 587 | toggleChatPanel(); 588 | }); 589 | 590 | ipcRenderer.on('toggle-tabbar', () => { 591 | const tabbar = document.getElementById('tabbar'); 592 | 593 | if (tabbar) { 594 | if (tabbar.style.display === 'none' || tabbar.style.display === '') { 595 | tabbar.style.display = 'block'; 596 | console.log('Tabbar shown via global shortcut'); 597 | } else { 598 | tabbar.style.display = 'none'; 599 | console.log('Tabbar hidden via global shortcut'); 600 | } 601 | } 602 | }); 603 | 604 | // Context menu functionality 605 | ipcRenderer.on('context-menu-action', (event, data) => { 606 | if (data.action === 'new-tab' && data.url) { 607 | createNewTab(data.url, getPageTitle(data.url)); 608 | } 609 | }); 610 | 611 | ipcRenderer.on('navigate-to-url', (event, url) => { 612 | if (url) { 613 | createNewTab(url, getPageTitle(url)); 614 | } 615 | }); 616 | 617 | // Webview reload functionality 618 | ipcRenderer.on('reload-webview-response', () => { 619 | const webview = document.getElementById('Browser'); 620 | if (webview) { 621 | webview.reload(); 622 | console.log('Webview reloaded'); 623 | } 624 | }); 625 | 626 | // Function to reload only the webview 627 | function reloadWebview() { 628 | const webview = document.getElementById('Browser'); 629 | if (webview) { 630 | webview.reload(); 631 | console.log('Webview reloaded manually'); 632 | } 633 | } 634 | 635 | // Function to go to home page 636 | function goToHome() { 637 | const webview = document.getElementById('Browser'); 638 | if (webview) { 639 | webview.loadURL('home.html'); 640 | updateCurrentTabUrl('home.html'); 641 | document.getElementById('searchbar').value = ''; 642 | console.log('Navigated to home'); 643 | } 644 | } 645 | 646 | // Session persistence functions 647 | function saveBrowserSession() { 648 | const sessionData = { 649 | tabs: tabs, 650 | activeTabIndex: activeTabIndex, 651 | settings: browserSettings, 652 | timestamp: Date.now() 653 | }; 654 | 655 | if (window.electronAPI && window.electronAPI.saveBrowserState) { 656 | window.electronAPI.saveBrowserState(sessionData); 657 | } else { 658 | localStorage.setItem('browserSession', JSON.stringify(sessionData)); 659 | } 660 | } 661 | 662 | function loadBrowserSession() { 663 | if (window.electronAPI && window.electronAPI.loadBrowserState) { 664 | window.electronAPI.loadBrowserState().then(result => { 665 | if (result.success && result.data) { 666 | restoreSession(result.data); 667 | } else { 668 | // Fallback to localStorage 669 | loadFromLocalStorage(); 670 | } 671 | }); 672 | } else { 673 | loadFromLocalStorage(); 674 | } 675 | } 676 | 677 | function loadFromLocalStorage() { 678 | try { 679 | const sessionData = localStorage.getItem('browserSession'); 680 | if (sessionData) { 681 | const parsedData = JSON.parse(sessionData); 682 | restoreSession(parsedData); 683 | } 684 | } catch (error) { 685 | console.log('Failed to load session from localStorage:', error); 686 | } 687 | } 688 | 689 | function restoreSession(sessionData) { 690 | if (sessionData.tabs && sessionData.tabs.length > 0) { 691 | tabs = sessionData.tabs; 692 | activeTabIndex = sessionData.activeTabIndex || 0; 693 | tabCounter = Math.max(...tabs.map(tab => parseInt(tab.id.split('-')[1]))); 694 | 695 | if (sessionData.settings) { 696 | browserSettings = { ...browserSettings, ...sessionData.settings }; 697 | applyUserSettings(); 698 | } 699 | 700 | updateTabsUI(); 701 | loadTabContent(activeTabIndex); 702 | console.log('Session restored with', tabs.length, 'tabs'); 703 | } 704 | } 705 | 706 | // Context menu setup for webview 707 | function setupWebviewContextMenu() { 708 | const webview = document.getElementById('Browser'); 709 | if (webview) { 710 | // Remove all existing event listeners 711 | webview.removeEventListener('contextmenu', handleWebviewContextMenu); 712 | webview.removeEventListener('context-menu', handleWebviewContextMenu); 713 | 714 | // Add context menu event listener for webview 715 | webview.addEventListener('context-menu', (e) => { 716 | e.preventDefault(); 717 | console.log('Context menu event detected:', e); 718 | showSimpleContextMenu(e.x || e.clientX || 100, e.y || e.clientY || 100); 719 | }); 720 | 721 | // Add regular right-click listener as backup 722 | webview.addEventListener('contextmenu', (e) => { 723 | e.preventDefault(); 724 | console.log('Regular contextmenu event:', e); 725 | showSimpleContextMenu(e.clientX || 100, e.clientY || 100); 726 | }); 727 | 728 | // Add mouseup listener for right clicks as another fallback 729 | webview.addEventListener('mouseup', function(e) { 730 | if (e.button === 2) { // Right click 731 | e.preventDefault(); 732 | console.log('Right click detected via mouseup'); 733 | showSimpleContextMenu(e.clientX, e.clientY); 734 | } 735 | }); 736 | 737 | // Disable the default context menu 738 | webview.addEventListener('contextmenu', (e) => e.preventDefault()); 739 | } 740 | } 741 | 742 | function handleWebviewContextMenu(e) { 743 | e.preventDefault(); 744 | 745 | // Create a simple HTML context menu 746 | const existingMenu = document.getElementById('custom-context-menu'); 747 | if (existingMenu) { 748 | existingMenu.remove(); 749 | } 750 | 751 | const menu = document.createElement('div'); 752 | menu.id = 'custom-context-menu'; 753 | menu.style.cssText = ` 754 | position: fixed; 755 | z-index: 10000; 756 | background: var(--bg-secondary); 757 | border: 1px solid var(--border-light); 758 | border-radius: 6px; 759 | box-shadow: var(--shadow-medium); 760 | padding: 4px 0; 761 | min-width: 180px; 762 | font-family: system-ui, -apple-system, sans-serif; 763 | font-size: 13px; 764 | color: var(--text-primary); 765 | left: ${e.clientX}px; 766 | top: ${e.clientY}px; 767 | backdrop-filter: blur(10px); 768 | `; 769 | 770 | const menuItems = []; 771 | 772 | // Get context info from the event 773 | const params = e.params || {}; 774 | const linkURL = params.linkURL || ''; 775 | const srcURL = params.srcURL || ''; 776 | const selectionText = params.selectionText || ''; 777 | const isEditable = params.isEditable || false; 778 | 779 | // Add link-specific options 780 | if (linkURL) { 781 | menuItems.push({ 782 | label: 'Open Link in New Tab', 783 | action: () => { 784 | createNewTab(linkURL, getPageTitle(linkURL)); 785 | } 786 | }); 787 | 788 | menuItems.push({ 789 | label: 'Open Link in New Window', 790 | action: () => { 791 | // For now, open in new tab (can be enhanced later) 792 | createNewTab(linkURL, getPageTitle(linkURL)); 793 | } 794 | }); 795 | 796 | menuItems.push({ type: 'separator' }); 797 | } 798 | 799 | // Add copy option if text is selected 800 | if (selectionText) { 801 | menuItems.push({ 802 | label: 'Copy', 803 | action: () => { 804 | const { clipboard } = require('electron'); 805 | clipboard.writeText(selectionText); 806 | } 807 | }); 808 | } else { 809 | // Always show copy option 810 | menuItems.push({ 811 | label: 'Copy', 812 | action: () => { 813 | const webview = document.getElementById('Browser'); 814 | if (webview) { 815 | webview.copy(); 816 | } 817 | } 818 | }); 819 | } 820 | 821 | // Add paste option 822 | menuItems.push({ 823 | label: 'Paste', 824 | action: () => { 825 | const webview = document.getElementById('Browser'); 826 | if (webview) { 827 | webview.paste(); 828 | } 829 | } 830 | }); 831 | 832 | menuItems.push({ type: 'separator' }); 833 | 834 | // Add inspect element 835 | menuItems.push({ 836 | label: 'Inspect Element', 837 | action: () => { 838 | const webview = document.getElementById('Browser'); 839 | if (webview) { 840 | webview.openDevTools(); 841 | } 842 | } 843 | }); 844 | 845 | // Add save image option for images 846 | if (srcURL) { 847 | menuItems.push({ 848 | label: 'Save Image As', 849 | action: () => { 850 | // Create a temporary link to download the image 851 | const link = document.createElement('a'); 852 | link.href = srcURL; 853 | link.download = srcURL.split('/').pop() || 'image'; 854 | link.target = '_blank'; 855 | document.body.appendChild(link); 856 | link.click(); 857 | document.body.removeChild(link); 858 | } 859 | }); 860 | } 861 | 862 | // Build the menu 863 | menuItems.forEach(item => { 864 | if (item.type === 'separator') { 865 | const separator = document.createElement('div'); 866 | separator.style.cssText = 'height: 1px; background: var(--border-light); margin: 4px 8px;'; 867 | menu.appendChild(separator); 868 | } else { 869 | const menuItem = document.createElement('div'); 870 | menuItem.textContent = item.label; 871 | menuItem.style.cssText = ` 872 | padding: 8px 16px; 873 | cursor: pointer; 874 | transition: background-color 0.1s ease; 875 | `; 876 | 877 | menuItem.addEventListener('mouseenter', () => { 878 | menuItem.style.backgroundColor = 'var(--bg-button-hover)'; 879 | }); 880 | 881 | menuItem.addEventListener('mouseleave', () => { 882 | menuItem.style.backgroundColor = 'transparent'; 883 | }); 884 | 885 | menuItem.addEventListener('click', () => { 886 | item.action(); 887 | menu.remove(); 888 | }); 889 | 890 | menu.appendChild(menuItem); 891 | } 892 | }); 893 | 894 | document.body.appendChild(menu); 895 | 896 | // Position the menu to stay within viewport 897 | const rect = menu.getBoundingClientRect(); 898 | if (rect.right > window.innerWidth) { 899 | menu.style.left = (e.clientX - rect.width) + 'px'; 900 | } 901 | if (rect.bottom > window.innerHeight) { 902 | menu.style.top = (e.clientY - rect.height) + 'px'; 903 | } 904 | 905 | // Remove menu on click elsewhere 906 | const removeMenu = (event) => { 907 | if (!menu.contains(event.target)) { 908 | menu.remove(); 909 | document.removeEventListener('click', removeMenu); 910 | document.removeEventListener('keydown', escapeHandler); 911 | } 912 | }; 913 | 914 | const escapeHandler = (event) => { 915 | if (event.key === 'Escape') { 916 | menu.remove(); 917 | document.removeEventListener('click', removeMenu); 918 | document.removeEventListener('keydown', escapeHandler); 919 | } 920 | }; 921 | 922 | // Add event listeners after a short delay to prevent immediate closure 923 | setTimeout(() => { 924 | document.addEventListener('click', removeMenu); 925 | document.addEventListener('keydown', escapeHandler); 926 | }, 10); 927 | } 928 | 929 | // Simple context menu that always appears on right-click 930 | function showSimpleContextMenu(x, y) { 931 | // Remove existing menu 932 | const existingMenu = document.getElementById('simple-context-menu'); 933 | if (existingMenu) { 934 | existingMenu.remove(); 935 | } 936 | 937 | const menu = document.createElement('div'); 938 | menu.id = 'simple-context-menu'; 939 | menu.style.cssText = ` 940 | position: fixed; 941 | z-index: 10000; 942 | background: var(--bg-secondary); 943 | border: 1px solid var(--border-light); 944 | border-radius: 6px; 945 | box-shadow: var(--shadow-medium); 946 | padding: 4px 0; 947 | min-width: 180px; 948 | font-family: system-ui, -apple-system, sans-serif; 949 | font-size: 13px; 950 | color: var(--text-primary); 951 | left: ${x}px; 952 | top: ${y}px; 953 | backdrop-filter: blur(10px); 954 | `; 955 | 956 | const menuItems = [ 957 | { 958 | label: 'Copy', 959 | action: () => { 960 | const webview = document.getElementById('Browser'); 961 | if (webview) { 962 | webview.copy(); 963 | } 964 | } 965 | }, 966 | { 967 | label: 'Paste', 968 | action: () => { 969 | const webview = document.getElementById('Browser'); 970 | if (webview) { 971 | webview.paste(); 972 | } 973 | } 974 | }, 975 | { type: 'separator' }, 976 | { 977 | label: 'Reload Page', 978 | action: () => { 979 | reloadWebview(); 980 | } 981 | }, 982 | { 983 | label: 'Go to Home', 984 | action: () => { 985 | goToHome(); 986 | } 987 | }, 988 | { type: 'separator' }, 989 | { 990 | label: 'Inspect Element', 991 | action: () => { 992 | const webview = document.getElementById('Browser'); 993 | if (webview) { 994 | webview.openDevTools(); 995 | } 996 | } 997 | } 998 | ]; 999 | 1000 | // Build the menu 1001 | menuItems.forEach(item => { 1002 | if (item.type === 'separator') { 1003 | const separator = document.createElement('div'); 1004 | separator.style.cssText = 'height: 1px; background: var(--border-light); margin: 4px 8px;'; 1005 | menu.appendChild(separator); 1006 | } else { 1007 | const menuItem = document.createElement('div'); 1008 | menuItem.textContent = item.label; 1009 | menuItem.style.cssText = ` 1010 | padding: 8px 16px; 1011 | cursor: pointer; 1012 | transition: background-color 0.1s ease; 1013 | `; 1014 | 1015 | menuItem.addEventListener('mouseenter', () => { 1016 | menuItem.style.backgroundColor = 'var(--bg-button-hover)'; 1017 | }); 1018 | 1019 | menuItem.addEventListener('mouseleave', () => { 1020 | menuItem.style.backgroundColor = 'transparent'; 1021 | }); 1022 | 1023 | menuItem.addEventListener('click', () => { 1024 | item.action(); 1025 | menu.remove(); 1026 | }); 1027 | 1028 | menu.appendChild(menuItem); 1029 | } 1030 | }); 1031 | 1032 | document.body.appendChild(menu); 1033 | 1034 | // Position the menu to stay within viewport 1035 | const rect = menu.getBoundingClientRect(); 1036 | if (rect.right > window.innerWidth) { 1037 | menu.style.left = (x - rect.width) + 'px'; 1038 | } 1039 | if (rect.bottom > window.innerHeight) { 1040 | menu.style.top = (y - rect.height) + 'px'; 1041 | } 1042 | 1043 | // Remove menu on click elsewhere or escape 1044 | const removeMenu = (event) => { 1045 | if (!menu.contains(event.target)) { 1046 | menu.remove(); 1047 | document.removeEventListener('click', removeMenu); 1048 | document.removeEventListener('keydown', escapeHandler); 1049 | } 1050 | }; 1051 | 1052 | const escapeHandler = (event) => { 1053 | if (event.key === 'Escape') { 1054 | menu.remove(); 1055 | document.removeEventListener('click', removeMenu); 1056 | document.removeEventListener('keydown', escapeHandler); 1057 | } 1058 | }; 1059 | 1060 | // Add event listeners after a short delay 1061 | setTimeout(() => { 1062 | document.addEventListener('click', removeMenu); 1063 | document.addEventListener('keydown', escapeHandler); 1064 | }, 10); 1065 | } 1066 | 1067 | // Auto-save session periodically 1068 | setInterval(saveBrowserSession, 30000); // Save every 30 seconds 1069 | 1070 | // Advanced context menu that detects links and handles different contexts 1071 | function showAdvancedContextMenu(x, y, webview) { 1072 | // Remove existing menu 1073 | const existingMenu = document.getElementById('advanced-context-menu'); 1074 | if (existingMenu) { 1075 | existingMenu.remove(); 1076 | } 1077 | 1078 | const menu = document.createElement('div'); 1079 | menu.id = 'advanced-context-menu'; 1080 | menu.style.cssText = ` 1081 | position: fixed; 1082 | z-index: 10000; 1083 | background: var(--bg-secondary); 1084 | border: 1px solid var(--border-light); 1085 | border-radius: 6px; 1086 | box-shadow: var(--shadow-medium); 1087 | padding: 4px 0; 1088 | min-width: 200px; 1089 | font-family: system-ui, -apple-system, sans-serif; 1090 | font-size: 13px; 1091 | color: var(--text-primary); 1092 | left: ${x}px; 1093 | top: ${y}px; 1094 | backdrop-filter: blur(10px); 1095 | `; 1096 | 1097 | // Get the webview's position to check if we can get context info 1098 | const webviewRect = webview.getBoundingClientRect(); 1099 | const relativeX = x - webviewRect.left; 1100 | const relativeY = y - webviewRect.top; 1101 | 1102 | // Try to get selection text or other context from the webview 1103 | let hasSelection = false; 1104 | let currentUrl = ''; 1105 | 1106 | try { 1107 | // Get current URL 1108 | currentUrl = webview.getURL() || ''; 1109 | 1110 | // Check if there's selected text (simplified detection) 1111 | webview.executeJavaScript(` 1112 | (function() { 1113 | const selection = window.getSelection(); 1114 | const selectedText = selection.toString().trim(); 1115 | const targetElement = document.elementFromPoint(${relativeX}, ${relativeY}); 1116 | let linkUrl = ''; 1117 | let imageUrl = ''; 1118 | 1119 | // Check if clicked element or its parent is a link 1120 | let element = targetElement; 1121 | while (element && element !== document.body) { 1122 | if (element.tagName === 'A' && element.href) { 1123 | linkUrl = element.href; 1124 | break; 1125 | } 1126 | element = element.parentElement; 1127 | } 1128 | 1129 | // Check if clicked element is an image 1130 | if (targetElement && targetElement.tagName === 'IMG' && targetElement.src) { 1131 | imageUrl = targetElement.src; 1132 | } 1133 | 1134 | return { 1135 | selectedText: selectedText, 1136 | linkUrl: linkUrl, 1137 | imageUrl: imageUrl, 1138 | hasSelection: selectedText.length > 0 1139 | }; 1140 | })() 1141 | `).then(result => { 1142 | // Update menu with context info 1143 | updateContextMenuWithInfo(menu, result, webview); 1144 | }).catch(() => { 1145 | // Fallback: show basic menu 1146 | buildBasicContextMenu(menu, webview); 1147 | }); 1148 | } catch (error) { 1149 | console.log('Error getting webview context:', error); 1150 | buildBasicContextMenu(menu, webview); 1151 | } 1152 | 1153 | // Initially show basic menu, will be updated if we get context info 1154 | buildBasicContextMenu(menu, webview); 1155 | 1156 | document.body.appendChild(menu); 1157 | 1158 | // Position the menu to stay within viewport 1159 | const rect = menu.getBoundingClientRect(); 1160 | let finalX = x; 1161 | let finalY = y; 1162 | 1163 | if (rect.right > window.innerWidth) { 1164 | finalX = x - rect.width; 1165 | } 1166 | if (rect.bottom > window.innerHeight) { 1167 | finalY = y - rect.height; 1168 | } 1169 | 1170 | menu.style.left = finalX + 'px'; 1171 | menu.style.top = finalY + 'px'; 1172 | 1173 | // Remove menu on click elsewhere or escape 1174 | const removeMenu = (event) => { 1175 | if (!menu.contains(event.target)) { 1176 | menu.remove(); 1177 | document.removeEventListener('click', removeMenu); 1178 | document.removeEventListener('keydown', escapeHandler); 1179 | } 1180 | }; 1181 | 1182 | const escapeHandler = (event) => { 1183 | if (event.key === 'Escape') { 1184 | menu.remove(); 1185 | document.removeEventListener('click', removeMenu); 1186 | document.removeEventListener('keydown', escapeHandler); 1187 | } 1188 | }; 1189 | 1190 | // Add event listeners after a short delay 1191 | setTimeout(() => { 1192 | document.addEventListener('click', removeMenu); 1193 | document.addEventListener('keydown', escapeHandler); 1194 | }, 10); 1195 | } 1196 | 1197 | function updateContextMenuWithInfo(menu, contextInfo, webview) { 1198 | // Clear existing menu items 1199 | while (menu.firstChild) { 1200 | menu.removeChild(menu.firstChild); 1201 | } 1202 | 1203 | const menuItems = []; 1204 | 1205 | // Add link-specific options if there's a link 1206 | if (contextInfo.linkUrl) { 1207 | menuItems.push({ 1208 | label: 'Open Link in New Tab', 1209 | action: () => { 1210 | createNewTab(contextInfo.linkUrl, getPageTitle(contextInfo.linkUrl)); 1211 | } 1212 | }); 1213 | 1214 | menuItems.push({ 1215 | label: 'Open Link in New Window', 1216 | action: () => { 1217 | if (window.electronAPI && window.electronAPI.openNewWindow) { 1218 | window.electronAPI.openNewWindow(contextInfo.linkUrl); 1219 | } else { 1220 | // Fallback: open in new tab 1221 | createNewTab(contextInfo.linkUrl, getPageTitle(contextInfo.linkUrl)); 1222 | } 1223 | } 1224 | }); 1225 | 1226 | menuItems.push({ 1227 | label: 'Copy Link Address', 1228 | action: () => { 1229 | if (navigator.clipboard) { 1230 | navigator.clipboard.writeText(contextInfo.linkUrl); 1231 | } else { 1232 | // Fallback for older browsers 1233 | const textArea = document.createElement('textarea'); 1234 | textArea.value = contextInfo.linkUrl; 1235 | document.body.appendChild(textArea); 1236 | textArea.select(); 1237 | document.execCommand('copy'); 1238 | document.body.removeChild(textArea); 1239 | } 1240 | } 1241 | }); 1242 | 1243 | menuItems.push({ type: 'separator' }); 1244 | } 1245 | 1246 | // Add copy option based on selection 1247 | if (contextInfo.hasSelection && contextInfo.selectedText) { 1248 | menuItems.push({ 1249 | label: 'Copy', 1250 | action: () => { 1251 | if (navigator.clipboard) { 1252 | navigator.clipboard.writeText(contextInfo.selectedText); 1253 | } else { 1254 | webview.copy(); 1255 | } 1256 | } 1257 | }); 1258 | } else { 1259 | menuItems.push({ 1260 | label: 'Copy', 1261 | action: () => { 1262 | webview.copy(); 1263 | } 1264 | }); 1265 | } 1266 | 1267 | // Add paste option 1268 | menuItems.push({ 1269 | label: 'Paste', 1270 | action: () => { 1271 | webview.paste(); 1272 | } 1273 | }); 1274 | 1275 | menuItems.push({ type: 'separator' }); 1276 | 1277 | // Add image-specific options if there's an image 1278 | if (contextInfo.imageUrl) { 1279 | menuItems.push({ 1280 | label: 'Save Image As', 1281 | action: () => { 1282 | // Create a temporary link to download the image 1283 | const link = document.createElement('a'); 1284 | link.href = contextInfo.imageUrl; 1285 | link.download = contextInfo.imageUrl.split('/').pop() || 'image'; 1286 | link.target = '_blank'; 1287 | document.body.appendChild(link); 1288 | link.click(); 1289 | document.body.removeChild(link); 1290 | } 1291 | }); 1292 | 1293 | menuItems.push({ 1294 | label: 'Copy Image Address', 1295 | action: () => { 1296 | if (navigator.clipboard) { 1297 | navigator.clipboard.writeText(contextInfo.imageUrl); 1298 | } else { 1299 | // Fallback 1300 | const textArea = document.createElement('textarea'); 1301 | textArea.value = contextInfo.imageUrl; 1302 | document.body.appendChild(textArea); 1303 | textArea.select(); 1304 | document.execCommand('copy'); 1305 | document.body.removeChild(textArea); 1306 | } 1307 | } 1308 | }); 1309 | 1310 | menuItems.push({ type: 'separator' }); 1311 | } 1312 | 1313 | // Add common options 1314 | menuItems.push({ 1315 | label: 'Reload Page', 1316 | action: () => { 1317 | reloadWebview(); 1318 | } 1319 | }); 1320 | 1321 | menuItems.push({ 1322 | label: 'Go to Home', 1323 | action: () => { 1324 | goToHome(); 1325 | } 1326 | }); 1327 | 1328 | menuItems.push({ type: 'separator' }); 1329 | 1330 | menuItems.push({ 1331 | label: 'Inspect Element', 1332 | action: () => { 1333 | webview.openDevTools(); 1334 | } 1335 | }); 1336 | 1337 | // Build the menu 1338 | buildMenuItems(menu, menuItems); 1339 | } 1340 | 1341 | function buildBasicContextMenu(menu, webview) { 1342 | // Clear existing menu items 1343 | while (menu.firstChild) { 1344 | menu.removeChild(menu.firstChild); 1345 | } 1346 | 1347 | const menuItems = [ 1348 | { 1349 | label: 'Copy', 1350 | action: () => { 1351 | webview.copy(); 1352 | } 1353 | }, 1354 | { 1355 | label: 'Paste', 1356 | action: () => { 1357 | webview.paste(); 1358 | } 1359 | }, 1360 | { type: 'separator' }, 1361 | { 1362 | label: 'Reload Page', 1363 | action: () => { 1364 | reloadWebview(); 1365 | } 1366 | }, 1367 | { 1368 | label: 'Go to Home', 1369 | action: () => { 1370 | goToHome(); 1371 | } 1372 | }, 1373 | { type: 'separator' }, 1374 | { 1375 | label: 'Inspect Element', 1376 | action: () => { 1377 | webview.openDevTools(); 1378 | } 1379 | } 1380 | ]; 1381 | 1382 | buildMenuItems(menu, menuItems); 1383 | } 1384 | 1385 | function buildMenuItems(menu, menuItems) { 1386 | menuItems.forEach(item => { 1387 | if (item.type === 'separator') { 1388 | const separator = document.createElement('div'); 1389 | separator.style.cssText = 'height: 1px; background: var(--border-light); margin: 4px 8px;'; 1390 | menu.appendChild(separator); 1391 | } else { 1392 | const menuItem = document.createElement('div'); 1393 | menuItem.textContent = item.label; 1394 | menuItem.style.cssText = ` 1395 | padding: 8px 16px; 1396 | cursor: pointer; 1397 | transition: background-color 0.1s ease; 1398 | user-select: none; 1399 | `; 1400 | 1401 | menuItem.addEventListener('mouseenter', () => { 1402 | menuItem.style.backgroundColor = 'var(--bg-button-hover)'; 1403 | }); 1404 | 1405 | menuItem.addEventListener('mouseleave', () => { 1406 | menuItem.style.backgroundColor = 'transparent'; 1407 | }); 1408 | 1409 | menuItem.addEventListener('click', () => { 1410 | item.action(); 1411 | menu.remove(); 1412 | }); 1413 | 1414 | menu.appendChild(menuItem); 1415 | } 1416 | }); 1417 | } 1418 | 1419 | // Working context menu with proper clipboard functionality 1420 | function showWorkingContextMenu(x, y, webview) { 1421 | console.log('Creating working context menu at:', x, y); 1422 | 1423 | // Remove any existing context menus 1424 | const existingMenus = document.querySelectorAll('[id*="context-menu"]'); 1425 | existingMenus.forEach(menu => menu.remove()); 1426 | 1427 | const menu = document.createElement('div'); 1428 | menu.id = 'working-context-menu'; 1429 | menu.style.cssText = ` 1430 | position: fixed; 1431 | left: ${x}px; 1432 | top: ${y}px; 1433 | z-index: 999999; 1434 | background: #ffffff; 1435 | border: 1px solid #cccccc; 1436 | border-radius: 6px; 1437 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); 1438 | padding: 4px 0; 1439 | min-width: 200px; 1440 | font-family: -apple-system, BlinkMacSystemFont, sans-serif; 1441 | font-size: 13px; 1442 | color: #333333; 1443 | user-select: none; 1444 | `; 1445 | 1446 | // Check if we're in dark mode and adjust colors 1447 | if (document.body.classList.contains('dark-theme')) { 1448 | menu.style.background = '#2d2d2d'; 1449 | menu.style.color = '#ffffff'; 1450 | menu.style.borderColor = '#555555'; 1451 | } 1452 | 1453 | // Get context information from webview 1454 | const webviewRect = webview.getBoundingClientRect(); 1455 | const relativeX = x - webviewRect.left; 1456 | const relativeY = y - webviewRect.top; 1457 | 1458 | // First, create basic menu items 1459 | const menuItems = [ 1460 | { 1461 | label: 'Copy', 1462 | action: async () => { 1463 | try { 1464 | // Try to get selected text from webview 1465 | const selectedText = await webview.executeJavaScript('window.getSelection().toString()'); 1466 | if (selectedText && selectedText.trim()) { 1467 | const { clipboard } = require('electron'); 1468 | clipboard.writeText(selectedText); 1469 | console.log('Copied selected text:', selectedText); 1470 | } else { 1471 | // Use webview's built-in copy if no selection 1472 | webview.copy(); 1473 | console.log('Used webview copy'); 1474 | } 1475 | } catch (error) { 1476 | console.log('Copy error:', error); 1477 | // Fallback to webview copy 1478 | webview.copy(); 1479 | } 1480 | } 1481 | }, 1482 | { 1483 | label: 'Paste', 1484 | action: async () => { 1485 | try { 1486 | const { clipboard } = require('electron'); 1487 | const clipboardText = clipboard.readText(); 1488 | if (clipboardText) { 1489 | // Try inserting text at current cursor position in webview 1490 | await webview.executeJavaScript(` 1491 | document.execCommand('insertText', false, ${JSON.stringify(clipboardText)}) 1492 | `); 1493 | console.log('Pasted text:', clipboardText); 1494 | } 1495 | } catch (error) { 1496 | console.log('Paste error:', error); 1497 | // Fallback to webview paste 1498 | webview.paste(); 1499 | } 1500 | } 1501 | }, 1502 | { type: 'separator' }, 1503 | { 1504 | label: 'Reload Page', 1505 | action: () => { 1506 | reloadWebview(); 1507 | } 1508 | }, 1509 | { 1510 | label: 'Go to Home', 1511 | action: () => { 1512 | goToHome(); 1513 | } 1514 | }, 1515 | { type: 'separator' }, 1516 | { 1517 | label: 'Inspect Element', 1518 | action: () => { 1519 | webview.openDevTools(); 1520 | } 1521 | } 1522 | ]; 1523 | 1524 | // Try to get more context (links, images) asynchronously 1525 | webview.executeJavaScript(` 1526 | (function() { 1527 | try { 1528 | const targetElement = document.elementFromPoint(${relativeX}, ${relativeY}); 1529 | let linkUrl = ''; 1530 | let imageUrl = ''; 1531 | let selectedText = window.getSelection().toString().trim(); 1532 | 1533 | // Check for link 1534 | let element = targetElement; 1535 | while (element && element !== document.body) { 1536 | if (element.tagName === 'A' && element.href) { 1537 | linkUrl = element.href; 1538 | break; 1539 | } 1540 | element = element.parentElement; 1541 | } 1542 | 1543 | // Check for image 1544 | if (targetElement && targetElement.tagName === 'IMG' && targetElement.src) { 1545 | imageUrl = targetElement.src; 1546 | } 1547 | 1548 | return { 1549 | linkUrl: linkUrl, 1550 | imageUrl: imageUrl, 1551 | selectedText: selectedText, 1552 | hasSelection: selectedText.length > 0 1553 | }; 1554 | } catch (e) { 1555 | console.error('Error getting context:', e); 1556 | return { linkUrl: '', imageUrl: '', selectedText: '', hasSelection: false }; 1557 | } 1558 | })() 1559 | `).then(contextInfo => { 1560 | // Update menu with context-specific options 1561 | updateMenuWithContext(menu, contextInfo, menuItems, webview); 1562 | }).catch(() => { 1563 | // Just build the basic menu 1564 | buildMenuFromItems(menu, menuItems); 1565 | }); 1566 | 1567 | // Build basic menu initially 1568 | buildMenuFromItems(menu, menuItems); 1569 | 1570 | document.body.appendChild(menu); 1571 | 1572 | // Position menu properly within viewport 1573 | const rect = menu.getBoundingClientRect(); 1574 | let adjustedX = x; 1575 | let adjustedY = y; 1576 | 1577 | if (rect.right > window.innerWidth) { 1578 | adjustedX = x - rect.width; 1579 | } 1580 | if (rect.bottom > window.innerHeight) { 1581 | adjustedY = y - rect.height; 1582 | } 1583 | 1584 | menu.style.left = adjustedX + 'px'; 1585 | menu.style.top = adjustedY + 'px'; 1586 | 1587 | // Add click-outside listener to close menu 1588 | const closeMenu = (event) => { 1589 | if (!menu.contains(event.target)) { 1590 | menu.remove(); 1591 | document.removeEventListener('click', closeMenu); 1592 | document.removeEventListener('keydown', handleEscape); 1593 | } 1594 | }; 1595 | 1596 | const handleEscape = (event) => { 1597 | if (event.key === 'Escape') { 1598 | menu.remove(); 1599 | document.removeEventListener('click', closeMenu); 1600 | document.removeEventListener('keydown', handleEscape); 1601 | } 1602 | }; 1603 | 1604 | // Add listeners after a brief delay to prevent immediate closure 1605 | setTimeout(() => { 1606 | document.addEventListener('click', closeMenu); 1607 | document.addEventListener('keydown', handleEscape); 1608 | }, 50); 1609 | } 1610 | 1611 | function updateMenuWithContext(menu, contextInfo, basicItems, webview) { 1612 | // Clear existing items 1613 | while (menu.firstChild) { 1614 | menu.removeChild(menu.firstChild); 1615 | } 1616 | 1617 | const menuItems = []; 1618 | 1619 | // Add link-specific options 1620 | if (contextInfo.linkUrl) { 1621 | menuItems.push({ 1622 | label: 'Open Link in New Tab', 1623 | action: () => { 1624 | createNewTab(contextInfo.linkUrl, getPageTitle(contextInfo.linkUrl)); 1625 | } 1626 | }); 1627 | 1628 | menuItems.push({ 1629 | label: 'Open Link in New Window', 1630 | action: () => { 1631 | if (window.electronAPI && window.electronAPI.openNewWindow) { 1632 | window.electronAPI.openNewWindow(contextInfo.linkUrl); 1633 | } else { 1634 | createNewTab(contextInfo.linkUrl, getPageTitle(contextInfo.linkUrl)); 1635 | } 1636 | } 1637 | }); 1638 | 1639 | menuItems.push({ 1640 | label: 'Copy Link Address', 1641 | action: async () => { 1642 | try { 1643 | await navigator.clipboard.writeText(contextInfo.linkUrl); 1644 | console.log('Copied link:', contextInfo.linkUrl); 1645 | } catch (error) { 1646 | console.log('Failed to copy link:', error); 1647 | } 1648 | } 1649 | }); 1650 | 1651 | menuItems.push({ type: 'separator' }); 1652 | } 1653 | 1654 | // Add enhanced copy option 1655 | if (contextInfo.hasSelection && contextInfo.selectedText) { 1656 | menuItems.push({ 1657 | label: `Copy "${contextInfo.selectedText.substring(0, 30)}${contextInfo.selectedText.length > 30 ? '...' : ''}"`, 1658 | action: async () => { 1659 | try { 1660 | await navigator.clipboard.writeText(contextInfo.selectedText); 1661 | console.log('Copied selected text:', contextInfo.selectedText); 1662 | } catch (error) { 1663 | console.log('Failed to copy selection:', error); 1664 | } 1665 | } 1666 | }); 1667 | } else { 1668 | menuItems.push(basicItems[0]); // Regular copy 1669 | } 1670 | 1671 | // Add paste 1672 | menuItems.push(basicItems[1]); 1673 | 1674 | // Add image options 1675 | if (contextInfo.imageUrl) { 1676 | menuItems.push({ type: 'separator' }); 1677 | menuItems.push({ 1678 | label: 'Save Image As', 1679 | action: () => { 1680 | const link = document.createElement('a'); 1681 | link.href = contextInfo.imageUrl; 1682 | link.download = contextInfo.imageUrl.split('/').pop() || 'image'; 1683 | link.target = '_blank'; 1684 | document.body.appendChild(link); 1685 | link.click(); 1686 | document.body.removeChild(link); 1687 | } 1688 | }); 1689 | 1690 | menuItems.push({ 1691 | label: 'Copy Image Address', 1692 | action: async () => { 1693 | try { 1694 | await navigator.clipboard.writeText(contextInfo.imageUrl); 1695 | console.log('Copied image URL:', contextInfo.imageUrl); 1696 | } catch (error) { 1697 | console.log('Failed to copy image URL:', error); 1698 | } 1699 | } 1700 | }); 1701 | } 1702 | 1703 | // Add remaining basic items 1704 | menuItems.push({ type: 'separator' }); 1705 | menuItems.push(basicItems[3]); // Reload 1706 | menuItems.push(basicItems[4]); // Home 1707 | menuItems.push({ type: 'separator' }); 1708 | menuItems.push(basicItems[6]); // Inspect 1709 | 1710 | buildMenuFromItems(menu, menuItems); 1711 | } 1712 | 1713 | function buildMenuFromItems(menu, items) { 1714 | items.forEach(item => { 1715 | if (item.type === 'separator') { 1716 | const separator = document.createElement('div'); 1717 | separator.style.cssText = ` 1718 | height: 1px; 1719 | background: ${document.body.classList.contains('dark-theme') ? '#555555' : '#e0e0e0'}; 1720 | margin: 4px 8px; 1721 | `; 1722 | menu.appendChild(separator); 1723 | } else { 1724 | const menuItem = document.createElement('div'); 1725 | menuItem.textContent = item.label; 1726 | menuItem.style.cssText = ` 1727 | padding: 8px 16px; 1728 | cursor: pointer; 1729 | transition: background-color 0.1s ease; 1730 | user-select: none; 1731 | `; 1732 | 1733 | const isDark = document.body.classList.contains('dark-theme'); 1734 | 1735 | menuItem.addEventListener('mouseenter', () => { 1736 | menuItem.style.backgroundColor = isDark ? '#444444' : '#f0f0f0'; 1737 | }); 1738 | 1739 | menuItem.addEventListener('mouseleave', () => { 1740 | menuItem.style.backgroundColor = 'transparent'; 1741 | }); 1742 | 1743 | menuItem.addEventListener('click', (e) => { 1744 | e.stopPropagation(); 1745 | item.action(); 1746 | menu.remove(); 1747 | }); 1748 | 1749 | menu.appendChild(menuItem); 1750 | } 1751 | }); 1752 | } 1753 | 1754 | // Save session before closing 1755 | window.addEventListener('beforeunload', () => { 1756 | saveBrowserSession(); 1757 | }); 1758 | 1759 | 1760 | 1761 | --------------------------------------------------------------------------------