mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
80244ea4e2
Squash of feat/koreader-widget-board. KOReader plugin 2.0.0 gets the widget-board dashboard, redesigned book details, menus, lists and dialogs, Decor page and reading companions. Highlights, notes and bookmarks now stay in step across web, Kobo, KOReader and the phones (defects D-00 to D-20, migration 0121), and reading progress and narration hand off cleanly between devices.
234 lines
10 KiB
Lua
234 lines
10 KiB
Lua
--[[--
|
|
Downloading many books at once, in one sheet: what to download and how, then
|
|
the run's progress, then what came of it.
|
|
|
|
Plan: the books (selected, this page or the whole list), where they go, which
|
|
format wins, what happens to files already there, and how read-aloud EPUBs
|
|
arrive; one Download button. Progress: which book, how far, the counts so
|
|
far, and Stop after this book. Summary: the counts that are not zero, what
|
|
needs attention, Retry failed and View on device.
|
|
]]
|
|
|
|
local UIManager = require("ui/uimanager")
|
|
local T = require("ffi/util").template
|
|
local _ = require("gettext")
|
|
|
|
local BoardMetrics = require("bookorbit_board_metrics")
|
|
local BoardUI = require("bookorbit_board_ui")
|
|
local DialogUI = require("bookorbit_dialog_ui")
|
|
local MenuUI = require("bookorbit_menu_ui")
|
|
|
|
local INK = BoardMetrics.INK
|
|
local px = BoardMetrics.px
|
|
local text, vstack, hstack, space, flex = BoardUI.text, BoardUI.vstack, BoardUI.hstack, BoardUI.space, BoardUI.flex
|
|
|
|
local BulkSheet = {}
|
|
|
|
local function wOf(widget) return widget:getSize().w end
|
|
|
|
-- Numbers side by side, each over its label.
|
|
local function stats(w, items)
|
|
local n = #items
|
|
local gap = px(8)
|
|
local cw = math.floor((w - gap * (n - 1)) / n)
|
|
local cells = {}
|
|
for _i, it in ipairs(items) do
|
|
table.insert(cells, vstack({ w = cw }, {
|
|
text(tostring(it.value), { s = 20, b = true, w = cw }),
|
|
text(it.label, { s = 12, col = INK.g4, w = cw }),
|
|
}))
|
|
end
|
|
return hstack({ w = w, gap = gap, valign = "top" }, cells)
|
|
end
|
|
|
|
-- ------------------------------------------------------------------ plan
|
|
local function planBody(page, w)
|
|
local catalog, plan = page.catalog, page.plan
|
|
local iw = MenuUI.rowsIn(w)
|
|
local items = { DialogUI.titleRow(w, plan.settings_only and _("Download settings") or _("Download books"), plan.sub) }
|
|
local source = plan.scopes and plan.scopes[page.scope] and plan.scopes[page.scope].source or nil
|
|
if plan.scopes and #plan.scopes > 1 then
|
|
local rows = {}
|
|
for index, scope in ipairs(plan.scopes) do
|
|
table.insert(rows, MenuUI.row(iw, { label = scope.label, sub = scope.sub, sub_data = true, radio = index == page.scope, action = { kind = "scope", index = index } }))
|
|
end
|
|
table.insert(items, DialogUI.section(w, _("Books"), MenuUI.group(w, rows)))
|
|
end
|
|
local BookPeek = require("bookorbit_book_peek")
|
|
local folder = catalog:getCurrentDownloadDir()
|
|
local how = {
|
|
MenuUI.row(iw, { icon = "folder-open", label = _("Folder"), sub = BookPeek.folderLabel(folder) or folder, sub_data = true, chevron = true, action = { kind = "folder" } }),
|
|
MenuUI.row(iw, { icon = "file-text", label = _("Format"), sub = catalog:bulkFormatPreset().label, chevron = true, action = { kind = "format" } }),
|
|
MenuUI.row(iw, { icon = "files", label = _("Files already there"), sub = catalog:bulkExistingPolicy().label, chevron = true, action = { kind = "existing" } }),
|
|
MenuUI.row(iw, { icon = "headphones", label = _("Read-aloud EPUBs"), sub = catalog:bulkDeliveryLabel(), chevron = true, action = { kind = "delivery" } }),
|
|
}
|
|
table.insert(items, DialogUI.section(w, _("How"), MenuUI.group(w, how)))
|
|
if plan.settings_only then return items end
|
|
|
|
if catalog:bulkExistingPolicy().id == "overwrite" then
|
|
table.insert(items, DialogUI.warn(w, _("Files already there are replaced"), _("Replacing a book can move its place and highlights. Each old file stays until its new copy is complete.")))
|
|
end
|
|
local skips = source and source.known_skips
|
|
if skips and (skips.on_device > 0 or skips.unsupported > 0) then
|
|
local parts = {}
|
|
if skips.on_device > 0 then table.insert(parts, T(_.ngettext("%1 is already on this device", "%1 are already on this device", skips.on_device), skips.on_device)) end
|
|
if skips.unsupported > 0 then table.insert(parts, T(_.ngettext("%1 has no format this reader opens", "%1 have no format this reader opens", skips.unsupported), skips.unsupported)) end
|
|
table.insert(items, DialogUI.note(w, "info", table.concat(parts, "; ") .. ".", { col = INK.g3 }))
|
|
end
|
|
local total = source and source.total or 0
|
|
table.insert(items, MenuUI.pill(catalog:bulkDownloadLabel(total), { icon = "download", w = w, action = { kind = "download" } }))
|
|
return items
|
|
end
|
|
|
|
-- -------------------------------------------------------------- progress
|
|
local function progressBody(page, w)
|
|
local ctx = page.ctx
|
|
local counts = ctx.counts or {}
|
|
local total = math.max(ctx.total or 0, ctx.index or 0)
|
|
local title
|
|
if (ctx.index or 0) > 0 then
|
|
title = T(_("Downloading %1 of %2"), ctx.index, total)
|
|
else
|
|
title = ctx.progress_status or _("Getting ready…")
|
|
end
|
|
local items = { DialogUI.titleRow(w, title, ctx.label, { close = false }) }
|
|
local book = ctx.progress_book
|
|
local now = vstack({ w = w }, {
|
|
book and text(book.title or _("Untitled"), { s = 15, b = true, w = w, data = true }) or false,
|
|
(ctx.index or 0) > 0 and ctx.progress_status and text(ctx.progress_status, { s = 13, col = INK.g4, w = w, data = true }) or false,
|
|
space(nil, px(8)),
|
|
BoardUI.bar(w, px(8), total > 0 and math.floor((ctx.index or 0) * 100 / total) or 0, { r = px(4), track = INK.gd }),
|
|
})
|
|
table.insert(items, now)
|
|
table.insert(items, stats(w, {
|
|
{ value = counts.downloaded or 0, label = _("Downloaded") },
|
|
{ value = (counts.skipped_on_device or 0) + (counts.skipped_present or 0), label = _("Already here") },
|
|
{ value = (counts.skipped_existing or 0) + (counts.skipped_unsupported or 0), label = _("Skipped") },
|
|
{ value = counts.failed or 0, label = _("Failed") },
|
|
}))
|
|
table.insert(items, MenuUI.pill(ctx.cancel_requested and _("Stopping after this book…") or _("Stop after this book"),
|
|
{ kind = "outline", w = w, action = ctx.cancel_requested and { kind = "noop" } or { kind = "stop" } }))
|
|
return items
|
|
end
|
|
|
|
-- --------------------------------------------------------------- summary
|
|
local function summaryBody(page, w)
|
|
local s = page.summary
|
|
local iw = MenuUI.rowsIn(w)
|
|
local items = {}
|
|
local tw = w - px(24) - px(12)
|
|
table.insert(items, hstack({ w = w, gap = px(12), valign = "top" }, {
|
|
s.stopped and BoardUI.circle(px(24), { bg = INK.ge }, BoardUI.icon("x", px(15), { sw = 3 })) or MenuUI.check(),
|
|
vstack({ w = tw }, {
|
|
text(s.title, { s = 18, b = true, w = tw }),
|
|
s.sub and text(s.sub, { s = 13, col = INK.g4, w = tw, data = true }) or false,
|
|
}),
|
|
}))
|
|
local rows = {}
|
|
for _i, r in ipairs(s.rows) do
|
|
local v = text(tostring(r.value), { s = 14, b = true })
|
|
local row = hstack({ w = iw, h = px(38) }, { text(r.label, { s = 14, col = INK.g3, w = iw - wOf(v) - px(16) }), flex(px(16)), v })
|
|
row._no_icon = true
|
|
table.insert(rows, row)
|
|
end
|
|
if #rows > 0 then table.insert(items, MenuUI.group(w, rows, { inset = 0 })) end
|
|
for _i, note in ipairs(s.notes) do
|
|
table.insert(items, DialogUI.note(w, "info", note, { col = INK.g3, lines = 3 }))
|
|
end
|
|
if #s.failed_titles > 0 then
|
|
local list = table.concat(s.failed_titles, ", ")
|
|
if s.more_failed > 0 then list = T(_("%1, and %2 more"), list, s.more_failed) end
|
|
table.insert(items, DialogUI.warn(w, _("These did not download"), list))
|
|
end
|
|
if s.can_retry then
|
|
table.insert(items, MenuUI.pill(_("Retry failed"), { icon = "refresh-cw", w = w, action = { kind = "retry" } }))
|
|
end
|
|
table.insert(items, DialogUI.pillRow(w, {
|
|
{ label = _("View on device"), kind = "outline", action = { kind = "on-device" } },
|
|
{ label = _("Done"), kind = s.can_retry and "outline" or nil, action = { kind = "close" } },
|
|
}))
|
|
return items
|
|
end
|
|
|
|
local function build(page, sw, sh)
|
|
local body = page.state == "progress" and progressBody or page.state == "summary" and summaryBody or planBody
|
|
return DialogUI.sheet(page, sw, sh, function(w) return body(page, w) end, { pop_w = 480 })
|
|
end
|
|
|
|
local function onAction(page, action)
|
|
local catalog = page.catalog
|
|
local kind = action.kind
|
|
local refresh = function() if not page.closed then page:refresh() end end
|
|
if kind == "scope" then
|
|
page.scope = action.index
|
|
refresh()
|
|
elseif kind == "folder" then
|
|
catalog:chooseDownloadDir(refresh)
|
|
elseif kind == "format" then
|
|
catalog:showBulkFormatPresetDialog(refresh)
|
|
elseif kind == "existing" then
|
|
catalog:showBulkExistingPolicyDialog(refresh)
|
|
elseif kind == "delivery" then
|
|
catalog:showDeliveryPreferenceDialog(refresh)
|
|
elseif kind == "download" then
|
|
local scope = page.plan.scopes[page.scope]
|
|
page:close()
|
|
UIManager:tickAfterNext(function() page.plan.on_download(scope.source) end)
|
|
elseif kind == "stop" then
|
|
catalog:bulkRequestStop(page.ctx)
|
|
elseif kind == "retry" then
|
|
page:close()
|
|
UIManager:tickAfterNext(page.summary.on_retry)
|
|
elseif kind == "on-device" then
|
|
page:close()
|
|
UIManager:tickAfterNext(function() catalog:loadOnDevice() end)
|
|
end
|
|
return true
|
|
end
|
|
|
|
-- plan: sub (the list's name), scopes ({ label, sub, source }), scope (the
|
|
-- one chosen at first), on_download(source), settings_only.
|
|
function BulkSheet.openPlan(catalog, plan)
|
|
return DialogUI.open{
|
|
name = "bookorbit_bulk_sheet",
|
|
build = build,
|
|
on_action = onAction,
|
|
catalog = catalog,
|
|
plan = plan,
|
|
scope = plan.scope or 1,
|
|
state = "plan",
|
|
}
|
|
end
|
|
|
|
-- The run's sheet: it follows ctx (index, total, counts, progress_status,
|
|
-- progress_book, cancel_requested) until it is given the summary.
|
|
function BulkSheet.openProgress(catalog, ctx)
|
|
local page = DialogUI.open{
|
|
name = "bookorbit_bulk_sheet",
|
|
build = build,
|
|
on_action = onAction,
|
|
catalog = catalog,
|
|
ctx = ctx,
|
|
state = "progress",
|
|
dismiss_outside = false,
|
|
}
|
|
-- Back must not close the only way to stop a run.
|
|
page.on_back = function() return true end
|
|
function page:update()
|
|
if self.closed then return end
|
|
self:refresh()
|
|
end
|
|
-- summary: title, sub, rows ({ label, value }), notes, failed_titles,
|
|
-- more_failed, stopped, can_retry, on_retry.
|
|
function page:finished(summary)
|
|
if self.closed then return end
|
|
self.state, self.summary = "summary", summary
|
|
self.dismiss_outside = true
|
|
self.on_back = nil
|
|
self:refresh()
|
|
end
|
|
return page
|
|
end
|
|
|
|
return BulkSheet
|