From c3bb2263b7cd35edb99516f903f2a31e7290bf7e Mon Sep 17 00:00:00 2001 From: hius07 <62179190+hius07@users.noreply.github.com> Date: Tue, 6 Feb 2024 08:44:37 +0200 Subject: [PATCH 01/15] DocSettings: check legacy history location only once (#11439) --- datastorage.lua | 1 + frontend/docsettings.lua | 10 ++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/datastorage.lua b/datastorage.lua index eda3b160f..ffed6826b 100644 --- a/datastorage.lua +++ b/datastorage.lua @@ -77,6 +77,7 @@ local function initDataDir() "data/dict", "data/tessdata", -- "docsettings", -- created when needed + -- "hashdocsettings", -- created when needed -- "history", -- legacy/obsolete sidecar files "ota", -- "patches", -- must be created manually by the interested user diff --git a/frontend/docsettings.lua b/frontend/docsettings.lua index c63db9322..de05ce9a7 100644 --- a/frontend/docsettings.lua +++ b/frontend/docsettings.lua @@ -35,6 +35,8 @@ local function isFile(file) return lfs.attributes(file, "mode") == "file" end +local is_history_location_enabled = isDir(HISTORY_DIR) + local doc_hash_cache = {} local is_hash_location_enabled @@ -164,7 +166,7 @@ function DocSettings:findSidecarFile(doc_path, no_legacy) return sidecar_file, location end end - if not no_legacy then + if is_history_location_enabled and not no_legacy then sidecar_file = self:getHistoryPath(doc_path) if isFile(sidecar_file) then return sidecar_file, "hist" -- for isSidecarFileNotInPreferredLocation() used in moveBookMetadata @@ -238,7 +240,7 @@ function DocSettings:open(doc_path) new.hash_sidecar_dir = new:getSidecarDir(doc_path, "hash") hash_sidecar_file = new.hash_sidecar_dir .. "/" .. new.sidecar_filename end - local history_file = new:getHistoryPath(doc_path) + local history_file = is_history_location_enabled and new:getHistoryPath(doc_path) -- Candidates list, in order of priority: local candidates_list = { @@ -257,9 +259,9 @@ function DocSettings:open(doc_path) -- Backup file of new sidecar file in hashdocsettings folder hash_sidecar_file and (hash_sidecar_file .. ".old") or "", -- Legacy history folder - history_file, + history_file or "", -- Backup file in legacy history folder - history_file .. ".old", + history_file and (history_file .. ".old") or "", -- Legacy kpdfview setting doc_path .. ".kpdfview.lua", } From b8090c641c64471c514fba4aaac2cff066118cd1 Mon Sep 17 00:00:00 2001 From: hius07 <62179190+hius07@users.noreply.github.com> Date: Wed, 7 Feb 2024 10:35:52 +0200 Subject: [PATCH 02/15] Minimize DocSettings:open() calls (#11437) --- frontend/apps/filemanager/filemanager.lua | 38 ++++++++----- .../filemanager/filemanagercollection.lua | 32 ++++++++--- .../apps/filemanager/filemanagerhistory.lua | 47 ++++++++++------ frontend/apps/filemanager/filemanagerutil.lua | 46 ++++++++++------ frontend/apps/reader/readerui.lua | 15 +++--- frontend/document/documentregistry.lua | 7 +-- frontend/readhistory.lua | 53 +++++++++---------- plugins/coverbrowser.koplugin/covermenu.lua | 6 +-- 8 files changed, 149 insertions(+), 95 deletions(-) diff --git a/frontend/apps/filemanager/filemanager.lua b/frontend/apps/filemanager/filemanager.lua index 246827635..2646ea1f2 100644 --- a/frontend/apps/filemanager/filemanager.lua +++ b/frontend/apps/filemanager/filemanager.lua @@ -269,14 +269,26 @@ function FileManager:setupLayout() } if is_file then - self.bookinfo = nil + self.book_props = nil -- in 'self' to provide access to it in CoverBrowser local has_provider = DocumentRegistry:hasProvider(file) - if has_provider or DocSettings:hasSidecarFile(file) then - self.bookinfo = file_manager.coverbrowser and file_manager.coverbrowser:getBookInfo(file) - table.insert(buttons, filemanagerutil.genStatusButtonsRow(file, close_dialog_refresh_callback)) + local has_sidecar = DocSettings:hasSidecarFile(file) + if has_provider or has_sidecar then + self.book_props = file_manager.coverbrowser and file_manager.coverbrowser:getBookInfo(file) + local doc_settings_or_file + if has_sidecar then + doc_settings_or_file = DocSettings:open(file) + if not self.book_props then + local props = doc_settings_or_file:readSetting("doc_props") + self.book_props = FileManagerBookInfo.extendProps(props, file) + self.book_props.has_cover = true -- to enable "Book cover" button, we do not know if cover exists + end + else + doc_settings_or_file = file + end + table.insert(buttons, filemanagerutil.genStatusButtonsRow(doc_settings_or_file, close_dialog_refresh_callback)) table.insert(buttons, {}) -- separator table.insert(buttons, { - filemanagerutil.genResetSettingsButton(file, close_dialog_refresh_callback), + filemanagerutil.genResetSettingsButton(doc_settings_or_file, close_dialog_refresh_callback), filemanagerutil.genAddRemoveFavoritesButton(file, close_dialog_callback), }) end @@ -288,12 +300,12 @@ function FileManager:setupLayout() file_manager:showOpenWithDialog(file) end, }, - filemanagerutil.genBookInformationButton(file, self.bookinfo, close_dialog_callback), + filemanagerutil.genBookInformationButton(file, self.book_props, close_dialog_callback), }) if has_provider then table.insert(buttons, { - filemanagerutil.genBookCoverButton(file, self.bookinfo, close_dialog_callback), - filemanagerutil.genBookDescriptionButton(file, self.bookinfo, close_dialog_callback), + filemanagerutil.genBookCoverButton(file, self.book_props, close_dialog_callback), + filemanagerutil.genBookDescriptionButton(file, self.book_props, close_dialog_callback), }) end if Device:canExecuteScript(file) then @@ -1414,13 +1426,14 @@ function FileManager:showOpenWithDialog(file) end end local t = {} - for extension, provider_key in BaseUtil.orderedPairs(associated_providers) do + for extension, provider_key in pairs(associated_providers) do local provider = DocumentRegistry:getProviderFromKey(provider_key) if provider then local space = string.rep(" ", max_len - #extension) table.insert(t, T("%1%2: %3", extension, space, provider.provider_name)) end end + table.sort(t) UIManager:show(InfoMessage:new{ text = table.concat(t, "\n"), monospace_font = true, @@ -1479,11 +1492,10 @@ function FileManager:showOpenWithDialog(file) end function FileManager:openFile(file, provider, doc_caller_callback, aux_caller_callback) - if not provider then -- check associated - local provider_key = DocumentRegistry:getAssociatedProviderKey(file) - provider = provider_key and DocumentRegistry:getProviderFromKey(provider_key) + if provider == nil then + provider = DocumentRegistry:getProvider(file, true) -- include auxiliary end - if provider and provider.order then -- auxiliary + if provider.order then -- auxiliary if aux_caller_callback then aux_caller_callback() end diff --git a/frontend/apps/filemanager/filemanagercollection.lua b/frontend/apps/filemanager/filemanagercollection.lua index 8b4422f90..4775916ff 100644 --- a/frontend/apps/filemanager/filemanagercollection.lua +++ b/frontend/apps/filemanager/filemanagercollection.lua @@ -1,7 +1,9 @@ local BD = require("ui/bidi") local ButtonDialog = require("ui/widget/buttondialog") local Device = require("device") +local DocSettings = require("docsettings") local DocumentRegistry = require("document/documentregistry") +local FileManagerBookInfo = require("apps/filemanager/filemanagerbookinfo") local Menu = require("ui/widget/menu") local ReadCollection = require("readcollection") local UIManager = require("ui/uimanager") @@ -49,7 +51,7 @@ end function FileManagerCollection:onMenuHold(item) local file = item.file self.collfile_dialog = nil - self.bookinfo = self.ui.coverbrowser and self.ui.coverbrowser:getBookInfo(file) + self.book_props = self.ui.coverbrowser and self.ui.coverbrowser:getBookInfo(file) local function close_dialog_callback() UIManager:close(self.collfile_dialog) @@ -66,11 +68,29 @@ function FileManagerCollection:onMenuHold(item) local is_currently_opened = file == (self.ui.document and self.ui.document.file) local buttons = {} - local doc_settings_or_file = is_currently_opened and self.ui.doc_settings or file + local doc_settings_or_file + if is_currently_opened then + doc_settings_or_file = self.ui.doc_settings + if not self.book_props then + self.book_props = self.ui.doc_props + self.book_props.has_cover = true + end + else + if DocSettings:hasSidecarFile(file) then + doc_settings_or_file = DocSettings:open(file) + if not self.book_props then + local props = doc_settings_or_file:readSetting("doc_props") + self.book_props = FileManagerBookInfo.extendProps(props, file) + self.book_props.has_cover = true + end + else + doc_settings_or_file = file + end + end table.insert(buttons, filemanagerutil.genStatusButtonsRow(doc_settings_or_file, close_dialog_update_callback)) table.insert(buttons, {}) -- separator table.insert(buttons, { - filemanagerutil.genResetSettingsButton(file, close_dialog_update_callback, is_currently_opened), + filemanagerutil.genResetSettingsButton(doc_settings_or_file, close_dialog_update_callback, is_currently_opened), { text = _("Remove from favorites"), callback = function() @@ -82,11 +102,11 @@ function FileManagerCollection:onMenuHold(item) }) table.insert(buttons, { filemanagerutil.genShowFolderButton(file, close_dialog_menu_callback), - filemanagerutil.genBookInformationButton(file, self.bookinfo, close_dialog_callback), + filemanagerutil.genBookInformationButton(file, self.book_props, close_dialog_callback), }) table.insert(buttons, { - filemanagerutil.genBookCoverButton(file, self.bookinfo, close_dialog_callback), - filemanagerutil.genBookDescriptionButton(file, self.bookinfo, close_dialog_callback), + filemanagerutil.genBookCoverButton(file, self.book_props, close_dialog_callback), + filemanagerutil.genBookDescriptionButton(file, self.book_props, close_dialog_callback), }) if Device:canExecuteScript(file) then diff --git a/frontend/apps/filemanager/filemanagerhistory.lua b/frontend/apps/filemanager/filemanagerhistory.lua index 6e1784526..8b4f5cfac 100644 --- a/frontend/apps/filemanager/filemanagerhistory.lua +++ b/frontend/apps/filemanager/filemanagerhistory.lua @@ -2,6 +2,8 @@ local BD = require("ui/bidi") local ButtonDialog = require("ui/widget/buttondialog") local CheckButton = require("ui/widget/checkbutton") local ConfirmBox = require("ui/widget/confirmbox") +local DocSettings = require("docsettings") +local FileManagerBookInfo = require("apps/filemanager/filemanagerbookinfo") local InputDialog = require("ui/widget/inputdialog") local Menu = require("ui/widget/menu") local UIManager = require("ui/uimanager") @@ -62,32 +64,25 @@ function FileManagerHistory:fetchStatuses(count) end function FileManagerHistory:updateItemTable() - -- try to stay on current page - local select_number = nil - if self.hist_menu.page and self.hist_menu.perpage and self.hist_menu.page > 0 then - select_number = (self.hist_menu.page - 1) * self.hist_menu.perpage + 1 - end self.count = { all = #require("readhistory").hist, reading = 0, abandoned = 0, complete = 0, deleted = 0, new = 0, } local item_table = {} for _, v in ipairs(require("readhistory").hist) do if self:isItemMatch(v) then - if self.is_frozen and v.status == "complete" then - v.mandatory_dim = true - end + v.mandatory_dim = (self.is_frozen and v.status == "complete") and true or nil table.insert(item_table, v) end if self.statuses_fetched then self.count[v.status] = self.count[v.status] + 1 end end - local subtitle + local subtitle = "" if self.search_string then subtitle = T(_("Search results (%1)"), #item_table) elseif self.filter ~= "all" then subtitle = T(_("Status: %1 (%2)"), filter_text[self.filter]:lower(), #item_table) end - self.hist_menu:switchItemTable(nil, item_table, select_number, nil, subtitle or "") + self.hist_menu:switchItemTable(nil, item_table, -1, nil, subtitle) end function FileManagerHistory:isItemMatch(item) @@ -126,7 +121,7 @@ end function FileManagerHistory:onMenuHold(item) local file = item.file self.histfile_dialog = nil - self.bookinfo = self.ui.coverbrowser and self.ui.coverbrowser:getBookInfo(file) + self.book_props = self.ui.coverbrowser and self.ui.coverbrowser:getBookInfo(file) local function close_dialog_callback() UIManager:close(self.histfile_dialog) @@ -137,7 +132,7 @@ function FileManagerHistory:onMenuHold(item) end local function close_dialog_update_callback() UIManager:close(self.histfile_dialog) - if self._manager.filter ~= "all" then + if self._manager.filter ~= "all" or self._manager.is_frozen then self._manager:fetchStatuses(false) else self._manager.statuses_fetched = false @@ -148,13 +143,31 @@ function FileManagerHistory:onMenuHold(item) local is_currently_opened = file == (self.ui.document and self.ui.document.file) local buttons = {} + local doc_settings_or_file + if is_currently_opened then + doc_settings_or_file = self.ui.doc_settings + if not self.book_props then + self.book_props = self.ui.doc_props + self.book_props.has_cover = true + end + else + if DocSettings:hasSidecarFile(file) then + doc_settings_or_file = DocSettings:open(file) + if not self.book_props then + local props = doc_settings_or_file:readSetting("doc_props") + self.book_props = FileManagerBookInfo.extendProps(props, file) + self.book_props.has_cover = true + end + else + doc_settings_or_file = file + end + end if not item.dim then - local doc_settings_or_file = is_currently_opened and self.ui.doc_settings or file table.insert(buttons, filemanagerutil.genStatusButtonsRow(doc_settings_or_file, close_dialog_update_callback)) table.insert(buttons, {}) -- separator end table.insert(buttons, { - filemanagerutil.genResetSettingsButton(file, close_dialog_update_callback, is_currently_opened), + filemanagerutil.genResetSettingsButton(doc_settings_or_file, close_dialog_update_callback, is_currently_opened), filemanagerutil.genAddRemoveFavoritesButton(file, close_dialog_callback, item.dim), }) table.insert(buttons, { @@ -182,11 +195,11 @@ function FileManagerHistory:onMenuHold(item) }) table.insert(buttons, { filemanagerutil.genShowFolderButton(file, close_dialog_menu_callback, item.dim), - filemanagerutil.genBookInformationButton(file, self.bookinfo, close_dialog_callback, item.dim), + filemanagerutil.genBookInformationButton(file, self.book_props, close_dialog_callback, item.dim), }) table.insert(buttons, { - filemanagerutil.genBookCoverButton(file, self.bookinfo, close_dialog_callback, item.dim), - filemanagerutil.genBookDescriptionButton(file, self.bookinfo, close_dialog_callback, item.dim), + filemanagerutil.genBookCoverButton(file, self.book_props, close_dialog_callback, item.dim), + filemanagerutil.genBookDescriptionButton(file, self.book_props, close_dialog_callback, item.dim), }) self.histfile_dialog = ButtonDialog:new{ diff --git a/frontend/apps/filemanager/filemanagerutil.lua b/frontend/apps/filemanager/filemanagerutil.lua index 2ed6be3f4..713c381a2 100644 --- a/frontend/apps/filemanager/filemanagerutil.lua +++ b/frontend/apps/filemanager/filemanagerutil.lua @@ -108,9 +108,14 @@ function filemanagerutil.getStatus(file) end -- Set a document status ("reading", "complete", or "abandoned") -function filemanagerutil.setStatus(file, status) +function filemanagerutil.setStatus(doc_settings_or_file, status) -- In case the book doesn't have a sidecar file, this'll create it - local doc_settings = DocSettings:open(file) + local doc_settings + if type(doc_settings_or_file) == "table" then + doc_settings = doc_settings_or_file + else + doc_settings = DocSettings:open(doc_settings_or_file) + end local summary = doc_settings:readSetting("summary", {}) summary.status = status summary.modified = os.date("%Y-%m-%d", os.time()) @@ -131,9 +136,9 @@ end -- Generate all book status file dialog buttons in a row function filemanagerutil.genStatusButtonsRow(doc_settings_or_file, caller_callback) local file, summary, status - if type(doc_settings_or_file) == "table" then -- currently opened file + if type(doc_settings_or_file) == "table" then file = doc_settings_or_file:readSetting("doc_path") - summary = doc_settings_or_file:readSetting("summary") + summary = doc_settings_or_file:readSetting("summary", {}) status = summary.status else file = doc_settings_or_file @@ -146,7 +151,7 @@ function filemanagerutil.genStatusButtonsRow(doc_settings_or_file, caller_callba enabled = status ~= to_status, callback = function() summary.status = to_status - filemanagerutil.setStatus(file, to_status) + filemanagerutil.setStatus(doc_settings_or_file, to_status) UIManager:broadcastEvent(Event:new("DocSettingsItemsChanged", file, { summary = summary })) -- for CoverBrowser caller_callback() end, @@ -160,9 +165,16 @@ function filemanagerutil.genStatusButtonsRow(doc_settings_or_file, caller_callba end -- Generate "Reset" file dialog button -function filemanagerutil.genResetSettingsButton(file, caller_callback, button_disabled) - file = ffiutil.realpath(file) or file - local has_sidecar_file = DocSettings:hasSidecarFile(file) +function filemanagerutil.genResetSettingsButton(doc_settings_or_file, caller_callback, button_disabled) + local doc_settings, file, has_sidecar_file + if type(doc_settings_or_file) == "table" then + doc_settings = doc_settings_or_file + file = doc_settings_or_file:readSetting("doc_path") + has_sidecar_file = true + else + file = ffiutil.realpath(doc_settings_or_file) or doc_settings_or_file + has_sidecar_file = DocSettings:hasSidecarFile(file) + end local custom_cover_file = DocSettings:findCustomCoverFile(file) local has_custom_cover_file = custom_cover_file and true or false local custom_metadata_file = DocSettings:findCustomMetadataFile(file) @@ -185,7 +197,7 @@ function filemanagerutil.genResetSettingsButton(file, caller_callback, button_di custom_cover_file = check_button_cover.checked and custom_cover_file, custom_metadata_file = check_button_metadata.checked and custom_metadata_file, } - DocSettings:open(file):purge(nil, data_to_purge) + (doc_settings or DocSettings:open(file)):purge(nil, data_to_purge) if data_to_purge.custom_cover_file or data_to_purge.custom_metadata_file then UIManager:broadcastEvent(Event:new("InvalidateMetadataCache", file)) end @@ -260,23 +272,23 @@ function filemanagerutil.genShowFolderButton(file, caller_callback, button_disab } end -function filemanagerutil.genBookInformationButton(file, bookinfo, caller_callback, button_disabled) +function filemanagerutil.genBookInformationButton(file, book_props, caller_callback, button_disabled) return { text = _("Book information"), enabled = not button_disabled, callback = function() caller_callback() local FileManagerBookInfo = require("apps/filemanager/filemanagerbookinfo") - FileManagerBookInfo:show(file, bookinfo and FileManagerBookInfo.extendProps(bookinfo)) + FileManagerBookInfo:show(file, book_props and FileManagerBookInfo.extendProps(book_props)) end, } end -function filemanagerutil.genBookCoverButton(file, bookinfo, caller_callback, button_disabled) - local has_cover = bookinfo and bookinfo.has_cover +function filemanagerutil.genBookCoverButton(file, book_props, caller_callback, button_disabled) + local has_cover = book_props and book_props.has_cover return { text = _("Book cover"), - enabled = (not button_disabled and (not bookinfo or has_cover)) and true or false, + enabled = (not button_disabled and (not book_props or has_cover)) and true or false, callback = function() caller_callback() local FileManagerBookInfo = require("apps/filemanager/filemanagerbookinfo") @@ -285,12 +297,12 @@ function filemanagerutil.genBookCoverButton(file, bookinfo, caller_callback, but } end -function filemanagerutil.genBookDescriptionButton(file, bookinfo, caller_callback, button_disabled) - local description = bookinfo and bookinfo.description +function filemanagerutil.genBookDescriptionButton(file, book_props, caller_callback, button_disabled) + local description = book_props and book_props.description return { text = _("Book description"), -- enabled for deleted books if description is kept in CoverBrowser bookinfo cache - enabled = (not (button_disabled or bookinfo) or description) and true or false, + enabled = (not (button_disabled or book_props) or description) and true or false, callback = function() caller_callback() local FileManagerBookInfo = require("apps/filemanager/filemanagerbookinfo") diff --git a/frontend/apps/reader/readerui.lua b/frontend/apps/reader/readerui.lua index acaa30599..708c39bff 100644 --- a/frontend/apps/reader/readerui.lua +++ b/frontend/apps/reader/readerui.lua @@ -470,21 +470,22 @@ function ReaderUI:init() -- And have an extended and customized copy in memory for quick access. self.doc_props = FileManagerBookInfo.extendProps(props, self.document.file) - -- Set "reading" status if there is no status. + local md5 = self.doc_settings:readSetting("partial_md5_checksum") + if md5 == nil then + md5 = util.partialMD5(self.document.file) + self.doc_settings:saveSetting("partial_md5_checksum", md5) + end + local summary = self.doc_settings:readSetting("summary", {}) if summary.status == nil then summary.status = "reading" summary.modified = os.date("%Y-%m-%d", os.time()) end - local md5 = self.doc_settings:readSetting("partial_md5_checksum") - if md5 == nil then - md5 = util.partialMD5(self.document.file) - self.doc_settings:saveSetting("partial_md5_checksum", md5) + if summary.status ~= "complete" or not G_reader_settings:isTrue("history_freeze_finished_books") then + require("readhistory"):addItem(self.document.file) -- (will update "lastfile") end - require("readhistory"):addItem(self.document.file) -- (will update "lastfile") - -- After initialisation notify that document is loaded and rendered -- CREngine only reports correct page count after rendering is done -- Need the same event for PDF document diff --git a/frontend/document/documentregistry.lua b/frontend/document/documentregistry.lua index 76a9cd46f..117b3100e 100644 --- a/frontend/document/documentregistry.lua +++ b/frontend/document/documentregistry.lua @@ -86,14 +86,15 @@ end --- Returns the preferred registered document handler or fallback provider. -- @string file +-- @bool include_aux include auxiliary (non-document) providers -- @treturn table provider -function DocumentRegistry:getProvider(file) +function DocumentRegistry:getProvider(file, include_aux) local providers = self:getProviders(file) - if providers then + if providers or include_aux then -- associated provider local provider_key = DocumentRegistry:getAssociatedProviderKey(file) local provider = provider_key and self.known_providers[provider_key] - if provider and not provider.order then -- excluding auxiliary + if provider and (not provider.order or include_aux) then -- excluding auxiliary by default return provider end -- highest weighted provider diff --git a/frontend/readhistory.lua b/frontend/readhistory.lua index f01be5672..ebd10642c 100644 --- a/frontend/readhistory.lua +++ b/frontend/readhistory.lua @@ -3,7 +3,6 @@ local DocSettings = require("docsettings") local datetime = require("datetime") local dump = require("dump") local ffiutil = require("ffi/util") -local filemanagerutil = require("apps/filemanager/filemanagerutil") local util = require("util") local joinPath = ffiutil.joinPath local lfs = require("libs/libkoreader-lfs") @@ -323,36 +322,32 @@ end --- Adds new item (last opened document) to the top of the history list. -- If item time (ts) is passed, add item to the history list at this time position. function ReadHistory:addItem(file, ts, no_flush) - if file ~= nil and lfs.attributes(file, "mode") == "file" then - local index = self:getIndexByFile(realpath(file)) - if index then -- book is in the history already - if ts and self.hist[index].time == ts then - return -- legacy item already added - end - if not ts and G_reader_settings:isTrue("history_freeze_finished_books") - and filemanagerutil.getStatus(file) == "complete" then - return -- book marked as finished, do not update timestamps of item and file - end - end - local now = ts or os.time() - local mtime = lfs.attributes(file, "modification") - lfs.touch(file, now, mtime) -- update book access time for sorting by last read date - if index == 1 and not ts then -- last book, update access time only - self.hist[1].time = now - self.hist[1].mandatory = getMandatory(now) - else -- old or new book - if index then -- old book - table.remove(self.hist, index) - end - index = ts and self:getIndexByTime(ts, file:gsub(".*/", "")) or 1 - table.insert(self.hist, index, buildEntry(now, file)) - end - if not no_flush then - self:_reduce() - self:_flush() + file = realpath(file) + if not file or (ts and lfs.attributes(file, "mode") ~= "file") then + return -- bad legacy item + end + local index = self:getIndexByFile(file) + if ts and index and self.hist[index].time == ts then + return -- legacy item already added + end + local now = ts or os.time() + local mtime = lfs.attributes(file, "modification") + lfs.touch(file, now, mtime) -- update book access time for sorting by last read date + if index == 1 and not ts then -- last book, update access time only + self.hist[1].time = now + self.hist[1].mandatory = getMandatory(now) + else -- old or new book + if index then -- old book + table.remove(self.hist, index) end - return true -- used while adding legacy items + index = ts and self:getIndexByTime(ts, file:gsub(".*/", "")) or 1 + table.insert(self.hist, index, buildEntry(now, file)) + end + if not no_flush then + self:_reduce() + self:_flush() end + return true -- used while adding legacy items end --- Updates last book access time on closing the document. diff --git a/plugins/coverbrowser.koplugin/covermenu.lua b/plugins/coverbrowser.koplugin/covermenu.lua index 8f53a4f6a..6b1bfb7d0 100644 --- a/plugins/coverbrowser.koplugin/covermenu.lua +++ b/plugins/coverbrowser.koplugin/covermenu.lua @@ -247,7 +247,7 @@ function CoverMenu:updateItems(select_number) -- and store it as self.file_dialog, and UIManager:show() it. self.showFileDialog_orig(self, file) - local bookinfo = self.bookinfo -- getBookInfo(file) called by FileManager + local bookinfo = self.book_props -- getBookInfo(file) called by FileManager if not bookinfo or bookinfo._is_directory then -- If no bookinfo (yet) about this file, or it's a directory, let the original dialog be return true @@ -326,7 +326,7 @@ function CoverMenu:onHistoryMenuHold(item) self.onMenuHold_orig(self, item) local file = item.file - local bookinfo = self.bookinfo -- getBookInfo(file) called by FileManagerHistory + local bookinfo = self.book_props -- getBookInfo(file) called by FileManagerHistory if not bookinfo then -- If no bookinfo (yet) about this file, let the original dialog be return true @@ -397,7 +397,7 @@ function CoverMenu:onCollectionsMenuHold(item) self.onMenuHold_orig(self, item) local file = item.file - local bookinfo = self.bookinfo -- getBookInfo(file) called by FileManagerCollection + local bookinfo = self.book_props -- getBookInfo(file) called by FileManagerCollection if not bookinfo then -- If no bookinfo (yet) about this file, let the original dialog be return true From 50fcc0472558c45a5d1d26bd0c5723085eb20ae3 Mon Sep 17 00:00:00 2001 From: hius07 <62179190+hius07@users.noreply.github.com> Date: Thu, 8 Feb 2024 07:57:19 +0200 Subject: [PATCH 03/15] CoverBrowser: do not call lfs on every item (#11449) --- plugins/coverbrowser.koplugin/listmenu.lua | 30 ++++++++------------ plugins/coverbrowser.koplugin/mosaicmenu.lua | 15 +++------- 2 files changed, 16 insertions(+), 29 deletions(-) diff --git a/plugins/coverbrowser.koplugin/listmenu.lua b/plugins/coverbrowser.koplugin/listmenu.lua index 92e3fef39..cf923eead 100644 --- a/plugins/coverbrowser.koplugin/listmenu.lua +++ b/plugins/coverbrowser.koplugin/listmenu.lua @@ -27,7 +27,6 @@ local VerticalGroup = require("ui/widget/verticalgroup") local VerticalSpan = require("ui/widget/verticalspan") local WidgetContainer = require("ui/widget/container/widgetcontainer") local filemanagerutil = require("apps/filemanager/filemanagerutil") -local lfs = require("libs/libkoreader-lfs") local logger = require("logger") local util = require("util") local _ = require("gettext") @@ -115,7 +114,7 @@ local ListMenuItem = InputContainer:extend{ } function ListMenuItem:init() - -- filepath may be provided as 'file' (history) or 'path' (filechooser) + -- filepath may be provided as 'file' (history, collection) or 'path' (filechooser) -- store it as attribute so we can use it elsewhere self.filepath = self.entry.file or self.entry.path @@ -224,9 +223,8 @@ function ListMenuItem:update() self.menu.cover_specs = false end - local file_mode = lfs.attributes(self.filepath, "mode") - if file_mode == "directory" then - self.is_directory = true + self.is_directory = not (self.entry.is_file or self.entry.file) + if self.is_directory then -- nb items on the right, directory name on the left local wright = TextWidget:new{ text = self.mandatory or "", @@ -261,13 +259,9 @@ function ListMenuItem:update() }, }, } - else - local is_file_selected = self.menu.filemanager and self.menu.filemanager.selected_files - and self.menu.filemanager.selected_files[self.filepath] - if file_mode ~= "file" or is_file_selected then - self.file_deleted = true -- dim file - end - -- File + else -- file + self.file_deleted = self.entry.dim -- entry with deleted file from History or selected file from FM + local fgcolor = self.file_deleted and Blitbuffer.COLOR_DARK_GRAY or nil local bookinfo = BookInfoManager:getBookInfo(self.filepath, self.do_cover_image) @@ -437,7 +431,7 @@ function ListMenuItem:update() local wfileinfo = TextWidget:new{ text = fileinfo_str, face = Font:getFace("cfont", fontsize_info), - fgcolor = self.file_deleted and Blitbuffer.COLOR_DARK_GRAY or nil, + fgcolor = fgcolor, } table.insert(wright_items, wfileinfo) end @@ -446,7 +440,7 @@ function ListMenuItem:update() local wpageinfo = TextWidget:new{ text = pages_str, face = Font:getFace("cfont", fontsize_info), - fgcolor = self.file_deleted and Blitbuffer.COLOR_DARK_GRAY or nil, + fgcolor = fgcolor, } table.insert(wright_items, wpageinfo) end @@ -584,7 +578,7 @@ function ListMenuItem:update() height_overflow_show_ellipsis = true, alignment = "left", bold = true, - fgcolor = self.file_deleted and Blitbuffer.COLOR_DARK_GRAY or nil, + fgcolor = fgcolor, } end local build_authors = function(height) @@ -601,7 +595,7 @@ function ListMenuItem:update() height_adjust = true, height_overflow_show_ellipsis = true, alignment = "left", - fgcolor = self.file_deleted and Blitbuffer.COLOR_DARK_GRAY or nil, + fgcolor = fgcolor, } end while true do @@ -727,7 +721,7 @@ function ListMenuItem:update() local wfileinfo = TextWidget:new{ text = fileinfo_str, face = Font:getFace("cfont", fontsize_info), - fgcolor = self.file_deleted and Blitbuffer.COLOR_DARK_GRAY or nil, + fgcolor = fgcolor, } local wpageinfo = TextWidget:new{ -- Empty but needed for similar positionning text = "", @@ -762,7 +756,7 @@ function ListMenuItem:update() face = Font:getFace("cfont", fontsize_no_bookinfo), width = dimen.w - 2 * Screen:scaleBySize(10) - wright_width - wright_right_padding, alignment = "left", - fgcolor = self.file_deleted and Blitbuffer.COLOR_DARK_GRAY or nil, + fgcolor = fgcolor, } -- reduce font size for next loop, in case text widget is too large to fit into ListMenuItem fontsize_no_bookinfo = fontsize_no_bookinfo - fontsize_dec_step diff --git a/plugins/coverbrowser.koplugin/mosaicmenu.lua b/plugins/coverbrowser.koplugin/mosaicmenu.lua index c84425f17..9d33f0c56 100644 --- a/plugins/coverbrowser.koplugin/mosaicmenu.lua +++ b/plugins/coverbrowser.koplugin/mosaicmenu.lua @@ -25,7 +25,6 @@ local UnderlineContainer = require("ui/widget/container/underlinecontainer") local VerticalGroup = require("ui/widget/verticalgroup") local VerticalSpan = require("ui/widget/verticalspan") local WidgetContainer = require("ui/widget/container/widgetcontainer") -local lfs = require("libs/libkoreader-lfs") local logger = require("logger") local util = require("util") local _ = require("gettext") @@ -462,9 +461,8 @@ function MosaicMenuItem:update() self.menu.cover_specs = false end - local file_mode = lfs.attributes(self.filepath, "mode") - if file_mode == "directory" then - self.is_directory = true + self.is_directory = not (self.entry.is_file or self.entry.file) + if self.is_directory then -- Directory : rounded corners local margin = Screen:scaleBySize(5) -- make directories less wide local padding = Screen:scaleBySize(5) @@ -528,13 +526,8 @@ function MosaicMenuItem:update() BottomContainer:new{ dimen = dimen_in, nbitems}, }, } - else - local is_file_selected = self.menu.filemanager and self.menu.filemanager.selected_files - and self.menu.filemanager.selected_files[self.filepath] - if file_mode ~= "file" or is_file_selected then - self.file_deleted = true -- dim file - end - -- File : various appearances + else -- file + self.file_deleted = self.entry.dim -- entry with deleted file from History or selected file from FM if self.do_hint_opened and DocSettings:hasSidecarFile(self.filepath) then self.been_opened = true From bdd475f55f9fe0530cbc3fd9825814831bdd86f7 Mon Sep 17 00:00:00 2001 From: Frans de Jonge Date: Thu, 8 Feb 2024 17:40:41 +0100 Subject: [PATCH 04/15] [i18n] ReaderSearch: add translation context to "All" button (#11352) Cf. https://github.com/koreader/koreader/pull/11313/files#r1439406753 --- frontend/apps/reader/modules/readersearch.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/apps/reader/modules/readersearch.lua b/frontend/apps/reader/modules/readersearch.lua index 22cda6d04..2fc1d1132 100644 --- a/frontend/apps/reader/modules/readersearch.lua +++ b/frontend/apps/reader/modules/readersearch.lua @@ -13,6 +13,7 @@ local Utf8Proc = require("ffi/utf8proc") local WidgetContainer = require("ui/widget/container/widgetcontainer") local logger = require("logger") local _ = require("gettext") +local C_ = _.pgettext local Screen = Device.screen local T = require("ffi/util").template @@ -196,8 +197,8 @@ function ReaderSearch:onShowFulltextSearchInput() end, }, { - -- @translators Search all entries in entire document - text = _("All"), + -- @translators Find all results in entire document, button displayed on the search bar, should be short. + text = C_("Search text", "All"), callback = function() self:searchCallback() end, From 8010808a1fa0460c9e2c01fc2c4c6f23ba11063a Mon Sep 17 00:00:00 2001 From: poire-z Date: Sun, 11 Feb 2024 00:46:23 +0100 Subject: [PATCH 05/15] bump base: libjpeg-turbo, libpng, and other tweaks (#11462) Includes: - bump thirdparty/libjpeg-turbo 3.0.2 - PB: make sure libinkview-compat actually ends up w/ inkview as a DT_NEEDED - Update to libpng 1.6.41 - cre.cpp: add overrideDocumentProp() --- base | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base b/base index f7482e5de..b6ca09e06 160000 --- a/base +++ b/base @@ -1 +1 @@ -Subproject commit f7482e5dea67bfb8cd90abffddffc210911bb954 +Subproject commit b6ca09e06615df75f2219270a5e09d8ea331cd00 From 0506ffe2896584332c9bf7badab9e8c0d3377bcf Mon Sep 17 00:00:00 2001 From: poire-z Date: Fri, 16 Feb 2024 12:24:31 +0100 Subject: [PATCH 06/15] HttpInspector: new plugin for developers to inspect KOReader (#11457) Can be used to inspect the state of the objects in a running KOReader. It can also be used to execute actions (like the ones available to associate to a gesture) with HTTP requests from a remote computer/devices/gadgets. The TCP server side is provided either with a new ZeroMQ StreamMessageQueueServer (thanks bneo99), or with a LuaSocket based SimpleTCPServer. Minor UIManager tweak to avoid uneeded inputevent when such a ZeroMQ module is running. --- frontend/ui/message/simpletcpserver.lua | 68 + .../ui/message/streammessagequeueserver.lua | 86 ++ frontend/ui/uimanager.lua | 8 +- plugins/httpinspector.koplugin/_meta.lua | 6 + plugins/httpinspector.koplugin/main.lua | 1271 +++++++++++++++++ 5 files changed, 1436 insertions(+), 3 deletions(-) create mode 100644 frontend/ui/message/simpletcpserver.lua create mode 100644 frontend/ui/message/streammessagequeueserver.lua create mode 100644 plugins/httpinspector.koplugin/_meta.lua create mode 100644 plugins/httpinspector.koplugin/main.lua diff --git a/frontend/ui/message/simpletcpserver.lua b/frontend/ui/message/simpletcpserver.lua new file mode 100644 index 000000000..1ec189812 --- /dev/null +++ b/frontend/ui/message/simpletcpserver.lua @@ -0,0 +1,68 @@ +local socket = require("socket") +local logger = require("logger") + +-- Reference: +-- https://lunarmodules.github.io/luasocket/tcp.html + +-- Drop-in alternative to streammessagequeueserver.lua, using +-- LuaSocket instead of ZeroMQ. +-- This SimpleTCPServer is still tied to HTTP, expecting lines of headers, +-- a blank like marking the end of the input request. + +local SimpleTCPServer = { + host = nil, + port = nil, +} + +function SimpleTCPServer:new(o) + o = o or {} + setmetatable(o, self) + self.__index = self + if o.init then o:init() end + return o +end + +function SimpleTCPServer:start() + self.server = socket.bind(self.host, self.port) + self.server:settimeout(0.01) -- set timeout (10ms) + logger.dbg("SimpleTCPServer: Server listening on port " .. self.port) +end + +function SimpleTCPServer:stop() + self.server:close() +end + +function SimpleTCPServer:waitEvent() + local client = self.server:accept() -- wait for a client to connect + if client then + -- We expect to get all headers in 100ms. We will block during this timeframe. + client:settimeout(0.1, "t") + local lines = {} + while true do + local data = client:receive("*l") -- read a line from input + if not data then -- timeout + client:close() + break + end + if data == "" then -- proper empty line after request headers + table.insert(lines, data) -- keep it in content + data = table.concat(lines, "\r\n") + logger.dbg("SimpleTCPServer: Received data: ", data) + -- Give us more time to process the request and send the response + client:settimeout(0.5, "t") + self.receiveCallback(data, client) + -- This should call SimpleTCPServer:send() to send + -- the response and close this connection. + else + table.insert(lines, data) + end + end + end +end + +function SimpleTCPServer:send(data, client) + client:send(data) -- send the response back to the client + client:close() -- close the connection to the client +end + +return SimpleTCPServer diff --git a/frontend/ui/message/streammessagequeueserver.lua b/frontend/ui/message/streammessagequeueserver.lua new file mode 100644 index 000000000..a55a38f5e --- /dev/null +++ b/frontend/ui/message/streammessagequeueserver.lua @@ -0,0 +1,86 @@ +local ffi = require("ffi") +local logger = require("logger") +local MessageQueue = require("ui/message/messagequeue") + +local _ = require("ffi/zeromq_h") +local czmq = ffi.load("libs/libczmq.so.1") +local C = ffi.C + +local StreamMessageQueueServer = MessageQueue:extend{ + host = nil, + port = nil, +} + +function StreamMessageQueueServer:start() + self.context = czmq.zctx_new() + self.socket = czmq.zsocket_new(self.context, C.ZMQ_STREAM) + self.poller = czmq.zpoller_new(self.socket, nil) + local endpoint = string.format("tcp://%s:%d", self.host, self.port) + logger.dbg("StreamMessageQueueServer: Binding to endpoint", endpoint) + local rc = czmq.zsocket_bind(self.socket, endpoint) + -- If success, rc is port number + if rc == -1 then + logger.err("StreamMessageQueueServer: Cannot bind to ", endpoint) + end +end + +function StreamMessageQueueServer:stop() + if self.poller ~= nil then + czmq.zpoller_destroy(ffi.new('zpoller_t *[1]', self.poller)) + end + if self.socket ~= nil then + czmq.zsocket_destroy(self.context, self.socket) + end + if self.context ~= nil then + czmq.zctx_destroy(ffi.new('zctx_t *[1]', self.context)) + end +end + +function StreamMessageQueueServer:handleZframe(frame) + local size = czmq.zframe_size(frame) + local data = nil + if size > 0 then + local frame_data = czmq.zframe_data(frame) + if frame_data ~= nil then + data = ffi.string(frame_data, size) + end + end + czmq.zframe_destroy(ffi.new('zframe_t *[1]', frame)) + return data +end + +function StreamMessageQueueServer:waitEvent() + local request, id + while czmq.zpoller_wait(self.poller, 0) ~= nil do + -- See about ZMQ_STREAM and these 2 frames at http://hintjens.com/blog:42 + local id_frame = czmq.zframe_recv(self.socket) + if id_frame ~= nil then + id = id_frame + end + + local frame = czmq.zframe_recv(self.socket) + if frame ~= nil then + local data = self:handleZframe(frame) + if data then + logger.dbg("StreamMessageQueueServer: Received data: ", data) + request = data + end + end + end + if self.receiveCallback and request ~= nil then + self.receiveCallback(request, id) + end +end + +function StreamMessageQueueServer:send(data, id_frame) + czmq.zframe_send(ffi.new('zframe_t *[1]', id_frame), self.socket, C.ZFRAME_MORE + C.ZFRAME_REUSE) + czmq.zmq_send(self.socket, ffi.cast("unsigned char*", data), #data, C.ZFRAME_MORE) + -- Note: We can't use czmq.zstr_send(self.socket, data), which would stop on the first + -- null byte in data (Lua strings can have null bytes inside). + + -- Close connection + czmq.zframe_send(ffi.new('zframe_t *[1]', id_frame), self.socket, C.ZFRAME_MORE) + czmq.zmq_send(self.socket, nil, 0, 0) +end + +return StreamMessageQueueServer diff --git a/frontend/ui/uimanager.lua b/frontend/ui/uimanager.lua index 412e95fd7..2bae82064 100644 --- a/frontend/ui/uimanager.lua +++ b/frontend/ui/uimanager.lua @@ -1439,11 +1439,13 @@ end -- Process all pending events on all registered ZMQs. function UIManager:processZMQs() - if self._zeromqs[1] then - self.event_hook:execute("InputEvent") - end + local sent_InputEvent = false for _, zeromq in ipairs(self._zeromqs) do for input_event in zeromq.waitEvent, zeromq do + if not sent_InputEvent then + self.event_hook:execute("InputEvent") + sent_InputEvent = true + end self:handleInputEvent(input_event) end end diff --git a/plugins/httpinspector.koplugin/_meta.lua b/plugins/httpinspector.koplugin/_meta.lua new file mode 100644 index 000000000..5f0a38d2d --- /dev/null +++ b/plugins/httpinspector.koplugin/_meta.lua @@ -0,0 +1,6 @@ +local _ = require("gettext") +return { + name = "httpinspector", + fullname = _("HTTP KOReader Inspector"), + description = _([[Allow browsing KOReader internal objects over HTTP. This is aimed at developers, and may pose some security risks. Only enable this on networks you can trust.]]), +} diff --git a/plugins/httpinspector.koplugin/main.lua b/plugins/httpinspector.koplugin/main.lua new file mode 100644 index 000000000..b0256fde3 --- /dev/null +++ b/plugins/httpinspector.koplugin/main.lua @@ -0,0 +1,1271 @@ +--[[-- +This plugin allows for inspecting KOReader's internal objects, +calling methods, sending events... over HTTP. +--]]-- + +local DataStorage = require("datastorage") +local Device = require("device") +local UIManager = require("ui/uimanager") +local WidgetContainer = require("ui/widget/container/widgetcontainer") +local Event = require("ui/event") +local ffiUtil = require("ffi/util") +local logger = require("logger") +local util = require("util") +local _ = require("gettext") +local T = require("ffi/util").template + +local HttpInspector = WidgetContainer:extend{ + name = "httpinspector", +} + +-- A plugin gets instantiated on each document load and reader/FM switch. +-- Ensure autostart only on KOReader startup, and keep the running state +-- across document load and reader/FM switch. +local should_run = G_reader_settings:isTrue("httpinspector_autostart") + +function HttpInspector:init() + self.port = G_reader_settings:readSetting("httpinspector_port", "8080") + if should_run then + -- Delay this until after all plugins are loaded + UIManager:nextTick(function() + self:start() + end) + end + self.ui.menu:registerToMainMenu(self) +end + +function HttpInspector:isRunning() + return self.http_socket ~= nil +end + +function HttpInspector:onEnterStandby() + logger.dbg("HttpInspector: onEnterStandby") + if self:isRunning() then + self:stop() + end +end + +function HttpInspector:onSuspend() + logger.dbg("HttpInspector: onSuspend") + if self:isRunning() then + self:stop() + end +end + +function HttpInspector:onExit() + logger.dbg("HttpInspector: onExit") + if self:isRunning() then + self:stop() + end +end + +function HttpInspector:onCloseWidget() + logger.dbg("HttpInspector: onCloseWidget") + if self:isRunning() then + self:stop() + end +end + +function HttpInspector:onLeaveStandby() + logger.dbg("HttpInspector: onLeaveStandby") + if should_run and not self:isRunning() then + self:start() + end +end + +function HttpInspector:onResume() + logger.dbg("HttpInspector: onResume") + if should_run and not self:isRunning() then + self:start() + end +end + +function HttpInspector:start() + logger.dbg("HttpInspector: Starting server...") + + -- Make a hole in the Kindle's firewall + if Device:isKindle() then + os.execute(string.format("%s %s %s", + "iptables -A INPUT -p tcp --dport", self.port, + "-m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT")) + os.execute(string.format("%s %s %s", + "iptables -A OUTPUT -p tcp --sport", self.port, + "-m conntrack --ctstate ESTABLISHED -j ACCEPT")) + end + + -- Using a simple LuaSocket based TCP server instead of a ZeroMQ based one + -- seems to solve strange issues with Chrome. + -- local ServerClass = require("ui/message/streammessagequeueserver") + local ServerClass = require("ui/message/simpletcpserver") + self.http_socket = ServerClass:new{ + host = "*", + port = self.port, + receiveCallback = function(data, id) return self:onRequest(data, id) end, + } + self.http_socket:start() + self.http_messagequeue = UIManager:insertZMQ(self.http_socket) + + logger.dbg("HttpInspector: Server listening on port " .. self.port) +end + +function HttpInspector:stop() + logger.dbg("HttpInspector: Stopping server...") + + -- Plug the hole in the Kindle's firewall + if Device:isKindle() then + os.execute(string.format("%s %s %s", + "iptables -D INPUT -p tcp --dport", self.port, + "-m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT")) + os.execute(string.format("%s %s %s", + "iptables -D OUTPUT -p tcp --sport", self.port, + "-m conntrack --ctstate ESTABLISHED -j ACCEPT")) + end + + if self.http_socket then + self.http_socket:stop() + self.http_socket = nil + end + if self.http_messagequeue then + UIManager:removeZMQ(self.http_messagequeue) + self.http_messagequeue = nil + end + + logger.dbg("HttpInspector: Server stopped.") +end + +function HttpInspector:addToMainMenu(menu_items) + menu_items.httpremote = { + text = _("KOReader HTTP inspector"), + sorting_hint = ("more_tools"), + sub_item_table = { + { + text_func = function() + if self:isRunning() then + return _("Stop HTTP server") + else + return _("Start HTTP server") + end + end, + keep_menu_open = true, + callback = function(touchmenu_instance) + if self:isRunning() then + should_run = false + self:stop() + else + should_run = true + self:start() + end + touchmenu_instance:updateItems() + end, + }, + { + text_func = function() + if self:isRunning() then + return T(_("Listening on port %1"), self.port) + else + return _("Not running") + end + end, + enabled_func = function() + return self:isRunning() + end, + separator = true, + }, + { + text = _("Auto start HTTP server"), + checked_func = function() + return G_reader_settings:isTrue("httpinspector_autostart") + end, + callback = function() + G_reader_settings:flipNilOrFalse("httpinspector_autostart") + end, + }, + { + text_func = function() + return T(_("Port: %1"), self.port) + end, + keep_menu_open = true, + callback = function(touchmenu_instance) + local InputDialog = require("ui/widget/inputdialog") + local port_dialog + port_dialog = InputDialog:new{ + title = _("Set custom port"), + input = self.port, + input_type = "number", + input_hint = _("Port number (default is 8080)"), + buttons = { + { + { + text = _("Cancel"), + id = "close", + callback = function() + UIManager:close(port_dialog) + end, + }, + { + text = _("OK"), + -- keep_menu_open = true, + callback = function() + local port = port_dialog:getInputValue() + logger.warn("port", port) + if port and port >= 1 and port <= 65535 then + self.port = port + G_reader_settings:saveSetting("httpinspector_port", port) + if self:isRunning() then + self:stop() + self:start() + end + end + UIManager:close(port_dialog) + touchmenu_instance:updateItems() + end, + }, + }, + }, + } + UIManager:show(port_dialog) + port_dialog:onShowKeyboard() + end, + } + }, + } +end + +local HTTP_RESPONSE_CODE = { + [200] = 'OK', + [201] = 'Created', + [202] = 'Accepted', + [204] = 'No Content', + [301] = 'Moved Permanently', + [302] = 'Found', + [304] = 'Not Modified', + [400] = 'Bad Request', + [401] = 'Unauthorized', + [403] = 'Forbidden', + [404] = 'Not Found', + [405] = 'Method Not Allowed', + [406] = 'Not Acceptable', + [408] = 'Request Timeout', + [410] = 'Gone', + [500] = 'Internal Server Error', + [501] = 'Not Implemented', + [503] = 'Service Unavailable', +} + +local CTYPE = { + CSS = "text/css", + HTML = "text/html", + JS = "application/javascript", + JSON = "application/json", + PNG = "image/png", + TEXT = "text/plain", +} + +function HttpInspector:sendResponse(reqinfo, http_code, content_type, body) + if not http_code then http_code = 400 end + if not body then body = "" end + if type(body) ~= "string" then body = tostring(body) end + + local response = {} + -- StreamMessageQueueServer:send() closes the connection, so announce + -- that with HTTP/1.0 and a "Connection: close" header. + table.insert(response, T("HTTP/1.0 %1 %2", http_code, HTTP_RESPONSE_CODE[http_code] or "Unspecified")) + -- If no content type provided, let the browser sniff it + if content_type then + -- Advertize all our text as being UTF-8 + local charset = "" + if util.stringStartsWith(content_type, "text/") then + charset = "; charset=utf-8" + end + table.insert(response, T("Content-Type: %1%2", content_type, charset)) + end + if http_code == 302 then + table.insert(response, T("Location: %1", body)) + body = "" + end + table.insert(response, T("Content-Length: %1", #body)) + table.insert(response, "Connection: close") + table.insert(response, "") + table.insert(response, body) + response = table.concat(response, "\r\n") + logger.dbg("HttpInspector: Sending response: " .. response:sub(1, 200)) + if self.http_socket then -- in case the plugin is gone... + self.http_socket:send(response, reqinfo.request_id) + end +end + +-- Process a uri, stepping one fragment (consider ? / = as separators) +local stepUriFragment = function(uri) + local ftype, fragment, remain = uri:match("^([/=?]*)([^/=?]+)(.*)$") + if ftype then + return ftype, fragment, remain + end + -- it ends with a separator: return it + return uri, nil, nil +end + +-- Parse multiple variables from uri, guessing their Lua type +-- ie with uri: nil/true/false/"true"/-1.2/"/"/abc/""/'d"/ef'/ +-- Nb args: 9 +-- 1: nil: nil +-- 2: boolean: true +-- 3: boolean: false +-- 4: string: true +-- 5: number: -1.2 +-- 6: string: / +-- 7: string: abc +-- 8: string: +-- 9: string: d"/ef +local getVariablesFromUri = function(uri) + local vars = {} + local nb_vars = 0 + if not uri then + return vars, nb_vars + end + local stop_char + local var_start_idx + local var_end_idx + local end_idx = #uri + local quoted + for i = 1, end_idx do + local c = uri:sub(i,i) + local skip = false + if not stop_char then + if c == "'" or c == '"' then + stop_char = c + var_start_idx = i + 1 + quoted = true + skip = true + elseif c == "/" then + skip = true + else + stop_char = "/" + var_start_idx = i + quoted = false + end + end + if not skip then + if c == stop_char or i == end_idx then + var_end_idx = c == stop_char and i-1 or i + local text = uri:sub(var_start_idx, var_end_idx) + -- (We properly get an empty string if var_end_idx= firstline then + if not lines then + lines = {} + end + table.insert(lines, line) + end + if num >= lastline then + break + end + num = num + 1 + end + f:close() + end + end + info = { + source = path, + firstline = firstline, + lastline = lastline, + } + if lines then + local signature = util.trim(lines[1]) + info.signature = signature + -- Try to guess (possibly wrongly) a few info from the signature string + local dummy, cnt + dummy, cnt = signature:gsub("%(%)","") -- check for "()", no arg + if cnt > 0 then + info.nb_args = 0 + else + dummy, cnt = signature:gsub(",","") -- check for nb of commas + info.nb_args = cnt and cnt + 1 or 1 + end + dummy, cnt = signature:gsub("%.%.%.","") -- check for "...", varargs + if cnt > 0 then + info.nb_args = -1 + end + dummy, cnt = signature:gsub("^[^(]*:","") + info.is_method = cnt > 0 + info.classname = signature:gsub(".-(%w+):.*","%1") + end + _function_info_cache[hash] = info + if not full_code then + return info + end + info = util.tableDeepCopy(info) + info.lines = lines + return info +end + +-- Guess class name of an object +local guessClassName = function(obj) + -- Look for some common methods we could infer a class from (add more as needed) + local classic_method_names = { + "init", + "new", + "getSize", + "paintTo", + "onReadSettings", + "onResume", + "onSuspend", + "onMenuHold", + "beforeSuspend", + "initNetworkManager", + "free", + "clear", + } + -- For an instance, we won't probably find them in the table itself, so we'll have to look + -- into its first metatable + local meta_table = getmetatable(obj) + local test_method, meta_test_method + for _, method_name in ipairs(classic_method_names) do + test_method = rawget(obj, method_name) + if test_method then + break + end + if meta_table and not meta_test_method then + meta_test_method = rawget(meta_table, method_name) + end + end + if not test_method then + test_method = meta_test_method + end + if test_method then + local func_info = getFunctionInfo(test_method) + return func_info.classname + end +end + +-- Nothing below is made available to translators: we output technical details +-- in HTML, for power users and developers, who should be fine with english. +local HOME_CONTENT = [[ + +KOReader inspector + +
+Welcome to KOReader inspector HTTP server!
+
+This service is aimed at developers, use at your own risk.
+
+Browse core objects:
+
  • ui the current application (ReaderUI or FileManager). +
  • device the Device object (get a screenshot: device/screen/bb). +
  • UIManager and its window stack. +
  • g_settings your global settings saved as settings.reader.lua. + +Send an event: +
  • list of dispatcher/gestures actions. +
  • (or broadcast an event if you know what you are doing.) +
  • + + +]] +-- Other ideas for entry points: +-- - Browse filesystem, koreader and library, allow upload of books +-- - Stream live crash.log + +-- Process HTTP request +function HttpInspector:onRequest(data, request_id) + -- Keep track of request info so nested calls can send the response + local reqinfo = { + request_id = request_id, + fragments = {}, + } + local method, uri = data:match("^(%u+) ([^\n]*) HTTP/%d%.%d\r?\n.*") + -- We only need to support GET, with our special simple URI syntax/grammar + if method ~= "GET" then + return self:sendResponse(reqinfo, 405, CTYPE.TEXT, "Only GET supported") + end + reqinfo.uri = uri + -- Decode any %-encoded stuff (should be ok to do it that early) + uri = util.urlDecode(uri) + logger.dbg("HttpInspector: Received request:", method, uri) + + if not util.stringStartsWith(uri, "/koreader/") then + -- Anything else is static content. + -- We allow the user to put anything he'd like to in /koreder/web/ and have + -- this content served as the main content, which can allow building a web + -- app with HTML/CSS/JS to interact with the API exposed under /koreader/. + if uri == "/" then + uri = "/index.html" + end + -- No security/sanity check for now + local filepath = DataStorage:getDataDir() .. "/web" .. uri + if uri == "/favicon.ico" then -- hijack this one to return our icon + filepath = "resources/koreader.png" + end + local f = io.open(filepath, "rb") + if f then + data = f:read("*all") + f:close() + return self:sendResponse(reqinfo, 200, nil, data) -- let content-type be sniffed + end + if uri == "/index.html" then + -- / but no /web/index.html created by the user: redirect to our /koreader/ + return self:sendResponse(reqinfo, 302, nil, "/koreader/") + end + return self:sendResponse(reqinfo, 404, CTYPE.TEXT, "Static file not found: koreader/web" .. uri) + end + + -- Request starts with /koreader/, followed by some predefined entry point + local ftype, fragment + ftype, fragment, uri = stepUriFragment(uri) -- skip "/koreader" + reqinfo.parsed_uri = ftype .. fragment + table.insert(reqinfo.fragments, 1, fragment) + + ftype, fragment, uri = stepUriFragment(uri) + if not fragment then + return self:sendResponse(reqinfo, 200, CTYPE.HTML, HOME_CONTENT) + -- return self:sendResponse(reqinfo, 400, CTYPE.TEXT, "Missing entry point.") + end + reqinfo.prev_parsed_uri = reqinfo.parsed_uri + reqinfo.parsed_uri = reqinfo.parsed_uri .. ftype .. fragment + table.insert(reqinfo.fragments, 1, fragment) + + -- We allow browsing a few of our core objects + if fragment == "ui" then + return self:exposeObject(self.ui, uri, reqinfo) + elseif fragment == "device" then + return self:exposeObject(Device, uri, reqinfo) + elseif fragment == "g_settings" then + return self:exposeObject(G_reader_settings, uri, reqinfo) + elseif fragment == "UIManager" then + return self:exposeObject(UIManager, uri, reqinfo) + elseif fragment == "event" then + return self:exposeEvent(uri, reqinfo) + elseif fragment == "broadcast" then + return self:exposeBroadcastEvent(uri, reqinfo) + end + + return self:sendResponse(reqinfo, 404, CTYPE.TEXT, "Unknown entry point.") +end + +-- Navigate object and its children according to uri, reach the +-- final object and act depending on its type and what's requested +function HttpInspector:exposeObject(obj, uri, reqinfo) + local ftype, fragment + local parent = obj + local current_key + while true do -- process URI + local obj_type = type(obj) + if ftype and fragment then + reqinfo.prev_parsed_uri = reqinfo.parsed_uri + reqinfo.parsed_uri = reqinfo.parsed_uri .. ftype .. fragment + table.insert(reqinfo.fragments, 1, fragment) + end + ftype, fragment, uri = stepUriFragment(uri) + + if obj_type == "table" then + if ftype == "/" then + if not fragment then + -- URI ends with 'object/': send a HTML page describing all this object's key/values + return self:browseObject(obj, reqinfo) + else + -- URI continues with 'object/key' + parent = obj + local as_number = tonumber(fragment) + fragment = as_number or fragment + current_key = fragment + obj = obj[fragment] + if obj == nil then + return self:sendResponse(reqinfo, 404, CTYPE.TEXT, "No such table/object key: "..fragment) + end + -- continue loop to process this children of our object + end + elseif ftype == "" then + -- URI ends with 'object' (without a trailing /): output it as JSON if possible + local ok, json = pcall(getAsJsonString, obj) + if ok then + return self:sendResponse(reqinfo, 200, CTYPE.JSON, json) + else + -- Probably nested/recursive data structures (ie. a widget with self.dialog pointing to a parent) + return self:sendResponse(reqinfo, 500, CTYPE.TEXT, json) + end + else + return self:sendResponse(reqinfo, 400, CTYPE.TEXT, "Invalid request: unexepected token after "..reqinfo.parsed_uri) + end + + elseif obj_type == "function" then + if ftype == "?" and not fragment then + -- URI ends with 'function?' : output some documentation about that function + return self:showFunctionDetails(obj, reqinfo) + elseif ftype == "/" or ftype == "?/" then + -- URI ends or continues with 'function/': call function, output return values as JSON + -- If 'function?/': do the same but output HTML, helpful for debugging + if fragment and uri then -- put back first argument into uri + uri = fragment .. uri + end + return self:callFunction(obj, parent, uri, ftype == "?/", reqinfo) + else + -- Nothing else accepted + return self:sendResponse(reqinfo, 400, CTYPE.TEXT, "Invalid request on function: use a trailing / to call, or ? to get details") + end + + elseif obj_type == "cdata" or obj_type == "userdata" or obj_type == "thread" then + -- We can't do much on these Lua types. + -- But try to guess if it's a BlitBuffer, that we can render as PNG ! + local ok, is_bb = pcall(function() return obj.writePNG ~= nil end) + if ok and is_bb then + local tmpfile = DataStorage:getDataDir() .. "/cache/tmp_bb.png" + ok = pcall(obj.writePNG, obj, tmpfile) + if ok then + local f = io.open(tmpfile, "rb") + if f then + local data = f:read("*all") + f:close() + os.remove(tmpfile) + return self:sendResponse(reqinfo, 200, CTYPE.PNG, data) + end + end + end + return self:sendResponse(reqinfo, 403, CTYPE.TEXT, "Can't act on object of type: "..obj_type) + + else + -- Simple Lua types: string, number, boolean, nil + if ftype == "" then + -- Return it as text + return self:sendResponse(reqinfo, 200, CTYPE.TEXT, tostring(obj)) + elseif (ftype == "=" or ftype == "?=") and fragment and uri then + -- 'property=value': assign value to property + -- 'property?=value': same, but output HTML allowing to get back to the parent + uri = fragment .. uri -- put back first frament into uri + local args, nb_args = getVariablesFromUri(uri) + if nb_args ~= 1 then + return self:sendResponse(reqinfo, 400, CTYPE.TEXT, "Variable assignment needs a single value") + end + local value = args[1] + parent[current_key] = value -- do what is asked: assign it + if ftype == "=" then + return self:sendResponse(reqinfo, 200, CTYPE.TEXT, T("Variable '%1' assigned with: %2", reqinfo.parsed_uri, tostring(value))) + else + value = tostring(value) + local html = {} + local add_html = function(h) table.insert(html, h) end + local html_quoted_value = value:gsub("&", "&"):gsub(">", ">"):gsub("<", "<") + add_html(T("%1.%2=%3", reqinfo.fragments[2], reqinfo.fragments[1], html_quoted_value)) + add_html(T("
    Variable '%1' assigned with: %2", reqinfo.parsed_uri, value))
    +                    add_html("")
    +                    add_html(T("Browse back to container object.", reqinfo.prev_parsed_uri))
    +                    html = table.concat(html, "\n")
    +                    return self:sendResponse(reqinfo, 200, CTYPE.HTML, html)
    +                end
    +            elseif ftype == "?" then
    +                return self:sendResponse(reqinfo, 400, CTYPE.TEXT, "No documentation available on simple types.")
    +            else
    +                -- Nothing else accepted
    +                return self:sendResponse(reqinfo, 400, CTYPE.TEXT, "Invalid request on variable")
    +            end
    +        end
    +    end
    +    return self:sendResponse(reqinfo, 400, CTYPE.TEXT, "Unexepected request") -- luacheck: ignore 511
    +end
    +
    +-- Send a HTML page describing all this object's key/values
    +function HttpInspector:browseObject(obj, reqinfo)
    +    local html = {}
    +    local add_html = function(h) table.insert(html, h) end
    +    -- We want to display keys sorted by value kind
    +    local KIND_OTHER    = 1 -- string/number/boolean/nil/cdata...
    +    local KIND_TABLE    = 2 -- table/object
    +    local KIND_FUNCTION = 3 -- function/method
    +    local KINDS = { KIND_OTHER, KIND_TABLE, KIND_FUNCTION }
    +    local html_by_obj_kind
    +    local reset_html_by_obj_kind = function() html_by_obj_kind = { {}, {}, {} } end
    +    local add_html_to_obj_kind = function(kind, h) table.insert(html_by_obj_kind[kind], h) end
    +
    +    local get_html_snippet = function(key, value, uri)
    +        local href = uri .. key
    +        local value_type = type(value)
    +        if value_type == "table" then
    +            local pad = ""
    +            local classinfo = guessClassName(value)
    +            if classinfo then
    +                pad = (" "):rep(32 - #(tostring(key)))
    +            end
    +            return T("J  %3 %4%5", href, href, key, pad, classinfo or ""), KIND_TABLE
    +        elseif value_type == "function" then
    +            local pad = (" "):rep(30 - #key)
    +            local func_info = getFunctionInfo(value)
    +            local siginfo = (func_info.is_method and "M" or "f") .. " " .. (func_info.nb_args >= 0 and func_info.nb_args or "*")
    +            return T("   %2() %3%4 %5", href, key, pad, siginfo, func_info.signature), KIND_FUNCTION
    +        elseif value_type == "string" or value_type == "number" or value_type == "boolean" or value_type == "nil" then
    +            -- This is not totally fullproof (\n will be eaten by Javascript prompt(), other stuff may fail or get corrupted),
    +            -- but it should be ok for simple strings.
    +            local quoted_value
    +            local html_value
    +            if value_type == "string" then
    +                quoted_value = '\\"' .. value:gsub('\\', '\\\\'):gsub('"', '"'):gsub("'", "'"):gsub('\n', '\\n'):gsub('<', '<'):gsub('>', '>') .. '\\"'
    +                html_value = value:gsub("&", "&"):gsub('"', """):gsub(">", ">"):gsub("<", "<")
    +                if html_value:match("\n") then
    +                    -- Newline in string: make it stand out
    +                    html_value = T("%1", html_value)
    +                end
    +            else
    +                quoted_value = tostring(value)
    +                html_value = tostring(value)
    +            end
    +            local ondblclick = T([[ondblclick='(function(){
    +                    var t=prompt("Update value of property: %1", "%2");
    +                    if (t!=null) {document.location.href="%3?="+t}
    +                    else {return false;}
    +                  })(); return false;']], key, quoted_value, href)
    +            return T("   %1: %3", key, ondblclick, html_value), KIND_OTHER
    +        else
    +            if value_type == "cdata" then
    +                local ok, is_bb = pcall(function() return value.writePNG ~= nil end)
    +                if ok and is_bb then
    +                    return T("   %2  BlitBuffer %3bpp %4x%5", href, key, value.getBpp(), value.w, value.h), KIND_OTHER
    +                end
    +            end
    +            return T("   %1: %2", key, value_type), KIND_OTHER
    +        end
    +    end
    +    -- add_html("")
    +    -- A little header may help noticing the page is updated (the browser url bar
    +    -- just above is usually updates before the page is loaded)
    +    add_html(T("%1", reqinfo.parsed_uri))
    +    add_html(T("
    %1/", reqinfo.parsed_uri))
    +    local classinfo = guessClassName(obj)
    +    if classinfo then
    +        add_html(T("  %1 instance", classinfo))
    +    end
    +    -- Keep track of names seen, so we can show these same names
    +    -- in super classes lighter, as they are then overriden.
    +    local seen_names = {}
    +    local seen_prefix = ""
    +    local seen_suffix = ""
    +    local prelude = ""
    +    while obj do
    +        local has_items = false
    +        reset_html_by_obj_kind()
    +        for key, value in ffiUtil.orderedPairs(obj) do
    +            local ignore = key == "__index"
    +            if not ignore then
    +                local snippet, kind = get_html_snippet(key, value, reqinfo.uri)
    +                if seen_names[key] then
    +                    add_html_to_obj_kind(kind, prelude .. seen_prefix .. snippet .. seen_suffix)
    +                else
    +                    add_html_to_obj_kind(kind, prelude .. snippet)
    +                end
    +                seen_names[key] = true
    +                prelude = ""
    +                has_items = true
    +            end
    +        end
    +        for _, kind in ipairs(KINDS) do
    +            for _, htm in ipairs(html_by_obj_kind[kind]) do
    +                add_html(htm)
    +            end
    +        end
    +        if not has_items then
    +            add_html("(empty table/object)")
    +        end
    +        obj = getmetatable(obj)
    +        if obj then
    +            prelude = "
    " + classinfo = guessClassName(obj) + if classinfo then + add_html(prelude .. T(" %1", classinfo)) + prelude = "" + end + end + end + add_html("
    ") + html = table.concat(html, "\n") + return self:sendResponse(reqinfo, 200, CTYPE.HTML, html) +end + +-- Send a HTML page describing a function or method +function HttpInspector:showFunctionDetails(obj, reqinfo) + local html = {} + local add_html = function(h) table.insert(html, h) end + local base_uri = reqinfo.parsed_uri + local func_info = getFunctionInfo(obj, true) + add_html(T("%1?", reqinfo.fragments[1])) + add_html(T("
    %1", reqinfo.parsed_uri))
    +    add_html(T("  %1", func_info.signature))
    +    add_html("")
    +    add_html(T("This is a %1, accepting or requiring up to %2 arguments.", (func_info.is_method and "method" or "function"), func_info.nb_args >= 0 and func_info.nb_args or "many"))
    +    add_html("")
    +    add_html("We can't tell you more, neither what type of arguments it expects, and what it will do (it may crash or let KOReader in an unusable state).")
    +    add_html("Only values of simple type (string, number, boolean, nil) can be provided as arguments and returned as results. Functions expecting tables or objects will most probably fail. Call at your own risk!")
    +    add_html("")
    +    local output_sample_uris = function(token)
    +        local some_uri = base_uri .. token
    +        local pad = (" "):rep(#base_uri + 25 - #some_uri)
    +        add_html(T("%2 %3 without args", some_uri, some_uri, pad))
    +        local nb_args = func_info.nb_args >= 0 and func_info.nb_args or 4 -- limit to 4 if varargs
    +        for i=1, nb_args do
    +            if i > 1 then
    +                some_uri = some_uri .. "/"
    +            end
    +            some_uri = some_uri .. "arg" .. tostring(i)
    +            pad = (" "):rep(#base_uri + 25 - #some_uri)
    +            add_html(T("%1 %2 with %3 args", some_uri, pad, i))
    +        end
    +    end
    +    add_html("It may be called, to get results as HTML, with:")
    +    output_sample_uris("?/")
    +    add_html("")
    +    add_html("It may be called, to get results as JSON, with:")
    +    output_sample_uris("/")
    +    add_html("")
    +    local dummy, git_commit = require("version"):getNormalizedCurrentVersion()
    +    local github_uri = T("https://github.com/koreader/koreader/blob/%1/%2#L%3", git_commit, func_info.source, func_info.firstline)
    +    add_html(T("Here's a snippet of the function code (it can be viewed with syntax coloring and line numbers on Github):", github_uri))
    +    add_html("
    ") + for _, line in ipairs(func_info.lines) do + add_html(line) + end + add_html("\n
    ") + add_html("
    ") + html = table.concat(html, "\n") + return self:sendResponse(reqinfo, 200, CTYPE.HTML, html) +end + +-- Call a function or method, send results as JSON or HTML +function HttpInspector:callFunction(func, instance, args_as_uri, output_html, reqinfo) + local html = {} + local add_html = function(h) table.insert(html, h) end + local args, nb_args = getVariablesFromUri(args_as_uri) + local func_info = getFunctionInfo(func) + if output_html then + add_html(T("%1(%2)", reqinfo.fragments[1], args_as_uri or "")) + add_html(T("
    %1 (%2)", reqinfo.parsed_uri, args_as_uri or ""))
    +        add_html(T("  %1", func_info.signature))
    +        add_html("")
    +        add_html(T("Nb args: %1", nb_args))
    +        for i=1, nb_args do
    +            local arg = args[i]
    +            add_html(T("  %1: %2: %3", i, type(arg), tostring(arg)))
    +        end
    +        add_html("")
    +    end
    +    local res, nbr, http_code, json, ok, ok2, err, trace
    +    if func_info.is_method then
    +        res = table.pack(xpcall(func, debug.traceback, instance, unpack(args, 1, nb_args)))
    +    else
    +        res = table.pack(xpcall(func, debug.traceback, unpack(args, 1, nb_args)))
    +    end
    +    ok = res[1]
    +    if ok then
    +        http_code = 200
    +        table.remove(res, 1) -- remove pcall's ok
    +        -- table.pack and JSON.encode may use this "n" key value to set the nb
    +        -- of element and guess it is an array. Keep it updated.
    +        nbr = res["n"]
    +        if nbr then
    +            nbr = nbr - 1
    +            res["n"] = nbr
    +            if nbr == 0 then
    +                res = nil
    +            end
    +        end
    +        if res == nil then
    +            -- getAsJsonString would return "null", let's return an empty array instead
    +            json = "[]"
    +        else
    +            ok2, json = pcall(getAsJsonString, res)
    +            if not ok2 then
    +                json = "[ 'can't be reprensented as json' ]"
    +            end
    +        end
    +    else
    +        http_code = 500
    +        -- On error, instead of the array on success, let's return an object,
    +        -- with keys 'error' and "stacktrace"
    +        err, trace = res[2]:match("^(.-)\n(.*)$")
    +        json = getAsJsonString({["error"] = err, ["stacktrace"] = trace})
    +    end
    +    if output_html then
    +        local bgcolor = ok and "#bbffbb" or "#ffbbbb"
    +        local status = ok and "Success" or "Failure"
    +        add_html(T("%2", bgcolor, status))
    +        if ok then
    +            add_html(T("Nb returned values: %1", nbr))
    +            for i=1, nbr do
    +                local r = res[i]
    +                add_html(T("  %1: %2: %3", i, type(r), tostring(r)))
    +            end
    +            add_html("")
    +            add_html("Returned values as JSON:")
    +            add_html(json)
    +        else
    +            add_html(err)
    +            add_html(trace)
    +        end
    +        add_html("
    ") + html = table.concat(html, "\n") + return self:sendResponse(reqinfo, http_code, CTYPE.HTML, html) + else + return self:sendResponse(reqinfo, http_code, CTYPE.JSON, json) + end +end + +-- Handy function for testing the above, to be called with: +-- /koreader/ui/httpinspector/someFunctionForInteractiveTesting?/ +function HttpInspector:someFunctionForInteractiveTesting(...) + if select(1, ...) then + HttpInspector.foo.bar = true -- error + end + return self and self.name or "no self", #(table.pack(...)), "original args follow", ... + -- Copy and append this as args to the url, to get an error: + -- /true/nil/true/false/"true"/-1.2/"/"/abc/'d"/ef'/ + -- and to get a success: + -- /false/nil/true/false/"true"/-1.2/"/"/abc/'d"/ef'/ +end + +local _dispatcher_actions + +local getOrderedDispatcherActions = function() + if _dispatcher_actions then + return _dispatcher_actions + end + local Dispatcher = require("dispatcher") + local settings, order + local n = 1 + while true do + local name, value = debug.getupvalue(Dispatcher.init, n) + if not name then break end + if name == "settingsList" then + settings = value + break + end + n = n + 1 + end + while true do + local name, value = debug.getupvalue(Dispatcher.registerAction, n) + if not name then break end + if name == "dispatcher_menu_order" then + order = value + break + end + n = n + 1 + end + -- Copied and pasted from Dispatcher (we can't reach that the same way as above) + local section_list = { + {"general", _("General")}, + {"device", _("Device")}, + {"screen", _("Screen and lights")}, + {"filemanager", _("File browser")}, + {"reader", _("Reader")}, + {"rolling", _("Reflowable documents (epub, fb2, txt…)")}, + {"paging", _("Fixed layout documents (pdf, djvu, pics…)")}, + } + _dispatcher_actions = {} + for _, section in ipairs(section_list) do + table.insert(_dispatcher_actions, section[2]) + local section_key = section[1] + for _, k in ipairs(order) do + if settings[k][section_key] == true then + local t = util.tableDeepCopy(settings[k]) + t.dispatcher_id = k + table.insert(_dispatcher_actions, t) + end + end + end + -- Add a useful one + table.insert(_dispatcher_actions, 2, { general=true, separator=true, event="Close", category="none", title="Close top most widget"}) + return _dispatcher_actions +end + +function HttpInspector:exposeEvent(uri, reqinfo) + local ftype, fragment -- luacheck: no unused + ftype, fragment, uri = stepUriFragment(uri) -- luacheck: no unused + if fragment then + -- Event name and args provided. + -- We may get multiple events, separated by a dummy arg /&/ + local events = {} + local ev_names = {fragment} + local cur_ev_args = {fragment} + local args, nb_args = getVariablesFromUri(uri) + for i=1, nb_args do + local arg = args[i] + if arg ~= "&" then + if #cur_ev_args == 0 then + table.insert(ev_names, arg) + end + table.insert(cur_ev_args, arg) + else + table.insert(events, Event:new(table.unpack(cur_ev_args))) + cur_ev_args = {} + end + end + if #cur_ev_args > 0 then + table.insert(events, Event:new(table.unpack(cur_ev_args))) + end + -- As events may switch/reload the document, or exit/restart KOReader, + -- we delay them a bit so we can send the HTTP response and properly + -- shutdown the HTTP server + UIManager:nextTick(function() + for _, ev in ipairs(events) do + UIManager:sendEvent(ev) + end + end) + return self:sendResponse(reqinfo, 200, CTYPE.TEXT, T("Event sent: %1", table.concat(ev_names, ", "))) + end + + -- No event provided. + -- We want to show the list of actions exposed by Dispatcher (that are all handled as Events). + local actions = getOrderedDispatcherActions() + -- if true then return self:sendResponse(reqinfo, 200, CTYPE.JSON, getAsJsonString(actions)) end + local html = {} + local add_html = function(h) table.insert(html, h) end + add_html(T("High-level KOReader events")) + add_html(T("
    List of high-level KOReader events\n(all those available as actions for gestures and profiles)"))
    +    for _, action in ipairs(actions) do
    +        if type(action) == "string" then
    +            add_html(T("
    %1", action)) + elseif action.condition == false then + -- Some bottom menu are just disabled on all devices, + -- so just don't show any disabled action + do end -- luacheck: ignore 541 + else + local active = false + if action.general or action.device or action.screen then + active = true + elseif action.reader and self.ui.view then + active = true + elseif action.rolling and self.ui.rolling then + active = true + elseif action.paging and self.ui.paging then + active = true + elseif action.filemanager and self.ui.onSwipeFM then + active = true + end + + local title = action.title + if not active then + title = T("%1 (no effect on current application/document)", title) + end + add_html(T("%1", title)) + + -- Same messy logic as in Dispatcher:execute() (not everything has been tested). + local get_base_href = function() + return reqinfo.parsed_uri .. (action.event and "/"..action.event or "") + end + if action.configurable then + -- Such actions sends a first (possibly single with KOpt settings) event + -- to update the setting value for the bottom menu + -- We'll have to insert it in our single URL which may then carry 2 events + get_base_href = function(v, is_indice, single) + return T("%1/%2/%3/%4%5", reqinfo.parsed_uri, "ConfigChange", action.configurable.name, is_indice and action.configurable.values[v] or v, single and "" or (action.event and "/&/"..action.event or "")) + end + end + + if action.category == "none" then + -- Shouldn't have any 'configurable' + local href + if action.arg ~= nil then + href = T("%1/%2", get_base_href(), tostring(action.arg)) + else + href = get_base_href() + end + add_html(T(" %1", href)) + elseif action.category == "string" then + -- Multiple values, can have a 'configurable' + local args, toggle + if not action.args and action.args_func then + args, toggle = action.args_func() + else + args, toggle = action.args, action.toggle + end + if type(args[1]) == "table" then + add_html(T(" %1/... unsupported (table arguments)", get_base_href("..."))) + else + for i=1, #args do + local href = T("%1/%2", get_base_href(i, true), tostring(args[i])) + local unit = action.unit and " "..action.unit or "" + local default = args[i] == action.default and " (default)" or "" + add_html(T(" %1 \t%2%3%4", href, toggle[i], unit, default)) + end + end + elseif action.category == "absolutenumber" then + local suggestions = {} + if action.configurable and action.configurable.values then + for num, val in ipairs(action.configurable.values) do + local unit = action.unit and " "..action.unit or "" + local default = val == action.default and " (default)" or "" + table.insert(suggestions, { val, T("%1%2%3", val, unit, default) }) + end + else + local min, max = action.min, action.max + if min == -1 and max > 1 then + table.insert(suggestions, { min, "off / none" }) + min = 0 + end + table.insert(suggestions, { min, "min" }) + -- Add interesting values for specific actions + if action.dispatcher_id == "page_jmp" then + table.insert(suggestions, { -1, "-1 page" }) + end + table.insert(suggestions, { (min + max)/2, "" }) + if action.dispatcher_id == "page_jmp" then + table.insert(suggestions, { 1, "+1 page" }) + end + table.insert(suggestions, { max, "max" }) + end + for _, suggestion in ipairs(suggestions) do + local href = T("%1/%2", get_base_href(suggestion[1]), tostring(suggestion[1])) + add_html(T(" %1 \t%2", href, suggestion[2])) + end + elseif action.category == "incrementalnumber" then + -- Shouldn't have any 'configurable' + local suggestions = {} + local min, max = action.min, action.max + table.insert(suggestions, { min, "min" }) + if action.step then + for i=1, 5 do + min = min + action.step + table.insert(suggestions, { min, "" }) + end + else + table.insert(suggestions, { (min + max)/2, "" }) + end + table.insert(suggestions, { max, "max" }) + for _, suggestion in ipairs(suggestions) do + local href = T("%1/%2", get_base_href(suggestion[1]), tostring(suggestion[1])) + add_html(T(" %1 \t%2", href, suggestion[2])) + end + elseif action.category == "arg" then + add_html(T(" %1/... unsupported (gesture arguments)", get_base_href("..."))) + elseif action.category == "configurable" then + -- No other action event to send + for i=1, #action.configurable.values do + local href = T("%1", get_base_href(i, true)) + add_html(T(" %1 \t%2", href, action.toggle[i])) + end + else + -- Should not happen + add_html(T(" %1/... not implemented", get_base_href("..."))) + add_html(getAsJsonString(action)) + end + if action.separator then + add_html("") + end + end + end + add_html("
    ") + html = table.concat(html, "\n") + return self:sendResponse(reqinfo, 200, CTYPE.HTML, html) +end + +function HttpInspector:exposeBroadcastEvent(uri, reqinfo) + -- Similar to previous one, without any list. + local ftype, fragment -- luacheck: no unused + ftype, fragment, uri = stepUriFragment(uri) -- luacheck: no unused + if fragment then + -- Event name and args provided. + -- We may get multiple events, separated by a dummy arg /&/ + local events = {} + local ev_names = {fragment} + local cur_ev_args = {fragment} + local args, nb_args = getVariablesFromUri(uri) + for i=1, nb_args do + local arg = args[i] + if arg ~= "&" then + if #cur_ev_args == 0 then + table.insert(ev_names, arg) + end + table.insert(cur_ev_args, arg) + else + table.insert(events, Event:new(table.unpack(cur_ev_args))) + cur_ev_args = {} + end + end + if #cur_ev_args > 0 then + table.insert(events, Event:new(table.unpack(cur_ev_args))) + end + -- As events may switch/reload the document, or exit/restart KOReader, + -- we delay them a bit so we can send the HTTP response and properly + -- shutdown the HTTP server + UIManager:nextTick(function() + for _, ev in ipairs(events) do + UIManager:broadcastEvent(ev) + end + end) + return self:sendResponse(reqinfo, 200, CTYPE.TEXT, T("Event broadcasted: %1", table.concat(ev_names, ", "))) + end + + -- No event provided. + local html = {} + local add_html = function(h) table.insert(html, h) end + add_html(T("Broadcast event")) + add_html(T("
    No suggestion, use at your own risk."))
    +    add_html(T("Usage: %1", "/koreader/broadcast/EventName/arg1/arg2"))
    +    add_html("
    ") + html = table.concat(html, "\n") + return self:sendResponse(reqinfo, 200, CTYPE.HTML, html) +end + +return HttpInspector From fb39fe93ed5a19875b62f6ccaf608bbe2bf5b70e Mon Sep 17 00:00:00 2001 From: poire-z Date: Fri, 16 Feb 2024 20:09:12 +0100 Subject: [PATCH 07/15] Fix docs CI failing after previous commit --- plugins/httpinspector.koplugin/main.lua | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/plugins/httpinspector.koplugin/main.lua b/plugins/httpinspector.koplugin/main.lua index b0256fde3..32a0f647a 100644 --- a/plugins/httpinspector.koplugin/main.lua +++ b/plugins/httpinspector.koplugin/main.lua @@ -1,7 +1,5 @@ ---[[-- -This plugin allows for inspecting KOReader's internal objects, -calling methods, sending events... over HTTP. ---]]-- +-- This plugin allows for inspecting KOReader's internal objects, +-- calling methods, sending events... over HTTP. local DataStorage = require("datastorage") local Device = require("device") From d77b5118495e9b07f184c921ee615d1398a8ef4f Mon Sep 17 00:00:00 2001 From: poire-z Date: Fri, 16 Feb 2024 20:09:14 +0100 Subject: [PATCH 08/15] bump crengine: text selection and footnotes fixes and tweaks Includes: - LvDocView: allow setting custom title/authors/series - elementFromPoint(): fix possible crash when float at end of document - Non-linear fragments: fix generic handling on erm_final - DrawBorder: fix bottom border inset/outset drawing - getSegmentRects(): allow segments to include images - getRangeText(): allow gathering images - Allow standalone image in link to trigger in-page footnotes - ldomDocument::render(): avoid uneeded deserialization on each page turn - lvtinydom: add ldomNode::getAllInnerAttributeValues() - LVFootNote: avoid retrieving internal CompactArray object - In-page footnotes: allows for multiple id= inside them cre.cpp: - rename overrideDocumentProp() to setAltDocumentProp() - isLinkToFootnote(): handle image-only links - text selection functions: add includeImages param --- base | 2 +- frontend/apps/reader/modules/readerlink.lua | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/base b/base index b6ca09e06..8f5f38d73 160000 --- a/base +++ b/base @@ -1 +1 @@ -Subproject commit b6ca09e06615df75f2219270a5e09d8ea331cd00 +Subproject commit 8f5f38d732bba170abdae5df015f9f4b475fac6e diff --git a/frontend/apps/reader/modules/readerlink.lua b/frontend/apps/reader/modules/readerlink.lua index da9f8faca..a28a5e182 100644 --- a/frontend/apps/reader/modules/readerlink.lua +++ b/frontend/apps/reader/modules/readerlink.lua @@ -1058,7 +1058,9 @@ function ReaderLink:onGoToPageLink(ges, internal_links_only, max_distance) for _, link in ipairs(links) do -- link.uri may be an empty string with some invalid links: ignore them if link.section or (link.uri and link.uri ~= "") then - if link.segments then + -- Note: we may get segments empty in some conditions (in which + -- case we'll fallback to the 'else' branch and using x/y) + if link.segments and #link.segments > 0 then -- With segments, each is a horizontal segment, with start_x < end_x, -- and we should compute the distance from gesture position to -- each segment. From 76bf85738a31efa3e9ed321bf50bb066b12810c8 Mon Sep 17 00:00:00 2001 From: poire-z Date: Fri, 16 Feb 2024 20:09:16 +0100 Subject: [PATCH 09/15] Style tweaks: add inpage foootnote classnames and a tweak --- frontend/ui/data/css_tweaks.lua | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/frontend/ui/data/css_tweaks.lua b/frontend/ui/data/css_tweaks.lua index 6e0dd413d..7aecc3b2b 100644 --- a/frontend/ui/data/css_tweaks.lua +++ b/frontend/ui/data/css_tweaks.lua @@ -965,6 +965,8 @@ This tweak can be duplicated as a user style tweak when books contain footnotes .footnote, .footnotes, .fn, .note, .note1, .note2, .note3, .ntb, .ntb-txt, .ntb-txt-j, +.fnote, .fnote1, +.duokan-footnote-item, /* Common chinese books */ .przypis, .przypis1, /* Polish footnotes */ .voetnoten /* Dutch footnotes */ { @@ -1114,6 +1116,21 @@ If the footnote text uses variable or absolute font sizes, line height or vertic font-size: inherit !important; line-height: inherit !important; vertical-align: inherit !important; +} + ]], + }, + { + id = "inpage_footnote_combine_non_linear", + title = _("Combine footnotes in a non-linear flow"), + description = _([[ +This will mark each section of consecutive footnotes (at their original location in the book) as being non-linear. +The menu checkbox "Hide non-linear fragments" will then be available after the document is reopened, allowing to hide these sections from the regular flow: they will be skipped when turning pages and not considered in the various book & chapter progress and time to read features.]]), + priority = 6, + css = [[ +*, autoBoxing { + -cr-hint: late; + -cr-only-if: inpage-footnote; + -cr-hint: non-linear-combining; } ]], }, From 5d4747c5935d4be6e0617332eba56edd897d7b90 Mon Sep 17 00:00:00 2001 From: hius07 <62179190+hius07@users.noreply.github.com> Date: Sat, 17 Feb 2024 02:17:59 +0200 Subject: [PATCH 10/15] FileManager: less lfs calls (#11452) Use item info provided by FileChooser. Also fix showing PathChooser with invalid path, and fix issue when opening non-supported files. --- frontend/apps/filemanager/filemanager.lua | 18 ++++++++---------- frontend/document/documentregistry.lua | 2 +- frontend/ui/widget/filechooser.lua | 7 +++++-- plugins/coverbrowser.koplugin/covermenu.lua | 5 +++-- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/frontend/apps/filemanager/filemanager.lua b/frontend/apps/filemanager/filemanager.lua index 2646ea1f2..a5956feea 100644 --- a/frontend/apps/filemanager/filemanager.lua +++ b/frontend/apps/filemanager/filemanager.lua @@ -185,18 +185,18 @@ function FileManager:setupLayout() return true end - function file_chooser:onFileHold(file) + function file_chooser:onFileHold(item) if file_manager.select_mode then file_manager:tapPlus() else - self:showFileDialog(file) + self:showFileDialog(item) end end - function file_chooser:showFileDialog(file) -- luacheck: ignore - local is_file = isFile(file) - local is_folder = lfs.attributes(file, "mode") == "directory" - local is_not_parent_folder = BaseUtil.basename(file) ~= ".." + function file_chooser:showFileDialog(item) -- luacheck: ignore + local file = item.path + local is_file = item.is_file + local is_not_parent_folder = not item.is_go_up local function close_dialog_callback() UIManager:close(self.file_dialog) @@ -324,9 +324,7 @@ function FileManager:setupLayout() }, }) end - end - - if is_folder then + else -- folder local folder = BaseUtil.realpath(file) table.insert(buttons, { { @@ -1495,7 +1493,7 @@ function FileManager:openFile(file, provider, doc_caller_callback, aux_caller_ca if provider == nil then provider = DocumentRegistry:getProvider(file, true) -- include auxiliary end - if provider.order then -- auxiliary + if provider and provider.order then -- auxiliary if aux_caller_callback then aux_caller_callback() end diff --git a/frontend/document/documentregistry.lua b/frontend/document/documentregistry.lua index 117b3100e..860148805 100644 --- a/frontend/document/documentregistry.lua +++ b/frontend/document/documentregistry.lua @@ -98,7 +98,7 @@ function DocumentRegistry:getProvider(file, include_aux) return provider end -- highest weighted provider - return providers[1].provider + return providers and providers[1].provider end return self:getFallbackProvider() end diff --git a/frontend/ui/widget/filechooser.lua b/frontend/ui/widget/filechooser.lua index ec57e588a..66875e5d2 100644 --- a/frontend/ui/widget/filechooser.lua +++ b/frontend/ui/widget/filechooser.lua @@ -232,6 +232,9 @@ end function FileChooser:init() self.path_items = {} + if lfs.attributes(self.path, "mode") ~= "directory" then + self.path = G_reader_settings:readSetting("home_dir") or filemanagerutil.getDefaultDir() + end self.item_table = self:genItemTableFromPath(self.path) Menu.init(self) -- call parent's init() end @@ -540,7 +543,7 @@ function FileChooser:onMenuSelect(item) end function FileChooser:onMenuHold(item) - self:onFileHold(item.path) + self:onFileHold(item) return true end @@ -549,7 +552,7 @@ function FileChooser:onFileSelect(file) return true end -function FileChooser:onFileHold(file) +function FileChooser:onFileHold(item) return true end diff --git a/plugins/coverbrowser.koplugin/covermenu.lua b/plugins/coverbrowser.koplugin/covermenu.lua index 6b1bfb7d0..579c74b4c 100644 --- a/plugins/coverbrowser.koplugin/covermenu.lua +++ b/plugins/coverbrowser.koplugin/covermenu.lua @@ -242,10 +242,11 @@ function CoverMenu:updateItems(select_number) -- Replace it with ours -- This causes luacheck warning: "shadowing upvalue argument 'self' on line 34". -- Ignoring it (as done in filemanager.lua for the same showFileDialog) - self.showFileDialog = function(self, file) -- luacheck: ignore + self.showFileDialog = function(self, item) -- luacheck: ignore + local file = item.path -- Call original function: it will create a ButtonDialog -- and store it as self.file_dialog, and UIManager:show() it. - self.showFileDialog_orig(self, file) + self.showFileDialog_orig(self, item) local bookinfo = self.book_props -- getBookInfo(file) called by FileManager if not bookinfo or bookinfo._is_directory then From 76980098ec65878662fe98c2918bbbad35b13a74 Mon Sep 17 00:00:00 2001 From: hasezoey Date: Sat, 17 Feb 2024 01:22:06 +0100 Subject: [PATCH 11/15] FileChooser: add new sorting method (#11369) "percent - unopened - finished last" (ie. 90% > 50% > 0% > unopened > 100%) --- frontend/ui/widget/filechooser.lua | 36 ++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/frontend/ui/widget/filechooser.lua b/frontend/ui/widget/filechooser.lua index 66875e5d2..15a296bce 100644 --- a/frontend/ui/widget/filechooser.lua +++ b/frontend/ui/widget/filechooser.lua @@ -206,6 +206,42 @@ local FileChooser = Menu:extend{ return item.opened and string.format("%d %%", 100 * item.percent_finished) or "–" end, }, + percent_natural = { + -- sort 90% > 50% > 0% > unopened > 100% + text = _("percent - unopened - finished last"), + menu_order = 90, + can_collate_mixed = false, + init_sort_func = function(cache) + local natsort + natsort, cache = sort.natsort_cmp(cache) + local sortfunc = function(a, b) + if a.percent_finished == b.percent_finished then + return natsort(a.text, b.text) + elseif a.percent_finished == 1 then + return false + elseif b.percent_finished == 1 then + return true + else + return a.percent_finished > b.percent_finished + end + end + + return sortfunc, cache + end, + item_func = function(item) + local percent_finished + item.opened = DocSettings:hasSidecarFile(item.path) + if item.opened then + local doc_settings = DocSettings:open(item.path) + percent_finished = doc_settings:readSetting("percent_finished") + end + -- smooth 2 decimal points (0.00) instead of 16 decimal numbers + item.percent_finished = math.floor((percent_finished or -1) * 100) / 100 + end, + mandatory_func = function(item) + return item.opened and string.format("%d %%", 100 * item.percent_finished) or "–" + end, + }, }, } From 041117cbb630a5f3840220953533acf274f1c2cd Mon Sep 17 00:00:00 2001 From: hugleo Date: Fri, 16 Feb 2024 21:26:21 -0300 Subject: [PATCH 12/15] ReaderZooming: revert Autocrop fix (#11325) From f990937f9f. --- frontend/apps/reader/modules/readerzooming.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/apps/reader/modules/readerzooming.lua b/frontend/apps/reader/modules/readerzooming.lua index 44ecd77e1..9247242cd 100644 --- a/frontend/apps/reader/modules/readerzooming.lua +++ b/frontend/apps/reader/modules/readerzooming.lua @@ -527,7 +527,7 @@ function ReaderZooming:getZoom(pageno) local ubbox_dimen = self.ui.document:getUsedBBoxDimensions(pageno, 1) -- if bbox is larger than the native page dimension render the full page -- See discussion in koreader/koreader#970. - if (ubbox_dimen.w <= page_size.w and ubbox_dimen.h <= page_size.h) or (self.ui.document.configurable.trim_page == 1) then + if ubbox_dimen.w <= page_size.w and ubbox_dimen.h <= page_size.h then page_size = ubbox_dimen self.view:onBBoxUpdate(ubbox_dimen) else From ef0077df2377006c7e586c75fc148826a1fa1307 Mon Sep 17 00:00:00 2001 From: hugleo Date: Fri, 16 Feb 2024 21:31:31 -0300 Subject: [PATCH 13/15] ReaderPaging: fix offset issue on next page in pan zoom (#11408) --- frontend/apps/reader/modules/readerpaging.lua | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frontend/apps/reader/modules/readerpaging.lua b/frontend/apps/reader/modules/readerpaging.lua index d83e4f8e5..3bf67199e 100644 --- a/frontend/apps/reader/modules/readerpaging.lua +++ b/frontend/apps/reader/modules/readerpaging.lua @@ -1029,8 +1029,15 @@ function ReaderPaging:onGotoPageRel(diff) goto_end(y) goto_end(x) elseif new_page > 0 then + -- Be sure that the new and old view areas are reset so that no value is carried over to next page. + -- Without this, we would have panned_y = new_va.y - old_va.y > 0, and panned_y will be added to the next page's y direction. + -- This occurs when the current page has a y > 0 position (for example, a cropped page) and can fit the whole page height, + -- while the next page needs scrolling in the height. self:_gotoPage(new_page) + new_va = self.visible_area:copy() + old_va = self.visible_area goto_end(y, -y_diff) + goto_end(x, -x_diff) else goto_end(x) end From 52fae11da79dda68caf634825f97272dc084b52d Mon Sep 17 00:00:00 2001 From: hugleo Date: Fri, 16 Feb 2024 21:34:40 -0300 Subject: [PATCH 14/15] ReaderView: ensure pan zoom mode on document open (#11425) When we open the document for the first time, the pan positions are not being applied. If I use the bottom-to-top mode we should see the bottom first, but the top is being shown instead. --- frontend/apps/reader/modules/readerview.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/apps/reader/modules/readerview.lua b/frontend/apps/reader/modules/readerview.lua index a8e5bbbe6..4e872edd2 100644 --- a/frontend/apps/reader/modules/readerview.lua +++ b/frontend/apps/reader/modules/readerview.lua @@ -695,7 +695,7 @@ function ReaderView:recalculate() -- start from right of page_area self.visible_area.x = self.page_area.x + self.page_area.w - self.visible_area.w end - if self.ui.zooming.zoom_bottom_to_top then + if self.document.configurable.zoom_direction >= 2 and self.document.configurable.zoom_direction <= 5 then -- zoom_bottom_to_top -- starts from bottom of page_area self.visible_area.y = self.page_area.y + self.page_area.h - self.visible_area.h else From d4c78aaa4fb4ccf5178183b5ac09a154c457a5b5 Mon Sep 17 00:00:00 2001 From: Hzj_jie Date: Sat, 17 Feb 2024 14:09:48 -0800 Subject: [PATCH 15/15] Kindle oasis has no ambient brightness sensor (#11456) I did not notice an ambient brightness sensor, nor adaptive brightness feature. (Ref: https://www.geekwire.com/2016/kindle-oasis-review-amazons-premium-e-reader-use-premium-features/) Was it previously wrongly configured? --- frontend/device/kindle/device.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/device/kindle/device.lua b/frontend/device/kindle/device.lua index febe6a49b..7c093f079 100644 --- a/frontend/device/kindle/device.lua +++ b/frontend/device/kindle/device.lua @@ -560,7 +560,6 @@ local KindleOasis = Kindle:extend{ model = "KindleOasis", isTouchDevice = yes, hasFrontlight = yes, - hasLightSensor = yes, hasKeys = yes, hasGSensor = yes, display_dpi = 300,