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,