Skip to content

Instantly share code, notes, and snippets.

@eros18123
Last active February 23, 2026 20:53
Show Gist options
  • Select an option

  • Save eros18123/7b8424c0aab78fe2cd7f0f17f55a2f03 to your computer and use it in GitHub Desktop.

Select an option

Save eros18123/7b8424c0aab78fe2cd7f0f17f55a2f03 to your computer and use it in GitHub Desktop.
Pesquisar deck e subdeck 2
# -*- coding: utf-8 -*-
# Nome do Addon: Pesquisa e Navegação de Decks Unificado (Auto-expansão Inteligente)
# Funcionalidades: Pesquisa, destaque, contador, ignora acentos, acha subdecks, AUTO-EXPANDE decks pais e Ocultar/Mostrar Barra.
import os
import json
from aqt import mw, gui_hooks
from aqt.qt import QTimer
# --- INÍCIO: LÓGICA DE CONFIGURAÇÃO E ESTADO ---
# Pega o caminho exato da pasta deste addon
ADDON_DIR = os.path.dirname(__file__)
CONFIG_FILE = os.path.join(ADDON_DIR, "user_config.json")
DEFAULT_CONFIG = {
"search_bar_visible": True
}
# Variável global para lembrar quais decks estavam fechados antes da pesquisa começar
addon_state = {
"is_searching": False,
"original_collapsed": {}
}
def carregar_config():
"""Lê o arquivo JSON diretamente da pasta do addon."""
try:
if os.path.exists(CONFIG_FILE):
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
print(f"[DeckSearchNavigation] Erro ao carregar config: {e}")
# Se não existir ou der erro, retorna o padrão
return DEFAULT_CONFIG.copy()
def salvar_config(config):
"""Força a criação/atualização do arquivo JSON na pasta do addon."""
try:
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
json.dump(config, f, indent=4)
except Exception as e:
print(f"[DeckSearchNavigation] Erro ao salvar config: {e}")
# --- FIM: LÓGICA DE CONFIGURAÇÃO E ESTADO ---
# --- INÍCIO: MANIPULADOR DE MENSAGENS DO JAVASCRIPT ---
def on_js_message(handled, message, context):
global addon_state
# Salva a preferência de visibilidade da barra no user_config.json
if message.startswith("save_search_state:"):
try:
new_state_str = message.split(":")[1]
new_state_bool = new_state_str.lower() == 'true'
config = carregar_config()
config["search_bar_visible"] = new_state_bool
salvar_config(config)
return (True, None)
except Exception as e:
print(f"[DeckSearchNavigation] Erro ao salvar estado: {e}")
return (False, None)
# Recebe o comando para expandir decks pais ocultos
elif message.startswith("auto_expand:"):
try:
missing_ids = json.loads(message.split(":", 1)[1])
changed = False
if not addon_state["is_searching"]:
addon_state["is_searching"] = True
addon_state["original_collapsed"].clear()
try:
for d in mw.col.decks.all_names_and_ids():
deck = mw.col.decks.get(d.id)
if deck:
addon_state["original_collapsed"][d.id] = deck.get('collapsed', False)
except AttributeError:
for d in mw.col.decks.all():
addon_state["original_collapsed"][d['id']] = d.get('collapsed', False)
for did_str in missing_ids:
try:
did = int(did_str)
deck = mw.col.decks.get(did)
if not deck: continue
name_parts = deck['name'].split('::')
current_name = ""
for i in range(len(name_parts) - 1):
current_name += ("::" if i > 0 else "") + name_parts[i]
parent_id = mw.col.decks.id(current_name, create=False)
if parent_id:
p_deck = mw.col.decks.get(parent_id)
if p_deck and p_deck.get('collapsed', False):
p_deck['collapsed'] = False
mw.col.decks.save(p_deck)
changed = True
except Exception as e:
print(f"Erro ao processar deck {did_str}: {e}")
if changed:
mw.deckBrowser.refresh()
return (True, None)
except Exception as e:
print(f"Erro no auto_expand: {e}")
return (False, None)
# Restaura os decks para o estado fechado original quando a pesquisa é limpa
elif message == "restore_collapse":
try:
if addon_state["is_searching"]:
changed = False
for did, was_collapsed in addon_state["original_collapsed"].items():
deck = mw.col.decks.get(did)
if deck and deck.get('collapsed', False) != was_collapsed:
deck['collapsed'] = was_collapsed
mw.col.decks.save(deck)
changed = True
addon_state["is_searching"] = False
addon_state["original_collapsed"].clear()
if changed:
mw.deckBrowser.refresh()
return (True, None)
except Exception as e:
print(f"Erro no restore_collapse: {e}")
return (False, None)
return handled
# --- FIM: MANIPULADOR DE MENSAGENS ---
def adicionar_funcionalidades_deck():
if not mw.deckBrowser or not mw.deckBrowser.web:
return
config = carregar_config()
is_visible_on_load = config.get("search_bar_visible", True)
js_is_visible = 'true' if is_visible_on_load else 'false'
decks_data = []
name_to_id = {}
try:
for d in mw.col.decks.all_names_and_ids():
deck_name_lower = d.name.lower()
deck_id_str = str(d.id)
decks_data.append({"id": deck_id_str, "name": deck_name_lower})
name_to_id[deck_name_lower] = deck_id_str
except AttributeError:
for d in mw.col.decks.all():
deck_name_lower = d['name'].lower()
deck_id_str = str(d['id'])
decks_data.append({"id": deck_id_str, "name": deck_name_lower})
name_to_id[deck_name_lower] = deck_id_str
decks_data_json = json.dumps(decks_data)
name_to_id_json = json.dumps(name_to_id)
script_unificado = f"""
const isVisibleOnLoad = {js_is_visible};
const allDecksData = {decks_data_json};
const nameToId = {name_to_id_json};
function sendMessageToPython(message) {{
if (typeof pybridge !== 'undefined') {{
pybridge.cmd(message);
}} else if (typeof pycmd !== 'undefined') {{
pycmd(message);
}} else {{
console.error("Nenhum método de comunicação com Python encontrado.");
}}
}}
var barraExistente = document.getElementById('search-deck-input');
if (barraExistente) {{ barraExistente.remove(); }}
var contadorExistente = document.getElementById('deck-counter');
if (contadorExistente) {{ contadorExistente.remove(); }}
if (typeof window.deckKeyHandler === 'function') {{
document.removeEventListener('keydown', window.deckKeyHandler, true);
}}
var input = document.createElement('input');
input.id = 'search-deck-input';
input.type = 'text';
input.placeholder = 'Digite para pesquisar deck... (Ctrl+H para ocultar)';
input.style.cssText = 'position: fixed !important; top: 5px !important; left: 5px !important; width: 300px !important; padding: 8px !important; z-index: 999999 !important; background: #2b2b2b !important; color: white !important; border: 2px solid #ff4444 !important; border-radius: 4px !important; font-size: 14px !important; outline: none !important; box-shadow: none !important; -webkit-appearance: none !important; -moz-appearance: textfield !important; appearance: none !important;';
document.body.appendChild(input);
var contador = document.createElement('div');
contador.id = 'deck-counter';
contador.style.cssText = 'position: fixed; top: 45px; left: 5px; padding: 4px 8px; z-index: 999999; background: #444; color: white; border: 1px solid #666; border-radius: 4px; font-size: 12px; display: none;';
contador.textContent = '';
document.body.appendChild(contador);
if (!isVisibleOnLoad) {{
input.style.display = 'none';
contador.style.display = 'none';
}} else {{
document.body.classList.add('addon-search-active');
}}
var currentIndex = -1;
var matchedDecks = [];
var searchInputElement = input;
var lastSelectedDeck = null;
var isSearchMode = false;
var expandTimeout = null;
function limparTodosDestaques() {{
document.querySelectorAll('tr.deck').forEach(row => row.classList.remove('search-highlighted'));
document.querySelectorAll('.subdeck-match-badge').forEach(el => el.remove());
}}
function removeAccents(str) {{
return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
}}
function atualizarDestaque(termo, isFromLoad = false) {{
limparTodosDestaques();
var deckRows = document.querySelectorAll('tr.deck');
matchedDecks = [];
currentIndex = -1;
sessionStorage.setItem('deck_search_term', termo);
if (termo.length > 0) {{
isSearchMode = true;
var termoNormalizado = removeAccents(termo.toLowerCase());
var idsToHighlight = new Set();
var subdeckMatchCount = {{}};
allDecksData.forEach(function(deck) {{
var deckNameNormalizado = removeAccents(deck.name);
if (deckNameNormalizado.includes(termoNormalizado)) {{
idsToHighlight.add(deck.id);
var parts = deck.name.split("::");
var currentPath = "";
for (var i = 0; i < parts.length - 1; i++) {{
currentPath += (i === 0 ? "" : "::") + parts[i];
var parentId = nameToId[currentPath];
if (parentId) {{
idsToHighlight.add(parentId);
subdeckMatchCount[parentId] = (subdeckMatchCount[parentId] || 0) + 1;
}}
}}
}}
}});
var missingIds = [];
idsToHighlight.forEach(function(id) {{
if (!document.getElementById(id)) {{
missingIds.push(id);
}}
}});
deckRows.forEach(function(row) {{
if (idsToHighlight.has(row.id)) {{
row.classList.add('search-highlighted');
matchedDecks.push(row);
if (subdeckMatchCount[row.id] > 0) {{
var count = subdeckMatchCount[row.id];
var badge = document.createElement('span');
badge.className = 'subdeck-match-badge';
badge.textContent = count + (count === 1 ? ' subdeck' : ' subdecks');
var deckLink = row.querySelector('a.deck');
if (deckLink) {{
deckLink.insertAdjacentElement('afterend', badge);
}}
}}
}}
}});
if (matchedDecks.length > 0) {{
contador.textContent = matchedDecks.length + ' deck' + (matchedDecks.length !== 1 ? 's' : '') + ' encontrado' + (matchedDecks.length !== 1 ? 's' : '');
if (searchInputElement.style.display !== 'none') {{
contador.style.display = 'block';
}}
currentIndex = 0;
selecionarDeck(0);
lastSelectedDeck = matchedDecks[0];
}} else {{
contador.style.display = 'none';
lastSelectedDeck = null;
}}
if (missingIds.length > 0 && !isFromLoad) {{
clearTimeout(expandTimeout);
expandTimeout = setTimeout(function() {{
sessionStorage.setItem('deck_search_focus', 'true');
sendMessageToPython('auto_expand:' + JSON.stringify(missingIds));
}}, 400);
}}
}} else {{
isSearchMode = false;
contador.style.display = 'none';
lastSelectedDeck = null;
clearTimeout(expandTimeout);
if (!isFromLoad) {{
sendMessageToPython('restore_collapse');
}}
sessionStorage.removeItem('deck_search_term');
sessionStorage.removeItem('deck_search_focus');
}}
}}
function selecionarDeck(index) {{
var currentDeck = document.querySelector('.current');
if (currentDeck) {{ currentDeck.classList.remove('current'); }}
if (index >= 0 && index < matchedDecks.length) {{
var selectedRow = matchedDecks[index];
selectedRow.classList.add('current');
selectedRow.scrollIntoView({{ behavior: 'smooth', block: 'center' }});
lastSelectedDeck = selectedRow;
}}
}}
function encontrarDeckAtual() {{
if (lastSelectedDeck && document.contains(lastSelectedDeck)) return lastSelectedDeck;
var current = document.querySelector('.current');
if (current) return current;
var firstDeck = document.querySelector('tr.deck');
if (firstDeck) {{
firstDeck.classList.add('current');
return firstDeck;
}}
return null;
}}
input.addEventListener('input', function() {{
atualizarDestaque(this.value, false);
}});
function handleGlobalKeydown(event) {{
// CTRL + F: Focar na pesquisa (e mostrar se estiver oculta)
if (event.ctrlKey && event.key === 'f') {{
event.preventDefault();
event.stopPropagation();
if (searchInputElement) {{
if (searchInputElement.style.display === 'none') {{
searchInputElement.style.display = 'block';
document.body.classList.add('addon-search-active');
sendMessageToPython('save_search_state:true');
}}
searchInputElement.focus();
searchInputElement.select();
}}
return false;
}}
// CTRL + H: Alternar visibilidade da barra de pesquisa
if (event.ctrlKey && event.key === 'h') {{
event.preventDefault();
event.stopPropagation();
if (searchInputElement && contador) {{
let isNowVisible;
if (searchInputElement.style.display === 'none') {{
searchInputElement.style.display = 'block';
document.body.classList.add('addon-search-active');
contador.style.display = (isSearchMode && contador.textContent) ? 'block' : 'none';
isNowVisible = true;
searchInputElement.focus();
}} else {{
searchInputElement.style.display = 'none';
document.body.classList.remove('addon-search-active');
contador.style.display = 'none';
searchInputElement.value = '';
atualizarDestaque('', false); // Limpa a pesquisa e restaura decks
searchInputElement.blur();
isNowVisible = false;
}}
sendMessageToPython('save_search_state:' + isNowVisible);
}}
return false;
}}
if (event.key === 'Escape' && document.activeElement === searchInputElement) {{
searchInputElement.value = '';
atualizarDestaque('', false);
searchInputElement.blur();
if (lastSelectedDeck && document.contains(lastSelectedDeck)) {{
document.querySelectorAll('.current').forEach(el => el.classList.remove('current'));
lastSelectedDeck.classList.add('current');
}}
event.preventDefault();
event.stopPropagation();
return false;
}}
if (document.activeElement === searchInputElement) return;
// Se a barra estiver oculta, não intercepta as setas (deixa o Anki agir normalmente)
if (searchInputElement && searchInputElement.style.display === 'none') {{
return;
}}
if (isSearchMode && matchedDecks.length > 0) {{
if (event.key === 'ArrowUp') {{
event.preventDefault();
event.stopPropagation();
currentIndex = Math.max(0, currentIndex - 1);
selecionarDeck(currentIndex);
return false;
}} else if (event.key === 'ArrowDown') {{
event.preventDefault();
event.stopPropagation();
currentIndex = Math.min(matchedDecks.length - 1, currentIndex + 1);
selecionarDeck(currentIndex);
return false;
}} else if (event.key === 'Enter' && currentIndex >= 0 && currentIndex < matchedDecks.length) {{
event.preventDefault();
event.stopPropagation();
var selectedLink = matchedDecks[currentIndex].querySelector('a.deck');
if (selectedLink) selectedLink.click();
return false;
}}
}} else {{
var current = encontrarDeckAtual();
if (current && (event.key === 'ArrowUp' || event.key === 'ArrowDown' || event.key === 'Enter')) {{
event.preventDefault();
event.stopPropagation();
if (event.key === 'ArrowUp') {{
var prev = current.previousElementSibling;
while (prev && (!prev.classList.contains('deck') || prev.classList.contains("top-level-drag-row"))) {{
prev = prev.previousElementSibling;
}}
if (prev && prev.classList.contains('deck')) {{
current.classList.remove('current');
prev.classList.add('current');
sendMessageToPython('select:' + prev.id);
prev.scrollIntoView({{block: "nearest"}});
lastSelectedDeck = prev;
}}
}} else if (event.key === 'ArrowDown') {{
var next = current.nextElementSibling;
while (next && !next.classList.contains('deck')) {{
next = next.nextElementSibling;
}}
if (next && next.classList.contains('deck')) {{
current.classList.remove('current');
next.classList.add('current');
sendMessageToPython('select:' + next.id);
next.scrollIntoView({{block: "nearest"}});
lastSelectedDeck = next;
}}
}} else if (event.key === 'Enter') {{
var deckLink = current.querySelector('a.deck');
if (deckLink) deckLink.click();
}}
return false;
}}
}}
}}
window.deckKeyHandler = handleGlobalKeydown;
document.addEventListener('keydown', handleGlobalKeydown, true);
var existingStyle = document.getElementById('deck-search-styles');
if (existingStyle) {{ existingStyle.remove(); }}
var style = document.createElement('style');
style.id = 'deck-search-styles';
style.textContent = `
#search-deck-input:focus {{
border-color: #00ff00 !important;
box-shadow: 0 0 5px rgba(0, 255, 0, 0.5) !important;
}}
tr.deck.search-highlighted {{ box-shadow: 0 0 0 2px #ff6b6b inset !important; }}
/* A borda vermelha AGORA SÓ APARECE se a barra de pesquisa estiver visível (addon-search-active) */
body.addon-search-active tr.deck.current {{ box-shadow: 0 0 0 3px #ff0000 inset !important; }}
.subdeck-match-badge {{
background-color: #ff4444;
color: white;
border-radius: 12px;
padding: 2px 8px;
font-size: 11px;
margin-left: 10px;
font-weight: bold;
vertical-align: middle;
display: inline-block;
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
pointer-events: none;
}}
`;
document.head.appendChild(style);
var savedTerm = sessionStorage.getItem('deck_search_term');
var shouldFocus = sessionStorage.getItem('deck_search_focus');
if (savedTerm) {{
input.value = savedTerm;
atualizarDestaque(savedTerm, true);
}}
if (shouldFocus === 'true') {{
setTimeout(function() {{
if (input.style.display !== 'none') {{
input.focus();
var val = input.value;
input.value = '';
input.value = val;
}}
}}, 50);
sessionStorage.removeItem('deck_search_focus');
}}
console.log("Sistema de pesquisa e navegação carregado com sucesso!");
"""
try:
mw.deckBrowser.web.eval(script_unificado)
except Exception as e:
print(f"Erro ao executar addon: {e}")
def on_deck_browser_rendered(deck_browser):
def set_focus_and_add_features():
adicionar_funcionalidades_deck()
# Só foca automaticamente se não houver um foco pendente E se a barra estiver visível
if mw.deckBrowser and mw.deckBrowser.web:
mw.deckBrowser.web.eval("""
if (!sessionStorage.getItem('deck_search_focus')) {
var searchInput = document.getElementById('search-deck-input');
if (searchInput && searchInput.style.display !== 'none') {
searchInput.focus();
}
}
""")
QTimer.singleShot(150, set_focus_and_add_features)
# --- INÍCIO: REGISTRO DOS HOOKS ---
gui_hooks.deck_browser_did_render.append(on_deck_browser_rendered)
gui_hooks.webview_did_receive_js_message.append(on_js_message)
# --- FIM: REGISTRO DOS HOOKS ---
print("[DeckSearchNavigation] Addon carregado!")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment