e0b537bc16
Downloading a Readest cloud library into KOReader previously required tapping each book one at a time. Add a "Download all books" action to the Library view menu that pulls every cloud-only book to the device in one pass. - LibraryStore:listCloudOnlyBooks() returns the downloadable cloud-present, not-local books for the current user (whole library, independent of the active search/group view). - librarywidget.downloadAll() streams them sequentially via the existing syncbooks.downloadBook path inside a Trapper coroutine: cancellable progress (Trapper:info Abort/Continue), per-book failures skipped and counted, summary toast at the end. Bridges downloadBook's sync-or-async callback with a coroutine suspended check so it serializes correctly either way. - Wire the action into the view-menu Actions section. - Add the new UI strings and translate them across all 33 locales. Closes #4751 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
180 lines
7.3 KiB
Lua
180 lines
7.3 KiB
Lua
-- libraryviewmenu.lua
|
|
-- The Library view-menu — a ButtonDialog with sections for View Mode,
|
|
-- Columns, Cover fit, Group by, Sort by, Rescan library, and Download
|
|
-- folder. Persists every choice to G_reader_settings.readest_sync.library_*
|
|
-- and notifies the caller (LibraryWidget) so it can re-query and re-render.
|
|
--
|
|
-- Live-KOReader-only; smoke-tested via the manual matrix.
|
|
|
|
local ButtonDialog = require("ui/widget/buttondialog")
|
|
local PathChooser = require("ui/widget/pathchooser")
|
|
local UIManager = require("ui/uimanager")
|
|
local _ = require("readest_i18n")
|
|
|
|
local M = {}
|
|
|
|
-- Settings that affect the Menu's dimensions / mixin selection. Changing
|
|
-- one requires a full Menu rebuild; M.refresh() (which only re-queries
|
|
-- listBooks) wouldn't pick them up because nb_cols_portrait and the
|
|
-- _recalculateDimen / _updateItemsBuildUI pointers are baked in at
|
|
-- Menu construction time.
|
|
local LAYOUT_KEYS = {
|
|
library_view_mode = true,
|
|
library_columns = true,
|
|
library_rows = true,
|
|
}
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Helper: persist + invoke the right kind of refresh
|
|
-- ---------------------------------------------------------------------------
|
|
local function set(opts, key, value)
|
|
opts.settings[key] = value
|
|
G_reader_settings:saveSetting("readest_sync", opts.settings)
|
|
if LAYOUT_KEYS[key] and opts.on_layout_change then
|
|
opts.on_layout_change()
|
|
elseif opts.on_change then
|
|
opts.on_change()
|
|
end
|
|
end
|
|
|
|
-- Per-key default values that match what librarywidget assumes when the
|
|
-- user hasn't explicitly chosen anything. The view menu's ✓ marker needs
|
|
-- to know these so the implicit default still shows as selected.
|
|
local DEFAULTS = {
|
|
library_view_mode = "mosaic",
|
|
library_group_by = "groups",
|
|
library_sort_by = "last_read_at",
|
|
library_sort_ascending = false,
|
|
}
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Helper: render a single row of mutually-exclusive options. Each option's
|
|
-- label gets a "✓ " prefix when its value matches the current setting (or
|
|
-- the default, if the setting hasn't been explicitly set).
|
|
-- The dialog-close call is added by the back-fill loop in show() so this
|
|
-- helper doesn't need to capture the dialog ref.
|
|
-- ---------------------------------------------------------------------------
|
|
local function row(opts, key, choices)
|
|
local current = opts.settings[key]
|
|
if current == nil then current = DEFAULTS[key] end
|
|
local buttons = {}
|
|
for _i, choice in ipairs(choices) do
|
|
local label = choice.label
|
|
if choice.value == current then label = "✓ " .. label end
|
|
buttons[#buttons + 1] = {
|
|
text = label,
|
|
callback = function() set(opts, key, choice.value) end,
|
|
}
|
|
end
|
|
return buttons
|
|
end
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- show(opts) — opts: { settings, on_change }
|
|
-- ---------------------------------------------------------------------------
|
|
function M.show(opts)
|
|
local dialog
|
|
dialog = ButtonDialog:new{
|
|
title = _("Library view"),
|
|
title_align = "center",
|
|
buttons = {
|
|
-- View Mode
|
|
{ { text = _("View"), enabled = false } },
|
|
row(opts, "library_view_mode", {
|
|
{ label = _("Grid"), value = "mosaic" },
|
|
{ label = _("List"), value = "list" },
|
|
}),
|
|
|
|
-- Group by — alphabetical order matching Readest's web UI.
|
|
-- Values mirror Readest's LibraryGroupByType ("authors",
|
|
-- "groups", "series"); active_group_by() in librarywidget
|
|
-- maps these to the SQL column names ("author",
|
|
-- "group_name", "series") at the store boundary.
|
|
-- "Books" is the no-grouping mode (flat list).
|
|
{ { text = _("Group by"), enabled = false } },
|
|
row(opts, "library_group_by", {
|
|
{ label = _("Authors"), value = "authors" },
|
|
{ label = _("Books"), value = "none" },
|
|
{ label = _("Groups"), value = "groups" },
|
|
{ label = _("Series"), value = "series" },
|
|
}),
|
|
|
|
-- Sort by
|
|
{ { text = _("Sort by"), enabled = false } },
|
|
row(opts, "library_sort_by", {
|
|
{ label = _("Date Read"), value = "last_read_at" },
|
|
{ label = _("Date Added"), value = "created_at" },
|
|
{ label = _("Title"), value = "title" },
|
|
{ label = _("Author"), value = "author" },
|
|
}),
|
|
row(opts, "library_sort_ascending", {
|
|
{ label = _("Descending"), value = false },
|
|
{ label = _("Ascending"), value = true },
|
|
}),
|
|
|
|
-- Actions
|
|
{ { text = _("Actions"), enabled = false } },
|
|
{
|
|
{
|
|
text = _("Download all books"),
|
|
callback = function()
|
|
require("library.librarywidget").downloadAll()
|
|
end,
|
|
},
|
|
},
|
|
{
|
|
{
|
|
text = _("Rescan library"),
|
|
callback = function()
|
|
local Trapper = require("ui/trapper")
|
|
local LibraryWidget = require("library.librarywidget")
|
|
local localscanner = require("library.localscanner")
|
|
Trapper:wrap(function()
|
|
localscanner.fullSidecarWalk({
|
|
store = LibraryWidget._store,
|
|
home_dir = G_reader_settings:readSetting("home_dir"),
|
|
})
|
|
LibraryWidget.refresh()
|
|
end)
|
|
end,
|
|
},
|
|
{
|
|
text = _("Download folder…"),
|
|
callback = function()
|
|
local picker
|
|
picker = PathChooser:new{
|
|
title = _("Pick a folder for downloaded books"),
|
|
path = opts.settings.library_download_dir
|
|
or G_reader_settings:readSetting("home_dir")
|
|
or require("datastorage"):getDataDir(),
|
|
select_directory = true,
|
|
select_file = false,
|
|
onConfirm = function(path)
|
|
set(opts, "library_download_dir", path)
|
|
end,
|
|
}
|
|
UIManager:show(picker)
|
|
end,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
-- Wrap every actionable callback with `UIManager:close(dialog)` so each
|
|
-- selection dismisses the dialog before applying its effect. Disabled
|
|
-- header rows have no callback and are skipped.
|
|
for _i, line in ipairs(dialog.buttons) do
|
|
for _j, btn in ipairs(line) do
|
|
if btn.callback then
|
|
local orig = btn.callback
|
|
btn.callback = function()
|
|
UIManager:close(dialog)
|
|
return orig()
|
|
end
|
|
end
|
|
end
|
|
end
|
|
UIManager:show(dialog)
|
|
end
|
|
|
|
return M
|