diff --git a/.circleci/config.yml b/.circleci/config.yml index be0ef607d..d8cdc07fa 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -38,6 +38,7 @@ jobs: CCACHE_MAXSIZE: "128M" CLICOLOR_FORCE: "1" EMULATE_READER: "1" + KODEBUG: "" MAKEFLAGS: "PARALLEL_JOBS=3 OUTPUT_DIR=build INSTALL_DIR=install" steps: # Checkout / fetch. {{{ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8bdd110ab..a8094073b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,6 +35,7 @@ jobs: # Bump first number to reset all caches. CACHE_KEY: "1-macOS-${{ matrix.image }}-${{ matrix.platform }}-XC${{ matrix.xcode_version }}-DT${{ matrix.deployment_target }}" CLICOLOR_FORCE: '1' + KODEBUG: "" MACOSX_DEPLOYMENT_TARGET: ${{ matrix.deployment_target }} MAKEFLAGS: 'OUTPUT_DIR=build INSTALL_DIR=install TARGET=macos' diff --git a/base b/base index e9a00bdf9..b4e76037f 160000 --- a/base +++ b/base @@ -1 +1 @@ -Subproject commit e9a00bdf9c764c170db081d5a8fa1d8b7cb2e4b4 +Subproject commit b4e76037fa8dbe9e6511c07486065f9fec1a96f8 diff --git a/frontend/apps/filemanager/filemanager.lua b/frontend/apps/filemanager/filemanager.lua index 9b4ce2c52..ed88a071a 100644 --- a/frontend/apps/filemanager/filemanager.lua +++ b/frontend/apps/filemanager/filemanager.lua @@ -129,7 +129,7 @@ function FileManager:setupLayout() button_padding = Screen:scaleBySize(5), left_icon = "home", left_icon_size_ratio = 1, - left_icon_tap_callback = function() self:goHome() end, + left_icon_tap_callback = function() self:onHome() end, left_icon_hold_callback = function() self:onShowFolderMenu() end, right_icon = self.selected_files and "check" or "plus", right_icon_size_ratio = 1, @@ -163,7 +163,7 @@ function FileManager:setupLayout() if file_manager.selected_files then -- toggle selection item.dim = not item.dim and true or nil file_manager.selected_files[item.path] = item.dim - self:updateItems() + self:updateItems(1, true) else file_manager:openFile(item.path) end @@ -212,7 +212,7 @@ function FileManager:setupLayout() if is_file then file_manager.selected_files[file] = true item.dim = true - self:updateItems() + self:updateItems(1, true) end end, }, @@ -649,10 +649,11 @@ function FileManager:tapPlus() end else -- no selected files + local folder = self.file_chooser.path local function refresh_titlebar_callback() - self:updateTitleBarPath() + self:updateTitleBarPath(folder) end - title = BD.dirpath(filemanagerutil.abbreviate(self.file_chooser.path)) + title = BD.dirpath(filemanagerutil.abbreviate(folder)) buttons = { { { @@ -696,7 +697,7 @@ function FileManager:tapPlus() text = _("Go to HOME folder"), callback = function() UIManager:close(plus_dialog) - self:goHome() + self:onHome() end }, }, @@ -706,12 +707,12 @@ function FileManager:tapPlus() callback = function() UIManager:close(plus_dialog) -- any random document - self:openRandomFile(self.file_chooser.path, false) + self:openRandomFile(folder, false) end, hold_callback = function() UIManager:close(plus_dialog) -- only previously unopened - self:openRandomFile(self.file_chooser.path, true) + self:openRandomFile(folder, true) end }, }, @@ -719,7 +720,7 @@ function FileManager:tapPlus() self.folder_shortcuts:genShowFolderShortcutsButton(close_dialog_callback), }, { - self.folder_shortcuts:genAddRemoveShortcutButton(self.file_chooser.path, close_dialog_callback, refresh_titlebar_callback), + self.folder_shortcuts:genAddRemoveShortcutButton(folder, close_dialog_callback, refresh_titlebar_callback), }, } @@ -727,12 +728,12 @@ function FileManager:tapPlus() table.insert(buttons, 4, { -- after "Paste" or "Import files here" button { text_func = function() - return Device:isValidPath(self.file_chooser.path) + return Device:isValidPath(folder) and _("Switch to SDCard") or _("Switch to internal storage") end, callback = function() UIManager:close(plus_dialog) - if Device:isValidPath(self.file_chooser.path) then + if Device:isValidPath(folder) then local ok, sd_path = Device:hasExternalSD() if ok then self.file_chooser:changeToPath(sd_path) @@ -749,10 +750,10 @@ function FileManager:tapPlus() table.insert(buttons, 4, { -- always after "Paste" button { text = _("Import files here"), - enabled = Device:isValidPath(self.file_chooser.path), + enabled = Device:isValidPath(folder), callback = function() UIManager:close(plus_dialog) - Device.importFile(self.file_chooser.path) + Device.importFile(folder) end, }, }) @@ -847,7 +848,10 @@ function FileManager:onRefresh() return true end -function FileManager:goHome() +FileManager.onRefreshContent = FileManager.onRefresh +FileManager.onBookMetadataChanged = FileManager.onRefresh + +function FileManager:onHome() if not self.file_chooser:goHome() then self:setHome() end @@ -1307,18 +1311,6 @@ function FileManager:copyRecursive(from, to) return ffiUtil.execute(self.cp_bin, "-r", from, to ) == 0 end -function FileManager:onHome() - return self:goHome() -end - -function FileManager:onRefreshContent() - self:onRefresh() -end - -function FileManager:onBookMetadataChanged() - self:onRefresh() -end - function FileManager:onShowFolderMenu() local button_dialog local function genButton(button_text, button_path) 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/apps/reader/modules/readerbookmark.lua b/frontend/apps/reader/modules/readerbookmark.lua index f05a6d176..538e557c3 100644 --- a/frontend/apps/reader/modules/readerbookmark.lua +++ b/frontend/apps/reader/modules/readerbookmark.lua @@ -99,6 +99,7 @@ function ReaderBookmark:addToMainMenu(menu_items) checked_func = function() return self.ui.paging.bookmark_flipping_mode end, + check_callback_closes_menu = true, callback = function(touchmenu_instance) self.ui.paging:onToggleBookmarkFlipping() touchmenu_instance:closeMenu() diff --git a/frontend/apps/reader/modules/readerdictionary.lua b/frontend/apps/reader/modules/readerdictionary.lua index e7fd048a6..67c11b0cc 100644 --- a/frontend/apps/reader/modules/readerdictionary.lua +++ b/frontend/apps/reader/modules/readerdictionary.lua @@ -852,6 +852,9 @@ function ReaderDictionary:cleanSelection(text, is_sane) text = text:gsub("\u{2019}", "'") -- Right single quotation mark -- Strip punctuation characters around selection text = util.stripPunctuation(text) + -- In some dictionaries, both interpuncts (·) and pipes (|) are used to delimiter syllables. + -- Up arrows (↑), are used in some dictionaries to indicate related words. + text = text:gsub("[·|↑]", "") -- Strip some common english grammatical construct text = text:gsub("'s$", '') -- english possessive -- Strip some common french grammatical constructs diff --git a/frontend/apps/reader/modules/readerhighlight.lua b/frontend/apps/reader/modules/readerhighlight.lua index ea4059870..70281d643 100644 --- a/frontend/apps/reader/modules/readerhighlight.lua +++ b/frontend/apps/reader/modules/readerhighlight.lua @@ -1015,13 +1015,10 @@ function ReaderHighlight:onTapSelectModeIcon() end function ReaderHighlight:onTap(_, ges) - -- We only actually need to clear if we have something to clear in the first place. - -- (We mainly want to avoid CRe's clearSelection, - -- which may incur a redraw as it invalidates the cache, c.f., #6854) - -- ReaderHighlight:clear can only return true if self.hold_pos was set anyway. - local cleared = self.hold_pos and self:clear() - -- We only care about potential taps on existing highlights, not on taps that closed a highlight menu. - if not cleared and ges and #self.view.highlight.visible_boxes > 0 then + if self.hold_pos then -- accidental tap while long-pressing + return self:onHoldRelease() + end + if ges and #self.view.highlight.visible_boxes > 0 then local pos = self.view:screenToPageTransform(ges.pos) local highlights_tapped = {} for _, box in ipairs(self.view.highlight.visible_boxes) do @@ -1487,7 +1484,6 @@ function ReaderHighlight:showHighlightDialog(index) end, } UIManager:show(edit_highlight_dialog) - return true end function ReaderHighlight:addToHighlightDialog(idx, fn_button) @@ -1526,7 +1522,11 @@ function ReaderHighlight:onShowHighlightMenu(index) anchor = function() return self:_getDialogAnchor(self.highlight_dialog, index) end, - tap_close_callback = function() self:handleEvent(Event:new("Tap")) end, + tap_close_callback = function() + if self.hold_pos then + self:clear() + end + end, } -- NOTE: Disable merging for this update, -- or the buggy Sage kernel may alpha-blend it into the page (with a bogus alpha value, to boot)... @@ -1678,15 +1678,13 @@ function ReaderHighlight:onHold(arg, ges) local image = self.ui.document:getImageFromPosition(self.hold_pos, true, true) if image then logger.dbg("hold on image") + self.hold_pos = nil local ImageViewer = require("ui/widget/imageviewer") - local imgviewer = ImageViewer:new{ + UIManager:show(ImageViewer:new{ image = image, - -- title_text = _("Document embedded image"), - -- No title, more room for image - with_title_bar = false, + with_title_bar = false, -- more room for image fullscreen = true, - } - UIManager:show(imgviewer) + }) self:onStopHighlightIndicator() return true end diff --git a/frontend/apps/reader/modules/readertoc.lua b/frontend/apps/reader/modules/readertoc.lua index 77bc104ec..5f6881ad3 100644 --- a/frontend/apps/reader/modules/readertoc.lua +++ b/frontend/apps/reader/modules/readertoc.lua @@ -1122,6 +1122,7 @@ See Style tweaks → Miscellaneous → Alternative ToC hints.]]) checked_func = function() return self.ui.document:isTocAlternativeToc() end, + check_callback_closes_menu = true, callback = function(touchmenu_instance) if self.ui.document:isTocAlternativeToc() then UIManager:show(ConfirmBox:new{ diff --git a/frontend/device/kindle/device.lua b/frontend/device/kindle/device.lua index 76e1a99ad..ac365a484 100644 --- a/frontend/device/kindle/device.lua +++ b/frontend/device/kindle/device.lua @@ -953,6 +953,7 @@ local KindleOasis = Kindle:extend{ hasKeys = yes, hasGSensor = yes, display_dpi = 300, + hasAuxBattery = yes, --[[ -- NOTE: Points to event3 on Wi-Fi devices, event4 on 3G devices... -- 3G devices apparently have an extra SX9500 Proximity/Capacitive controller for mysterious purposes... @@ -1351,9 +1352,11 @@ function KindleOasis:init() self.powerd = require("device/kindle/powerd"):new{ device = self, fl_intensity_file = "/sys/class/backlight/max77696-bl/brightness", - -- NOTE: Points to the embedded battery. The one in the cover is codenamed "soda". + -- NOTE: Points to the embedded battery. The one in the cover is codenamed "soda", see aux_batt_capacity_file below. batt_capacity_file = "/sys/devices/system/wario_battery/wario_battery0/battery_capacity", is_charging_file = "/sys/devices/system/wario_charger/wario_charger0/charging", + aux_batt_capacity_file = "/sys/devices/platform/soda/power_supply/soda_fg/capacity", + aux_batt_status_file = "/sys/devices/platform/soda/power_supply/soda_fg/status", hall_file = "/sys/devices/system/wario_hall/wario_hall0/hall_enable", } diff --git a/frontend/device/kindle/powerd.lua b/frontend/device/kindle/powerd.lua index c58a3a27a..7e27735df 100644 --- a/frontend/device/kindle/powerd.lua +++ b/frontend/device/kindle/powerd.lua @@ -24,6 +24,33 @@ function KindlePowerD:init() self.fl_max = self.fl_max + 1 end + if self.device:hasAuxBattery() then + self.getAuxCapacityHW = function(this) + return this:unchecked_read_int_file(self.aux_batt_capacity_file) + end + + self.isAuxBatteryConnectedHW = function(this) + local status = this:read_str_file(self.aux_batt_status_file) + if status == nil then + -- File could not be read, assume not connected + return false + end + -- File was read, assume aux battery is connected + return true + end + + self.isAuxChargingHW = function(this) + -- "Discharging" when discharging + -- "Full" when full + -- "Charging" when charging via DCP + return this:read_str_file(this.aux_batt_status_file) ~= "Discharging" + end + + self.isAuxChargedHW = function(this) + return this:read_str_file(this.aux_batt_status_file) == "Full" + end + end + self:initWakeupMgr() end diff --git a/frontend/dispatcher.lua b/frontend/dispatcher.lua index 760d09740..ab9ca5152 100644 --- a/frontend/dispatcher.lua +++ b/frontend/dispatcher.lua @@ -995,11 +995,10 @@ function Dispatcher:addSubMenu(caller, menu, location, settings) menu.ignored_by_menu_search = true -- all those would be duplicated table.insert(menu, { text = _("Nothing"), - keep_menu_open = true, - no_refresh_on_check = true, checked_func = function() return location[settings] ~= nil and Dispatcher:_itemsCount(location[settings]) == 0 end, + check_callback_updates_menu = true, callback = function(touchmenu_instance) local function do_remove() local actions = location[settings] diff --git a/frontend/document/document.lua b/frontend/document/document.lua index 811382d54..0afaf49dc 100644 --- a/frontend/document/document.lua +++ b/frontend/document/document.lua @@ -487,7 +487,7 @@ function Document:renderPage(pageno, rect, zoom, rotation, gamma, white_threshol -- 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 1ad28c765..d18f48645 100644 --- a/frontend/document/koptinterface.lua +++ b/frontend/document/koptinterface.lua @@ -299,7 +299,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) @@ -1435,7 +1435,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/elements/screen_rotation_menu_table.lua b/frontend/ui/elements/screen_rotation_menu_table.lua index e3b69ae46..678523729 100644 --- a/frontend/ui/elements/screen_rotation_menu_table.lua +++ b/frontend/ui/elements/screen_rotation_menu_table.lua @@ -14,6 +14,7 @@ local function genMenuItem(text, mode) return Screen:getRotationMode() == mode end, radio = true, + check_callback_closes_menu = true, callback = function(touchmenu_instance) UIManager:broadcastEvent(Event:new("SetRotationMode", mode)) touchmenu_instance:closeMenu() 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/htmlboxwidget.lua b/frontend/ui/widget/htmlboxwidget.lua index fb66077fd..6b08a01b3 100644 --- a/frontend/ui/widget/htmlboxwidget.lua +++ b/frontend/ui/widget/htmlboxwidget.lua @@ -264,6 +264,8 @@ function HtmlBoxWidget:setContent(body, css, default_font_size, is_xhtml, no_css self.document:layoutDocument(self.dimen.w, self.dimen.h, default_font_size) self.page_count = self.document:getPages() + self.page_boxes = nil + self:clearHighlight() end function HtmlBoxWidget:_render() 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/frontend/ui/widget/touchmenu.lua b/frontend/ui/widget/touchmenu.lua index ee20bad17..577e3bff4 100644 --- a/frontend/ui/widget/touchmenu.lua +++ b/frontend/ui/widget/touchmenu.lua @@ -50,6 +50,8 @@ local TouchMenuItem = InputContainer:extend{ dimen = nil, face = Font:getFace("smallinfofont"), show_parent = nil, + check_callback_updates_menu = nil, -- set to true for item with checkmark if its callback updates menu + check_callback_closes_menu = nil, -- set to true for item with checkmark if its callback closes menu } function TouchMenuItem:init() @@ -206,8 +208,7 @@ function TouchMenuItem:onTapSelect(arg, ges) -- Unhighlight -- self.item_frame.invert = false - -- NOTE: If the menu is going to be closed, we can safely drop that. - if self.item.keep_menu_open then + if self.item.keep_menu_open or self.item.check_callback_updates_menu then UIManager:widgetInvert(self.item_frame, highlight_dimen.x, highlight_dimen.y, highlight_dimen.w) UIManager:setDirty(nil, "ui", highlight_dimen) end @@ -470,7 +471,6 @@ local TouchMenu = FocusManager:extend{ fface = Font:getFace("ffont"), width = nil, height = nil, - page = 1, max_per_page_default = 10, -- for UIManager:setDirty show_parent = nil, @@ -480,6 +480,7 @@ local TouchMenu = FocusManager:extend{ } function TouchMenu:init() + self.screen_size = Screen:getSize() -- We won't include self.bordersize in our width calculations, so that -- borders are pushed off-(screen-)width and so not visible. -- We'll then be similar to bottom menu ConfigDialog (where this @@ -499,8 +500,8 @@ function TouchMenu:init() ges = "tap", range = Geom:new{ x = 0, y = 0, - w = Screen:getWidth(), - h = Screen:getHeight(), + w = self.screen_size.w, + h = self.screen_size.h, } } } @@ -623,7 +624,7 @@ function TouchMenu:init() -- This CenterContainer will make the left and right borders drawn -- off-screen self[1] = CenterContainer:new{ - dimen = Screen:getSize(), + dimen = self.screen_size, ignore = "height", self.menu_frame } @@ -642,51 +643,31 @@ function TouchMenu:init() HorizontalSpan:new{width = Size.span.horizontal_default}, } self.footer_top_margin = VerticalSpan:new{width = Size.span.vertical_default} + + local menu_height = self.height and math.min(self.height, self.screen_size.h) or self.screen_size.h + local items_height = menu_height - self.bar:getSize().h - self.footer_top_margin:getSize().h - self.footer:getSize().h + self.max_per_page = math.floor(items_height / (self.item_height + self.split_line:getSize().h)) + self.bar:switchToTab(self.last_index or 1) end -function TouchMenu:onCloseWidget() - -- NOTE: We don't pass a region in order to ensure a full-screen flash to avoid ghosting, - -- but we only need to do that if we actually have a FM or RD below us. - -- Don't do anything when we're switching between the two, or if we don't actually have a live instance of 'em... - local FileManager = require("apps/filemanager/filemanager") - local ReaderUI = require("apps/reader/readerui") - if (FileManager.instance and not FileManager.instance.tearing_down) - or (ReaderUI.instance and not ReaderUI.instance.tearing_down) then - UIManager:setDirty(nil, "flashui") - end -end - -function TouchMenu:_recalculatePageLayout() - local content_height -- content == item_list + footer - - local bar_height = self.bar:getSize().h - local footer_height = self.footer:getSize().h - if self.height then - content_height = self.height - bar_height - else - content_height = #self.item_table * self.item_height + footer_height - -- split line height - content_height = content_height + (#self.item_table - 1) - content_height = content_height + self.footer_top_margin:getSize().h - end - if content_height + bar_height > Screen:getHeight() then - content_height = Screen:getHeight() - bar_height - end - - local item_list_content_height = content_height - footer_height - self.perpage = math.floor(item_list_content_height / self.item_height) - local max_per_page = self.item_table.max_per_page or self.max_per_page_default - if self.perpage > max_per_page then - self.perpage = max_per_page - end - +function TouchMenu:updateItems(target_page, target_item_id) + if #self.item_table == 0 then return end + self.perpage = math.min(self.max_per_page, self.item_table.max_per_page or self.max_per_page_default) self.page_num = math.ceil(#self.item_table / self.perpage) -end + if target_item_id ~= nil then -- show menu page with target item + for i, v in ipairs(self.item_table) do + if v.menu_item_id == target_item_id then + target_page = math.floor( (i - 1) / self.perpage ) + 1 + break + end + end + end + self.page = target_page or self.page + if self.page > self.page_num then + self.page = self.page_num + end -function TouchMenu:updateItems() - local old_dimen = self.dimen and self.dimen:copy() - self:_recalculatePageLayout() self.item_group:clear() self.layout = {} table.insert(self.item_group, self.bar) @@ -707,14 +688,12 @@ function TouchMenu:updateItems() h = self.item_height, }, show_parent = self.show_parent, - item_visible_index = c, } table.insert(self.item_group, item_tmp) if item_tmp:isEnabled() then table.insert(self.layout, {[self.cur_tab] = item_tmp}) -- for the focusmanager end if item.separator and c ~= self.perpage and i ~= #self.item_table then - -- insert split line table.insert(self.item_group, self.split_line) end else @@ -742,7 +721,6 @@ function TouchMenu:updateItems() local batt_lvl = powerd:getCapacity() local batt_symbol = powerd:getBatterySymbol(powerd:isCharged(), powerd:isCharging(), batt_lvl) time_info_txt = BD.wrap(time_info_txt) .. " " .. BD.wrap("⌁") .. BD.wrap(batt_symbol) .. BD.wrap(batt_lvl .. "%") - if Device:hasAuxBattery() and powerd:isAuxBatteryConnected() then local aux_batt_lvl = powerd:getAuxCapacity() local aux_batt_symbol = powerd:getBatterySymbol(powerd:isAuxCharged(), powerd:isAuxCharging(), aux_batt_lvl) @@ -752,6 +730,7 @@ function TouchMenu:updateItems() self.time_info:setText(time_info_txt) -- recalculate dimen based on new layout + local old_dimen = self.dimen:copy() self.dimen.w = self.width self.dimen.h = self.item_group:getSize().h + self.bordersize*2 + self.padding -- (no padding at top) self:moveFocusTo(self.cur_tab, 1, FocusManager.NOT_FOCUS) -- reset the position of the focusmanager @@ -790,13 +769,11 @@ function TouchMenu:switchMenuTab(tab_num) -- It's like getting a new menu every time we switch tab! -- Also, switching to the _same_ tab resets the stack and takes us back to -- the top of the menu tree - self.page = 1 - -- clear item table stack self.item_table_stack = {} self.parent_id = nil self.cur_tab = tab_num self.item_table = self.tab_item_table[tab_num] - self:updateItems() + self:updateItems(1) end function TouchMenu:backToUpperMenu(no_close) @@ -807,68 +784,40 @@ function TouchMenu:backToUpperMenu(no_close) if self.item_table.needs_refresh and self.item_table.refresh_func then self.item_table = self.item_table.refresh_func() end - self.page = 1 - if self.parent_id then - self:_recalculatePageLayout() -- we need an accurate self.perpage - for i = 1, #self.item_table do - if self.item_table[i].menu_item_id == self.parent_id then - self.page = math.floor( (i - 1) / self.perpage ) + 1 - break - end - end - self.parent_id = nil - end - self:updateItems() + self:updateItems(1, self.parent_id) + self.parent_id = nil elseif not no_close then self:closeMenu() end end -function TouchMenu:closeMenu() - self.close_callback() +function TouchMenu:onBack() + self:backToUpperMenu() end function TouchMenu:onNextPage() - if self.page < self.page_num then - self.page = self.page + 1 - elseif self.page == self.page_num then - self.page = 1 - end - self:updateItems() - return true + return self:onGotoPage(self.page + 1) end function TouchMenu:onPrevPage() - if self.page > 1 then - self.page = self.page - 1 - elseif self.page == 1 then - self.page = self.page_num - end - self:updateItems() - return true + return self:onGotoPage(self.page - 1) end function TouchMenu:onFirstPage() - self.page = 1 - self:updateItems() - return true + return self:onGotoPage(1) end function TouchMenu:onLastPage() - self.page = self.page_num - self:updateItems() - return true + return self:onGotoPage(self.page_num) end function TouchMenu:onGotoPage(nb) - if nb > self.page_num then - self.page = self.page_num + if nb > self.page_num then -- cycle by swipes only + nb = 1 elseif nb < 1 then - self.page = 1 - else - self.page = nb + nb = self.page_num end - self:updateItems() + self:updateItems(nb) return true end @@ -924,7 +873,7 @@ function TouchMenu:onMenuSelect(item, tap_on_checkmark) -- must set keep_menu_open=true if that is wished) callback(self) if refresh then - if not item.no_refresh_on_check then + if not (item.check_callback_updates_menu or item.check_callback_closes_menu) then self:updateItems() end elseif not item.keep_menu_open then @@ -936,18 +885,7 @@ function TouchMenu:onMenuSelect(item, tap_on_checkmark) item.menu_item_id = item.menu_item_id or tostring(item) -- unique id self.parent_id = item.menu_item_id self.item_table = sub_item_table - self.page = 1 - if self.item_table.open_on_menu_item_id_func then - self:_recalculatePageLayout() -- we need an accurate self.perpage - local open_id = self.item_table.open_on_menu_item_id_func() - for i = 1, #self.item_table do - if self.item_table[i].menu_item_id == open_id then - self.page = math.floor( (i - 1) / self.perpage ) + 1 - break - end - end - end - self:updateItems() + self:updateItems(1, self.item_table.open_on_menu_item_id_func and self.item_table.open_on_menu_item_id_func()) end end return true @@ -999,6 +937,10 @@ function TouchMenu:onMenuHold(item, text_truncated) return true end +function TouchMenu:closeMenu() + self.close_callback() +end + function TouchMenu:onTapCloseAllMenus(arg, ges_ev) if ges_ev.pos:notIntersectWith(self.dimen) then self:closeMenu() @@ -1009,8 +951,16 @@ function TouchMenu:onClose() self:closeMenu() end -function TouchMenu:onBack() - self:backToUpperMenu() +function TouchMenu:onCloseWidget() + -- NOTE: We don't pass a region in order to ensure a full-screen flash to avoid ghosting, + -- but we only need to do that if we actually have a FM or RD below us. + -- Don't do anything when we're switching between the two, or if we don't actually have a live instance of 'em... + local FileManager = require("apps/filemanager/filemanager") + local ReaderUI = require("apps/reader/readerui") + if (FileManager.instance and not FileManager.instance.tearing_down) + or (ReaderUI.instance and not ReaderUI.instance.tearing_down) then + UIManager:setDirty(nil, "flashui") + end end -- Menu search feature @@ -1018,7 +968,7 @@ function TouchMenu:search(search_for) local found_menu_items = {} local MAX_MENU_DEPTH = 10 -- our menu max depth is currently 6 - local function recurse(item_table, path, text, icon, depth, is_disabled) + local function recurse(item_table, path, text, icon, depth) if item_table.ignored_by_menu_search then return end @@ -1030,8 +980,7 @@ function TouchMenu:search(search_for) if type(v) == "table" and not v.ignored_by_menu_search then local entry_text = v.text_func and v.text_func() or v.text local entry_displayed_text = entry_text - is_disabled = is_disabled or v.enabled == false or (v.enabled_func and v.enabled_func() == false) - if is_disabled then + if v.enabled == false or (v.enabled_func and v.enabled_func() == false) then entry_displayed_text = "\u{2592}\u{200A}" .. entry_displayed_text -- Medium Shade (▒) + Hair Space end local indent = "\u{2192}\u{200A}" -- Rightwards Arrow (→) + Hair Space @@ -1048,7 +997,7 @@ function TouchMenu:search(search_for) sub_item_table = v.sub_item_table_func() end if sub_item_table and not sub_item_table.ignored_by_menu_search then - recurse(sub_item_table, walk_path, walk_text, icon, depth, is_disabled) + recurse(sub_item_table, walk_path, walk_text, icon, depth) end end end @@ -1170,10 +1119,9 @@ function TouchMenu:openMenu(path, with_animation) end end elseif step == STEPS.MENU_ITEM_HIGHLIGHT then - local item_visible_index = (item_nb - 1) % self.perpage + 1 local item_widget - for i, w in ipairs(self.item_group) do - if w.item_visible_index == item_visible_index then + for _, w in ipairs(self.item_group) do + if w.item and w.item.idx == item_nb then item_widget = w break end @@ -1311,8 +1259,8 @@ function TouchMenu:onShowMenuSearch() title = _("Search results"), subtitle = T(_("Query: %1"), search_string), item_table = get_current_search_results(), - width = math.floor(Screen:getWidth() * 0.9), - height = math.floor(Screen:getHeight() * 0.9), + width = math.floor(self.screen_size.w * 0.9), + height = math.floor(self.screen_size.h * 0.9), single_line = true, items_per_page = 10, items_font_size = Menu.getItemFontSize(10), @@ -1329,7 +1277,7 @@ function TouchMenu:onShowMenuSearch() -- build container self.results_menu_container = CenterContainer:new{ - dimen = Screen:getSize(), + dimen = self.screen_size, results_menu, } diff --git a/frontend/util.lua b/frontend/util.lua index 7e22edddf..65e306009 100644 --- a/frontend/util.lua +++ b/frontend/util.lua @@ -948,14 +948,17 @@ end --- Replaces characters that are invalid filenames. -- --- Replaces the characters \/:*?"<>| with an _. +-- Replaces the characters \/:*?"<>| with an _ +-- and removes trailing dots and spaces, in line with . -- These characters are problematic on Windows filesystems. On Linux only -- / poses a problem. ---- @string str filename ---- @treturn string sanitized filename local function replaceAllInvalidChars(str) if str then - return str:gsub('[\\/:*?"<>|]', '_') + str = str:gsub('[\\/:*?"<>|]', '_') + str = str:gsub("[.%s]+$", "") + return str end end diff --git a/kodev b/kodev index d492c1f0a..bf6856fcb 100755 --- a/kodev +++ b/kodev @@ -475,12 +475,18 @@ ANDROID TARGET: if [[ -n "${VALUE}" ]]; then declare -a "wrap=(${VALUE})" else - # Try to use friendly defaults for GDB: - # - DDD is a slightly less nice GUI - # - cgdb is a nice curses-based GDB front - # - GDB standard CLI has a fallback - if ! wrap=("$(command -v ddd cgdb gdb | head -n1)"); then - die 1 "Couldn't find GDB." + if is_system macOS; then + if ! wrap=("$(command -v lldb | head -n1)"); then + die 1 "Couldn't find LLDB." + fi + else + # Try to use friendly defaults for GDB: + # - DDD is a slightly less nice GUI + # - cgdb is a nice curses-based GDB front + # - GDB standard CLI has a fallback + if ! wrap=("$(command -v ddd cgdb gdb | head -n1)"); then + die 1 "Couldn't find GDB." + fi fi fi if [[ ${#wrap[@]} -eq 1 ]]; then diff --git a/plugins/SSH.koplugin/main.lua b/plugins/SSH.koplugin/main.lua index 26480298f..4d554eb19 100644 --- a/plugins/SSH.koplugin/main.lua +++ b/plugins/SSH.koplugin/main.lua @@ -170,8 +170,8 @@ function SSH:addToMainMenu(menu_items) sub_item_table = { { text = _("SSH server"), - keep_menu_open = true, checked_func = function() return self:isRunning() end, + check_callback_updates_menu = true, callback = function(touchmenu_instance) self:onToggleSSHServer() -- sleeping might not be needed, but it gives the feeling diff --git a/plugins/autoturn.koplugin/main.lua b/plugins/autoturn.koplugin/main.lua index 4801aab33..10a96bf7f 100644 --- a/plugins/autoturn.koplugin/main.lua +++ b/plugins/autoturn.koplugin/main.lua @@ -131,6 +131,7 @@ function AutoTurn:addToMainMenu(menu_items) return self:_enabled() and T(_("Autoturn: %1"), time_string) or _("Autoturn") end, checked_func = function() return self:_enabled() end, + check_callback_updates_menu = true, callback = function(menu) local DateTimeWidget = require("ui/widget/datetimewidget") local autoturn_seconds = G_reader_settings:readSetting("autoturn_timeout_seconds", 30) diff --git a/plugins/autowarmth.koplugin/main.lua b/plugins/autowarmth.koplugin/main.lua index 4fa87fdb7..0a9534ff0 100644 --- a/plugins/autowarmth.koplugin/main.lua +++ b/plugins/autowarmth.koplugin/main.lua @@ -608,6 +608,7 @@ function AutoWarmth:getSubMenuItems() return not self.easy_mode end, help_text = _("In the expert mode, different types of twilight can be used in addition to civil twilight."), + check_callback_updates_menu = true, callback = function(touchmenu_instance) self.easy_mode = not self.easy_mode G_reader_settings:saveSetting("autowarmth_easy_mode", self.easy_mode) @@ -662,6 +663,7 @@ function AutoWarmth:getFlOffDuringDayMenu() return _("Frontlight off during day") end end, + check_callback_updates_menu = true, callback = function(touchmenu_instance) if self.easy_mode then self.fl_off_during_day = not self.fl_off_during_day @@ -889,6 +891,7 @@ function AutoWarmth:getScheduleMenu() checked_func = function() return self.scheduler_times[num] ~= nil end, + check_callback_updates_menu = true, callback = function(touchmenu_instance) local hh = 12 local mm = 0 @@ -1112,6 +1115,7 @@ function AutoWarmth:getWarmthMenu() }) end end, + check_callback_updates_menu = true, callback = function(touchmenu_instance) if Device:hasNaturalLight() then if self.control_warmth and self.control_nightmode then diff --git a/plugins/calibre.koplugin/main.lua b/plugins/calibre.koplugin/main.lua index 25977fe9e..66995d771 100644 --- a/plugins/calibre.koplugin/main.lua +++ b/plugins/calibre.koplugin/main.lua @@ -67,7 +67,7 @@ function Calibre:onDispatcherRegisterActions() Dispatcher:registerAction("calibre_browse_authors", { category="none", event="CalibreBrowseBy", arg="authors", title=_("Browse all calibre authors"), general=true,}) Dispatcher:registerAction("calibre_browse_titles", { category="none", event="CalibreBrowseBy", arg="title", title=_("Browse all calibre titles"), general=true, separator=true,}) Dispatcher:registerAction("calibre_start_connection", { category="none", event="StartWirelessConnection", title=_("Calibre wireless connect"), general=true,}) - Dispatcher:registerAction("calibre_close_connection", { category="none", event="CloseWirelessConnection", title=_("Calibre wireless disconnect"), general=true,}) + Dispatcher:registerAction("calibre_close_connection", { category="none", event="CloseWirelessConnection", title=_("Calibre wireless disconnect"), general=true, separator=true,}) end function Calibre:init() @@ -303,6 +303,7 @@ function Calibre:getWirelessMenuTable() checked_func = function() return G_reader_settings:has("calibre_wireless_url") end, + check_callback_updates_menu = true, callback = function(touchmenu_instance) local MultiInputDialog = require("ui/widget/multiinputdialog") local url_dialog diff --git a/plugins/coverimage.koplugin/main.lua b/plugins/coverimage.koplugin/main.lua index f77407f07..8709fc219 100644 --- a/plugins/coverimage.koplugin/main.lua +++ b/plugins/coverimage.koplugin/main.lua @@ -502,6 +502,7 @@ function CoverImage:menuEntryCache() checked_func = function() return self.cover_image_cache_maxfiles >= 0 end, + check_callback_updates_menu = true, callback = function(touchmenu_instance) self:sizeSpinner(touchmenu_instance, "cover_image_cache_maxfiles", _("Number of covers"), -1, 100, 36, self.cleanCache) end, @@ -522,6 +523,7 @@ function CoverImage:menuEntryCache() checked_func = function() return self.cover_image_cache_maxsize >= 0 end, + check_callback_updates_menu = true, callback = function(touchmenu_instance) self:sizeSpinner(touchmenu_instance, "cover_image_cache_maxsize", _("Cache size"), -1, 100, 5, self.cleanCache, C_("Data storage size", "MB")) end, @@ -574,6 +576,7 @@ function CoverImage:menuEntrySetPath(key, title, help, info, default, folder_onl checked_func = function() return isFileOk(self[key]) or (isPathAllowed(self[key]) and folder_only) end, + check_callback_updates_menu = true, callback = function(touchmenu_instance) UIManager:show(ConfirmBox:new{ text = info, diff --git a/plugins/gestures.koplugin/main.lua b/plugins/gestures.koplugin/main.lua index 199b36b2c..5886559e8 100644 --- a/plugins/gestures.koplugin/main.lua +++ b/plugins/gestures.koplugin/main.lua @@ -295,11 +295,10 @@ function Gestures:genMenu(ges) if gestures_list[ges] ~= nil then table.insert(sub_items, { text = T(_("%1 (default)"), Dispatcher:menuTextFunc(self.defaults[ges])), - keep_menu_open = true, - no_refresh_on_check = true, checked_func = function() return util.tableEquals(self.gestures[ges], self.defaults[ges]) end, + check_callback_updates_menu = true, callback = function(touchmenu_instance) local function do_remove() self.gestures[ges] = util.tableDeepCopy(self.defaults[ges]) @@ -313,11 +312,10 @@ function Gestures:genMenu(ges) end table.insert(sub_items, { text = _("Pass through"), - keep_menu_open = true, - no_refresh_on_check = true, checked_func = function() return self.gestures[ges] == nil end, + check_callback_updates_menu = true, callback = function(touchmenu_instance) local function do_remove() self.gestures[ges] = nil diff --git a/plugins/hotkeys.koplugin/main.lua b/plugins/hotkeys.koplugin/main.lua index cbd84b620..4ea748f3e 100644 --- a/plugins/hotkeys.koplugin/main.lua +++ b/plugins/hotkeys.koplugin/main.lua @@ -192,11 +192,10 @@ function HotKeys:genMenu(hotkey) local default_text = default_action and Dispatcher:menuTextFunc(default_action) or _("No action") table.insert(sub_items, { text = T(_("%1 (default)"), default_text), - keep_menu_open = true, - no_refresh_on_check = true, checked_func = function() return util.tableEquals(self.hotkeys[hotkey], self.defaults[hotkey]) end, + check_callback_updates_menu = true, callback = function(touchmenu_instance) local function do_remove() self.hotkeys[hotkey] = util.tableDeepCopy(self.defaults[hotkey]) @@ -209,12 +208,10 @@ function HotKeys:genMenu(hotkey) end table.insert(sub_items, { text = _("No action"), - keep_menu_open = true, - no_refresh_on_check = true, - separator = true, checked_func = function() return self.hotkeys[hotkey] == nil or next(self.hotkeys[hotkey]) == nil end, + check_callback_updates_menu = true, callback = function(touchmenu_instance) local function do_remove() self.hotkeys[hotkey] = nil @@ -223,6 +220,7 @@ function HotKeys:genMenu(hotkey) end Dispatcher.removeActions(self.hotkeys[hotkey], do_remove) end, + separator = true, }) Dispatcher:addSubMenu(self, sub_items, self.hotkeys, hotkey) -- Since we are already handling potential conflicts via overrideConflictingKeyEvents(), both "No action" and "Nothing", diff --git a/plugins/kosync.koplugin/main.lua b/plugins/kosync.koplugin/main.lua index cb90551b0..7c66e3a3c 100644 --- a/plugins/kosync.koplugin/main.lua +++ b/plugins/kosync.koplugin/main.lua @@ -6,6 +6,7 @@ local InfoMessage = require("ui/widget/infomessage") local Math = require("optmath") local MultiInputDialog = require("ui/widget/multiinputdialog") local NetworkMgr = require("ui/network/manager") +local Notification = require("ui/widget/notification") local UIManager = require("ui/uimanager") local WidgetContainer = require("ui/widget/container/widgetcontainer") local logger = require("logger") @@ -164,6 +165,10 @@ local function validateUser(user, pass) end function KOSync:onDispatcherRegisterActions() + Dispatcher:registerAction("kosync_set_autosync", + { category="string", event="KOSyncToggleAutoSync", title=_("Set auto progress sync"), reader=true, + args={true, false}, toggle={_("on"), _("off")},}) + Dispatcher:registerAction("kosync_toggle_autosync", { category="none", event="KOSyncToggleAutoSync", title=_("Toggle auto progress sync"), reader=true,}) Dispatcher:registerAction("kosync_push_progress", { category="none", event="KOSyncPushProgress", title=_("Push progress from this device"), reader=true,}) Dispatcher:registerAction("kosync_pull_progress", { category="none", event="KOSyncPullProgress", title=_("Pull progress from other devices"), reader=true, separator=true,}) end @@ -225,23 +230,7 @@ function KOSync:addToMainMenu(menu_items) checked_func = function() return self.settings.auto_sync end, help_text = _([[This may lead to nagging about toggling WiFi on document close and suspend/resume, depending on the device's connectivity.]]), callback = function() - -- Actively recommend switching the before wifi action to "turn_on" instead of prompt, as prompt will just not be practical (or even plain usable) here. - if Device:hasSeamlessWifiToggle() and G_reader_settings:readSetting("wifi_enable_action") ~= "turn_on" and not self.settings.auto_sync then - UIManager:show(InfoMessage:new{ text = _("You will have to switch the 'Action when Wi-Fi is off' Network setting to 'turn on' to be able to enable this feature!") }) - return - end - - self.settings.auto_sync = not self.settings.auto_sync - self:registerEvents() - if self.settings.auto_sync then - -- Since we will update the progress when closing the document, - -- pull the current progress now so as not to silently overwrite it. - self:getProgress(true, true) - else - -- Since we won't update the progress when closing the document, - -- push the current progress now so as not to lose it. - self:updateProgress(true, true) - end + self:onKOSyncToggleAutoSync(nil, true) end, }, { @@ -919,6 +908,37 @@ function KOSync:onKOSyncPullProgress() self:getProgress(true, true) end +function KOSync:onKOSyncToggleAutoSync(toggle, from_menu) + if toggle == self.settings.auto_sync then + return true + end + -- Actively recommend switching the before wifi action to "turn_on" instead of prompt, + -- as prompt will just not be practical (or even plain usable) here. + if not self.settings.auto_sync + and Device:hasSeamlessWifiToggle() + and G_reader_settings:readSetting("wifi_enable_action") ~= "turn_on" then + UIManager:show(InfoMessage:new{ text = _("You will have to switch the 'Action when Wi-Fi is off' Network setting to 'turn on' to be able to enable this feature!") }) + return true + end + self.settings.auto_sync = not self.settings.auto_sync + self:registerEvents() + + if self.settings.auto_sync then + -- Since we will update the progress when closing the document, + -- pull the current progress now so as not to silently overwrite it. + self:getProgress(true, true) + elseif from_menu then + -- Since we won't update the progress when closing the document, + -- push the current progress now so as not to lose it. + self:updateProgress(true, true) + end + + if not from_menu then + Notification:notify(self.settings.auto_sync and _("Auto progress sync: on") or _("Auto progress sync: off")) + end + return true +end + function KOSync:registerEvents() if self.settings.auto_sync then self.onCloseDocument = self._onCloseDocument diff --git a/plugins/opds.koplugin/main.lua b/plugins/opds.koplugin/main.lua index feaab388d..f45f4b7db 100644 --- a/plugins/opds.koplugin/main.lua +++ b/plugins/opds.koplugin/main.lua @@ -45,12 +45,14 @@ local OPDS = WidgetContainer:extend{ } function OPDS:init() - self.settings = LuaSettings:open(self.opds_settings_file) - if next(self.settings.data) == nil then + self.opds_settings = LuaSettings:open(self.opds_settings_file) + if next(self.opds_settings.data) == nil then self.updated = true -- first run, force flush end - self.servers = self.settings:readSetting("servers", self.default_servers) - self.downloads = self.settings:readSetting("downloads", {}) + self.servers = self.opds_settings:readSetting("servers", self.default_servers) + self.downloads = self.opds_settings:readSetting("downloads", {}) + self.settings = self.opds_settings:readSetting("settings", {}) + self.pending_syncs = self.opds_settings:readSetting("pending_syncs", {}) self:onDispatcherRegisterActions() self.ui.menu:registerToMainMenu(self) end @@ -76,6 +78,8 @@ function OPDS:onShowOPDSCatalog() self.opds_browser = OPDSBrowser:new{ servers = self.servers, downloads = self.downloads, + settings = self.settings, + pending_syncs = self.pending_syncs, title = _("OPDS catalog"), is_popout = false, is_borderless = true, @@ -121,7 +125,7 @@ end function OPDS:onFlushSettings() if self.updated then - self.settings:flush() + self.opds_settings:flush() self.updated = nil end end diff --git a/plugins/opds.koplugin/opdsbrowser.lua b/plugins/opds.koplugin/opdsbrowser.lua index a307c6091..d5b4c348b 100644 --- a/plugins/opds.koplugin/opdsbrowser.lua +++ b/plugins/opds.koplugin/opdsbrowser.lua @@ -3,6 +3,7 @@ local ButtonDialog = require("ui/widget/buttondialog") local Cache = require("cache") local CheckButton = require("ui/widget/checkbutton") local ConfirmBox = require("ui/widget/confirmbox") +local Device = require("device") local DocumentRegistry = require("document/documentregistry") local InfoMessage = require("ui/widget/infomessage") local InputDialog = require("ui/widget/inputdialog") @@ -12,6 +13,9 @@ local NetworkMgr = require("ui/network/manager") local Notification = require("ui/widget/notification") local OPDSParser = require("opdsparser") local OPDSPSE = require("opdspse") +local SpinWidget = require("ui/widget/spinwidget") +local TextViewer = require("ui/widget/textviewer") +local Trapper = require("ui/trapper") local UIManager = require("ui/uimanager") local http = require("socket.http") local lfs = require("libs/libkoreader-lfs") @@ -59,22 +63,96 @@ local OPDSBrowser = Menu:extend{ function OPDSBrowser:init() self.item_table = self:genItemTableFromRoot() self.catalog_title = nil - self.title_bar_left_icon = "plus" + self.title_bar_left_icon = "appbar.menu" self.onLeftButtonTap = function() - self:addEditCatalog() + self:showOPDSMenu() end Menu.init(self) -- call parent's init() end +function OPDSBrowser:showOPDSMenu() + local dialog + dialog = ButtonDialog:new{ + buttons = { + {{ + text = _("Add catalog"), + callback = function() + UIManager:close(dialog) + self:addEditCatalog() + end, + align = "left", + }}, + {}, + {{ + text = _("Sync all catalogs"), + callback = function() + UIManager:close(dialog) + NetworkMgr:runWhenConnected(function() + self.sync_force = false + self:checkSyncDownload() + end) + end, + align = "left", + }}, + {{ + text = _("Force sync all catalogs"), + callback = function() + UIManager:close(dialog) + NetworkMgr:runWhenConnected(function() + self.sync_force = true + self:checkSyncDownload() + end) + end, + align = "left", + }}, + {{ + text = _("Set max number of files to sync"), + callback = function() + self:setMaxSyncDownload() + end, + align = "left", + }}, + {{ + text = _("Set sync folder"), + callback = function() + self:setSyncDir() + end, + align = "left", + }}, + {{ + text = _("Set file types to sync"), + callback = function() + self:setSyncFiletypes() + end, + align = "left", + }}, + }, + 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 = "" + if server.username then + icons = "\u{f2c0}" + end + if server.sync then + icons = "\u{f46a} " .. icons + end return { text = server.title, - mandatory = server.username and "\u{f2c0}", + mandatory = icons, url = server.url, username = server.username, password = server.password, raw_names = server.raw_names, -- use server raw filenames for download searchable = server.url and server.url:match("%%s") and true or false, + sync = server.sync, } end @@ -120,7 +198,7 @@ function OPDSBrowser:addEditCatalog(item) title = _("Add OPDS catalog") end - local dialog, check_button_raw_names + local dialog, check_button_raw_names, check_button_sync_catalog dialog = MultiInputDialog:new{ title = title, fields = fields, @@ -138,6 +216,7 @@ function OPDSBrowser:addEditCatalog(item) callback = function() local new_fields = dialog:getFields() new_fields[5] = check_button_raw_names.checked or nil + new_fields[6] = check_button_sync_catalog.checked or nil self:editCatalogFromInput(new_fields, item) UIManager:close(dialog) end, @@ -150,7 +229,13 @@ function OPDSBrowser:addEditCatalog(item) checked = item and item.raw_names, parent = dialog, } + check_button_sync_catalog = CheckButton:new{ + text = _("Sync catalog"), + checked = item and item.sync, + parent = dialog, + } dialog:addWidget(check_button_raw_names) + dialog:addWidget(check_button_sync_catalog) UIManager:show(dialog) dialog:onShowKeyboard() end @@ -198,6 +283,7 @@ function OPDSBrowser:editCatalogFromInput(fields, item, no_refresh) username = fields[3] ~= "" and fields[3] or nil, password = fields[4] ~= "" and fields[4] or nil, raw_names = fields[5], + sync = fields[6], } local new_item = buildRootEntry(new_server) local new_idx, itemnumber @@ -208,7 +294,7 @@ function OPDSBrowser:editCatalogFromInput(fields, item, no_refresh) new_idx = #self.servers + 2 itemnumber = new_idx end - self.servers[new_idx - 1] = new_server + self.servers[new_idx - 1] = new_server -- first item is "Downloads" self.item_table[new_idx] = new_item if not no_refresh then self:switchItemTable(nil, self.item_table, itemnumber) @@ -366,25 +452,27 @@ function OPDSBrowser:genItemTableFromCatalog(catalog, item_url) hrefs[link.rel] = build_href(link.href) end end - -- 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, - }) - has_opensearch = true + if not self.sync then + -- 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, + }) + has_opensearch = true + end 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, - }) + -- 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, + }) + end end end end @@ -460,13 +548,16 @@ function OPDSBrowser:genItemTableFromCatalog(catalog, item_url) -- Check for the presence of the pdf suffix and add it -- if it's missing. local href = link.href - if util.getFileNameSuffix(href) ~= "pdf" then - href = href .. ".pdf" + -- Calibre web OPDS download links end with "//" + if not util.stringEndsWith(href, "/pdf/") then + if util.getFileNameSuffix(href) ~= "pdf" then + href = href .. ".pdf" + end + table.insert(item.acquisitions, { + type = link.title, + href = build_href(href), + }) end - table.insert(item.acquisitions, { - type = link.title, - href = build_href(href), - }) end end end @@ -514,6 +605,7 @@ 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) end @@ -577,14 +669,7 @@ end -- Shows dialog to download / stream a book function OPDSBrowser:showDownloads(item) local acquisitions = item.acquisitions - local filename = item.title - if item.author then - filename = item.author .. " - " .. filename - end - local filename_orig = filename - if self.root_catalog_raw_names then - filename = nil - end + local filename, filename_orig = self:getFileName(item) local function createTitle(path, file) -- title for ButtonDialog return T(_("Download folder:\n%1\n\nDownload filename:\n%2\n\nDownload file type:"), @@ -635,14 +720,7 @@ function OPDSBrowser:showDownloads(item) enabled = false, }) else - local filetype = util.getFileNameSuffix(acquisition.href) - logger.dbg("Filetype for download is", filetype) - if not DocumentRegistry:hasProvider("dummy." .. filetype) then - filetype = nil - end - if not filetype and DocumentRegistry:hasProvider(nil, acquisition.type) then - filetype = DocumentRegistry:mimeToExt(acquisition.type) - end + local filetype = self.getFiletype(acquisition) if filetype then -- supported file type local text = url.unescape(acquisition.title or string.upper(filetype)) table.insert(download_buttons, { @@ -663,7 +741,7 @@ function OPDSBrowser:showDownloads(item) password = self.root_catalog_password, }) self._manager.updated = true - Notification:notify(_("Book added to download list")) + Notification:notify(_("Book added to download list"), Notification.SOURCE_OTHER) end, }) end @@ -700,7 +778,7 @@ function OPDSBrowser:showDownloads(item) G_reader_settings:saveSetting("download_dir", path) self.download_dialog:setTitle(createTitle(path, filename)) end, - }:chooseDir(self.getCurrentDownloadDir()) + }:chooseDir(self:getCurrentDownloadDir()) end, }, { @@ -729,7 +807,7 @@ function OPDSBrowser:showDownloads(item) filename = filename_orig end UIManager:close(dialog) - self.download_dialog:setTitle(createTitle(self.getCurrentDownloadDir(), filename)) + self.download_dialog:setTitle(createTitle(self:getCurrentDownloadDir(), filename)) end, }, } @@ -753,7 +831,6 @@ function OPDSBrowser:showDownloads(item) text = _("Book information"), enabled = type(item.content) == "string", callback = function() - local TextViewer = require("ui/widget/textviewer") UIManager:show(TextViewer:new{ title = item.text, title_multilines = true, @@ -765,19 +842,35 @@ function OPDSBrowser:showDownloads(item) }) self.download_dialog = ButtonDialog:new{ - title = createTitle(self.getCurrentDownloadDir(), filename), + title = createTitle(self:getCurrentDownloadDir(), filename), buttons = buttons, } UIManager:show(self.download_dialog) end +-- Helper function to get the filetype from an acquisitions table +function OPDSBrowser.getFiletype(link) + local filetype = util.getFileNameSuffix(link.href) + if not DocumentRegistry:hasProvider("dummy." .. filetype) then + filetype = nil + end + if not filetype and DocumentRegistry:hasProvider(nil, link.type) then + filetype = DocumentRegistry:mimeToExt(link.type) + end + return filetype +end + -- Returns user selected or last opened folder -function OPDSBrowser.getCurrentDownloadDir() - return G_reader_settings:readSetting("download_dir") or G_reader_settings:readSetting("lastdir") +function OPDSBrowser:getCurrentDownloadDir() + if self.sync then + return self.settings.sync_dir + else + return G_reader_settings:readSetting("download_dir") or G_reader_settings:readSetting("lastdir") + end end function OPDSBrowser:getLocalDownloadPath(filename, filetype, remote_url) - local download_dir = OPDSBrowser.getCurrentDownloadDir() + local download_dir = self:getCurrentDownloadDir() filename = filename and filename .. "." .. filetype:lower() or self:getServerFileName(remote_url) filename = util.getSafeFilename(filename, download_dir) filename = (download_dir ~= "/" and download_dir or "") .. '/' .. filename @@ -895,6 +988,29 @@ function OPDSBrowser:onMenuHold(item) title = item.text, title_align = "center", buttons = { + { + { + text = _("Force sync"), + callback = function() + UIManager:close(dialog) + NetworkMgr:runWhenConnected(function() + self.sync_force = true + self:checkSyncDownload(item.idx) + end) + end, + }, + { + text = _("Sync"), + callback = function() + UIManager:close(dialog) + NetworkMgr:runWhenConnected(function() + self.sync_force = false + self:checkSyncDownload(item.idx) + end) + end, + }, + }, + {}, { { text = _("Delete"), @@ -974,6 +1090,8 @@ function OPDSBrowser:showDownloadList() title_bar_fm_style = true, onMenuSelect = self.showDownloadListItemDialog, _manager = self, + title_bar_left_icon = "appbar.menu", + onLeftButtonTap = self.showDownloadListMenu } self.download_list.close_callback = function() UIManager:close(self.download_list) @@ -988,6 +1106,35 @@ function OPDSBrowser:showDownloadList() UIManager:show(self.download_list) end +function OPDSBrowser:showDownloadListMenu() + local dialog + dialog = ButtonDialog:new{ + buttons = { + {{ + text = _("Download all"), + callback = function() + UIManager:close(dialog) + self._manager:confirmDownloadDownloadList() + end, + align = "left", + }}, + {{ + text = _("Remove all"), + callback = function() + UIManager:close(dialog) + self._manager:confirmClearDownloadList() + end, + align = "left", + }}, + }, + shrink_unneeded_width = true, + anchor = function() + return self.title_bar.left_button.image.dimen + end, + } + UIManager:show(dialog) +end + function OPDSBrowser:updateDownloadListItemTable(item_table) if item_table == nil then item_table = {} @@ -1002,6 +1149,35 @@ function OPDSBrowser:updateDownloadListItemTable(item_table) self.download_list:switchItemTable(title, item_table) end +function OPDSBrowser:confirmDownloadDownloadList() + UIManager:show(ConfirmBox:new{ + text = _("Download all books?\nExisting files will be overwritten."), + ok_text = _("Download"), + ok_callback = function() + NetworkMgr:runWhenConnected(function() + Trapper:wrap(function() + self:downloadDownloadList() + end) + end) + end, + }) +end + +function OPDSBrowser:confirmClearDownloadList() + UIManager:show(ConfirmBox:new{ + text = _("Remove all downloads?"), + ok_text = _("Remove"), + ok_callback = function() + for i in ipairs(self.downloads) do + self.downloads[i] = nil + end + self.download_list_updated = true + self._manager.updated = true + self.download_list:close_callback() + end, + }) +end + function OPDSBrowser:showDownloadListItemDialog(item) local dl_item = self._manager.downloads[item.idx] local textviewer @@ -1040,36 +1216,14 @@ function OPDSBrowser:showDownloadListItemDialog(item) text = _("Remove all"), callback = function() textviewer:onClose() - UIManager:show(ConfirmBox:new{ - text = _("Remove all downloads?"), - ok_text = _("Remove"), - ok_callback = function() - for i in ipairs(self._manager.downloads) do - self._manager.downloads[i] = nil - end - self._manager.download_list_updated = true - self._manager._manager.updated = true - self:close_callback() - end, - }) + self._manager:confirmClearDownloadList() end, }, { text = _("Download all"), callback = function() textviewer:onClose() - UIManager:show(ConfirmBox:new{ - text = _("Download all books?\nExisting files will be overwritten."), - ok_text = _("Download"), - ok_callback = function() - NetworkMgr:runWhenConnected(function() - local Trapper = require("ui/trapper") - Trapper:wrap(function() - self._manager:downloadDownloadList() - end) - end) - end, - }) + self._manager:confirmDownloadDownloadList() end, }, }, @@ -1084,7 +1238,6 @@ function OPDSBrowser:showDownloadListItemDialog(item) TextBoxWidget.PTF_BOLD_START, _("Description"), TextBoxWidget.PTF_BOLD_END, "\n", dl_item.info, }) - local TextViewer = require("ui/widget/textviewer") textviewer = TextViewer:new{ title = dl_item.catalog, text = text, @@ -1095,11 +1248,11 @@ function OPDSBrowser:showDownloadListItemDialog(item) return true end +-- Download whole download list function OPDSBrowser:downloadDownloadList() local info = InfoMessage:new{ text = _("Downloading… (tap to cancel)") } UIManager:show(info) UIManager:forceRePaint() - local Trapper = require("ui/trapper") local completed, downloaded = Trapper:dismissableRunInSubprocess(function() local dl = {} for _, item in ipairs(self.downloads) do @@ -1137,4 +1290,383 @@ function OPDSBrowser:downloadDownloadList() end end +function OPDSBrowser:setMaxSyncDownload() + local current_max_dl = self.settings.sync_max_dl or 50 + local spin = SpinWidget:new{ + title_text = "Set maximum sync size", + info_text = "Set the max number of books to download at a time", + value = current_max_dl, + value_min = 0, + value_max = 1000, + value_step = 10, + value_hold_step = 50, + default_value = 50, + wrap = true, + ok_text = "Save", + callback = function(spin) + self.settings.sync_max_dl = spin.value + self._manager.updated = true + end, + } + UIManager:show(spin) +end + +function OPDSBrowser:setSyncDir() + local force_chooser_dir + if Device:isAndroid() then + force_chooser_dir = Device.home_dir + end + + require("ui/downloadmgr"):new{ + onConfirm = function(inbox) + logger.info("set opds sync folder", inbox) + self.settings.sync_dir = inbox + self._manager.updated = true + end, + }:chooseDir(force_chooser_dir) +end + +-- Set string for desired filetypes +function OPDSBrowser:setSyncFiletypes(filetype_list) + local input = self.settings.filetypes + local dialog + dialog = InputDialog:new{ + title = _("File types to sync"), + description = _("A comma separated list of desired filetypes"), + input_hint = _("epub, mobi"), + input = input, + buttons = { + { + { + text = _("Cancel"), + id = "close", + callback = function() + UIManager:close(dialog) + end, + }, + { + text = _("Save"), + is_enter_default = true, + callback = function() + local str = dialog:getInputText() + self.settings.filetypes = str ~= "" and str or nil + self._manager.updated = true + UIManager:close(dialog) + end, + }, + }, + }, + } + UIManager:show(dialog) + dialog:onShowKeyboard() +end + +-- Helper function to get filename and set nil if using raw names +function OPDSBrowser:getFileName(item) + local filename = item.title + if item.author then + filename = item.author .. " - " .. filename + end + local filename_orig = filename + if self.root_catalog_raw_names then + filename = nil + end + return filename, filename_orig +end + +function OPDSBrowser:updateFieldInCatalog(item, name, value) + item[name] = value + self._manager.updated = true +end + +function OPDSBrowser:checkSyncDownload(idx) + if self.settings.sync_dir then + self.sync = true + local info = InfoMessage:new{ + text = _("Synchronizing lists…"), + } + UIManager:show(info) + UIManager:forceRePaint() + if idx then + self:fillPendingSyncs(self.servers[idx-1]) -- First item is "Downloads" + else + for _, item in ipairs(self.servers) do + if item.sync then + self:fillPendingSyncs(item) + end + end + end + UIManager:close(info) + if #self.pending_syncs > 0 then + Trapper:wrap(function() + self:downloadPendingSyncs() + end) + else + UIManager:show(InfoMessage:new{ + text = _("Up to date!"), + }) + end + self.sync = false + else + UIManager:show(InfoMessage:new{ + text = _("Please choose a folder for sync downloads first"), + }) + end +end + +-- Add entries to self.pending_syncs +function OPDSBrowser:fillPendingSyncs(server) + self.root_catalog_password = server.password + self.root_catalog_raw_names = server.raw_names + self.root_catalog_username = server.username + self.root_catalog_title = server.title + self.sync_server = server + self.sync_server_list = self.sync_server_list or {} + self.sync_max_dl = self.settings.sync_max_dl or 50 + + local file_list + local file_str = self.settings.filetypes + local new_last_download = nil + local dl_count = 1 + if file_str then + file_list = {} + for filetype in util.gsplit(file_str, ",") do + file_list[util.trim(filetype)] = true + end + end + local sync_list = self:getSyncDownloadList() + if sync_list then + for i, entry in ipairs(sync_list) do + -- for project gutenberg + local sub_table = {} + local item + if entry.url then + sub_table = self:getSyncDownloadList(entry.url) + end + if #sub_table > 0 then + -- The first element seems to be most compatible. Second element has most options + item = sub_table[2] + else + item = entry + end + for j, link in ipairs(item.acquisitions) do + -- Only save first link in case of several file types + if i == 1 and j == 1 then + new_last_download = link.href + end + local filetype = self.getFiletype(link) + if filetype then + if not file_str or file_list and file_list[filetype] then + local filename = self:getFileName(entry) + local download_path = self:getLocalDownloadPath(filename, filetype, link.href) + if dl_count <= self.sync_max_dl then -- Append only max_dl entries... may still have sync backlog + table.insert(self.pending_syncs, { + file = download_path, + url = link.href, + username = self.root_catalog_username, + password = self.root_catalog_password, + catalog = server.url, + }) + dl_count = dl_count + 1 + end + break + end + end + end + end + end + self.sync_server_list[server.url] = true + if new_last_download then + logger.dbg("Updating opds last download for server", server.title, "to", new_last_download) + self:updateFieldInCatalog(server, "last_download", new_last_download) + end + +end + +-- Get list of books to download bigger than sync_max_dl +function OPDSBrowser:getSyncDownloadList(url_arg) + local sync_table = {} + local fetch_url = url_arg or self.sync_server.url + local sub_table + local up_to_date = false + while #sync_table < self.sync_max_dl and not up_to_date do + sub_table = self:genItemTableFromURL(fetch_url) + -- timeout + if #sub_table == 0 then + return sync_table + end + local count = 1 + local acquisitions_empty = false + -- For project gutenberg + while #sub_table[count].acquisitions == 0 do + if util.stringEndsWith(sub_table[count].url, ".opds") then + acquisitions_empty = true + break + end + if count == #sub_table then + return sync_table + end + count = count + 1 + end + -- First entry in table is the newest + -- If already downloaded, return + local first_href + if acquisitions_empty then + first_href = sub_table[count].url + else + first_href = sub_table[1].acquisitions[1].href + end + if first_href == self.sync_server.last_download and not self.sync_force then + return nil + end + local href + for i, entry in ipairs(sub_table) do + if acquisitions_empty then + if i >= count then + href = entry.url + else + href = nil + end + else + href = entry.acquisitions[1].href + end + if href then + if href == self.sync_server.last_download and not self.sync_force then + up_to_date = true + break + else + table.insert(sync_table, entry) + end + end + end + if not sub_table.hrefs.next then + break + end + fetch_url = sub_table.hrefs.next + end + return sync_table +end + +-- Download pending syncs list +function OPDSBrowser:downloadPendingSyncs() + local dl_list = self.pending_syncs + local function dismissable_download() + local info = InfoMessage:new{ text = _("Downloading… (tap to cancel)") } + UIManager:show(info) + UIManager:forceRePaint() + local completed, downloaded, duplicate_list = Trapper:dismissableRunInSubprocess(function() + local dl = {} + local dupe_list = {} + for _, item in ipairs(dl_list) do + if self.sync_server_list[item.catalog] then + if lfs.attributes(item.file) and not self.sync_force then + table.insert(dupe_list, item) + else + if self:downloadFile(item.file, item.url, item.username, item.password) then + dl[item.file] = true + end + end + end + end + return dl, dupe_list + end, info) + + if completed then + UIManager:close(info) + end + local dl_count = 0 + local dl_size = #dl_list + for i = dl_size, 1, -1 do + local item = dl_list[i] + if downloaded and downloaded[item.file] then + dl_count = dl_count + 1 + table.remove(dl_list, i) + else -- if subprocess has been interrupted, check for the downloaded file + local attr = lfs.attributes(item.file) + if attr then + if attr.size > 0 then + table.remove(dl_list, i) + if attr.modification > os.time() - 300 then -- Only count files touched in the last 5 mins + dl_count = dl_count + 1 + end + else -- incomplete download + os.remove(item.file) + end + end + end + end + local duplicate_count = duplicate_list and #duplicate_list or 0 + dl_count = dl_count - duplicate_count + -- Make downloaded count timeout if there's a duplicate file prompt + local timeout = nil + if duplicate_count > 0 then + timeout = 3 + end + if dl_count > 0 then + UIManager:show(InfoMessage:new{ text = T(N_("1 book downloaded", "%1 books downloaded", dl_count), dl_count), timeout = timeout,}) + end + self._manager.updated = true + return duplicate_list + end + + local duplicate_list = dismissable_download() + + if duplicate_list and #duplicate_list > 0 then + local textviewer + local duplicate_files = { _("These files are already on the device:") } + for _, entry in ipairs(duplicate_list) do + table.insert(duplicate_files, entry.file) + end + local text = table.concat(duplicate_files, "\n") + textviewer = TextViewer:new{ + title = _("Duplicate files"), + text = text, + buttons_table = { + { + { + text = _("Do nothing"), + callback = function() + textviewer:onClose() + end + }, + { + text = _("Overwrite"), + callback = function() + self.sync_force = true + textviewer:onClose() + for _, entry in ipairs(duplicate_list) do + table.insert(dl_list, entry) + end + Trapper:wrap(function() + dismissable_download() + end) + end + }, + { + text = _("Download copies"), + callback = function() + self.sync_force = true + textviewer:onClose() + local copy_download_dir, original_dir, copies_dir, copy_download_path + copies_dir = "copies" + original_dir = util.splitFilePathName(duplicate_list[1].file) + copy_download_dir = original_dir .. copies_dir .. "/" + util.makePath(copy_download_dir) + for _, entry in ipairs(duplicate_list) do + local _, file_name = util.splitFilePathName(entry.file) + copy_download_path = copy_download_dir .. file_name + entry.file = copy_download_path + table.insert(dl_list, entry) + end + Trapper:wrap(function() + dismissable_download() + end) + end + }, + }, + }, + } + UIManager:show(textviewer) + end +end return OPDSBrowser diff --git a/plugins/profiles.koplugin/main.lua b/plugins/profiles.koplugin/main.lua index af61e7dcc..ba5d52fc0 100644 --- a/plugins/profiles.koplugin/main.lua +++ b/plugins/profiles.koplugin/main.lua @@ -629,10 +629,10 @@ function Profiles:genAutoExecPathChangedMenuItem(text, event, profile_name, sepa local value = util.tableGetValue(self.autoexec, event, profile_name, condition) return value and txt .. ": " .. value or txt end, - no_refresh_on_check = true, checked_func = function() return util.tableGetValue(self.autoexec, event, profile_name, condition) end, + check_callback_updates_menu = true, callback = function(touchmenu_instance) local dialog local buttons = {{ @@ -760,10 +760,10 @@ function Profiles:genAutoExecDocConditionalMenuItem(text, event, profile_name, s local txt = util.tableGetValue(self.autoexec, event, profile_name, condition, prop) return txt and title .. " " .. txt or title:sub(1, -2) end, - no_refresh_on_check = true, checked_func = function() return util.tableGetValue(self.autoexec, event, profile_name, condition, prop) and true end, + check_callback_updates_menu = true, callback = function(touchmenu_instance) local dialog local buttons = self.document == nil and {} or {{ @@ -830,10 +830,10 @@ function Profiles:genAutoExecDocConditionalMenuItem(text, event, profile_name, s enabled_func = function() return not util.tableGetValue(self.autoexec, event_always, profile_name) end, - no_refresh_on_check = true, checked_func = function() return util.tableGetValue(self.autoexec, event, profile_name, conditions[3][2]) and true end, + check_callback_updates_menu = true, callback = function(touchmenu_instance) local condition = conditions[3][2] local dialog @@ -895,10 +895,10 @@ function Profiles:genAutoExecDocConditionalMenuItem(text, event, profile_name, s enabled_func = function() return not util.tableGetValue(self.autoexec, event_always, profile_name) end, - no_refresh_on_check = true, checked_func = function() return util.tableGetValue(self.autoexec, event, profile_name, conditions[4][2]) and true end, + check_callback_updates_menu = true, callback = function(touchmenu_instance) local condition = conditions[4][2] local collections = util.tableGetValue(self.autoexec, event, profile_name, condition) diff --git a/plugins/readtimer.koplugin/main.lua b/plugins/readtimer.koplugin/main.lua index ae992943d..073dfbabb 100644 --- a/plugins/readtimer.koplugin/main.lua +++ b/plugins/readtimer.koplugin/main.lua @@ -1,6 +1,7 @@ local CheckButton = require("ui/widget/checkbutton") local ConfirmBox = require("ui/widget/confirmbox") local DateTimeWidget = require("ui/widget/datetimewidget") +local Dispatcher = require("dispatcher") local Event = require("ui/event") local InfoMessage = require("ui/widget/infomessage") local UIManager = require("ui/uimanager") @@ -17,6 +18,15 @@ local ReadTimer = WidgetContainer:extend{ last_interval_time = 0, } +function ReadTimer:onDispatcherRegisterActions() + Dispatcher:registerAction("show_alarm", + {category="none", event="ShowAlarm", title=_("Set reader alarm"), general=true}) + Dispatcher:registerAction("show_timer", + {category="none", event="ShowTimer", title=_("Set reader timer"), general=true}) + Dispatcher:registerAction("stop_timer", + {category="none", event="StopTimer", title=_("Stop reader timer"), general=true, separator=true}) +end + function ReadTimer:init() self.timer_symbol = "\u{23F2}" -- ⏲ timer symbol self.timer_letter = "T" @@ -91,6 +101,7 @@ function ReadTimer:init() end self.ui.menu:registerToMainMenu(self) + self:onDispatcherRegisterActions() end function ReadTimer:update_status_bars(seconds) @@ -248,85 +259,14 @@ function ReadTimer:addToMainMenu(menu_items) text = _("Set time"), keep_menu_open = true, callback = function(touchmenu_instance) - local now_t = os.date("*t") - local curr_hour = now_t.hour - local curr_min = now_t.min - local time_widget = DateTimeWidget:new{ - hour = curr_hour, - min = curr_min, - ok_text = _("Set alarm"), - title_text = _("New alarm"), - info_text = _("Enter a time in hours and minutes."), - callback = function(alarm_time) - self.last_interval_time = 0 - self:unschedule() - local then_t = now_t - then_t.hour = alarm_time.hour - then_t.min = alarm_time.min - then_t.sec = 0 - local seconds = os.difftime(os.time(then_t), os.time()) - if seconds <= 0 then - then_t.day = then_t.day + 1 - seconds = os.difftime(os.time(then_t), os.time()) - end - self:rescheduleIn(seconds) - local user_duration_format = G_reader_settings:readSetting("duration_format") - UIManager:show(InfoMessage:new{ - -- @translators %1:%2 is a clock time (HH:MM), %3 is a duration - text = T(_("Timer set for %1:%2.\n\nThat's %3 from now."), - string.format("%02d", alarm_time.hour), string.format("%02d", alarm_time.min), - datetime.secondsToClockDuration(user_duration_format, seconds, false)), - timeout = 5, - }) - if touchmenu_instance then touchmenu_instance:updateItems() end - end - } - self:addCheckboxes(time_widget) - UIManager:show(time_widget) + self:onShowAlarm(touchmenu_instance) end, }, { text = _("Set interval"), keep_menu_open = true, callback = function(touchmenu_instance) - local remain_time = {} - local remain_hours, remain_minutes = self:remainingTime() - if not remain_hours and not remain_minutes then - remain_time = G_reader_settings:readSetting("reader_timer_remain_time") - if remain_time then - remain_hours = remain_time[1] - remain_minutes = remain_time[2] - end - end - local time_widget = DateTimeWidget:new{ - hour = remain_hours or 0, - min = remain_minutes or 0, - hour_max = 17, - ok_text = _("Set timer"), - title_text = _("Set reader timer"), - info_text = _("Enter a time in hours and minutes."), - callback = function(timer_time) - self:unschedule() - local seconds = timer_time.hour * 3600 + timer_time.min * 60 - if seconds > 0 then - self.last_interval_time = seconds - self:rescheduleIn(seconds) - local user_duration_format = G_reader_settings:readSetting("duration_format") - UIManager:show(InfoMessage:new{ - -- @translators This is a duration - text = T(_("Timer will expire in %1."), - datetime.secondsToClockDuration(user_duration_format, seconds, true)), - timeout = 5, - }) - remain_time = {timer_time.hour, timer_time.min} - G_reader_settings:saveSetting("reader_timer_remain_time", remain_time) - if touchmenu_instance then touchmenu_instance:updateItems() end - end - end - } - - self:addCheckboxes(time_widget) - UIManager:show(time_widget) + self:onShowTimer(touchmenu_instance) end, }, { @@ -336,9 +276,7 @@ function ReadTimer:addToMainMenu(menu_items) return self:scheduled() end, callback = function(touchmenu_instance) - self.last_interval_time = 0 - self:unschedule() - touchmenu_instance:updateItems() + self:onStopTimer(touchmenu_instance) end, }, }, @@ -365,4 +303,108 @@ function ReadTimer:onResume() end end +function ReadTimer:onShowAlarm(touchmenu_instance) + local now_t = os.date("*t") + local curr_hour = now_t.hour + local curr_min = now_t.min + local time_widget = DateTimeWidget:new{ + hour = curr_hour, + min = curr_min, + ok_text = _("Set alarm"), + title_text = _("New alarm"), + info_text = _("Enter a time in hours and minutes."), + callback = function(alarm_time) + self:setAlarm(alarm_time, now_t, touchmenu_instance) + end + } + self:addCheckboxes(time_widget) + UIManager:show(time_widget) + return true +end + +function ReadTimer:setAlarm(alarm_time, then_t, touchmenu_instance) + then_t.hour = alarm_time.hour + then_t.min = alarm_time.min + then_t.sec = 0 + local seconds = os.difftime(os.time(then_t), os.time()) + if seconds <= 0 then + then_t.day = then_t.day + 1 + seconds = os.difftime(os.time(then_t), os.time()) + end + self.last_interval_time = 0 + self:unschedule() + self:rescheduleIn(seconds) + + local user_duration_format = G_reader_settings:readSetting("duration_format") + UIManager:show(InfoMessage:new{ + -- @translators %1:%2 is a clock time (HH:MM), %3 is a duration + text = T(_("Timer set for %1:%2.\n\nThat's %3 from now."), + string.format("%02d", alarm_time.hour), string.format("%02d", alarm_time.min), + datetime.secondsToClockDuration(user_duration_format, seconds, false)), + timeout = 5, + }) + if touchmenu_instance then touchmenu_instance:updateItems() end +end + +function ReadTimer:onShowTimer(touchmenu_instance) + local remain_hours, remain_minutes = self:remainingTime() + if not remain_hours and not remain_minutes then + local remain_time = G_reader_settings:readSetting("reader_timer_remain_time") + if remain_time then + remain_hours = remain_time[1] + remain_minutes = remain_time[2] + end + end + local time_widget = DateTimeWidget:new{ + hour = remain_hours or 0, + min = remain_minutes or 0, + hour_max = 17, + ok_text = _("Set timer"), + title_text = _("Set reader timer"), + info_text = _("Enter a time in hours and minutes."), + callback = function(timer_time) + self:setInterval(timer_time, touchmenu_instance) + end + } + + self:addCheckboxes(time_widget) + UIManager:show(time_widget) + return true +end + +function ReadTimer:setInterval(timer_time, touchmenu_instance) + local seconds = timer_time.hour * 3600 + timer_time.min * 60 + if seconds > 0 then + self:unschedule() + self.last_interval_time = seconds + self:rescheduleIn(seconds) + + local user_duration_format = G_reader_settings:readSetting("duration_format") + UIManager:show(InfoMessage:new{ + -- @translators This is a duration + text = T(_("Timer will expire in %1."), + datetime.secondsToClockDuration(user_duration_format, seconds, true)), + timeout = 5, + }) + if touchmenu_instance then touchmenu_instance:updateItems() end + + -- Save settings + local remain_time = {timer_time.hour, timer_time.min} + G_reader_settings:saveSetting("reader_timer_remain_time", remain_time) + end +end + +function ReadTimer:onStopTimer(touchmenu_instance) + if self:scheduled() then + self.last_interval_time = 0 + self:unschedule() + if touchmenu_instance then + touchmenu_instance:updateItems() + else + UIManager:show(InfoMessage:new{text=_("Timer stopped")}) + end + end + return true +end + return ReadTimer diff --git a/plugins/terminal.koplugin/main.lua b/plugins/terminal.koplugin/main.lua index 2245510ab..6a42d774a 100644 --- a/plugins/terminal.koplugin/main.lua +++ b/plugins/terminal.koplugin/main.lua @@ -477,6 +477,9 @@ function Terminal:generateInputDialog() end, }, }}, + del_word_callback = function() + self:transmit("\023") -- Ctrl+U + end, enter_callback = function() self:transmit("\r") end, diff --git a/plugins/terminal.koplugin/terminputtext.lua b/plugins/terminal.koplugin/terminputtext.lua index 68a9c5cd3..8c7a067c3 100644 --- a/plugins/terminal.koplugin/terminputtext.lua +++ b/plugins/terminal.koplugin/terminputtext.lua @@ -779,6 +779,12 @@ function TermInputText:delChar() InputText.delChar(self) end +function TermInputText:delWord(left_to_cursor) + if self.parent and self.parent.del_word_callback then + self.parent.del_word_callback(left_to_cursor) + end +end + function TermInputText:delToStartOfLine() return end diff --git a/plugins/wallabag.koplugin/main.lua b/plugins/wallabag.koplugin/main.lua index 5a2758276..b1e001e9a 100644 --- a/plugins/wallabag.koplugin/main.lua +++ b/plugins/wallabag.koplugin/main.lua @@ -88,6 +88,7 @@ function Wallabag:init() -- These settings do have defaults self.filter_tag = self.wb_settings.data.wallabag.filter_tag or "" + self.filter_starred = self.wb_settings.data.wallabag.filter_starred or false self.ignore_tags = self.wb_settings.data.wallabag.ignore_tags or "" self.auto_tags = self.wb_settings.data.wallabag.auto_tags or "" self.archive_finished = self.wb_settings.data.wallabag.archive_finished or true @@ -260,6 +261,17 @@ function Wallabag:addToMainMenu(menu_items) ) end, }, + { + text = _("Only download starred articles"), + keep_menu_open = true, + checked_func = function() + return self.filter_starred or false + end, + callback = function() + self.filter_starred = not self.filter_starred + self:saveSettings() + end, + }, { text = _("Prefer original non-HTML document"), keep_menu_open = true, @@ -616,6 +628,10 @@ function Wallabag:getArticleList() filtering = "&tags=" .. self.filter_tag end + if self.filter_starred then + filtering = filtering .. "&starred=1" + end + local article_list = {} local page = 1 @@ -1593,6 +1609,7 @@ function Wallabag:saveSettings() password = self.password, directory = self.directory, filter_tag = self.filter_tag, + filter_starred = self.filter_starred, ignore_tags = self.ignore_tags, auto_tags = self.auto_tags, archive_finished = self.archive_finished,