Guest User

Untitled

a guest
Jun 30th, 2025
45
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. // ==UserScript==
  2. // @name         Kara Keep Advanced Search Assistant
  3. // @namespace    http://karakeep.app/
  4. // @version      2.0
  5. // @description  Reliable search filter insertion for Kara Keep
  6. // @author       KaraKeep Assistant
  7. // @match        *://karakeep.app/*
  8. // @match        *://try.karakeep.app/*
  9. // @grant        none
  10. // @run-at       document-end
  11. // ==/UserScript==
  12.  
  13. (function() {
  14.     'use strict';
  15.  
  16.     // Configuration
  17.     const ADD_SPACE_AFTER_FILTER = true;
  18.     const VERSION = "2.0";
  19.     const RETRY_LIMIT = 10;
  20.     const RETRY_DELAY = 300;
  21.  
  22.     // Setup logging
  23.     if (!window.__karaKeepBookmarkletLog) window.__karaKeepBookmarkletLog = [];
  24.     const log = msg => {
  25.         window.__karaKeepBookmarkletLog.push({ time: Date.now(), msg });
  26.         console.log("[🔍 KaraKeep Assistant] " + msg);
  27.     };
  28.  
  29.     // Prevent duplicate initialization
  30.     if (window.__karaKeepUIVersion === VERSION) {
  31.         log("UI already active. Use 'removeKaraKeepFilterUI()' to remove");
  32.         return;
  33.     }
  34.     window.__karaKeepUIVersion = VERSION;
  35.  
  36.     log("Activated! Version " + VERSION);
  37.     log(`Space after filters: ${ADD_SPACE_AFTER_FILTER ? "ENABLED" : "DISABLED"}`);
  38.  
  39.     // UI Elements (created once and reused)
  40.     const btn = document.createElement("button");
  41.     btn.id = "karaKeepFilterBtn";
  42.     btn.textContent = "Search Filters";
  43.     btn.type = "button";
  44.     btn.style.cssText = `
  45.         display: inline-flex;
  46.         align-items: center;
  47.         justify-content: center;
  48.         white-space: nowrap;
  49.         border-radius: 0.375rem;
  50.         padding: 0 1rem;
  51.         height: 2.5rem;
  52.         font-size: 0.875rem;
  53.         font-weight: 500;
  54.         cursor: pointer;
  55.         background-color: transparent;
  56.         border: none;
  57.         color: hsl(var(--foreground));
  58.         transition: background-color 0.2s ease;
  59.         margin-left: 0.5rem;
  60.     `;
  61.  
  62.     const menu = document.createElement("div");
  63.     menu.id = "karaKeepFilterMenu";
  64.     menu.style.cssText = `
  65.         display: none;
  66.         position: fixed;
  67.         z-index: 100000;
  68.         background: #fff;
  69.         box-shadow: 0 8px 20px rgba(0,0,0,.18);
  70.         border: 1px solid #ccc;
  71.         border-radius: 6px;
  72.         padding: 8px 0;
  73.         color: #222;
  74.         font-size: 15px;
  75.         max-height: 260px;
  76.         overflow-y: auto;
  77.     `;
  78.  
  79.     // Filters list
  80.     const FILTERS = [
  81.         { name: "Favorited", value: "is:fav", desc: "Favorited bookmarks" },
  82.         { name: "Archived", value: "is:archived", desc: "Archived bookmarks" },
  83.         { name: "Tagged", value: "is:tagged", desc: "Has one or more tags" },
  84.         { name: "In List", value: "is:inlist", desc: "Is in at least one list" },
  85.         { name: "Type: Link", value: "is:link", desc: "Type: link" },
  86.         { name: "Type: Text", value: "is:text", desc: "Type: text" },
  87.         { name: "Type: Media", value: "is:media", desc: "Type: media" },
  88.         { name: "URL", value: "url:example.com", desc: "Match URL substring" },
  89.         { name: "After Date", value: "after:2024-01-01", desc: "After date (YYYY-MM-DD)" },
  90.         { name: "Before Date", value: "before:2025-01-01", desc: "Before date (YYYY-MM-DD)" },
  91.         { name: "Tag", value: "#important", desc: "With tag" },
  92.         { name: "List", value: 'list:"to review"', desc: "In specific list" },
  93.         { name: "Feed", value: "feed:Hackernews", desc: "Imported from feed" },
  94.         { name: "Age <1w", value: "age:<1w", desc: "Younger than 1 week" },
  95.         { name: "Age >1y", value: "age:>1y", desc: "Older than 1 year" }
  96.     ];
  97.  
  98.     // Initialize UI components
  99.     function initUI() {
  100.         // Create filter menu items
  101.         FILTERS.forEach(f => {
  102.             const item = document.createElement("button");
  103.             item.textContent = f.name;
  104.             item.title = f.desc;
  105.             item.style.cssText = `
  106.                 display: block;
  107.                 width: 100%;
  108.                 background: none;
  109.                 border: none;
  110.                 text-align: left;
  111.                 padding: 6px 20px;
  112.                 cursor: pointer;
  113.                 font-size: 15px;
  114.                 color: #222;
  115.                 outline: none;
  116.                 transition: background 0.2s;
  117.             `;
  118.             item.addEventListener('mouseenter', () => item.style.background = "#e6f0fa");
  119.             item.addEventListener('mouseleave', () => item.style.background = "none");
  120.            
  121.             item.addEventListener('click', e => {
  122.                 e.preventDefault();
  123.                 const searchInput = document.querySelector('input[placeholder="Search"]');
  124.                 if (!searchInput) return;
  125.                
  126.                 searchInput.focus();
  127.                 let start = searchInput.selectionStart;
  128.                 let end = searchInput.selectionEnd;
  129.                
  130.                 if (typeof start === "number") {
  131.                     const orig = searchInput.value;
  132.                     const insertText = f.value + (ADD_SPACE_AFTER_FILTER ? " " : "");
  133.                     searchInput.value = orig.slice(0, start) + insertText + orig.slice(end);
  134.                     searchInput.selectionStart = searchInput.selectionEnd = start + insertText.length;
  135.                    
  136.                     // Trigger input event for React
  137.                     const inputEvent = new Event('input', { bubbles: true });
  138.                     searchInput.dispatchEvent(inputEvent);
  139.                 } else {
  140.                     searchInput.value += f.value + (ADD_SPACE_AFTER_FILTER ? " " : "");
  141.                 }
  142.                
  143.                 menu.style.display = "none";
  144.                 log(`Added filter: '${f.value}${ADD_SPACE_AFTER_FILTER ? ' ' : ''}'`);
  145.             });
  146.             menu.appendChild(item);
  147.         });
  148.  
  149.         // Button hover effects
  150.         btn.addEventListener('mouseenter', () => {
  151.             btn.style.backgroundColor = 'hsl(var(--accent))';
  152.         });
  153.         btn.addEventListener('mouseleave', () => {
  154.             btn.style.backgroundColor = 'transparent';
  155.         });
  156.  
  157.         // Menu toggle
  158.         btn.addEventListener('click', e => {
  159.             e.stopPropagation();
  160.             menu.style.display = menu.style.display === "block" ? "none" : "block";
  161.             if (menu.style.display === "block") {
  162.                 const rect = btn.getBoundingClientRect();
  163.                 menu.style.top = (rect.bottom + 4) + "px";
  164.                 menu.style.left = rect.left + "px";
  165.                 if (!document.getElementById('karaKeepFilterMenu')) {
  166.                     document.body.appendChild(menu);
  167.                 }
  168.             }
  169.         });
  170.  
  171.         // Close menu when clicking outside
  172.         document.addEventListener("mousedown", e => {
  173.             if (!btn.contains(e.target) && !menu.contains(e.target)) {
  174.                 menu.style.display = "none";
  175.             }
  176.         });
  177.     }
  178.  
  179.     // Add styles to document
  180.     function addStyles() {
  181.         const style = document.createElement('style');
  182.         style.id = 'kara-keep-assistant-styles';
  183.         style.textContent = `
  184.             .kara-keep-filter-btn {
  185.                 background-color: transparent;
  186.                 transition: background-color 0.2s ease;
  187.                 margin-left: 0.5rem;
  188.             }
  189.             .kara-keep-filter-btn:hover {
  190.                 background-color: hsl(var(--accent)) !important;
  191.             }
  192.             #karaKeepFilterMenu {
  193.                 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
  194.                 z-index: 100000;
  195.             }
  196.             #karaKeepFilterMenu button:hover {
  197.                 background-color: #e6f0fa !important;
  198.             }
  199.         `;
  200.         document.head.appendChild(style);
  201.         return style;
  202.     }
  203.  
  204.     // Inject button into toolbar
  205.     function injectButton(retryCount = 0) {
  206.         if (retryCount > RETRY_LIMIT) {
  207.             log("Failed to find button container after " + RETRY_LIMIT + " attempts");
  208.             return;
  209.         }
  210.  
  211.         const buttonsContainer = document.querySelector('.flex.min-w-max.flex-wrap.overflow-hidden');
  212.         if (!buttonsContainer) {
  213.             setTimeout(() => injectButton(retryCount + 1), RETRY_DELAY);
  214.             return;
  215.         }
  216.  
  217.         // Check if button already exists
  218.         if (document.getElementById('karaKeepFilterBtn')) {
  219.             return;
  220.         }
  221.  
  222.         // Find insertion point
  223.         const editButton = buttonsContainer.querySelector('button:not([disabled])');
  224.         if (editButton) {
  225.             buttonsContainer.insertBefore(btn, editButton);
  226.             log("UI injected successfully!");
  227.         } else {
  228.             buttonsContainer.appendChild(btn);
  229.             log("UI injected at end of buttons");
  230.         }
  231.     }
  232.  
  233.     // Setup DOM observer
  234.     function setupObserver() {
  235.         const observer = new MutationObserver(mutations => {
  236.             // Check if our button was removed
  237.             if (!document.contains(btn)) {
  238.                 log("Button was removed - reinjecting");
  239.                 injectButton();
  240.             }
  241.         });
  242.  
  243.         observer.observe(document.body, {
  244.             childList: true,
  245.             subtree: true
  246.         });
  247.     }
  248.  
  249.     // Cleanup function
  250.     function setupCleanup() {
  251.         window.removeKaraKeepFilterUI = () => {
  252.             btn.remove();
  253.             menu.remove();
  254.             const styles = document.getElementById('kara-keep-assistant-styles');
  255.             if (styles) styles.remove();
  256.             window.__karaKeepUIVersion = null;
  257.             log("Assistant removed. Refresh page to restore original UI");
  258.         };
  259.     }
  260.  
  261.     // Main initialization
  262.     function init() {
  263.         log("Initializing UI components...");
  264.         initUI();
  265.         const styles = addStyles();
  266.         injectButton();
  267.         setupObserver();
  268.         setupCleanup();
  269.        
  270.         // Add menu to DOM
  271.         document.body.appendChild(menu);
  272.        
  273.         log("Assistant is fully initialized");
  274.     }
  275.  
  276.     // Start initialization when DOM is ready
  277.     if (document.readyState === 'loading') {
  278.         document.addEventListener('DOMContentLoaded', init);
  279.     } else {
  280.         setTimeout(init, 1000);
  281.     }
  282. })();
  283.  
Advertisement
Add Comment
Please, Sign In to add comment