Last active
September 26, 2025 18:20
-
-
Save mallomar/32314dac4fefd5e5f8838dc3b0646480 to your computer and use it in GitHub Desktop.
Modifies the Anki plugin to export book title, chapter title and book reference page to the sentence context field (requires Anki plugin: https://github.com/Ajatt-Tools/anki.koplugin)
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
| -- anki-modify.lua - Version that uses proper EPUB navigation for page mapping | |
| local logger = require("logger") | |
| logger.info("=== ANKI CONTEXT PATCH LOADING ===") | |
| local function patch_anki_note() | |
| local success, AnkiNote = pcall(require, "ankinote") | |
| if not success then | |
| logger.warn("Anki Context Patch: Could not load ankinote module") | |
| return false | |
| end | |
| logger.info("Anki Context Patch: Patching AnkiNote:get_word_context") | |
| -- Store the original method | |
| local original_get_word_context = AnkiNote.get_word_context | |
| -- Create our enhanced version | |
| function AnkiNote:get_word_context(word) | |
| logger.info("AnkiNote#get_custom_context() " .. tostring(self.prev_context_size) .. | |
| " " .. tostring(self.next_context_size) .. | |
| " " .. tostring(self.prev_context_num) .. | |
| " " .. tostring(self.next_context_num)) | |
| local context_text = "" | |
| local UIManager = require("ui/uimanager") | |
| local reader_ui = nil | |
| local document = nil | |
| local view = nil | |
| local book_title = "" | |
| local chapter_title = "" | |
| -- Method 1: Check UIManager for running instance | |
| if UIManager._running_instance then | |
| reader_ui = UIManager._running_instance | |
| logger.info("Anki Context Patch: Found UIManager._running_instance") | |
| end | |
| -- Method 2: Look through active widgets | |
| if not reader_ui and UIManager.active_widgets then | |
| logger.info("Anki Context Patch: Checking active_widgets, count: " .. tostring(#UIManager.active_widgets)) | |
| for i, widget in ipairs(UIManager.active_widgets) do | |
| logger.info("Anki Context Patch: Widget " .. i .. " type: " .. tostring(type(widget))) | |
| if widget and widget.document and widget.view then | |
| reader_ui = widget | |
| logger.info("Anki Context Patch: Found reader in active_widgets") | |
| break | |
| elseif widget and widget.ui and widget.ui.document then | |
| reader_ui = widget.ui | |
| logger.info("Anki Context Patch: Found reader via widget.ui") | |
| break | |
| end | |
| end | |
| end | |
| -- Method 3: Try global access patterns | |
| if not reader_ui then | |
| -- Look for common global variables that might contain the reader | |
| local possible_globals = {"_G", "require('apps/reader/readerui')", "package.loaded['apps/reader/readerui']"} | |
| for _, global_name in ipairs(possible_globals) do | |
| local success, result = pcall(function() | |
| if global_name == "_G" then | |
| -- Check if there's a reader instance in the global table | |
| for k, v in pairs(_G) do | |
| if type(v) == "table" and v.document and v.view then | |
| return v | |
| end | |
| end | |
| else | |
| local module = loadstring("return " .. global_name)() | |
| if module and module.instance then | |
| return module.instance | |
| end | |
| end | |
| return nil | |
| end) | |
| if success and result then | |
| reader_ui = result | |
| logger.info("Anki Context Patch: Found reader via " .. global_name) | |
| break | |
| end | |
| end | |
| end | |
| if reader_ui then | |
| document = reader_ui.document | |
| view = reader_ui.view | |
| logger.info("Anki Context Patch: Successfully found document and view") | |
| -- Get book title using document metadata and better title extraction | |
| if document then | |
| -- Try to get title from document properties first | |
| if document.props and document.props.title then | |
| book_title = document.props.title | |
| logger.info("Anki Context Patch: Book title from document props: " .. book_title) | |
| elseif reader_ui.doc_props and reader_ui.doc_props.title then | |
| book_title = reader_ui.doc_props.title | |
| logger.info("Anki Context Patch: Book title from reader doc_props: " .. book_title) | |
| elseif document.file then | |
| -- Better file path parsing - extract from directory structure | |
| local filename = document.file | |
| logger.info("Anki Context Patch: Full file path: " .. filename) | |
| -- Try to extract from the directory name which should contain the full title | |
| -- Pattern: .../Author/Book Title (###)/filename.epub | |
| local dir_title = filename:match("/([^/]+)%s*%([^)]+%)/[^/]+%.%w+$") | |
| if dir_title then | |
| book_title = dir_title:gsub("_", " "):gsub("%s+", " "):gsub("^%s+", ""):gsub("%s+$", "") | |
| logger.info("Anki Context Patch: Book title from directory: " .. book_title) | |
| else | |
| -- Fallback: extract from filename but handle it better | |
| local title_part = filename:match("([^/]+)%.%w+$") or "Unknown Book" | |
| -- Remove author and catalog info | |
| local clean_title = title_part:match("^(.+)%s*%-%s*[^-]+$") or title_part | |
| clean_title = clean_title:gsub("_", " "):gsub("%s+", " "):gsub("^%s+", ""):gsub("%s+$", "") | |
| book_title = clean_title | |
| logger.info("Anki Context Patch: Book title from filename: " .. book_title) | |
| end | |
| end | |
| end | |
| -- Keep the existing chapter detection exactly as it is (WORKING) | |
| local current_page = nil | |
| local current_pos = nil | |
| if view then | |
| -- Try different methods to get current page | |
| if view.getCurrentPage then | |
| current_page = view:getCurrentPage() | |
| elseif view.state and view.state.page then | |
| current_page = view.state.page | |
| elseif view.current_page then | |
| current_page = view.current_page | |
| end | |
| logger.info("Anki Context Patch: Current page: " .. tostring(current_page)) | |
| end | |
| -- Also try to get the current document position | |
| if document then | |
| if document.getXPointer then | |
| current_pos = document:getXPointer() | |
| logger.info("Anki Context Patch: Current position: " .. tostring(current_pos)) | |
| end | |
| end | |
| -- Try to find chapter using table of contents and current position | |
| if reader_ui.toc and reader_ui.toc.toc then | |
| logger.info("Anki Context Patch: TOC available with " .. tostring(#reader_ui.toc.toc) .. " entries") | |
| -- Method 1: Try using current position if available | |
| if current_pos and reader_ui.toc.getChapterByXPointer then | |
| local chapter = reader_ui.toc:getChapterByXPointer(current_pos) | |
| if chapter and chapter.title then | |
| chapter_title = chapter.title | |
| logger.info("Anki Context Patch: Chapter title via XPointer: " .. chapter_title) | |
| end | |
| end | |
| -- Method 2: Try using page number with better matching | |
| if chapter_title == "" and current_page then | |
| local best_chapter = nil | |
| local best_distance = math.huge | |
| for _, chapter in ipairs(reader_ui.toc.toc) do | |
| if chapter.page and chapter.title then | |
| -- Find the chapter that's closest but not after current page | |
| if chapter.page <= current_page then | |
| local distance = current_page - chapter.page | |
| if distance < best_distance then | |
| best_distance = distance | |
| best_chapter = chapter | |
| end | |
| end | |
| end | |
| end | |
| if best_chapter then | |
| chapter_title = best_chapter.title | |
| logger.info("Anki Context Patch: Chapter title via best match: " .. chapter_title .. " (page " .. best_chapter.page .. ")") | |
| end | |
| end | |
| -- Method 3: Try direct chapter lookup if available | |
| if chapter_title == "" and reader_ui.toc.getCurrentChapter then | |
| local current_chapter = reader_ui.toc:getCurrentChapter() | |
| if current_chapter and current_chapter.title then | |
| chapter_title = current_chapter.title | |
| logger.info("Anki Context Patch: Chapter title via getCurrentChapter: " .. chapter_title) | |
| end | |
| end | |
| end | |
| else | |
| logger.warn("Anki Context Patch: Could not find reader_ui instance") | |
| end | |
| -- NEW: Proper EPUB page mapping using navigation document | |
| local reference_page = nil | |
| if document and view then | |
| logger.info("Anki Context Patch: Attempting to find reference page using EPUB navigation") | |
| -- Method 1: Try to access the navigation document directly | |
| if document.getNavigation then | |
| local nav_doc = document:getNavigation() | |
| if nav_doc and nav_doc.page_list then | |
| logger.info("Anki Context Patch: Found navigation page list") | |
| -- Current document position/page | |
| local current_display_page = view.state and view.state.page or (view.getCurrentPage and view:getCurrentPage()) | |
| local current_xpointer = document.getXPointer and document:getXPointer() | |
| -- Try to map current position to reference page using nav page list | |
| for _, page_entry in ipairs(nav_doc.page_list) do | |
| if page_entry.href and page_entry.number then | |
| -- Check if current position matches this page entry | |
| if current_xpointer and page_entry.href:find(current_xpointer, 1, true) then | |
| reference_page = tonumber(page_entry.number) | |
| logger.info("Anki Context Patch: Found reference page via nav XPointer match: " .. tostring(reference_page)) | |
| break | |
| end | |
| end | |
| end | |
| end | |
| end | |
| -- Method 2: Try to access EPUB page mapping through document structure | |
| if not reference_page and document.getPageMap then | |
| local page_map = document:getPageMap() | |
| if page_map then | |
| logger.info("Anki Context Patch: Found document page map with " .. tostring(#page_map) .. " entries") | |
| local current_display_page = view.state and view.state.page or (view.getCurrentPage and view:getCurrentPage()) | |
| local current_pos = document.getXPointer and document:getXPointer() | |
| -- Try different approaches to map current position | |
| if current_pos then | |
| logger.info("Anki Context Patch: Looking for position " .. current_pos .. " in page map") | |
| for i, page_info in ipairs(page_map) do | |
| if type(page_info) == "table" then | |
| -- Method A: Check if CFI/href matches current position | |
| if page_info.cfi and current_pos:find(page_info.cfi, 1, true) then | |
| reference_page = page_info.label or page_info.number | |
| if reference_page then | |
| logger.info("Anki Context Patch: Found reference page via CFI match: " .. tostring(reference_page)) | |
| break | |
| end | |
| end | |
| -- Method B: Check href patterns | |
| if page_info.href then | |
| local href_file = page_info.href:match("([^#]+)") | |
| local current_file = current_pos:match("([^/]+)") | |
| if href_file and current_file and current_pos:find(href_file, 1, true) then | |
| reference_page = page_info.label or page_info.number | |
| if reference_page then | |
| logger.info("Anki Context Patch: Found reference page via href match: " .. tostring(reference_page)) | |
| break | |
| end | |
| end | |
| end | |
| -- Method C: Try page number correlation | |
| if current_display_page and page_info.page then | |
| local map_page = tonumber(page_info.page) | |
| if map_page and map_page == current_display_page then | |
| -- Keep page label as string to handle roman numerals (i, ii, iii) and other formats | |
| reference_page = page_info.label or page_info.number | |
| if reference_page then | |
| logger.info("Anki Context Patch: Found reference page via page number match: " .. tostring(reference_page)) | |
| break | |
| end | |
| end | |
| end | |
| elseif type(page_info) == "string" then | |
| -- Handle string-based page info | |
| local page_num = page_info:match("(%d+)") | |
| if page_num then | |
| reference_page = page_num -- Keep as string | |
| logger.info("Anki Context Patch: Found reference page from string entry: " .. tostring(reference_page)) | |
| break | |
| end | |
| end | |
| end | |
| end | |
| end | |
| end | |
| -- Method 3: Try to get page number from the document's internal page system | |
| if not reference_page and document.getPageFromXPointer then | |
| local current_pos = document:getXPointer() | |
| if current_pos then | |
| -- Try with different parameters to get reference page | |
| local possible_pages = { | |
| document:getPageFromXPointer(current_pos, true), -- Reference pages | |
| document:getPageFromXPointer(current_pos, "reference"), | |
| document:getPageFromXPointer(current_pos, "original") | |
| } | |
| for _, page_num in ipairs(possible_pages) do | |
| if page_num and type(page_num) == "number" and page_num > 0 and page_num < 1000 then | |
| local current_display = view.state and view.state.page or 29 -- fallback | |
| if page_num ~= current_display then -- Different from display page | |
| reference_page = page_num | |
| logger.info("Anki Context Patch: Found reference page via getPageFromXPointer: " .. tostring(reference_page)) | |
| break | |
| end | |
| end | |
| end | |
| end | |
| end | |
| -- Method 4: Try to parse navigation from document directly | |
| if not reference_page and document.readFile then | |
| local nav_success, nav_content = pcall(function() | |
| return document:readFile("nav.xhtml") or document:readFile("toc.ncx") or document:readFile("navigation.xhtml") | |
| end) | |
| if nav_success and nav_content then | |
| logger.info("Anki Context Patch: Found navigation file content") | |
| local current_pos = document:getXPointer() | |
| if current_pos then | |
| -- Parse the navigation content to find page mappings | |
| -- Look for page-list entries that match current position | |
| for page_ref in nav_content:gmatch('<a href="([^"]+)">(%d+)</a>') do | |
| local href, page_num = page_ref:match("([^>]+)>(%d+)") | |
| if href and current_pos:find(href, 1, true) then | |
| reference_page = tonumber(page_num) | |
| if reference_page then | |
| logger.info("Anki Context Patch: Found reference page via nav parsing: " .. tostring(reference_page)) | |
| break | |
| end | |
| end | |
| end | |
| end | |
| end | |
| end | |
| -- Fallback: If all else fails, use display page but log it | |
| if not reference_page then | |
| local display_page = view.state and view.state.page or (view.getCurrentPage and view:getCurrentPage()) | |
| if display_page then | |
| reference_page = display_page | |
| logger.warn("Anki Context Patch: Using display page as final fallback: " .. tostring(reference_page)) | |
| end | |
| end | |
| end | |
| -- Build page and source info | |
| local source_info = "" | |
| if book_title ~= "" then | |
| source_info = book_title | |
| if chapter_title ~= "" then | |
| source_info = source_info .. ", " .. chapter_title | |
| end | |
| if reference_page then | |
| source_info = source_info .. ", Page " .. tostring(reference_page) | |
| end | |
| elseif reference_page then | |
| source_info = "Page " .. tostring(reference_page) | |
| end | |
| -- Get the original context | |
| local original_result = original_get_word_context(self, word) | |
| if original_result and source_info ~= "" then | |
| -- If we have both context and source info, combine them | |
| context_text = original_result .. " - " .. source_info | |
| elseif original_result then | |
| -- If we only have original context | |
| context_text = original_result | |
| elseif source_info ~= "" then | |
| -- If we only have source info | |
| context_text = source_info | |
| else | |
| -- Fallback | |
| context_text = "Context not available" | |
| end | |
| logger.info("Anki Context Patch: Final context: " .. context_text) | |
| return context_text | |
| end | |
| logger.info("Anki Context Patch: Successfully patched AnkiNote:get_word_context") | |
| return true | |
| end | |
| -- Try to patch immediately | |
| if not patch_anki_note() then | |
| logger.warn("=== ANKI CONTEXT PATCH FAILED ===") | |
| logger.info("Anki Context Patch: Initial patch failed, scheduling delayed attempts") | |
| local UIManager = require("ui/uimanager") | |
| -- Schedule multiple retry attempts | |
| local retry_count = 0 | |
| local max_retries = 5 | |
| local function retry_patch() | |
| retry_count = retry_count + 1 | |
| logger.info("Anki Context Patch: Retry attempt " .. retry_count .. "/" .. max_retries) | |
| if patch_anki_note() then | |
| logger.info("=== ANKI CONTEXT PATCH APPLIED SUCCESSFULLY ===") | |
| return | |
| end | |
| if retry_count < max_retries then | |
| UIManager:scheduleIn(2, retry_patch) | |
| else | |
| logger.warn("=== ANKI CONTEXT PATCH FAILED AFTER ALL RETRIES ===") | |
| end | |
| end | |
| UIManager:scheduleIn(1, retry_patch) | |
| else | |
| logger.info("=== ANKI CONTEXT PATCH APPLIED SUCCESSFULLY ===") | |
| end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment