├── .github └── workflows │ └── tests.yml ├── .gitignore ├── .nvmrc ├── LICENSE.md ├── README.md ├── biome.json ├── build.js ├── chrome └── manifest.json ├── firefox └── manifest.json ├── nodemon.json ├── package-lock.json ├── package.json ├── safari ├── Legacy iOS │ ├── .gitignore │ ├── App Store Screenshots │ │ ├── Simulator Screenshot - iPad Pro (12.9-inch) (2nd generation) - 2024-01-01 at 23.07.55.png │ │ ├── Simulator Screenshot - iPad Pro (12.9-inch) (6th generation) - 2024-01-01 at 23.08.32.png │ │ ├── Simulator Screenshot - iPhone 15 Plus - 2024-01-01 at 22.56.32.png │ │ ├── Simulator Screenshot - iPhone 15 Plus - 2024-01-01 at 22.56.44.png │ │ ├── Simulator Screenshot - iPhone 15 Plus - 2024-01-01 at 22.56.59.png │ │ ├── Simulator Screenshot - iPhone 8 Plus - 2024-01-01 at 23.02.04.png │ │ ├── Simulator Screenshot - iPhone 8 Plus - 2024-01-01 at 23.04.12.png │ │ ├── Simulator Screenshot - iPhone 8 Plus - 2024-01-01 at 23.04.26.png │ │ ├── Simulator Screenshot - iPhone SE (3rd generation) - 2024-01-01 at 22.57.50.png │ │ ├── Simulator Screenshot - iPhone SE (3rd generation) - 2024-01-01 at 22.58.42.png │ │ └── Simulator Screenshot - iPhone SE (3rd generation) - 2024-01-01 at 22.59.19.png │ ├── KagiForSafari Extension │ │ ├── Info.plist │ │ ├── Resources │ │ │ ├── _locales │ │ │ │ └── en │ │ │ │ │ └── messages.json │ │ │ ├── background.js │ │ │ ├── content.js │ │ │ ├── images │ │ │ │ └── icon.png │ │ │ ├── manifest.json │ │ │ ├── popup.html │ │ │ └── popup.js │ │ └── SafariWebExtensionHandler.swift │ ├── KagiForSafari.xcodeproj │ │ ├── project.pbxproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── KagiForSafari │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── Contents.json │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ │ └── ItunesArtwork@2x.png │ │ │ ├── Contents.json │ │ │ └── main_icon.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── ItunesArtwork@2x.png │ │ ├── Base.lproj │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Config.xcconfig │ │ ├── Info.plist │ │ ├── Localization │ │ │ └── en.lproj │ │ │ │ ├── InfoPlist.strings │ │ │ │ └── Localizable.strings │ │ ├── Resources │ │ │ └── lufga-regular-webfont.ttf │ │ └── ViewController.swift │ └── README.md ├── Legacy macOS │ ├── .gitignore │ ├── App Store Screenshots │ │ ├── 1286x0w.png │ │ ├── Kagi Search for Safari App Store Demo v2.mp4 │ │ └── Main.png │ ├── Kagi Search Extension │ │ ├── Base.lproj │ │ │ └── SafariExtensionViewController.xib │ │ ├── Info.plist │ │ ├── Kagi_Search_Extension.entitlements │ │ ├── Resources │ │ │ └── ToolbarItemIcon.png │ │ ├── SafariExtensionHandler.swift │ │ └── SafariExtensionViewController.swift │ ├── Kagi Search.xcodeproj │ │ ├── project.pbxproj │ │ └── project.xcworkspace │ │ │ └── contents.xcworkspacedata │ ├── Kagi Search │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── Contents.json │ │ │ │ ├── icon_128x128.png │ │ │ │ ├── icon_128x128@2x.png │ │ │ │ ├── icon_16x16.png │ │ │ │ ├── icon_16x16@2x.png │ │ │ │ ├── icon_256x256.png │ │ │ │ ├── icon_256x256@2x.png │ │ │ │ ├── icon_32x32.png │ │ │ │ ├── icon_32x32@2x.png │ │ │ │ ├── icon_512x512.png │ │ │ │ └── icon_512x512@2x.png │ │ │ └── Contents.json │ │ ├── Base.lproj │ │ │ ├── Main.html │ │ │ └── Main.storyboard │ │ ├── Kagi Search.entitlements │ │ ├── Kagi_Search.entitlements │ │ ├── Resources │ │ │ ├── Enable.png │ │ │ ├── Enable_dark.png │ │ │ ├── Icon.png │ │ │ ├── Script.js │ │ │ └── Style.css │ │ └── ViewController.swift │ ├── MainConfig.xcconfig │ └── README.md ├── README.md └── Universal │ ├── App Store Screenshots │ ├── Simulator Screenshot - iPad Pro (12.9-inch) (2nd generation) - 2023-12-21 at 22.18.59.png │ ├── Simulator Screenshot - iPad Pro (12.9-inch) (2nd generation) - 2023-12-21 at 22.20.03.png │ ├── Simulator Screenshot - iPad Pro (12.9-inch) (6th generation) - 2023-12-21 at 22.12.47.png │ ├── Simulator Screenshot - iPad Pro (12.9-inch) (6th generation) - 2023-12-21 at 22.13.39.png │ ├── Simulator Screenshot - iPhone 8 Plus - 2023-12-21 at 22.04.02.png │ ├── Simulator Screenshot - iPhone 8 Plus - 2023-12-21 at 22.04.30.png │ ├── Simulator Screenshot - iPhone 8 Plus - 2023-12-21 at 22.06.02.png │ ├── iOS 5.5inch - 01.png │ ├── iOS 5.5inch - 02.png │ ├── iOS 6.7inch - 01.png │ ├── iOS 6.7inch - 02.png │ ├── iOS 6.7inch - 03.png │ ├── iOS 6.7inch - 04.png │ ├── iPad 2nd Gen - 01.png │ ├── iPad 2nd Gen - 02.png │ ├── iPad 2nd Gen - 03.png │ ├── iPad 6th Gen - 01.png │ ├── iPad 6th Gen - 02.png │ ├── iPad 6th Gen - 03.png │ └── macOS - 01.png │ ├── Kagi Search (iOS) │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ ├── Kagi Search (iOS).entitlements │ ├── Resources │ │ ├── Enable_Extension_Cropped_dark.png │ │ ├── Enable_Extension_Cropped_light.png │ │ ├── Grant_Permissions_Animation_dark copy.gif │ │ ├── Grant_Permissions_Animation_dark.gif │ │ ├── Grant_Permissions_Animation_light copy.gif │ │ ├── Grant_Permissions_Animation_light.gif │ │ ├── Grant_Permissions_Cropped.acorn │ │ ├── Grant_Permissions_Cropped_dark.png │ │ ├── Grant_Permissions_Cropped_light.png │ │ ├── Private_Token.acorn │ │ ├── Private_Token_dark.png │ │ ├── Private_Token_light.png │ │ ├── Settings_Animation_dark.gif │ │ └── Settings_Animation_light.gif │ └── SceneDelegate.swift │ ├── Kagi Search (iOS)Tests │ └── Kagi_Search__iOS_Tests.swift │ ├── Kagi Search (iOS)UITests │ ├── Kagi_Search__iOS_UITests.swift │ └── Kagi_Search__iOS_UITestsLaunchTests.swift │ ├── Kagi Search (macOS) │ ├── AppDelegate.swift │ ├── Base.lproj │ │ └── Main.storyboard │ ├── Kagi Search (macOS).entitlements │ ├── Kagi-Search-macOS-Info.plist │ ├── NSImage+Helpers.swift │ ├── NSWindow+Helpers.swift │ ├── Screenshots │ │ ├── Permissions Dark.acorn │ │ ├── Permissions Dark.png │ │ ├── Permissions Light.acorn │ │ └── Permissions Light.png │ └── macOS Assets.xcassets │ │ ├── Contents.json │ │ └── PermissionsPopup.imageset │ │ ├── Contents.json │ │ ├── Permissions Dark.png │ │ └── Permissions Light.png │ ├── Kagi Search Extension iOS copy-Info.plist │ ├── Kagi Search iOS copy-Info.plist │ ├── Kagi Search.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ ├── Kagi Search iOS (Legacy).xcscheme │ │ └── Kagi Search iOS.xcscheme │ ├── MainConfig.xcconfig │ ├── README.md │ ├── Shared (App) │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppBackground.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── 1024.png │ │ │ ├── 1024x1024.png │ │ │ ├── 128.png │ │ │ ├── 256 1.png │ │ │ ├── 256.png │ │ │ ├── 32 1.png │ │ │ ├── 32.png │ │ │ ├── 512 1.png │ │ │ ├── 512.png │ │ │ ├── 64.png │ │ │ ├── Contents.json │ │ │ └── macOS-16.png │ │ ├── Contents.json │ │ ├── LargeIcon.imageset │ │ │ ├── 512.png │ │ │ └── Contents.json │ │ ├── TitlebarPermissionEnableScreenshot.imageset │ │ │ ├── Contents.json │ │ │ └── TitlebarSmaller.png │ │ └── ToolbarItemIcon.imageset │ │ │ ├── 128.png │ │ │ └── Contents.json │ ├── Base.lproj │ │ └── Main.html │ ├── Deeplinks.swift │ ├── Resources │ │ ├── ScreenshotIcon.svg │ │ ├── Script.js │ │ ├── Style.css │ │ ├── allow_private_browsing.png │ │ ├── basicLightbox.min.js │ │ ├── enable_extension.png │ │ ├── grant_permissions.png │ │ ├── ios_enable_extension.png │ │ ├── ios_grant_permissions_1.png │ │ ├── ios_grant_permissions_2.png │ │ ├── ios_grant_permissions_3.png │ │ ├── ios_private_browsing.png │ │ ├── ios_private_session_link.png │ │ ├── ios_select_safari_engine.png │ │ ├── private_session_link.png │ │ └── select_safari_engine.png │ └── ViewController.swift │ ├── Shared (Extension) │ ├── Preferences.swift │ ├── Resources │ │ ├── _locales │ │ │ └── en │ │ │ │ └── messages.json │ │ ├── background.js │ │ ├── content-script.js │ │ ├── images │ │ │ ├── Icon.png │ │ │ ├── ToolbarItemIcon.pdf │ │ │ └── ToolbarItemIcon.png │ │ ├── kagi-content-script.js │ │ ├── popup.css │ │ ├── popup.html │ │ ├── popup.js │ │ └── rule-builder.js │ └── SafariWebExtensionHandler.swift │ ├── iOS (Extension) │ ├── Info.plist │ ├── Kagi Search Extension (iOS).entitlements │ └── manifest.json │ └── macOS (Extension) │ ├── Info.plist │ ├── Kagi Search Extension (macOS).entitlements │ ├── UpgradeChecker.swift │ └── manifest.json └── shared ├── icons ├── favicon-48.png ├── icon_16px.png ├── icon_180px.png └── icon_32px.png └── src ├── background.js ├── background_page.html ├── lib └── utils.js ├── popup.css ├── popup.html ├── popup.js ├── summarize_result.css ├── summarize_result.html └── summarize_result.js /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - uses: actions/setup-node@v3 11 | with: 12 | node-version-file: ".nvmrc" 13 | - uses: actions/cache@v3 14 | with: 15 | path: | 16 | ~/.npm 17 | ~/.nvm 18 | node_modules 19 | key: ${{ runner.os }}-${{ hashFiles('package-lock.json') }} 20 | - run: npm ci 21 | - run: npm test 22 | - run: npm run build 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | built/ 3 | .vscode/ 4 | xcuserdata/ 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.9.0 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2018-2023 Kagi Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kagi Search Extension 2 | 3 | This contains the source for the [Kagi Search](https://kagi.com) extension for Firefox and Chrome. 4 | 5 | Get it for your browser here: 6 | 7 | - [Chrome](https://chrome.google.com/webstore/detail/kagi-search-for-chrome/cdglnehniifkbagbbombnjghhcihifij) 8 | - [Firefox](https://addons.mozilla.org/en-US/firefox/addon/kagi-search-for-firefox/) 9 | 10 | ## Contents 11 | 12 | - [Extension Features](#extension-features) 13 | - [Other Browsers](#other-browsers) 14 | - [Installing from source](#installing-from-source) 15 | - [Download](#download) 16 | - [Load the extension](#load-the-extension) 17 | - [Firefox](#firefox) 18 | - [Chrome](#chrome) 19 | - [Contributing](#contributing) 20 | - [Accepted Contributions](#accepted-contributions) 21 | - [Instructions](#instructions) 22 | - [Development](#development) 23 | - [Testing/sharing/debugging](#testing-sharing-debugging) 24 | 25 | ## Extension Features 26 | 27 | - Sets Kagi as your default search engine 28 | - Automatic login when searching in incognito/private browsing 29 | - Use the Universal Summarizer on the current page 30 | - ... more in the future! 31 | 32 | ## Other Browsers 33 | 34 | - [Safari (iOS)](https://apps.apple.com/us/app/kagi-search-for-safari/id1607766153) 35 | 36 | ## Loading from source 37 | 38 | ### Building 39 | 40 | To build the extension, you will need node and npm installed. 41 | 42 | 1. Obtain the files from this repo, either via `git clone https://github.com/kagisearch/browser_extensions` or by downloading the source zip. 43 | 2. run `npm i` to install `adm-zip` which is used to package up the files. 44 | 3. You can now run `npm run build-firefox` or `npm run build-chrome` to zip up the relevant files and output a zip file. 45 | 46 | You can also download a pre-packaged zip from our releases page. 47 | 48 | ### Download 49 | 50 | First, obtain the source code from the release page: 51 | 52 | [GitHub Releases](https://github.com/kagisearch/browser_extensions/releases) 53 | 54 | Or by cloning the repo: 55 | 56 | `git clone https://github.com/kagisearch/browser_extensions` 57 | 58 | ### Loading the extension 59 | 60 | #### Firefox 61 | 62 | 1. Head to `about:debugging` 63 | 2. Click on "This Firefox" 64 | 3. Click "Load Temporary Add-On" 65 | 4. Select the zip file or manifest.json of the extension. 66 | 67 | #### Chrome 68 | 69 | 1. Head to `chrome://extensions` 70 | 2. Turn on "Developer mode" in the top right and then some new buttons will pop up. 71 | 3. Click on `Load unpacked extension` 72 | 4. Select the zip file or you may have to unzip the zip and select the folder outputted from extraction. 73 | 74 | ## Contributing 75 | 76 | ### Accepted Contributions 77 | 78 | Bugfixes and improvements to the extension's code are welcome! 79 | 80 | At the time we are not accepting PRs for new features without prior approval. 81 | 82 | You can inquire about new features or report bugs here: 83 | 84 | - [Issue tracker](https://github.com/kagisearch/browser_extensions/issues) 85 | - [Kagi Feedback Forum](https://kagifeedback.org/) 86 | 87 | ### Instructions 88 | 89 | 1. Fork it () 90 | 2. Create your feature branch (`git checkout -b my-new-feature`) 91 | 3. Commit your changes (`git commit -am 'Add some feature'`) 92 | 4. Push to the branch (`git push origin my-new-feature`) 93 | 5. Create a new Pull Request 94 | 95 | ### Development 96 | 97 | Check the recommended and required `node` and `npm` versions in the `package.json` and `.nvmrc` files. 98 | 99 | ```sh 100 | npm ci # install dependencies 101 | npm run watch-firefox # builds the firefox zip file and unzips it into the `built/` directory every time a file changes 102 | npm run watch-chrome # builds the chrome zip file and unzips it into the `built/` directory every time a file changes 103 | npm run format # formats the code 104 | ``` 105 | 106 | ### Testing/sharing/debugging 107 | 108 | ```sh 109 | npm run test # runs the linter & formatter checks 110 | npm run build # builds the chrome and firefox zip files in the `built/` directory 111 | npm run build-firefox # builds the firefox zip file in the `built/` directory 112 | npm run build-chrome # builds the chrome zip file in the `built/` directory 113 | ``` 114 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.4.1/schema.json", 3 | "organizeImports": { 4 | "enabled": false 5 | }, 6 | "files": { 7 | "ignore": ["built/"] 8 | }, 9 | "linter": { 10 | "enabled": true, 11 | "rules": { 12 | "recommended": true, 13 | "complexity": { 14 | "noExtraBooleanCast": "off", 15 | "noForEach": "off" 16 | } 17 | }, 18 | "ignore": ["safari/"] 19 | }, 20 | "formatter": { 21 | "enabled": true, 22 | "indentStyle": "space", 23 | "indentWidth": 2, 24 | "ignore": ["built/", "safari/"] 25 | }, 26 | "javascript": { 27 | "formatter": { 28 | "quoteStyle": "single" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | const AdmZip = require('adm-zip'); 2 | const args = process.argv.slice(2); 3 | 4 | if (args.length === 0) { 5 | console.log('Usage: node build.js '); 6 | process.exit(1); 7 | } 8 | 9 | const extension = args[0]; 10 | if (extension !== 'firefox' && extension !== 'chrome') { 11 | console.log('Only firefox and chrome extension building is supported'); 12 | process.exit(1); 13 | } 14 | 15 | const isWatching = args[1] === 'watch'; 16 | 17 | // Load the manifest json to grab version information and name. 18 | const manifest = require(`./${extension}/manifest.json`); 19 | const version = manifest.version; 20 | 21 | /* 22 | * We want to zip up the relevant extension files, which would be 23 | * all `shared/*` files and then the relevant `extension/*` files. 24 | * This approach seemed easier than copying some things around to a temp directory. 25 | * And! It's cross-platform! 26 | */ 27 | const zip = new AdmZip(); 28 | zip.addLocalFolder('shared'); 29 | zip.addLocalFolder(extension); 30 | 31 | zip.writeZip(`${__dirname}/built/kagi_${extension}_${version}.zip`); 32 | console.log(`Done: built/kagi_${extension}_${version}.zip`); 33 | 34 | if (isWatching) { 35 | zip.extractAllTo(`${__dirname}/built/`, true); 36 | console.log(`Done: Extracted built/kagi_${extension}_${version}.zip`); 37 | } 38 | -------------------------------------------------------------------------------- /chrome/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Kagi Search for Chrome", 4 | "version": "0.7.3.1", 5 | "description": "A simple extension for setting Kagi as a default search engine, and automatically logging in to Kagi in incognito browsing windows", 6 | "background": { 7 | "service_worker": "src/background.js", 8 | "type": "module" 9 | }, 10 | "icons": { 11 | "16": "icons/icon_16px.png", 12 | "32": "icons/icon_32px.png", 13 | "48": "icons/favicon-48.png", 14 | "180": "icons/icon_180px.png" 15 | }, 16 | "action": { 17 | "default_icon": "icons/icon_32px.png", 18 | "default_title": "Kagi Search", 19 | "default_popup": "src/popup.html" 20 | }, 21 | "permissions": [ 22 | "cookies", 23 | "declarativeNetRequestWithHostAccess", 24 | "webRequest", 25 | "storage", 26 | "contextMenus" 27 | ], 28 | "optional_permissions": ["activeTab"], 29 | "host_permissions": ["https://*.kagi.com/*"], 30 | "chrome_settings_overrides": { 31 | "search_provider": { 32 | "name": "Kagi", 33 | "search_url": "https://kagi.com/search?q={searchTerms}", 34 | "favicon_url": "https://assets.kagi.com/v2/favicon-32x32.png", 35 | "keyword": "@kagi", 36 | "is_default": true, 37 | "suggest_url": "https://kagisuggest.com/api/autosuggest?q={searchTerms}", 38 | "encoding": "UTF-8" 39 | } 40 | }, 41 | "incognito": "spanning", 42 | "commands": { 43 | "_execute_action": { 44 | "description": "Open the Kagi extension" 45 | }, 46 | "summarize-active-page": { 47 | "description": "Summarize the currently active page" 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /firefox/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Kagi Search for Firefox", 4 | "version": "0.7.4", 5 | "description": "A simple helper extension for setting Kagi as a default search engine, and automatically logging in to Kagi in incognito browsing windows.", 6 | "background": { 7 | "page": "src/background_page.html" 8 | }, 9 | "icons": { 10 | "16": "icons/icon_16px.png", 11 | "32": "icons/icon_32px.png", 12 | "48": "icons/favicon-48.png", 13 | "180": "icons/icon_180px.png" 14 | }, 15 | "action": { 16 | "default_icon": "icons/icon_32px.png", 17 | "default_title": "Kagi Search", 18 | "default_popup": "src/popup.html" 19 | }, 20 | "permissions": [ 21 | "cookies", 22 | "declarativeNetRequestWithHostAccess", 23 | "webRequest", 24 | "storage", 25 | "contextMenus" 26 | ], 27 | "optional_permissions": ["activeTab"], 28 | "host_permissions": ["https://*.kagi.com/*"], 29 | "chrome_settings_overrides": { 30 | "search_provider": { 31 | "name": "Kagi", 32 | "search_url": "https://kagi.com/search?q={searchTerms}", 33 | "favicon_url": "icons/icon_32px.png", 34 | "keyword": "@kagi", 35 | "is_default": true, 36 | "suggest_url": "https://kagisuggest.com/api/autosuggest?q={searchTerms}", 37 | "encoding": "UTF-8" 38 | } 39 | }, 40 | "incognito": "spanning", 41 | "commands": { 42 | "_execute_action": { 43 | "description": "Open the Kagi extension" 44 | }, 45 | "summarize-active-page": { 46 | "description": "Summarize the currently active page" 47 | } 48 | }, 49 | "browser_specific_settings": { 50 | "gecko": { 51 | "id": "search@kagi.com", 52 | "strict_min_version": "102.0" 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": false, 3 | "ignore": [".git", "built/**", "node_modules/**/node_modules"], 4 | "ext": "js,json,css,html" 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kagi_browser_ext", 3 | "version": "1.0.0", 4 | "description": "A simple helper extension for setting Kagi as a default search engine, and automatically logging in to Kagi in incognito browsing windows", 5 | "main": "build.js", 6 | "private": true, 7 | "scripts": { 8 | "watch-firefox": "nodemon build.js firefox watch", 9 | "watch-chrome": "nodemon build.js chrome watch", 10 | "build": "npm run build-firefox && npm run build-chrome", 11 | "build-firefox": "node build.js firefox", 12 | "build-chrome": "node build.js chrome", 13 | "format": "biome format --write . && biome check --apply .", 14 | "lint": "biome check .", 15 | "test": "npm run lint" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/kagisearch/browser_extensions.git" 20 | }, 21 | "author": "", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/kagisearch/browser_extensions/issues" 25 | }, 26 | "homepage": "https://github.com/kagisearch/browser_extensions#readme", 27 | "devDependencies": { 28 | "@biomejs/biome": "1.6.1", 29 | "adm-zip": "0.5.12", 30 | "nodemon": "3.1.0" 31 | }, 32 | "engines": { 33 | "node": "20.x", 34 | "npm": "10.x" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /safari/Legacy iOS/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.xcuserdatad 3 | -------------------------------------------------------------------------------- /safari/Legacy iOS/App Store Screenshots/Simulator Screenshot - iPad Pro (12.9-inch) (2nd generation) - 2024-01-01 at 23.07.55.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy iOS/App Store Screenshots/Simulator Screenshot - iPad Pro (12.9-inch) (2nd generation) - 2024-01-01 at 23.07.55.png -------------------------------------------------------------------------------- /safari/Legacy iOS/App Store Screenshots/Simulator Screenshot - iPad Pro (12.9-inch) (6th generation) - 2024-01-01 at 23.08.32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy iOS/App Store Screenshots/Simulator Screenshot - iPad Pro (12.9-inch) (6th generation) - 2024-01-01 at 23.08.32.png -------------------------------------------------------------------------------- /safari/Legacy iOS/App Store Screenshots/Simulator Screenshot - iPhone 15 Plus - 2024-01-01 at 22.56.32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy iOS/App Store Screenshots/Simulator Screenshot - iPhone 15 Plus - 2024-01-01 at 22.56.32.png -------------------------------------------------------------------------------- /safari/Legacy iOS/App Store Screenshots/Simulator Screenshot - iPhone 15 Plus - 2024-01-01 at 22.56.44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy iOS/App Store Screenshots/Simulator Screenshot - iPhone 15 Plus - 2024-01-01 at 22.56.44.png -------------------------------------------------------------------------------- /safari/Legacy iOS/App Store Screenshots/Simulator Screenshot - iPhone 15 Plus - 2024-01-01 at 22.56.59.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy iOS/App Store Screenshots/Simulator Screenshot - iPhone 15 Plus - 2024-01-01 at 22.56.59.png -------------------------------------------------------------------------------- /safari/Legacy iOS/App Store Screenshots/Simulator Screenshot - iPhone 8 Plus - 2024-01-01 at 23.02.04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy iOS/App Store Screenshots/Simulator Screenshot - iPhone 8 Plus - 2024-01-01 at 23.02.04.png -------------------------------------------------------------------------------- /safari/Legacy iOS/App Store Screenshots/Simulator Screenshot - iPhone 8 Plus - 2024-01-01 at 23.04.12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy iOS/App Store Screenshots/Simulator Screenshot - iPhone 8 Plus - 2024-01-01 at 23.04.12.png -------------------------------------------------------------------------------- /safari/Legacy iOS/App Store Screenshots/Simulator Screenshot - iPhone 8 Plus - 2024-01-01 at 23.04.26.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy iOS/App Store Screenshots/Simulator Screenshot - iPhone 8 Plus - 2024-01-01 at 23.04.26.png -------------------------------------------------------------------------------- /safari/Legacy iOS/App Store Screenshots/Simulator Screenshot - iPhone SE (3rd generation) - 2024-01-01 at 22.57.50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy iOS/App Store Screenshots/Simulator Screenshot - iPhone SE (3rd generation) - 2024-01-01 at 22.57.50.png -------------------------------------------------------------------------------- /safari/Legacy iOS/App Store Screenshots/Simulator Screenshot - iPhone SE (3rd generation) - 2024-01-01 at 22.58.42.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy iOS/App Store Screenshots/Simulator Screenshot - iPhone SE (3rd generation) - 2024-01-01 at 22.58.42.png -------------------------------------------------------------------------------- /safari/Legacy iOS/App Store Screenshots/Simulator Screenshot - iPhone SE (3rd generation) - 2024-01-01 at 22.59.19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy iOS/App Store Screenshots/Simulator Screenshot - iPhone SE (3rd generation) - 2024-01-01 at 22.59.19.png -------------------------------------------------------------------------------- /safari/Legacy iOS/KagiForSafari Extension/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionPointIdentifier 8 | com.apple.Safari.web-extension 9 | NSExtensionPrincipalClass 10 | $(PRODUCT_MODULE_NAME).SafariWebExtensionHandler 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /safari/Legacy iOS/KagiForSafari Extension/Resources/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension_name": { 3 | "message": "Kagi Search", 4 | "description": "..." 5 | }, 6 | "extension_description": { 7 | "message": "With Kagi Search extension you can get your search queries sent to Kagi while using Safari.", 8 | "description": "..." 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /safari/Legacy iOS/KagiForSafari Extension/Resources/background.js: -------------------------------------------------------------------------------- 1 | chrome.webNavigation.onBeforeNavigate.addListener(function(details) { 2 | let url = details.url; 3 | if (typeof(url) !== "undefined" && url.startsWith(`https://kagi.com`)) { 4 | chrome.storage.local.set({ lastKagiUrl: url }); 5 | } else { 6 | chrome.storage.local.set({ lastKagiUrl: "" }); 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /safari/Legacy iOS/KagiForSafari Extension/Resources/content.js: -------------------------------------------------------------------------------- 1 | const searchQuery = navigateIfNeeded(window.location); 2 | 3 | if (window.location.href.startsWith("https://kagi.com/")) { 4 | if (window.location.href.startsWith("https://kagi.com/signin") && getCookie("extRedirected") == "") { 5 | setCookie("extRedirected", "1"); 6 | chrome.storage.local.get(["privateSessionLink", "lastKagiUrl"], function(value) { 7 | var privateSessionLink = value.privateSessionLink; 8 | var lastKagiUrl = value.lastKagiUrl; 9 | var searchQuery = getParameterByName("q", lastKagiUrl) 10 | if (typeof(privateSessionLink) !== "undefined" && privateSessionLink.startsWith(`https://kagi.com/`)) { 11 | if (privateSessionLink.includes("q=%s")) { 12 | window.location.replace(privateSessionLink.replace("%s", searchQuery != null ? searchQuery : "")); 13 | } else { 14 | window.location.replace(privateSessionLink.concat("&q=", searchQuery != null ? searchQuery : "")); 15 | } 16 | } 17 | }); 18 | } else { 19 | setCookie("extRedirected", ""); 20 | } 21 | } else if (searchQuery != null) { 22 | if (!shouldSkipRedirect()) { 23 | window.location.replace(`https://kagi.com/search?q=${searchQuery}`); 24 | } 25 | } 26 | 27 | function shouldSkipRedirect(url = window.location.href) { 28 | var result = false; 29 | const host = location.host.replace("www.", ""); 30 | if (host.includes("google.") && getParameterByName("client") == null) { 31 | result = true; 32 | } else if (host.includes("bing.") && getParameterByName("form") == null) { 33 | result = true; 34 | } else if (host.includes("duckduckgo.") && getParameterByName("t") == null) { 35 | result = true; 36 | } else if (host.includes("search.yahoo.") && getParameterByName("fr") == null) { 37 | result = true; 38 | } 39 | 40 | return result; 41 | } 42 | 43 | function navigateIfNeeded(location) { 44 | var result = null; 45 | const host = location.host.replace("www.", ""); 46 | 47 | ["google.", "bing.", "ecosia.", "search.yahoo"].forEach(function(item){ 48 | if (host.includes(item)) { 49 | if (location.pathname === "/search") { 50 | result = getParameterByName((host.includes("search.yahoo")) ? "p" : "q"); 51 | } 52 | } 53 | }); 54 | 55 | 56 | if (host.startsWith("duckduckgo.")) { 57 | result = getParameterByName("q"); 58 | } 59 | 60 | if (host.startsWith("yandex.")) { 61 | if (location.pathname === "/search/touch/") { 62 | result = getParameterByName("text"); 63 | } 64 | } 65 | 66 | ["baidu.", "so."].forEach(function(item){ 67 | if (host.includes(item)) { 68 | result = getParameterByName((host.includes("baidu.")) ? "oq" : "src"); 69 | } 70 | }); 71 | 72 | if (host.startsWith("sogou.")) { 73 | if (location.pathname === "/web") { 74 | result = getParameterByName("query"); 75 | } 76 | } 77 | 78 | return result; 79 | } 80 | 81 | function getParameterByName(name, url = window.location.href) { 82 | name = name.replace(/[\[\]]/g, '\\$&'); 83 | var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'), 84 | results = regex.exec(url); 85 | 86 | if (!results) return null; 87 | if (!results[2]) return ''; 88 | 89 | return results[2].replace(/\+/g, ' '); 90 | } 91 | 92 | function setCookie(cname, cvalue) { 93 | const d = new Date(); 94 | d.setTime(d.getTime() + (5000)); 95 | let expires = "expires="+ d.toUTCString(); 96 | document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/"; 97 | } 98 | 99 | function getCookie(cname) { 100 | let name = cname + "="; 101 | let decodedCookie = decodeURIComponent(document.cookie); 102 | let ca = decodedCookie.split(';'); 103 | for(let i = 0; i " ], 23 | "run_at": "document_start" 24 | }], 25 | 26 | "browser_action": { 27 | "default_popup": "popup.html", 28 | "default_icon": { 29 | "16": "images/icon.png", 30 | "19": "images/icon.png", 31 | "32": "images/icon.png", 32 | "48": "images/icon.png", 33 | "72": "images/icon.png" 34 | } 35 | }, 36 | 37 | "permissions": [ "", "nativeMessaging", "storage", "webNavigation" ] 38 | } 39 | -------------------------------------------------------------------------------- /safari/Legacy iOS/KagiForSafari Extension/Resources/popup.js: -------------------------------------------------------------------------------- 1 | document.getElementById("footerLink").onclick = function() { 2 | window.open("https://kagi.com/settings?p=user_details#sessionlink"); 3 | }; 4 | 5 | chrome.storage.local.get("privateSessionLink", function(value) { 6 | var privateSessionLink = value.privateSessionLink; 7 | if (typeof(privateSessionLink) !== "undefined") { 8 | document.getElementById('privateSessionLink').value = value.privateSessionLink; 9 | } 10 | }); 11 | 12 | document.getElementById('privateSessionLink').addEventListener('input', function (evt) { 13 | chrome.storage.local.set({ privateSessionLink: document.getElementById('privateSessionLink').value}); 14 | }); 15 | -------------------------------------------------------------------------------- /safari/Legacy iOS/KagiForSafari Extension/SafariWebExtensionHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SafariWebExtensionHandler.swift 3 | // KagiForSafari Extension 4 | // 5 | // Created by apples on 31.01.2022. 6 | // 7 | 8 | import SafariServices 9 | import os.log 10 | 11 | class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { 12 | func beginRequest(with context: NSExtensionContext) { 13 | let response = NSExtensionItem() 14 | context.completeRequest(returningItems: [response], completionHandler: nil) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /safari/Legacy iOS/KagiForSafari.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /safari/Legacy iOS/KagiForSafari.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /safari/Legacy iOS/KagiForSafari/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // KagiForSafari 4 | // 5 | 6 | import UIKit 7 | 8 | @main 9 | class AppDelegate: UIResponder, UIApplicationDelegate { 10 | 11 | var window: UIWindow? 12 | 13 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 14 | 15 | self.window = UIWindow(frame: UIScreen.main.bounds) 16 | self.window?.rootViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "ViewController") 17 | self.window?.makeKeyAndVisible() 18 | 19 | return true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /safari/Legacy iOS/KagiForSafari/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Icon-App-20x20@2x.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "Icon-App-20x20@3x.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "Icon-App-29x29@1x.png", 17 | "idiom" : "iphone", 18 | "scale" : "1x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "Icon-App-29x29@2x.png", 23 | "idiom" : "iphone", 24 | "scale" : "2x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "Icon-App-29x29@3x.png", 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "29x29" 32 | }, 33 | { 34 | "filename" : "Icon-App-40x40@2x.png", 35 | "idiom" : "iphone", 36 | "scale" : "2x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "Icon-App-40x40@3x.png", 41 | "idiom" : "iphone", 42 | "scale" : "3x", 43 | "size" : "40x40" 44 | }, 45 | { 46 | "filename" : "Icon-App-60x60@2x.png", 47 | "idiom" : "iphone", 48 | "scale" : "2x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "filename" : "Icon-App-60x60@3x.png", 53 | "idiom" : "iphone", 54 | "scale" : "3x", 55 | "size" : "60x60" 56 | }, 57 | { 58 | "filename" : "Icon-App-20x20@1x.png", 59 | "idiom" : "ipad", 60 | "scale" : "1x", 61 | "size" : "20x20" 62 | }, 63 | { 64 | "filename" : "Icon-App-20x20@2x.png", 65 | "idiom" : "ipad", 66 | "scale" : "2x", 67 | "size" : "20x20" 68 | }, 69 | { 70 | "filename" : "Icon-App-29x29@1x.png", 71 | "idiom" : "ipad", 72 | "scale" : "1x", 73 | "size" : "29x29" 74 | }, 75 | { 76 | "filename" : "Icon-App-29x29@2x.png", 77 | "idiom" : "ipad", 78 | "scale" : "2x", 79 | "size" : "29x29" 80 | }, 81 | { 82 | "filename" : "Icon-App-40x40@1x.png", 83 | "idiom" : "ipad", 84 | "scale" : "1x", 85 | "size" : "40x40" 86 | }, 87 | { 88 | "filename" : "Icon-App-40x40@2x.png", 89 | "idiom" : "ipad", 90 | "scale" : "2x", 91 | "size" : "40x40" 92 | }, 93 | { 94 | "filename" : "Icon-App-76x76@1x.png", 95 | "idiom" : "ipad", 96 | "scale" : "1x", 97 | "size" : "76x76" 98 | }, 99 | { 100 | "filename" : "Icon-App-76x76@2x.png", 101 | "idiom" : "ipad", 102 | "scale" : "2x", 103 | "size" : "76x76" 104 | }, 105 | { 106 | "filename" : "Icon-App-83.5x83.5@2x.png", 107 | "idiom" : "ipad", 108 | "scale" : "2x", 109 | "size" : "83.5x83.5" 110 | }, 111 | { 112 | "filename" : "ItunesArtwork@2x.png", 113 | "idiom" : "ios-marketing", 114 | "scale" : "1x", 115 | "size" : "1024x1024" 116 | } 117 | ], 118 | "info" : { 119 | "author" : "xcode", 120 | "version" : 1 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /safari/Legacy iOS/KagiForSafari/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy iOS/KagiForSafari/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /safari/Legacy iOS/KagiForSafari/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy iOS/KagiForSafari/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /safari/Legacy iOS/KagiForSafari/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy iOS/KagiForSafari/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /safari/Legacy iOS/KagiForSafari/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy iOS/KagiForSafari/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /safari/Legacy iOS/KagiForSafari/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy iOS/KagiForSafari/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /safari/Legacy iOS/KagiForSafari/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy iOS/KagiForSafari/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /safari/Legacy iOS/KagiForSafari/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy iOS/KagiForSafari/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /safari/Legacy iOS/KagiForSafari/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy iOS/KagiForSafari/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /safari/Legacy iOS/KagiForSafari/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy iOS/KagiForSafari/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /safari/Legacy iOS/KagiForSafari/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy iOS/KagiForSafari/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /safari/Legacy iOS/KagiForSafari/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy iOS/KagiForSafari/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /safari/Legacy iOS/KagiForSafari/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy iOS/KagiForSafari/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /safari/Legacy iOS/KagiForSafari/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy iOS/KagiForSafari/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /safari/Legacy iOS/KagiForSafari/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy iOS/KagiForSafari/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /safari/Legacy iOS/KagiForSafari/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy iOS/KagiForSafari/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png -------------------------------------------------------------------------------- /safari/Legacy iOS/KagiForSafari/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /safari/Legacy iOS/KagiForSafari/Assets.xcassets/main_icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "ItunesArtwork@2x.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /safari/Legacy iOS/KagiForSafari/Assets.xcassets/main_icon.imageset/ItunesArtwork@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy iOS/KagiForSafari/Assets.xcassets/main_icon.imageset/ItunesArtwork@2x.png -------------------------------------------------------------------------------- /safari/Legacy iOS/KagiForSafari/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /safari/Legacy iOS/KagiForSafari/Config.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Config.xcconfig 3 | // KagiForSafari 4 | // 5 | 6 | MARKETING_VERSION = 2.1.0 7 | CURRENT_PROJECT_VERSION = 16 8 | -------------------------------------------------------------------------------- /safari/Legacy iOS/KagiForSafari/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleURLTypes 6 | 7 | 8 | CFBundleTypeRole 9 | Editor 10 | CFBundleURLSchemes 11 | 12 | prefs 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /safari/Legacy iOS/KagiForSafari/Localization/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | CFBundleDisplayName = "Kagi Search"; 2 | -------------------------------------------------------------------------------- /safari/Legacy iOS/KagiForSafari/Localization/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "info_label" = "How to configure extension:\n1) Open iOS Settings -> Safari -> Extensions\n2) Select \"Kagi Search\"\n3) In \"Permissions for Kagi Search\" below select All Websites -> Allow\n4) (optional) Tap the extension once activated in Safari to add private session link for using Kagi even in private windows"; 2 | "settings_title" = "Open iOS Settings"; 3 | "visit_title" = "Visit kagi.com"; 4 | -------------------------------------------------------------------------------- /safari/Legacy iOS/KagiForSafari/Resources/lufga-regular-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy iOS/KagiForSafari/Resources/lufga-regular-webfont.ttf -------------------------------------------------------------------------------- /safari/Legacy iOS/KagiForSafari/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // KagiForSafari 4 | // 5 | 6 | import UIKit 7 | 8 | final class ViewController: UIViewController { 9 | @IBOutlet weak var label: UILabel! 10 | @IBOutlet weak var settingsButton: UIButton! 11 | @IBOutlet weak var visitButton: UIButton! 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | 16 | let font = UIFont(name: "Lufga-Regular", size: 17) 17 | self.label.font = font 18 | self.label.text = NSLocalizedString("info_label", comment: "You can turn on Kagi Search Safari extension in iOS Settings -> Safari -> Extensions") 19 | 20 | self.settingsButton.setAttributedTitle( 21 | NSAttributedString( 22 | string: NSLocalizedString("settings_title", comment: "Open iOS Settings"), 23 | attributes: [NSAttributedString.Key.font: font as Any] 24 | ), 25 | for: .normal 26 | ) 27 | 28 | self.visitButton.setAttributedTitle( 29 | NSAttributedString( 30 | string: NSLocalizedString("visit_title", comment: "Visit kagi.com"), 31 | attributes: [NSAttributedString.Key.font: font as Any] 32 | ), for: .normal 33 | ) 34 | } 35 | 36 | @IBAction func openSettingsTapped() { 37 | guard let url = URL(string: UIApplication.openSettingsURLString) else { 38 | return 39 | } 40 | UIApplication.shared.open(url, options: [:], completionHandler: nil) 41 | } 42 | 43 | @IBAction func visitKagiTapped() { 44 | UIApplication.shared.open(URL(string: "https://kagi.com")!, options: [:], completionHandler: nil) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /safari/Legacy iOS/README.md: -------------------------------------------------------------------------------- 1 | # Kagi Search for Safari (iOS) 2 | 3 | screenshot 4 | 5 | Minimum iOS version supported: 15.0
6 | Language: Swift 7 | -------------------------------------------------------------------------------- /safari/Legacy macOS/.gitignore: -------------------------------------------------------------------------------- 1 | *.xcuserstate 2 | .DS_Store 3 | xcshareddata 4 | xcuserdata 5 | -------------------------------------------------------------------------------- /safari/Legacy macOS/App Store Screenshots/1286x0w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy macOS/App Store Screenshots/1286x0w.png -------------------------------------------------------------------------------- /safari/Legacy macOS/App Store Screenshots/Kagi Search for Safari App Store Demo v2.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy macOS/App Store Screenshots/Kagi Search for Safari App Store Demo v2.mp4 -------------------------------------------------------------------------------- /safari/Legacy macOS/App Store Screenshots/Main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy macOS/App Store Screenshots/Main.png -------------------------------------------------------------------------------- /safari/Legacy macOS/Kagi Search Extension/Base.lproj/SafariExtensionViewController.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | Use this to search with Kagi even in a private window, logged in to your current account. You can find Session Link in your Kagi Settings -> Privacy menu screen. Do not share this URL with anyone. 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 54 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /safari/Legacy macOS/Kagi Search Extension/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionPointIdentifier 8 | com.apple.Safari.extension 9 | NSExtensionPrincipalClass 10 | $(PRODUCT_MODULE_NAME).SafariExtensionHandler 11 | SFSafariToolbarItem 12 | 13 | Action 14 | Popover 15 | Identifier 16 | Button 17 | Image 18 | ToolbarItemIcon.png 19 | Label 20 | Kagi Search 21 | 22 | SFSafariWebsiteAccess 23 | 24 | Allowed Domains for Header Injection 25 | 26 | kagi.com 27 | 28 | Level 29 | All 30 | 31 | 32 | NSHumanReadableDescription 33 | Kagi Search is a quick, user-centric, 100% privacy-respecting search engine with results augmented by non-commercial indexes and personalized searches. 34 | 35 | 36 | -------------------------------------------------------------------------------- /safari/Legacy macOS/Kagi Search Extension/Kagi_Search_Extension.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /safari/Legacy macOS/Kagi Search Extension/Resources/ToolbarItemIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy macOS/Kagi Search Extension/Resources/ToolbarItemIcon.png -------------------------------------------------------------------------------- /safari/Legacy macOS/Kagi Search Extension/SafariExtensionHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SafariExtensionHandler.swift 3 | // Kagi Search Extension 4 | // 5 | 6 | import SafariServices 7 | 8 | class SafariExtensionHandler: SFSafariExtensionHandler { 9 | struct SearchSource { 10 | let host: String 11 | let queryParameter: String 12 | } 13 | 14 | let sources = [ 15 | SearchSource(host: "google.", queryParameter: "q"), 16 | SearchSource(host: "search.yahoo.com", queryParameter: "p"), 17 | SearchSource(host: "bing.", queryParameter: "q"), 18 | SearchSource(host: "bi.ng", queryParameter: "q"), 19 | SearchSource(host: "duckduckgo.com", queryParameter: "q"), 20 | SearchSource(host: "baidu.", queryParameter: "wd"), 21 | SearchSource(host: "yandex.", queryParameter: "text"), 22 | SearchSource(host: "ya.", queryParameter: "text"), 23 | SearchSource(host: "ecosia.org", queryParameter: "q"), 24 | SearchSource(host: "search.brave.com", queryParameter: "q"), 25 | SearchSource(host: "startpage.com", queryParameter: "query"), 26 | SearchSource(host: "neeva.com", queryParameter: "q"), 27 | SearchSource(host: "qwant.com", queryParameter: "q"), 28 | ] 29 | 30 | func kagiSearchURL(url: URL) -> URL? { 31 | if let host = url.host, 32 | let source = sources.first(where: { host.contains($0.host) }), 33 | let textQuery = URLComponents(string: url.absoluteString)?.percentEncodedQueryItems?.first(where: { $0.name == source.queryParameter })?.value, 34 | !self.shouldSkipRedirect(url: url) { 35 | return URL(string: "https://kagi.com/search?q=\(textQuery)") 36 | } 37 | return nil 38 | } 39 | 40 | // bangs handling 41 | func shouldSkipRedirect(url: URL) -> Bool { 42 | let host = url.host?.replacingOccurrences(of: "www.", with: "") 43 | 44 | func paramWithName(_ name: String) -> Any? { 45 | URLComponents(string: url.absoluteString)?.queryItems?.first(where: { $0.name == name })?.value 46 | } 47 | return [ 48 | host?.contains("google.") == true && paramWithName("client") == nil, 49 | host?.contains("google.") == true && ((paramWithName("sxsrf") as? String)?.isEmpty == false || (paramWithName("source") as? String)?.isEmpty == false), 50 | host?.contains("google.") == true && paramWithName("client") as? String == "internal-element-cse", 51 | 52 | host?.contains("bing.") == true && paramWithName("form") == nil, 53 | host?.contains("bing.") == true && (((paramWithName("cvid") as? String)?.isEmpty == false) || ((paramWithName("sc") as? String)?.isEmpty == false) || ((paramWithName("qs") as? String)?.isEmpty == false)), 54 | 55 | host?.contains("duckduckgo.") == true && paramWithName("t") == nil, 56 | host?.contains("duckduckgo.") == true && ((paramWithName("t") == nil) || ((paramWithName("t") as? String) == "h_")), 57 | 58 | host?.contains("search.yahoo.") == true && paramWithName("fr") == nil, 59 | host?.contains("search.yahoo.") == true && (paramWithName("fp") as? String == "1"), 60 | ].contains(true) 61 | } 62 | 63 | override func page(_ page: SFSafariPage, willNavigateTo url: URL?) { 64 | guard UserDefaults.standard.bool(forKey: SafariExtensionViewController.enableExtensionKey), 65 | let url = url, 66 | let kagiSearchURL = kagiSearchURL(url: url) else { 67 | return 68 | } 69 | page.getContainingTab { tab in 70 | tab.navigate(to: kagiSearchURL) 71 | } 72 | } 73 | 74 | // only called for domains specified in Info.plist 75 | override func additionalRequestHeaders(for url: URL, completionHandler: @escaping ([String : String]?) -> Void) { 76 | func privateSessionToken() -> String? { 77 | if let privateSessionLink = UserDefaults.standard.string(forKey: SafariExtensionViewController.sessionLinkKey), 78 | let url = URL(string: privateSessionLink.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!), 79 | let token = URLComponents(string: url.absoluteString)?.queryItems?.first(where: { $0.name == "token" })?.value { 80 | return token 81 | } 82 | return nil 83 | } 84 | 85 | if let token = privateSessionToken() { 86 | completionHandler(["Authorization": token]) 87 | } 88 | } 89 | 90 | override func validateToolbarItem(in window: SFSafariWindow, validationHandler: @escaping ((Bool, String) -> Void)) { 91 | validationHandler(true, "") 92 | } 93 | 94 | override func popoverViewController() -> SFSafariExtensionViewController { 95 | return SafariExtensionViewController.shared 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /safari/Legacy macOS/Kagi Search Extension/SafariExtensionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SafariExtensionViewController.swift 3 | // Kagi Search Extension 4 | // 5 | 6 | import SafariServices 7 | 8 | class SafariExtensionViewController: SFSafariExtensionViewController, NSTextFieldDelegate { 9 | 10 | @IBOutlet weak var enableExtensionCheckmark: NSButton! 11 | @IBOutlet weak var linkField: NSTextField! 12 | 13 | static let sessionLinkKey = "kagiSessionLink" 14 | static let enableExtensionKey = "enableKagiSearch" 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | 19 | linkField.delegate = self 20 | } 21 | 22 | override func viewWillAppear() { 23 | super.viewWillAppear() 24 | 25 | linkField.stringValue = UserDefaults.standard.string(forKey: Self.sessionLinkKey) ?? "" 26 | enableExtensionCheckmark.state = UserDefaults.standard.bool(forKey: Self.enableExtensionKey) ? .on : .off 27 | } 28 | 29 | @IBAction func enableExtensionValueChanged(_ sender: NSButton) { 30 | UserDefaults.standard.set(sender.state == .on, forKey: Self.enableExtensionKey) 31 | } 32 | 33 | func controlTextDidChange(_ obj: Notification) { 34 | UserDefaults.standard.set(linkField.stringValue, forKey: Self.sessionLinkKey) 35 | } 36 | 37 | @IBAction func getLinkTapped(_ sender: Any) { 38 | SFSafariApplication.getActiveWindow { window in 39 | window?.openTab( 40 | with: URL(string: "https://kagi.com/settings?p=user_details#sessionlink")!, 41 | makeActiveIfPossible: true 42 | ) 43 | } 44 | } 45 | 46 | static let shared: SafariExtensionViewController = { 47 | SafariExtensionViewController() 48 | }() 49 | } 50 | -------------------------------------------------------------------------------- /safari/Legacy macOS/Kagi Search.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /safari/Legacy macOS/Kagi Search/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Kagi Search 4 | // 5 | 6 | import Cocoa 7 | 8 | @main 9 | class AppDelegate: NSObject, NSApplicationDelegate { 10 | 11 | func applicationDidFinishLaunching(_ notification: Notification) { 12 | // Override point for customization after application launch. 13 | } 14 | 15 | func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 16 | return true 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /safari/Legacy macOS/Kagi Search/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon_16x16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "icon_16x16@2x.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "icon_32x32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "icon_32x32@2x.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "icon_128x128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "icon_128x128@2x.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "icon_256x256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "icon_256x256@2x.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "icon_512x512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "icon_512x512@2x.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /safari/Legacy macOS/Kagi Search/Assets.xcassets/AppIcon.appiconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy macOS/Kagi Search/Assets.xcassets/AppIcon.appiconset/icon_128x128.png -------------------------------------------------------------------------------- /safari/Legacy macOS/Kagi Search/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy macOS/Kagi Search/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /safari/Legacy macOS/Kagi Search/Assets.xcassets/AppIcon.appiconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy macOS/Kagi Search/Assets.xcassets/AppIcon.appiconset/icon_16x16.png -------------------------------------------------------------------------------- /safari/Legacy macOS/Kagi Search/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy macOS/Kagi Search/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /safari/Legacy macOS/Kagi Search/Assets.xcassets/AppIcon.appiconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy macOS/Kagi Search/Assets.xcassets/AppIcon.appiconset/icon_256x256.png -------------------------------------------------------------------------------- /safari/Legacy macOS/Kagi Search/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy macOS/Kagi Search/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /safari/Legacy macOS/Kagi Search/Assets.xcassets/AppIcon.appiconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy macOS/Kagi Search/Assets.xcassets/AppIcon.appiconset/icon_32x32.png -------------------------------------------------------------------------------- /safari/Legacy macOS/Kagi Search/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy macOS/Kagi Search/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /safari/Legacy macOS/Kagi Search/Assets.xcassets/AppIcon.appiconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy macOS/Kagi Search/Assets.xcassets/AppIcon.appiconset/icon_512x512.png -------------------------------------------------------------------------------- /safari/Legacy macOS/Kagi Search/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy macOS/Kagi Search/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /safari/Legacy macOS/Kagi Search/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /safari/Legacy macOS/Kagi Search/Base.lproj/Main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Kagi Search Icon 14 |

You can turn on Kagi Search’s extension in Safari Extensions preferences.

15 |

Kagi Search’s extension is currently on.

16 |

Kagi Search’s extension is currently off.

17 | 18 |

After enabling, click Kagi toolbar icon
and check "Make Kagi default search engine"

19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /safari/Legacy macOS/Kagi Search/Kagi Search.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | com.apple.security.network.client 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /safari/Legacy macOS/Kagi Search/Kagi_Search.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /safari/Legacy macOS/Kagi Search/Resources/Enable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy macOS/Kagi Search/Resources/Enable.png -------------------------------------------------------------------------------- /safari/Legacy macOS/Kagi Search/Resources/Enable_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy macOS/Kagi Search/Resources/Enable_dark.png -------------------------------------------------------------------------------- /safari/Legacy macOS/Kagi Search/Resources/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Legacy macOS/Kagi Search/Resources/Icon.png -------------------------------------------------------------------------------- /safari/Legacy macOS/Kagi Search/Resources/Script.js: -------------------------------------------------------------------------------- 1 | function show(enabled) { 2 | if (typeof enabled === "boolean") { 3 | document.body.classList.toggle(`state-on`, enabled); 4 | document.body.classList.toggle(`state-off`, !enabled); 5 | } else { 6 | document.body.classList.remove(`state-on`); 7 | document.body.classList.remove(`state-off`); 8 | } 9 | } 10 | 11 | function openPreferences() { 12 | webkit.messageHandlers.controller.postMessage("open-preferences"); 13 | } 14 | 15 | document.querySelector("button.open-preferences").addEventListener("click", openPreferences); 16 | 17 | function updateEnableImage(isDark) { 18 | if (isDark) { 19 | document.getElementById("enable_img").src = "../Enable_dark.png"; 20 | } else { 21 | document.getElementById("enable_img").src = "../Enable.png"; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /safari/Legacy macOS/Kagi Search/Resources/Style.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-user-select: none; 3 | -webkit-user-drag: none; 4 | cursor: default; 5 | } 6 | 7 | :root { 8 | color-scheme: light dark; 9 | 10 | --spacing: 0px; 11 | } 12 | 13 | html { 14 | height: 100%; 15 | } 16 | 17 | img.rounded { 18 | border-radius: 23px; 19 | } 20 | 21 | body { 22 | display: flex; 23 | align-items: center; 24 | justify-content: center; 25 | flex-direction: column; 26 | 27 | gap: var(--spacing); 28 | margin: 0 calc(var(--spacing) * 2); 29 | height: 100%; 30 | 31 | font: -apple-system-short-body; 32 | text-align: center; 33 | } 34 | 35 | body:not(.state-on, .state-off) :is(.state-on, .state-off) { 36 | display: none; 37 | } 38 | 39 | body.state-on :is(.state-off, .state-unknown) { 40 | display: none; 41 | } 42 | 43 | body.state-off :is(.state-on, .state-unknown) { 44 | display: none; 45 | } 46 | 47 | button { 48 | font-size: 1em; 49 | } 50 | -------------------------------------------------------------------------------- /safari/Legacy macOS/Kagi Search/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Kagi Search 4 | // 5 | 6 | import Cocoa 7 | import SafariServices 8 | import WebKit 9 | 10 | let extensionBundleIdentifier = "com.kagimacOS.Kagi-Search.Extension" 11 | 12 | extension Notification.Name { 13 | static let AppleInterfaceThemeChangedNotification = Notification.Name("AppleInterfaceThemeChangedNotification") 14 | } 15 | 16 | class ViewController: NSViewController, WKNavigationDelegate, WKScriptMessageHandler { 17 | 18 | enum InterfaceStyle: String { 19 | case Dark, Light 20 | 21 | init() { 22 | if #available(OSX 10.14, *) { 23 | self = NSApp.effectiveAppearance.name == .darkAqua ? .Dark : .Light 24 | 25 | } else { 26 | let type = UserDefaults.standard.string(forKey: "AppleInterfaceStyle") ?? "Light" 27 | self = InterfaceStyle(rawValue: type)! 28 | } 29 | } 30 | } 31 | 32 | @IBOutlet var webView: WKWebView! 33 | 34 | override func viewDidLoad() { 35 | super.viewDidLoad() 36 | 37 | self.webView.navigationDelegate = self 38 | self.webView.configuration.userContentController.add(self, name: "controller") 39 | self.webView.loadFileURL(Bundle.main.url(forResource: "Main", withExtension: "html")!, allowingReadAccessTo: Bundle.main.resourceURL!) 40 | 41 | DistributedNotificationCenter.default.addObserver( 42 | self, 43 | selector: #selector(interfaceModeChanged), 44 | name: .AppleInterfaceThemeChangedNotification, 45 | object: nil 46 | ) 47 | } 48 | 49 | @objc 50 | private func interfaceModeChanged() { 51 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { 52 | self.webView.evaluateJavaScript("updateEnableImage(\(InterfaceStyle() == .Dark));") 53 | } 54 | } 55 | 56 | func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { 57 | SFSafariExtensionManager.getStateOfSafariExtension(withIdentifier: extensionBundleIdentifier) { (state, error) in 58 | guard let state = state, error == nil else { 59 | return 60 | } 61 | 62 | DispatchQueue.main.async { 63 | webView.evaluateJavaScript(""" 64 | show(\(state.isEnabled)); 65 | updateEnableImage(\(InterfaceStyle() == .Dark)); 66 | """) 67 | } 68 | } 69 | } 70 | 71 | func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { 72 | if (message.body as! String != "open-preferences") { 73 | return; 74 | } 75 | 76 | SFSafariApplication.showPreferencesForExtension(withIdentifier: extensionBundleIdentifier) { _ in } 77 | } 78 | 79 | 80 | } 81 | -------------------------------------------------------------------------------- /safari/Legacy macOS/MainConfig.xcconfig: -------------------------------------------------------------------------------- 1 | MARKETING_VERSION = 2.1.0 2 | CURRENT_PROJECT_VERSION = 16 // this needs to be increased with each version change as well (not set to 1 when version is updated) 3 | -------------------------------------------------------------------------------- /safari/Legacy macOS/README.md: -------------------------------------------------------------------------------- 1 | # Kagi Search for Safari (macOS) 2 | 3 | ![Screenshot](https://is5-ssl.mzstatic.com/image/thumb/PurpleSource122/v4/28/1d/83/281d8352-7035-d44d-f662-79d1b1fb03e5/4bae250d-e314-47c1-b0ee-a981ec2f2291_Screen_Shot_2022-05-06_at_18.28.11.png/2880x1800bb.png) 4 | 5 | Minimum support macOS version: 10.14
6 | Language: Swift 7 | -------------------------------------------------------------------------------- /safari/README.md: -------------------------------------------------------------------------------- 1 | # Kagi Search for Safari 2 | 3 | > [!IMPORTANT] 4 | > The Universal extension is still under development. The current implementation uses background/service-worker scripts to redirect to Kagi. It has proven to be unreliable, with the background script at risk of being killed or not loading. -------------------------------------------------------------------------------- /safari/Universal/App Store Screenshots/Simulator Screenshot - iPad Pro (12.9-inch) (2nd generation) - 2023-12-21 at 22.18.59.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/App Store Screenshots/Simulator Screenshot - iPad Pro (12.9-inch) (2nd generation) - 2023-12-21 at 22.18.59.png -------------------------------------------------------------------------------- /safari/Universal/App Store Screenshots/Simulator Screenshot - iPad Pro (12.9-inch) (2nd generation) - 2023-12-21 at 22.20.03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/App Store Screenshots/Simulator Screenshot - iPad Pro (12.9-inch) (2nd generation) - 2023-12-21 at 22.20.03.png -------------------------------------------------------------------------------- /safari/Universal/App Store Screenshots/Simulator Screenshot - iPad Pro (12.9-inch) (6th generation) - 2023-12-21 at 22.12.47.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/App Store Screenshots/Simulator Screenshot - iPad Pro (12.9-inch) (6th generation) - 2023-12-21 at 22.12.47.png -------------------------------------------------------------------------------- /safari/Universal/App Store Screenshots/Simulator Screenshot - iPad Pro (12.9-inch) (6th generation) - 2023-12-21 at 22.13.39.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/App Store Screenshots/Simulator Screenshot - iPad Pro (12.9-inch) (6th generation) - 2023-12-21 at 22.13.39.png -------------------------------------------------------------------------------- /safari/Universal/App Store Screenshots/Simulator Screenshot - iPhone 8 Plus - 2023-12-21 at 22.04.02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/App Store Screenshots/Simulator Screenshot - iPhone 8 Plus - 2023-12-21 at 22.04.02.png -------------------------------------------------------------------------------- /safari/Universal/App Store Screenshots/Simulator Screenshot - iPhone 8 Plus - 2023-12-21 at 22.04.30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/App Store Screenshots/Simulator Screenshot - iPhone 8 Plus - 2023-12-21 at 22.04.30.png -------------------------------------------------------------------------------- /safari/Universal/App Store Screenshots/Simulator Screenshot - iPhone 8 Plus - 2023-12-21 at 22.06.02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/App Store Screenshots/Simulator Screenshot - iPhone 8 Plus - 2023-12-21 at 22.06.02.png -------------------------------------------------------------------------------- /safari/Universal/App Store Screenshots/iOS 5.5inch - 01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/App Store Screenshots/iOS 5.5inch - 01.png -------------------------------------------------------------------------------- /safari/Universal/App Store Screenshots/iOS 5.5inch - 02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/App Store Screenshots/iOS 5.5inch - 02.png -------------------------------------------------------------------------------- /safari/Universal/App Store Screenshots/iOS 6.7inch - 01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/App Store Screenshots/iOS 6.7inch - 01.png -------------------------------------------------------------------------------- /safari/Universal/App Store Screenshots/iOS 6.7inch - 02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/App Store Screenshots/iOS 6.7inch - 02.png -------------------------------------------------------------------------------- /safari/Universal/App Store Screenshots/iOS 6.7inch - 03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/App Store Screenshots/iOS 6.7inch - 03.png -------------------------------------------------------------------------------- /safari/Universal/App Store Screenshots/iOS 6.7inch - 04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/App Store Screenshots/iOS 6.7inch - 04.png -------------------------------------------------------------------------------- /safari/Universal/App Store Screenshots/iPad 2nd Gen - 01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/App Store Screenshots/iPad 2nd Gen - 01.png -------------------------------------------------------------------------------- /safari/Universal/App Store Screenshots/iPad 2nd Gen - 02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/App Store Screenshots/iPad 2nd Gen - 02.png -------------------------------------------------------------------------------- /safari/Universal/App Store Screenshots/iPad 2nd Gen - 03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/App Store Screenshots/iPad 2nd Gen - 03.png -------------------------------------------------------------------------------- /safari/Universal/App Store Screenshots/iPad 6th Gen - 01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/App Store Screenshots/iPad 6th Gen - 01.png -------------------------------------------------------------------------------- /safari/Universal/App Store Screenshots/iPad 6th Gen - 02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/App Store Screenshots/iPad 6th Gen - 02.png -------------------------------------------------------------------------------- /safari/Universal/App Store Screenshots/iPad 6th Gen - 03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/App Store Screenshots/iPad 6th Gen - 03.png -------------------------------------------------------------------------------- /safari/Universal/App Store Screenshots/macOS - 01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/App Store Screenshots/macOS - 01.png -------------------------------------------------------------------------------- /safari/Universal/Kagi Search (iOS)/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // iOS (App) 4 | // 5 | // Created by Nano Anderson on 10/17/23. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | var window: UIWindow? 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | // Override point for customization after application launch. 17 | return true 18 | } 19 | 20 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 21 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /safari/Universal/Kagi Search (iOS)/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /safari/Universal/Kagi Search (iOS)/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /safari/Universal/Kagi Search (iOS)/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleURLTypes 6 | 7 | 8 | CFBundleTypeRole 9 | Editor 10 | CFBundleURLName 11 | com.kagimacOS.Kagi-Search 12 | CFBundleURLSchemes 13 | 14 | kagisearch 15 | 16 | 17 | 18 | ITSAppUsesNonExemptEncryption 19 | 20 | UIApplicationSceneManifest 21 | 22 | UIApplicationSupportsMultipleScenes 23 | 24 | UISceneConfigurations 25 | 26 | UIWindowSceneSessionRoleApplication 27 | 28 | 29 | UISceneConfigurationName 30 | Default Configuration 31 | UISceneDelegateClassName 32 | $(PRODUCT_MODULE_NAME).SceneDelegate 33 | UISceneStoryboardFile 34 | Main 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /safari/Universal/Kagi Search (iOS)/Kagi Search (iOS).entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.application-groups 6 | 7 | group.kagi-search-for-safari 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /safari/Universal/Kagi Search (iOS)/Resources/Enable_Extension_Cropped_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/Kagi Search (iOS)/Resources/Enable_Extension_Cropped_dark.png -------------------------------------------------------------------------------- /safari/Universal/Kagi Search (iOS)/Resources/Enable_Extension_Cropped_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/Kagi Search (iOS)/Resources/Enable_Extension_Cropped_light.png -------------------------------------------------------------------------------- /safari/Universal/Kagi Search (iOS)/Resources/Grant_Permissions_Animation_dark copy.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/Kagi Search (iOS)/Resources/Grant_Permissions_Animation_dark copy.gif -------------------------------------------------------------------------------- /safari/Universal/Kagi Search (iOS)/Resources/Grant_Permissions_Animation_dark.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/Kagi Search (iOS)/Resources/Grant_Permissions_Animation_dark.gif -------------------------------------------------------------------------------- /safari/Universal/Kagi Search (iOS)/Resources/Grant_Permissions_Animation_light copy.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/Kagi Search (iOS)/Resources/Grant_Permissions_Animation_light copy.gif -------------------------------------------------------------------------------- /safari/Universal/Kagi Search (iOS)/Resources/Grant_Permissions_Animation_light.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/Kagi Search (iOS)/Resources/Grant_Permissions_Animation_light.gif -------------------------------------------------------------------------------- /safari/Universal/Kagi Search (iOS)/Resources/Grant_Permissions_Cropped.acorn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/Kagi Search (iOS)/Resources/Grant_Permissions_Cropped.acorn -------------------------------------------------------------------------------- /safari/Universal/Kagi Search (iOS)/Resources/Grant_Permissions_Cropped_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/Kagi Search (iOS)/Resources/Grant_Permissions_Cropped_dark.png -------------------------------------------------------------------------------- /safari/Universal/Kagi Search (iOS)/Resources/Grant_Permissions_Cropped_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/Kagi Search (iOS)/Resources/Grant_Permissions_Cropped_light.png -------------------------------------------------------------------------------- /safari/Universal/Kagi Search (iOS)/Resources/Private_Token.acorn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/Kagi Search (iOS)/Resources/Private_Token.acorn -------------------------------------------------------------------------------- /safari/Universal/Kagi Search (iOS)/Resources/Private_Token_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/Kagi Search (iOS)/Resources/Private_Token_dark.png -------------------------------------------------------------------------------- /safari/Universal/Kagi Search (iOS)/Resources/Private_Token_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/Kagi Search (iOS)/Resources/Private_Token_light.png -------------------------------------------------------------------------------- /safari/Universal/Kagi Search (iOS)/Resources/Settings_Animation_dark.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/Kagi Search (iOS)/Resources/Settings_Animation_dark.gif -------------------------------------------------------------------------------- /safari/Universal/Kagi Search (iOS)/Resources/Settings_Animation_light.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/Kagi Search (iOS)/Resources/Settings_Animation_light.gif -------------------------------------------------------------------------------- /safari/Universal/Kagi Search (iOS)/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // iOS (App) 4 | // 5 | // Created by Nano Anderson on 10/17/23. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 15 | guard let _ = (scene as? UIWindowScene) else { return } 16 | 17 | if let url = connectionOptions.urlContexts.first?.url { 18 | Deeplinks.handleIncomingURL(url) 19 | } 20 | } 21 | 22 | func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { 23 | if let url = URLContexts.first?.url { 24 | Deeplinks.handleIncomingURL(url) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /safari/Universal/Kagi Search (iOS)Tests/Kagi_Search__iOS_Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Kagi_Search__iOS_Tests.swift 3 | // Kagi Search (iOS)Tests 4 | // 5 | // Created by Nano Anderson on 11/21/23. 6 | // 7 | 8 | import XCTest 9 | @testable import Kagi_Search__iOS_ 10 | 11 | final class Kagi_Search__iOS_Tests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | // Any test you write for XCTest can be annotated as throws and async. 25 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. 26 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. 27 | } 28 | 29 | func testPerformanceExample() throws { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /safari/Universal/Kagi Search (iOS)UITests/Kagi_Search__iOS_UITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Kagi_Search__iOS_UITests.swift 3 | // Kagi Search (iOS)UITests 4 | // 5 | // Created by Nano Anderson on 11/21/23. 6 | // 7 | 8 | import XCTest 9 | 10 | final class Kagi_Search__iOS_UITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | func testExample() throws { 26 | // UI tests must launch the application that they test. 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | // Use XCTAssert and related functions to verify your tests produce the correct results. 31 | } 32 | 33 | func testLaunchPerformance() throws { 34 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { 35 | // This measures how long it takes to launch your application. 36 | measure(metrics: [XCTApplicationLaunchMetric()]) { 37 | XCUIApplication().launch() 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /safari/Universal/Kagi Search (iOS)UITests/Kagi_Search__iOS_UITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Kagi_Search__iOS_UITestsLaunchTests.swift 3 | // Kagi Search (iOS)UITests 4 | // 5 | // Created by Nano Anderson on 11/21/23. 6 | // 7 | 8 | import XCTest 9 | 10 | final class Kagi_Search__iOS_UITestsLaunchTests: XCTestCase { 11 | 12 | override class var runsForEachTargetApplicationUIConfiguration: Bool { 13 | true 14 | } 15 | 16 | override func setUpWithError() throws { 17 | continueAfterFailure = false 18 | } 19 | 20 | func testLaunch() throws { 21 | let app = XCUIApplication() 22 | app.launch() 23 | 24 | // Insert steps here to perform after app launch but before taking a screenshot, 25 | // such as logging into a test account or navigating somewhere in the app 26 | 27 | let attachment = XCTAttachment(screenshot: app.screenshot()) 28 | attachment.name = "Launch Screen" 29 | attachment.lifetime = .keepAlways 30 | add(attachment) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /safari/Universal/Kagi Search (macOS)/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // macOS (App) 4 | // 5 | // Created by Nano Anderson on 10/17/23. 6 | // 7 | 8 | import Cocoa 9 | 10 | @main 11 | class AppDelegate: NSObject, NSApplicationDelegate { 12 | 13 | var contentViewController: ViewController? { 14 | get { 15 | if let window = NSApplication.shared.mainWindow, 16 | let viewController = window.contentViewController as? ViewController { 17 | return viewController 18 | } 19 | return nil 20 | } 21 | } 22 | 23 | func applicationDidFinishLaunching(_ notification: Notification) { 24 | // Override point for customization after application launch. 25 | } 26 | 27 | func application(_ application: NSApplication, open urls: [URL]) { 28 | if let url = urls.first { 29 | Deeplinks.handleIncomingURL(url) 30 | } 31 | } 32 | 33 | func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 34 | return true 35 | } 36 | 37 | @IBAction func openHelpWebpage(_ sender: NSMenuItem?) { 38 | NSWorkspace.shared.open(URL(string:"https://kagi.com")!) 39 | } 40 | 41 | @IBAction func syncWithSafari(_ sender: NSMenuItem?) { 42 | contentViewController?.syncDataToSafari(sender) 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /safari/Universal/Kagi Search (macOS)/Kagi Search (macOS).entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.application-groups 8 | 9 | group.kagi-search-for-safari 10 | 11 | com.apple.security.files.user-selected.read-only 12 | 13 | com.apple.security.network.client 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /safari/Universal/Kagi Search (macOS)/Kagi-Search-macOS-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleURLTypes 6 | 7 | 8 | CFBundleTypeRole 9 | Editor 10 | CFBundleURLName 11 | com.kagimacOS.Kagi-Search 12 | CFBundleURLSchemes 13 | 14 | kagisearch 15 | 16 | 17 | 18 | ITSAppUsesNonExemptEncryption 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /safari/Universal/Kagi Search (macOS)/NSImage+Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSImage+Helpers.swift 3 | // Kagi Search for Safari (macOS) 4 | // 5 | // Created by Nano Anderson on 11/7/23. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | 11 | extension NSImage { 12 | func pngData() -> Data? { 13 | guard let localCGImage = cgImage(forProposedRect: nil, context: nil, hints: nil) else { 14 | return nil 15 | } 16 | let imageRep = NSBitmapImageRep(cgImage: localCGImage) 17 | imageRep.size = size 18 | return imageRep.representation(using: .png, properties: [:]) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /safari/Universal/Kagi Search (macOS)/NSWindow+Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSWindow+Helpers.swift 3 | // Kagi Search for Safari (macOS) 4 | // 5 | // Created by Nano Anderson on 11/1/23. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | 11 | extension NSWindow { 12 | var titlebarHeight: CGFloat { 13 | frame.height - contentRect(forFrameRect: frame).height 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /safari/Universal/Kagi Search (macOS)/Screenshots/Permissions Dark.acorn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/Kagi Search (macOS)/Screenshots/Permissions Dark.acorn -------------------------------------------------------------------------------- /safari/Universal/Kagi Search (macOS)/Screenshots/Permissions Dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/Kagi Search (macOS)/Screenshots/Permissions Dark.png -------------------------------------------------------------------------------- /safari/Universal/Kagi Search (macOS)/Screenshots/Permissions Light.acorn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/Kagi Search (macOS)/Screenshots/Permissions Light.acorn -------------------------------------------------------------------------------- /safari/Universal/Kagi Search (macOS)/Screenshots/Permissions Light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/Kagi Search (macOS)/Screenshots/Permissions Light.png -------------------------------------------------------------------------------- /safari/Universal/Kagi Search (macOS)/macOS Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /safari/Universal/Kagi Search (macOS)/macOS Assets.xcassets/PermissionsPopup.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Permissions Light.png", 5 | "idiom" : "universal" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "dark" 12 | } 13 | ], 14 | "filename" : "Permissions Dark.png", 15 | "idiom" : "universal" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /safari/Universal/Kagi Search (macOS)/macOS Assets.xcassets/PermissionsPopup.imageset/Permissions Dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/Kagi Search (macOS)/macOS Assets.xcassets/PermissionsPopup.imageset/Permissions Dark.png -------------------------------------------------------------------------------- /safari/Universal/Kagi Search (macOS)/macOS Assets.xcassets/PermissionsPopup.imageset/Permissions Light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/Kagi Search (macOS)/macOS Assets.xcassets/PermissionsPopup.imageset/Permissions Light.png -------------------------------------------------------------------------------- /safari/Universal/Kagi Search Extension iOS copy-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionPointIdentifier 8 | com.apple.Safari.web-extension 9 | NSExtensionPrincipalClass 10 | $(PRODUCT_MODULE_NAME).SafariWebExtensionHandler 11 | SFSafariAppExtensionBundleIdentifiersToReplace 12 | 13 | com.kagi.searchExtensionContainer.extension 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /safari/Universal/Kagi Search iOS copy-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleURLTypes 6 | 7 | 8 | CFBundleTypeRole 9 | Editor 10 | CFBundleURLName 11 | com.kagi.searchExtensionContainer 12 | CFBundleURLSchemes 13 | 14 | kagisearch 15 | 16 | 17 | 18 | ITSAppUsesNonExemptEncryption 19 | 20 | UIApplicationSceneManifest 21 | 22 | UIApplicationSupportsMultipleScenes 23 | 24 | UISceneConfigurations 25 | 26 | UIWindowSceneSessionRoleApplication 27 | 28 | 29 | UISceneConfigurationName 30 | Default Configuration 31 | UISceneDelegateClassName 32 | $(PRODUCT_MODULE_NAME).SceneDelegate 33 | UISceneStoryboardFile 34 | Main 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /safari/Universal/Kagi Search.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /safari/Universal/Kagi Search.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /safari/Universal/Kagi Search.xcodeproj/xcshareddata/xcschemes/Kagi Search iOS (Legacy).xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 6 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /safari/Universal/Kagi Search.xcodeproj/xcshareddata/xcschemes/Kagi Search iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 35 | 41 | 42 | 43 | 46 | 52 | 53 | 54 | 55 | 56 | 66 | 68 | 74 | 75 | 76 | 77 | 83 | 85 | 91 | 92 | 93 | 94 | 96 | 97 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /safari/Universal/MainConfig.xcconfig: -------------------------------------------------------------------------------- 1 | MARKETING_VERSION = 2.2.7 2 | CURRENT_PROJECT_VERSION = 38 // this needs to be increased with each version change as well (not set to 1 when version is updated) 3 | PRODUCT_NAME = Kagi for Safari 4 | -------------------------------------------------------------------------------- /safari/Universal/README.md: -------------------------------------------------------------------------------- 1 | # Kagi Search for Safari (macOS) 2 | 3 | ![Screenshot](https://is5-ssl.mzstatic.com/image/thumb/PurpleSource122/v4/28/1d/83/281d8352-7035-d44d-f662-79d1b1fb03e5/4bae250d-e314-47c1-b0ee-a981ec2f2291_Screen_Shot_2022-05-06_at_18.28.11.png/2880x1800bb.png) 4 | 5 | Minimum support macOS version: 10.14
6 | Language: Swift 7 | -------------------------------------------------------------------------------- /safari/Universal/Shared (App)/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "25", 9 | "green" : "175", 10 | "red" : "255" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /safari/Universal/Shared (App)/Assets.xcassets/AppBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xF2", 9 | "green" : "0xF6", 10 | "red" : "0xF7" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "55", 27 | "green" : "40", 28 | "red" : "38" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /safari/Universal/Shared (App)/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/Shared (App)/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /safari/Universal/Shared (App)/Assets.xcassets/AppIcon.appiconset/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/Shared (App)/Assets.xcassets/AppIcon.appiconset/1024x1024.png -------------------------------------------------------------------------------- /safari/Universal/Shared (App)/Assets.xcassets/AppIcon.appiconset/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/Shared (App)/Assets.xcassets/AppIcon.appiconset/128.png -------------------------------------------------------------------------------- /safari/Universal/Shared (App)/Assets.xcassets/AppIcon.appiconset/256 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/Shared (App)/Assets.xcassets/AppIcon.appiconset/256 1.png -------------------------------------------------------------------------------- /safari/Universal/Shared (App)/Assets.xcassets/AppIcon.appiconset/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/Shared (App)/Assets.xcassets/AppIcon.appiconset/256.png -------------------------------------------------------------------------------- /safari/Universal/Shared (App)/Assets.xcassets/AppIcon.appiconset/32 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/Shared (App)/Assets.xcassets/AppIcon.appiconset/32 1.png -------------------------------------------------------------------------------- /safari/Universal/Shared (App)/Assets.xcassets/AppIcon.appiconset/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/Shared (App)/Assets.xcassets/AppIcon.appiconset/32.png -------------------------------------------------------------------------------- /safari/Universal/Shared (App)/Assets.xcassets/AppIcon.appiconset/512 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/Shared (App)/Assets.xcassets/AppIcon.appiconset/512 1.png -------------------------------------------------------------------------------- /safari/Universal/Shared (App)/Assets.xcassets/AppIcon.appiconset/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/Shared (App)/Assets.xcassets/AppIcon.appiconset/512.png -------------------------------------------------------------------------------- /safari/Universal/Shared (App)/Assets.xcassets/AppIcon.appiconset/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/Shared (App)/Assets.xcassets/AppIcon.appiconset/64.png -------------------------------------------------------------------------------- /safari/Universal/Shared (App)/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "1024x1024.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | }, 9 | { 10 | "filename" : "macOS-16.png", 11 | "idiom" : "mac", 12 | "scale" : "1x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "32 1.png", 17 | "idiom" : "mac", 18 | "scale" : "2x", 19 | "size" : "16x16" 20 | }, 21 | { 22 | "filename" : "32.png", 23 | "idiom" : "mac", 24 | "scale" : "1x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "64.png", 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "32x32" 32 | }, 33 | { 34 | "filename" : "128.png", 35 | "idiom" : "mac", 36 | "scale" : "1x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "256 1.png", 41 | "idiom" : "mac", 42 | "scale" : "2x", 43 | "size" : "128x128" 44 | }, 45 | { 46 | "filename" : "256.png", 47 | "idiom" : "mac", 48 | "scale" : "1x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "512 1.png", 53 | "idiom" : "mac", 54 | "scale" : "2x", 55 | "size" : "256x256" 56 | }, 57 | { 58 | "filename" : "512.png", 59 | "idiom" : "mac", 60 | "scale" : "1x", 61 | "size" : "512x512" 62 | }, 63 | { 64 | "filename" : "1024.png", 65 | "idiom" : "mac", 66 | "scale" : "2x", 67 | "size" : "512x512" 68 | } 69 | ], 70 | "info" : { 71 | "author" : "xcode", 72 | "version" : 1 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /safari/Universal/Shared (App)/Assets.xcassets/AppIcon.appiconset/macOS-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/Shared (App)/Assets.xcassets/AppIcon.appiconset/macOS-16.png -------------------------------------------------------------------------------- /safari/Universal/Shared (App)/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /safari/Universal/Shared (App)/Assets.xcassets/LargeIcon.imageset/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/Shared (App)/Assets.xcassets/LargeIcon.imageset/512.png -------------------------------------------------------------------------------- /safari/Universal/Shared (App)/Assets.xcassets/LargeIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "512.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /safari/Universal/Shared (App)/Assets.xcassets/TitlebarPermissionEnableScreenshot.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "TitlebarSmaller.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /safari/Universal/Shared (App)/Assets.xcassets/TitlebarPermissionEnableScreenshot.imageset/TitlebarSmaller.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/Shared (App)/Assets.xcassets/TitlebarPermissionEnableScreenshot.imageset/TitlebarSmaller.png -------------------------------------------------------------------------------- /safari/Universal/Shared (App)/Assets.xcassets/ToolbarItemIcon.imageset/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/Shared (App)/Assets.xcassets/ToolbarItemIcon.imageset/128.png -------------------------------------------------------------------------------- /safari/Universal/Shared (App)/Assets.xcassets/ToolbarItemIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "128.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /safari/Universal/Shared (App)/Base.lproj/Main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | Kagi Search for Safari Icon 15 |

Instructions

16 |
    17 |
  1. 18 | Enable extension in Safari Extension Settings, and Allow permissions for "kagi.com"Safari Extension Settings. 19 | 20 |
    21 | 22 |
    23 |
  2. 24 |
25 | 26 | 27 |
    28 |
  1. After your first search, click the Kagi extension icon in Safari's toolbar and select "Always Allow on This Website". 29 | 30 |
  2. 31 |
32 | 33 | 34 |
    35 |
  1. 36 |

    Optional: Select the search engine you want to redirect to Kagi in the extension popup.

    37 |
  2. 38 |
  3. 39 | To enable Kagi in Private Browsing, check the "Allow in Private Browsing" checkboxenable the "Private Browsing" toggle switch. Then copy your Kagi private session link from your Kagi account settings into the extension popup. 40 | 41 |
  4. 42 |
  5. This completes the setup. You can now search with Kagi.
  6. 43 |
44 |
45 |

Sorry that Apple made this so complicated. We even started building an entire WebKit browser called Orion to make it easier to use Kagi on Mac. We’d appreciate if you gave it a try.

46 |
47 | 48 | 49 | -------------------------------------------------------------------------------- /safari/Universal/Shared (App)/Deeplinks.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Deeplinks.swift 3 | // Kagi Search 4 | // 5 | // Created by Nano Anderson on 1/24/24. 6 | // 7 | 8 | import Foundation 9 | #if os(macOS) 10 | import SafariServices 11 | #elseif os(iOS) 12 | import UIKit 13 | #endif 14 | 15 | struct Deeplinks { 16 | 17 | static func handleIncomingURL(_ url: URL) { 18 | guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { 19 | return 20 | } 21 | 22 | if url.scheme == "kagisearch", 23 | components.host == "open-app", 24 | let appDestinationItem = components.queryItems?.first(where: { $0.name.lowercased() == "destination" }), 25 | let appDestinationValue = appDestinationItem.value { 26 | let appOpenURL: URL? 27 | switch appDestinationValue { 28 | case "safari-extension-settings": 29 | appOpenURL = URL(string: "App-Prefs:SAFARI&path=WEB_EXTENSIONS") 30 | break 31 | case "safari-extension-settings-deep": 32 | appOpenURL = URL(string: "App-Prefs:SAFARI&path=WEB_EXTENSIONS/Kagi%20for%20Safari") 33 | break 34 | default: 35 | appOpenURL = nil 36 | break 37 | } 38 | if let appOpenURL = appOpenURL { 39 | #if os(iOS) 40 | UIApplication.shared.open(appOpenURL) 41 | #elseif os(macOS) 42 | SFSafariApplication.showPreferencesForExtension(withIdentifier: macExtensionBundleIdentifier) { error in 43 | guard error == nil else { return } 44 | } 45 | #endif 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /safari/Universal/Shared (App)/Resources/ScreenshotIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /safari/Universal/Shared (App)/Resources/Script.js: -------------------------------------------------------------------------------- 1 | function show(platform, enabled, useSettingsInsteadOfPreferences) { 2 | document.body.classList.add(`platform-${platform}`); 3 | 4 | // if (useSettingsInsteadOfPreferences) { 5 | // document.getElementsByClassName('platform-mac state-on')[0].innerText = "Kagi Search for Safari’s extension is currently on. You can turn it off in the Extensions section of Safari Settings."; 6 | // document.getElementsByClassName('platform-mac state-off')[0].innerText = "Kagi Search for Safari’s extension is currently off. You can turn it on in the Extensions section of Safari Settings."; 7 | // document.getElementsByClassName('platform-mac state-unknown')[0].innerText = "You can turn on Kagi Search for Safari’s extension in the Extensions section of Safari Settings."; 8 | // document.getElementsByClassName('platform-mac open-preferences')[0].innerText = "Quit and Open Safari Settings…"; 9 | // } 10 | 11 | if (typeof enabled === "boolean") { 12 | document.body.classList.toggle(`state-on`, enabled); 13 | document.body.classList.toggle(`state-off`, !enabled); 14 | } else { 15 | document.body.classList.remove(`state-on`); 16 | document.body.classList.remove(`state-off`); 17 | } 18 | } 19 | 20 | function addEngineListener() { 21 | const engineSelect = document.getElementById('engine-select'); 22 | engineSelect.addEventListener('change', () => { 23 | const index = engineSelect.selectedIndex; 24 | const newEngine = engineSelect.options[index].value; 25 | updateUIForEngine(newEngine); 26 | webkit.messageHandlers.controller.postMessage({"action": "engine-changed", "newEngine": newEngine}); 27 | }); 28 | } 29 | 30 | function addPrivateSessionLinkListener() { 31 | const privateSessionInput = document.getElementById('private-session-link'); 32 | privateSessionInput.addEventListener('change', () => { 33 | const newLink = privateSessionInput.value; 34 | webkit.messageHandlers.controller.postMessage({"action": "private-session-link-changed", "newLink": newLink}); 35 | }); 36 | } 37 | 38 | function updateUIForEngine(newEngine) { 39 | if (newEngine == "All") { 40 | document.querySelector('#engines').classList.toggle(`allEngines`, true); 41 | } else { 42 | document.querySelector('#engines').classList.toggle(`allEngines`, false); 43 | } 44 | } 45 | 46 | function selectCurrentEngine(currentEngine) { 47 | const engineSelect = document.getElementById('engine-select'); 48 | engineSelect.selectedIndex = Array.from(engineSelect.options).indexOf(engineSelect.namedItem(currentEngine)); 49 | updateUIForEngine(currentEngine); 50 | } 51 | 52 | function setCurrentPrivateSessionLink(currentLink) { 53 | const privateSessionInput = document.getElementById('private-session-link'); 54 | privateSessionInput.value = currentLink; 55 | } 56 | 57 | function openPreferences() { 58 | webkit.messageHandlers.controller.postMessage({"action": "open-preferences"}); 59 | } 60 | 61 | function syncWithSafari() { 62 | webkit.messageHandlers.controller.postMessage({"action": "sync-with-safari"}); 63 | document.querySelector("button.sync-with-safari").innerText = "Syncing…"; 64 | } 65 | 66 | function cachePrescreenshotSize() { 67 | webkit.messageHandlers.controller.postMessage({ 68 | "action": "cache-prescreenshot-size" 69 | }); 70 | } 71 | 72 | function updateWindowSizeToCachedSize() { 73 | webkit.messageHandlers.controller.postMessage({ 74 | "action": "update-window-size", 75 | "useCachedSize": true 76 | }); 77 | } 78 | 79 | function updateWindowSizeToMatchWebviewContentSize() { 80 | var newWidth = 0 81 | let lightbox = document.querySelector(".basicLightbox"); 82 | if (lightbox != null && !lightbox.classList.contains("closing")) { 83 | newWidth = Math.max(800, lightbox.getBoundingClientRect().width); 84 | } 85 | webkit.messageHandlers.controller.postMessage({ 86 | "action": "update-window-size", 87 | "newHeight": document.getElementById("wrapper").getBoundingClientRect().height, 88 | "newWidth": newWidth 89 | }); 90 | } 91 | 92 | // Returns true if an external link or screenshot link is detected. Prevents in-app webview from opening links. 93 | function handleLinkElement(el) { 94 | if (el.href.startsWith("http")) { 95 | webkit.messageHandlers.controller.postMessage({"action": "open-external-link", "url": el.href }); 96 | return true 97 | } else if (el.classList.contains("screenshot")) { 98 | let screenshotHref = el.href; 99 | cachePrescreenshotSize(); 100 | var imageHTML = ''; 101 | if (el.classList.contains("multi-image")) { 102 | let images = el.getAttribute("data-images").split(","); 103 | let imageWidth = 300; 104 | let containerWidth = (images.length * (imageWidth + 12)) // 300px width, ~12px buffer between each 105 | imageHTML = ''; 110 | } 111 | basicLightbox.create(imageHTML, { 112 | onClose: (instance) => { 113 | instance.element().classList.add("closing"); 114 | document.body.classList.toggle(`lightbox-open`, false); 115 | webkit.messageHandlers.controller.postMessage({"action": "toggle-statusbar", "isVisible": true }); 116 | updateWindowSizeToCachedSize(); 117 | }, 118 | onShow: (instance) => { 119 | document.body.classList.toggle(`lightbox-open`, true); 120 | webkit.messageHandlers.controller.postMessage({"action": "toggle-statusbar", "isVisible": false }); 121 | } 122 | }).show(); 123 | setTimeout(function(){ 124 | updateWindowSizeToMatchWebviewContentSize(); 125 | }, 50); 126 | return true 127 | } 128 | return false 129 | } 130 | 131 | document.querySelectorAll("button.open-preferences, a.open-preferences").forEach(function(el){el.addEventListener("click", openPreferences);}); 132 | //document.querySelector("button.sync-with-safari").addEventListener("click", syncWithSafari); 133 | 134 | document.onclick = function (e) { 135 | e = e || window.event; 136 | var element = e.target || e.srcElement; 137 | 138 | if (element.tagName == 'A') { 139 | if (handleLinkElement(element)) { return false; } 140 | } else if (element.parentElement.tagName == 'A') { 141 | if (handleLinkElement(element.parentElement)) { return false; } 142 | } 143 | }; 144 | -------------------------------------------------------------------------------- /safari/Universal/Shared (App)/Resources/Style.css: -------------------------------------------------------------------------------- 1 | * { 2 | /* -webkit-user-select: none;*/ 3 | -webkit-user-drag: none; 4 | cursor: default; 5 | } 6 | a[href] { 7 | cursor: pointer; 8 | } 9 | 10 | :root { 11 | color-scheme: light dark; 12 | 13 | --spacing: 10px; 14 | 15 | --captionColorLight: #494949; 16 | --captionColorDark: #B8B8B8; 17 | 18 | --kagiColorCreamLighter: #F7F6F2; 19 | --kagiColorPurpleDarkmode: rgba(38,40,55, 1); 20 | 21 | --orionAccent: #8E70FF; 22 | } 23 | 24 | html { 25 | height: 100%; 26 | } 27 | html, body { 28 | background: var(--kagiColorCreamLighter); 29 | } 30 | 31 | #wrapper { 32 | display: flex; 33 | align-items: center; 34 | justify-content: center; 35 | flex-direction: column; 36 | 37 | gap: var(--spacing); 38 | margin: 0 calc(var(--spacing) * 2); 39 | height: 100%; 40 | 41 | font: -apple-system-short-body; 42 | text-align: left; 43 | } 44 | 45 | body:not(.platform-mac, .platform-ios) :is(.platform-mac, .platform-ios) { 46 | display: none; 47 | } 48 | 49 | body.platform-ios .platform-mac { 50 | display: none; 51 | } 52 | 53 | body.platform-mac .platform-ios { 54 | display: none; 55 | } 56 | 57 | body.platform-ios .platform-mac { 58 | display: none; 59 | } 60 | 61 | body:not(.state-on, .state-off) :is(.state-on, .state-off) { 62 | display: none; 63 | } 64 | 65 | body.state-on :is(.state-off, .state-unknown) { 66 | display: none; 67 | } 68 | 69 | body.state-off :is(.state-on, .state-unknown) { 70 | display: none; 71 | } 72 | 73 | button, input, select, option { 74 | font-size: 1em; 75 | } 76 | 77 | input, select, p, label, button { 78 | width: 95%; 79 | } 80 | .sync-with-safari { 81 | width: 85%; 82 | margin-left: auto; 83 | margin-right: auto; 84 | display: block; 85 | } 86 | 87 | button.sync-with-safari { 88 | font-weight: bold; 89 | } 90 | p.sync-with-safari { 91 | margin-top: 4px; 92 | text-align: center; 93 | } 94 | 95 | p, label { 96 | display: block; 97 | margin: .5em 0; 98 | } 99 | 100 | fieldset, ol, h1, h2, h3, h4, h5, h6, #wrapper > p { 101 | width: 82%; 102 | max-width: 400px; 103 | } 104 | fieldset { 105 | border: 0; 106 | } 107 | 108 | body.platform-ios fieldset { 109 | width: 95%; 110 | } 111 | 112 | body.platform-ios select { 113 | /* height: 20px;*/ 114 | } 115 | 116 | body.platform-ios select { 117 | height: 38px; 118 | margin-bottom: 8px; 119 | } 120 | 121 | input { 122 | border: solid 1px #aaa; 123 | border-radius: 2px; 124 | } 125 | 126 | body.platform-ios input { 127 | border-radius: 6px; 128 | } 129 | 130 | body.platform-ios input, body.platform-ios select { 131 | margin-bottom: 8px; 132 | } 133 | 134 | body.platform-mac select { 135 | -webkit-appearance: menulist-button; 136 | } 137 | 138 | a { 139 | color: var(--orionAccent); 140 | } 141 | 142 | a.screenshot { 143 | overflow: visible; 144 | opacity: .7; 145 | /* transition: opacity 1s ease-in-out;*/ 146 | display: inline-block; 147 | width: 16px; 148 | height: 14px; 149 | position: relative; 150 | top: 2px; 151 | /* margin-bottom: -2px;*/ 152 | } 153 | a.screenshot:hover { 154 | opacity: 1; 155 | } 156 | 157 | a.screenshot > object { 158 | pointer-events: none; 159 | overflow: visible; 160 | width: 16px; 161 | height: 14px; 162 | /* fill-opacity: .7;*/ 163 | /* transition: fill-opacity 00.5s ease-in-out;*/ 164 | } 165 | a.screenshot:hover > object { 166 | /* fill-opacity: 1;*/ 167 | } 168 | 169 | .instruction-animation { 170 | display: block; 171 | width: 70vw; 172 | height: auto; 173 | max-width: 450px; /* this is slightly larger than the widest iPhone in portrait mode. we want 100% width in portrait on iPhone, otherwise this maximum keeps things looking good. */ 174 | border: solid 2px #ccc; 175 | } 176 | 177 | hr { 178 | border: solid 0.5px var(--captionColorDark); 179 | width: 70%; 180 | } 181 | 182 | .caption { 183 | font-size: 0.9em; 184 | color: var(--captionColorLight); 185 | margin-top: 0; 186 | } 187 | h2 { 188 | margin-bottom: 0; 189 | } 190 | .instructions { 191 | padding-inline-start: 0; 192 | margin: 0 auto; 193 | } 194 | .instructions li:not(:first-child) { 195 | margin-top: var(--spacing); /* this margin makes up for not getting the gap applied like the children of #wrapper do */ 196 | } 197 | .instructions li::marker { 198 | color: var(--captionColorLight); 199 | } 200 | 201 | @media (prefers-color-scheme: light) { 202 | .darkmode { display: none; } 203 | } 204 | @media (prefers-color-scheme: dark) { 205 | html, body { 206 | background: var(--kagiColorPurpleDarkmode); 207 | } 208 | #wrapper { 209 | font-weight: 300; 210 | } 211 | .caption, .instructions li::marker { 212 | color: var(--captionColorDark); 213 | } 214 | hr { 215 | border-color: var(--captionColorLight); 216 | } 217 | .lightmode { display: none; } 218 | .instruction-animation { 219 | border-color: #555; 220 | } 221 | } 222 | 223 | .info { 224 | text-indent: -19px; 225 | /* background: url('data:image/svg+xml;utf8,'); */ 226 | } 227 | .bi-info-circle { 228 | position: relative; 229 | top: 7px; 230 | right: 6px; 231 | opacity: 0.5; 232 | } 233 | .caption:hover .bi-info-circle { 234 | opacity: 1; 235 | } 236 | .inlineIcon { 237 | -webkit-filter: grayscale(1); 238 | position: relative; 239 | top: 3px; 240 | } 241 | #engines p.allEngines { 242 | display: none; 243 | } 244 | #engines.allEngines p.allEngines { 245 | display: block; 246 | } 247 | 248 | /* basicLightbox styles, minified */ 249 | .basicLightbox.closing {width:100%;} 250 | .platform-ios .basicLightbox {background:rgba(255,255,255,.8);} 251 | .platform-mac .basicLightbox {background:rgba(0,0,0,.8);} 252 | .basicLightbox{position:fixed;display:flex;justify-content:center;align-items:center;top:0;left:0;width:100%;height:100vh;opacity:.01; 253 | transition:opacity 0.5s ease; 254 | z-index:1000;will-change:opacity} 255 | .basicLightbox--visible{opacity:1} 256 | .basicLightbox__placeholder{max-width:100%;-webkit-transform:scale(.9);transform:scale(.9); 257 | transition:transform 0.5s ease; 258 | z-index:1;will-change:transform} 259 | .basicLightbox__placeholder>iframe:first-child:last-child,.basicLightbox__placeholder>img:first-child:last-child,.basicLightbox__placeholder>video:first-child:last-child{display:block;position:absolute;top:0;right:0;bottom:0;left:0;margin:auto;max-width:95%;max-height:90vh}.basicLightbox__placeholder>iframe:first-child:last-child,.basicLightbox__placeholder>video:first-child:last-child{pointer-events:auto}.basicLightbox__placeholder>img:first-child:last-child,.basicLightbox__placeholder>video:first-child:last-child{width:auto;height:auto}.basicLightbox--iframe .basicLightbox__placeholder,.basicLightbox--img .basicLightbox__placeholder,.basicLightbox--video .basicLightbox__placeholder{width:100%;height:100%;pointer-events:none}.basicLightbox--visible .basicLightbox__placeholder{-webkit-transform:scale(1);transform:scale(1);overflow-x:scroll;}.basicLightbox--visible .basicLightbox__placeholder::-webkit-scrollbar{display: none;} 260 | .platform-ios .modal img { 261 | display: inline-block; 262 | } 263 | .platform-ios .basicLightbox img { 264 | box-shadow: 0 0 15px gray; 265 | margin: 20px 6px; 266 | max-height: 90vh; 267 | } 268 | .platform-ios .basicLightbox__placeholder>iframe:first-child:last-child, .platform-ios .basicLightbox__placeholder>img:first-child:last-child, .platform-ios .basicLightbox__placeholder>video:first-child:last-child { 269 | margin: 20px 6px; 270 | } 271 | @media (prefers-color-scheme: dark) { 272 | .platform-ios .basicLightbox { 273 | background: rgba(0,0,0,.8); 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /safari/Universal/Shared (App)/Resources/allow_private_browsing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/Shared (App)/Resources/allow_private_browsing.png -------------------------------------------------------------------------------- /safari/Universal/Shared (App)/Resources/basicLightbox.min.js: -------------------------------------------------------------------------------- 1 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).basicLightbox=e()}}(function(){return function i(c,u,a){function s(n,e){if(!u[n]){if(!c[n]){var t="function"==typeof require&&require;if(!e&&t)return t(n,!0);if(l)return l(n,!0);var o=new Error("Cannot find module '"+n+"'");throw o.code="MODULE_NOT_FOUND",o}var r=u[n]={exports:{}};c[n][0].call(r.exports,function(e){return s(c[n][1][e]||e)},r,r.exports,i,c,u,a)}return u[n].exports}for(var l="function"==typeof require&&require,e=0;e\n\t\t\t\n\t\t\n\t')),o=t.querySelector(".basicLightbox__placeholder");e.forEach(function(e){return o.appendChild(e)});var r=a(o,"IMG"),i=a(o,"VIDEO"),c=a(o,"IFRAME");return!0===r&&t.classList.add("basicLightbox--img"),!0===i&&t.classList.add("basicLightbox--video"),!0===c&&t.classList.add("basicLightbox--iframe"),t}(e=function(e){var n="string"==typeof e,t=e instanceof HTMLElement==1;if(!1===n&&!1===t)throw new Error("Content must be a DOM element/node or string");return!0===n?Array.from(u(e,!0)):"TEMPLATE"===e.tagName?[e.content.cloneNode(!0)]:Array.from(e.children)}(e),o=function(){var e=0 SearchSource? { 33 | return Self.sources.first(where: { $0.name == engineName }) 34 | } 35 | 36 | static func withIdentifier(_ identifier: String) -> SearchSource? { 37 | return Self.sources.first(where: { $0.systemIdentifier == identifier }) 38 | } 39 | } 40 | 41 | /// Helper for UserDefaults preference storage. 42 | /// 43 | /// All preferences are stored as a `[ProfileUUIDString: Object]` Dictionary. 44 | class Preferences: NSObject { 45 | 46 | static let shared = Preferences() 47 | 48 | let defaults: UserDefaults? 49 | static private let NoProfileUUID = "NoProfileUUID" 50 | 51 | enum Keys: String, CaseIterable { 52 | case engine 53 | case privateSessionLink 54 | case checkedIfUpgradedFromLegacyExtension 55 | case didUpgradeFromLegacyExtension 56 | 57 | static var allRawValues = allCases.map({ $0.rawValue }) 58 | var cfNotificationNameString: String { 59 | "com.kagimacOS.Kagi-Search.PreferenceUpdateNotification.\(rawValue)" 60 | } 61 | var cfNotificationName: CFNotificationName { 62 | CFNotificationName(cfNotificationNameString as NSString) 63 | } 64 | } 65 | static let PreferenceUpdatedNotificationKey = "PreferenceUpdatedNotificationKey" 66 | 67 | override init() { 68 | defaults = UserDefaults(suiteName: "group.kagi-search-for-safari") 69 | super.init() 70 | } 71 | 72 | func setEngine(_ engine: SearchSource, profile: UUID?) { 73 | var engines = defaults?.dictionary(forKey: Keys.engine.rawValue) as? [String: String] ?? [:] 74 | engines[uuidKey(for: profile)] = engine.name 75 | defaults?.set(engines, forKey: Keys.engine.rawValue) 76 | } 77 | 78 | /// Default engine is Safari's default (if it can be detected), or Google 79 | func engine(for profile: UUID?) -> SearchSource? { 80 | if let engines = defaults?.dictionary(forKey: Keys.engine.rawValue) as? [String: String], 81 | let engineName = engines[uuidKey(for: profile)], 82 | let engine = SearchSource.named(engineName) { 83 | return engine 84 | } 85 | 86 | // Check system for Safari's default 87 | if let systemProviderIdentifier = (defaults?.dictionary(forKey: "NSPreferredWebServices")?["NSWebServicesProviderWebSearch"] as? [String: Any])?["NSProviderIdentifier"] as? String { 88 | return SearchSource.withIdentifier(systemProviderIdentifier) 89 | } 90 | 91 | return SearchSource.named("Google") 92 | } 93 | 94 | func setPrivateSessionLink(_ link: String, profile: UUID?) { 95 | var links = defaults?.dictionary(forKey: Keys.privateSessionLink.rawValue) as? [String: String] ?? [:] 96 | links[uuidKey(for: profile)] = link 97 | defaults?.set(links, forKey: Keys.privateSessionLink.rawValue) 98 | } 99 | 100 | func privateSessionLink(for profile: UUID?) -> String? { 101 | if let links = defaults?.privateSessionLinkWrapper, 102 | let link = links[uuidKey(for: profile)] { 103 | return link 104 | } 105 | 106 | if profile == nil, 107 | let legacySessionlinkKey = UserDefaults.standard.string(forKey: "kagiSessionLink")?.replacingOccurrences(of: "&q=%s", with: "") { 108 | setPrivateSessionLink(legacySessionlinkKey, profile: nil) // Don't assign this to a specific profile, to avoid accidentally using it in a profile where the user didn't expect it to be 109 | CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), Keys.privateSessionLink.cfNotificationName, nil, nil, true) 110 | return legacySessionlinkKey 111 | } 112 | 113 | return nil 114 | } 115 | 116 | var checkedIfUpgradedFromLegacyExtension: Bool { 117 | get { 118 | defaults?.bool(forKey: Keys.checkedIfUpgradedFromLegacyExtension.rawValue) == true 119 | } 120 | set { 121 | defaults?.set(newValue, forKey: Keys.checkedIfUpgradedFromLegacyExtension.rawValue) 122 | } 123 | } 124 | 125 | var didUpgradeFromLegacyExtension: Bool? { 126 | get { 127 | let possibleBool = defaults?.object(forKey: Keys.didUpgradeFromLegacyExtension.rawValue) 128 | if let possibleBool = possibleBool as? Bool { 129 | return possibleBool 130 | } else { 131 | return nil 132 | } 133 | } 134 | set { 135 | defaults?.set(newValue, forKey: Keys.didUpgradeFromLegacyExtension.rawValue) 136 | } 137 | } 138 | 139 | private func uuidKey(for profile: UUID?) -> String { 140 | return Self.NoProfileUUID // Can't access profile info from the app, so ignoring profiles for now 141 | // return profile?.uuidString ?? Self.NoProfileUUID 142 | } 143 | } 144 | 145 | extension UserDefaults { 146 | @objc dynamic var privateSessionLinkWrapper: [String: String]? { 147 | dictionary(forKey: Preferences.Keys.privateSessionLink.rawValue) as? [String: String] 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /safari/Universal/Shared (Extension)/Resources/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension_name": { 3 | "message": "Kagi for Safari", 4 | "description": "The display name for the extension." 5 | }, 6 | "extension_description": { 7 | "message": "Kagi is a 100% privacy-respecting search engine with results augmented by non-commercial indexes.", 8 | "description": "Description of what the extension does." 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /safari/Universal/Shared (Extension)/Resources/content-script.js: -------------------------------------------------------------------------------- 1 | browser.runtime.sendMessage({ greeting: "kagi" }).then((response) => { 2 | console.log("Received response: ", response); 3 | }); 4 | 5 | browser.runtime.onMessage.addListener((request, sender, sendResponse) => { 6 | console.log("Received request: ", request); 7 | }); 8 | -------------------------------------------------------------------------------- /safari/Universal/Shared (Extension)/Resources/images/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/Shared (Extension)/Resources/images/Icon.png -------------------------------------------------------------------------------- /safari/Universal/Shared (Extension)/Resources/images/ToolbarItemIcon.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/Shared (Extension)/Resources/images/ToolbarItemIcon.pdf -------------------------------------------------------------------------------- /safari/Universal/Shared (Extension)/Resources/images/ToolbarItemIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/safari/Universal/Shared (Extension)/Resources/images/ToolbarItemIcon.png -------------------------------------------------------------------------------- /safari/Universal/Shared (Extension)/Resources/kagi-content-script.js: -------------------------------------------------------------------------------- 1 | var scrapedToken = ""; 2 | try { 3 | let openSearchHrefSplit = document.querySelector("link[rel=search]").getAttribute("href").split(".xml/"); 4 | if (openSearchHrefSplit.length == 2) { 5 | scrapedToken = openSearchHrefSplit[1]; 6 | } 7 | } catch (e) { 8 | console.error(`[Extension] Error finding Kagi session token on page: ${e.message}`); 9 | } 10 | 11 | // Fetch current session token 12 | fetch("https://kagi.com/user/session", { 13 | credentials: "include" 14 | }) 15 | .then((response) => response.json()) 16 | .then((json) => { 17 | if (json == null) { 18 | return; 19 | } 20 | var token = json["id"]; 21 | if (scrapedToken.length > 0) { 22 | token = scrapedToken; 23 | } 24 | 25 | if (typeof token == "string" && token.length > 0) { 26 | var tokenLink = "https://kagi.com/search?token=" + token 27 | browser.storage.local.set({ "kagiPrivateSessionLink": tokenLink }) 28 | .then((result) => { 29 | browser.runtime.sendMessage({ 30 | "updatedKagiPrivateSessionLink": tokenLink 31 | }); 32 | if (location.href.indexOf("https://kagi.com/signin") == 0) { 33 | let kagi_sse_replay = localStorage.getItem("kagi_sse_replay") 34 | if (typeof kagi_sse_replay !== "undefined" && kagi_sse_replay != null) { 35 | let values = JSON.parse(kagi_sse_replay); 36 | if (typeof values !== "undefined" && values != null) { 37 | let keys = Object.keys(values); 38 | if (keys.length > 0) { 39 | let searchString = keys[0]; 40 | if (searchString.indexOf("search?q=") == 0) { 41 | window.location = "http://kagi.com/" + searchString + "&token=" + token; 42 | } 43 | } 44 | } 45 | } 46 | } 47 | }) 48 | } 49 | }) 50 | .catch((e) => console.error(`[Extension] Error fetching Kagi session token through API: ${e.message}`)); 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /safari/Universal/Shared (Extension)/Resources/popup.css: -------------------------------------------------------------------------------- 1 | * { 2 | /* -webkit-user-select: none;*/ 3 | -webkit-user-drag: none; 4 | cursor: default; 5 | } 6 | a[href] { 7 | cursor: pointer; 8 | } 9 | 10 | :root { 11 | color-scheme: light dark; 12 | 13 | --spacing: 10px; 14 | 15 | --captionColorLight: #494949; 16 | --captionColorDark: #B8B8B8; 17 | --borderColorLight: #494949; 18 | --borderColorDark: #D8D8D8; 19 | 20 | --backgroundDark: #262837; 21 | --backgroundDarkTransparent: rgba(38,40,55, 0.5); 22 | --foregroundLight: #E5E5E5; 23 | 24 | --orionAccent: #8E70FF; 25 | } 26 | 27 | html, 28 | body { 29 | margin:0; 30 | padding:0; 31 | /* height: clamp(300px, 160vw, 600px); */ 32 | width: clamp(350px, 100vw, 600px); 33 | } 34 | 35 | .hidden { display: none; } 36 | 37 | #wrapper { 38 | display: flex; 39 | align-items: center; 40 | justify-content: center; 41 | flex-direction: column; 42 | 43 | gap: var(--spacing); 44 | margin: var(--spacing) calc(var(--spacing) * 2); 45 | 46 | font: -apple-system-short-body; 47 | text-align: left; 48 | } 49 | 50 | 51 | fieldset, ul, ol, h1, h2, h3, h4, h5, h6, #wrapper > p { 52 | width: 92%; 53 | } 54 | 55 | h4 { 56 | margin-bottom: 0.8em; 57 | } 58 | 59 | p, label { 60 | display: block; 61 | margin: .7em 0; 62 | } 63 | 64 | ul { 65 | list-style: none; 66 | } 67 | 68 | li p strong { 69 | font-weight: 500; 70 | } 71 | 72 | fieldset { 73 | border: 0; 74 | padding: 0; 75 | margin-left: 0; 76 | } 77 | input, select, p, label, button { 78 | width: 100%; 79 | } 80 | input[type="checkbox"], label[checkbox] { 81 | width: auto; 82 | } 83 | label[checkbox] { 84 | display: contents; 85 | } 86 | fieldset input, fieldset select { 87 | font-size: 0.9em; 88 | font-weight: 300; 89 | } 90 | fieldset select { 91 | margin-bottom: 8px; 92 | } 93 | fieldset input { 94 | border-radius: 3px; 95 | border-style: solid; 96 | border-width: 1px; 97 | border-color: #eee; 98 | padding: 3px; 99 | } 100 | 101 | a { 102 | color: var(--orionAccent); 103 | } 104 | 105 | hr { 106 | border: solid 0.5px var(--borderColorDark); 107 | width: 70%; 108 | margin-top: 13px; 109 | } 110 | 111 | .caption { 112 | font-size: 0.9em; 113 | color: var(--captionColorLight); 114 | margin-top: 0; 115 | } 116 | h2 { 117 | margin-bottom: 0; 118 | } 119 | #instructions { 120 | padding-inline-start: 0; 121 | margin-top: 0; 122 | } 123 | #instructions>li { 124 | 125 | } 126 | #instructions>li:not(:first-child) { 127 | margin-top: 1em; 128 | } 129 | 130 | summary { 131 | outline: none; 132 | } 133 | 134 | .noListStyle { 135 | list-style: none; 136 | } 137 | 138 | #current-overrides ul { 139 | padding-left: 1em; 140 | list-style: circle inside; 141 | margin-bottom: 0.7em; 142 | } 143 | #current-overrides .noListStyle { 144 | list-style: inherit; 145 | } 146 | #overrides h3>.enabledForThisDomain { 147 | color: #60AA55; 148 | } 149 | .allUrls details .revokePermissions { 150 | display: none; 151 | } 152 | .revokePermissions { 153 | text-decoration: none; 154 | } 155 | .revokePermissions img { 156 | height: 1em; 157 | position: relative; 158 | top: 2px; 159 | } 160 | .revokePermissions .confirmationText { 161 | display: none; 162 | font-size: smaller; 163 | } 164 | .revokePermissions.readyToConfirm .confirmationText { display: inline; } 165 | 166 | li:has(.showMore) > *:not(h4) { display: none; } 167 | li.displayChildren:has(.showMore) > *:not(h4) { display: inherit; } 168 | 169 | @media (prefers-color-scheme: dark) { 170 | body { 171 | background: var(--backgroundDarkTransparent); 172 | } 173 | #wrapper { 174 | font-weight: 300; 175 | } 176 | .caption, #instructions li::marker { 177 | color: var(--captionColorDark); 178 | } 179 | hr { 180 | border-color: var(--borderColorLight); 181 | } 182 | 183 | #instructions>li:not(:first-child) { 184 | border-color: var(--borderColorLight); 185 | } 186 | } 187 | 188 | body:not(.setupPermissionsGranted) .setupPermissionsGranted, 189 | body.setupPermissionsGranted .noSetupPermissions { 190 | display: none !important; 191 | } 192 | -------------------------------------------------------------------------------- /safari/Universal/Shared (Extension)/Resources/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
    14 |
  • 15 |

    Kagi extension enabled on:

    16 | 17 |
    18 |
    19 | Search Engine Name 20 |
      21 |
    • redirecting url
    • 22 |
    23 |
    24 |
    25 |
  • 26 |
  • 27 |

    Engine to redirect

    28 |

    Select the search engine you want to redirect to Kagi.

    29 |
    30 | 41 |
    42 |
  • 43 |
  • 44 |

    Setup Pending

    45 |

    To set up Kagi redirects for search engines, grant setup permissions, or run a search in Safari's location bar, then reopen the extension to enable redirects to Kagi. 46 |

    47 | 48 |
    49 | 50 |
    51 | 52 |
  • 53 |
  • 54 | 55 | 56 |
  • 57 |
  • 58 |

    Private browsing

    59 |

    To search in Private Browsing, paste the Kagi private session link in the textbox below after copying it from your Kagi account settings.

    60 |
    61 | 62 | 63 |
    64 |
  • 65 |
66 |
67 | 68 | 69 | -------------------------------------------------------------------------------- /safari/Universal/Shared (Extension)/SafariWebExtensionHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SafariWebExtensionHandler.swift 3 | // Shared (Extension) 4 | // 5 | // Created by Nano Anderson on 10/17/23. 6 | // 7 | 8 | import SafariServices 9 | import os.log 10 | 11 | #if os(macOS) 12 | typealias ResponderObject = NSObject 13 | #elseif os(iOS) 14 | typealias ResponderObject = UIResponder 15 | #endif 16 | 17 | class SafariWebExtensionHandler: ResponderObject, NSExtensionRequestHandling { 18 | 19 | #if os(macOS) 20 | private static let appURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: "com.kagimacOS.Kagi-Search") 21 | #elseif os(iOS) 22 | private static let appURL = URL(string: "kagisearch://") 23 | #endif 24 | 25 | override init() { 26 | super.init() 27 | #if os(macOS) 28 | UpgradeChecker.shared.startObservers() 29 | #endif 30 | } 31 | 32 | func beginRequest(with context: NSExtensionContext) { 33 | let request = context.inputItems.first as? NSExtensionItem 34 | 35 | let profile: UUID? 36 | if #available(iOS 17.0, macOS 14.0, *) { 37 | profile = request?.userInfo?[SFExtensionProfileKey] as? UUID 38 | } else { 39 | profile = request?.userInfo?["profile"] as? UUID 40 | } 41 | 42 | let message: Any? 43 | if #available(iOS 17.0, macOS 14.0, *) { 44 | message = request?.userInfo?[SFExtensionMessageKey] 45 | } else { 46 | message = request?.userInfo?["message"] 47 | } 48 | 49 | os_log(.default, "Received message from browser.runtime.sendNativeMessage: %@ (profile: %@)", String(describing: message), profile?.uuidString ?? "none") 50 | 51 | guard let messageDict = message as? [String: Any], 52 | let messageType = messageDict["type"] as? String, 53 | let currentEngine = Preferences.shared.engine(for: profile) else { 54 | return 55 | } 56 | 57 | let response = NSExtensionItem() 58 | var responseData = ["type": "undefined"] 59 | 60 | switch messageType { 61 | case "privateSessionLink": 62 | if let privateSessionLink = Preferences.shared.privateSessionLink(for: profile) { 63 | responseData["type"] = "privateSessionUpdated" 64 | responseData["privateSessionLink"] = privateSessionLink 65 | } 66 | break 67 | case "currentEngine": 68 | responseData["type"] = "engineChanged" 69 | responseData["currentEngine"] = currentEngine.name 70 | case "openApp": 71 | if let appURL = Self.appURL { 72 | openAppURL(appURL, context: context) 73 | responseData["type"] = "openAppSuccess" 74 | } else { 75 | responseData["type"] = "openAppFailure" 76 | responseData["errorMessage"] = "Kagi app not found" 77 | } 78 | break 79 | case "migratePrivateSessionLink": 80 | if let previousPrivateSessionLink = messageDict["privateSessionLink"] as? String { 81 | Preferences.shared.setPrivateSessionLink(previousPrivateSessionLink, profile: nil) 82 | } 83 | break 84 | default: 85 | break 86 | } 87 | response.userInfo = [ SFExtensionMessageKey: responseData ] 88 | 89 | context.completeRequest(returningItems: [ response ], completionHandler: nil) 90 | } 91 | 92 | @objc func openURL(_ url: URL) { 93 | return 94 | } 95 | 96 | func openAppURL(_ url: URL, context: NSExtensionContext? = nil) { 97 | #if os(macOS) 98 | if #available(macOSApplicationExtension 10.15, *) { 99 | NSWorkspace.shared.openApplication(at: url, configuration: .init(), completionHandler: { runningApp, error in 100 | 101 | }) 102 | } else { 103 | NSWorkspace.shared.launchApplication("Kagi Search for Safari") 104 | // Fallback on earlier versions 105 | } 106 | #elseif os(iOS) 107 | let selector = #selector(NSExtensionContext.open(_:completionHandler:)) 108 | // var responder: ResponderObject? = self 109 | // let selector = #selector(SafariWebExtensionHandler.openURL(_:)) 110 | if context?.responds(to: selector) == true { 111 | // context?.perform(selector, with: url) 112 | context?.open(url) 113 | } 114 | 115 | // while responder != nil { 116 | //// if responder!.responds(to: selector) && responder != self { 117 | // if responder!.responds(to: selector) { 118 | // responder!.perform(selector, with: url) 119 | // return 120 | // } 121 | // responder = responder?.next 122 | // } 123 | #endif 124 | 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /safari/Universal/iOS (Extension)/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionPointIdentifier 8 | com.apple.Safari.web-extension 9 | NSExtensionPrincipalClass 10 | $(PRODUCT_MODULE_NAME).SafariWebExtensionHandler 11 | SFSafariAppExtensionBundleIdentifiersToReplace 12 | 13 | com.kagi.searchExtensionContainer.extension 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /safari/Universal/iOS (Extension)/Kagi Search Extension (iOS).entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.application-groups 6 | 7 | group.kagi-search-for-safari 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /safari/Universal/iOS (Extension)/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "default_locale": "en", 4 | 5 | "name": "__MSG_extension_name__", 6 | "description": "__MSG_extension_description__", 7 | "version": "2.2.7", 8 | 9 | "icons": { 10 | "512": "images/Icon.png" 11 | }, 12 | 13 | "background": { 14 | "scripts": [ 15 | "rule-builder.js", "background.js" 16 | ], 17 | "persistent": false 18 | }, 19 | 20 | "content_scripts": [{ 21 | "js": [ "content-script.js" ], 22 | "matches": [ "" ], 23 | "exclude_matches" : ["*://*.kagi.com/*"], 24 | "run_at": "document_start" 25 | }, { 26 | "js": ["kagi-content-script.js"], 27 | "matches": ["*://*.kagi.com/*"], 28 | "run_at": "document_start" 29 | }], 30 | 31 | "action": { 32 | "default_popup": "popup.html", 33 | "default_icon": { 34 | "128": "images/ToolbarItemIcon.png" 35 | } 36 | }, 37 | 38 | "permissions": [ 39 | "nativeMessaging", 40 | "webNavigation", 41 | "declarativeNetRequestWithHostAccess", 42 | "storage", 43 | "activeTab" 44 | ], 45 | 46 | "host_permissions": [ 47 | "" 48 | ], 49 | 50 | "optional_permissions": [] 51 | } 52 | -------------------------------------------------------------------------------- /safari/Universal/macOS (Extension)/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionPointIdentifier 8 | com.apple.Safari.web-extension 9 | NSExtensionPrincipalClass 10 | $(PRODUCT_MODULE_NAME).SafariWebExtensionHandler 11 | SFSafariAppExtensionBundleIdentifiersToReplace 12 | 13 | com.kagimacOS.Kagi-Search.Extension 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /safari/Universal/macOS (Extension)/Kagi Search Extension (macOS).entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.application-groups 8 | 9 | group.kagi-search-for-safari 10 | 11 | com.apple.security.files.user-selected.read-only 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /safari/Universal/macOS (Extension)/UpgradeChecker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UpgradeChecker.swift 3 | // Kagi Search Extension macOS 4 | // 5 | // Created by Nano Anderson on 11/29/23. 6 | // 7 | 8 | import Foundation 9 | 10 | class UpgradeChecker { 11 | 12 | static let shared = UpgradeChecker() 13 | 14 | static let RequestNotificationName = "com.kagimacOS.Kagi-Search.Extension.UpgradeCheckRequestNotification" as CFString 15 | static let ResponseNotificationName = "com.kagimacOS.Kagi-Search.Extension.UpgradeCheckResponseNotification" as CFString 16 | static let FirstResponseNotificationName = "com.kagimacOS.Kagi-Search.Extension.UpgradeCheckFirstResponseNotification" as CFString 17 | 18 | private var backoffInterval: TimeInterval { Double(currentChecks) } 19 | private var currentChecks = 0 20 | 21 | /// Should only ever be called from the Extension target, not the App target 22 | func startObservers() { 23 | CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), UnsafeRawPointer(Unmanaged.passUnretained(self).toOpaque()), { _, observer, _, object, _ in 24 | guard let observer = observer else { return } 25 | let unmanagedSelf = Unmanaged.fromOpaque(observer).takeUnretainedValue() 26 | unmanagedSelf.checkForLegacyDefaults() 27 | }, Self.RequestNotificationName as CFString, nil, .hold) 28 | } 29 | 30 | deinit { 31 | CFNotificationCenterRemoveEveryObserver(CFNotificationCenterGetDarwinNotifyCenter(), UnsafeRawPointer(Unmanaged.passUnretained(self).toOpaque())) 32 | } 33 | 34 | private func checkForLegacyDefaults() { 35 | Timer.scheduledTimer(withTimeInterval: backoffInterval, repeats: false) { _ in 36 | guard Preferences.shared.checkedIfUpgradedFromLegacyExtension == false else { 37 | if self.currentChecks <= 1 { 38 | CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), CFNotificationName(UpgradeChecker.ResponseNotificationName), nil, nil, true) 39 | } 40 | self.currentChecks -= 1 41 | return 42 | } 43 | let previousVersionExisted = UserDefaults.standard.object(forKey: "enableKagiSearch") != nil 44 | Preferences.shared.checkedIfUpgradedFromLegacyExtension = true 45 | Preferences.shared.didUpgradeFromLegacyExtension = previousVersionExisted 46 | CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), CFNotificationName(UpgradeChecker.FirstResponseNotificationName), nil, nil, true) 47 | self.currentChecks -= 1 48 | } 49 | currentChecks += 1 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /safari/Universal/macOS (Extension)/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "default_locale": "en", 4 | 5 | "name": "__MSG_extension_name__", 6 | "description": "__MSG_extension_description__", 7 | "version": "2.2.3", 8 | 9 | "icons": { 10 | "512": "images/Icon.png" 11 | }, 12 | 13 | "background": { 14 | "scripts": [ 15 | "background.js" 16 | ], 17 | "persistent": false 18 | }, 19 | 20 | "browser_action": { 21 | "default_popup": "popup.html", 22 | "default_icon": { 23 | "128": "images/ToolbarItemIcon.pdf" 24 | } 25 | }, 26 | 27 | "permissions": [ 28 | "nativeMessaging", 29 | "webNavigation", 30 | "storage" 31 | ], 32 | 33 | "optional_permissions": [] 34 | } 35 | -------------------------------------------------------------------------------- /shared/icons/favicon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/shared/icons/favicon-48.png -------------------------------------------------------------------------------- /shared/icons/icon_16px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/shared/icons/icon_16px.png -------------------------------------------------------------------------------- /shared/icons/icon_180px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/shared/icons/icon_180px.png -------------------------------------------------------------------------------- /shared/icons/icon_32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kagisearch/browser_extensions/e8537b86b04da239c6eadbe3c6122e3a816c7bb8/shared/icons/icon_32px.png -------------------------------------------------------------------------------- /shared/src/background_page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /shared/src/lib/utils.js: -------------------------------------------------------------------------------- 1 | if (!globalThis.browser) { 2 | globalThis.browser = chrome; 3 | } 4 | 5 | export async function summarizeContent({ 6 | url, 7 | text, 8 | summary_type, 9 | target_language, 10 | api_engine, 11 | token, 12 | api_token, 13 | }) { 14 | let summary = 'Unknown error'; 15 | let success = false; 16 | let timeSavedInMinutes = 0; 17 | const useApi = Boolean( 18 | api_token && ((api_engine && api_engine !== 'cecil') || text), 19 | ); 20 | 21 | try { 22 | const requestParams = { 23 | url, 24 | summary_type, 25 | }; 26 | 27 | if (target_language) { 28 | requestParams.target_language = target_language; 29 | } 30 | 31 | if (api_engine && useApi) { 32 | requestParams.engine = api_engine; 33 | } 34 | 35 | if (useApi) { 36 | if (api_engine) { 37 | requestParams.engine = api_engine; 38 | } 39 | 40 | if (text) { 41 | requestParams.text = text; 42 | requestParams.url = undefined; 43 | } 44 | } 45 | 46 | const searchParams = new URLSearchParams(requestParams); 47 | 48 | const requestOptions = { 49 | method: 'GET', 50 | headers: { 51 | 'Content-Type': 'application/json', 52 | Authorization: useApi ? `Bot ${api_token}` : `${token}`, 53 | }, 54 | credentials: 'include', 55 | }; 56 | 57 | const response = await fetch( 58 | `${ 59 | useApi 60 | ? 'https://kagi.com/api/v0/summarize' 61 | : 'https://kagi.com/mother/summary_labs' 62 | }?${searchParams.toString()}`, 63 | requestOptions, 64 | ); 65 | 66 | if (response.status === 200) { 67 | const result = await response.json(); 68 | 69 | console.debug('summarize response', result); 70 | 71 | if (useApi) { 72 | if (result.data?.output) { 73 | summary = result.data.output; 74 | } else if (result.error) { 75 | summary = JSON.stringify(result.error); 76 | } 77 | } else { 78 | summary = result?.output_text || 'Unknown error'; 79 | timeSavedInMinutes = result?.output_data?.word_stats?.time_saved || 0; 80 | } 81 | 82 | success = Boolean(result) && !Boolean(result.error); 83 | } else { 84 | console.error('summarize error', response.status, response.statusText); 85 | 86 | if (response.status === 401) { 87 | summary = 'Invalid Token! Please set a new one.'; 88 | } else { 89 | summary = `Error: ${response.status} - ${response.statusText}`; 90 | } 91 | } 92 | } catch (error) { 93 | summary = error.message ? `Error: ${error.message}` : JSON.stringify(error); 94 | } 95 | 96 | return { 97 | summary, 98 | success, 99 | timeSavedInMinutes, 100 | }; 101 | } 102 | 103 | export async function fetchSettings() { 104 | const sessionObject = await browser.storage.local.get('session_token'); 105 | const syncObject = await browser.storage.local.get('sync_existing'); 106 | const apiObject = await browser.storage.local.get('api_token'); 107 | const apiEngineObject = await browser.storage.local.get('api_engine'); 108 | const summaryTypeObject = await browser.storage.local.get('summary_type'); 109 | const targetLanguageObject = 110 | await browser.storage.local.get('target_language'); 111 | const privacyConsentObject = 112 | await browser.storage.local.get('privacy_consent'); 113 | 114 | return { 115 | token: sessionObject?.session_token, 116 | sync_existing: 117 | typeof syncObject?.sync_existing !== 'undefined' 118 | ? syncObject.sync_existing 119 | : true, 120 | api_token: apiObject?.api_token, 121 | api_engine: apiEngineObject?.api_engine, 122 | summary_type: summaryTypeObject?.summary_type, 123 | target_language: targetLanguageObject?.target_language, 124 | privacy_consent: 125 | typeof privacyConsentObject?.privacy_consent !== 'undefined' 126 | ? privacyConsentObject.privacy_consent 127 | : false, 128 | }; 129 | } 130 | 131 | export async function getActiveTab(fetchingFromShortcut = false) { 132 | const tabsQuery = { 133 | active: true, 134 | lastFocusedWindow: true, 135 | }; 136 | 137 | // Don't look just in the last focused window if we're opening from the shortcut 138 | if (fetchingFromShortcut) { 139 | tabsQuery.lastFocusedWindow = undefined; 140 | } 141 | 142 | const tabs = await browser.tabs.query(tabsQuery); 143 | 144 | // Chrome/Firefox might give us more than one active tab when something like "chrome://*" or "about:*" is also open 145 | const tab = 146 | tabs.find( 147 | (tab) => 148 | tab?.url?.startsWith('http://') || tab?.url?.startsWith('https://'), 149 | ) || tabs[0]; 150 | 151 | if (tab?.url?.startsWith('about:reader?url=')) { 152 | const newUrl = new URL(tab.url); 153 | tab.url = newUrl.searchParams.get('url'); 154 | } 155 | 156 | if (!tab || !tab.url) { 157 | console.error('No tab/url found.'); 158 | console.error(JSON.stringify(tabs)); 159 | return null; 160 | } 161 | 162 | return tab; 163 | } 164 | 165 | export async function requestActiveTabPermission() { 166 | try { 167 | const granted = await browser.permissions.request({ 168 | permissions: ['activeTab'], 169 | }); 170 | if (!granted) { 171 | console.error('Permission not granted for activeTab.'); 172 | return false; 173 | } 174 | return true; 175 | } catch (error) { 176 | console.error('Error requesting activeTab permission:', error); 177 | return false; 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /shared/src/popup.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | background-color: transparent; 6 | font-family: Arial, Helvetica, sans-serif; 7 | line-height: 16px; 8 | } 9 | 10 | * { 11 | -moz-box-sizing: border-box; 12 | -o-box-sizing: border-box; 13 | -webkit-box-sizing: border-box; 14 | box-sizing: border-box; 15 | } 16 | 17 | #content { 18 | display: flex; 19 | flex-direction: column; 20 | align-items: center; 21 | justify-content: center; 22 | width: 500px; 23 | } 24 | 25 | #token { 26 | width: 100%; 27 | } 28 | 29 | button { 30 | width: 100%; 31 | margin-top: 10px; 32 | } 33 | 34 | input { 35 | width: 100%; 36 | border: 1px solid #e1e0db; 37 | } 38 | 39 | input[type="password"], input[type="text"] { 40 | background-color: #f7f6f2; 41 | border: 1px solid #e1e0db; 42 | height: 48px; 43 | border-radius: 5px; 44 | padding: 0 10px; 45 | } 46 | 47 | input[type="password"]:focus, input[type="text"]:focus { 48 | outline: 1px solid #ffb319; 49 | } 50 | 51 | #header { 52 | margin: 10px 25px; 53 | width: 100%; 54 | text-align: center; 55 | display: flex; 56 | align-items: center; 57 | justify-content: space-between; 58 | } 59 | 60 | .logo { 61 | width: 58px; 62 | height: 29px; 63 | flex: 0; 64 | justify-content: flex-start; 65 | display: flex; 66 | margin: 0 15px; 67 | } 68 | 69 | .logo svg { 70 | height: 100%; 71 | } 72 | 73 | #links { 74 | flex: 1; 75 | justify-content: flex-start; 76 | display: flex; 77 | font-size: 11px; 78 | color: rgba(0, 0, 0, 0.6); 79 | margin: 0 10px; 80 | visibility: hidden; 81 | } 82 | 83 | #links a { 84 | display: inline-block; 85 | color: rgba(0, 0, 0, 0.6); 86 | text-decoration: none; 87 | } 88 | 89 | #links a:hover, #links a:focus { 90 | text-decoration: underline; 91 | } 92 | 93 | #links span { 94 | display: inline-block; 95 | margin: 0 7px; 96 | } 97 | 98 | #status { 99 | font-size: 0.875rem; 100 | flex: 0; 101 | } 102 | 103 | #status svg { 104 | width: 24px; 105 | height: 24px; 106 | } 107 | 108 | #status svg:first-child { 109 | color: #fd6820; 110 | fill: #fd6820; 111 | font-weight: 700; 112 | } 113 | 114 | #status svg#loading { 115 | font-size: 0.875rem; 116 | flex: 0; 117 | } 118 | 119 | #status svg:last-child { 120 | color: #00a854; 121 | fill: #00a854; 122 | font-weight: 700; 123 | } 124 | 125 | #header #advanced { 126 | cursor: pointer; 127 | text-decoration: underline; 128 | font-size: 14px; 129 | margin-left: 5px; 130 | flex: 0; 131 | } 132 | 133 | #header #advanced svg:first-child { 134 | width: 30px; 135 | height: 30px; 136 | margin: 3px; 137 | } 138 | 139 | #header #advanced svg:last-child { 140 | width: 36px; 141 | height: 36px; 142 | } 143 | 144 | #status_permission_message, 145 | #status_error_message, 146 | #save_error { 147 | color: #fd6820; 148 | } 149 | 150 | #status_error_message, #status_loading_message { 151 | margin: 10px; 152 | font-weight: 700; 153 | text-align: center; 154 | font-size: 14px; 155 | line-height: 1.2rem; 156 | } 157 | 158 | #status_error_message a { 159 | display: block; 160 | font-size: 1.5rem; 161 | line-height: 2rem; 162 | margin: 1rem auto; 163 | } 164 | 165 | h3 { 166 | font-weight: 400; 167 | } 168 | 169 | #sync_checkbox { 170 | display: flex; 171 | flex-direction: column; 172 | align-items: center; 173 | } 174 | 175 | #incognito { 176 | max-width: 100%; 177 | padding: 0 15px; 178 | font-size: 0.9rem; 179 | } 180 | 181 | .setting_row { 182 | display: flex; 183 | justify-content: space-between; 184 | width: 100%; 185 | padding: 0 15px; 186 | margin-bottom: 20px; 187 | } 188 | 189 | .desc { 190 | color: rgba(0, 0, 0, 0.6); 191 | font-size: 0.75rem; 192 | margin-top: 5px; 193 | } 194 | 195 | .title { 196 | font-size: 0.875rem; 197 | font-weight: 700; 198 | padding-right: 0.75rem; 199 | } 200 | 201 | p { 202 | width: 70%; 203 | text-align: center; 204 | margin-top: 5px; 205 | } 206 | 207 | #token_save { 208 | background-color: #ffb319; 209 | border: 1px solid #ffb319; 210 | color: #191919; 211 | border-radius: 5px; 212 | height: 46px; 213 | max-width: 150px; 214 | font-size: 0.9375rem; 215 | cursor: pointer; 216 | } 217 | 218 | #token_save:hover { 219 | background-color: #f7a808; 220 | border: 1px solid #d9950d; 221 | } 222 | 223 | .checkbox_box h3 { 224 | margin-bottom: 10px; 225 | } 226 | 227 | .k_ui_toggle_switch { 228 | --bg-color: transparent; 229 | --active-bg-color: transparent; 230 | --border-color: #15273f; 231 | --active-border-color: #15273f; 232 | --knob-bg-color: #15273f; 233 | --active-knob-bg-color: #15273f; 234 | padding-top: 0; 235 | padding-bottom: 0; 236 | padding-left: 0; 237 | } 238 | 239 | .k_ui_toggle_switch.with_label { 240 | margin-left: auto; 241 | position: relative; 242 | padding-right: 35px; 243 | display: inline-block; 244 | color: var(--border-color); 245 | } 246 | 247 | .k_ui_toggle_switch_status { 248 | position: absolute; 249 | left: 0; 250 | top: 0; 251 | transform: translate(55px, 5px); 252 | font-size: 0.75rem; 253 | font-weight: 700; 254 | display: flex; 255 | align-items: center; 256 | pointer-events: none; 257 | line-height: normal; 258 | opacity: 0.3; 259 | } 260 | 261 | .k_ui_toggle_switch_status .__on { 262 | display: none; 263 | } 264 | 265 | .k_ui_toggle_switch_bar { 266 | background-color: var(--bg-color); 267 | border: 1px solid var(--border-color); 268 | position: relative; 269 | padding: 3px; 270 | width: 48px; 271 | height: 24px; 272 | border-radius: 100px; 273 | cursor: pointer; 274 | opacity: 0.3; 275 | } 276 | 277 | .k_ui_toggle_switch_bar:after { 278 | content: ""; 279 | width: 18px; 280 | height: 18px; 281 | background-color: var(--knob-bg-color); 282 | position: absolute; 283 | left: 2px; 284 | top: 2px; 285 | border-radius: 100px; 286 | transition: left 0.15s ease-in-out; 287 | } 288 | 289 | .k_ui_toggle_switch input { 290 | display: none; 291 | } 292 | 293 | .k_ui_toggle_switch input:checked ~ .k_ui_toggle_switch_bar { 294 | border: 1px solid var(--active-border-color); 295 | background-color: var(--active-bg-color); 296 | opacity: unset; 297 | } 298 | 299 | .k_ui_toggle_switch input:checked ~ .k_ui_toggle_switch_status { 300 | opacity: unset; 301 | } 302 | 303 | .k_ui_toggle_switch input:checked ~ .k_ui_toggle_switch_status .__on { 304 | display: block; 305 | } 306 | 307 | .k_ui_toggle_switch input:checked ~ .k_ui_toggle_switch_status .__off { 308 | display: none; 309 | } 310 | 311 | .k_ui_toggle_switch input:checked ~ .k_ui_toggle_switch_bar:after { 312 | background-color: var(--active-knob-bg-color); 313 | left: unset; 314 | right: 2px; 315 | } 316 | 317 | #fastgpt, #summarize, #request_permissions { 318 | margin-top: 10px; 319 | width: 100%; 320 | padding: 0 15px; 321 | } 322 | 323 | #summarize .setting_row, #request_permissions .setting_row, #fastgpt .setting_row { 324 | margin-top: 10px; 325 | padding: 0; 326 | justify-content: space-between; 327 | } 328 | 329 | #summarize .setting_row > div, #request_permissions .setting_row > div, #fastgpt .setting_row > form { 330 | margin-right: 10px; 331 | } 332 | 333 | #fastgpt_form { 334 | width: 100%; 335 | display: flex; 336 | } 337 | 338 | #fastgpt_form input { 339 | margin-right: 10px 340 | } 341 | 342 | #summarize .setting_row label { 343 | display: block; 344 | margin-bottom: 5px; 345 | } 346 | 347 | #summarize_page, #request_permissions_button, #fastgpt_submit, #privacy_consent_button { 348 | background-color: #ffb319; 349 | border: 1px solid #ffb319; 350 | color: #191919; 351 | border-radius: 5px; 352 | height: 46px; 353 | max-width: 150px; 354 | font-size: 0.9375rem; 355 | cursor: pointer; 356 | margin-top: 0; 357 | margin-bottom: 10px; 358 | margin-right: 10px; 359 | } 360 | 361 | #fastgpt_submit { 362 | width: 120px; 363 | margin-right: 0; 364 | } 365 | 366 | #request_permissions_button { 367 | max-width: 400px; 368 | margin-left: auto; 369 | margin-right: auto; 370 | } 371 | 372 | #summarize_page:hover, #request_permissions_button:hover, #fastgpt_submit:hover, #privacy_consent_button:hover { 373 | background-color: #f7a808; 374 | border: 1px solid #d9950d; 375 | } 376 | -------------------------------------------------------------------------------- /shared/src/summarize_result.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | width: 500px; 4 | background-color: transparent; 5 | font-family: Helvetica, Arial, sans-serif; 6 | line-height: 16px; 7 | margin: 0 auto; 8 | color-scheme: light dark; 9 | } 10 | 11 | * { 12 | -moz-box-sizing: border-box; 13 | -o-box-sizing: border-box; 14 | -webkit-box-sizing: border-box; 15 | box-sizing: border-box; 16 | } 17 | 18 | #content { 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | } 24 | 25 | .logo { 26 | width: 58px; 27 | height: 29px; 28 | flex: 1; 29 | justify-content: flex-start; 30 | display: flex; 31 | margin: 10px auto; 32 | } 33 | 34 | .logo svg { 35 | height: 100%; 36 | } 37 | 38 | #loading { 39 | width: 50px; 40 | } 41 | 42 | button { 43 | width: 100%; 44 | margin-top: 10px; 45 | } 46 | 47 | h3 { 48 | font-weight: 400; 49 | } 50 | 51 | p { 52 | margin-top: 10px; 53 | } 54 | 55 | #summary_result { 56 | padding: 10px; 57 | font-size: 1rem; 58 | line-height: 1.25; 59 | } 60 | 61 | /* This "hack" makes the "key moments" summary type look more readable */ 62 | #summary_result br { 63 | margin-top: 10px; 64 | display: block; 65 | content: ""; 66 | } 67 | 68 | #summary_result.error { 69 | color: #ac2915; 70 | } 71 | 72 | #copy_summary { 73 | background-color: #ffb319; 74 | border: 1px solid #ffb319; 75 | color: #191919; 76 | border-radius: 5px; 77 | height: 46px; 78 | max-width: 150px; 79 | font-size: 0.9375rem; 80 | cursor: pointer; 81 | margin: 10px auto; 82 | display: block; 83 | } 84 | 85 | #copy_summary:hover { 86 | background-color: #f7a808; 87 | border: 1px solid #d9950d; 88 | } 89 | 90 | #summary_stats { 91 | padding: 10px; 92 | font-size: 1rem; 93 | line-height: 1.25rem; 94 | } 95 | 96 | #close_summary { 97 | background-color: #e5e5e5; 98 | border: 1px solid #e5e5e5; 99 | color: #191919; 100 | border-radius: 3px; 101 | height: 32px; 102 | max-width: 80px; 103 | font-size: 0.75rem; 104 | cursor: pointer; 105 | margin: 10px auto; 106 | display: block; 107 | } 108 | 109 | #close_summary:hover { 110 | background-color: #cecece; 111 | border: 1px solid #b7b7b7; 112 | } 113 | -------------------------------------------------------------------------------- /shared/src/summarize_result.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 20 |
21 | 34 | 38 | 47 | 48 | 49 |
50 | 51 | 54 | 55 | 56 | 57 | 60 | 61 |
62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /shared/src/summarize_result.js: -------------------------------------------------------------------------------- 1 | import { fetchSettings, getActiveTab } from './lib/utils.js'; 2 | 3 | if (!globalThis.browser) { 4 | globalThis.browser = chrome; 5 | } 6 | 7 | let summaryTextContents = ''; 8 | 9 | async function setup() { 10 | const loadingElement = document.querySelector('#loading'); 11 | if (!loadingElement) { 12 | console.error('Could not find loading div'); 13 | return; 14 | } 15 | 16 | const summaryResultElement = document.querySelector('#summary_result'); 17 | if (!summaryResultElement) { 18 | console.error('Could not find summarize result div'); 19 | return; 20 | } 21 | 22 | summaryResultElement.style.display = 'none'; 23 | summaryResultElement.classList.remove('error'); 24 | 25 | const copySummaryElement = document.querySelector('#copy_summary'); 26 | if (!copySummaryElement) { 27 | console.error('Could not find copy summary button'); 28 | return; 29 | } 30 | 31 | copySummaryElement.style.display = 'none'; 32 | 33 | copySummaryElement.addEventListener('click', async () => { 34 | if (!summaryTextContents) { 35 | return; 36 | } 37 | 38 | try { 39 | const summaryToCopy = summaryTextContents.trim().replaceAll('\n', '\n\n'); 40 | await navigator.clipboard.writeText(summaryToCopy); 41 | 42 | copySummaryElement.innerText = 'Copied!'; 43 | 44 | setTimeout(() => { 45 | copySummaryElement.innerText = 'Copy summary'; 46 | }, 3000); 47 | } catch (error) { 48 | console.error('error copying summary to clipboard: ', error); 49 | } 50 | }); 51 | 52 | const summaryStatsElement = document.querySelector('#summary_stats'); 53 | if (!summaryStatsElement) { 54 | console.error('Could not find summarize stats div'); 55 | return; 56 | } 57 | 58 | summaryStatsElement.style.display = 'none'; 59 | 60 | const summaryStatsTimeSavedElement = document.querySelector( 61 | '#summary_stats_time_saved', 62 | ); 63 | if (!summaryStatsTimeSavedElement) { 64 | console.error('Could not find summarize stats time saved element'); 65 | return; 66 | } 67 | 68 | summaryStatsTimeSavedElement.innerText = '0 minutes'; 69 | 70 | const summaryCloseElement = document.getElementById('close_summary'); 71 | if (!summaryCloseElement) { 72 | console.error('Could not find summarize close element'); 73 | return; 74 | } 75 | 76 | summaryCloseElement.style.display = 'none'; 77 | 78 | browser.runtime.onMessage.addListener(async (data) => { 79 | const searchParams = new URLSearchParams(window.location.search); 80 | const url = searchParams.get('url'); 81 | 82 | if (data.type === 'summary_finished' && data.url === url) { 83 | loadingElement.style.display = 'none'; 84 | 85 | if (data.success) { 86 | summaryResultElement.classList.remove('error'); 87 | summaryTextContents = new DOMParser().parseFromString( 88 | data.summary.replaceAll(/
/g, '\n'), 89 | 'text/html', 90 | ).documentElement.textContent; 91 | copySummaryElement.style.display = ''; 92 | } else { 93 | summaryResultElement.classList.add('error'); 94 | summaryTextContents = data.summary; 95 | copySummaryElement.style.display = 'none'; 96 | } 97 | 98 | summaryResultElement.style.display = ''; 99 | const [title, ...restContents] = summaryTextContents.split('\n'); 100 | const titleEl = document.createElement('h1'); 101 | titleEl.textContent = title; 102 | summaryResultElement.innerText = restContents.join('\n'); 103 | summaryResultElement.prepend(titleEl); 104 | 105 | if (data.timeSavedInMinutes) { 106 | summaryStatsElement.style.display = ''; 107 | summaryStatsTimeSavedElement.innerText = `${ 108 | data.timeSavedInMinutes 109 | } minute${data.timeSavedInMinutes !== 1 ? 's' : ''}`; 110 | } 111 | 112 | summaryCloseElement.style.display = ''; 113 | summaryCloseElement.addEventListener('click', () => { 114 | window.close(); 115 | }); 116 | } 117 | }); 118 | 119 | window.addEventListener('keydown', (event) => { 120 | if (event.key === 'Escape') window.close(); 121 | }); 122 | 123 | async function requestPageSummary() { 124 | const hasTabAccess = await browser.permissions.contains({ 125 | permissions: ['activeTab'], 126 | }); 127 | 128 | if (!hasTabAccess) { 129 | summaryResultElement.style.display = ''; 130 | summaryResultElement.classList.add('error'); 131 | summaryResultElement.innerText = 132 | "You can't summarize without allowing access to the currently active tab."; 133 | return; 134 | } 135 | 136 | const searchParams = new URLSearchParams(window.location.search); 137 | 138 | if (!searchParams.get('summary_type')) { 139 | searchParams.set('summary_type', 'summary'); 140 | } 141 | 142 | if (!searchParams.get('target_language')) { 143 | searchParams.set('target_language', ''); 144 | } 145 | 146 | // If there's no URL, get the currently active tab and default params 147 | if (!searchParams.get('url')) { 148 | const tab = await getActiveTab(true); 149 | 150 | if (!tab) { 151 | console.error('No tab/url found.'); 152 | return; 153 | } 154 | 155 | searchParams.set('url', tab.url); 156 | 157 | // Add ?url= to the window, so it receives the proper summary 158 | const popupUrl = new URL(window.location.href); 159 | popupUrl.searchParams.set('url', tab.url); 160 | window.history.replaceState(null, '', popupUrl.toString()); 161 | } 162 | 163 | const { token, api_token, api_engine, summary_type, target_language } = 164 | await fetchSettings(); 165 | 166 | if (token) { 167 | searchParams.set('token', token); 168 | } 169 | if (api_token) { 170 | searchParams.set('api_token', api_token); 171 | } 172 | if (api_engine) { 173 | searchParams.set('api_engine', api_engine); 174 | } 175 | if (summary_type) { 176 | searchParams.set('summary_type', summary_type); 177 | } 178 | if (target_language) { 179 | searchParams.set('target_language', target_language); 180 | } 181 | 182 | loadingElement.style.display = ''; 183 | summaryResultElement.classList.remove('error'); 184 | summaryResultElement.style.display = ''; 185 | summaryResultElement.innerText = 'Summarizing...'; 186 | copySummaryElement.style.display = 'none'; 187 | summaryTextContents = ''; 188 | 189 | await browser.runtime.sendMessage({ 190 | type: 'summarize_page', 191 | ...Object.fromEntries(searchParams), 192 | }); 193 | } 194 | 195 | await requestPageSummary(); 196 | } 197 | 198 | document.addEventListener('DOMContentLoaded', setup); 199 | --------------------------------------------------------------------------------