Last active
June 24, 2025 22:30
-
-
Save schuhwerk/fe21d0118d885c7697e1962a9696c3f0 to your computer and use it in GitHub Desktop.
Search to Anki (Airtable Edition)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Search to Anki (Airtable Edition) - Incognito Aware | |
// @namespace https://violentmonkey.github.io/ | |
// @version 4.72 | |
// @description Automatically detects recurring Google/DuckDuckGo/etc searches and prompts you to save them as flashcards in an Airtable base. Does not track in Incognito/Private mode. | |
// @author Vitus Schuhwerk (Refactored by AI) | |
// @homepageURL https://gist.github.com/schuhwerk/fe21d0118d885c7697e1962a9696c3f0 | |
// @updateURL https://gist.github.com/schuhwerk/fe21d0118d885c7697e1962a9696c3f0/raw | |
// @match https://www.google.com/search* | |
// @match https://duckduckgo.com/ | |
// @match https://www.bing.com/search* | |
// @match https://search.brave.com/search* | |
// @match https://www.ecosia.org/search* | |
// @grant GM_setValue | |
// @grant GM_getValue | |
// @grant GM_deleteValue | |
// @grant GM_registerMenuCommand | |
// @grant GM_xmlhttpRequest | |
// @grant GM_addStyle | |
// @grant GM_info | |
// @connect api.airtable.com | |
// ==/UserScript== | |
(async function() { | |
'use strict'; | |
// ================================================================================= | |
// SETUP GUIDE (AIRTABLE) | |
// ================================================================================= | |
// | |
// 1. PREREQUISITES: | |
// - ⚠️ Be careful who to trust with your data! This stores all your searches. | |
// - Violentmonkey Browser Extension (or another userscript manager). | |
// - An Airtable account. | |
// | |
// 2. AIRTABLE SETUP: | |
// a. Create a new Base in Airtable (e.g., "My Anki Cards"). | |
// b. Inside the base, you'll have a default table. Rename it if you like (e.g., "Searches"). | |
// c. Your table MUST have fields (columns) with these exact names and types: | |
// - "Question" (Field type: Single line text or Long text) | |
// - "Answer" (Field type: Single line text or Long text) | |
// - "Tags" (Field type: Single line text or Multiple select) | |
// | |
// 3. GET YOUR AIRTABLE CREDENTIALS: | |
// a. API Key: Go to your Airtable Account page (https://airtable.com/account) to find your API key. | |
// b. Base ID: Go to the API documentation for your base (https://airtable.com/api), select your base, and you'll find the ID in the introduction section (it starts with "app..."). | |
// c. Table Name: This is simply the name of the table you created in step 2b. | |
// | |
// 4. INITIAL SCRIPT CONFIGURATION: | |
// - After installing this script, go to a supported search engine (like Google). | |
// - When you search for something repeatedly, the script will prompt you to configure it. | |
// - Alternatively, click the Violentmonkey extension icon, and select "⚙️ Configure Settings". | |
// | |
// ================================================================================= | |
// --- SCRIPT CONSTANTS --- | |
const DEBUG = false; | |
// --- DEFAULT CONFIGURATION --- | |
const defaultConfig = { | |
searchRepetitionThreshold: 3, | |
searchPeriodInMonths: 12, | |
fuzzinessThreshold: 0.8, | |
ankiCooldownInMonths: 6, | |
airtableApiKey: '', | |
airtableBaseId: '', | |
airtableTableName: '', | |
}; | |
let config = {}; | |
// --- LOGGER --- | |
const log = (...args) => { if (DEBUG) console.log('[SearchToAnki]', ...args); }; | |
const logError = (...args) => { console.error('[SearchToAnki]', ...args); }; | |
// --- INCOGNITO MODE CHECK --- | |
const isIncognito = async () => { | |
// WebExtensions API (Chrome, Firefox) | |
if (typeof browser !== 'undefined' && browser.extension && typeof browser.extension.isAllowedIncognitoAccess === 'function') { | |
const hasAccess = await browser.extension.isAllowedIncognitoAccess(); | |
// If the extension has access, we need to check if the window is actually incognito | |
if (hasAccess) { | |
return browser.extension.inIncognitoContext; | |
} | |
} | |
// Fallback for other userscript managers (e.g., Tampermonkey) | |
if (typeof GM_info !== 'undefined' && GM_info.isIncognito) { | |
return true; | |
} | |
return false; // If the API is blocked or absent | |
}; | |
// --- CONFIGURATION LOADER & MENU --- | |
const loadConfig = async () => { | |
const savedConfig = await GM_getValue('config', {}); | |
config = { ...defaultConfig, ...savedConfig }; | |
log('Configuration loaded:', config); | |
}; | |
const showConfigModal = async () => { | |
if (document.getElementById('anki-config-modal')) return; | |
const currentConfig = { ...defaultConfig, ...(await GM_getValue('config', {})) }; | |
const modalHTML = ` | |
<div id="anki-config-overlay"></div> | |
<div id="anki-config-modal"> | |
<h3>⚙️ Search to Anki Configuration</h3> | |
<form id="anki-config-form"> | |
<p>Enter your Airtable credentials and adjust settings.</p> | |
<div class="form-group"> | |
<label for="cfg-airtableApiKey">Airtable API Key</label> | |
<input type="password" id="cfg-airtableApiKey" value="${currentConfig.airtableApiKey}" required> | |
</div> | |
<div class="form-group"> | |
<label for="cfg-airtableBaseId">Airtable Base ID</label> | |
<input type="text" id="cfg-airtableBaseId" value="${currentConfig.airtableBaseId}" placeholder="appXXXXXXXXXXXXXX" required> | |
</div> | |
<div class="form-group"> | |
<label for="cfg-airtableTableName">Airtable Table Name</label> | |
<input type="text" id="cfg-airtableTableName" value="${currentConfig.airtableTableName}" required> | |
</div> | |
<hr> | |
<div class="form-group"> | |
<label for="cfg-searchRepetitionThreshold">Search Repetition Threshold</label> | |
<input type="number" id="cfg-searchRepetitionThreshold" value="${currentConfig.searchRepetitionThreshold}" min="1" required> | |
</div> | |
<div class="form-group"> | |
<label for="cfg-fuzzinessThreshold">Fuzziness Threshold (0.1 - 1.0)</label> | |
<input type="number" id="cfg-fuzzinessThreshold" value="${currentConfig.fuzzinessThreshold}" min="0.1" max="1.0" step="0.05" required> | |
</div> | |
<div class="form-group"> | |
<label for="cfg-ankiCooldownInMonths">Cooldown in Months</label> | |
<input type="number" id="cfg-ankiCooldownInMonths" value="${currentConfig.ankiCooldownInMonths}" min="0" required> | |
</div> | |
<div class="button-group"> | |
<button type="submit" id="config-save">Save & Close</button> | |
<button type="button" id="config-cancel">Cancel</button> | |
</div> | |
</form> | |
</div>`; | |
document.body.insertAdjacentHTML('beforeend', modalHTML); | |
const closeModal = () => { | |
document.getElementById('anki-config-modal')?.remove(); | |
document.getElementById('anki-config-overlay')?.remove(); | |
}; | |
document.getElementById('anki-config-form').addEventListener('submit', async (e) => { | |
e.preventDefault(); | |
const newConfig = { | |
airtableApiKey: document.getElementById('cfg-airtableApiKey').value.trim(), | |
airtableBaseId: document.getElementById('cfg-airtableBaseId').value.trim(), | |
airtableTableName: document.getElementById('cfg-airtableTableName').value.trim(), | |
searchRepetitionThreshold: parseInt(document.getElementById('cfg-searchRepetitionThreshold').value, 10), | |
fuzzinessThreshold: parseFloat(document.getElementById('cfg-fuzzinessThreshold').value), | |
ankiCooldownInMonths: parseInt(document.getElementById('cfg-ankiCooldownInMonths').value, 10), | |
}; | |
await GM_setValue('config', newConfig); | |
await loadConfig(); | |
closeModal(); | |
}); | |
document.getElementById('config-cancel').addEventListener('click', closeModal); | |
document.getElementById('anki-config-overlay').addEventListener('click', closeModal); | |
}; | |
const resetConfig = async () => { | |
if (confirm('Are you sure you want to reset all settings? This will also clear your cooldown tracker for already-added cards.')) { | |
await GM_deleteValue('config'); | |
await GM_deleteValue('addedAnkiCards'); | |
await loadConfig(); | |
alert('Configuration has been reset. Please reload the page.'); | |
} | |
}; | |
await loadConfig(); | |
GM_registerMenuCommand('⚙️ Configure Settings', showConfigModal); | |
GM_registerMenuCommand('🔥 Reset to Defaults', resetConfig); | |
// --- UI STYLES --- | |
GM_addStyle(` | |
:root{--bg-color:#fff;--text-color:#24292f;--field-bg-color:#f6f8fa;--field-text-color:#24292f;--border-color:#d0d7de;--primary-color:#0969da;--primary-text-color:#fff;--shadow-color:rgba(140,149,159,.3);--success-color:#238636;--error-color:#cf222e;} | |
@media (prefers-color-scheme:dark){:root{--bg-color:#1c2128;--text-color:#c9d1d9;--field-bg-color:#0d1117;--field-text-color:#c9d1d9;--border-color:#30363d;--primary-color:#238636;--success-color:#3fb950;--error-color:#f85149;--shadow-color:rgba(0,0,0,.4)}} | |
@keyframes fadeIn{from{opacity:0;transform:scale(0.95)}to{opacity:1;transform:scale(1)}} | |
#anki-dialog{position:fixed;top:25px;right:25px;background-color:var(--bg-color);color:var(--text-color);border:1px solid var(--border-color);padding:1.5rem;z-index:9999;box-shadow:0 8px 24px var(--shadow-color);max-width:420px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif;border-radius:12px;animation:fadeIn .2s ease-out;display:flex;flex-direction:column;gap:1rem;min-height:150px;box-sizing:border-box;} | |
#anki-dialog-form-content{display:flex;flex-direction:column;gap:1rem;} | |
#anki-dialog h3{margin:0;font-size:1.1rem;font-weight:600} | |
#anki-dialog ul{max-height:150px;overflow-y:auto;padding:.75rem 1rem;border:1px solid var(--border-color);background-color:var(--field-bg-color);border-radius:8px;list-style-position:inside} | |
#anki-dialog li b{font-weight:500} | |
#anki-dialog .anki-form-group{display:flex;flex-direction:column;gap:.5rem} | |
#anki-dialog input,#anki-dialog textarea{width:100%;padding:8px 12px;background-color:var(--field-bg-color);color:var(--field-text-color);border:1px solid var(--border-color);border-radius:6px;box-sizing:border-box;} | |
#anki-dialog .anki-button-group{display:flex;flex-wrap:wrap;gap:.5rem;margin-top:.5rem} | |
#anki-dialog button{padding:5px 12px;font-size:.85rem;font-weight:500;border:1px solid var(--border-color);border-radius:6px;cursor:pointer;background-color:var(--field-bg-color);color:var(--text-color);box-sizing:border-box;} | |
#anki-dialog button:disabled{opacity:0.6;cursor:not-allowed;} | |
#anki-dialog button#anki-yes{background-color:var(--primary-color);color:var(--primary-text-color);border-color:var(--primary-color)} | |
/* Feedback states */ | |
.feedback-container{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:0.5rem;flex-grow:1;} | |
.feedback-container .feedback-icon{width:48px;height:48px;} | |
.feedback-container.success .feedback-icon{stroke:var(--success-color);} | |
.feedback-container p{font-size:1rem;margin:0;} | |
.feedback-container button{margin-top:1rem;} | |
/* Configuration Modal Styles... */ | |
#anki-config-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background-color:rgba(0,0,0,0.5);z-index:10000;animation:fadeIn 0.2s linear;} | |
#anki-config-modal{position:fixed;top:50%;left:50%;transform:translate(-50%, -50%);background-color:var(--bg-color);color:var(--text-color);padding:2rem;border-radius:12px;box-shadow:0 8px 24px var(--shadow-color);z-index:10001;width:90%;max-width:500px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif;animation:fadeIn .2s ease-out;} | |
#anki-config-modal h3{margin-top:0;} #anki-config-modal p{margin-top:0;margin-bottom:1.5rem;} #anki-config-modal hr{border:none;border-top:1px solid var(--border-color);margin:1.5rem 0;} | |
#anki-config-form .form-group{display:flex;flex-direction:column;margin-bottom:1rem;} #anki-config-form label{margin-bottom:0.5rem;font-weight:500;} | |
#anki-config-form input{padding:8px 12px;border:1px solid var(--border-color);border-radius:6px;background-color:var(--field-bg-color);color:var(--field-text-color);} | |
#anki-config-modal .button-group{display:flex;gap:1rem;justify-content:flex-end;margin-top:1.5rem;} #anki-config-modal button{padding:8px 16px;border-radius:6px;border:1px solid var(--border-color);cursor:pointer;font-weight:500;} | |
#anki-config-modal #config-save{background-color:var(--primary-color);color:var(--primary-text-color);border-color:var(--primary-color);} | |
`); | |
// --- CORE LOGIC & HELPER FUNCTIONS --- | |
const levenshtein = (a, b) => { const m = Array(b.length+1).fill(null).map(()=>Array(a.length+1).fill(null));for(let i=0;i<=a.length;i+=1){m[0][i]=i}for(let j=0;j<=b.length;j+=1){m[j][0]=j}for(let j=1;j<=b.length;j+=1){for(let i=1;i<=a.length;i+=1){const ind=a[i-1]===b[j-1]?0:1;m[j][i]=Math.min(m[j][i-1]+1,m[j-1][i]+1,m[j-1][i-1]+ind)}}return m[b.length][a.length]}; | |
const calculateSimilarity = (a, b) => {if(!a||!b)return 0;const d=levenshtein(a.toLowerCase(),b.toLowerCase());const maxL=Math.max(a.length,b.length);if(maxL===0)return 1;return 1-(d/maxL)}; | |
const logSearch = async (query) => {let h=await GM_getValue('searchHistory',{});const n=new Date().toISOString();const r=Object.keys(h).slice(-10);for(const p of r){if(calculateSimilarity(query,p)>0.95){h[p].push(n);await GM_setValue('searchHistory',h);return}}if(!h[query]){h[query]=[]}h[query].push(n);await GM_setValue('searchHistory',h)}; | |
const getSimilarSearches = async (q) => {const h=await GM_getValue('searchHistory',{});const s=[];let t=[];for(const p in h){if(calculateSimilarity(q,p)>=config.fuzzinessThreshold){const ts=h[p];s.push({query:p,timestamps:ts});t=t.concat(ts)}}return{similarSearches:s,allTimestamps:t}}; | |
const isNthSearch = (ts,n,p) => {const N=new Date();const s=new Date(N.setMonth(N.getMonth()-p));const r=ts.filter(t=>new Date(t)>=s);log(`Found ${r.length} similar searches in the last ${p} months.`);return r.length>=n}; | |
const isRecentlyAdded = async (q) => {const c=await GM_getValue('addedAnkiCards',{});if(Object.keys(c).length===0)return!1;const N=new Date();const s=new Date(new Date().setMonth(N.getMonth()-config.ankiCooldownInMonths));for(const a in c){if(calculateSimilarity(q,a)>=config.fuzzinessThreshold){const d=new Date(c[a]);if(d>=s){log(`Query is similar to a recently added card ("${a}") from ${d.toLocaleDateString()}. Cooldown is active.`);return!0}}}return!1}; | |
const showAnkiDialog = (currentQuery, similarSearchResults) => { | |
const dialogId = 'anki-dialog'; | |
if (document.getElementById(dialogId)) return; | |
let pastSearchesList = similarSearchResults.map(result => `<li><b>${result.query}</b> (${result.timestamps.length} time(s))</li>`).join(''); | |
const dialogHTML = ` | |
<div id="${dialogId}"> | |
<div id="anki-dialog-form-content"> | |
<h3>You've asked similar questions...</h3> | |
<ul>${pastSearchesList}</ul> | |
<div class="anki-form-group"> | |
<label for="anki-query">Do you want to add this to your Airtable?</label> | |
<input type="text" id="anki-query" placeholder="Question" value="${currentQuery}"> | |
</div> | |
<div class="anki-form-group"> | |
<label for="anki-answer">Answer:</label> | |
<textarea id="anki-answer" rows="4" placeholder="Answer"></textarea> | |
</div> | |
<div class="anki-button-group"> | |
<button id="anki-yes">Yes, Add to Airtable</button> | |
<button id="anki-not-now">Not Now</button> | |
<button id="anki-dont-ask-again">Never for this search</button> | |
</div> | |
</div> | |
<div id="anki-dialog-feedback-content" style="display: none;"></div> | |
</div>`; | |
document.body.insertAdjacentHTML('beforeend', dialogHTML); | |
const dialogElement = document.getElementById(dialogId); | |
const formContent = document.getElementById('anki-dialog-form-content'); | |
const feedbackContent = document.getElementById('anki-dialog-feedback-content'); | |
const yesButton = document.getElementById('anki-yes'); | |
const otherButtons = dialogElement.querySelectorAll('.anki-button-group button:not(#anki-yes)'); | |
// Capture the initial, ideal size of the dialog window. | |
const initialDialogWidth = dialogElement.offsetWidth; | |
const initialDialogHeight = dialogElement.offsetHeight; | |
const showLoadingState = () => { | |
// Lock the dialog's size to its initial state to prevent resizing. | |
dialogElement.style.width = `${initialDialogWidth}px`; | |
dialogElement.style.height = `${initialDialogHeight}px`; | |
// Set the button's min-width to its current width before changing the text. | |
yesButton.style.minWidth = `${yesButton.offsetWidth}px`; | |
yesButton.disabled = true; | |
yesButton.textContent = 'Saving...'; | |
otherButtons.forEach(btn => btn.disabled = true); | |
}; | |
const showSuccessState = () => { | |
formContent.style.display = 'none'; | |
feedbackContent.innerHTML = ` | |
<div class="feedback-container success"> | |
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" class="feedback-icon"><polyline points="20 6 9 17 4 12"></polyline></svg> | |
<p>Card saved!</p> | |
</div>`; | |
feedbackContent.style.display = 'flex'; | |
setTimeout(() => dialogElement.remove(), 1500); | |
}; | |
const showErrorState = (message) => { | |
formContent.style.display = 'none'; | |
feedbackContent.innerHTML = ` | |
<div class="feedback-container error"> | |
<p><b>Error:</b> ${message}</p> | |
<button id="anki-try-again">Try Again</button> | |
</div>`; | |
feedbackContent.style.display = 'flex'; | |
document.getElementById('anki-try-again').addEventListener('click', () => { | |
feedbackContent.style.display = 'none'; | |
formContent.style.display = 'flex'; | |
// Reset dialog and button size properties before restoring form | |
dialogElement.style.width = ''; | |
dialogElement.style.height = ''; | |
yesButton.style.minWidth = ''; | |
yesButton.disabled = false; | |
yesButton.textContent = 'Yes, Add to Airtable'; | |
otherButtons.forEach(btn => btn.disabled = false); | |
}); | |
}; | |
yesButton.addEventListener('click', async () => { | |
const modifiedQuery = document.getElementById('anki-query').value; | |
const answer = document.getElementById('anki-answer').value; | |
if (!modifiedQuery || !answer) { | |
alert('Please provide both a Question and an Answer.'); | |
return; | |
} | |
showLoadingState(); | |
const result = await addSearchToAirtable(modifiedQuery, answer, currentQuery); | |
if (result.success) { | |
showSuccessState(); | |
} else { | |
showErrorState(result.error); | |
} | |
}); | |
document.getElementById('anki-not-now').addEventListener('click', () => dialogElement.remove()); | |
document.getElementById('anki-dont-ask-again').addEventListener('click', async () => { | |
let ignoredSearches = await GM_getValue('ignoredSearches', []); | |
ignoredSearches.push(currentQuery); | |
await GM_setValue('ignoredSearches', ignoredSearches); | |
dialogElement.remove(); | |
}); | |
document.getElementById('anki-answer').focus(); | |
}; | |
// --- AIRTABLE API FUNCTION --- | |
const addCardToAirtableApi = (question, answer) => { | |
const url = `https://api.airtable.com/v0/${config.airtableBaseId}/${config.airtableTableName}`; | |
const record = { fields: { "Question": question, "Answer": answer, "Tags": "from_search" } }; | |
return new Promise((resolve, reject) => { | |
GM_xmlhttpRequest({ | |
method: 'POST', | |
url: url, | |
headers: { 'Authorization': `Bearer ${config.airtableApiKey}`, 'Content-Type': 'application/json' }, | |
data: JSON.stringify({ records: [record] }), | |
onload: (response) => { | |
if (response.status >= 200 && response.status < 300) { | |
resolve(JSON.parse(response.responseText)); | |
} else { | |
reject({ status: response.status, responseText: response.responseText }); | |
} | |
}, | |
onerror: (error) => reject({ status: 'NETWORK_ERROR', responseText: 'Could not connect to Airtable.'}) | |
}); | |
}); | |
}; | |
const addSearchToAirtable = async (query, answer, originalQuery) => { | |
try { | |
await addCardToAirtableApi(query, answer); | |
const addedAnkiCards = await GM_getValue('addedAnkiCards', {}); | |
// Add the final, user-approved query to the cooldown list | |
addedAnkiCards[query] = new Date().toISOString(); | |
log(`Added "${query}" to the cooldown list.`); | |
// If the user edited the question, also add the original query to the cooldown list. | |
if (originalQuery && query !== originalQuery) { | |
addedAnkiCards[originalQuery] = new Date().toISOString(); | |
log(`Also added original trigger "${originalQuery}" to the cooldown list.`); | |
} | |
await GM_setValue('addedAnkiCards', addedAnkiCards); | |
return { success: true }; | |
} catch (error) { | |
logError('Failed to add card to Airtable:', error); | |
let message = 'An unknown error occurred.'; | |
if (error.status === 401) { | |
message = 'Authentication failed. Is your API Key correct?'; | |
} else if (error.status === 404) { | |
message = 'Base or Table not found. Check your Base ID and Table Name.'; | |
} else if (error.status === 422) { | |
message = 'Invalid field data. Do "Question", "Answer", and "Tags" fields exist in your table?'; | |
} else if (error.status) { | |
message = `API returned status ${error.status}. Check console for details.`; | |
} else { | |
message = `Could not connect to Airtable. Check your network.`; | |
} | |
return { success: false, error: message }; | |
} | |
}; | |
// --- MAIN EXECUTION --- | |
const main = async () => { | |
if (await isIncognito()) { | |
log('Incognito mode detected. Tracking is disabled.'); | |
return; | |
} | |
const searchEngines = [ | |
{ name: 'Google', host: 'google.com', queryParam: 'q', paginationParam: 'start' }, | |
{ name: 'DuckDuckGo', host: 'duckduckgo.com', queryParam: 'q', paginationParam: 's' }, | |
{ name: 'Bing', host: 'bing.com', queryParam: 'q', paginationParam: 'first' }, | |
{ name: 'Brave', host: 'search.brave.com', queryParam: 'q', paginationParam: 'offset' }, | |
{ name: 'Ecosia', host: 'ecosia.org', queryParam: 'q', paginationParam: 'p' } | |
]; | |
const currentHost = window.location.hostname.replace('www.', ''); | |
const currentEngine = searchEngines.find(e => currentHost === e.host); | |
if (!currentEngine) return; | |
log(`Running on ${currentEngine.name}.`); | |
const urlParams = new URLSearchParams(window.location.search); | |
const query = urlParams.get(currentEngine.queryParam)?.trim(); | |
if (!query) { log('No search query found in URL.'); return; } | |
log(`Detected query: "${query}"`); | |
// --- PAGINATION CHECK --- | |
const pageParam = currentEngine.paginationParam; | |
if (pageParam && urlParams.has(pageParam)) { | |
const pageValue = parseInt(urlParams.get(pageParam), 10); | |
if (!isNaN(pageValue) && pageValue > 0 && pageParam !== 's') { // DDG uses 's' for offsets, not pages | |
log(`Pagination detected (${pageParam}=${urlParams.get(pageParam)}). Not tracking.`); | |
return; | |
} | |
} | |
// ------------------------ | |
await logSearch(query); | |
if ((await GM_getValue('ignoredSearches', [])).includes(query)) { log('Query is on the manual ignore list. Aborting.'); return; } | |
if (await isRecentlyAdded(query)) { log('Query is on cooldown from a recent add. Aborting.'); return; } | |
const { similarSearches, allTimestamps } = await getSimilarSearches(query); | |
if (isNthSearch(allTimestamps, config.searchRepetitionThreshold, config.searchPeriodInMonths)) { | |
const isConfigured = config.airtableApiKey && config.airtableBaseId && config.airtableTableName; | |
if (isConfigured) { | |
showAnkiDialog(query, similarSearches); | |
} else { | |
log('Threshold met, but script is not configured. Opening settings.'); | |
alert("It looks like you're searching for this often! Please configure the Airtable connection to start saving cards."); | |
showConfigModal(); | |
} | |
} else { | |
log('Repetition threshold not met. Not showing dialog.'); | |
} | |
}; | |
main(); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment