|
// ==UserScript== |
|
// @name Kagi Service Switcher |
|
// @namespace http://tampermonkey.net/ |
|
// @version 1.1 |
|
// @description Add quick navigation buttons for Kagi Assistant, Translate, and News |
|
// @author jerieljan |
|
// @match https://www.kagi.com/* |
|
// @match https://kagi.com/* |
|
// @grant GM_addStyle |
|
// @run-at document-end |
|
// ==/UserScript== |
|
|
|
(function() { |
|
'use strict'; |
|
|
|
// Service definitions with URLs and icons |
|
const services =[ |
|
{ |
|
name: 'Assistant', |
|
url: 'https://assistant.kagi.com', |
|
icon: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="10.5"></circle><circle cx="12" cy="12" r="7.75"></circle><path d="M19.2 14.9c-1.1.5-2.1 1.1-4.2 1.1-4 0-5-3-8.5-3-.9 0-1.6.1-2.1.3m15-3.5c-1.1.5-2.1 1.2-4.4 1.2-4 0-5-3-8.5-3-.4 0-.8 0-1.2.1"></path></svg>', |
|
title: 'Kagi Assistant' |
|
}, |
|
{ |
|
name: 'Translate', |
|
url: 'https://translate.kagi.com', |
|
icon: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><path d="M12.75 9.5V6.25a3.25 3.25 0 00-3.25-3.25H5.5a3.25 3.25 0 00-3.25 3.25v5.75a3.25 3.25 0 003.25 3.25H9.5a3.25 3.25 0 001.5-.25"></path><rect x="11" y="9.25" width="10.5" height="12.25" rx="3.25"></rect><path d="M7.4 5.4 9.6 10.3 8.2 10.2M10 6 4.8 6.7M9.9 12.2H6.6A1.7 1.7 0 014.9 10.5M10.2 7.9l-5.2.7" stroke-width="1"></path><path d="M3.5 16.5a2.5 2.5 0 002.5 2.5h1.5m-.5-1.75 1.25 1.75-1.25 1.75M19.75 6.5a2.5 2.5 0 00-2.5-2.5h-1.5m.5 1.75-1.25-1.75 1.25-1.75" stroke-width="1"></path><path d="M14.25 18.25v-4.25a1 1 0 014.25 0v4.25m-4.25-2.5h4.25" stroke-width="1"></path></svg>', |
|
title: 'Kagi Translate' |
|
}, |
|
{ |
|
name: 'News', |
|
url: 'https://news.kagi.com', |
|
icon: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><path d="M4.25 7.5a1 1 0 010-4.5H9c1.5 0 2.25 0 2.75.5s.5 1.25.5 2.75V7.5ZM19 12.5C19 10.25 19 9 18.25 8.25 17.5 7.5 16.25 7.5 13.5 7.5H4.25a2.25 2.25 0 01-2.25-2.25V14.5C2 17.25 2 18.5 2.75 19.25S5 20 7.5 20H14.5"></path><path d="M4.5 11h7.5m-7.5 2.5h4.5m-4.5 2.5h4.5" stroke-width="1"></path><circle cx="17.5" cy="16.5" r="4.5"></circle><path d="M22 16.5a4.5 4.5 90 00-5.75 4.25M13.25 16.5a4.5 4.5 90 005.75-4.25" stroke-width=".75"></path></svg>', |
|
title: 'Kagi News' |
|
} |
|
]; |
|
|
|
// Add styles |
|
GM_addStyle(` |
|
.kagi-switcher-container { |
|
display: flex; |
|
align-items: center; |
|
gap: 8px; |
|
} |
|
|
|
/* Base button structural styles (Colors removed & separated) */ |
|
.kagi-switcher-btn { |
|
display: inline-flex; |
|
align-items: center; |
|
gap: 6px; |
|
padding: 8px 14px; |
|
text-decoration: none; |
|
border-radius: 24px; |
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
|
font-size: 13px; |
|
font-weight: 500; |
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); |
|
border: none; |
|
cursor: pointer; |
|
white-space: nowrap; |
|
} |
|
|
|
.kagi-switcher-btn:hover { |
|
transform: translateY(-1px); |
|
} |
|
|
|
.kagi-switcher-btn svg { |
|
width: 14px; |
|
height: 14px; |
|
flex-shrink: 0; |
|
} |
|
|
|
.kagi-switcher-btn span { |
|
display: inline; |
|
} |
|
|
|
/* ======================================= |
|
1. Assistant Theme (#c9c1ff) |
|
======================================= */ |
|
.kagi-switcher-btn:nth-child(1) { |
|
/* Starts slightly lighter, ends on your exact target hex */ |
|
background: linear-gradient(135deg, #e0daff 0%, #c9c1ff 100%); |
|
box-shadow: 0 2px 8px rgba(201, 193, 255, 0.4); |
|
/* Dark text for readability, as #c9c1ff is very bright */ |
|
color: #2b2a33; |
|
} |
|
|
|
.kagi-switcher-btn:nth-child(1):hover { |
|
/* Shifts darker on hover */ |
|
background: linear-gradient(135deg, #c9c1ff 0%, #a295f5 100%); |
|
box-shadow: 0 4px 12px rgba(162, 149, 245, 0.4); |
|
} |
|
|
|
/* ======================================= |
|
2. Translate Theme (#74bd44) |
|
======================================= */ |
|
.kagi-switcher-btn:nth-child(2) { |
|
background: linear-gradient(135deg, #8cd45c 0%, #74bd44 100%); |
|
box-shadow: 0 2px 8px rgba(116, 189, 68, 0.3); |
|
color: #2b2a33; |
|
} |
|
|
|
.kagi-switcher-btn:nth-child(2):hover { |
|
background: linear-gradient(135deg, #74bd44 0%, #5b9e31 100%); |
|
box-shadow: 0 4px 12px rgba(116, 189, 68, 0.4); |
|
} |
|
|
|
/* ======================================= |
|
3. News Theme (#ffb319) |
|
======================================= */ |
|
.kagi-switcher-btn:nth-child(3) { |
|
background: linear-gradient(135deg, #ffc94d 0%, #ffb319 100%); |
|
box-shadow: 0 2px 8px rgba(255, 179, 25, 0.3); |
|
color: #2b2a33; |
|
} |
|
|
|
.kagi-switcher-btn:nth-child(3):hover { |
|
background: linear-gradient(135deg, #ffb319 0%, #e59b00 100%); |
|
box-shadow: 0 4px 12px rgba(255, 179, 25, 0.4); |
|
} |
|
|
|
/* ======================================= |
|
Responsive & Modifiers |
|
======================================= */ |
|
.kagi-switcher-compact .kagi-switcher-btn { |
|
padding: 6px 10px; |
|
font-size: 12px; |
|
} |
|
|
|
.kagi-switcher-compact .kagi-switcher-btn span { |
|
display: none; |
|
} |
|
|
|
@media (max-width: 1200px) { |
|
.kagi-switcher-btn span { |
|
display: none; |
|
} |
|
.kagi-switcher-btn { |
|
padding: 8px; |
|
} |
|
} |
|
|
|
.kagi-switcher-search-position { |
|
display: flex; |
|
align-items: center; |
|
margin-left: 16px; |
|
} |
|
`); |
|
|
|
// Create button elements |
|
function createSwitcherButtons(compact = false) { |
|
const container = document.createElement('div'); |
|
container.className = `kagi-switcher-container${compact ? ' kagi-switcher-compact' : ''}`; |
|
|
|
services.forEach(service => { |
|
const btn = document.createElement('a'); |
|
btn.href = service.url; |
|
btn.className = 'kagi-switcher-btn'; |
|
btn.title = service.title; |
|
btn.innerHTML = `${service.icon}<span>${service.name}</span>`; |
|
container.appendChild(btn); |
|
}); |
|
|
|
return container; |
|
} |
|
|
|
// Insert on homepage (left of app_nav_dropdown) |
|
function insertOnHomepage() { |
|
// Try to find the app_nav_dropdown or account container |
|
const navDropdown = document.querySelector('.app_nav_dropdown, [class*="app_nav_dropdown"]'); |
|
|
|
if (navDropdown && navDropdown.parentElement) { |
|
const container = createSwitcherButtons(); |
|
navDropdown.parentElement.insertBefore(container, navDropdown); |
|
console.log('[Kagi Switcher] Inserted buttons on homepage (left of nav dropdown)'); |
|
return true; |
|
} |
|
|
|
// Fallback: Try to find the account container |
|
const accountContainer = document.querySelector('#accountContainer, .user-auth-bar'); |
|
if (accountContainer) { |
|
const firstChild = accountContainer.querySelector('.flex, .header_links, button'); |
|
if (firstChild) { |
|
const container = createSwitcherButtons(); |
|
accountContainer.insertBefore(container, firstChild); |
|
console.log('[Kagi Switcher] Inserted buttons in account container'); |
|
return true; |
|
} |
|
} |
|
|
|
// Fallback: Try to find the top-right navigation area |
|
const headerNav = document.querySelector('header nav, nav[class*="header"], [class*="top-nav"]'); |
|
if (headerNav) { |
|
const container = createSwitcherButtons(); |
|
headerNav.appendChild(container); |
|
console.log('[Kagi Switcher] Inserted buttons in header navigation'); |
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
// Insert on search page (before app_nav_dropdown) |
|
function insertOnSearchPage() { |
|
// Try to find the app_nav_dropdown in the account container |
|
const navDropdown = document.querySelector('.app_nav_dropdown, [class*="app_nav_dropdown"]'); |
|
|
|
if (navDropdown && navDropdown.parentElement) { |
|
const container = createSwitcherButtons(true); |
|
container.classList.add('kagi-switcher-search-position'); |
|
navDropdown.parentElement.insertBefore(container, navDropdown); |
|
console.log('[Kagi Switcher] Inserted buttons on search page (left of app_nav_dropdown)'); |
|
return true; |
|
} |
|
|
|
// Fallback: Try the account container |
|
const accountContainer = document.querySelector('#accountContainer, .user-auth-bar'); |
|
if (accountContainer) { |
|
// Find the flex container within account area |
|
const flexContainer = accountContainer.querySelector('.flex.align-center') || accountContainer; |
|
const firstChild = flexContainer.firstElementChild; |
|
if (firstChild) { |
|
const container = createSwitcherButtons(true); |
|
container.classList.add('kagi-switcher-search-position'); |
|
flexContainer.insertBefore(container, firstChild); |
|
console.log('[Kagi Switcher] Inserted buttons in search header'); |
|
return true; |
|
} |
|
} |
|
|
|
// Last resort: Insert after search form |
|
const searchFormBox = document.querySelector('.search_form_box'); |
|
if (searchFormBox && searchFormBox.parentElement) { |
|
const container = createSwitcherButtons(true); |
|
container.classList.add('kagi-switcher-search-position'); |
|
// Insert after the search form box |
|
const nextSibling = searchFormBox.nextElementSibling; |
|
if (nextSibling) { |
|
searchFormBox.parentElement.insertBefore(container, nextSibling); |
|
} else { |
|
searchFormBox.parentElement.appendChild(container); |
|
} |
|
console.log('[Kagi Switcher] Inserted buttons after search form'); |
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
// Main insertion logic |
|
function insertButtons() { |
|
// Check if already inserted |
|
if (document.querySelector('.kagi-switcher-container')) { |
|
return; |
|
} |
|
|
|
const isSearchPage = window.location.pathname.startsWith('/search') || |
|
window.location.pathname.startsWith('/html/search'); |
|
|
|
if (isSearchPage) { |
|
if (!insertOnSearchPage()) { |
|
// Retry after a short delay |
|
setTimeout(insertOnSearchPage, 500); |
|
} |
|
} else { |
|
if (!insertOnHomepage()) { |
|
// Retry after a short delay in case of dynamic loading |
|
setTimeout(insertOnHomepage, 1000); |
|
} |
|
} |
|
} |
|
|
|
// Initialize |
|
if (document.readyState === 'loading') { |
|
document.addEventListener('DOMContentLoaded', insertButtons); |
|
} else { |
|
insertButtons(); |
|
} |
|
|
|
// Handle dynamic page changes (SPA navigation) |
|
const observer = new MutationObserver((mutations) => { |
|
if (!document.querySelector('.kagi-switcher-container')) { |
|
insertButtons(); |
|
} |
|
}); |
|
|
|
observer.observe(document.body, { |
|
childList: true, |
|
subtree: true |
|
}); |
|
|
|
// Also try inserting after a delay for dynamically loaded content |
|
setTimeout(insertButtons, 1500); |
|
|
|
console.log('[Kagi Switcher] Userscript loaded and ready'); |
|
})(); |