├── .git-blame-ignore-revs ├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── build.mjs ├── docs ├── CNAME ├── _config.yml ├── favicon.png ├── good_bye.png ├── google8ff6b6721332d32e.html ├── gpxfollower_512.png ├── index.md ├── privacy.html ├── uninstall.css └── uninstall.html ├── icons ├── bard │ ├── icon.pdn │ ├── icon_128.png │ └── icon_512.png ├── bingchat │ ├── icon_128.png │ └── icon_512.png └── optisearch │ ├── icon_128.png │ ├── icon_16.png │ └── icon_48.png ├── manifest_bard.json ├── manifest_bingchat.json ├── manifest_optisearch.json ├── package-lock.json ├── package.json ├── screenshots ├── bard │ ├── bigpromo.png │ ├── promo.pdn │ ├── promo.png │ ├── screenshot_apple.png │ ├── screenshot_code.png │ ├── screenshot_iphone.png │ ├── screenshot_jwst.png │ └── screenshot_premium.png ├── bingchat │ ├── bigpromo.png │ ├── promo.pdn │ ├── promo.png │ ├── screenshot_code.png │ ├── screenshot_jwst.png │ ├── screenshot_pancake.png │ └── screenshot_premium.png ├── dark_mode_stack.png ├── description.txt ├── ecosia_plot.png ├── filter_py.png ├── icon_128.pdn ├── icon_512.png ├── jaune_lyrics.png ├── large_promo.pdn ├── large_promo.png ├── marquee.pdn ├── marquee.png ├── screenshot2.png ├── screenshot3.png ├── screenshot4.png ├── screenshot_ecosia_createElement.png ├── screenshot_ecosia_lyrics.png ├── small_promo.pdn ├── small_promo.png ├── ss_bard_1.png ├── ss_bard_2.png └── store_icon.png ├── src ├── _locales.json ├── background │ ├── background.js │ ├── background_extpay.js │ ├── background_loader.js │ ├── background_loader_firefox.html │ └── websocket_utils.js ├── chat │ ├── bard_session.js │ ├── bingchat_session.js │ ├── chat_session.js │ ├── chatgpt_session.js │ ├── init.js │ ├── message.js │ └── offscreen │ │ ├── bing_socket.js │ │ ├── iframe_script.js │ │ ├── offscreen.html │ │ └── offscreen.js ├── constants.js ├── context.js ├── engine-specifics.js ├── engines.json ├── images │ ├── bard.png │ ├── bard_conversation_example.png │ ├── bingchat.png │ ├── bingchat_conversation_example.png │ ├── chatgpt.png │ ├── chatgpt_conversation_example.png │ ├── copilot.png │ ├── engines │ │ ├── Baidu.png │ │ ├── Bing.png │ │ ├── Brave Search.png │ │ ├── DuckDuckGo.png │ │ ├── Ecosia.png │ │ ├── Google.png │ │ └── Yahoo.png │ ├── genius.png │ ├── gpxfollower.png │ ├── mathworks.ico │ ├── mdn.png │ ├── optisearch_conversation_example.png │ ├── stackexchange.ico │ ├── unity.ico │ ├── w3schools.ico │ └── wikipedia.ico ├── libs │ ├── ExtPay.js │ ├── drawdown.js │ ├── highlight.min.js │ ├── sha3.min.js │ └── tex-svg.js ├── popup │ ├── popup.css │ ├── popup.html │ ├── popup.js │ ├── premium.css │ ├── premium.html │ └── premium.js ├── run.js ├── settings.js ├── sites │ ├── cplusplus.js │ ├── genius.js │ ├── mathworks.js │ ├── mdn.js │ ├── results.js │ ├── sites.js │ ├── stackoverflow.js │ ├── unity.js │ ├── w3schools.js │ └── wikipedia.js ├── styles │ ├── box.css │ ├── chatgpt.css │ ├── code-dark-theme.css │ ├── code-light-theme.css │ ├── genius.css │ ├── panel.css │ ├── w3schools.css │ └── wikipedia.css └── utils.js └── tests └── test.js /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # [REF] Format all files 2 | f40140ce3f45131af841df96f8502e09372b3405 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /versions/* 2 | node_modules/ 3 | manifest.json 4 | _metadata/ 5 | build/ 6 | _locales/ 7 | .*/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OptiSearch | Copilot in Google | Gemini next to Google results 2 | 3 | ![License](https://img.shields.io/github/license/dj0ulo/optisearch) 4 | 5 | This repository contains the code of **OptiSearch**, **Copilot in Google** and **Gemini next to Google results** browser extensions. They share the same codebase core. 6 | 7 | ### OptiSearch icon OptiSearch 8 | ![License](https://img.shields.io/chrome-web-store/users/bbojmeobdaicehcopocnfhaagefleiae?label=Chrome%20Users) ![License](https://img.shields.io/amo/users/optisearch?label=Firefox%20Users) 9 | 10 | Displays relevant informations from search engine results directly alongside them. 11 | 12 | [Install from Chrome Web Store](https://chrome.google.com/webstore/detail/optisearch/bbojmeobdaicehcopocnfhaagefleiae) 13 | 14 | [Install from Mozilla Add-on Store](https://addons.mozilla.org/fr/firefox/addon/optisearch/) 15 | 16 | ### Copilot in Google icon Copilot in Google 17 | ![License](https://img.shields.io/chrome-web-store/users/pcnhobmoglanpljipbomknafhdlcgcng?label=Chrome%20Users) ![License](https://img.shields.io/amo/users/bing-chat-gpt-4-in-google?label=Firefox%20Users) 18 | 19 | Displays the answer from Microsoft Copilot alongside search engine results. 20 | 21 | [Install from Chrome Web Store](https://chrome.google.com/webstore/detail/bing-chat-gpt-4-in-google/pcnhobmoglanpljipbomknafhdlcgcng) 22 | 23 | [Install from Mozilla Add-on Store](https://addons.mozilla.org/fr/firefox/addon/bing-chat-gpt-4-in-google/) 24 | 25 | ### Gemini next to Google results Gemini next to Google results 26 | ![License](https://img.shields.io/chrome-web-store/users/pkdmfoabhnkpkcacnmgilaeghiggdbgf?label=Chrome%20Users) ![License](https://img.shields.io/amo/users/bard-for-search-engines?label=Firefox%20Users) 27 | 28 | Displays the answer from Google Gemini alongside search engine results. 29 | 30 | [Install from Chrome Web Store](https://chrome.google.com/webstore/detail/bard-for-search-engines/pkdmfoabhnkpkcacnmgilaeghiggdbgf) 31 | 32 | [Install from Mozilla Add-on Store](https://addons.mozilla.org/fr/firefox/addon/bard-for-search-engines/) 33 | 34 | ## Supported Search Engines 35 | Google, Bing, Baidu, DuckDuckGo, Ecosia, Brave Search 36 | 37 | ## Build from source 38 | 1. Clone repo 39 | 1. Install dependencies with: 40 | ```sh 41 | npm i 42 | ``` 43 | 1. You can load both extension in your favorite browser directly from the root of the repo, you just need to build the manifest first, use the flag `-f` to build the manifest for Firefox. 44 | 45 | E.g. to build the firefox manifest for **Copilot in Google**: 46 | ``` 47 | node build.mjs bingchat -f 48 | ``` 49 | 50 | 1. You can copy the source for a given extension with the flag `-b` followed by the name of the desired directory (default: `build/`). 51 | 52 | E.g. to copy **OptiSearch** sources for Chrome in *DIR* 53 | ``` 54 | node build.mjs optisearch -b DIR 55 | ``` 56 | E.g. to copy **Gemini next to Google results** sources for Firefox in `build/bard` 57 | ``` 58 | node build.mjs bard -bf 59 | ``` 60 | or 61 | ``` 62 | node build.mjs bard -b -f 63 | ``` 64 | 1. You can create a zip from the source using the flag `-z` followed by the output filename. This will also copy the sources in the default build directory (unless you specified another one with `-b`). Use `-t` (for "tidy") to delete the build directory after the operation. 65 | 66 | 1. Finally, to build and zip all extensions for Chrome and Firefox and put them in the `versions` directory: 67 | ``` 68 | npm run build 69 | ``` 70 | 71 | ## Contributing 72 | You are welcome to make a **PR** or post an **Issue**, I will look at them as soon as I can ! 73 | 74 | ## Donate 75 | I made this extension on my free time, if it is useful for you [please consider sending me a tip on paypal](https://www.paypal.com/donate?hosted_button_id=VPF2BYBDBU5AA). 76 | -------------------------------------------------------------------------------- /build.mjs: -------------------------------------------------------------------------------- 1 | #!node 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import archiver from 'archiver'; 5 | 6 | const names = ['optisearch', 'bingchat', 'bard']; 7 | 8 | const usage = 9 | `Usage: node build.mjs [name] [-f] [-b ] [-z ] [-t] 10 | 11 | name: [ ${names.join(' | ')} ] 12 | -f: build for Firefox 13 | -b: copy the files to 14 | -z: create the output zip file to 15 | -t: tidy up build dir`; 16 | 17 | const ADDITIONAL_FILES = { 18 | 'optisearch': [], 19 | 'bingchat': ['src/chat/offscreen/*'], 20 | 'bard': [], 21 | 'all': ['src/images/gpxfollower.png'], 22 | } 23 | const ADDITIONAL_FILES_V2 = { 24 | 'optisearch': [], 25 | 'bingchat': [], 26 | 'bard': [], 27 | 'all': [], 28 | } 29 | const OFFSCREEN_DOC = { 30 | 'optisearch': null, 31 | 'bingchat': 'src/background/background_loader_firefox.html', 32 | 'bard': null, 33 | } 34 | const PERMISSION_V2 = { 35 | 'optisearch': ["https://extensionpay.com/*"], 36 | 'bingchat': ["https://extensionpay.com/*"], 37 | 'bard': ["https://extensionpay.com/*"], 38 | } 39 | 40 | 41 | let name = ''; 42 | 43 | (async function main() { 44 | name = names.find(n => parseArg(n)); 45 | if (!name) { 46 | errorUsage(); 47 | } 48 | 49 | const firefox = parseArg('-f'); 50 | buildManifest(`manifest_${name}.json`, firefox); 51 | console.log(`${name} manifest for ${firefox ? 'Firefox' : 'Chrome'} created`); 52 | 53 | const mf = readJsonFile('manifest.json'); 54 | 55 | if (fs.existsSync('_locales')) { 56 | fs.rmSync('_locales', { recursive: true }); 57 | } 58 | if (mf.default_locale){ 59 | makeLocalesDir(`src/_locales.json`) 60 | } 61 | 62 | const isCopyToBuildDir = parseArg('-b'); 63 | const buildDir = typeof isCopyToBuildDir === 'string' ? isCopyToBuildDir : `build/${name}${firefox ? '_firefox' : ''}`; 64 | const makeZip = parseArg('-z'); 65 | 66 | if (isCopyToBuildDir || makeZip) { 67 | copyToBuildDir(buildDir); 68 | console.log(`Source copied to "${buildDir}" directory`); 69 | } 70 | 71 | if (makeZip) { 72 | const out = typeof makeZip === 'string' ? makeZip : `versions/${name}_${mf.version}${firefox ? '_firefox' : ''}.zip`; 73 | await zipDir(buildDir, out); 74 | console.log(`Extension zipped into "${out}"`); 75 | } 76 | 77 | if (parseArg('-t')) { 78 | fs.rmSync(buildDir, { recursive: true }); 79 | console.log(`Directory "${buildDir}" cleaned`); 80 | } 81 | 82 | console.log(); 83 | })(); 84 | 85 | 86 | /** 87 | * Builds the manifest.json file from a manifest file in version 3. 88 | * This function does not exhaustively copy all possible fields. 89 | * @param {string} pathManifestV3 Path to the manifest file in version 3. 90 | * @param {boolean} firefox build for Firefox 91 | */ 92 | function buildManifest(pathManifestV3, firefox = false) { 93 | if (!firefox) { 94 | fs.copyFileSync(pathManifestV3, 'manifest.json'); 95 | return; 96 | } 97 | 98 | const mfv3 = readJsonFile(pathManifestV3); 99 | const mfv2 = { manifest_version: 2 }; 100 | const fields = ['name', 'version', 'description', 'default_locale', 'author', 'icons', 101 | 'background', 'content_scripts']; 102 | 103 | fields.forEach(f => { 104 | if (!(f in mfv3)) 105 | return; 106 | mfv2[f] = mfv3[f]; 107 | }); 108 | 109 | if ('action' in mfv3) { 110 | mfv2['browser_action'] = mfv3['action']; 111 | } 112 | if(OFFSCREEN_DOC[name]) { 113 | mfv2['background'] = { page: OFFSCREEN_DOC[name] }; 114 | } else if ('background' in mfv3) { 115 | const scripts = parseBackgroundScripts(mfv3['background']['service_worker']) 116 | mfv2['background'] = { scripts }; 117 | } 118 | 119 | if ('permissions' in mfv3) { 120 | mfv2['permissions'] = []; 121 | const permissions = mfv2['permissions']; 122 | mfv3['permissions'].forEach(p => { 123 | if (p.startsWith('declarativeNetRequest')) { 124 | !permissions.includes('webRequest') && permissions.push('webRequest'); 125 | !permissions.includes('webRequestBlocking') && permissions.push('webRequestBlocking'); 126 | } else if (p !== 'offscreen') { 127 | permissions.push(p); 128 | } 129 | }); 130 | } 131 | 132 | if ('host_permissions' in mfv3) { 133 | mfv2['permissions'] ??= []; 134 | mfv2['permissions'] = [...mfv2['permissions'], ...mfv3['host_permissions']]; 135 | } 136 | if (PERMISSION_V2[name]) { 137 | mfv2['permissions'] = [...mfv2['permissions'], ...PERMISSION_V2[name]]; 138 | } 139 | 140 | if ('web_accessible_resources' in mfv3) { 141 | mfv2['web_accessible_resources'] = []; 142 | mfv3['web_accessible_resources'].forEach(war => { 143 | mfv2['web_accessible_resources'] = [...mfv2['web_accessible_resources'], ...war['resources']]; 144 | }); 145 | } 146 | 147 | // Write the version 2 manifest file 148 | fs.writeFileSync('manifest.json', JSON.stringify(mfv2, null, 4)); 149 | } 150 | 151 | /** 152 | * Parse files from manifest.json and copy them to the build folder. 153 | * @param {string} buildDir 154 | */ 155 | function copyToBuildDir(buildDir) { 156 | const mf = readJsonFile('manifest.json'); 157 | 158 | // get content script js and web-accessible resources 159 | const contentScripts = mf['content_scripts']; 160 | let scripts = []; 161 | for (let cs of contentScripts) { 162 | scripts = scripts.concat(cs['js']); 163 | } 164 | 165 | let resources = []; 166 | for (let res of mf['web_accessible_resources']) { 167 | if (typeof res === 'object') { 168 | resources = resources.concat(res['resources']); 169 | } else { 170 | resources.push(res); 171 | } 172 | } 173 | 174 | // add additional files as resources to enable directory 175 | for (let file of ADDITIONAL_FILES[name].concat(ADDITIONAL_FILES.all)) { 176 | resources.push(file); 177 | } 178 | if (mf.manifest_version === 2) { 179 | for (let file of ADDITIONAL_FILES_V2[name].concat(ADDITIONAL_FILES_V2.all)) { 180 | resources.push(file); 181 | } 182 | } 183 | 184 | let popupHtml = ''; 185 | if ('action' in mf) { 186 | popupHtml = mf['action']['default_popup']; 187 | } else if ('browser_action' in mf) { 188 | popupHtml = mf['browser_action']['default_popup']; 189 | } 190 | 191 | let files = []; 192 | if ('background' in mf) { 193 | if ('service_worker' in mf['background']) { 194 | files.push(mf['background']['service_worker']); 195 | files.push(...parseBackgroundScripts(mf['background']['service_worker'])); 196 | } 197 | if ('scripts' in mf['background']) { 198 | files = files.concat(mf['background']['scripts']); 199 | } 200 | if ('page' in mf['background']) { 201 | files.push(mf['background']['page']); 202 | files.push(...parseBackgroundScripts(mf['background']['page'])); 203 | } 204 | } 205 | 206 | files = files.concat(scripts); 207 | 208 | if ('declarative_net_request' in mf) { 209 | for (let r of mf['declarative_net_request']['rule_resources']) { 210 | files.push(r['path']); 211 | } 212 | } 213 | 214 | files = files.concat(Object.values(mf['icons'])); 215 | 216 | // loop in resources and add them to files, if one is directory, add all files in it 217 | for (let resource of resources) { 218 | if (resource.slice(-1) === '*' && fs.lstatSync(resource.slice(0, -1)).isDirectory()) { 219 | const dir = resource.slice(0, -1); 220 | for (let file of fs.readdirSync(dir)) { 221 | if (fs.lstatSync(path.join(dir, file)).isFile()) { 222 | files.push(path.join(dir, file)); 223 | } 224 | } 225 | } else { 226 | files.push(resource); 227 | } 228 | } 229 | // get parent dir of popupHtml 230 | const popupParentDir = path.dirname(popupHtml); 231 | 232 | // add popupParentDir files to files 233 | for (let file of fs.readdirSync(popupParentDir)) { 234 | files.push(path.join(popupParentDir, file)); 235 | } 236 | 237 | if (buildDir.slice(-1) !== '/') { 238 | buildDir += '/'; 239 | } 240 | // delete all files in src folder 241 | if (fs.existsSync(buildDir)) 242 | fs.rmSync(buildDir, { recursive: true }); 243 | fs.mkdirSync(buildDir, { recursive: true }); 244 | 245 | // copy files to src folder, conserve the folder structure, creates directory based on the path of the file 246 | fs.copyFileSync('manifest.json', path.join(buildDir, 'manifest.json')); 247 | if(fs.existsSync('_locales')) 248 | copyDir('_locales', path.join(buildDir, '_locales')); 249 | fs.mkdirSync(path.join(buildDir, 'src')); 250 | 251 | for (let file of files) { 252 | const directory = path.join(buildDir, path.dirname(file)); 253 | if (!fs.existsSync(directory)) { 254 | fs.mkdirSync(directory, { recursive: true }); 255 | } 256 | const filename = path.basename(file); 257 | fs.copyFileSync(file, path.join(directory, filename)); 258 | } 259 | } 260 | 261 | function makeLocalesDir(localesFile) { 262 | fs.mkdirSync('_locales'); 263 | const json = readJsonFile(localesFile); 264 | const locales = {}; 265 | for (let [key, types] of Object.entries(json)) { 266 | for (let [lang, value] of Object.entries(types.message ?? types)) { 267 | locales[lang] ??= {}; 268 | locales[lang][key.replaceAll(/[^\w]/g, '_')] = { 269 | message: value, 270 | ...(types.placeholders ? { placeholders: types.placeholders } : {}), 271 | }; 272 | } 273 | } 274 | Object.entries(locales).forEach(([lang, entries]) => { 275 | fs.mkdirSync(`_locales/${lang}`); 276 | const jsonToSave = {}; 277 | for (let [key, value] of Object.entries(entries)) { 278 | jsonToSave[key] = value; 279 | } 280 | fs.writeFileSync(`_locales/${lang}/messages.json`, JSON.stringify(jsonToSave, null, 4)); 281 | }) 282 | } 283 | 284 | /** 285 | * Parse all imports from a background loader 286 | */ 287 | function parseBackgroundScripts(backgroundLoaderPath) { 288 | const backgroundLoader = fs.readFileSync(backgroundLoaderPath, 'utf8'); 289 | const dirBackgroundLoader = path.dirname(backgroundLoaderPath); 290 | const regexImportFiles = /(?!import|src).*?["'`](.*?)["'`]/g; 291 | return (backgroundLoader.match(regexImportFiles) ?? []) 292 | .map(f => `${dirBackgroundLoader}/${f.replace(regexImportFiles, '$1')}`) 293 | .filter(f => fs.existsSync(f)) 294 | } 295 | 296 | function readJsonFile(path) { 297 | return JSON.parse(fs.readFileSync(path, 'utf8')); 298 | } 299 | 300 | 301 | /** 302 | * Copy a directory recursively 303 | * @param {string} src 304 | * @param {string} dest 305 | */ 306 | function copyDir(src, dest) { 307 | fs.mkdirSync(dest, { recursive: true }); 308 | const entries = fs.readdirSync(src, { withFileTypes: true }); 309 | 310 | for (let entry of entries) { 311 | const srcPath = path.join(src, entry.name); 312 | const destPath = path.join(dest, entry.name); 313 | 314 | if (entry.isDirectory()) { 315 | copyDir(srcPath, destPath); 316 | } else { 317 | fs.copyFileSync(srcPath, destPath); 318 | } 319 | } 320 | } 321 | 322 | async function zipDir(dir, out) { 323 | fs.mkdirSync(path.dirname(out), { recursive: true }); 324 | const output = fs.createWriteStream(out); 325 | const archive = archiver('zip', { 326 | zlib: { level: 9 }, 327 | }); 328 | archive.pipe(output); 329 | archive.directory(dir, false); 330 | await archive.finalize(); 331 | } 332 | 333 | function parseArg(argName) { 334 | const index = process.argv.indexOf(argName); 335 | if (index === -1) { 336 | if (argName.length === 2 && argName[0] === '-') { 337 | return !!process.argv.find(arg => arg[0] === '-' && arg.includes(argName[1])); 338 | } 339 | return false; 340 | } 341 | if (process.argv[index][0] === '-' && index < process.argv.length - 1 && process.argv[index + 1][0] !== '-') { 342 | return process.argv[index + 1]; 343 | } 344 | return true; 345 | } 346 | 347 | function errorUsage() { 348 | console.log(usage); 349 | process.exit(1); 350 | } 351 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | www.optisearch.io -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /docs/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/docs/favicon.png -------------------------------------------------------------------------------- /docs/good_bye.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/docs/good_bye.png -------------------------------------------------------------------------------- /docs/google8ff6b6721332d32e.html: -------------------------------------------------------------------------------- 1 | google-site-verification: google8ff6b6721332d32e.html -------------------------------------------------------------------------------- /docs/gpxfollower_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/docs/gpxfollower_512.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # OptiSearch 2 | 3 | ## Description 4 | Save some time and energy when you work ! 5 | 6 | OptiSearch brings the useful content from the Google results and displays it alongside them. 7 | 8 | It can show you the most pertinent StackOverflow answer, give you an example from W3Schools and even ask ChatGPT ! 9 | 10 | Currently, the following sites are supported: 11 | - ChatGPT 12 | - All StackExchange sites 13 | - Wikipedia 14 | - W3Schools 15 | - MDN Web Docs 16 | - Genius 17 | - Unity Answers 18 | - MATLAB Answers 19 | 20 | In bonus, it also works with Bing, DuckDuckGo, Ecosia and Brave Search ! 21 | 22 | ## Privacy Policy 23 | You can read the privacy policy [by clicking here](./privacy.html). -------------------------------------------------------------------------------- /docs/privacy.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Privacy Policy 6 | 7 | 34 | 35 | 36 | 37 |
38 | 45 |
46 |
47 |

Privacy Policy

48 |

Personal data

49 |

50 | OptiSearch does not collect searching history, browser history, or any other personal data. 51 |

52 |

53 | In future OptiSearch may collect browser version, platform name, display settings and user's OptiSearch settings 54 | (but never data which can help identify user). Such information could be needed for decision 55 | on implementing or removing features as well as finding bugs. 56 |

57 |

58 | It will happen only with your permission. 59 |

60 | 61 |

Local Storage

62 |

63 | OptiSearch uses Chrome (Chromium) or WebExtensions Storage Sync API for storing user's settings, as well as 64 | files 65 | (written by the developpers of the extension) fetched from 66 | Github Gist necessary 67 | for the extension to work. 68 |

69 | 70 |

Reliance on External Services

71 | 72 |

OptiSearch, Copilot in Google, and Gemini next to Google results ("the Extensions") 73 | rely on external services and libraries, including ExtPay.js, which utilizes Stripe for 74 | payment processing and PayPal for donation processing, to provide certain features and 75 | functionalities, such as processing payments for premium memberships and accepting donations. 76 | Additionally, the Extensions integrate with third-party applications, including Microsoft Copilot, ChatGPT, and Google Gemini, 77 | to enhance their functionalities and user experiences. While we make every effort to ensure the smooth operation of the Extensions, 78 | it is important to understand that we may not have full control over these external services, and they may be subject to changes, disruptions, or discontinuation by their respective providers. 79 |

80 | 81 |

Changes to External Services

82 | 83 |

The providers of external services and libraries, including ExtPay.js, Stripe, and PayPal, 84 | may update their services, change their policies, or cease to operate at any time without prior notice. 85 | These changes may impact the functionality of the Extensions, including premium membership payment 86 | processing, donation processing, and other related services. Similarly, third-party applications such as Microsoft Copilot, ChatGPT, and Google Gemini may introduce updates or changes that affect the integration with the Extensions. 87 |

88 | 89 |

Liability and Continuity

90 | 91 |

We will strive to adapt to any changes or disruptions in external services, third-party applications, and payment processors to minimize any potential impact on the Extensions and its users. However, we cannot guarantee the uninterrupted availability or performance of these external services, payment processors, and third-party applications, and we shall not be held liable for any issues arising from such changes or disruptions. 92 |

93 | 94 |

User Responsibility

95 | 96 |

Users of the Extensions are responsible for reviewing the terms and policies of external services, such as Stripe and PayPal, and third-party applications, including Microsoft Copilot, ChatGPT, and Google Gemini, that they interact with through the Extensions. Users should also ensure that they comply with the terms and conditions of these services and applications. 97 |

98 | 99 |

The links to the terms and policies of those services are listed below:

100 | 107 | 108 |

Contact

109 |

110 | If you have any questions or comments about this privacy policy, you may contact me at my email address 111 | info.optisearch@gmail.com. 112 |

113 |

114 | March 2024 115 |

