Files
readest/apps/readest.koplugin/library/libraryviewmenu.lua
T
Huang Xin e0b537bc16 feat(koplugin): bulk download all cloud books from Library view (#4751) (#4765)
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>
2026-06-24 17:42:31 +02:00

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