Last active
February 23, 2026 20:53
-
-
Save eros18123/7b8424c0aab78fe2cd7f0f17f55a2f03 to your computer and use it in GitHub Desktop.
Pesquisar deck e subdeck 2
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
| # -*- 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