116 |
117 | 118 | 119 | -------------------------------------------------------------------------------- /docs/uninstall.css: -------------------------------------------------------------------------------- 1 | /* :root { 2 | --color: #222; 3 | --bg: rgb(215, 210, 204); 4 | --darker-bg: #eee; 5 | } */ 6 | 7 | :root { 8 | --color: rgb(215, 210, 204); 9 | --bg: #1c1c1c; 10 | --darker-bg: #111111; 11 | --dark-grey: rgb(51, 51, 51); 12 | --grey: #111; 13 | --dark-grey: #ddd; 14 | --lighter-grey: #222; 15 | } 16 | 17 | body { 18 | background-color: var(--bg); 19 | color: var(--color); 20 | display: flex; 21 | flex-direction: column; 22 | align-items: center; 23 | font-family: Open Sans, Arial, system-ui; 24 | overflow-y: hidden; 25 | } 26 | 27 | 28 | 29 | #goodbye { 30 | background-image: url('good_bye.png'); 31 | width: 100%; 32 | height: 100%; 33 | position: absolute; 34 | z-index: -10; 35 | left: 0; 36 | } 37 | #over_goodbye { 38 | background: #111e; 39 | width: 100%; 40 | height: 100%; 41 | position: absolute; 42 | z-index: -5; 43 | color: transparent; 44 | } 45 | 46 | h1, h3{ 47 | text-align: center; 48 | } 49 | 50 | h1 { 51 | margin: 3em 0; 52 | font-size: 3em; 53 | } 54 | 55 | h3 { 56 | margin: 1em 0; 57 | font-size: 2em; 58 | } 59 | 60 | /* GPX Follower ad */ 61 | #gpxfollower { 62 | text-align: left; 63 | display: flex; 64 | flex-direction: column; 65 | align-items: flex-start; /* Align items to the start (left) */ 66 | background-color: #fff; 67 | padding: 0.8em 1em; 68 | margin: 1em; 69 | text-decoration: none; 70 | box-shadow: rgb(0 0 0 / 25%) 0px 3px 20px 5px; 71 | border-radius: .5em; 72 | } 73 | 74 | #gpxfollower > #also-from { 75 | font-size: 0.9em; 76 | color: var(--lighter-grey); 77 | margin-bottom: .5em; /* Spacing below "Also from OptiSearch" */ 78 | } 79 | 80 | .gpxfollower-app { 81 | display: flex; 82 | align-items: center; 83 | } 84 | 85 | .gpxfollower-app .logo { 86 | width: 6em; /* Adjust as needed */ 87 | height: auto; 88 | border-radius: 20%; 89 | } 90 | 91 | #gpxfollower:hover .text-app .app-name { 92 | text-decoration: underline; 93 | } 94 | 95 | .text-app .app-name { 96 | font-weight: bold; 97 | font-size: 1.2em; 98 | margin-bottom: 0.4em; 99 | } 100 | .text-app { 101 | display: flex; 102 | flex-direction: column; 103 | padding: .5em; 104 | margin: 0 2em; 105 | } 106 | 107 | .text-app .app-desc { 108 | color: var(--lighter-grey); 109 | } 110 | 111 | .icon-container { 112 | position: relative; 113 | display: inline-block; /* Ensure the container wraps around the icon and text */ 114 | } 115 | 116 | .googleplay { 117 | height: 5em; 118 | width: max-content; 119 | margin: -.6em; 120 | margin-top: .5em; 121 | } 122 | 123 | .new-text { 124 | position: absolute; 125 | top: 0; 126 | right: 0; 127 | font-weight: bold; 128 | color: var(--color); 129 | padding: 3px 5px; /* Adjust as needed */ 130 | background-color: red; /* Adjust as needed */ 131 | animation: bounce 0.5s infinite alternate; /* Adjust duration and timing as needed */ 132 | } 133 | 134 | @keyframes bounce { 135 | 0% { 136 | transform: translate(20%, 20%) rotate(40deg) scale(0.8); /* Initial scale */ 137 | } 138 | 100% { 139 | transform: translate(20%, 20%) rotate(40deg) scale(.9); /* Scale at the peak of the bounce */ 140 | } 141 | } -------------------------------------------------------------------------------- /docs/uninstall.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Bye, bye! 6 | 7 | 8 | 9 | 10 |
11 |
12 |

We are so sorry to see you go!

13 |

Love getting active outdoors?
You might want to try our new WearOS and Android app :

14 | 15 | Also from OptiSearch 16 |
17 |
18 | 19 | NEW 20 |
21 |
22 | Import GPX from Strava, Komoot 23 | Import GPX routes from Strava & Komoot and follow them on your watch! 24 | Get it on Google Play 25 |
26 |
27 |
28 | 29 | -------------------------------------------------------------------------------- /icons/bard/icon.pdn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/icons/bard/icon.pdn -------------------------------------------------------------------------------- /icons/bard/icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/icons/bard/icon_128.png -------------------------------------------------------------------------------- /icons/bard/icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/icons/bard/icon_512.png -------------------------------------------------------------------------------- /icons/bingchat/icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/icons/bingchat/icon_128.png -------------------------------------------------------------------------------- /icons/bingchat/icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/icons/bingchat/icon_512.png -------------------------------------------------------------------------------- /icons/optisearch/icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/icons/optisearch/icon_128.png -------------------------------------------------------------------------------- /icons/optisearch/icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/icons/optisearch/icon_16.png -------------------------------------------------------------------------------- /icons/optisearch/icon_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/icons/optisearch/icon_48.png -------------------------------------------------------------------------------- /manifest_bard.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "__MSG_bardName__", 3 | "version": "2.5.2", 4 | "manifest_version": 3, 5 | "description": "__MSG_bardDesc__", 6 | "default_locale": "en", 7 | "permissions": [ 8 | "storage" 9 | ], 10 | "host_permissions": [ 11 | "https://gemini.google.com/" 12 | ], 13 | "action": { 14 | "default_popup": "src/popup/popup.html" 15 | }, 16 | "icons": { 17 | "128": "icons/bard/icon_128.png" 18 | }, 19 | "web_accessible_resources": [{ 20 | "resources": [ 21 | "src/engines.json", 22 | "src/styles/chatgpt.css", 23 | "src/styles/box.css", 24 | "src/styles/panel.css", 25 | "src/styles/code-light-theme.css", 26 | "src/styles/code-dark-theme.css", 27 | "src/images/bard.png", 28 | "src/images/bard_conversation_example.png", 29 | "src/images/engines/*", 30 | "icons/optisearch/icon_128.png", 31 | "icons/bingchat/icon_128.png", 32 | "icons/bard/icon_128.png" 33 | ], 34 | "matches": [ 35 | "*://duckduckgo.com/*", 36 | "*://www.bing.com/*", 37 | "*://www.ecosia.org/*", 38 | "*://search.brave.com/*", 39 | "*://www.baidu.com/*", 40 | "*://www.google.com/*", "*://www.google.ac/*", "*://www.google.ad/*", "*://www.google.ae/*", "*://www.google.com.af/*", "*://www.google.com.ag/*", "*://www.google.com.ai/*", "*://www.google.al/*", "*://www.google.am/*", "*://www.google.co.ao/*", "*://www.google.com.ar/*", "*://www.google.as/*", "*://www.google.at/*", "*://www.google.com.au/*", "*://www.google.az/*", "*://www.google.ba/*", "*://www.google.com.bd/*", "*://www.google.be/*", "*://www.google.bf/*", "*://www.google.bg/*", "*://www.google.com.bh/*", "*://www.google.bi/*", "*://www.google.bj/*", "*://www.google.com.bn/*", "*://www.google.com.bo/*", "*://www.google.com.br/*", "*://www.google.bs/*", "*://www.google.bt/*", "*://www.google.co.bw/*", "*://www.google.by/*", "*://www.google.com.bz/*", "*://www.google.ca/*", "*://www.google.com.kh/*", "*://www.google.cc/*", "*://www.google.cd/*", "*://www.google.cf/*", "*://www.google.cat/*", "*://www.google.cg/*", "*://www.google.ch/*", "*://www.google.ci/*", "*://www.google.co.ck/*", "*://www.google.cl/*", "*://www.google.cm/*", "*://www.google.cn/*", "*://www.google.com.co/*", "*://www.google.co.cr/*", "*://www.google.com.cu/*", "*://www.google.cv/*", "*://www.google.com.cy/*", "*://www.google.cz/*", "*://www.google.de/*", "*://www.google.dj/*", "*://www.google.dk/*", "*://www.google.dm/*", "*://www.google.com.do/*", "*://www.google.dz/*", "*://www.google.com.ec/*", "*://www.google.ee/*", "*://www.google.com.eg/*", "*://www.google.es/*", "*://www.google.com.et/*", "*://www.google.fi/*", "*://www.google.com.fj/*", "*://www.google.fm/*", "*://www.google.fr/*", "*://www.google.ga/*", "*://www.google.ge/*", "*://www.google.gf/*", "*://www.google.gg/*", "*://www.google.com.gh/*", "*://www.google.com.gi/*", "*://www.google.gl/*", "*://www.google.gm/*", "*://www.google.gp/*", "*://www.google.gr/*", "*://www.google.com.gt/*", "*://www.google.gy/*", "*://www.google.com.hk/*", "*://www.google.hn/*", "*://www.google.hr/*", "*://www.google.ht/*", "*://www.google.hu/*", "*://www.google.co.id/*", "*://www.google.iq/*", "*://www.google.ie/*", "*://www.google.co.il/*", "*://www.google.im/*", "*://www.google.co.in/*", "*://www.google.io/*", "*://www.google.is/*", "*://www.google.it/*", "*://www.google.je/*", "*://www.google.com.jm/*", "*://www.google.jo/*", "*://www.google.co.jp/*", "*://www.google.co.ke/*", "*://www.google.ki/*", "*://www.google.kg/*", "*://www.google.co.kr/*", "*://www.google.com.kw/*", "*://www.google.kz/*", "*://www.google.la/*", "*://www.google.com.lb/*", "*://www.google.com.lc/*", "*://www.google.li/*", "*://www.google.lk/*", "*://www.google.co.ls/*", "*://www.google.lt/*", "*://www.google.lu/*", "*://www.google.lv/*", "*://www.google.com.ly/*", "*://www.google.co.ma/*", "*://www.google.md/*", "*://www.google.me/*", "*://www.google.mg/*", "*://www.google.mk/*", "*://www.google.ml/*", "*://www.google.com.mm/*", "*://www.google.mn/*", "*://www.google.ms/*", "*://www.google.com.mt/*", "*://www.google.mu/*", "*://www.google.mv/*", "*://www.google.mw/*", "*://www.google.com.mx/*", "*://www.google.com.my/*", "*://www.google.co.mz/*", "*://www.google.com.na/*", "*://www.google.ne/*", "*://www.google.com.nf/*", "*://www.google.com.ng/*", "*://www.google.com.ni/*", "*://www.google.nl/*", "*://www.google.no/*", "*://www.google.com.np/*", "*://www.google.nr/*", "*://www.google.nu/*", "*://www.google.co.nz/*", "*://www.google.com.om/*", "*://www.google.com.pk/*", "*://www.google.com.pa/*", "*://www.google.com.pe/*", "*://www.google.com.ph/*", "*://www.google.pl/*", "*://www.google.com.pg/*", "*://www.google.pn/*", "*://www.google.com.pr/*", "*://www.google.ps/*", "*://www.google.pt/*", "*://www.google.com.py/*", "*://www.google.com.qa/*", "*://www.google.ro/*", "*://www.google.rs/*", "*://www.google.ru/*", "*://www.google.rw/*", "*://www.google.com.sa/*", "*://www.google.com.sb/*", "*://www.google.sc/*", "*://www.google.se/*", "*://www.google.com.sg/*", "*://www.google.sh/*", "*://www.google.si/*", "*://www.google.sk/*", "*://www.google.com.sl/*", "*://www.google.sn/*", "*://www.google.sm/*", "*://www.google.so/*", "*://www.google.st/*", "*://www.google.sr/*", "*://www.google.com.sv/*", "*://www.google.td/*", "*://www.google.tg/*", "*://www.google.co.th/*", "*://www.google.com.tj/*", "*://www.google.tk/*", "*://www.google.tl/*", "*://www.google.tm/*", "*://www.google.to/*", "*://www.google.tn/*", "*://www.google.com.tr/*", "*://www.google.tt/*", "*://www.google.com.tw/*", "*://www.google.co.tz/*", "*://www.google.com.ua/*", "*://www.google.co.ug/*", "*://www.google.co.uk/*", "*://www.google.com/*", "*://www.google.com.uy/*", "*://www.google.co.uz/*", "*://www.google.com.vc/*", "*://www.google.co.ve/*", "*://www.google.vg/*", "*://www.google.co.vi/*", "*://www.google.com.vn/*", "*://www.google.vu/*", "*://www.google.ws/*", "*://www.google.co.za/*", "*://www.google.co.zm/*", "*://www.google.co.zw/*" 41 | ] 42 | }], 43 | "background": { 44 | "service_worker": "src/background/background_loader.js" 45 | }, 46 | "content_scripts": [ 47 | { 48 | "matches": [ 49 | "*://duckduckgo.com/?*", 50 | "*://www.bing.com/search?*", 51 | "*://www.ecosia.org/search?*", 52 | "*://search.brave.com/search?*", 53 | "*://www.baidu.com/*", 54 | "*://www.google.com/search?*", "*://www.google.ac/search?*", "*://www.google.ad/search?*", "*://www.google.ae/search?*", "*://www.google.com.af/search?*", "*://www.google.com.ag/search?*", "*://www.google.com.ai/search?*", "*://www.google.al/search?*", "*://www.google.am/search?*", "*://www.google.co.ao/search?*", "*://www.google.com.ar/search?*", "*://www.google.as/search?*", "*://www.google.at/search?*", "*://www.google.com.au/search?*", "*://www.google.az/search?*", "*://www.google.ba/search?*", "*://www.google.com.bd/search?*", "*://www.google.be/search?*", "*://www.google.bf/search?*", "*://www.google.bg/search?*", "*://www.google.com.bh/search?*", "*://www.google.bi/search?*", "*://www.google.bj/search?*", "*://www.google.com.bn/search?*", "*://www.google.com.bo/search?*", "*://www.google.com.br/search?*", "*://www.google.bs/search?*", "*://www.google.bt/search?*", "*://www.google.co.bw/search?*", "*://www.google.by/search?*", "*://www.google.com.bz/search?*", "*://www.google.ca/search?*", "*://www.google.com.kh/search?*", "*://www.google.cc/search?*", "*://www.google.cd/search?*", "*://www.google.cf/search?*", "*://www.google.cat/search?*", "*://www.google.cg/search?*", "*://www.google.ch/search?*", "*://www.google.ci/search?*", "*://www.google.co.ck/search?*", "*://www.google.cl/search?*", "*://www.google.cm/search?*", "*://www.google.cn/search?*", "*://www.google.com.co/search?*", "*://www.google.co.cr/search?*", "*://www.google.com.cu/search?*", "*://www.google.cv/search?*", "*://www.google.com.cy/search?*", "*://www.google.cz/search?*", "*://www.google.de/search?*", "*://www.google.dj/search?*", "*://www.google.dk/search?*", "*://www.google.dm/search?*", "*://www.google.com.do/search?*", "*://www.google.dz/search?*", "*://www.google.com.ec/search?*", "*://www.google.ee/search?*", "*://www.google.com.eg/search?*", "*://www.google.es/search?*", "*://www.google.com.et/search?*", "*://www.google.fi/search?*", "*://www.google.com.fj/search?*", "*://www.google.fm/search?*", "*://www.google.fr/search?*", "*://www.google.ga/search?*", "*://www.google.ge/search?*", "*://www.google.gf/search?*", "*://www.google.gg/search?*", "*://www.google.com.gh/search?*", "*://www.google.com.gi/search?*", "*://www.google.gl/search?*", "*://www.google.gm/search?*", "*://www.google.gp/search?*", "*://www.google.gr/search?*", "*://www.google.com.gt/search?*", "*://www.google.gy/search?*", "*://www.google.com.hk/search?*", "*://www.google.hn/search?*", "*://www.google.hr/search?*", "*://www.google.ht/search?*", "*://www.google.hu/search?*", "*://www.google.co.id/search?*", "*://www.google.iq/search?*", "*://www.google.ie/search?*", "*://www.google.co.il/search?*", "*://www.google.im/search?*", "*://www.google.co.in/search?*", "*://www.google.io/search?*", "*://www.google.is/search?*", "*://www.google.it/search?*", "*://www.google.je/search?*", "*://www.google.com.jm/search?*", "*://www.google.jo/search?*", "*://www.google.co.jp/search?*", "*://www.google.co.ke/search?*", "*://www.google.ki/search?*", "*://www.google.kg/search?*", "*://www.google.co.kr/search?*", "*://www.google.com.kw/search?*", "*://www.google.kz/search?*", "*://www.google.la/search?*", "*://www.google.com.lb/search?*", "*://www.google.com.lc/search?*", "*://www.google.li/search?*", "*://www.google.lk/search?*", "*://www.google.co.ls/search?*", "*://www.google.lt/search?*", "*://www.google.lu/search?*", "*://www.google.lv/search?*", "*://www.google.com.ly/search?*", "*://www.google.co.ma/search?*", "*://www.google.md/search?*", "*://www.google.me/search?*", "*://www.google.mg/search?*", "*://www.google.mk/search?*", "*://www.google.ml/search?*", "*://www.google.com.mm/search?*", "*://www.google.mn/search?*", "*://www.google.ms/search?*", "*://www.google.com.mt/search?*", "*://www.google.mu/search?*", "*://www.google.mv/search?*", "*://www.google.mw/search?*", "*://www.google.com.mx/search?*", "*://www.google.com.my/search?*", "*://www.google.co.mz/search?*", "*://www.google.com.na/search?*", "*://www.google.ne/search?*", "*://www.google.com.nf/search?*", "*://www.google.com.ng/search?*", "*://www.google.com.ni/search?*", "*://www.google.nl/search?*", "*://www.google.no/search?*", "*://www.google.com.np/search?*", "*://www.google.nr/search?*", "*://www.google.nu/search?*", "*://www.google.co.nz/search?*", "*://www.google.com.om/search?*", "*://www.google.com.pk/search?*", "*://www.google.com.pa/search?*", "*://www.google.com.pe/search?*", "*://www.google.com.ph/search?*", "*://www.google.pl/search?*", "*://www.google.com.pg/search?*", "*://www.google.pn/search?*", "*://www.google.com.pr/search?*", "*://www.google.ps/search?*", "*://www.google.pt/search?*", "*://www.google.com.py/search?*", "*://www.google.com.qa/search?*", "*://www.google.ro/search?*", "*://www.google.rs/search?*", "*://www.google.ru/search?*", "*://www.google.rw/search?*", "*://www.google.com.sa/search?*", "*://www.google.com.sb/search?*", "*://www.google.sc/search?*", "*://www.google.se/search?*", "*://www.google.com.sg/search?*", "*://www.google.sh/search?*", "*://www.google.si/search?*", "*://www.google.sk/search?*", "*://www.google.com.sl/search?*", "*://www.google.sn/search?*", "*://www.google.sm/search?*", "*://www.google.so/search?*", "*://www.google.st/search?*", "*://www.google.sr/search?*", "*://www.google.com.sv/search?*", "*://www.google.td/search?*", "*://www.google.tg/search?*", "*://www.google.co.th/search?*", "*://www.google.com.tj/search?*", "*://www.google.tk/search?*", "*://www.google.tl/search?*", "*://www.google.tm/search?*", "*://www.google.to/search?*", "*://www.google.tn/search?*", "*://www.google.com.tr/search?*", "*://www.google.tt/search?*", "*://www.google.com.tw/search?*", "*://www.google.co.tz/search?*", "*://www.google.com.ua/search?*", "*://www.google.co.ug/search?*", "*://www.google.co.uk/search?*", "*://www.google.com/search?*", "*://www.google.com.uy/search?*", "*://www.google.co.uz/search?*", "*://www.google.com.vc/search?*", "*://www.google.co.ve/search?*", "*://www.google.vg/search?*", "*://www.google.co.vi/search?*", "*://www.google.com.vn/search?*", "*://www.google.vu/search?*", "*://www.google.ws/search?*", "*://www.google.co.za/search?*", "*://www.google.co.zm/search?*", "*://www.google.co.zw/search?*" 55 | ], 56 | "run_at": "document_end", 57 | "js": [ 58 | "src/libs/highlight.min.js", 59 | "src/libs/tex-svg.js", 60 | "src/libs/drawdown.js", 61 | "src/libs/ExtPay.js", 62 | 63 | "src/utils.js", 64 | "src/constants.js", 65 | "src/settings.js", 66 | 67 | "src/chat/message.js", 68 | "src/chat/chat_session.js", 69 | "src/chat/bard_session.js", 70 | 71 | "src/context.js", 72 | "src/engine-specifics.js", 73 | "src/chat/init.js", 74 | 75 | "src/run.js" 76 | ] 77 | } 78 | ] 79 | } 80 | -------------------------------------------------------------------------------- /manifest_bingchat.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "__MSG_bingchatName__", 3 | "version": "2.6", 4 | "manifest_version": 3, 5 | "description": "__MSG_bingchatDesc__", 6 | "default_locale": "en", 7 | "permissions": [ 8 | "storage", 9 | "offscreen" 10 | ], 11 | "host_permissions": [ 12 | "https://copilot.microsoft.com/" 13 | ], 14 | "action": { 15 | "default_popup": "src/popup/popup.html" 16 | }, 17 | "icons": { 18 | "128": "icons/bingchat/icon_128.png" 19 | }, 20 | "web_accessible_resources": [{ 21 | "resources": [ 22 | "src/engines.json", 23 | "src/styles/chatgpt.css", 24 | "src/styles/box.css", 25 | "src/styles/panel.css", 26 | "src/styles/code-light-theme.css", 27 | "src/styles/code-dark-theme.css", 28 | "src/images/copilot.png", 29 | "src/images/bingchat_conversation_example.png", 30 | "src/images/engines/*", 31 | "src/background/websocket_utils.js", 32 | "src/chat/offscreen/bing_socket.js", 33 | "icons/optisearch/icon_128.png", 34 | "icons/bingchat/icon_128.png", 35 | "icons/bard/icon_128.png" 36 | ], 37 | "matches": [ 38 | "*://copilot.microsoft.com/*", 39 | "*://duckduckgo.com/*", 40 | "*://www.bing.com/*", 41 | "*://www.ecosia.org/*", 42 | "*://search.brave.com/*", 43 | "*://www.baidu.com/*", 44 | "*://www.google.com/*", "*://www.google.ac/*", "*://www.google.ad/*", "*://www.google.ae/*", "*://www.google.com.af/*", "*://www.google.com.ag/*", "*://www.google.com.ai/*", "*://www.google.al/*", "*://www.google.am/*", "*://www.google.co.ao/*", "*://www.google.com.ar/*", "*://www.google.as/*", "*://www.google.at/*", "*://www.google.com.au/*", "*://www.google.az/*", "*://www.google.ba/*", "*://www.google.com.bd/*", "*://www.google.be/*", "*://www.google.bf/*", "*://www.google.bg/*", "*://www.google.com.bh/*", "*://www.google.bi/*", "*://www.google.bj/*", "*://www.google.com.bn/*", "*://www.google.com.bo/*", "*://www.google.com.br/*", "*://www.google.bs/*", "*://www.google.bt/*", "*://www.google.co.bw/*", "*://www.google.by/*", "*://www.google.com.bz/*", "*://www.google.ca/*", "*://www.google.com.kh/*", "*://www.google.cc/*", "*://www.google.cd/*", "*://www.google.cf/*", "*://www.google.cat/*", "*://www.google.cg/*", "*://www.google.ch/*", "*://www.google.ci/*", "*://www.google.co.ck/*", "*://www.google.cl/*", "*://www.google.cm/*", "*://www.google.cn/*", "*://www.google.com.co/*", "*://www.google.co.cr/*", "*://www.google.com.cu/*", "*://www.google.cv/*", "*://www.google.com.cy/*", "*://www.google.cz/*", "*://www.google.de/*", "*://www.google.dj/*", "*://www.google.dk/*", "*://www.google.dm/*", "*://www.google.com.do/*", "*://www.google.dz/*", "*://www.google.com.ec/*", "*://www.google.ee/*", "*://www.google.com.eg/*", "*://www.google.es/*", "*://www.google.com.et/*", "*://www.google.fi/*", "*://www.google.com.fj/*", "*://www.google.fm/*", "*://www.google.fr/*", "*://www.google.ga/*", "*://www.google.ge/*", "*://www.google.gf/*", "*://www.google.gg/*", "*://www.google.com.gh/*", "*://www.google.com.gi/*", "*://www.google.gl/*", "*://www.google.gm/*", "*://www.google.gp/*", "*://www.google.gr/*", "*://www.google.com.gt/*", "*://www.google.gy/*", "*://www.google.com.hk/*", "*://www.google.hn/*", "*://www.google.hr/*", "*://www.google.ht/*", "*://www.google.hu/*", "*://www.google.co.id/*", "*://www.google.iq/*", "*://www.google.ie/*", "*://www.google.co.il/*", "*://www.google.im/*", "*://www.google.co.in/*", "*://www.google.io/*", "*://www.google.is/*", "*://www.google.it/*", "*://www.google.je/*", "*://www.google.com.jm/*", "*://www.google.jo/*", "*://www.google.co.jp/*", "*://www.google.co.ke/*", "*://www.google.ki/*", "*://www.google.kg/*", "*://www.google.co.kr/*", "*://www.google.com.kw/*", "*://www.google.kz/*", "*://www.google.la/*", "*://www.google.com.lb/*", "*://www.google.com.lc/*", "*://www.google.li/*", "*://www.google.lk/*", "*://www.google.co.ls/*", "*://www.google.lt/*", "*://www.google.lu/*", "*://www.google.lv/*", "*://www.google.com.ly/*", "*://www.google.co.ma/*", "*://www.google.md/*", "*://www.google.me/*", "*://www.google.mg/*", "*://www.google.mk/*", "*://www.google.ml/*", "*://www.google.com.mm/*", "*://www.google.mn/*", "*://www.google.ms/*", "*://www.google.com.mt/*", "*://www.google.mu/*", "*://www.google.mv/*", "*://www.google.mw/*", "*://www.google.com.mx/*", "*://www.google.com.my/*", "*://www.google.co.mz/*", "*://www.google.com.na/*", "*://www.google.ne/*", "*://www.google.com.nf/*", "*://www.google.com.ng/*", "*://www.google.com.ni/*", "*://www.google.nl/*", "*://www.google.no/*", "*://www.google.com.np/*", "*://www.google.nr/*", "*://www.google.nu/*", "*://www.google.co.nz/*", "*://www.google.com.om/*", "*://www.google.com.pk/*", "*://www.google.com.pa/*", "*://www.google.com.pe/*", "*://www.google.com.ph/*", "*://www.google.pl/*", "*://www.google.com.pg/*", "*://www.google.pn/*", "*://www.google.com.pr/*", "*://www.google.ps/*", "*://www.google.pt/*", "*://www.google.com.py/*", "*://www.google.com.qa/*", "*://www.google.ro/*", "*://www.google.rs/*", "*://www.google.ru/*", "*://www.google.rw/*", "*://www.google.com.sa/*", "*://www.google.com.sb/*", "*://www.google.sc/*", "*://www.google.se/*", "*://www.google.com.sg/*", "*://www.google.sh/*", "*://www.google.si/*", "*://www.google.sk/*", "*://www.google.com.sl/*", "*://www.google.sn/*", "*://www.google.sm/*", "*://www.google.so/*", "*://www.google.st/*", "*://www.google.sr/*", "*://www.google.com.sv/*", "*://www.google.td/*", "*://www.google.tg/*", "*://www.google.co.th/*", "*://www.google.com.tj/*", "*://www.google.tk/*", "*://www.google.tl/*", "*://www.google.tm/*", "*://www.google.to/*", "*://www.google.tn/*", "*://www.google.com.tr/*", "*://www.google.tt/*", "*://www.google.com.tw/*", "*://www.google.co.tz/*", "*://www.google.com.ua/*", "*://www.google.co.ug/*", "*://www.google.co.uk/*", "*://www.google.com/*", "*://www.google.com.uy/*", "*://www.google.co.uz/*", "*://www.google.com.vc/*", "*://www.google.co.ve/*", "*://www.google.vg/*", "*://www.google.co.vi/*", "*://www.google.com.vn/*", "*://www.google.vu/*", "*://www.google.ws/*", "*://www.google.co.za/*", "*://www.google.co.zm/*", "*://www.google.co.zw/*" 45 | ] 46 | }], 47 | "background": { 48 | "service_worker": "src/background/background_loader.js" 49 | }, 50 | "content_scripts": [ 51 | { 52 | "matches": [ 53 | "https://copilot.microsoft.com/favicon.ico?bing-chat-gpt-4-in-google" 54 | ], 55 | "all_frames": true, 56 | "run_at": "document_start", 57 | "js": [ 58 | "src/chat/offscreen/iframe_script.js" 59 | ] 60 | }, 61 | { 62 | "matches": [ 63 | "*://duckduckgo.com/?*", 64 | "*://www.bing.com/search?*", 65 | "*://www.ecosia.org/search?*", 66 | "*://search.brave.com/search?*", 67 | "*://www.baidu.com/*", 68 | "*://www.google.com/search?*", "*://www.google.ac/search?*", "*://www.google.ad/search?*", "*://www.google.ae/search?*", "*://www.google.com.af/search?*", "*://www.google.com.ag/search?*", "*://www.google.com.ai/search?*", "*://www.google.al/search?*", "*://www.google.am/search?*", "*://www.google.co.ao/search?*", "*://www.google.com.ar/search?*", "*://www.google.as/search?*", "*://www.google.at/search?*", "*://www.google.com.au/search?*", "*://www.google.az/search?*", "*://www.google.ba/search?*", "*://www.google.com.bd/search?*", "*://www.google.be/search?*", "*://www.google.bf/search?*", "*://www.google.bg/search?*", "*://www.google.com.bh/search?*", "*://www.google.bi/search?*", "*://www.google.bj/search?*", "*://www.google.com.bn/search?*", "*://www.google.com.bo/search?*", "*://www.google.com.br/search?*", "*://www.google.bs/search?*", "*://www.google.bt/search?*", "*://www.google.co.bw/search?*", "*://www.google.by/search?*", "*://www.google.com.bz/search?*", "*://www.google.ca/search?*", "*://www.google.com.kh/search?*", "*://www.google.cc/search?*", "*://www.google.cd/search?*", "*://www.google.cf/search?*", "*://www.google.cat/search?*", "*://www.google.cg/search?*", "*://www.google.ch/search?*", "*://www.google.ci/search?*", "*://www.google.co.ck/search?*", "*://www.google.cl/search?*", "*://www.google.cm/search?*", "*://www.google.cn/search?*", "*://www.google.com.co/search?*", "*://www.google.co.cr/search?*", "*://www.google.com.cu/search?*", "*://www.google.cv/search?*", "*://www.google.com.cy/search?*", "*://www.google.cz/search?*", "*://www.google.de/search?*", "*://www.google.dj/search?*", "*://www.google.dk/search?*", "*://www.google.dm/search?*", "*://www.google.com.do/search?*", "*://www.google.dz/search?*", "*://www.google.com.ec/search?*", "*://www.google.ee/search?*", "*://www.google.com.eg/search?*", "*://www.google.es/search?*", "*://www.google.com.et/search?*", "*://www.google.fi/search?*", "*://www.google.com.fj/search?*", "*://www.google.fm/search?*", "*://www.google.fr/search?*", "*://www.google.ga/search?*", "*://www.google.ge/search?*", "*://www.google.gf/search?*", "*://www.google.gg/search?*", "*://www.google.com.gh/search?*", "*://www.google.com.gi/search?*", "*://www.google.gl/search?*", "*://www.google.gm/search?*", "*://www.google.gp/search?*", "*://www.google.gr/search?*", "*://www.google.com.gt/search?*", "*://www.google.gy/search?*", "*://www.google.com.hk/search?*", "*://www.google.hn/search?*", "*://www.google.hr/search?*", "*://www.google.ht/search?*", "*://www.google.hu/search?*", "*://www.google.co.id/search?*", "*://www.google.iq/search?*", "*://www.google.ie/search?*", "*://www.google.co.il/search?*", "*://www.google.im/search?*", "*://www.google.co.in/search?*", "*://www.google.io/search?*", "*://www.google.is/search?*", "*://www.google.it/search?*", "*://www.google.je/search?*", "*://www.google.com.jm/search?*", "*://www.google.jo/search?*", "*://www.google.co.jp/search?*", "*://www.google.co.ke/search?*", "*://www.google.ki/search?*", "*://www.google.kg/search?*", "*://www.google.co.kr/search?*", "*://www.google.com.kw/search?*", "*://www.google.kz/search?*", "*://www.google.la/search?*", "*://www.google.com.lb/search?*", "*://www.google.com.lc/search?*", "*://www.google.li/search?*", "*://www.google.lk/search?*", "*://www.google.co.ls/search?*", "*://www.google.lt/search?*", "*://www.google.lu/search?*", "*://www.google.lv/search?*", "*://www.google.com.ly/search?*", "*://www.google.co.ma/search?*", "*://www.google.md/search?*", "*://www.google.me/search?*", "*://www.google.mg/search?*", "*://www.google.mk/search?*", "*://www.google.ml/search?*", "*://www.google.com.mm/search?*", "*://www.google.mn/search?*", "*://www.google.ms/search?*", "*://www.google.com.mt/search?*", "*://www.google.mu/search?*", "*://www.google.mv/search?*", "*://www.google.mw/search?*", "*://www.google.com.mx/search?*", "*://www.google.com.my/search?*", "*://www.google.co.mz/search?*", "*://www.google.com.na/search?*", "*://www.google.ne/search?*", "*://www.google.com.nf/search?*", "*://www.google.com.ng/search?*", "*://www.google.com.ni/search?*", "*://www.google.nl/search?*", "*://www.google.no/search?*", "*://www.google.com.np/search?*", "*://www.google.nr/search?*", "*://www.google.nu/search?*", "*://www.google.co.nz/search?*", "*://www.google.com.om/search?*", "*://www.google.com.pk/search?*", "*://www.google.com.pa/search?*", "*://www.google.com.pe/search?*", "*://www.google.com.ph/search?*", "*://www.google.pl/search?*", "*://www.google.com.pg/search?*", "*://www.google.pn/search?*", "*://www.google.com.pr/search?*", "*://www.google.ps/search?*", "*://www.google.pt/search?*", "*://www.google.com.py/search?*", "*://www.google.com.qa/search?*", "*://www.google.ro/search?*", "*://www.google.rs/search?*", "*://www.google.ru/search?*", "*://www.google.rw/search?*", "*://www.google.com.sa/search?*", "*://www.google.com.sb/search?*", "*://www.google.sc/search?*", "*://www.google.se/search?*", "*://www.google.com.sg/search?*", "*://www.google.sh/search?*", "*://www.google.si/search?*", "*://www.google.sk/search?*", "*://www.google.com.sl/search?*", "*://www.google.sn/search?*", "*://www.google.sm/search?*", "*://www.google.so/search?*", "*://www.google.st/search?*", "*://www.google.sr/search?*", "*://www.google.com.sv/search?*", "*://www.google.td/search?*", "*://www.google.tg/search?*", "*://www.google.co.th/search?*", "*://www.google.com.tj/search?*", "*://www.google.tk/search?*", "*://www.google.tl/search?*", "*://www.google.tm/search?*", "*://www.google.to/search?*", "*://www.google.tn/search?*", "*://www.google.com.tr/search?*", "*://www.google.tt/search?*", "*://www.google.com.tw/search?*", "*://www.google.co.tz/search?*", "*://www.google.com.ua/search?*", "*://www.google.co.ug/search?*", "*://www.google.co.uk/search?*", "*://www.google.com/search?*", "*://www.google.com.uy/search?*", "*://www.google.co.uz/search?*", "*://www.google.com.vc/search?*", "*://www.google.co.ve/search?*", "*://www.google.vg/search?*", "*://www.google.co.vi/search?*", "*://www.google.com.vn/search?*", "*://www.google.vu/search?*", "*://www.google.ws/search?*", "*://www.google.co.za/search?*", "*://www.google.co.zm/search?*", "*://www.google.co.zw/search?*" 69 | ], 70 | "run_at": "document_end", 71 | "js": [ 72 | "src/libs/highlight.min.js", 73 | "src/libs/tex-svg.js", 74 | "src/libs/drawdown.js", 75 | "src/libs/ExtPay.js", 76 | 77 | "src/utils.js", 78 | "src/constants.js", 79 | "src/settings.js", 80 | 81 | "src/chat/message.js", 82 | "src/chat/chat_session.js", 83 | "src/chat/bingchat_session.js", 84 | 85 | "src/context.js", 86 | "src/engine-specifics.js", 87 | "src/chat/init.js", 88 | 89 | "src/run.js" 90 | ] 91 | } 92 | ] 93 | } 94 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "archiver": "^5.3.1", 4 | "mocha": "^10.2.0", 5 | "puppeteer": "^23.1.0" 6 | }, 7 | "scripts": { 8 | "test": "node ./node_modules/mocha/bin/mocha ./tests/test.js", 9 | "build": "node build.mjs optisearch -z; node build.mjs optisearch -zft; node build.mjs bingchat -z; node build.mjs bingchat -zft; node build.mjs bard -z; node build.mjs bard -zft" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /screenshots/bard/bigpromo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/screenshots/bard/bigpromo.png -------------------------------------------------------------------------------- /screenshots/bard/promo.pdn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/screenshots/bard/promo.pdn -------------------------------------------------------------------------------- /screenshots/bard/promo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/screenshots/bard/promo.png -------------------------------------------------------------------------------- /screenshots/bard/screenshot_apple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/screenshots/bard/screenshot_apple.png -------------------------------------------------------------------------------- /screenshots/bard/screenshot_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/screenshots/bard/screenshot_code.png -------------------------------------------------------------------------------- /screenshots/bard/screenshot_iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/screenshots/bard/screenshot_iphone.png -------------------------------------------------------------------------------- /screenshots/bard/screenshot_jwst.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/screenshots/bard/screenshot_jwst.png -------------------------------------------------------------------------------- /screenshots/bard/screenshot_premium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/screenshots/bard/screenshot_premium.png -------------------------------------------------------------------------------- /screenshots/bingchat/bigpromo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/screenshots/bingchat/bigpromo.png -------------------------------------------------------------------------------- /screenshots/bingchat/promo.pdn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/screenshots/bingchat/promo.pdn -------------------------------------------------------------------------------- /screenshots/bingchat/promo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/screenshots/bingchat/promo.png -------------------------------------------------------------------------------- /screenshots/bingchat/screenshot_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/screenshots/bingchat/screenshot_code.png -------------------------------------------------------------------------------- /screenshots/bingchat/screenshot_jwst.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/screenshots/bingchat/screenshot_jwst.png -------------------------------------------------------------------------------- /screenshots/bingchat/screenshot_pancake.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/screenshots/bingchat/screenshot_pancake.png -------------------------------------------------------------------------------- /screenshots/bingchat/screenshot_premium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/screenshots/bingchat/screenshot_premium.png -------------------------------------------------------------------------------- /screenshots/dark_mode_stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/screenshots/dark_mode_stack.png -------------------------------------------------------------------------------- /screenshots/description.txt: -------------------------------------------------------------------------------- 1 | OptiSearch vous permet d'accélérer de vos recherches. Elle vous affiche directement dans la page de recherche de Google et d'Ecosia, le contenu pertinent de certains sites compatibles tel que Wikipédia, StackOverflow, StackExchange et d'autres. Certaines autres fonctionnaliés sont disponibles, tels que le dessin de graphs de fonctions et le résultats de calculs tapés dans la barres de recherches. De plus OptiSearch implémente les bangs de DuckDuckGo dans Google et Ecosia ! 2 | 3 | OptiSearch allows you to speed up your searches. It displays you directly in the search page of Google and Ecosia, the relevant content of certain compatible sites such as Wikipedia, StackOverflow, StackExchange and others. Some other features are available, such as drawing function graphs and the results of calculations typed in the search bars. In addition OptiSearch implements the bangs of DuckDuckGo in Google and Ecosia! -------------------------------------------------------------------------------- /screenshots/ecosia_plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/screenshots/ecosia_plot.png -------------------------------------------------------------------------------- /screenshots/filter_py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/screenshots/filter_py.png -------------------------------------------------------------------------------- /screenshots/icon_128.pdn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/screenshots/icon_128.pdn -------------------------------------------------------------------------------- /screenshots/icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/screenshots/icon_512.png -------------------------------------------------------------------------------- /screenshots/jaune_lyrics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/screenshots/jaune_lyrics.png -------------------------------------------------------------------------------- /screenshots/large_promo.pdn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/screenshots/large_promo.pdn -------------------------------------------------------------------------------- /screenshots/large_promo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/screenshots/large_promo.png -------------------------------------------------------------------------------- /screenshots/marquee.pdn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/screenshots/marquee.pdn -------------------------------------------------------------------------------- /screenshots/marquee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/screenshots/marquee.png -------------------------------------------------------------------------------- /screenshots/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/screenshots/screenshot2.png -------------------------------------------------------------------------------- /screenshots/screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/screenshots/screenshot3.png -------------------------------------------------------------------------------- /screenshots/screenshot4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/screenshots/screenshot4.png -------------------------------------------------------------------------------- /screenshots/screenshot_ecosia_createElement.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/screenshots/screenshot_ecosia_createElement.png -------------------------------------------------------------------------------- /screenshots/screenshot_ecosia_lyrics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/screenshots/screenshot_ecosia_lyrics.png -------------------------------------------------------------------------------- /screenshots/small_promo.pdn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/screenshots/small_promo.pdn -------------------------------------------------------------------------------- /screenshots/small_promo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/screenshots/small_promo.png -------------------------------------------------------------------------------- /screenshots/ss_bard_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/screenshots/ss_bard_1.png -------------------------------------------------------------------------------- /screenshots/ss_bard_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/screenshots/ss_bard_2.png -------------------------------------------------------------------------------- /screenshots/store_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/screenshots/store_icon.png -------------------------------------------------------------------------------- /src/background/background.js: -------------------------------------------------------------------------------- 1 | fetchEngines(isDebugMode); 2 | 3 | chrome.runtime.setUninstallURL('https://www.optisearch.io/uninstall.html'); 4 | 5 | chrome.runtime.onMessage.addListener((action, _, sendResponse) => { 6 | if (action.target === 'offscreen') return; 7 | handleAction(action).then(sendResponse); 8 | return true; 9 | }); 10 | 11 | const eventStreams = []; 12 | const sessionStorage = {}; 13 | 14 | async function handleAction(action) { 15 | const { action: actionType } = action; 16 | if (!actionType) 17 | return; 18 | const handlers = { 19 | 'fetch': handleActionFetch, 20 | 'fetch-result': handleActionFetchResult, 21 | 'image-blob': handleActionImageBlob, 22 | 'session-storage': handleSessionStorage, 23 | 'setup-bing-offscreen': handleSetupOffscreen, 24 | 'window': handleActionWindow, 25 | 'event-stream': handleActionEventStream, 26 | 'websocket': handleActionWebsocket, 27 | }; 28 | if (actionType in handlers) 29 | return handlers[actionType](action); 30 | throw new Error(`Unknown action type: "${actionType}"`); 31 | } 32 | 33 | /** Handles fetch action */ 34 | async function handleActionFetch(action) { 35 | const response = await fetch(action.url, action.params && JSON.parse(action.params)) 36 | .catch(e => ({ errorInBackgroundScript: true, error: e.toString() })); 37 | 38 | if (response.type === "opaqueredirect") { 39 | return { status: 302 }; 40 | } 41 | if (!response.ok) 42 | return { 43 | status: response.status, 44 | ...(response.text && { body: response.text() }) 45 | }; 46 | 47 | const contentType = response.headers.get('content-type') ?? ''; 48 | if (contentType.startsWith("application/json")) { 49 | const text = await response.text(); 50 | try { return JSON.parse(text); } 51 | catch (e) { return text; }; 52 | } 53 | if (contentType.startsWith("text/event-stream")) { 54 | eventStreams.push(response.body.getReader()); 55 | return { eventStream: true, id: eventStreams.length - 1 }; 56 | } 57 | return response.text(); 58 | } 59 | 60 | /** Fetch from site result using defined api or link url */ 61 | async function handleActionFetchResult(action) { 62 | let url = String(action.api || action.link); 63 | if (url.startsWith('http://')) 64 | url = 'https' + url.slice(4); 65 | const response = await fetch(url, { credentials: action.credentials ?? "omit" }).catch(e => ({ error: e.toString() })); 66 | return [action, await response.text()] 67 | } 68 | 69 | /** Fetch image blob from source */ 70 | function handleActionImageBlob({ url }) { 71 | return fetch(url).then(r => r.blob()); 72 | } 73 | 74 | function handleSessionStorage({ type, key, value }) { 75 | if (type == 'set') 76 | sessionStorage[key] = value; 77 | if (type == 'get') 78 | return sessionStorage[key]; 79 | } 80 | 81 | /** Fetch image blob from source */ 82 | function handleActionWindow(action) { 83 | chrome.windows.create({ 84 | url: action.url, 85 | type: action.type ?? 'normal', 86 | width: action.width ?? 800, 87 | height: action.height ?? 800, 88 | focused: true 89 | }); 90 | return { status: 'Window created !' }; 91 | } 92 | 93 | /** Handles new data received from an event-stream */ 94 | function handleActionEventStream(action) { 95 | const { id } = action; 96 | 97 | if (!eventStreams[id]) 98 | return { error: `Error: event-stream ${id} not available` }; 99 | 100 | return eventStreams[id].read().then(({ done, value }) => ({ 101 | done, 102 | data: value && [...value.values()].map(c => String.fromCharCode(c)).join(''), 103 | })); 104 | } 105 | 106 | /** 107 | * Check if there is an offscreen document active and create one if not. 108 | * This method should be executed only on the Copilot extension and with manifest v3. 109 | */ 110 | async function handleSetupOffscreen() { 111 | const already = await setupOffscreenDocument('src/chat/offscreen/offscreen.html'); 112 | return { 'status': already ? 'Offscreen already running' : 'Offscreen setup' }; 113 | } 114 | 115 | let creating; // A global promise to avoid concurrency issues 116 | 117 | /** 118 | * Check if there is an offscreen document active and create one if not. 119 | * This method should be executed only on the Copilot extension and with manifest v3. 120 | */ 121 | async function setupOffscreenDocument(path) { 122 | const offscreenUrl = chrome.runtime.getURL(path); 123 | 124 | if (await hasOffscreenDocument(offscreenUrl)) { 125 | return true; 126 | } 127 | 128 | if (creating) { 129 | await creating; 130 | } else { 131 | const createOffscreenDocument = () => chrome.offscreen.createDocument({ 132 | url: path, 133 | reasons: ['IFRAME_SCRIPTING'], 134 | justification: 'Open WebSocket inside https://copilot.microsoft.com context', 135 | }); 136 | creating = createOffscreenDocument().catch(async error => { 137 | if (error.message.startsWith('Only a single offscreen document may be created.')) { 138 | await chrome.offscreen.closeDocument(); 139 | creating = null; 140 | return createOffscreenDocument(); 141 | } 142 | throw error; 143 | }); 144 | await creating; 145 | creating = null; 146 | } 147 | return false; 148 | } 149 | 150 | /** 151 | * Check if there is an offscreen document active. 152 | * This method should be executed only on the Copilot extension and with manifest v3. 153 | */ 154 | async function hasOffscreenDocument(offscreenUrl) { 155 | const matchedClients = await clients.matchAll(); 156 | 157 | for (const client of matchedClients) { 158 | if (client.url === offscreenUrl) { 159 | return true; 160 | } 161 | } 162 | return false; 163 | } 164 | 165 | /** 166 | * Fetches engines properties on a Gist. If it fails to fetch it, it gets it in local. 167 | * @param {boolean} local If true: bypass the attempt to fetch it from the Gist 168 | * @returns 169 | */ 170 | async function fetchEngines(local = false) { 171 | const distantPath = "https://raw.githubusercontent.com/Dj0ulo/OptiSearch/master/src/engines.json"; 172 | const localPath = chrome.runtime.getURL("./src/engines.json"); 173 | const SAVE_QUERIES_ENGINE = "save_queries_engine"; 174 | let url = local ? localPath : distantPath; 175 | const response = await fetch(url).catch(() => { 176 | if (local) 177 | throw new Error("No local engines found..."); 178 | return fetchEngines(true); 179 | }); 180 | const json = await response.json().catch(_ => ({})); 181 | if (!local && !json["Google"]) { 182 | return fetchEngines(true); 183 | } 184 | chrome.storage.local.set({ [SAVE_QUERIES_ENGINE]: json }); 185 | console.log(`Engines properties fetched ${local ? 'locally' : `from ${url}`}: `, json); 186 | return json; 187 | } 188 | -------------------------------------------------------------------------------- /src/background/background_extpay.js: -------------------------------------------------------------------------------- 1 | const extpay = ExtPay('optisearch'); 2 | extpay.startBackground(); 3 | -------------------------------------------------------------------------------- /src/background/background_loader.js: -------------------------------------------------------------------------------- 1 | importScripts('../libs/ExtPay.js'); 2 | importScripts('../constants.js'); 3 | importScripts('background_extpay.js'); 4 | importScripts('websocket_utils.js'); 5 | importScripts('background.js'); 6 | -------------------------------------------------------------------------------- /src/background/background_loader_firefox.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/background/websocket_utils.js: -------------------------------------------------------------------------------- 1 | const websockets = []; 2 | 3 | /** 4 | * Creates a websocket from `url` and add it to the array `websockets`. 5 | * If `socketID` is specified, it will get the corresponding websocket from the array. 6 | * If `toSend` is specified, it will send it. 7 | * If `socketID` is specified, but not `toSend` it will return back the first message received 8 | * in a FIFO queue. 9 | */ 10 | async function handleActionWebsocket(action, tryTimes = 3) { 11 | const { socketID, url, toSend } = action; 12 | if (socketID == null) { 13 | let ws = null; 14 | try { 15 | ws = new WebSocket(url); 16 | } catch (error) { 17 | if (tryTimes <= 0) 18 | return { error: error.toString() }; 19 | await new Promise(resolve => setTimeout(resolve, 500)); 20 | return handleActionWebsocket(action, tryTimes - 1); 21 | } 22 | ws.stream = new Stream(); 23 | websockets.push(ws); 24 | ws.onopen = () => { 25 | if (toSend) 26 | ws.send(toSend); 27 | ws.stream.write('{eventWebSocket:"open"}'); 28 | } 29 | ws.onmessage = ({ data }) => { 30 | ws.stream.write(data); 31 | } 32 | ws.onclose = ({ wasClean }) => { 33 | ws.stream.write(`{wasClean:${wasClean}}`); 34 | }; 35 | return { socketID: websockets.length - 1 }; 36 | } 37 | const ws = websockets[socketID]; 38 | if (!ws) { 39 | return { error: `Error: websocket ${socketID} not available` }; 40 | } 41 | if (toSend) { 42 | ws.send(toSend); 43 | return { status: 'Success' }; 44 | } 45 | return ws.stream.read().then((packet) => ({ readyState: ws.readyState, packet })); 46 | } 47 | 48 | class Stream { 49 | constructor() { 50 | this.buffer = []; 51 | this.readPromise = null; 52 | } 53 | 54 | async read() { 55 | if (this.buffer.length > 0) 56 | return this.buffer.shift(); 57 | 58 | if (this.readPromise === null) { 59 | this.readPromise = new Promise(resolve => this.resolveReadPromise = resolve); 60 | } 61 | return this.readPromise; 62 | } 63 | 64 | write(data) { 65 | // console.debug('WebSocket receives: ', data); 66 | this.buffer.push(data); 67 | if (this.readPromise !== null) { 68 | this.resolveReadPromise(this.buffer.shift()); 69 | this.readPromise = null; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/chat/bard_session.js: -------------------------------------------------------------------------------- 1 | class BardSession extends ChatSession { 2 | properties = { 3 | name: "Gemini", 4 | link: "https://gemini.google.com", 5 | icon: "src/images/bard.png", 6 | icon: "bard.png", 7 | href: this.urlPrefix, 8 | }; 9 | static errors = { 10 | session: { 11 | code: "BARD_SESSION", 12 | url: "https://accounts.google.com/", 13 | text: _t("Please login to $AI$, then refresh", "Google"), 14 | button: _t("Login to $AI$", "Google"), 15 | }, 16 | }; 17 | static accountConfigKeys = { 18 | index: 'QrtxK', 19 | email: 'oPEP7c', 20 | at: 'SNlM0e', 21 | bl: 'cfb2h', 22 | } 23 | static get storageKey() { 24 | return "SAVE_BARD"; 25 | } 26 | get urlPrefix() { 27 | return `https://gemini.google.com/u/${Context.get("googleAccount")}`; 28 | } 29 | 30 | constructor() { 31 | super("bard"); 32 | } 33 | 34 | async init() { 35 | if (ChatSession.debug) return; 36 | await this.fetchSession(); 37 | } 38 | 39 | async fetchSession() { 40 | const { at, bl, hasNotBard } = await BardSession.fetchAccountData(Context.get("googleAccount")); 41 | if (hasNotBard) { 42 | this.chooseGoogleAccount(); 43 | return null; 44 | } 45 | this.session = { at, bl }; 46 | return this.session; 47 | } 48 | 49 | static async fetchAvailableAccounts(offset = 0) { 50 | const testCount = 8; 51 | const accounts = await Promise.all( 52 | [...Array(testCount).keys()].map(async (i) => { 53 | try { 54 | const { index, name, email, hasNotBard, img32 } = await BardSession.fetchAccountData( 55 | offset + i 56 | ); 57 | if (i + offset != index) { 58 | return null; 59 | } 60 | return { 61 | index: parseInt(index), 62 | name, 63 | email, 64 | hasBard: !hasNotBard, 65 | img32, 66 | }; 67 | } catch (error) { 68 | if (error.code === "BARD_CAPTCHA" || error.code === "BARD_SESSION") { 69 | return null; 70 | } 71 | throw error; 72 | } 73 | }) 74 | ); 75 | if (accounts.at(-1) !== null) { 76 | return [ 77 | ...accounts.filter((a) => !!a), 78 | ...(await BardSession.fetchAvailableAccounts(offset + testCount)), 79 | ]; 80 | } 81 | return accounts.filter((a) => !!a); 82 | } 83 | 84 | static async fetchAccountData(user_id = 0) { 85 | const parseData = (html) => { 86 | let str = "window.WIZ_global_data = "; 87 | let beg = html.indexOf(str) + str.length; 88 | let end = html.indexOf("", beg); 89 | const raw = html.slice(beg, end); 90 | const data = JSON.parse(raw.slice(0, raw.lastIndexOf("}") + 1)); 91 | if (!(BardSession.accountConfigKeys.email in data)) { 92 | throw BardSession.errors.session; 93 | } 94 | const res = { 95 | name: parseStr(html, /
(.*?)<\/div>/), 96 | img32: parseStr(html, /(https:\/\/lh3\.googleusercontent\.com\/[^\s'"]*?s32[^\s'"]*)/), 97 | img64: parseStr(html, /(https:\/\/lh3\.googleusercontent\.com\/[^\s'"]*?s64[^\s'"]*)/), 98 | }; 99 | Object.entries(BardSession.accountConfigKeys).forEach(([k, v]) => res[k] = data[v]); 100 | res.hasNotBard = false; 101 | return res; 102 | }; 103 | const url = `https://gemini.google.com/u/${user_id}/`; 104 | const r = await bgFetch(url, { credentials: "include", redirect: "manual" }); 105 | if (r.status) { 106 | switch (r.status) { 107 | case 0: // redirected, which means that the user is not logged in at this account index 108 | throw BardSession.errors.session; 109 | case 200: 110 | if(r.body) { 111 | return parseData(r.body); 112 | } 113 | throw BardSession.errors.session; 114 | case 429: 115 | throw { 116 | code: "BARD_CAPTCHA", 117 | url, 118 | text: _t("Too many requests. Please solve the captcha and refresh"), 119 | button: _t("Solve Google Gemini captcha"), 120 | }; 121 | default: 122 | throw BardSession.errors.session; 123 | } 124 | } else if(typeof r === "string") { 125 | return parseData(r); 126 | } 127 | } 128 | 129 | async send(prompt) { 130 | super.send(prompt); 131 | if (ChatSession.debug) { 132 | return; 133 | } 134 | 135 | const fetchResponse = () => { 136 | return this.api("assistant.lamda.BardFrontendService/StreamGenerate", {}, [ 137 | null, 138 | JSON.stringify([[prompt], null, this.session.conversation ?? ["", "", ""]]), 139 | ]); 140 | }; 141 | 142 | const parseRawResponse = (raw) => { 143 | const sectionsRegex = /(\d+)\s*\[\s*\[/g; 144 | const sections = [...raw.matchAll(sectionsRegex)]; 145 | const blockObjects = sections.map((section, i) => { 146 | const start = section.index + section[1].length; 147 | const end = sections[i + 1]?.index ?? raw.length; 148 | try { 149 | return JSON.parse(raw.slice(start, end)); 150 | } catch { 151 | if (e instanceof SyntaxError) return null; 152 | } 153 | }); 154 | return blockObjects 155 | .filter((obj) => obj && obj[0] && typeof obj[0][2] === "string") // filter the relevant objects 156 | .map((obj) => JSON.parse(obj[0][2])) // parse them 157 | .find((obj) => obj[4] && obj[4].length); // find the first one that has some answers 158 | }; 159 | 160 | const parseConversationId = (jsonResp) => jsonResp[1]; 161 | const parseAnswersList = (jsonResp) => jsonResp[4]; 162 | const parseSourcesAnswer = (answer) => { 163 | if (!answer[2]) return []; 164 | const sources = answer[2][0]; 165 | if (!sources) return []; 166 | return sources.map((s, i) => { 167 | const href = escapeHtml(s[2][0]); 168 | return { 169 | start: s[0], 170 | end: s[1], 171 | href, 172 | html: `${i+1}`, 173 | }; 174 | }).filter(({href}) => href); 175 | }; 176 | const parseTextAnswer = (answer) => answer[1][0]; 177 | const parseImagesAnswer = (answer) => { 178 | let images = answer[12][1]; 179 | if (!images) return []; 180 | return images.map(img => { 181 | const [substr, source, url, title] = [img[7][0], img[1][0][0], img[3][0][0], img[7][2]].map(escapeHtml); 182 | return { 183 | substr, 184 | html: ` 185 | 186 | ${title} 187 | `.trim(), 188 | }; 189 | }); 190 | }; 191 | 192 | const buildMessage = (answer) => { 193 | let text = parseTextAnswer(answer); 194 | let offset = 0; 195 | const sources = parseSourcesAnswer(answer); 196 | sources.forEach(({end}, i) => { 197 | const position = text.slice(0, end + offset).lastIndexOf(' '); 198 | if (position === -1) return; 199 | text = text.slice(0, position) + `\uF8FD${i}\uF8FE` + text.slice(position); 200 | offset += 3; 201 | }); 202 | let bodyHTML = runMarkdown(text).replace(/\uF8FD(\d+)\uF8FE/g, (_, i) => sources[i]?.html); 203 | parseImagesAnswer(answer).forEach(({substr, html}) => { 204 | bodyHTML = bodyHTML.replace(substr, html); 205 | }); 206 | return [bodyHTML, sources]; 207 | }; 208 | 209 | const rawResponse = await fetchResponse(); 210 | const formattedResponse = parseRawResponse(rawResponse); 211 | if (!formattedResponse) { 212 | this.chooseGoogleAccount(); 213 | return; 214 | } 215 | this.allowSend(); 216 | 217 | try { 218 | const answersList = parseAnswersList(formattedResponse); 219 | const firstAnswer = answersList[0]; 220 | this.session.conversation = parseConversationId(formattedResponse); 221 | this.session.conversation.push(firstAnswer[0]); 222 | this.onMessage(...buildMessage(firstAnswer)); 223 | } catch (e) { 224 | this.onErrorMessage(_t("An error occured while parsing the response:
$error$", e)); 225 | } 226 | } 227 | 228 | async chooseGoogleAccount(isError = true) { 229 | const accounts = await BardSession.fetchAvailableAccounts(); 230 | const htmlMessage = ` 231 | ${ 232 | isError 233 | ? _t( 234 | 'This Google account does not have access to Gemini yet, please visit this link to activate it or choose another Google account for Gemini', 235 | this.urlPrefix 236 | ) 237 | : _t("Choose a Google account for Gemini") 238 | } 239 |
240 | 249 | `; 250 | if (isError) { 251 | this.handleActionError({ 252 | code: "BARD_ACCOUNT", 253 | text: htmlMessage, 254 | action: "refresh", 255 | }); 256 | } else { 257 | this.setCurrentAction("refresh"); 258 | this.onMessage(htmlMessage); 259 | } 260 | const input = $("[name=google-account]", this.panel); 261 | input.value = Context.get("googleAccount"); 262 | input.addEventListener("change", () => Context.set("googleAccount", parseInt(input.value))); 263 | } 264 | 265 | removeConversation() { 266 | if ( 267 | ChatSession.debug || 268 | !this.session || 269 | !this.session.conversation || 270 | this.session.conversation.length === 0 271 | ) 272 | return; 273 | 274 | return this.api("batchexecute", { rpcids: "GzXR5e", "source-path": "/" }, [ 275 | [["GzXR5e", `["${this.session.conversation[0]}"]`, null, "generic"]], 276 | ]); 277 | } 278 | 279 | api(method, params, fReq) { 280 | params = { 281 | bl: this.session.bl, 282 | rt: "c", 283 | ...params, 284 | }; 285 | return bgFetch( 286 | `${this.urlPrefix}/_/BardChatUi/data/${method}?${this.encodeURIParams(params)}`, 287 | { 288 | headers: { 289 | accept: "*/*", 290 | "cache-control": "no-cache", 291 | "content-type": "application/x-www-form-urlencoded;charset=UTF-8", 292 | pragma: "no-cache", 293 | }, 294 | body: this.encodeURIParams({ 295 | "f.req": fReq, 296 | at: this.session.at, 297 | }), 298 | method: "POST", 299 | mode: "cors", 300 | credentials: "include", 301 | } 302 | ); 303 | } 304 | 305 | encodeURIParams(params) { 306 | return Object.entries(params) 307 | .map(([k, v]) => `${k}=${encodeURIComponent(typeof v === "object" ? JSON.stringify(v) : v)}`) 308 | .join("&"); 309 | } 310 | 311 | createPanel(directchat = true) { 312 | super.createPanel(directchat); 313 | 314 | const rightButtonsContainer = $(".right-buttons-container", this.panel); 315 | const accountButton = el( 316 | "div", 317 | { className: "bust", title: _t("Switch Google account") }, 318 | rightButtonsContainer 319 | ); 320 | setSvg(accountButton, SVG.user); 321 | accountButton.addEventListener("click", () => { 322 | this.clear(); 323 | this.chooseGoogleAccount(false); 324 | }); 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /src/chat/bingchat_session.js: -------------------------------------------------------------------------------- 1 | class BingChatSession extends ChatSession { 2 | properties = { 3 | name: "Copilot", 4 | link: "https://copilot.microsoft.com/", 5 | icon: "src/images/copilot.png", 6 | icon: "copilot.png", 7 | href: "https://copilot.microsoft.com/", 8 | } 9 | static get storageKey() { 10 | return "SAVE_BINGCHAT"; 11 | } 12 | 13 | /** @type {HTMLImageElement | null} */ 14 | bingIconElement = null; 15 | 16 | constructor() { 17 | super('bingchat'); 18 | this.socketID = null; 19 | this.uuid = generateUUID(); // for conversation continuation 20 | } 21 | 22 | async init() { 23 | if (ChatSession.debug) return; 24 | await this.fetchSession(); 25 | } 26 | 27 | async fetchSession() { 28 | const session = await BingChatSession.offscreenAction({ action: "session" }); 29 | this.session = { conversationId: session.id }; 30 | this.session.isStartOfSession = true; 31 | return this.session; 32 | } 33 | 34 | async send(prompt) { 35 | super.send(prompt); 36 | if (ChatSession.debug) { 37 | return; 38 | } 39 | this.bingIconElement?.classList.add('disabled'); 40 | 41 | bgWorker({ 42 | action: 'session-storage', type: 'set', key: this.uuid, 43 | value: { ...this.session, inputText: prompt } 44 | }); 45 | 46 | if(!this.socketID) { 47 | this.socketID = await this.createSocket(); 48 | const { packet } = await this.socketReceive(); 49 | if (packet !== '{eventWebSocket:"open"}') { 50 | this.onErrorMessage(); 51 | err(`Error with Bing Copilot: first packet received is ${packet}`); 52 | return; 53 | } 54 | } 55 | 56 | await this.socketSend(await this.config(prompt)); 57 | this.rawMessage = ""; 58 | return this.next(); 59 | } 60 | 61 | async next() { 62 | const res = await this.socketReceive(); 63 | if (!res) { 64 | return; 65 | } 66 | /**@type {{packet: string, readyState: number}} */ 67 | const { packet, readyState } = res; 68 | this.session.isStartOfSession = false; 69 | 70 | /** 71 | * @param {*} body 72 | * @returns 73 | */ 74 | const parseResponseBody = (body) => { 75 | switch (body.event) { 76 | case "received": return; 77 | case "startMessage": return; 78 | case "appendText": 79 | this.rawMessage += body.text; 80 | break; 81 | case "partCompleted": return; 82 | case "titleUpdate": return; 83 | case "done": 84 | this.allowSend(); 85 | return 'close'; 86 | default: return; 87 | } 88 | let text = this.rawMessage; 89 | if (!text) return; 90 | 91 | const bodyHTML = runMarkdown(text); 92 | 93 | this.onMessage( 94 | bodyHTML, 95 | ); 96 | } 97 | 98 | const response = JSON.parse(packet); 99 | const doClose = parseResponseBody(response); 100 | 101 | if (doClose || readyState === WebSocket.CLOSED) 102 | return; 103 | 104 | return this.next(); 105 | } 106 | 107 | removeConversation() { 108 | if (ChatSession.debug || !this.session) 109 | return; 110 | const { conversationId } = this.session; 111 | return BingChatSession.offscreenAction({ 112 | action: "delete", 113 | conversationId, 114 | }); 115 | } 116 | 117 | async createSocket() { 118 | const url = 'wss://copilot.microsoft.com/c/api/chat?api-version=2'; 119 | const res = await BingChatSession.offscreenAction({ 120 | action: "socket", 121 | url, 122 | toSend: JSON.stringify({ event: "setOptions", supportedCards: ["image"], ads: null }), 123 | }); 124 | if (!('socketID' in res)) { 125 | throw "Socket ID not returned"; 126 | } 127 | return res.socketID; 128 | } 129 | 130 | socketSend(body) { 131 | if (this.socketID == null) 132 | throw "Need socket ID to send"; 133 | return BingChatSession.offscreenAction({ 134 | action: "socket", 135 | socketID: this.socketID, 136 | toSend: JSON.stringify(body), 137 | }); 138 | } 139 | 140 | socketReceive() { 141 | if (this.socketID == null) 142 | throw "Need socket ID to receive"; 143 | return BingChatSession.offscreenAction({ 144 | action: "socket", 145 | socketID: this.socketID, 146 | }); 147 | } 148 | 149 | static async offscreenAction(params) { 150 | if (onChrome()) { 151 | await bgWorker({ action: "setup-bing-offscreen" }); 152 | } 153 | return await bgWorker({ 154 | ...params, 155 | target: 'offscreen', 156 | }); 157 | } 158 | 159 | async config(prompt) { 160 | if (!this.session) 161 | throw "Session has to be fetched first"; 162 | 163 | return { 164 | event: "send", 165 | conversationId: this.session.conversationId, 166 | content: [{ type: "text", text: prompt }], 167 | mode: "chat", 168 | }; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/chat/chatgpt_session.js: -------------------------------------------------------------------------------- 1 | class ChatGPTSession extends ChatSession { 2 | properties = { 3 | name: "ChatGPT", 4 | link: "https://chatgpt.com", 5 | icon: "src/images/chatgpt.png", 6 | icon: "chatgpt.png", 7 | href: "https://chatgpt.com", 8 | } 9 | static errors = { 10 | session: { 11 | code: 'CHAT_GPT_SESSION', 12 | url: 'https://chatgpt.com', 13 | text: _t("Please login to $AI$, then refresh", "ChatGPT"), 14 | button: _t("Login to $AI$", "ChatGPT"), 15 | }, 16 | cloudflare: { 17 | code: 'CHAT_GPT_CLOUDFLARE', 18 | url: 'https://chatgpt.com', 19 | text: _t("Please pass the Cloudflare check on ChatGPT, then refresh"), 20 | button: _t("Cloudflare check"), 21 | }, 22 | } 23 | static get storageKey() { 24 | return "SAVE_CHATGPT"; 25 | } 26 | 27 | constructor() { 28 | super('chatgpt'); 29 | this.eventStreamID = null; 30 | } 31 | 32 | async init() { 33 | if (ChatSession.debug) return; 34 | await this.fetchSession(); 35 | await this.fetchModels(); 36 | } 37 | 38 | async fetchSession() { 39 | const session = await bgFetch('https://chatgpt.com/api/auth/session', { 40 | credentials: "include", 41 | }); 42 | if (session.error) { 43 | if (session.error === 'RefreshAccessTokenError') 44 | throw ChatGPTSession.errors.session; 45 | throw session.error; 46 | } 47 | if (session.status === 403) 48 | throw ChatGPTSession.errors.cloudflare; 49 | if (!session.accessToken) 50 | throw ChatGPTSession.errors.session; 51 | this.session = session; 52 | return this.session; 53 | } 54 | 55 | async registerWebSocket() { 56 | const url = (await this.backendApi("register-websocket", null, 'POST')).wss_url; 57 | this.socketID = await this.createSocket(url); 58 | } 59 | 60 | async fetchModels() { 61 | this.models = (await this.backendApi("models")).models; 62 | return this.models; 63 | } 64 | 65 | async send(prompt) { 66 | super.send(prompt); 67 | if (ChatSession.debug) 68 | return; 69 | 70 | const requirements = await this.backendApi('sentinel/chat-requirements', { 71 | p: "gAAAAAC" + await this.generateProofToken("" + Math.random(), "0"), 72 | }); 73 | this.session.sentinelToken = requirements.token; 74 | if (requirements.proofofwork?.required){ 75 | this.session.proofToken = "gAAAAAB" + await this.generateProofToken(requirements.proofofwork.seed, requirements.proofofwork.difficulty); 76 | } 77 | const res = await this.backendApi('conversation', this.config(prompt)); 78 | if (res.eventStream) { 79 | this.eventStreamID = res.id; 80 | } 81 | await this.next(); 82 | } 83 | 84 | async next() { 85 | const fetchPackets = async () => { 86 | const streamData = await this.readStream(); 87 | if (!streamData) return []; 88 | let packetBody = null; 89 | if (this.eventStreamID !== null) { 90 | if (streamData.done) return ["DONE"]; 91 | if (!streamData.data) return []; 92 | packetBody = streamData.data; 93 | } else { 94 | if (streamData.readyState === WebSocket.CLOSED) return ["DONE"]; 95 | if (!streamData.packet) return []; 96 | packetBody = atob(JSON.parse(streamData.packet).body); 97 | } 98 | return packetBody 99 | .split('\n\n') 100 | .map((p, i) => { 101 | if (!p) return null; 102 | let packet = p; 103 | if (i === 0 && !p.startsWith('data: ')) { 104 | packet = this.halfPacket + packet; 105 | this.halfPacket = ""; 106 | } 107 | packet = packet.substring(6); 108 | if (packet === "[DONE]") return "DONE"; 109 | try { 110 | return JSON.parse(packet); 111 | } 112 | catch (e) { 113 | if (!e instanceof SyntaxError) throw e 114 | this.halfPacket = p; 115 | } 116 | return null; 117 | }) 118 | .filter(p => !!p); 119 | }; 120 | 121 | const handlePacket = (data) => { 122 | if (data === "DONE") { 123 | this.allowSend(); 124 | return true; 125 | } 126 | 127 | this.session.conversation_id = data.conversation_id; 128 | if (data.error) { 129 | this.onErrorMessage(data.error); 130 | return true; 131 | } 132 | if (!data.message) { 133 | return false; 134 | } 135 | 136 | this.session.parent_message_id = data.message.id; 137 | if (!data.message.content?.parts) { 138 | return false; 139 | } 140 | const text = data.message.content.parts[0]; 141 | if (text) { 142 | this.onMessage(runMarkdown(text)); 143 | } 144 | return false; 145 | } 146 | 147 | const packets = await fetchPackets(); 148 | 149 | for (const packet of packets) { 150 | if (handlePacket(packet)) return; 151 | } 152 | return this.next(); 153 | } 154 | 155 | async createSocket(url) { 156 | const res = await bgWorker({ 157 | action: "websocket", 158 | url, 159 | }); 160 | if (!('socketID' in res)) { 161 | throw "Socket ID not returned"; 162 | } 163 | return res.socketID; 164 | } 165 | 166 | readStream() { 167 | if (this.eventStreamID !== null) { 168 | return bgWorker({ 169 | action: 'event-stream', 170 | id: this.eventStreamID, 171 | }); 172 | } 173 | 174 | if (this.socketID !== null) { 175 | return bgWorker({ 176 | action: "websocket", 177 | socketID: this.socketID, 178 | }); 179 | } 180 | 181 | throw "Need socket or event stream ID to send"; 182 | } 183 | 184 | removeConversation() { 185 | if (ChatGPTSession.debug || !this.session || !this.session.conversation_id) 186 | return; 187 | return this.backendApi(`conversation/${this.session.conversation_id}`, { 188 | is_visible: false 189 | }, 'PATCH'); 190 | } 191 | 192 | config(prompt) { 193 | if (!this.session) 194 | throw "Session has to be fetched first"; 195 | const id = generateUUID(); 196 | const pid = this.session.parent_message_id ? this.session.parent_message_id : generateUUID(); 197 | return { 198 | action: "next", 199 | conversation_mode: { 200 | kind: "primary_assistant" 201 | }, 202 | force_nulligen: false, 203 | force_paragen: false, 204 | force_paragen_model_slug: "", 205 | force_rate_limit: false, 206 | history_and_training_disabled: false, 207 | ...(this.session.conversation_id && { conversation_id: this.session.conversation_id }), 208 | messages: [{ 209 | id, 210 | author: { role: "user" }, 211 | content: { 212 | content_type: "text", 213 | parts: [prompt], 214 | } 215 | }], 216 | parent_message_id: pid, 217 | model: this.models.at(-1).slug, 218 | suggestions: [], 219 | } 220 | } 221 | 222 | /** 223 | * @param {string} seed float number between 0 and 1 encoded in a string 224 | * @param {string} difficulty integer encoded in a string with leading zeros 225 | * @returns Proof token 226 | */ 227 | async generateProofToken(seed, difficulty) { 228 | const config = [ 229 | navigator.hardwareConcurrency + screen.width + screen.height, 230 | new Date().toString(), 231 | 4294705152, 232 | 0, 233 | navigator.userAgent, 234 | "", 235 | "", 236 | navigator.language, 237 | navigator.languages.join(","), 238 | 0, 239 | ]; 240 | const encodeConfig = (data) => { 241 | const jsonData = JSON.stringify(data); 242 | return btoa(String.fromCharCode(...new TextEncoder().encode(jsonData))); 243 | } 244 | const start = performance.now(); 245 | for (let i = 0; i < 3e5; i++) { 246 | config[3] = i, config[9] = Math.round(performance.now() - start); 247 | const base = encodeConfig(config); 248 | const hash = sha3_512(seed + base); 249 | if (hash.slice(0, difficulty.length) <= difficulty) return base; 250 | } 251 | return "wQ8Lk5FbGpA2NcR9dShT6gYjU7VxZ4D" + encodeConfig(`"${seed}"`); 252 | } 253 | 254 | backendApi(service, body, method) { 255 | if (!method) { 256 | method = body ? "POST" : "GET"; 257 | } 258 | const headers = { 259 | authorization: `Bearer ${this.session.accessToken}`, 260 | ...(body && { "content-type": "application/json" }), 261 | } 262 | if (service === "conversation") { 263 | headers["accept"] = 'text/event-stream'; 264 | if (this.session.sentinelToken) headers["openai-sentinel-chat-requirements-token"] = this.session.sentinelToken; 265 | if (this.session.proofToken) headers["openai-sentinel-proof-token"] = this.session.proofToken; 266 | } 267 | const params = { 268 | headers, 269 | credentials: "include", 270 | method, 271 | ...(body && { "body": JSON.stringify(body) }), 272 | } 273 | return bgFetch(`https://chatgpt.com/backend-api/${service}`, params); 274 | } 275 | } -------------------------------------------------------------------------------- /src/chat/init.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | Context.initChat = () => { 3 | if (isOptiSearch && !Context.isActive('chatgpt')) 4 | return; 5 | 6 | Context.chatSession = (() => { 7 | if (typeof BingChatSession !== 'undefined') 8 | return new BingChatSession(); 9 | if (typeof BardSession !== 'undefined') 10 | return new BardSession(); 11 | if (typeof ChatGPTSession !== 'undefined') 12 | return new ChatGPTSession(); 13 | return null; 14 | })(); 15 | 16 | if (!Context.chatSession) 17 | return; 18 | Context.chatSession.createPanel(Context.isActive('directchat')); 19 | }; 20 | })(); 21 | -------------------------------------------------------------------------------- /src/chat/message.js: -------------------------------------------------------------------------------- 1 | const Author = { 2 | User: 0, 3 | Bot: 1, 4 | } 5 | 6 | class Message { 7 | static USER = 0; 8 | static BOT = 1; 9 | constructor(author = Author.Bot, text = '') { 10 | this.author = author; 11 | this.text = text; 12 | } 13 | } 14 | 15 | class MessageContainer extends Message { 16 | constructor(author, html) { 17 | super(author, html); 18 | this.box = el('div', { className: `box-message-container ${author === Author.User ? 'user' : 'bot'}` }); 19 | this.bubble = el('div', { className: `message-container` }, this.box); 20 | this.html = html; 21 | } 22 | /** 23 | * @param {string} html 24 | */ 25 | set html(html) { 26 | this.text = html; 27 | this.bubble.innerHTML = html; 28 | if (this.bubble.children.length === 1 && this.bubble.firstChild.tagName === 'P') 29 | this.bubble.innerHTML = this.bubble.firstChild.innerHTML; 30 | prettifyCode(this.bubble); 31 | } 32 | get html() { 33 | return this.text; 34 | } 35 | get el() { 36 | return this.box; 37 | } 38 | } 39 | 40 | class Discussion { 41 | el = el('div', { className: 'discussion-container' }) 42 | /** @type {MessageContainer[]} */ 43 | messageContainers = [] 44 | isScrolledToBottom = true 45 | get length() { 46 | return this.messageContainers.length; 47 | } 48 | appendMessage(messageContainer) { 49 | this.messageContainers.push(messageContainer); 50 | this.el.appendChild(this.messageContainers.at(-1).el); 51 | this.el.scrollTop = this.el.scrollHeight; 52 | } 53 | setLastMessageHTML(html) { 54 | this.isScrolledToBottom = Math.abs(this.el.scrollTop + this.el.offsetHeight - this.el.scrollHeight) <= 1; 55 | if (this.messageContainers.length === 0) { 56 | this.appendMessage(new MessageContainer(Author.Bot, html)); 57 | } else { 58 | this.messageContainers.at(-1).html = html; 59 | } 60 | if (this.isScrolledToBottom) { 61 | this.el.scrollTop = this.el.scrollHeight; 62 | } 63 | } 64 | clear() { 65 | this.el.innerHTML = ''; 66 | this.messageContainers = []; 67 | this.isScrolledToBottom = true; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/chat/offscreen/bing_socket.js: -------------------------------------------------------------------------------- 1 | window.parent.postMessage('socket-script-ready', '*'); 2 | 3 | window.addEventListener('message', async (event) => { 4 | window.parent.postMessage({ 5 | message: await handleMessage(event.data.message), 6 | messageId: event.data.messageId, 7 | }, '*'); 8 | }); 9 | 10 | async function handleMessage(message) { 11 | switch (message.action) { 12 | case 'session': 13 | const response = await fetch("https://copilot.microsoft.com/c/api/conversations", { 14 | method: "POST", 15 | body: null, 16 | credentials: "include", 17 | }); 18 | return await response.json(); 19 | case 'delete': 20 | return await fetch(`https://copilot.microsoft.com/c/api/conversations/${message.conversationId}`, { 21 | method: "DELETE", 22 | credentials: "include", 23 | }).then(r => r.text()); 24 | default: 25 | return handleActionWebsocket(message); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/chat/offscreen/iframe_script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Script running in the iframe that has the correct origin. 3 | */ 4 | 5 | (() => { 6 | window.parent.postMessage('iframe-script-ready', '*'); 7 | 8 | window.addEventListener('message', onReceiveMessageFromParent); 9 | 10 | function onReceiveMessageFromParent(event) { 11 | if (event.origin !== new URL(chrome.runtime.getURL("")).origin) return; 12 | 13 | const data = event.data; 14 | if (!('scripts' in data)) return; 15 | 16 | data.scripts.forEach(insertScript); 17 | acknowledge(data.messageId, data.scriptElementId); 18 | 19 | window.removeEventListener('message', onReceiveMessageFromParent); 20 | } 21 | 22 | function acknowledge(messageId, scriptElementId) { 23 | window.parent.postMessage({ 24 | message: `Script "${scriptElementId}" succesfully injected`, 25 | messageId: messageId, 26 | }, '*'); 27 | } 28 | 29 | function insertScript(src) { 30 | const scriptElement = document.createElement('script'); 31 | scriptElement.type = 'text/javascript'; 32 | scriptElement.src = src; 33 | document.body.appendChild(scriptElement); 34 | } 35 | 36 | })(); -------------------------------------------------------------------------------- /src/chat/offscreen/offscreen.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/chat/offscreen/offscreen.js: -------------------------------------------------------------------------------- 1 | const strings = { 2 | scripts: ["src/background/websocket_utils.js", "src/chat/offscreen/bing_socket.js"], 3 | iframeSrc: "https://copilot.microsoft.com/favicon.ico?bing-chat-gpt-4-in-google", 4 | } 5 | 6 | const socketScriptReady = { 7 | _val: false, 8 | _listener: () => {}, 9 | set val(val) { 10 | this._val = val; 11 | this._listener(val); 12 | }, 13 | get val() { 14 | return this._val; 15 | }, 16 | get promise() { 17 | return new Promise(resolve => this._listener = resolve); 18 | } 19 | }; 20 | 21 | setupIframe(strings.scripts.map((src) => chrome.runtime.getURL(src))); 22 | 23 | function setupIframe(scripts) { 24 | const iframe = createIframe(strings.iframeSrc); 25 | window.addEventListener('message', ({data}) => { 26 | switch (data) { 27 | case 'iframe-script-ready': 28 | injectScriptToIframe(iframe, scripts); 29 | break; 30 | case 'socket-script-ready': 31 | socketScriptReady.val = true; 32 | break; 33 | } 34 | }); 35 | chrome.runtime.onMessage.addListener(onReceiveMessageFromExtension); 36 | } 37 | 38 | function createIframe(src) { 39 | const iframe = document.createElement('iframe'); 40 | iframe.src = src; 41 | document.firstElementChild.appendChild(iframe); 42 | return iframe; 43 | } 44 | 45 | function injectScriptToIframe(iframe, scripts) { 46 | const iframeWindow = iframe.contentWindow; 47 | iframeWindow.postMessage({ scripts }, "*"); 48 | } 49 | 50 | function onReceiveMessageFromExtension(message, _, sendResponse) { 51 | if (message.target !== 'offscreen') return; 52 | switch (message.action) { 53 | case 'url': 54 | sendResponse(window.location.href); 55 | break; 56 | default: 57 | sendMessageToIframe(message).then(sendResponse); 58 | break; 59 | } 60 | return true; 61 | } 62 | 63 | async function sendMessageToIframe(message) { 64 | const iframe = document.querySelector('iframe'); 65 | if (!iframe) { 66 | throw 'No iframe'; 67 | } 68 | 69 | if (!socketScriptReady.val) { 70 | await socketScriptReady.promise; 71 | } 72 | 73 | const messageId = Math.random().toString(36).substring(7); 74 | return new Promise(resolve => { 75 | const messageHandler = (event) => { 76 | if (event.data && event.data.messageId === messageId) { 77 | resolve(event.data.message); 78 | window.removeEventListener('message', messageHandler); 79 | } 80 | }; 81 | 82 | window.addEventListener('message', messageHandler); 83 | iframe.contentWindow.postMessage({ message, messageId }, '*'); 84 | }); 85 | } 86 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | const WhichExtension = (() => { 2 | const iconPath = chrome.runtime.getManifest().icons["128"]; 3 | if (iconPath.includes('bingchat')) 4 | return 'bingchat'; 5 | if (iconPath.includes('bard')) 6 | return 'bard'; 7 | return 'optisearch'; 8 | })(); 9 | const isOptiSearch = WhichExtension === 'optisearch'; 10 | const WhichChat = isOptiSearch ? 'chatgpt' : WhichExtension; 11 | const isDebugMode = !chrome.runtime.getManifest().update_url; // if it is set, it means that we loaded the extension from source (not from the store) 12 | 13 | const webstores = { 14 | 'optisearch': typeof browser === 'undefined' ? 15 | 'https://chrome.google.com/webstore/detail/optisearch/bbojmeobdaicehcopocnfhaagefleiae' : 16 | 'https://addons.mozilla.org/fr/firefox/addon/optisearch', 17 | 'bingchat': typeof browser === 'undefined' ? 18 | 'https://chrome.google.com/webstore/detail/bing-chat-gpt-4-in-google/pcnhobmoglanpljipbomknafhdlcgcng': 19 | 'https://addons.mozilla.org/fr/firefox/addon/bing-chat-gpt-4-in-google', 20 | 'bard': typeof browser === 'undefined' ? 21 | 'https://chrome.google.com/webstore/detail/bard-for-search-engines/pkdmfoabhnkpkcacnmgilaeghiggdbgf': 22 | 'https://addons.mozilla.org/fr/firefox/addon/bard-for-search-engines', 23 | } 24 | const webstore = webstores[WhichExtension]; 25 | const donationLink = `https://www.paypal.com/donate?hosted_button_id=${WhichExtension === 'bingchat' ? 'BXBP3JELVS4FL' : 'VPF2BYBDBU5AA'}`; 26 | 27 | const Google = "Google", Ecosia = "Ecosia", Bing = "Bing", Yahoo = "Yahoo", DuckDuckGo = "DuckDuckGo", Baidu = "Baidu", Brave = "Brave Search"; 28 | const OrderEngines = [Google, Bing, Baidu, DuckDuckGo, Ecosia, Brave, Yahoo]; 29 | const EngineTechnicalNames = { 30 | [Google]: 'google', 31 | [Bing]: 'bing', 32 | [Baidu]: 'baidu', 33 | [DuckDuckGo]: 'duckduckgo', 34 | [Ecosia]: 'ecosia', 35 | [Brave]: 'brave', 36 | [Yahoo]: 'yahoo', 37 | }; 38 | -------------------------------------------------------------------------------- /src/engine-specifics.js: -------------------------------------------------------------------------------- 1 | (function (){ 2 | Context.processEngine[Google] = () => { 3 | const udm = parseInt(new URL(window.location.href).searchParams.get("udm") || "0", 10); 4 | if ( udm == 0 || udm == 14 ) return; 5 | 6 | // We are not on the main Google page 7 | Context.rightColumn.parentNode.removeChild(Context.rightColumn); 8 | Context.rightColumn = null; 9 | }; 10 | 11 | Context.processEngine[Brave] = () => { 12 | if (!$(".optisearch-start")) { 13 | Context.rightColumn.prepend(el("div", { className: "optisearch-start" })); 14 | } 15 | 16 | setObserver((mutations) => { 17 | const removedBoxes = getRemovedNodes(mutations) 18 | .filter((x) => x.nodeType === Node.ELEMENT_NODE && x.matches(Context.BOX_SELECTOR)); 19 | if (removedBoxes.length) { 20 | Context.appendBoxes(removedBoxes); 21 | } 22 | }, 23 | Context.rightColumn, 24 | { childList: true } 25 | ); 26 | }; 27 | 28 | /** 29 | * Special method to deal with Ecosia. 30 | * Because in Ecosia, the main column can be removed after few seconds and added again. 31 | * Also Ecosia is the only engine for which the HTML does not change if it is on mobile 32 | * (only @media CSS instructions make it change). 33 | * This also means that we have to deal with eventual resizing of the page 34 | */ 35 | Context.processEngine[Ecosia] = () => { 36 | const searchNav = $(Context.engine.searchNav); 37 | setObserver((mutations) => { 38 | if (getRemovedNodes(mutations).some((n) => n === Context.centerColumn || n === Context.rightColumn)) { 39 | Context.centerColumn = $(Context.engine.centerColumn); 40 | Context.setupRightColumn(); 41 | Context.appendBoxes(Context.boxes); 42 | } 43 | 44 | if (!$(Context.engine.searchNav)) { 45 | insertAfter(searchNav, $(Context.engine.searchNavNeighbor)); 46 | } 47 | }, 48 | document.body, 49 | { childList: true, subtree: true } 50 | ); 51 | 52 | if (typeof Context.engine.onMobile !== "number") return; 53 | 54 | let wasOnMobile = Context.computeIsOnMobile(); 55 | window.addEventListener("resize", () => { 56 | const isOnMobile = Context.computeIsOnMobile(); 57 | if (isOnMobile === wasOnMobile) return; 58 | wasOnMobile = isOnMobile; 59 | const allBoxes = $$(Context.BOX_SELECTOR); 60 | allBoxes.forEach((p) => p.classList[isOnMobile ? "add" : "remove"](Context.MOBILE_CLASS)); 61 | Context.appendBoxes(allBoxes); 62 | }); 63 | }; 64 | 65 | function getRemovedNodes(mutations) { 66 | const removedNodes = []; 67 | for(const mutation of mutations) { 68 | removedNodes.push(...mutation.removedNodes); 69 | } 70 | return removedNodes; 71 | } 72 | })(); 73 | -------------------------------------------------------------------------------- /src/engines.json: -------------------------------------------------------------------------------- 1 | { 2 | "Google": { 3 | "link": "https://www.google.com", 4 | "icon": "https://www.google.com/images/branding/googleg/1x/googleg_standard_color_128dp.png", 5 | "rightColumn": "#rhs", 6 | "centerColumn": "#center_col", 7 | "resultRow": "#rso span:has(> a > h3), #rso div:has(> a [role=link])", 8 | "searchBox": "input.gLFyf.gsfi, input.gLFyf, textarea.gLFyf", 9 | "active": true, 10 | "regex": "^www\\.google\\.", 11 | "style": "[data-optisearch-column]:has(.TzHB6b) .optisearchbox {margin-left: -21px;} .optisearchbox {min-width: 400px;}", 12 | "onMobile": "#cnt > #center_col" 13 | }, 14 | "Bing": { 15 | "link": "https://www.bing.com", 16 | "icon": "https://www.bing.com/favicon.ico", 17 | "rightColumn": "#b_context", 18 | "centerColumn": "main", 19 | "resultRow": ".b_algo", 20 | "searchBox": ".b_searchbox#sb_form_q", 21 | "active": true, 22 | "regex": "\\.bing\\.com$", 23 | "style": ".optisearchbox {margin-right: -20px; margin-left: -20px;}", 24 | "onMobile": "meta[name='viewport']" 25 | }, 26 | "Ecosia": { 27 | "link": "https://www.ecosia.org", 28 | "icon": "https://cdn-static.ecosia.org/static/icons/favicon.ico", 29 | "rightColumn": ".sidebar.web__sidebar[data-test-id=sidebar]", 30 | "rightColumnRemovable": true, 31 | "searchNav": "div.main-header__search-navigation", 32 | "searchNavNeighbor": "div.main-header__navigation", 33 | "centerColumn": ".mainline", 34 | "resultRow": ".mainline__result-wrapper", 35 | "searchBox": ".search-form__input, #search-form-input", 36 | "active": true, 37 | "regex": "\\.ecosia\\.org$", 38 | "style": ".sidebar.web__sidebar[data-test-id=sidebar]{grid-area: sidebar;}", 39 | "onMobile": 992 40 | }, 41 | "Yahoo": { 42 | "link": "https://www.yahoo.com", 43 | "icon": "https://s.yimg.com/oa/build/images/favicons/yahoo.png", 44 | "rightColumn": "#right", 45 | "resultRow": ".dd.algo", 46 | "searchBox": "#yschsp", 47 | "active": false, 48 | "regex": "search\\.yahoo\\.com$" 49 | }, 50 | "DuckDuckGo": { 51 | "link": "https://duckduckgo.com", 52 | "icon": "https://duckduckgo.com/favicon.ico", 53 | "rightColumn": ".js-react-sidebar", 54 | "centerColumn": "[data-area='mainline']", 55 | "resultRow": ".react-results--main > li", 56 | "searchBox": "#search_form_input", 57 | "resultsContainer": "[data-area='mainline']", 58 | "active": true, 59 | "regex": "duckduckgo\\.com$", 60 | "style": "[data-optisearch-column=wide] { max-width: var(--opti-max-width) !important; } .cw{max-width:unset;}", 61 | "onMobile": null 62 | }, 63 | "Brave Search": { 64 | "link": "https://search.brave.com", 65 | "icon": "https://cdn.search.brave.com/serp/v1/static/brand/16c26cd189da3f0f7ba4e55a584ddde6a7853c9cc340ff9f381afc6cb18e9a1e-favicon-32x32.png", 66 | "rightColumn": ".sidebar", 67 | "centerColumn": "#results", 68 | "resultRow": "#results > .snippet", 69 | "searchBox": "#searchbox", 70 | "active": true, 71 | "regex": "search\\.brave\\.com$", 72 | "style": ".optisearchbox { box-shadow: none; border-radius: var(--border-radius-xlarge); } .column-layout {max-width: 100% !important;} [data-optisearch-column=wide] {max-width: var(--opti-max-width); min-width: var(--opti-max-width) !important;}", 73 | "onMobile": "meta[name='viewport']" 74 | }, 75 | "Baidu": { 76 | "link": "https://www.baidu.com/", 77 | "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAPFBMVEVHcEz////////9/f/////p6/2srvK6vPXZ2vrIyvcACt8LG+A9ReUbJuFpb+ooMeFWXOeLj+2bn+98gexUG/hRAAAABHRSTlMAEaz5aKQWgAAAAU9JREFUeAGF092SgyAMBWCr/J54DOr7v+tCRqhdZ7fchJJvMk2QaXrNi/tjLfOr5t2/6zXNY+9DtJhCfoN5GvUTRNYaI4htgGUa21VVJTgnNeJdY4CEmuDmklh8gtgSuvb4BNkSpccncHL9B1j8BJHYXYYI648DRHFJsS0dZKgi3Odg9Vg6KFqXuPsK1my6ABuAt2nGbGBrZxIvgA6WAhGJHTAY6LwNk0arONiLNuDB1oY7LW8JOzpHm/7cQrslhZhY+9F9UNas3YQ1+JxkgA6gXJff4AQBqYBsgEyfoIgcLSYkRx5uJ/IdnNJ6TsmAGLAmRxdoYyDkDrgPYB+JfVJ3oPoGQRTOlasCsRvgG2TpBROixQpYBrDrki157yvNNfiTKuEGYhUUAUACIkLlerW5WMhEy5f9ODZtAmd/OP3ppZx9f01130c9f3+8357/D50CHYMuiWz1AAAAAElFTkSuQmCC", 78 | "rightColumn": "#content_right.cr-offset", 79 | "centerColumn": "#content_left", 80 | "resultRow": ".result", 81 | "searchBox": "#kw", 82 | "active": true, 83 | "regex": "www\\.baidu\\.com$", 84 | "style": "#container.sam_newgrid .right-ceiling {position: static !important;}" 85 | } 86 | } -------------------------------------------------------------------------------- /src/images/bard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/src/images/bard.png -------------------------------------------------------------------------------- /src/images/bard_conversation_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/src/images/bard_conversation_example.png -------------------------------------------------------------------------------- /src/images/bingchat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/src/images/bingchat.png -------------------------------------------------------------------------------- /src/images/bingchat_conversation_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/src/images/bingchat_conversation_example.png -------------------------------------------------------------------------------- /src/images/chatgpt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/src/images/chatgpt.png -------------------------------------------------------------------------------- /src/images/chatgpt_conversation_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/src/images/chatgpt_conversation_example.png -------------------------------------------------------------------------------- /src/images/copilot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/src/images/copilot.png -------------------------------------------------------------------------------- /src/images/engines/Baidu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/src/images/engines/Baidu.png -------------------------------------------------------------------------------- /src/images/engines/Bing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/src/images/engines/Bing.png -------------------------------------------------------------------------------- /src/images/engines/Brave Search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/src/images/engines/Brave Search.png -------------------------------------------------------------------------------- /src/images/engines/DuckDuckGo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/src/images/engines/DuckDuckGo.png -------------------------------------------------------------------------------- /src/images/engines/Ecosia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/src/images/engines/Ecosia.png -------------------------------------------------------------------------------- /src/images/engines/Google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/src/images/engines/Google.png -------------------------------------------------------------------------------- /src/images/engines/Yahoo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/src/images/engines/Yahoo.png -------------------------------------------------------------------------------- /src/images/genius.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/src/images/genius.png -------------------------------------------------------------------------------- /src/images/gpxfollower.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/src/images/gpxfollower.png -------------------------------------------------------------------------------- /src/images/mathworks.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/src/images/mathworks.ico -------------------------------------------------------------------------------- /src/images/mdn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/src/images/mdn.png -------------------------------------------------------------------------------- /src/images/optisearch_conversation_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/src/images/optisearch_conversation_example.png -------------------------------------------------------------------------------- /src/images/stackexchange.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/src/images/stackexchange.ico -------------------------------------------------------------------------------- /src/images/unity.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/src/images/unity.ico -------------------------------------------------------------------------------- /src/images/w3schools.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/src/images/w3schools.ico -------------------------------------------------------------------------------- /src/images/wikipedia.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dj0ulo/OptiSearch/47816328b304a7e699c8af1b271b804542ba1db5/src/images/wikipedia.ico -------------------------------------------------------------------------------- /src/libs/drawdown.js: -------------------------------------------------------------------------------- 1 | /** 2 | * https://github.com/adamvleggett/drawdown 3 | * (c) Adam Leggett 4 | */ 5 | 6 | 7 | ; function markdown(src) { 8 | 9 | var rx_lt = //g; 11 | var rx_space = /\t|\r|\uf8ff/g; 12 | var rx_escape = /\\([\\\|`*_{}\[\]()#+\-~])/g; 13 | var rx_hr = /^([*\-=_] *){3,}$/gm; 14 | var rx_blockquote = /\n *> *([^]*?)(?=(\n|$){2})/g; 15 | var rx_list = /\n( *)(?:[*\-+]|((\d+)|([a-z])|[A-Z])[.)]) +([^]*?)(?=(\n|$){2})/g; 16 | var rx_listjoin = /<\/(ol|ul)>\n\n<\1>/g; 17 | var rx_highlight = /(^|[^A-Za-z\d\\])(([*_])|(~)|(\^)|(--)|(\+\+))(\2?)([^<]*?)\2\8(?!\2)(?=\W|_|$)/g; 18 | var rx_code = /```\w*\n?(((?!```).)*)(```|$)/gs; 19 | var rx_one_line_code = /`(((?!`).)*)(`|$)/g; 20 | var rx_link = /((!?)\[(.*?)\]\((.*?)( ".*")?\)|\\([\\`*_{}\[\]()#+\-.!~]))/g; 21 | var rx_table = /\n(( *\|.*?\| *\n)+)/g; 22 | var rx_thead = /^.*\n( *\|( *\:?-+\:?-+\:? *\|)* *\n|)/; 23 | var rx_row = /.*\n/g; 24 | var rx_cell = /\||(.*?[^\\])\|/g; 25 | var rx_heading = /(?=^|>|\n)([>\s]*?)(#{1,6}) (.*?)( #*)? *(?=\n|$)/g; 26 | var rx_para = /(?=^|>|\n)\s*\n+([^<]+?)\n+\s*(?=\n|<|$)/g; 27 | var rx_stash = /-\d+\uf8ff/g; 28 | 29 | function replace(rex, fn) { 30 | src = src.replace(rex, fn); 31 | } 32 | 33 | function element(tag, content) { 34 | return '<' + tag + '>' + content + ''; 35 | } 36 | 37 | function blockquote(src) { 38 | return src.replace(rx_blockquote, (all, content) => { 39 | return element('blockquote', blockquote(highlight(content.replace(/^ *> */gm, '')))); 40 | }); 41 | } 42 | 43 | function list(src) { 44 | return src.replace(rx_list, (all, ind, ol, num, low, content) => { 45 | var entry = element('li', highlight(content.split( 46 | RegExp('\n ?' + ind + '(?:(?:\\d+|[a-zA-Z])[.)]|[*\\-+]) +', 'g')).map(list).join('
  • '))); 47 | 48 | return '\n' + (ol 49 | ? '
      ' 51 | : parseInt(ol, 36) - 9 + '" style="list-style-type:' + (low ? 'low' : 'upp') + 'er-alpha">') + entry + '
    ' 52 | : element('ul', entry)); 53 | }); 54 | } 55 | 56 | function highlight(src) { 57 | return src.replace(rx_highlight, (all, _, p1, emp, sub, sup, small, big, p2, content) => { 58 | return _ + element( 59 | emp ? (p2 ? 'strong' : 'em') 60 | : sub ? (p2 ? 's' : 'sub') 61 | : sup ? 'sup' 62 | : small ? 'small' 63 | : big ? 'big' 64 | : 'code', 65 | highlight(content)); 66 | }); 67 | } 68 | 69 | function unesc(str) { 70 | return str.replace(rx_escape, '$1'); 71 | } 72 | 73 | var stash = []; 74 | var si = 0; 75 | 76 | src = '\n' + src + '\n'; 77 | 78 | replace(rx_lt, '<'); 79 | replace(rx_gt, '>'); 80 | replace(rx_space, ' '); 81 | 82 | // multiline code 83 | replace(rx_code, (all, p1) => { 84 | stash[--si] = element('pre', element('code', p1.trim())); 85 | return si + '\uf8ff'; 86 | }); 87 | 88 | // blockquote 89 | src = blockquote(src); 90 | 91 | // horizontal rule 92 | replace(rx_hr, '
    '); 93 | 94 | // list 95 | src = list(src); 96 | replace(rx_listjoin, ''); 97 | 98 | // link or image 99 | replace(rx_link, (all, p1, p2, p3, p4, p5, p6) => { 100 | stash[--si] = p4 101 | ? p2 102 | ? '' + p3 + '' 103 | : '' + unesc(highlight(p3)) + '' 104 | : p6; 105 | return si + '\uf8ff'; 106 | }); 107 | 108 | // table 109 | replace(rx_table, (all, table) => { 110 | var sep = table.match(rx_thead)[1]; 111 | return '\n' + element('table', 112 | table.replace(rx_row, (row, ri) => { 113 | return row == sep ? '' : element('tr', row.replace(rx_cell, (all, cell, ci) => { 114 | return ci ? element(sep && !ri ? 'th' : 'td', unesc(highlight(cell || ''))) : '' 115 | })) 116 | }) 117 | ) 118 | }); 119 | 120 | // heading 121 | replace(rx_heading, (all, _, p1, p2) => _ + element('h' + p1.length, unesc(highlight(p2)))); 122 | 123 | // paragraph 124 | replace(rx_para, (all, content) => element('p', unesc(highlight(content)))); 125 | 126 | // one line code 127 | replace(rx_one_line_code, (all, p1) => element('code', p1)); 128 | 129 | // stash 130 | replace(rx_stash, (all) => stash[parseInt(all)]); 131 | 132 | return src.trim(); 133 | }; -------------------------------------------------------------------------------- /src/libs/sha3.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Minified by jsDelivr using Terser v5.19.2. 3 | * Original file: /npm/js-sha3@0.9.3/src/sha3.js 4 | * 5 | * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files 6 | */ 7 | /** 8 | * [js-sha3]{@link https://github.com/emn178/js-sha3} 9 | * 10 | * @version 0.9.3 11 | * @author Chen, Yi-Cyuan [emn178@gmail.com] 12 | * @copyright Chen, Yi-Cyuan 2015-2023 13 | * @license MIT 14 | */ 15 | !function(){"use strict";var t="input is invalid type",e="object"==typeof window,r=e?window:{};r.JS_SHA3_NO_WINDOW&&(e=!1);var n=!e&&"object"==typeof self;!r.JS_SHA3_NO_NODE_JS&&"object"==typeof process&&process.versions&&process.versions.node?r=global:n&&(r=self);for(var i=!r.JS_SHA3_NO_COMMON_JS&&"object"==typeof module&&module.exports,o="function"==typeof define&&define.amd,a=!r.JS_SHA3_NO_ARRAY_BUFFER&&"undefined"!=typeof ArrayBuffer,s="0123456789abcdef".split(""),u=[4,1024,262144,67108864],f=[0,8,16,24],c=[1,0,32898,0,32906,2147483648,2147516416,2147483648,32907,0,2147483649,0,2147516545,2147483648,32777,2147483648,138,0,136,0,2147516425,0,2147483658,0,2147516555,0,139,2147483648,32905,2147483648,32771,2147483648,32770,2147483648,128,2147483648,32778,0,2147483658,2147483648,2147516545,2147483648,32896,2147483648,2147483649,0,2147516424,2147483648],h=[224,256,384,512],p=[128,256],d=["hex","buffer","arrayBuffer","array","digest"],l={128:168,256:136},y=r.JS_SHA3_NO_NODE_JS||!Array.isArray?function(t){return"[object Array]"===Object.prototype.toString.call(t)}:Array.isArray,b=!a||!r.JS_SHA3_NO_ARRAY_BUFFER_IS_VIEW&&ArrayBuffer.isView?ArrayBuffer.isView:function(t){return"object"==typeof t&&t.buffer&&t.buffer.constructor===ArrayBuffer},v=function(e){var r=typeof e;if("string"===r)return[e,!0];if("object"!==r||null===e)throw new Error(t);if(a&&e.constructor===ArrayBuffer)return[new Uint8Array(e),!1];if(!y(e)&&!b(e))throw new Error(t);return[e,!1]},A=function(t){return 0===v(t)[0].length},g=function(t){for(var e=[],r=0;r>5,this.byteCount=this.blockCount<<2,this.outputBlocks=r>>5,this.extraBytes=(31&r)>>3;for(var n=0;n<50;++n)this.s[n]=0}function I(t,e,r){H.call(this,t,e,r)}H.prototype.update=function(t){if(this.finalized)throw new Error("finalize already called");var e=v(t);t=e[0];for(var r,n,i=e[1],o=this.blocks,a=this.byteCount,s=t.length,u=this.blockCount,c=0,h=this.s;c>2]|=n<>2]|=(192|n>>6)<>2]|=(128|63&n)<=57344?(o[r>>2]|=(224|n>>12)<>2]|=(128|n>>6&63)<>2]|=(128|63&n)<>2]|=(240|n>>18)<>2]|=(128|n>>12&63)<>2]|=(128|n>>6&63)<>2]|=(128|63&n)<>2]|=t[c]<=a){for(this.start=r-a,this.block=o[u],r=0;r>=8);r>0;)i.unshift(r),r=255&(t>>=8),++n;return e?i.push(n):i.unshift(n),this.update(i),i.length},H.prototype.encodeString=function(t){var e=v(t);t=e[0];var r=e[1],n=0,i=t.length;if(r)for(var o=0;o=57344?n+=3:(a=65536+((1023&a)<<10|1023&t.charCodeAt(++o)),n+=4)}else n=i;return n+=this.encode(8*n),this.update(t),n},H.prototype.bytepad=function(t,e){for(var r=this.encode(e),n=0;n>2]|=this.padding[3&e],this.lastByteIndex===this.byteCount)for(t[0]=t[r],e=1;e>4&15]+s[15&t]+s[t>>12&15]+s[t>>8&15]+s[t>>20&15]+s[t>>16&15]+s[t>>28&15]+s[t>>24&15];a%e==0&&(r=g(r),R(r),o=0)}return i&&(t=r[o],u+=s[t>>4&15]+s[15&t],i>1&&(u+=s[t>>12&15]+s[t>>8&15]),i>2&&(u+=s[t>>20&15]+s[t>>16&15])),u},H.prototype.arrayBuffer=function(){this.finalize();var t,e=this.blockCount,r=this.s,n=this.outputBlocks,i=this.extraBytes,o=0,a=0,s=this.outputBits>>3;t=i?new ArrayBuffer(n+1<<2):new ArrayBuffer(s);for(var u=new Uint32Array(t);a>8&255,u[t+2]=e>>16&255,u[t+3]=e>>24&255;s%r==0&&(n=g(n),R(n))}return o&&(t=s<<2,e=n[a],u[t]=255&e,o>1&&(u[t+1]=e>>8&255),o>2&&(u[t+2]=e>>16&255)),u},I.prototype=new H,I.prototype.finalize=function(){return this.encode(this.outputBits,!0),H.prototype.finalize.call(this)};var R=function(t){var e,r,n,i,o,a,s,u,f,h,p,d,l,y,b,v,A,g,B,_,k,w,S,C,x,m,O,z,N,J,M,j,E,H,I,R,F,U,D,V,W,Y,K,q,G,L,P,Q,T,X,Z,$,tt,et,rt,nt,it,ot,at,st,ut,ft,ct;for(n=0;n<48;n+=2)i=t[0]^t[10]^t[20]^t[30]^t[40],o=t[1]^t[11]^t[21]^t[31]^t[41],a=t[2]^t[12]^t[22]^t[32]^t[42],s=t[3]^t[13]^t[23]^t[33]^t[43],u=t[4]^t[14]^t[24]^t[34]^t[44],f=t[5]^t[15]^t[25]^t[35]^t[45],h=t[6]^t[16]^t[26]^t[36]^t[46],p=t[7]^t[17]^t[27]^t[37]^t[47],e=(d=t[8]^t[18]^t[28]^t[38]^t[48])^(a<<1|s>>>31),r=(l=t[9]^t[19]^t[29]^t[39]^t[49])^(s<<1|a>>>31),t[0]^=e,t[1]^=r,t[10]^=e,t[11]^=r,t[20]^=e,t[21]^=r,t[30]^=e,t[31]^=r,t[40]^=e,t[41]^=r,e=i^(u<<1|f>>>31),r=o^(f<<1|u>>>31),t[2]^=e,t[3]^=r,t[12]^=e,t[13]^=r,t[22]^=e,t[23]^=r,t[32]^=e,t[33]^=r,t[42]^=e,t[43]^=r,e=a^(h<<1|p>>>31),r=s^(p<<1|h>>>31),t[4]^=e,t[5]^=r,t[14]^=e,t[15]^=r,t[24]^=e,t[25]^=r,t[34]^=e,t[35]^=r,t[44]^=e,t[45]^=r,e=u^(d<<1|l>>>31),r=f^(l<<1|d>>>31),t[6]^=e,t[7]^=r,t[16]^=e,t[17]^=r,t[26]^=e,t[27]^=r,t[36]^=e,t[37]^=r,t[46]^=e,t[47]^=r,e=h^(i<<1|o>>>31),r=p^(o<<1|i>>>31),t[8]^=e,t[9]^=r,t[18]^=e,t[19]^=r,t[28]^=e,t[29]^=r,t[38]^=e,t[39]^=r,t[48]^=e,t[49]^=r,y=t[0],b=t[1],L=t[11]<<4|t[10]>>>28,P=t[10]<<4|t[11]>>>28,z=t[20]<<3|t[21]>>>29,N=t[21]<<3|t[20]>>>29,st=t[31]<<9|t[30]>>>23,ut=t[30]<<9|t[31]>>>23,Y=t[40]<<18|t[41]>>>14,K=t[41]<<18|t[40]>>>14,H=t[2]<<1|t[3]>>>31,I=t[3]<<1|t[2]>>>31,v=t[13]<<12|t[12]>>>20,A=t[12]<<12|t[13]>>>20,Q=t[22]<<10|t[23]>>>22,T=t[23]<<10|t[22]>>>22,J=t[33]<<13|t[32]>>>19,M=t[32]<<13|t[33]>>>19,ft=t[42]<<2|t[43]>>>30,ct=t[43]<<2|t[42]>>>30,et=t[5]<<30|t[4]>>>2,rt=t[4]<<30|t[5]>>>2,R=t[14]<<6|t[15]>>>26,F=t[15]<<6|t[14]>>>26,g=t[25]<<11|t[24]>>>21,B=t[24]<<11|t[25]>>>21,X=t[34]<<15|t[35]>>>17,Z=t[35]<<15|t[34]>>>17,j=t[45]<<29|t[44]>>>3,E=t[44]<<29|t[45]>>>3,C=t[6]<<28|t[7]>>>4,x=t[7]<<28|t[6]>>>4,nt=t[17]<<23|t[16]>>>9,it=t[16]<<23|t[17]>>>9,U=t[26]<<25|t[27]>>>7,D=t[27]<<25|t[26]>>>7,_=t[36]<<21|t[37]>>>11,k=t[37]<<21|t[36]>>>11,$=t[47]<<24|t[46]>>>8,tt=t[46]<<24|t[47]>>>8,q=t[8]<<27|t[9]>>>5,G=t[9]<<27|t[8]>>>5,m=t[18]<<20|t[19]>>>12,O=t[19]<<20|t[18]>>>12,ot=t[29]<<7|t[28]>>>25,at=t[28]<<7|t[29]>>>25,V=t[38]<<8|t[39]>>>24,W=t[39]<<8|t[38]>>>24,w=t[48]<<14|t[49]>>>18,S=t[49]<<14|t[48]>>>18,t[0]=y^~v&g,t[1]=b^~A&B,t[10]=C^~m&z,t[11]=x^~O&N,t[20]=H^~R&U,t[21]=I^~F&D,t[30]=q^~L&Q,t[31]=G^~P&T,t[40]=et^~nt&ot,t[41]=rt^~it&at,t[2]=v^~g&_,t[3]=A^~B&k,t[12]=m^~z&J,t[13]=O^~N&M,t[22]=R^~U&V,t[23]=F^~D&W,t[32]=L^~Q&X,t[33]=P^~T&Z,t[42]=nt^~ot&st,t[43]=it^~at&ut,t[4]=g^~_&w,t[5]=B^~k&S,t[14]=z^~J&j,t[15]=N^~M&E,t[24]=U^~V&Y,t[25]=D^~W&K,t[34]=Q^~X&$,t[35]=T^~Z&tt,t[44]=ot^~st&ft,t[45]=at^~ut&ct,t[6]=_^~w&y,t[7]=k^~S&b,t[16]=J^~j&C,t[17]=M^~E&x,t[26]=V^~Y&H,t[27]=W^~K&I,t[36]=X^~$&q,t[37]=Z^~tt&G,t[46]=st^~ft&et,t[47]=ut^~ct&rt,t[8]=w^~y&v,t[9]=S^~b&A,t[18]=j^~C&m,t[19]=E^~x&O,t[28]=Y^~H&R,t[29]=K^~I&F,t[38]=$^~q&L,t[39]=tt^~G&P,t[48]=ft^~et&nt,t[49]=ct^~rt&it,t[0]^=c[n],t[1]^=c[n+1]};if(i)module.exports=m;else{for(z=0;z ul { 107 | padding-inline-start: 0px; 108 | } 109 | 110 | #version { 111 | font-size: 11px; 112 | } 113 | 114 | .menu { 115 | margin: 5px; 116 | } 117 | 118 | .menu_title { 119 | display: block; 120 | white-space: nowrap; 121 | font-weight: bold; 122 | user-select: none; 123 | font-size: x-small; 124 | overflow: hidden; 125 | display: flex; 126 | width: 100%; 127 | margin-bottom: 1em; 128 | margin-top: 1em; 129 | } 130 | 131 | .menu_title > span { 132 | padding: 0 2em; 133 | flex: 1 1 100px; 134 | text-align: center; 135 | } 136 | 137 | .menu_title > span > span { 138 | vertical-align: top; 139 | } 140 | 141 | .ad { 142 | display: block; 143 | text-align: center; 144 | margin: 1em 0; 145 | } 146 | 147 | hr { 148 | border-color: rgb(38, 44, 46); 149 | height: 0.5px; 150 | } 151 | 152 | hr.flexchild { 153 | display: inline-block; 154 | flex: 1 1 200px; 155 | } 156 | 157 | label.setting { 158 | /* user-select: none; */ 159 | overflow: hidden; 160 | } 161 | 162 | label.setting > :last-child { 163 | vertical-align: top; 164 | } 165 | 166 | .setting-title { 167 | display: inline-block; 168 | text-align: right; 169 | width: 140px; 170 | overflow: hidden; 171 | text-overflow: ellipsis; 172 | margin-right: 10px; 173 | position: relative; 174 | } 175 | 176 | .setting-title .input { 177 | margin-left: 20px; 178 | } 179 | 180 | .checkbox { 181 | height: 15px; 182 | width: 15px; 183 | cursor: pointer; 184 | } 185 | 186 | a { 187 | text-decoration: none; 188 | color: #4590c5; 189 | } 190 | 191 | a:hover { 192 | text-decoration: underline; 193 | } 194 | 195 | select, 196 | input { 197 | background-color: var(--dark-grey); 198 | color: var(--color); 199 | border-radius: 2px; 200 | padding: 2px 5px; 201 | cursor: pointer; 202 | } 203 | 204 | select:focus, 205 | input:focus { 206 | outline: none; 207 | } 208 | 209 | /*** FOOTER ***/ 210 | 211 | footer { 212 | margin: auto; 213 | width: 100%; 214 | background-color: var(--bg); 215 | box-shadow: 0px 1px 20px 0px black; 216 | } 217 | 218 | .footlinks-container { 219 | display: flex; 220 | justify-content: space-evenly; 221 | } 222 | 223 | .footlinks-container a { 224 | margin: 0 8px; 225 | } 226 | 227 | .grid { 228 | display: grid; 229 | justify-content: center; 230 | } 231 | 232 | /*** UPGRADE BUTTON ***/ 233 | 234 | #premium { 235 | width: 100%; 236 | height: 1.5rem; 237 | margin: 10px 0; 238 | padding: 5px 5px; 239 | } 240 | 241 | .premium-link { 242 | display: block; 243 | text-align: center; 244 | font-size: 0.9em; 245 | margin-bottom: 10px; 246 | color: var(--lighter-grey); 247 | } 248 | 249 | .upgrade-button { 250 | border-radius: 3px; 251 | 252 | cursor: pointer; 253 | 254 | color: #fff; 255 | background-color: #EB9B05;; 256 | 257 | text-align: center; 258 | 259 | transition: 0.3s; 260 | opacity: 0.9; 261 | } 262 | 263 | .upgrade-button:hover { 264 | opacity: 1; 265 | } 266 | 267 | .upgrade-button > span { 268 | display: inline-block; 269 | vertical-align: middle; 270 | line-height: 1.2em; 271 | font-size: 1.3em; 272 | font-weight: bold; 273 | } 274 | 275 | .anim-right { 276 | animation: slideInFromRight 0.5s ease forwards; 277 | grid-column: 3 /4; 278 | opacity: 0; 279 | } 280 | .anim-left { 281 | animation: slideInFromLeft 0.5s ease forwards; 282 | grid-column: 1 /2; 283 | opacity: 0; 284 | } 285 | 286 | @keyframes slideInFromRight { 287 | from { 288 | transform: translateX(+5em); 289 | } 290 | to { 291 | opacity: 1; 292 | transform: translateX(0); 293 | } 294 | } 295 | @keyframes slideInFromLeft { 296 | from { 297 | transform: translateX(-5em); 298 | } 299 | to { 300 | opacity: 1; 301 | transform: translateX(0); 302 | } 303 | } 304 | 305 | /* GPX Follower ad */ 306 | 307 | #gpxfollower { 308 | display: flex; 309 | flex-direction: column; 310 | align-items: flex-start; /* Align items to the start (left) */ 311 | background-color: var(--darker-bg); 312 | padding: 0.8em 1em; 313 | margin-top: 1em; 314 | text-decoration: none; 315 | } 316 | 317 | #gpxfollower > #also-from { 318 | font-size: 0.9em; 319 | color: var(--lighter-grey); 320 | margin-bottom: .5em; /* Spacing below "Also from OptiSearch" */ 321 | } 322 | 323 | .gpxfollower-app { 324 | display: flex; 325 | align-items: center; 326 | } 327 | 328 | .gpxfollower-app img { 329 | width: 72px; /* Adjust as needed */ 330 | height: auto; 331 | border-radius: 20%; 332 | } 333 | 334 | #gpxfollower:hover .text-app span:first-child { 335 | text-decoration: underline; 336 | } 337 | 338 | .text-app span:first-child { 339 | font-weight: bold; 340 | font-size: 1.2em; 341 | margin-bottom: 0.4em; 342 | } 343 | .text-app { 344 | display: flex; 345 | flex-direction: column; 346 | padding: .5em; 347 | } 348 | 349 | .text-app span:last-child { 350 | color: var(--lighter-grey); 351 | } 352 | 353 | .icon-container { 354 | position: relative; 355 | display: inline-block; /* Ensure the container wraps around the icon and text */ 356 | } 357 | 358 | .new-text { 359 | position: absolute; 360 | top: 0; 361 | right: 0; 362 | font-weight: bold; 363 | color: var(--color); 364 | padding: 3px 5px; /* Adjust as needed */ 365 | background-color: red; /* Adjust as needed */ 366 | animation: bounce 0.5s infinite alternate; /* Adjust duration and timing as needed */ 367 | } 368 | 369 | @keyframes bounce { 370 | 0% { 371 | transform: translate(20%, 20%) rotate(40deg) scale(0.8); /* Initial scale */ 372 | } 373 | 100% { 374 | transform: translate(20%, 20%) rotate(40deg) scale(.9); /* Scale at the peak of the bounce */ 375 | } 376 | } 377 | -------------------------------------------------------------------------------- /src/popup/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
    16 |

    17 | 18 | Extension icon 19 | 20 | 21 |

    22 |
    23 |
    24 |
    25 |
    26 |
    27 |
    28 |
    29 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/popup/popup.js: -------------------------------------------------------------------------------- 1 | (async function () { 2 | /** 3 | * Create a title and a bar for an section in the options 4 | * @param {string} name 5 | * @returns Element 6 | */ 7 | const titleSection = (name) => { 8 | const title = el("span", { className: "menu_title" }); 9 | el("hr", { className: 'flexchild' }, title) 10 | el("span", { textContent: _t(name) }, title); 11 | el("hr", { className: 'flexchild' }, title) 12 | return title; 13 | } 14 | 15 | renderDocText(); 16 | 17 | const manifest = chrome.runtime.getManifest(); 18 | $('#name').textContent = manifest.name; 19 | $('#version').textContent = manifest.version; 20 | const extensionIcon = $('header img'); 21 | extensionIcon.src = chrome.runtime.getURL(manifest.icons[128]); 22 | extensionIcon.title = _t("Go to Chrome Web Store"); 23 | $('.title > a').href = webstore; 24 | $('#feedback').href = webstore + '/reviews'; 25 | 26 | const upgradeButton = document.getElementById("premium"); 27 | const upgradeButtonSpan = upgradeButton.querySelector('span'); 28 | const premiumLink = document.getElementById('premium-link'); 29 | 30 | const extpay = ExtPay('optisearch'); 31 | extpay.getUser().then(user => { 32 | upgradeButton.classList.add('upgrade-button'); 33 | if (user.paidAt) { 34 | upgradeButtonSpan.textContent = _t('Manage subscription'); 35 | upgradeButton.addEventListener('click', extpay.openPaymentPage); 36 | premiumLink.style.display = 'none'; 37 | } else { 38 | upgradeButtonSpan.textContent = _t('Upgrade to Premium'); 39 | upgradeButton.addEventListener('click', premiumPresentationPopup); 40 | premiumLink.addEventListener('click', extpay.openLoginPage); 41 | } 42 | }).catch(err => { 43 | upgradeButtonSpan.textContent = _t('Failed to load subscription status'); 44 | premiumLink.style.display = 'none'; 45 | }) 46 | 47 | const liEng = document.querySelector("#engines"); 48 | 49 | const [settings, engines, save] = await Promise.all([getSettings(), loadEngines(), loadSettings()]); 50 | 51 | OrderEngines.forEach((engineName, i) => { 52 | const e = engines[engineName]; 53 | if (e.active) { 54 | const div = el("div", { 55 | className: "engine", 56 | style: `--order: ${i + 1};`, 57 | onclick: () => chrome.tabs.create({ active: true, url: e.link }) 58 | }, liEng); 59 | 60 | el("img", { 61 | src: `../images/engines/${engineName}.png`, 62 | title: engineName, 63 | className: 'icon', 64 | }, div); 65 | } 66 | }); 67 | 68 | 69 | const optionsContainer = document.getElementById("options-container"); 70 | const disabledOptions = []; 71 | //options 72 | Object.keys(settings).forEach((category) => { 73 | const optionsInCategory = Object.entries(settings[category]) 74 | .filter(([_, spec]) => !('active' in spec) || spec['active'] === true); 75 | if (optionsInCategory.length === 0) return; 76 | 77 | optionsContainer.append(titleSection(category)); 78 | 79 | const sublist = el("ul", { className: "sublist", style: "display: block" }, optionsContainer); 80 | 81 | optionsInCategory.forEach(([o, spec]) => { 82 | if ('active' in spec && spec['active'] === false) return; 83 | if (!save[o] && spec.slaves) 84 | disabledOptions.push(...spec.slaves); 85 | const li = el("li", { id: o }, sublist); 86 | 87 | const label = el("label", { 88 | className: "setting", 89 | style: "display: inline-block" 90 | }, li); 91 | 92 | const spanImg = el("span", { 93 | className: "setting-title", 94 | innerHTML: spec.href ? `${_t(spec.name)}` : _t(spec.name), 95 | title: _t(spec.title ?? spec.name), 96 | }, label); 97 | 98 | if (spec.icon) { 99 | const img = el("div", { className: "icon" }, spanImg); 100 | img.style = `background-image: url(../images/${spec.icon}); 101 | background-size: contain; 102 | width: 14px; 103 | height: 14px; 104 | display: inline-block;`; 105 | spanImg.prepend(img); 106 | } 107 | 108 | if (typeof spec.default === 'number') { 109 | el("input", { 110 | type: "number", 111 | style: "width: 2em", 112 | value: save[o], 113 | min: spec.min, 114 | max: spec.max, 115 | onchange: ({ target }) => set(o, target.value), 116 | }, label) 117 | return; 118 | } 119 | if (spec.options) { 120 | if (!Object.keys(spec.options).includes(save[o])) { 121 | set(o, spec.default); 122 | } 123 | const select = el("select", { 124 | value: save[o], 125 | onchange: ({ target }) => set(o, target.value), 126 | }, label); 127 | Object.entries(spec.options).forEach(([key, props]) => { 128 | el('option', { value: key, text: _t(props.name), selected: save[o] === key }, select); 129 | }); 130 | return; 131 | } 132 | 133 | const checkDiv = el("div", { 134 | style: "display: inline-block" 135 | }, label) 136 | 137 | const checkbox = el('input', { 138 | className: "checkbox", 139 | type: "checkbox", 140 | checked: save[o], 141 | onchange: ({ target }) => { 142 | set(o, target.checked); 143 | if (spec.slaves) { 144 | spec.slaves.forEach((slave) => { 145 | $$(`#${slave} input`).forEach((checkbox) => { 146 | checkbox.disabled = !save[o]; 147 | }); 148 | }) 149 | } 150 | } 151 | }, checkDiv); 152 | 153 | if (disabledOptions.includes(o)) { 154 | checkbox.disabled = true; 155 | } 156 | }); 157 | 158 | if (isOptiSearch && category === 'AI Assitant') { 159 | el('a', { 160 | className: 'ad', 161 | innerHTML: _t('Get answers from $AI$', 'Microsoft Copilot'), 162 | href: webstores['bingchat'], 163 | }, sublist); 164 | el('a', { 165 | className: 'ad', 166 | innerHTML: _t('Get answers from $AI$', 'Google Gemini'), 167 | href: webstores['bard'], 168 | }, sublist); 169 | } 170 | 171 | }); 172 | 173 | if (!isOptiSearch) { 174 | el('a', { 175 | className: 'ad', 176 | innerHTML: _t('I want answers from ChatGPT and StackOverflow too!'), 177 | href: webstores['optisearch'], 178 | }, optionsContainer); 179 | } 180 | if (WhichExtension === 'bingchat') { 181 | el('a', { 182 | className: 'ad', 183 | innerHTML: _t('I want answers from $AI$ too!', 'Google Gemini'), 184 | href: webstores['bard'], 185 | }, optionsContainer); 186 | } 187 | if (WhichExtension === 'bard') { 188 | el('a', { 189 | className: 'ad', 190 | innerHTML: _t('I want answers from $AI$ too!', 'Microsoft Copilot'), 191 | href: webstores['bingchat'], 192 | }, optionsContainer); 193 | } 194 | 195 | function set(key, value) { 196 | save[key] = value; 197 | saveSettings(save); 198 | chrome.tabs.query({}, (tabs) => { 199 | tabs.forEach(({ id }) => { 200 | chrome.tabs.sendMessage(id, { type: 'updateSetting', key, value }, 201 | () => chrome.runtime.lastError // reading lastError prevents from logging an error for the tabs w/o content script 202 | ); 203 | }); 204 | }); 205 | } 206 | 207 | if (onChrome()) hrefPopUp(); 208 | })(); -------------------------------------------------------------------------------- /src/popup/premium.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --premium-yellow: #FCD53F; 3 | --text-color: #111; 4 | --bg: #fff; 5 | --grey: #686868; 6 | --scroll-bar-color: var(--grey); 7 | --scroll-bar-bg-color: var(--bg); 8 | --footer-height: 5rem; 9 | } 10 | body { 11 | font-family: "Segoe UI", Consolas, Tahoma, sans-serif; 12 | font-size: 15px; 13 | background: var(--bg); 14 | color: var(--text-color); 15 | text-align: center; 16 | overflow-y: auto; 17 | margin: 0; 18 | } 19 | 20 | main { 21 | padding: 1rem; 22 | } 23 | 24 | #title-container { 25 | font-size: 1.6rem; 26 | font-weight: bold; 27 | margin-bottom: 1rem; 28 | } 29 | #title-container img, 30 | #other-extensions img { 31 | position: relative; 32 | height: 2.2em; 33 | top: 0.8em; 34 | margin-right: 0.5em; 35 | } 36 | #extension-name { 37 | display: inline; 38 | margin-bottom: 1em; 39 | } 40 | #benefits { 41 | display: grid; 42 | width: fit-content; 43 | margin: 0 auto; 44 | grid-template-columns: 1fr auto; 45 | text-align: left; 46 | } 47 | #benefits .icon > svg { 48 | height: 2rem; 49 | } 50 | #benefits .icon { 51 | position: relative; 52 | top: 0.5em; 53 | padding: 0 1em; 54 | fill: var(--grey); 55 | stroke: var(--grey); 56 | } 57 | #benefits .description { 58 | padding: 1rem 0; 59 | } 60 | 61 | .clear { 62 | height: var(--footer-height); 63 | margin-top: 2rem; 64 | } 65 | 66 | #example-screenshot { 67 | display: block; 68 | margin: 1rem auto; 69 | } 70 | #also { 71 | font-size: 1.2rem; 72 | margin-top: 1rem; 73 | } 74 | #other-extensions { 75 | text-align: left; 76 | width: fit-content; 77 | margin: 0 auto; 78 | } 79 | #other-extensions > *{ 80 | margin: .5em 0; 81 | } 82 | #other-extensions a { 83 | font-size: 1.2rem; 84 | font-weight: bold; 85 | text-decoration: none; 86 | } 87 | #other-extensions a:hover { 88 | text-decoration: underline; 89 | } 90 | 91 | 92 | footer { 93 | position: fixed; 94 | margin: auto; 95 | height: var(--footer-height); 96 | bottom: 0; 97 | width: 100%; 98 | background-color: var(--bg); 99 | box-shadow: 0px 3px 8px 8px #00000024; 100 | } 101 | 102 | #upgrade-button { 103 | padding: 10px 20px; 104 | font-size: 1.2em; 105 | font-weight: bold; 106 | background-color: var(--premium-yellow); 107 | color: #111; 108 | border-radius: 5px; 109 | cursor: pointer; 110 | margin: 0.8em 20%; 111 | } 112 | 113 | *::-webkit-scrollbar { 114 | width: 12px; 115 | height: 12px; 116 | } 117 | 118 | *::-webkit-scrollbar-track { 119 | background: var(--scroll-bar-bg-color); 120 | } 121 | 122 | *::-webkit-scrollbar-thumb { 123 | background-color: var(--scroll-bar-color); 124 | border-radius: 20px; 125 | border: 3px solid var(--scroll-bar-bg-color); 126 | } -------------------------------------------------------------------------------- /src/popup/premium.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Upgrade to Premium 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
    14 |
    15 | Extension icon 16 | 17 |
    18 |
    19 |
    20 |
    21 | 22 |
    23 |
    24 |
    25 | 26 |
    27 |
    28 |
    29 |
    30 | 31 | 32 | -------------------------------------------------------------------------------- /src/popup/premium.js: -------------------------------------------------------------------------------- 1 | (async function () { 2 | renderDocText(); 3 | 4 | document.body.className = WhichExtension; 5 | const manifest = chrome.runtime.getManifest(); 6 | $('#extension-name').textContent = manifest.name; 7 | $('#title-container img').src = chrome.runtime.getURL(manifest.icons[128]); 8 | 9 | const benefits = { 10 | save: [SVG.filledBookmark, _t('Save your conversation for later')], 11 | chat: [SVG.chat, _t('Chat directly in the result page')], 12 | }; 13 | const benefitsContainer = $("#benefits"); 14 | Object.entries(benefits).forEach(([id, [icon, html]]) => { 15 | if (id === 'bingSearch' && WhichExtension !== 'bingchat') return; 16 | setSvg(el("div", { className: "icon" }, benefitsContainer), icon); 17 | el("div", { className: "description", innerHTML: html }, benefitsContainer); 18 | }); 19 | 20 | el('img', { src: `../images/${WhichExtension}_conversation_example.png` }, $('#example-screenshot')); 21 | const extensionsNames = { 22 | 'optisearch': _t('OptiSearch (for ChatGPT)'), 23 | 'bingchat': _t('bingchatName'), 24 | 'bard': _t('bardName'), 25 | }; 26 | const otherExtensionsContainer = $('#other-extensions'); 27 | Object.entries(extensionsNames).forEach(([id, name]) => { 28 | if (id === WhichExtension) return; 29 | const container = el('div', {}, otherExtensionsContainer); 30 | el("img", { 31 | alt: `${name} icon`, 32 | src: chrome.runtime.getURL(`icons/${id}/icon_128.png`), 33 | }, container); 34 | el("a", { 35 | textContent: name, 36 | href: webstores[id], 37 | }, container); 38 | }); 39 | 40 | const upgradeButton = $("#upgrade-button"); 41 | upgradeButton.addEventListener('click', async () => { 42 | ExtPay('optisearch').openPaymentPage(); 43 | setInterval(window.close, 1000); 44 | }); 45 | 46 | hrefPopUp(); 47 | })(); -------------------------------------------------------------------------------- /src/run.js: -------------------------------------------------------------------------------- 1 | Context.run(); 2 | -------------------------------------------------------------------------------- /src/settings.js: -------------------------------------------------------------------------------- 1 | const Settings = { 2 | Options: { 3 | wideColumn: { 4 | name: "Force large panel width", 5 | default: false, 6 | active: false, 7 | }, 8 | }, 9 | 'AI Assitant': { 10 | premium: { 11 | name: 'Is the user premium', 12 | default: null, 13 | active: false, 14 | }, 15 | directchat: { 16 | name: 'Ask at search', 17 | title: 'Ask the AI assistant as soon as the result page is loaded', 18 | default: true, 19 | }, 20 | }, 21 | }; 22 | 23 | switch(WhichExtension) { 24 | case 'optisearch': 25 | Settings['Sites'] = Sites; 26 | Settings['Options']['maxResults'] = { 27 | name: "Maximum number of result panels", 28 | default: 3, 29 | min: 0, 30 | max: 9, 31 | }; 32 | Settings['AI Assitant'] = { 33 | chatgpt: { 34 | icon: "chatgpt.png", 35 | name: "ChatGPT", 36 | default: true, 37 | slaves: ['directchat'], 38 | }, 39 | ...Settings['AI Assitant'], 40 | } 41 | break; 42 | case 'bard': 43 | Settings['AI Assitant']['googleAccount'] = { 44 | name: "Google account ID", 45 | title: 'Google account number to use with Gemini', 46 | default: 0, 47 | min: 0, 48 | active: false, 49 | }; 50 | break; 51 | } 52 | 53 | const getSettings = () => Settings; 54 | 55 | const SAVE_QUERIES_ENGINE = "save_queries_engine" 56 | const SAVE_OPTIONS_KEY = "save_options_key"; 57 | 58 | function loadEngines() { 59 | return new Promise(resolve => { 60 | chrome.storage.local.get(SAVE_QUERIES_ENGINE, async (storage) => { 61 | resolve(storage[SAVE_QUERIES_ENGINE] ?? await fetch(chrome.runtime.getURL(`./src/engines.json`)).then(res => res.json())); 62 | }); 63 | }); 64 | } 65 | 66 | async function defaultSettings() { 67 | let save = {}; 68 | const settings = await getSettings(); 69 | Object.keys(settings).forEach(category => { 70 | Object.keys(settings[category]).forEach(k => { 71 | save[k] = settings[category][k].default ?? true; 72 | }) 73 | }) 74 | return save; 75 | } 76 | 77 | /** @returns {Promise} user settings saved in local */ 78 | function loadSettings() { 79 | return new Promise(resolve => { 80 | chrome.storage.local.get([SAVE_OPTIONS_KEY], async (storage) => { 81 | resolve({ ...(await defaultSettings()), ...storage[SAVE_OPTIONS_KEY] }); 82 | }); 83 | }) 84 | } 85 | 86 | /** Save user settings */ 87 | function saveSettings(save) { 88 | return new Promise(resolve => { 89 | chrome.storage.local.set({ 90 | [SAVE_OPTIONS_KEY]: save 91 | }, resolve) 92 | }) 93 | } 94 | -------------------------------------------------------------------------------- /src/sites/cplusplus.js: -------------------------------------------------------------------------------- 1 | Sites.cplusplus.msgApi = () => ({}) 2 | 3 | Sites.cplusplus.get = (from, doc) => { 4 | const body = doc.querySelector("body"); 5 | 6 | console.log(body); 7 | 8 | return { 9 | title: doc.title.trim(), 10 | lib: body.querySelector('#I_file')?.innerHTML, 11 | type: body.querySelector('#I_type')?.innerHTML, 12 | proto: body.querySelector('.C_prototype')?.innerHTML 13 | } 14 | } 15 | 16 | Sites.cplusplus.set = msg => ({ 17 | body: el('div', { 18 | innerHTML: `${msg.type} ${msg.lib} ${msg.proto}` 19 | }) 20 | }) -------------------------------------------------------------------------------- /src/sites/genius.js: -------------------------------------------------------------------------------- 1 | Sites.genius.msgApi = () => ({}) 2 | 3 | /** 4 | * 5 | * @param {*} from 6 | * @param {Document} doc 7 | * @returns 8 | */ 9 | Sites.genius.get = (from, doc) => { 10 | const body = doc.querySelector("body"); 11 | 12 | let lyricsDiv = body.querySelector('[class*=Lyrics__Root], .lyrics'); 13 | if (!lyricsDiv) 14 | return; 15 | $$('a', lyricsDiv).forEach(a => a.outerHTML = a.innerHTML); 16 | $$('[class^=LyricsHeader__Container]', lyricsDiv).forEach(h => h.parentNode.removeChild(h)); 17 | 18 | let lyrics = ""; 19 | if (lyricsDiv.classList.contains('lyrics')) 20 | lyrics = lyricsDiv?.innerHTML; 21 | else { 22 | lyrics = $$('[class^=Lyrics__Container]', lyricsDiv) 23 | .map(e => e.innerHTML) 24 | .join('

    '); 25 | lyrics = `

    ${lyrics}

    `; 26 | } 27 | return { 28 | title: doc.title.split('|')[0].trim(), 29 | lyrics, 30 | } 31 | } 32 | 33 | Sites.genius.set = msg => ({ 34 | body: el('div', { className: 'geniusBody', innerHTML: msg.lyrics }) 35 | }) -------------------------------------------------------------------------------- /src/sites/mathworks.js: -------------------------------------------------------------------------------- 1 | Sites.mathworks.msgApi = (_) => ({}); 2 | 3 | /** 4 | * @param {*} from 5 | * @param {Document} doc 6 | * @returns 7 | */ 8 | Sites.mathworks.get = (from, doc) => { 9 | const QUERIES = { 10 | answer: ".answer", 11 | bodyAnswer: ".content", 12 | editions: ".contribution", 13 | time: ".answered-date, .answered-edit-date", 14 | details: ".author_inline", 15 | title: ".question_title h1", 16 | }; 17 | 18 | const body = doc.body; 19 | 20 | const isPointing = from.link.search('#:~:text'); 21 | if (isPointing !== -1) { 22 | from.link = from.link.substring(0, isPointing); 23 | } 24 | 25 | const title = doc.querySelector(QUERIES.title); 26 | if (!title) return; 27 | 28 | const res = { 29 | title: title.textContent, 30 | } 31 | 32 | const answer = body.querySelector(QUERIES.answer); // top answer 33 | 34 | if (!answer) return res; 35 | 36 | res.link = `${from.link}#${answer.id}`; 37 | const bodyAnswer = answer.querySelector(QUERIES.bodyAnswer); 38 | [...bodyAnswer.querySelectorAll('div.CodeBlock')].forEach(codeblock => { 39 | const pre = document.createElement("pre"); 40 | pre.innerHTML = codeblock.innerHTML; 41 | codeblock.parentNode.replaceChild(pre, codeblock); 42 | }); 43 | 44 | res.html = bodyAnswer.innerHTML; 45 | 46 | // editions 47 | const editions = [...answer.querySelectorAll(QUERIES.editions)]; 48 | editions.forEach(e => { 49 | const time = e.querySelector(QUERIES.time); 50 | time.style.display = "inline-block"; 51 | }); 52 | 53 | let time = editions[0].querySelector(QUERIES.time); 54 | if (time) { 55 | time = time.outerHTML; 56 | } 57 | let details = editions[0].querySelector(QUERIES.details); 58 | details.style.display = "inline-block"; 59 | 60 | res.author = { 61 | name: details.outerHTML, 62 | answered: time 63 | } 64 | 65 | if (editions.length > 1) { 66 | let name = editions.at(-1).querySelector(QUERIES.details); 67 | res.editor = { 68 | name: name?.outerHTML ?? res.author.name, 69 | answered: editions.at(-1).querySelector(QUERIES.time).outerHTML 70 | } 71 | } 72 | 73 | return res; 74 | } 75 | 76 | Sites.mathworks.set = (answer) => { 77 | const body = el("div", { className: 'stackbody' }); 78 | 79 | if (!answer.html) { 80 | body.innerHTML = `No answer on this question... If you know the answer, submit it!`; 81 | body.style.margin = '1em 0px'; 82 | return { body }; 83 | } 84 | 85 | body.innerHTML = answer.html; 86 | 87 | const foot = document.createElement("div"); 88 | foot.className = "stackfoot"; 89 | let foothtml = answer.author.name + (answer.author.answered ? ` – ${answer.author.answered}` : ''); 90 | foot.innerHTML = foothtml; 91 | 92 | return { body, foot }; 93 | } -------------------------------------------------------------------------------- /src/sites/mdn.js: -------------------------------------------------------------------------------- 1 | Sites.mdn.msgApi = (link) => { 2 | return { 3 | } 4 | } 5 | Sites.mdn.get = (from, doc) => { 6 | const body = doc.querySelector("body"); 7 | 8 | const article = body.querySelector("article"); 9 | if (!article) { 10 | return; 11 | } 12 | 13 | 14 | const syntaxTitle = article.querySelector("#syntax, #syntaxe"); 15 | const syntax = syntaxTitle && syntaxTitle.nextSibling.querySelector("pre"); 16 | 17 | const summary = Array.from(article.querySelectorAll("p")).find(p => p.textContent != "" && !p.closest('header')); 18 | 19 | const underS = underSummary(summary); 20 | 21 | const title = body.querySelector(".title, h1") 22 | return { 23 | title: title?.textContent ?? "", 24 | summary: (summary?.outerHTML ?? '') + (underS?.outerHTML ?? ''), 25 | syntax: syntax?.outerHTML ?? "", 26 | } 27 | } 28 | 29 | Sites.mdn.set = msg => { 30 | const bodyPanel = document.createElement("div"); 31 | bodyPanel.className = "mdnbody"; 32 | bodyPanel.innerHTML = (msg.summary ? msg.summary : "") + (msg.syntax ? msg.syntax : ""); 33 | 34 | return { body: bodyPanel }; 35 | } -------------------------------------------------------------------------------- /src/sites/results.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | Context.parseResults = () => { 3 | Context.currentPanelIndex = 0; 4 | Context.panels = []; 5 | Context.links = []; 6 | 7 | const results = $$(Context.engine.resultRow); 8 | if (results.length > 0) { 9 | results.forEach(handleResult); 10 | return; 11 | } 12 | 13 | if (Context.engineName === DuckDuckGo) { 14 | const resultsContainer = $(Context.engine.resultsContainer); 15 | const observer = new MutationObserver((mutationRecords) => { 16 | // Handle mutations 17 | mutationRecords 18 | .filter(mr => mr.addedNodes.length > 0) 19 | .map(mr => mr.addedNodes[0]) 20 | .filter(n => n?.matches(Context.engine.resultRow)) 21 | .forEach(handleResult); 22 | }); 23 | 24 | observer.observe(resultsContainer, { childList: true }); 25 | } 26 | 27 | debug("No result detected"); 28 | } 29 | 30 | /** 31 | * Take the result Element and send a request to the site if it is supported 32 | * @param {Element} result the result 33 | */ 34 | async function handleResult(result) { 35 | if (Context.links.length >= Context.save.maxResults) 36 | return; 37 | 38 | let linksInResultContainer = []; 39 | if (Context.engineName === Baidu) { 40 | linksInResultContainer = [result.getAttribute('mu')]; 41 | } else { 42 | linksInResultContainer = $$("a", result).map(a => a.href); 43 | } 44 | 45 | let siteLink = linksInResultContainer.find(l => !l?.startsWith(Context.engine.link) && l !== 'javascript:void(0)'); 46 | let intermediateLink = null; 47 | if (!siteLink && Context.engineName === Bing) { 48 | siteLink = $('cite', result)?.textContent; 49 | intermediateLink = linksInResultContainer[0]; 50 | } 51 | if (!siteLink) 52 | return; 53 | 54 | const find = Object.entries(Sites).find(([_, { link }]) => siteLink.search(link) !== -1); 55 | if (!find) 56 | return; 57 | const [siteName, siteProps] = find; 58 | if (!Context.isActive(siteName)) 59 | return; 60 | 61 | const paramsToSend = { 62 | action: 'fetch-result', 63 | engine: Context.engineName, 64 | link: siteLink, 65 | site: siteName, 66 | type: "html", 67 | credentials: siteProps.credentials, 68 | ...siteProps.msgApi(siteLink), 69 | }; 70 | 71 | if (intermediateLink) { 72 | const html = await bgFetch(intermediateLink); 73 | const start = html.lastIndexOf('"', html.search(siteProps.link)) + 1; 74 | const end = html.indexOf('"', start); 75 | siteLink = html.substring(start, end); 76 | paramsToSend.link = siteLink; 77 | } 78 | 79 | const isSameURL = (a, b) => a.host === b.host && a.pathname === b.pathname && a.search === b.search; 80 | 81 | const urlLink = new URL(siteLink); 82 | if (Context.links.some(l => isSameURL(l, urlLink))) 83 | return; 84 | const panelIndex = Context.links.length; 85 | Context.links.push(new URL(siteLink)); 86 | 87 | chrome.runtime.sendMessage(paramsToSend, async (resp) => { 88 | if (!resp) 89 | return; 90 | const [msg, text] = resp; 91 | const site = Sites[msg.site]; 92 | if (!site) 93 | return; 94 | 95 | let doc; 96 | switch (msg.type) { 97 | case 'html': doc = new DOMParser().parseFromString(text, "text/html"); break; 98 | case 'json': doc = JSON.parse(text); break; 99 | default: return; 100 | } 101 | 102 | const siteData = { 103 | icon: $('[rel="shortcut icon"]', doc)?.href ?? chrome.runtime.getURL(`src/images/${site.icon}`), 104 | ...msg, 105 | ...(await site.get(msg, doc)) 106 | }; 107 | const content = site.set(siteData); // set body and foot 108 | 109 | if (content && content.body.innerHTML && siteData.title !== undefined) 110 | Context.panels[panelIndex] = panelFromSite({ ...siteData, ...content }); 111 | else 112 | Context.panels[panelIndex] = null; 113 | 114 | updatePanels(); 115 | }); 116 | } 117 | 118 | function panelFromSite({ site, title, link, icon, header, body, foot }) { 119 | const panel = el("div", { className: "optipanel" }); 120 | const headPanel = el("div", { className: "optiheader" }, panel); 121 | 122 | const a = el("a", { href: link, className: "result-link" }, headPanel); 123 | 124 | toTeX(el("div", { className: "title result-title", textContent: title }, a)); 125 | 126 | const linkElement = el("cite", { className: "optilink result-url" }, a); 127 | el("img", { width: 16, height: 16, src: icon }, linkElement); 128 | el("span", { textContent: link }, linkElement); 129 | 130 | if (body) 131 | hline(panel); 132 | 133 | const content = el('div', { className: "opticontent" }, panel); 134 | 135 | // HEADER 136 | if (header) { 137 | content.append(header); 138 | hline(content); 139 | } 140 | // BODY 141 | if (body) { 142 | body.classList.add("optibody"); 143 | 144 | if (site === "stackexchange") { 145 | toTeX(body); 146 | } 147 | 148 | prettifyCode(body); 149 | content.append(body); 150 | } 151 | 152 | // FOOT 153 | if (foot) { 154 | foot.classList.add("optifoot") 155 | foot.id = "output"; 156 | hline(content); 157 | content.append(foot); 158 | } 159 | 160 | writeHostOnLinks(link, panel); 161 | 162 | return panel; 163 | } 164 | 165 | /** 166 | * Draw the panels in order. Only when the previous are not undefined 167 | */ 168 | function updatePanels() { 169 | while (Context.currentPanelIndex < Context.links.length) { 170 | const panel = Context.panels[Context.currentPanelIndex]; 171 | if (panel === undefined) 172 | return; 173 | if (panel !== null) 174 | Context.appendPanel(panel); 175 | 176 | Context.currentPanelIndex++; 177 | } 178 | } 179 | 180 | })(); -------------------------------------------------------------------------------- /src/sites/sites.js: -------------------------------------------------------------------------------- 1 | const Sites = Object.freeze({ 2 | wikipedia: { 3 | name: "Wikipedia", 4 | link: "wikipedia.org/wiki/", 5 | icon: "wikipedia.ico", 6 | href: "https://en.wikipedia.org/", 7 | }, 8 | stackexchange: { 9 | name: "Stack Exchange sites", 10 | link: /((((stackexchange)|(stackoverflow)|(serverfault)|(superuser)|(askubuntu)|(stackapps))\.com)|(mathoverflow\.net))\/((questions)|q)\//, 11 | title: "Includes Stack Overflow, Super User and many others", 12 | icon: "stackexchange.ico", 13 | href: "https://stackexchange.com/sites", 14 | }, 15 | w3schools: { 16 | name: "W3Schools", 17 | link: "https://www.w3schools.com/", 18 | icon: "w3schools.ico", 19 | href: "https://www.w3schools.com/", 20 | }, 21 | mdn: { 22 | name: "MDN Web Docs", 23 | link: "https://developer.mozilla.org/", 24 | icon: "mdn.png", 25 | href: "https://developer.mozilla.org/", 26 | }, 27 | genius: { 28 | name: "Genius", 29 | link: /https:\/\/genius\.com\/[^\/]*$/, 30 | icon: "genius.png", 31 | href: "https://genius.com/", 32 | }, 33 | unity: { 34 | name: "Unity Answers", 35 | link: /https:\/\/answers\.unity\.com\/((questions)|q)\//, 36 | icon: "unity.ico", 37 | href: "https://answers.unity.com/", 38 | }, 39 | mathworks: { 40 | name: "MATLAB Answers", 41 | link: "https://www.mathworks.com/matlabcentral/answers/", 42 | icon: "mathworks.ico", 43 | href: "https://www.mathworks.com/matlabcentral/answers/", 44 | }, 45 | }); -------------------------------------------------------------------------------- /src/sites/stackoverflow.js: -------------------------------------------------------------------------------- 1 | Sites.stackexchange.msgApi = (_) => ({}); 2 | Sites.stackexchange.credentials = "include"; 3 | 4 | /** 5 | * @param {*} from 6 | * @param {Document} doc 7 | * @returns 8 | */ 9 | Sites.stackexchange.get = (from, doc) => { 10 | const QUERIES = { 11 | "acceptedAnswer": ".accepted-answer", 12 | "answer": ".answer", 13 | "bodyAnswer": ".js-post-body", 14 | "editions": ".user-info", 15 | "time": ".user-action-time", 16 | "details": ".user-details", 17 | "title": "#question-header h1", 18 | "attributeAnswerId": "data-answerid" 19 | } 20 | 21 | const body = doc.body; 22 | // link 23 | const isPointing = from.link.search('#:~:text'); 24 | if (isPointing !== -1) { 25 | from.link = from.link.substring(0, isPointing); 26 | } 27 | 28 | const title = doc.querySelector(QUERIES.title); 29 | if (!title) { 30 | if (!body.innerHTML.includes("_cf_chl_opt")) return; 31 | 32 | // this is a Cloudflare challenge page 33 | return { 34 | isCloudflareChallenge: true, 35 | title: _t("Robot check ✓"), 36 | }; 37 | } 38 | 39 | const res = { 40 | title: title.textContent, 41 | } 42 | 43 | const acceptedAnswer = body.querySelector(QUERIES.answer); // always answer with most upvotes 44 | 45 | if (!acceptedAnswer) { 46 | return res; 47 | } 48 | 49 | res.link = `${from.link}#${acceptedAnswer.getAttribute(QUERIES.attributeAnswerId)}`; 50 | 51 | // body 52 | const bodyAnswer = acceptedAnswer.querySelector(QUERIES.bodyAnswer); 53 | Array.from(bodyAnswer.querySelectorAll('.snippet')) 54 | .forEach(s => { 55 | if (s.previousElementSibling.outerHTML === "

    ") 56 | s.previousElementSibling.remove(); 57 | if (s.nextElementSibling.outerHTML === "

    ") 58 | s.nextElementSibling.remove(); 59 | 60 | s.classList.remove("snippet"); 61 | // s.className = "code-snippet"; 62 | 63 | // el("a", {textContent: "Try it Yourself »"}, s); 64 | }) 65 | res.html = acceptedAnswer.querySelector(QUERIES.bodyAnswer).innerHTML; 66 | 67 | // editions 68 | const editions = acceptedAnswer.querySelectorAll(QUERIES.editions); 69 | editions.forEach(e => { 70 | const time = e.querySelector(QUERIES.time); 71 | if (time) 72 | time.style.display = "inline-block"; 73 | }); 74 | 75 | let time = editions[editions.length - 1].querySelector(QUERIES.time); 76 | if (time) { 77 | time = time.outerHTML; 78 | } 79 | let details = editions[editions.length - 1].querySelector(QUERIES.details); 80 | const a = details.querySelector("a") 81 | if (a) { 82 | details = a; 83 | } 84 | details.style.display = "inline-block"; 85 | 86 | res.author = { 87 | name: details.outerHTML, 88 | answered: time 89 | } 90 | 91 | if (editions.length > 1) { 92 | let name = editions[0].querySelector(QUERIES.details).querySelector("a"); 93 | res.editor = { 94 | name: name?.outerHTML ?? res.author.name, 95 | answered: editions[0].querySelector(QUERIES.time).outerHTML 96 | } 97 | } 98 | 99 | return res; 100 | } 101 | 102 | Sites.stackexchange.set = (answer) => { 103 | const body = el("div", { className: 'stackbody' }); 104 | 105 | if (answer.isCloudflareChallenge) { 106 | body.innerHTML = ` 107 |

    111 |

    115 | `; 116 | renderDocText(body); 117 | 118 | $("a", body).addEventListener("click", (e) => { 119 | e.preventDefault(); 120 | window.open(answer.link, 'newwindow', 'width=800,height=800,focused=true'); 121 | return false; 122 | }); 123 | return { body }; 124 | } 125 | 126 | if (!answer.html) { 127 | body.innerHTML = ` 128 |

    132 | `; 133 | renderDocText(body); 134 | return { body }; 135 | } 136 | 137 | body.innerHTML = answer.html; 138 | 139 | const foot = document.createElement("div"); 140 | foot.className = "stackfoot"; 141 | let foothtml = answer.author.name + (answer.author.answered ? ` – ${answer.author.answered}` : ''); 142 | foot.innerHTML = foothtml; 143 | 144 | return { body, foot }; 145 | } -------------------------------------------------------------------------------- /src/sites/unity.js: -------------------------------------------------------------------------------- 1 | Sites.unity.msgApi = (_) => ({}); 2 | 3 | /** 4 | * @param {*} from 5 | * @param {Document} doc 6 | * @returns 7 | */ 8 | Sites.unity.get = (from, doc) => { 9 | const QUERIES = { 10 | title: ".question-title", 11 | answer: ".answer", 12 | bodyAnswer: ".answer-body", 13 | author: ".author-info", 14 | time: ".post-info", 15 | } 16 | 17 | const body = doc.body; 18 | // link 19 | const isPointing = from.link.search('#:~:text'); 20 | if (isPointing !== -1) { 21 | from.link = from.link.substring(0, isPointing); 22 | } 23 | 24 | const res = { 25 | title: doc.querySelector(QUERIES.title).textContent, 26 | } 27 | 28 | const acceptedAnswer = body.querySelector(QUERIES.answer); 29 | 30 | if (!acceptedAnswer) { 31 | return res; 32 | } 33 | 34 | res.link = from.link; 35 | 36 | // body 37 | const bodyAnswer = acceptedAnswer.querySelector(QUERIES.bodyAnswer); 38 | bodyAnswer.childNodes.forEach(c => { 39 | if (c.outerHTML === "

    ") 40 | c.remove(); 41 | }) 42 | res.html = bodyAnswer.innerHTML; 43 | 44 | // author 45 | res.author = acceptedAnswer.querySelector(QUERIES.author).outerHTML; 46 | return res; 47 | } 48 | 49 | Sites.unity.set = (answer) => { 50 | const body = el("div", { className: 'stackbody' }); 51 | 52 | if (!answer.html) { 53 | body.innerHTML = `No answer on this question... If you know the answer, submit it!`; 54 | body.style.margin = '1em 0px'; 55 | return { body }; 56 | } 57 | 58 | body.innerHTML = answer.html; 59 | 60 | const foot = document.createElement("div"); 61 | foot.className = "stackfoot"; 62 | const foothtml = answer.author; 63 | foot.innerHTML = foothtml; 64 | 65 | return { body, foot }; 66 | } 67 | -------------------------------------------------------------------------------- /src/sites/w3schools.js: -------------------------------------------------------------------------------- 1 | Sites.w3schools.msgApi = (link) => { 2 | return { 3 | } 4 | } 5 | 6 | Sites.w3schools.get = (from, doc) => { 7 | const body = doc.querySelector("body"); 8 | 9 | const article = body.querySelector("#main"); 10 | if (!article) { 11 | return; 12 | } 13 | const children = article.children; 14 | 15 | const replaceBr = (ih) => ih.replace(/
    /g, "\n") 16 | 17 | let summary = "", syntax = ""; 18 | for (let i = 0; i < children.length; i++) { 19 | const c = children[i]; 20 | if (c.tagName == "H2") { 21 | if (c.textContent == "Definition and Usage") { 22 | for (let k = i + 1; k < children.length; k++) { 23 | const p = children[k]; 24 | if (p.tagName != "P") 25 | break; 26 | summary += p.outerHTML; 27 | } 28 | } 29 | else if (c.textContent == "Syntax") { 30 | syntax = "
    " + replaceBr(c.nextElementSibling.innerHTML.replace(/\n/g, "").trim()) + "
    "; 31 | } 32 | } 33 | } 34 | 35 | 36 | const example = article.querySelector(".w3-example"); 37 | 38 | if (example) { 39 | if (!example.querySelector(".w3-code")) 40 | return; 41 | const location = from.link.substring(0, from.link.lastIndexOf("/")); 42 | 43 | Array.from(example.querySelectorAll("img")).forEach(img => { 44 | if (!img.src.startsWith("https://")) { 45 | img.src = `${location}/${new URL(img.src).pathname}`; 46 | } 47 | }); 48 | 49 | const codes = example.querySelectorAll(".w3-code"); 50 | codes?.forEach(code => { 51 | const str = replaceBr(code.innerHTML.replace(/\n|\t/g, "").trim()) 52 | .split('\n') 53 | .map(line => line.trim()) 54 | .join('\n'); 55 | 56 | code.outerHTML = `
    ${str}
    `; 57 | }) 58 | } 59 | 60 | 61 | const title = article.querySelector("h1") 62 | return { 63 | title: title?.textContent ?? "", 64 | summary: summary, 65 | syntax: syntax, 66 | example: example?.outerHTML ?? "", 67 | } 68 | } 69 | 70 | 71 | Sites.w3schools.set = function setW3(msg) { 72 | const bodyPanel = document.createElement("div") 73 | bodyPanel.className = "w3body" 74 | bodyPanel.innerHTML = `${msg.summary}${msg.syntax}${msg.example}` 75 | 76 | return { body: bodyPanel }; 77 | } -------------------------------------------------------------------------------- /src/sites/wikipedia.js: -------------------------------------------------------------------------------- 1 | Sites.wikipedia.msgApi = (link) => { 2 | const url = new URL(link); 3 | const pageName = url.pathname.match(/\/([^\/]+$)/)[1] 4 | return { 5 | // api: `${url.origin}/w/api.php?action=parse&format=json&prop=text&formatversion=2&page=${pageName}`, 6 | // type: 'json' 7 | } 8 | } 9 | 10 | /** 11 | * 12 | * @param {*} from 13 | * @param {Document} doc 14 | * @returns 15 | */ 16 | Sites.wikipedia.get = async (from, doc) => { 17 | const body = doc.body; 18 | const article = body.querySelector("#mw-content-text .mw-parser-output"); 19 | const infobox = article.querySelector("[class^=infobox]"); 20 | 21 | let img; 22 | if (infobox) 23 | img = infobox.querySelector(".images > .image"); 24 | if (!img) 25 | img = article.querySelector(".thumbinner .image"); 26 | if (img) 27 | img.className = "imgwiki"; 28 | 29 | const children = [...article.querySelectorAll(":scope > p")]; 30 | const summary = children.find(c => !c.className && c.textContent.trim() != ""); 31 | 32 | if (img && !onChrome()) { 33 | const actualImg = img.querySelector('img'); 34 | actualImg.src = await srcToBase64(actualImg.src); 35 | } 36 | 37 | const title = body.querySelector("#firstHeading") 38 | return { 39 | title: title ? title.textContent : "", 40 | summary: (summary?.outerHTML ?? '') + (underSummary(summary)?.outerHTML ?? ''), 41 | img: img?.outerHTML, 42 | } 43 | } 44 | 45 | Sites.wikipedia.set = msg => { 46 | return { 47 | body: el("div", { 48 | className: 'wikibody', 49 | innerHTML: (msg.img ?? "") + (msg.summary ?? ""), 50 | }) 51 | }; 52 | } -------------------------------------------------------------------------------- /src/styles/box.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --line-dark: #34363e; 3 | --expand-time: 0.5s; 4 | --opti-max-width: 600px; 5 | } 6 | 7 | .optisearchbox { 8 | margin: 20px 0 20px 0; 9 | border-radius: 8px; 10 | min-width: 250px; 11 | max-width: var(--opti-max-width); 12 | box-shadow: rgb(0 0 0 / 15%) 0px 0px 2px 0px, rgb(0 0 0 / 15%) 1px 3px 8px 0px; 13 | overflow: hidden; 14 | transition: min-width var(--expand-time) linear; 15 | } 16 | 17 | [data-optisearch-column=wide] .optisearchbox { 18 | min-width: var(--opti-max-width); 19 | } -------------------------------------------------------------------------------- /src/styles/chatgpt.css: -------------------------------------------------------------------------------- 1 | .optichat .optiheader { 2 | margin-top: 10px; 3 | margin-bottom: 5px; 4 | display: flex; 5 | } 6 | 7 | .optichat .title { 8 | font-size: 1.3em; 9 | } 10 | 11 | .chatgpt-button { 12 | display: block; 13 | width: 100%; 14 | padding: 8px 10px; 15 | margin: 0.5em 0; 16 | border-radius: 5px; 17 | border: 1px solid; 18 | font-size: large; 19 | font-weight: 700; 20 | cursor: pointer; 21 | text-align: center; 22 | transition: 0.3s; 23 | } 24 | 25 | .chatgpt-button[name=google-account] { 26 | font-size: medium; 27 | text-align: left; 28 | } 29 | 30 | 31 | .optichat .optifoot a.source { 32 | position: relative; 33 | white-space: nowrap; 34 | padding: 3px 5px; 35 | border-radius: 3px; 36 | margin: 2px 1px; 37 | line-height: 1.8em; 38 | } 39 | .optichat a.source.superscript { 40 | font-size: smaller; 41 | bottom: 0.5em; 42 | padding: 1px 4px; 43 | position: relative; 44 | border-radius: 3px; 45 | } 46 | .chat-title { 47 | bottom: 10px; 48 | position: relative; 49 | margin: 0px 5px; 50 | cursor: pointer; 51 | } 52 | 53 | a.chat-title { 54 | color: inherit !important; 55 | } 56 | 57 | .chat-title:hover { 58 | text-decoration: underline; 59 | } 60 | 61 | .chat-info { 62 | margin-top: 1em; 63 | margin-bottom: 1em; 64 | } 65 | 66 | .ai-name { 67 | display: inline-block; 68 | position: relative; 69 | user-select: none; 70 | white-space: nowrap; 71 | } 72 | 73 | .learnmore > a[data-invisible-count="0"] { 74 | display: none; 75 | } 76 | .learnmore > .showmore { 77 | cursor: pointer; 78 | } 79 | .learnmore.less > a[more] { 80 | display: none; 81 | } 82 | 83 | /*---------------Discussion------------------*/ 84 | .response-container .input-container, 85 | .response-container .box-message-container.user{ 86 | display: none; 87 | } 88 | .response-container .message-container { 89 | margin-block-start: 1em; 90 | margin-block-end: 1em; 91 | } 92 | .chat-container *::-webkit-scrollbar { 93 | width: 5px; 94 | height: 5px; 95 | } 96 | .chat-container *::-webkit-scrollbar-thumb { 97 | background-color: #686868; 98 | border-radius: 20px; 99 | } 100 | .chat-container .discussion-container { 101 | max-height: 400px; 102 | padding: 0 5px; 103 | overflow-y: auto; 104 | overflow-x: clip; 105 | } 106 | .chat-container .box-message-container { 107 | display: flex; 108 | margin: 0.5em 0; 109 | } 110 | .chat-container .box-message-container.user{ 111 | justify-content: flex-end; 112 | } 113 | .chat-container .message-container { 114 | border-radius: 8px 8px 8px 0; 115 | padding: 0.4em 0.5em; 116 | max-width: -webkit-fill-available; 117 | } 118 | .chat-container .user > .message-container { 119 | border-radius: 8px 8px 0 8px; 120 | } 121 | .chat-container .message-container p, 122 | .chat-container .message-container ul, 123 | .chat-container .message-container ol { 124 | margin-block-start: 0em !important; 125 | } 126 | .chat-container .message-container .chat-info { 127 | margin: 0; 128 | } 129 | .chat-container .message-container pre.prettyprint{ 130 | max-height: unset; 131 | } 132 | .chat-container .input-container { 133 | padding: 8px 12px; 134 | border: solid 1px; 135 | border-radius: 8px; 136 | } 137 | .chat-container textarea { 138 | width: -webkit-fill-available; 139 | resize: none; 140 | border: none; 141 | font-family: inherit; 142 | } 143 | .chat-container .info-container { 144 | display: flex; 145 | justify-content: space-between; 146 | font-size: 12px; 147 | } 148 | .chat-container .send-button { 149 | cursor: pointer; 150 | width: 1rem; 151 | height: 1rem; 152 | } 153 | .chat-container .send-button[disabled] { 154 | fill-opacity: 0.4; 155 | cursor: not-allowed; 156 | } 157 | 158 | .bard-image-link { 159 | display: inline; 160 | } 161 | .bard-image-link img { 162 | display: block; 163 | margin: 1em 0; 164 | border-radius: 5px; 165 | box-shadow: rgb(0 0 0 / 25%) 1px 1px 5px 0; 166 | } 167 | 168 | 169 | 170 | 171 | /* COLOR THEMES */ 172 | 173 | .bright .chatgpt-button { 174 | color: #222222; 175 | border-color: #dcdcdc; 176 | background: #f7f7f7; 177 | } 178 | 179 | .dark .chatgpt-button { 180 | color: #dcdcdc; 181 | border-color: #222222; 182 | background-color: #3b3b3b; 183 | } 184 | 185 | .bright .chatgpt-button:active { 186 | border-color: #bbc0c4; 187 | } 188 | 189 | .dark .chatgpt-button:active { 190 | border-color: #979797; 191 | } 192 | 193 | .bright .chatgpt-button:hover { 194 | color: black; 195 | } 196 | 197 | .dark .chatgpt-button:hover { 198 | color: white; 199 | } 200 | 201 | .bright.optichat a.source { 202 | color: #123bb6; 203 | background-color: #d1dbfa; 204 | } 205 | .dark.optichat a.source { 206 | background-color: #293740; 207 | } 208 | 209 | .bright .chat-container .message-container { 210 | color: #222222; 211 | background: #f7f7f7; 212 | } 213 | .dark .chat-container .message-container { 214 | color: #dcdcdc; 215 | background-color: #3b3b3b; 216 | } 217 | .bright .chat-container .user > .message-container{ 218 | color: white; 219 | background-color: #0051ff; 220 | } 221 | .dark .chat-container .user > .message-container { 222 | background-color: #0068eb; 223 | } 224 | 225 | .bright .chat-container pre, 226 | .bright .chat-container code { 227 | background-color: white; 228 | } 229 | 230 | .bright .chat-container .input-container { 231 | /* background-color: #f7f7f7; */ 232 | border-color: #d9d9d9; 233 | } 234 | .dark .chat-container .input-container { 235 | /* background-color: #3b3b3b; */ 236 | border-color: #969696; 237 | } 238 | .chat-container textarea { 239 | background-color: inherit; 240 | } 241 | .chat-container textarea:focus { 242 | outline: none; 243 | border: none; 244 | } 245 | 246 | .bright .message-container pre.prettyprint{ 247 | background-color: #eeeeee; 248 | } 249 | 250 | .bright select { 251 | border-color: #ccc; 252 | } 253 | .dark select { 254 | border-color: #333; 255 | } -------------------------------------------------------------------------------- /src/styles/code-dark-theme.css: -------------------------------------------------------------------------------- 1 | /*! 2 | Theme: StackOverflow Dark 3 | Description: Dark theme as used on stackoverflow.com 4 | Author: stackoverflow.com 5 | Maintainer: @Hirse 6 | Website: https://github.com/StackExchange/Stacks 7 | License: MIT 8 | Updated: 2021-05-15 9 | 10 | Updated for @stackoverflow/stacks v0.64.0 11 | Code Blocks: /blob/v0.64.0/lib/css/components/_stacks-code-blocks.less 12 | Colors: /blob/v0.64.0/lib/css/exports/_stacks-constants-colors.less 13 | */ 14 | 15 | .dark .hljs { 16 | /* var(--highlight-color) */ 17 | color: #ffffff; 18 | /* var(--highlight-bg) */ 19 | /* background: #1c1b1b; */ 20 | } 21 | 22 | .dark .hljs-subst { 23 | /* var(--highlight-color) */ 24 | color: #ffffff; 25 | } 26 | 27 | .dark .hljs-comment { 28 | /* var(--highlight-comment) */ 29 | color: #999999; 30 | } 31 | 32 | .dark .hljs-keyword, 33 | .dark .hljs-selector-tag, 34 | .dark .hljs-meta, 35 | .dark .hljs-keyword, 36 | .dark .hljs-doctag, 37 | .dark .hljs-section { 38 | /* var(--highlight-keyword) */ 39 | color: #88aece; 40 | } 41 | 42 | .dark .hljs-attr { 43 | /* var(--highlight-attribute); */ 44 | color: #88aece; 45 | } 46 | 47 | .dark .hljs-attribute { 48 | /* var(--highlight-symbol) */ 49 | color: #c59bc1; 50 | } 51 | 52 | .dark .hljs-name, 53 | .dark .hljs-type, 54 | .dark .hljs-number, 55 | .dark .hljs-selector-id, 56 | .dark .hljs-quote, 57 | .dark .hljs-template-tag { 58 | /* var(--highlight-namespace) */ 59 | color: #f08d49; 60 | } 61 | 62 | .dark .hljs-selector-class { 63 | /* var(--highlight-keyword) */ 64 | color: #88aece; 65 | } 66 | 67 | .dark .hljs-string, 68 | .dark .hljs-regexp, 69 | .dark .hljs-symbol, 70 | .dark .hljs-variable, 71 | .dark .hljs-template-variable, 72 | .dark .hljs-link, 73 | .dark .hljs-selector-attr { 74 | /* var(--highlight-variable) */ 75 | color: #b5bd68; 76 | } 77 | 78 | .dark .hljs-meta, 79 | .dark .hljs-selector-pseudo { 80 | /* var(--highlight-keyword) */ 81 | color: #88aece; 82 | } 83 | 84 | .dark .hljs-built_in, 85 | .dark .hljs-title, 86 | .dark .hljs-literal { 87 | /* var(--highlight-literal) */ 88 | color: #f08d49; 89 | } 90 | 91 | .dark .hljs-bullet, 92 | .dark .hljs-code { 93 | /* var(--highlight-punctuation) */ 94 | color: #cccccc; 95 | } 96 | 97 | .dark .hljs-meta, 98 | .dark .hljs-string { 99 | /* var(--highlight-variable) */ 100 | color: #b5bd68; 101 | } 102 | 103 | .dark .hljs-deletion { 104 | /* var(--highlight-deletion) */ 105 | color: #de7176; 106 | } 107 | 108 | .dark .hljs-addition { 109 | /* var(--highlight-addition) */ 110 | color: #76c490; 111 | } 112 | 113 | .dark .hljs-emphasis { 114 | font-style: italic; 115 | } 116 | 117 | .dark .hljs-strong { 118 | font-weight: bold; 119 | } 120 | 121 | /* purposely ignored 122 | .dark .hljs-formula, 123 | .dark .hljs-operator, 124 | .dark .hljs-params, 125 | .dark .hljs-property, 126 | .dark .hljs-punctuation, 127 | .dark .hljs-tag {} 128 | */ 129 | -------------------------------------------------------------------------------- /src/styles/code-light-theme.css: -------------------------------------------------------------------------------- 1 | /*! 2 | Theme: StackOverflow Light 3 | Description: Light theme as used on stackoverflow.com 4 | Author: stackoverflow.com 5 | Maintainer: @Hirse 6 | Website: https://github.com/StackExchange/Stacks 7 | License: MIT 8 | Updated: 2021-05-15 9 | 10 | Updated for @stackoverflow/stacks v0.64.0 11 | Code Blocks: /blob/v0.64.0/lib/css/components/_stacks-code-blocks.less 12 | Colors: /blob/v0.64.0/lib/css/exports/_stacks-constants-colors.less 13 | */ 14 | 15 | .bright .hljs { 16 | /* var(--highlight-color) */ 17 | color: #2f3337; 18 | /* var(--highlight-bg) */ 19 | /* background: #f6f6f6; */ 20 | } 21 | 22 | .bright .hljs-subst { 23 | /* var(--highlight-color) */ 24 | color: #2f3337; 25 | } 26 | 27 | .bright .hljs-comment { 28 | /* var(--highlight-comment) */ 29 | color: #656e77; 30 | } 31 | 32 | .bright .hljs-keyword, 33 | .bright .hljs-selector-tag, 34 | .bright .hljs-meta, 35 | .bright .hljs-keyword, 36 | .bright .hljs-doctag, 37 | .bright .hljs-section { 38 | /* var(--highlight-keyword) */ 39 | color: #015692; 40 | } 41 | 42 | .bright .hljs-attr { 43 | /* var(--highlight-attribute); */ 44 | color: #015692; 45 | } 46 | 47 | .bright .hljs-attribute { 48 | /* var(--highlight-symbol) */ 49 | color: #803378; 50 | } 51 | 52 | .bright .hljs-name, 53 | .bright .hljs-type, 54 | .bright .hljs-number, 55 | .bright .hljs-selector-id, 56 | .bright .hljs-quote, 57 | .bright .hljs-template-tag { 58 | /* var(--highlight-namespace) */ 59 | color: #b75501; 60 | } 61 | 62 | .bright .hljs-selector-class { 63 | /* var(--highlight-keyword) */ 64 | color: #015692; 65 | } 66 | 67 | .bright .hljs-string, 68 | .bright .hljs-regexp, 69 | .bright .hljs-symbol, 70 | .bright .hljs-variable, 71 | .bright .hljs-template-variable, 72 | .bright .hljs-link, 73 | .bright .hljs-selector-attr { 74 | /* var(--highlight-variable) */ 75 | color: #54790d; 76 | } 77 | 78 | .bright .hljs-meta, 79 | .bright .hljs-selector-pseudo { 80 | /* var(--highlight-keyword) */ 81 | color: #015692; 82 | } 83 | 84 | .bright .hljs-built_in, 85 | .bright .hljs-title, 86 | .bright .hljs-literal { 87 | /* var(--highlight-literal) */ 88 | color: #b75501; 89 | } 90 | 91 | .bright .hljs-bullet, 92 | .bright .hljs-code { 93 | /* var(--highlight-punctuation) */ 94 | color: #535a60; 95 | } 96 | 97 | .bright .hljs-meta, 98 | .bright .hljs-string { 99 | /* var(--highlight-variable) */ 100 | color: #54790d; 101 | } 102 | 103 | .bright .hljs-deletion { 104 | /* var(--highlight-deletion) */ 105 | color: #c02d2e; 106 | } 107 | 108 | .bright .hljs-addition { 109 | /* var(--highlight-addition) */ 110 | color: #2f6f44; 111 | } 112 | 113 | .bright .hljs-emphasis { 114 | font-style: italic; 115 | } 116 | 117 | .bright .hljs-strong { 118 | font-weight: bold; 119 | } 120 | 121 | /* purposely ignored 122 | .bright .hljs-formula, 123 | .bright .hljs-operator, 124 | .bright .hljs-params, 125 | .bright .hljs-property, 126 | .bright .hljs-punctuation, 127 | .bright .hljs-tag {} 128 | */ 129 | -------------------------------------------------------------------------------- /src/styles/genius.css: -------------------------------------------------------------------------------- 1 | .optibody.geniusBody { 2 | max-height: 400px; 3 | } 4 | -------------------------------------------------------------------------------- /src/styles/panel.css: -------------------------------------------------------------------------------- 1 | .mobile { 2 | width: 100%; 3 | max-width: unset; 4 | margin: 0; 5 | border-radius: 0; 6 | } 7 | 8 | .bright { 9 | color: #202124; 10 | background-color: #fff; 11 | } 12 | .dark { 13 | color: rgb(215, 210, 204); 14 | color-scheme: dark; 15 | } 16 | 17 | .unfold_button { 18 | width: 100%; 19 | text-align: left; 20 | height: 2rem; 21 | border: 1px rgb(220, 220, 220); 22 | cursor: pointer; 23 | position: relative; 24 | box-shadow: rgb(255 255 255) 0px -15px 20px -3px; 25 | padding: 10px 0px 0px 10px; 26 | font-size: smaller; 27 | } 28 | .dark .unfold_button { 29 | box-shadow: rgb(0, 0, 0) 0px -15px 20px -3px; 30 | } 31 | .unfold_button:active { 32 | background-color: rgb(177, 177, 177); 33 | } 34 | 35 | .optipanel { 36 | border-radius: 8px; 37 | padding: 8px 20px; 38 | font-size: 15px; 39 | position: relative; 40 | transition: max-height 5s; 41 | /* overflow-y: auto; */ 42 | max-height: initial; 43 | line-height: 1.5em; 44 | } 45 | 46 | .folded { 47 | overflow-y: hidden; 48 | max-height: 400px; 49 | } 50 | 51 | .optiheader .headerhover { 52 | opacity: 0; 53 | transition: opacity 0.2s; 54 | } 55 | .optiheader:hover .headerhover { 56 | opacity: 1; 57 | } 58 | .optiheader .watermark { 59 | opacity: 1; 60 | transition: opacity 0.2s; 61 | z-index: 1; 62 | } 63 | .optiheader:hover .watermark { 64 | opacity: 0; 65 | } 66 | 67 | .optiheader .watermark, 68 | .optiheader .top-buttons-container { 69 | position: absolute; 70 | right: 0; 71 | margin: 0px 15px; 72 | color: #999999; 73 | font-size: 10px; 74 | top: 4px; 75 | } 76 | .optiheader .top-buttons-container { 77 | z-index: 2; 78 | } 79 | .optiheader .top-buttons-container > * { 80 | margin-left: 5px; 81 | cursor: pointer; 82 | display: inline-block; 83 | user-select: none; 84 | position: relative; 85 | transition: color 0.3s, text-shadow 0.3s; 86 | color: transparent; 87 | text-shadow: 0 0 0 #bdbdbd; 88 | } 89 | .optiheader .top-buttons-container a { 90 | text-decoration: none; 91 | color: inherit !important; 92 | } 93 | .optiheader .top-buttons-container a:hover { 94 | text-decoration: none; 95 | } 96 | .optiheader .thumb:hover { 97 | text-shadow: 0 0 0 #31a1fd; 98 | } 99 | .optiheader .star:hover { 100 | text-shadow: 0 0 0 #ffe03e; 101 | } 102 | .optiheader .heart:hover { 103 | text-shadow: 0 0 0 #ff4949; 104 | } 105 | 106 | .optiheader .left-buttons-container, 107 | .optiheader .right-buttons-container { 108 | display: block; 109 | min-width: fit-content; 110 | } 111 | 112 | .optiheader .left-buttons-container > *, 113 | .optiheader .right-buttons-container > * { 114 | margin-left: 5px; 115 | display: inline-block; 116 | width: 21px; 117 | height: 21px; 118 | } 119 | 120 | .svg-container { 121 | fill: #bdbdbd; 122 | stroke: #bdbdbd; 123 | stroke-width: 0; 124 | transition: fill 0.3s, stroke 0.3s; 125 | cursor: pointer; 126 | } 127 | 128 | .bright .svg-container:hover:not([disabled]) { 129 | fill: #616161; 130 | stroke: #616161; 131 | } 132 | .dark .svg-container:hover:not([disabled]) { 133 | fill: white; 134 | stroke: white; 135 | } 136 | 137 | .svg-container[disabled] { 138 | fill-opacity: 0.4; 139 | cursor: not-allowed; 140 | } 141 | 142 | .optiheader .left-buttons-container { 143 | position: relative; 144 | margin-left: 5px; 145 | top: 5px; 146 | } 147 | .optiheader .right-buttons-container { 148 | position: absolute; 149 | right: 0; 150 | top: 28px; 151 | margin-right: 10px; 152 | } 153 | 154 | .optiheader .expand-arrow { 155 | transition: transform var(--expand-time) ease-in-out, color 0.3s, text-shadow 0.3s; 156 | transform-origin: center; 157 | } 158 | .optiheader .expand-arrow.rotated { 159 | transform: rotate(-180deg); 160 | } 161 | 162 | .optiheader { 163 | margin-top: 15px; 164 | margin-bottom: 15px; 165 | display: grid; 166 | grid-auto-columns: 75% 1fr; 167 | } 168 | 169 | .title { 170 | font-size: large; 171 | font-weight: bold; 172 | line-height: initial; 173 | word-wrap: break-word; 174 | } 175 | .bright .title.result-title { 176 | color: #1a0dab; 177 | } 178 | 179 | .optiheader a.result-link:hover > .result-title { 180 | text-decoration: underline; 181 | } 182 | .optiheader a.result-link:link { 183 | text-decoration: none !important; 184 | } 185 | 186 | .optilink { 187 | display: block; 188 | white-space: nowrap; 189 | overflow: hidden; 190 | text-overflow: ellipsis; 191 | max-width: 400px; 192 | padding-top: 5px; 193 | font-style: normal; 194 | font-size: small; 195 | } 196 | 197 | .bright .optilink { 198 | color: #3c4043 !important; 199 | } 200 | .dark .optilink { 201 | color: rgb(167, 159, 147) !important; 202 | } 203 | 204 | .optilink img { 205 | vertical-align: middle; 206 | margin-right: 5px; 207 | margin-top: -3px; 208 | max-width: 16px; 209 | display: inline; 210 | } 211 | 212 | hr { 213 | height: 1px; 214 | margin: 0 .5em .5em .5em; 215 | border: 0px; 216 | background-color: rgb(220, 220, 220); 217 | } 218 | .dark hr { 219 | background-color: var(--line-dark); 220 | } 221 | 222 | .opticopy { 223 | position: absolute; 224 | width: auto; 225 | height: auto; 226 | right: 6px; 227 | top: 6px; 228 | user-select: none; 229 | font-size: x-small; 230 | opacity: 70%; 231 | } 232 | 233 | .opticopy svg { 234 | width: 15px; 235 | height: 15px; 236 | stroke: #bbc0c4; 237 | } 238 | .opticopy svg:hover { 239 | stroke: #9fa6ad; 240 | cursor: pointer; 241 | } 242 | .optibody p, 243 | .optibody ul, 244 | .optibody ol { 245 | line-height: 1.5; 246 | margin-block-start: 1em; 247 | margin-block-end: 1em; 248 | } 249 | 250 | .optibody ul, 251 | .optibody ol { 252 | padding-left: 15px; 253 | } 254 | .optibody img { 255 | max-width: 100%; 256 | } 257 | 258 | .optibody li, 259 | .optibody ol { 260 | list-style: initial; 261 | margin: 0.4rem; 262 | padding: 0 0.4rem; 263 | } 264 | 265 | blockquote { 266 | position: relative; 267 | padding-left: 12px; 268 | } 269 | 270 | blockquote:before { 271 | content: ""; 272 | display: block; 273 | position: absolute; 274 | top: 0; 275 | bottom: 0; 276 | left: 0; 277 | width: 4px; 278 | height: inherit; 279 | border-radius: 8px; 280 | background: #c8ccd0; 281 | } 282 | 283 | code { 284 | font-family: Consolas, Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 285 | Bitstream Vera Sans Mono, Courier New, monospace, sans-serif; 286 | padding: 0.25em 0.25em; 287 | border-radius: 3px; 288 | font-size: 100%; 289 | } 290 | 291 | pre code { 292 | display: block; 293 | } 294 | 295 | pre { 296 | border: 0px; 297 | margin-bottom: 1em; 298 | padding: 12px 8px; 299 | width: auto; 300 | max-height: 600px; 301 | overflow: auto; 302 | font-family: Consolas, Menlo, Monaco, monospace; 303 | font-size: 13px; 304 | border-radius: 3px; 305 | scrollbar-color: rgba(0, 0, 0, 0.2) transparent; 306 | white-space: pre-wrap; 307 | } 308 | 309 | span.math-container > mjx-container[jax="SVG"][display="true"]{ 310 | margin: 0; 311 | } 312 | 313 | .opticontent { 314 | overflow-y: auto; 315 | } 316 | 317 | a { 318 | text-decoration: none; 319 | } 320 | .bright a { 321 | color: #0077cc; 322 | } 323 | .dark a { 324 | color: #8ab4f8; 325 | } 326 | a:hover { 327 | text-decoration: underline; 328 | } 329 | 330 | .stackfoot .d-none { 331 | display: none; 332 | } 333 | 334 | .stackfoot { 335 | margin-top: 15px; 336 | line-height: 23px; 337 | font-size: small; 338 | } 339 | 340 | .bright pre, 341 | .bright code { 342 | background: #f7f7f7; 343 | } 344 | 345 | .dark pre, 346 | .dark code { 347 | background: #222; 348 | } 349 | 350 | .dark.duckduckgo { 351 | background-color: #282828; 352 | } 353 | 354 | .dark.duckduckgo pre, 355 | .dark.duckduckgo code { 356 | background-color: #222; 357 | } 358 | -------------------------------------------------------------------------------- /src/styles/w3schools.css: -------------------------------------------------------------------------------- 1 | .optibody.w3body h3 { 2 | font-weight: bold; 3 | color: #4a4a4a; 4 | } 5 | .dark .optibody.w3body h3 { 6 | color: #b5b5b5; 7 | } 8 | .optibody.w3body b { 9 | font-weight: bold; 10 | } 11 | .optibody.w3body .w3-example { 12 | background-color: #eff0f1; 13 | border-radius: 5px; 14 | padding: 10px 10px; 15 | font-size: small; 16 | } 17 | .optibody.w3body .w3-example h3 { 18 | margin: initial; 19 | } 20 | -------------------------------------------------------------------------------- /src/styles/wikipedia.css: -------------------------------------------------------------------------------- 1 | .optibody.wikibody .imgwiki img { 2 | width: 40%; 3 | height: 100%; 4 | float: right; 5 | border-radius: 5px; 6 | margin: 10px; 7 | } 8 | .optibody.wikibody p { 9 | margin: 0.5em 0; 10 | } 11 | -------------------------------------------------------------------------------- /tests/test.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require('puppeteer'); 2 | const assert = require('assert'); 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | /** @type {puppeteer.Page} */ 7 | let page = null; 8 | let engines = null; 9 | 10 | let { ElementHandle, Page } = puppeteer; 11 | 12 | /** 13 | * Set value on a select element 14 | * @param {string} value 15 | * @returns {Promise} 16 | */ 17 | ElementHandle.prototype.select = async function (value) { 18 | await this._page.evaluateHandle((el, value) => { 19 | const event = new Event("change", { bubbles: true }); 20 | event.simulated = true; 21 | el.querySelector(`option[value="${value}"]`).selected = true; 22 | el.dispatchEvent(event); 23 | }, this, value); 24 | }; 25 | 26 | /** 27 | * Check if element is visible in the DOM 28 | * @returns {Promise} 29 | **/ 30 | ElementHandle.prototype.isVisible = async function () { 31 | return (await this.boundingBox() !== null); 32 | }; 33 | 34 | /** 35 | * Get element attribute 36 | * @param {string} attr 37 | * @returns {Promise} 38 | */ 39 | ElementHandle.prototype.get = async function (attr, page) { 40 | const handle = await page.evaluateHandle((el, attr) => el[attr], this, attr); 41 | return await handle.jsonValue(); 42 | }; 43 | 44 | 45 | Page.prototype.assertEqual = function (actual, expected) { 46 | assert.equal(actual, expected, `"${this.engineName}": {${actual}==${expected}}`); 47 | } 48 | Page.prototype.assertOk = function (value, message) { 49 | assert.ok(value, `"${this.engineName}": ${message}`); 50 | } 51 | 52 | const EXTENSION_PATH = __dirname+"/.."; 53 | 54 | const read = (file) => new Promise((resolve, reject) => { 55 | fs.readFile(path.join(EXTENSION_PATH, file), { encoding: 'utf-8' }, (err, data) => { 56 | if (err) { 57 | reject(err); 58 | return; 59 | } 60 | resolve(data); 61 | }); 62 | }); 63 | 64 | 65 | 66 | /** @type {puppeteer.Browser} */ 67 | let browser = null; 68 | 69 | const boot = async () => { 70 | browser = await puppeteer.launch({ 71 | headless: false, // extension are allowed only in head-full mode 72 | // executablePath: 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe', 73 | defaultViewport: null, 74 | devtools: true, 75 | // slowMo: true, 76 | args: [ 77 | `--disable-extensions-except=${EXTENSION_PATH}`, 78 | `--load-extension=${EXTENSION_PATH}`, 79 | '--enable-automation', 80 | `--window-size=1920,1080`, 81 | ] 82 | }); 83 | } 84 | 85 | /** @type {puppeteer.Page[]} */ 86 | let pages = []; 87 | 88 | async function ap(testFun) { 89 | await Promise.all(pages.map(testFun)); 90 | } 91 | 92 | 93 | // Type of test 94 | describe('OptiPanel', function () { 95 | this.timeout(2000000); // default is 2 seconds and that may not be enough to boot browsers and pages. 96 | before(async () => { 97 | await boot(); 98 | engines = JSON.parse(await read(`./src/engines.json`)); 99 | }); 100 | 101 | // Part of the app 102 | describe('Engines selector', async () => { 103 | before(async () => { 104 | pages = await Promise.all(Object.entries(engines).filter(([e, v]) => v.active).map(async ([e, v]) => { 105 | const p = await browser.newPage(); 106 | p.logs = []; 107 | p.engine = v; 108 | p.engineName = e; 109 | p.on('console', message => { 110 | const text = message.text(); 111 | if (text.startsWith("%c[OptiSearch]")) { 112 | p.logs.push({ 113 | type: message.type(), 114 | text: text.substring("%c[OptiSearch] font-weight: bold; ".length), 115 | }); 116 | } 117 | }); 118 | return p; 119 | })); 120 | 121 | const helloPromises = Promise.all(pages.map(p => new Promise(resolve => { 122 | p.on('console', () => p.logs.length && resolve()); 123 | }))); 124 | 125 | await Promise.all(pages.map(p => { 126 | if (p.engineName === "DuckDuckGo") 127 | return p.goto(`${p.engine.link}/?q=setinterval%20js%20stackoverflow`); 128 | if (p.engineName === "Baidu") 129 | return p.goto(`${p.engine.link}/s?wd=setinterval%20js%20stackoverflow`); 130 | return p.goto(`${p.engine.link}/search?q=setinterval%20js%20stackoverflow`); 131 | })); 132 | 133 | await helloPromises; 134 | }); 135 | 136 | // Functionality 137 | it('Right Column', async () => await ap(async p => { 138 | const el = await p.$(p.engine.rightColumn); 139 | p.assertOk(el); 140 | })); 141 | it('Search string', async () => await ap(async p => { 142 | const el = await p.$(p.engine.searchBox); 143 | p.assertEqual(await el.get("value", p), "setinterval js stackoverflow"); 144 | })); 145 | it('Result row', async () => await ap(async p => { 146 | const els = await p.$$(p.engine.resultRow); 147 | p.assertOk(els.length > 0, "Failed to parse results row"); 148 | })); 149 | it('Stackoverflow panel', async () => await ap(async p => { 150 | const els = await p.$$(".stackbody.optibody"); 151 | p.assertOk(els.length > 0, 'No Stackoverflow panel found'); 152 | })); 153 | }); 154 | 155 | // after(async () => await browser.close()); 156 | }); 157 | 158 | 159 | 160 | --------------------------------------------------------------------------------