Ctrl K
ComunidadGuías…

koreader-plugin/bookorbit.koplugin/bookorbit_bulk_sheet.lua

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
--[[--
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