diff --git a/frontend/apps/filemanager/filemanagercollection.lua b/frontend/apps/filemanager/filemanagercollection.lua index 7072578d2..c08870af0 100644 --- a/frontend/apps/filemanager/filemanagercollection.lua +++ b/frontend/apps/filemanager/filemanagercollection.lua @@ -1388,7 +1388,7 @@ function FileManagerCollection:searchCollections(coll_name) -- Fortunately, this is run in a subprocess, so we won't be affecting the -- main process's crengine state or any document opened in the main -- process (we furthermore prevent this feature when one is opened). - -- To avoid creating half-rendered/invalide cache files, it's best to disable + -- To avoid creating half-rendered/invalid cache files, it's best to disable -- crengine saving of such cache files. if not self.is_cre_cache_disabled then local cre = require("document/credocument"):engineInit() diff --git a/frontend/document/document.lua b/frontend/document/document.lua index 1308e0630..30f4ede2c 100644 --- a/frontend/document/document.lua +++ b/frontend/document/document.lua @@ -486,7 +486,7 @@ function Document:renderPage(pageno, rect, zoom, rotation, gamma, hinting) -- Make the context match the rotation, -- by pointing at the rotated origin via coordinates offsets. -- NOTE: We rotate our *Screen* bb on rotation (SetRotationMode), not the document, - -- so we hardly ever exercize this codepath... + -- so we hardly ever exercise this codepath... -- AFAICT, the only thing that *ever* (attempted to) rotate the document was ReaderRotation's key bindings (RotationUpdate). --- @note: It was broken as all hell (it had likely never worked outside of its original implementation in KPV), and has been removed in #12658 if rotation == 90 then diff --git a/frontend/document/koptinterface.lua b/frontend/document/koptinterface.lua index e97d7b058..30b7cad6a 100644 --- a/frontend/document/koptinterface.lua +++ b/frontend/document/koptinterface.lua @@ -297,7 +297,7 @@ function KoptInterface:reflowPage(doc, pageno, bbox, background) kc:setPreCache() self.bg_thread = true end - -- Caculate zoom. + -- Calculate zoom. kc.zoom = (1.5 * kc.zoom * kc.quality * kc.dev_width) / bbox.x1 -- Generate pixmap. local page = doc._document:openPage(pageno) @@ -1433,7 +1433,7 @@ end local function get_pattern_list(pattern, case_insensitive) -- pattern list of single words local plist = {} - -- (as in util.splitToWords(), but only splitting on spaces, keeping punctuations) + -- (as in util.splitToWords(), but only splitting on spaces, keeping punctuation marks) for word in util.gsplit(pattern, "%s+") do if util.hasCJKChar(word) then for char in util.gsplit(word, "[\192-\255][\128-\191]+", true) do diff --git a/frontend/ui/widget/bookmapwidget.lua b/frontend/ui/widget/bookmapwidget.lua index 8e8c3ee7c..69bdfced9 100644 --- a/frontend/ui/widget/bookmapwidget.lua +++ b/frontend/ui/widget/bookmapwidget.lua @@ -618,7 +618,7 @@ function BookMapRow:paintTo(bb, x, y) alt_bb = glyph.bb:rotatedCopy(indicator.rotation) end -- Glyph's bb fit the blackbox of the glyph, so there's no cropping - -- or complicated positionning to do + -- or complicated positioning to do -- By default, just center the glyph at x local d_x_pct = indicator.shift_x_pct or 0.5 local d_x = math.floor(glyph.bb:getWidth() * d_x_pct) @@ -759,7 +759,7 @@ function BookMapWidget:init() } end - -- No real need for any explicite edge and inter-row padding: + -- No real need for any explicit edge and inter-row padding: -- we use the scrollbar width on both sides for balance (we may put a start -- page number on the left space), and each BookMapRow will have itself some -- blank space at bottom below page slots (where we may put hanging markers diff --git a/frontend/ui/widget/inputdialog.lua b/frontend/ui/widget/inputdialog.lua index 8ab3340af..0887e725e 100644 --- a/frontend/ui/widget/inputdialog.lua +++ b/frontend/ui/widget/inputdialog.lua @@ -643,7 +643,7 @@ function InputDialog:toggleKeyboard(force_toggle) -- Remember the *current* visibility, as the following close will reset it local visible = self:isKeyboardVisible() - -- When we forcibly close the keyboard, remember its current visiblity state, so that we can properly restore it later. + -- When we forcibly close the keyboard, remember its current visibility state, so that we can properly restore it later. -- (This is used by some buttons in fullscreen mode, where we might want to keep the original keyboard hidden when popping up a new one for another InputDialog). if force_toggle == false then -- NOTE: visible will be nil between our own init and a show of the keyboard, which is precisely what happens when we *hide* the keyboard. diff --git a/plugins/autowarmth.koplugin/main.lua b/plugins/autowarmth.koplugin/main.lua index 0a9534ff0..702144fd8 100644 --- a/plugins/autowarmth.koplugin/main.lua +++ b/plugins/autowarmth.koplugin/main.lua @@ -352,7 +352,7 @@ function AutoWarmth:scheduleMidnightUpdate(from_resume) self.current_times_h[1] = nil -- Solar midnight prev. day self.current_times_h[2] = nil -- Astronomical dawn self.current_times_h[3] = nil -- Nautical dawn - self.current_times_h[6] = nil -- Solar noon + -- self.current_times_h[6] = nil -- Solar noon self.current_times_h[9] = nil -- Nautical dusk self.current_times_h[10] = nil -- Astronomical dusk self.current_times_h[11] = nil -- Solar midnight @@ -642,7 +642,7 @@ function AutoWarmth:getSubMenuItems() return self.activate ~= 0 end, text = Device:hasNaturalLight() and _("Warmth and night mode settings") or _("Night mode settings"), - sub_item_table = self:getWarmthMenu(), + sub_item_table_func = function() return self:getWarmthMenu() end, }, self:getFlOffDuringDayMenu(), self:getTimesMenu(_("Currently active parameters")), @@ -1148,7 +1148,7 @@ function AutoWarmth:getWarmthMenu() text = Device:hasNaturalLight() and _("Set warmth and night mode for:") or _("Set night mode for:"), enabled = false, }, - getWarmthMenuEntry(_("Solar noon"), 6, false), + getWarmthMenuEntry(_("Solar noon"), 6), getWarmthMenuEntry(_("Sunset and sunrise"), 5), getWarmthMenuEntry(_("Darkest time of civil twilight"), 4), getWarmthMenuEntry(_("Darkest time of nautical twilight"), 3, false), @@ -1247,7 +1247,7 @@ function AutoWarmth:showTimesInfo(title, location, activator, request_easy) local face = Font:getFace("scfont") UIManager:show(InfoMessage:new{ face = face, - width = math.floor(Screen:getWidth() * (self.easy_mode and 0.75 or 0.90)), + width = math.floor(Screen:getWidth() * (self.easy_mode and 0.85 or 0.90)), text = title .. location_string .. ":\n\n" .. info_line(0, _("Solar midnight:"), times[1], 1, face, request_easy) .. add_line(2, _("Dawn"), request_easy) .. @@ -1258,8 +1258,8 @@ function AutoWarmth:showTimesInfo(title, location, activator, request_easy) add_line(2, _("Dawn"), request_easy) .. info_line(0, _("Sunrise:"), times[5], 5, face) .. "\n" .. - info_line(0, _("Solar noon:"), times[6], 6, face, request_easy) .. - add_line(0, "", request_easy) .. + info_line(0, _("Solar noon:"), times[6], 6, face) .. + "\n" .. info_line(0, _("Sunset:"), times[7], 7, face) .. add_line(2, _("Dusk"), request_easy) .. info_line(request_easy and 0 or 4, diff --git a/plugins/opds.koplugin/opdsbrowser.lua b/plugins/opds.koplugin/opdsbrowser.lua index d5b4c348b..f4b808577 100644 --- a/plugins/opds.koplugin/opdsbrowser.lua +++ b/plugins/opds.koplugin/opdsbrowser.lua @@ -18,6 +18,7 @@ local TextViewer = require("ui/widget/textviewer") local Trapper = require("ui/trapper") local UIManager = require("ui/uimanager") local http = require("socket.http") +local ffiUtil = require("ffi/util") local lfs = require("libs/libkoreader-lfs") local logger = require("logger") local ltn12 = require("ltn12") @@ -27,7 +28,7 @@ local url = require("socket.url") local util = require("util") local _ = require("gettext") local N_ = _.ngettext -local T = require("ffi/util").template +local T = ffiUtil.template -- cache catalog parsed from feed xml local CatalogCache = Cache:new{ @@ -42,6 +43,7 @@ local OPDSBrowser = Menu:extend{ acquisition_rel = "^http://opds%-spec%.org/acquisition", borrow_rel = "http://opds-spec.org/acquisition/borrow", stream_rel = "http://vaemendis.net/opds-pse/stream", + facet_rel = "http://opds-spec.org/facet", image_rel = { ["http://opds-spec.org/image"] = true, ["http://opds-spec.org/cover"] = true, -- ManyBooks.net, not in spec @@ -56,6 +58,7 @@ local OPDSBrowser = Menu:extend{ root_catalog_title = nil, root_catalog_username = nil, root_catalog_password = nil, + facet_groups = nil, -- Stores OPDS facet groups title_shrink_font_to_fit = true, } @@ -67,6 +70,7 @@ function OPDSBrowser:init() self.onLeftButtonTap = function() self:showOPDSMenu() end + self.facet_groups = nil -- Initialize facet groups storage Menu.init(self) -- call parent's init() end @@ -135,6 +139,74 @@ function OPDSBrowser:showOPDSMenu() UIManager:show(dialog) end +-- Shows facet menu for OPDS catalogs with facets/search support +function OPDSBrowser:showFacetMenu() + local buttons = {} + local dialog + local catalog_url = self.paths[#self.paths].url + + -- Add sub-catalog to bookmarks option first + table.insert(buttons, {{ + text = "\u{f067} " .. _("Add catalog"), + callback = function() + UIManager:close(dialog) + self:addSubCatalog(catalog_url) + end, + align = "left", + }}) + table.insert(buttons, {}) -- separator + + -- Add search option if available + if self.search_url then + table.insert(buttons, {{ + text = "\u{f002} " .. _("Search"), + callback = function() + UIManager:close(dialog) + self:searchCatalog(self.search_url) + end, + align = "left", + }}) + table.insert(buttons, {}) -- separator + end + + -- Add facet groups + if self.facet_groups then + for group_name, facets in ffiUtil.orderedPairs(self.facet_groups) do + table.insert(buttons, { + { text = "\u{f0b0} " .. group_name, enabled = false, align = "left" } + }) + + for __, link in ipairs(facets) do + local facet_text = link.title + if link["thr:count"] then + facet_text = T(_("%1 (%2)"), facet_text, link["thr:count"]) + end + if link["opds:activeFacet"] == "true" then + facet_text = "✓ " .. facet_text + end + table.insert(buttons, {{ + text = facet_text, + callback = function() + UIManager:close(dialog) + self:updateCatalog(url.absolute(catalog_url, link.href)) + end, + align = "left", + }}) + end + table.insert(buttons, {}) -- separator between groups + end + end + + dialog = ButtonDialog:new{ + buttons = buttons, + shrink_unneeded_width = true, + anchor = function() + return self.title_bar.left_button.image.dimen + end, + } + UIManager:show(dialog) +end + local function buildRootEntry(server) local icons = "" @@ -430,13 +502,18 @@ function OPDSBrowser:genItemTableFromURL(item_url) return self:genItemTableFromCatalog(catalog, item_url) end +-- Generates catalog item table and processes OPDS facets/search links function OPDSBrowser:genItemTableFromCatalog(catalog, item_url) local item_table = {} + self.facet_groups = nil -- Reset facets + self.search_url = nil -- Reset search URL + if not catalog then return item_table end local feed = catalog.feed or catalog + self.facet_groups = {} -- Initialize table to store facet groups local function build_href(href) return url.absolute(item_url, href) @@ -456,24 +533,24 @@ function OPDSBrowser:genItemTableFromCatalog(catalog, item_url) -- OpenSearch if link.type:find(self.search_type) then if link.href then - table.insert(item_table, { -- the first item in each subcatalog - text = "\u{f002} " .. _("Search"), -- append SEARCH icon - url = build_href(self:getSearchTemplate(build_href(link.href))), - searchable = true, - }) + self.search_url = build_href(self:getSearchTemplate(build_href(link.href))) has_opensearch = true end end -- Calibre search (also matches the actual template for OpenSearch!) if link.type:find(self.search_template_type) and link.rel and link.rel:find("search") then if link.href and not has_opensearch then - table.insert(item_table, { - text = "\u{f002} " .. _("Search"), - url = build_href(link.href:gsub("{searchTerms}", "%%s")), - searchable = true, - }) + self.search_url = build_href(link.href:gsub("{searchTerms}", "%%s")) end end + -- Process OPDS facets + if link.rel == self.facet_rel then + local group_name = link["opds:facetGroup"] or _("Filters") + if not self.facet_groups[group_name] then + self.facet_groups[group_name] = {} + end + table.insert(self.facet_groups[group_name], link) + end end end end @@ -591,13 +668,16 @@ function OPDSBrowser:genItemTableFromCatalog(catalog, item_url) item.content = entry.content or entry.summary table.insert(item_table, item) end + + if next(self.facet_groups) == nil then self.facet_groups = nil end -- Clear if empty + return item_table end -- Requests and shows updated list of catalog entries function OPDSBrowser:updateCatalog(item_url, paths_updated) local menu_table = self:genItemTableFromURL(item_url) - if #menu_table > 0 then + if #menu_table > 0 or self.facet_groups or self.search_url then if not paths_updated then table.insert(self.paths, { url = item_url, @@ -605,10 +685,20 @@ function OPDSBrowser:updateCatalog(item_url, paths_updated) }) end self:switchItemTable(self.catalog_title, menu_table) - self:setTitleBarLeftIcon("plus") - self.onLeftButtonTap = function() - self:addSubCatalog(item_url) + + -- Set appropriate title bar icon based on content + if self.facet_groups or self.search_url then + self:setTitleBarLeftIcon("appbar.menu") + self.onLeftButtonTap = function() + self:showFacetMenu() + end + else + self:setTitleBarLeftIcon("plus") + self.onLeftButtonTap = function() + self:addSubCatalog(item_url) + end end + if self.page_num <= 1 then -- Request more content, but don't change the page self:onNextPage(true) @@ -620,11 +710,8 @@ end function OPDSBrowser:appendCatalog(item_url) local menu_table = self:genItemTableFromURL(item_url) if #menu_table > 0 then - for _, item in ipairs(menu_table) do - -- Don't append multiple search entries - if not item.searchable then - table.insert(self.item_table, item) - end + for __, item in ipairs(menu_table) do + table.insert(self.item_table, item) end self.item_table.hrefs = menu_table.hrefs self:switchItemTable(self.catalog_title, self.item_table, -1) diff --git a/plugins/terminal.koplugin/main.lua b/plugins/terminal.koplugin/main.lua index 6a42d774a..ce33c8792 100644 --- a/plugins/terminal.koplugin/main.lua +++ b/plugins/terminal.koplugin/main.lua @@ -6,6 +6,7 @@ This plugin provides a terminal emulator (VT52 (+some ANSI and some VT100)) local Device = require("device") local logger = require("logger") +local buffer = require("string.buffer") local util = require("util") local ffi = require("ffi") local C = ffi.C @@ -135,8 +136,7 @@ function Terminal:init() self:onDispatcherRegisterActions() self.ui.menu:registerToMainMenu(self) - self.chunk_size = CHUNK_SIZE - self.chunk = ffi.new('uint8_t[?]', self.chunk_size) + self.chunk = buffer.new(CHUNK_SIZE) self.terminal_data = DataStorage:getDataDir() lfs.mkdir(self.terminal_data .. "/scripts") @@ -272,23 +272,21 @@ function Terminal:spawnShell(cols, rows) end function Terminal:receive() - local last_result = "" + local ptr = self.chunk:reset():ref() + local free = CHUNK_SIZE repeat C.tcdrain(self.ptmx) - local count = tonumber(C.read(self.ptmx, self.chunk, self.chunk_size)) - if count > 0 then - last_result = last_result .. string.sub(ffi.string(self.chunk), 1, count) + local count = tonumber(C.read(self.ptmx, ptr, free)) + if count <= 0 then + break end - until count <= 0 or #last_result >= self.chunk_size - 1 - return last_result + ptr = ptr + count + free = free - count + until free == 0 + return self.chunk:commit(CHUNK_SIZE - free):get() end -function Terminal:refresh(reset) - if reset then - self.refresh_time = 1/32 - UIManager:unschedule(Terminal.refresh) - end - +function Terminal:refresh() local next_text = self:receive() if next_text ~= "" then self.input_widget:interpretAnsiSeq(next_text) @@ -314,7 +312,11 @@ end function Terminal:transmit(chars) C.write(self.ptmx, chars, #chars) - self:refresh(true) + self.refresh_time = 1/32 + UIManager:unschedule(Terminal.refresh) + UIManager:tickAfterNext(function() + UIManager:scheduleIn(self.refresh_time, Terminal.refresh, self) + end) end --- kills a running shell diff --git a/spec/unit/opds_spec.lua b/spec/unit/opds_spec.lua index cb312e77b..f2c56239c 100644 --- a/spec/unit/opds_spec.lua +++ b/spec/unit/opds_spec.lua @@ -186,6 +186,39 @@ One Thousand Mythological Characters Briefly Described ]] +-- https://www.gutenberg.org/catalog/osd-books.xml +local opensearch_sample = [[ + + + Project Gutenberg + Gutenberg + Search the Project Gutenberg ebook catalog. + free ebooks books public domain + Marcello Perathoner + webmaster@gutenberg.org + + + + + + + + + + + + Search Data Copyright 1971-2012, Project Gutenberg, All Rights Reserved. + open + en-us + UTF-8 + UTF-8 + +]] + local popular_new_sample = [[ @@ -320,38 +353,70 @@ describe("OPDS module", function() end) describe("OPDS browser module", function() + before_each(function() + local Cache = require("cache") + stub(Cache, "check", function() return nil end) + end) + + after_each(function() + local Cache = require("cache") + if Cache.check.revert then + Cache.check:revert() + end + end) + describe("URL generation", function() - it("should generate search item #internet", function() - local catalog = OPDSParser:parse(navigation_sample) - local item_table = OPDSBrowser:genItemTableFromCatalog(catalog, "https://www.gutenberg.org/ebooks.opds/?format=opds") + it("should generate search url and catalog items #internet", function() + local fetch_feed_stub = stub(OPDSBrowser, "getSearchTemplate", function(self, osd_url) + local search_descriptor = OPDSParser:parse(opensearch_sample) + if search_descriptor and search_descriptor.OpenSearchDescription and search_descriptor.OpenSearchDescription.Url then + for _, candidate in ipairs(search_descriptor.OpenSearchDescription.Url) do + if candidate.type and candidate.template and candidate.type:find(self.search_template_type) then + return candidate.template:gsub("{searchTerms}", "%%s") + end + end + end + return nil + end) + + local main_catalog = OPDSParser:parse(navigation_sample) + local item_table = OPDSBrowser:genItemTableFromCatalog(main_catalog, "https://www.gutenberg.org/ebooks.opds/?format=opds") + + assert.truthy(OPDSBrowser.search_url) + assert.are.same("http://m.gutenberg.org/ebooks/search.opds/?query=%s", OPDSBrowser.search_url) assert.truthy(item_table) - assert.are.same(item_table[1].text, "\u{f002} " .. "Search") + assert.are.same(3, #item_table) + assert.are.same("Popular", item_table[1].title) + assert.are.same("Latest", item_table[2].title) + assert.are.same("Random", item_table[3].title) + + fetch_feed_stub:revert() end) it("should generate URL on rel=subsection #internet", function() local catalog = OPDSParser:parse(navigation_sample) local item_table = OPDSBrowser:genItemTableFromCatalog(catalog, "https://www.gutenberg.org/ebooks.opds/?format=opds") assert.truthy(item_table) - assert.are.same(item_table[2].title, "Popular") - assert.are.same(item_table[2].url, "https://www.gutenberg.org/ebooks/search.opds/?sort_order=downloads") + assert.are.same(item_table[1].title, "Popular") + assert.are.same(item_table[1].url, "https://www.gutenberg.org/ebooks/search.opds/?sort_order=downloads") end) it("should generate URL on rel=popular and rel=new #internet", function() local catalog = OPDSParser:parse(popular_new_sample) local item_table = OPDSBrowser:genItemTableFromCatalog(catalog, "http://www.feedbooks.com/publicdomain/catalog.atom") assert.truthy(item_table) - assert.are.same(item_table[2].title, "Most popular") - assert.are.same(item_table[2].url, "https://catalog.feedbooks.com/publicdomain/browse/top.atom?lang=en") - assert.are.same(item_table[3].title, "Recently added") - assert.are.same(item_table[3].url, "https://catalog.feedbooks.com/publicdomain/browse/recent.atom?lang=en") + assert.are.same(item_table[1].title, "Most popular") + assert.are.same(item_table[1].url, "https://catalog.feedbooks.com/publicdomain/browse/top.atom?lang=en") + assert.are.same(item_table[2].title, "Recently added") + assert.are.same(item_table[2].url, "https://catalog.feedbooks.com/publicdomain/browse/recent.atom?lang=en") end) it("should use the main URL for faceted links as long as faceted links aren't properly supported #internet", function() local catalog = OPDSParser:parse(facet_sample) local item_table = OPDSBrowser:genItemTableFromCatalog(catalog, "http://flibusta.is/opds") assert.truthy(item_table) - assert.are.same(item_table[2].url, "http://flibusta.is/opds/author/75357") + assert.are.same(item_table[1].url, "http://flibusta.is/opds/author/75357") end) end) @@ -360,7 +425,7 @@ describe("OPDS module", function() local item_table = OPDSBrowser:genItemTableFromCatalog(catalog, "http://flibusta.is/opds") assert.truthy(item_table) - assert.are_not.same(item_table[2].image, "http://flibusta.is/opds/author/75357") + assert.are_not.same(item_table[1].image, "http://flibusta.is/opds/author/75357") end) end) end)