Ctrl K
ComunidadGuías…

koreader-plugin/bookorbit.koplugin/bookorbit_download_sheet.lua

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

Archivo original ↗Preguntar a KiBot
--[[--
One sheet for downloading a book: what used to be Choose file, Download
options, the folder and file name dialog, Overwrite? and Open now?

Choose: the editions the server offers, then where it goes (folder and file
name), then Download. A file already at that path shows a warning in the
sheet and the button says Replace. The same sheet then shows the download's
progress, and ends on Start reading or on what went wrong.
]]

local UIManager = require("ui/uimanager")
local lfs = require("libs/libkoreader-lfs")
local T = require("ffi/util").template
local _ = require("gettext")

local BoardMetrics = require("bookorbit_board_metrics")
local BoardUI = require("bookorbit_board_ui")
local CatalogUtil = require("bookorbit_catalog_util")
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 = BoardUI.text, BoardUI.vstack, BoardUI.hstack, BoardUI.space

local DownloadSheet = {}

local formatBytes = CatalogUtil.formatBytes

local FORMAT_WORDS = {
    epub = _("The ebook"),
    kepub = _("The ebook, for Kobo"),
    pdf = _("Fixed pages"),
    cbz = _("The comic"),
    cbr = _("The comic"),
    mobi = _("The ebook"),
    azw3 = _("The ebook"),
    fb2 = _("The ebook"),
    djvu = _("Scanned pages"),
}

-- The editions a book offers: each supported file, and for a read-along
-- EPUB both of its forms, text only and with its narration. Pure, for specs.
-- Each edition: { file, variant, label, sub }.
function DownloadSheet.editions(files)
    local out = {}
    for _i, file in ipairs(files or {}) do
        local format = string.lower(file.format or "")
        if file.sourceMediaOverlayAvailable == true then
            local text_size = file.downloadVariant == "audioless_epub" and file.sizeBytes or nil
            local full_size = file.downloadVariant == "original" and file.sizeBytes or file.sourceSizeBytes
            table.insert(out, {
                file = file, variant = "audioless_epub",
                label = _("Read-along EPUB, text only"),
                sub = text_size and T(_("Narration taken out, %1"), formatBytes(text_size)) or _("Narration taken out, so it stays small"),
            })
            table.insert(out, {
                file = file, variant = "original",
                label = _("Read-along EPUB with narration"),
                sub = full_size and T(_("Includes the audio, %1"), formatBytes(full_size)) or _("Includes the audio"),
            })
        else
            local size = file.sizeBytes or file.sourceSizeBytes
            local words = FORMAT_WORDS[format] or string.upper(format)
            table.insert(out, {
                file = file, variant = file.downloadVariant,
                label = string.upper(format),
                sub = size and (words .. ", " .. formatBytes(size)) or words,
            })
        end
    end
    return out
end

-- Which edition a file and variant pick.
function DownloadSheet.indexOf(editions, file_id, variant)
    for index, e in ipairs(editions) do
        if e.file.id == file_id and (variant == nil or e.variant == variant or e.variant == nil) then return index end
    end
    return 1
end

-- The size the download will be, when it is known.
local function editionSize(e)
    local f = e.file
    if e.variant == f.downloadVariant and f.sizeBytes then return f.sizeBytes end
    if f.sourceMediaOverlayAvailable == true then
        return e.variant == "original" and f.sourceSizeBytes or nil
    end
    return f.sizeBytes or f.sourceSizeBytes
end

-- Where the chosen edition would land: the folder, the name without its
-- extension, the extension, and the full path.
local function target(page)
    local e = page.editions[page.choice]
    local f = e.file
    local ext = string.lower(f.format or "bin")
    local folder, name = page.catalog:getLocalDownloadPreview(page.filename or CatalogUtil.safeFilenameBase(page.detail), ext, f.devicePath, page.filename ~= nil)
    return folder, name, ext, folder .. "/" .. name .. "." .. ext
end

local function head(page, w, close)
    local d = page.detail
    local authors = {}
    for _i, a in ipairs(d.authors or {}) do table.insert(authors, a) end
    return DialogUI.bookHead(w, { title = d.title, author = table.concat(authors, ", ") }, { cover_path = page.catalog:cachedThumbnailPath(d), close = close })
end

local function chooseBody(page, w, two_col)
    local folder, name, ext, path = target(page)
    local BookPeek = require("bookorbit_book_peek")
    local exists = lfs.attributes(path, "mode") == "file"
    local iw = MenuUI.rowsIn(w)
    local rows = {}
    for index, e in ipairs(page.editions) do
        table.insert(rows, MenuUI.row(iw, { label = e.label, sub = e.sub, radio = index == page.choice, action = { kind = "edition", index = index } }))
    end
    local editions = DialogUI.section(w, _("Edition"), MenuUI.group(w, rows))
    local save = DialogUI.section(w, _("Save to"), MenuUI.group(w, {
        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 = "pen-line", label = _("File name"), sub = name .. "." .. ext, sub_data = true, sublines = 2, chevron = true, action = { kind = "rename" } }),
    }))
    local size = formatBytes(editionSize(page.editions[page.choice]))
    local label = exists and _("Replace") or _("Download")
    if size ~= "" then label = T(_("%1, %2"), label, size) end
    local button = MenuUI.pill(label, { icon = "download", w = w, action = { kind = "start" } })
    local warn = exists and DialogUI.warn(w, _("Already on this device"), _("Replacing it can move your place and highlights. The old copy stays until the new one is complete.")) or nil
    if two_col then
        return { left = editions, right = { save, warn or false, button } }
    end
    return { head(page, w), editions, save, warn or false, button }
end

local function progressBody(page, w)
    local p = page.progress or {}
    local folder = target(page)
    local BookPeek = require("bookorbit_book_peek")
    local pct = (p.total and p.total > 0) and math.floor((p.received or 0) * 100 / p.total) or nil
    local sub = (p.total and p.total > 0) and T(_("%1 of %2"), formatBytes(p.received or 0), formatBytes(p.total)) or formatBytes(p.received or 0)
    if sub == "" then sub = _("Starting…") end
    return {
        head(page, w, false),
        vstack({ w = w }, {
            BoardUI.spread(w, text(_("Downloading"), { s = 16, b = true }), pct and text(T(_("%1%"), math.min(99, pct)), { s = 16, b = true }) or nil),
            space(nil, px(8)),
            BoardUI.bar(w, px(8), pct or 0, { r = px(4), track = INK.gd }),
            space(nil, px(8)),
            text(T(_("%1, to %2"), sub, BookPeek.folderLabel(folder) or folder), { s = 13, col = INK.g4, w = w, data = true }),
        }),
        MenuUI.pill(_("Cancel"), { kind = "outline", w = w, action = { kind = "cancel" } }),
    }
end

local function doneBody(page, w)
    local r = page.result or {}
    local folder = target(page)
    local BookPeek = require("bookorbit_book_peek")
    local tw = w - px(24) - px(12)
    local lines = r.linked and _("Linked to BookOrbit, so your place and highlights sync.")
        or _("Saved, but not linked for sync yet. Open it, then use Sync now in the BookOrbit menu.")
    return {
        head(page, w),
        hstack({ w = w, gap = px(12), valign = "top" }, {
            MenuUI.check(),
            vstack({ w = tw }, {
                text(T(_("Saved to %1"), BookPeek.folderLabel(folder) or folder), { s = 16, b = true, w = tw, data = true }),
                text(lines, { s = 13, col = INK.g4, w = tw, lines = 2 }),
            }),
        }),
        DialogUI.pillRow(w, {
            { label = _("Start reading"), icon = "book-open", action = { kind = "read" } },
            { label = _("Done"), kind = "outline", action = { kind = "close" } },
        }),
    }
end

local function errorBody(page, w)
    return {
        head(page, w),
        DialogUI.warn(w, _("The download did not finish"), page.error_text),
        DialogUI.pillRow(w, {
            { label = _("Try again"), icon = "refresh-cw", action = { kind = "start" } },
            { label = _("Close"), kind = "outline", action = { kind = "close" } },
        }),
    }
end

local function build(page, sw, sh)
    local state = page.state
    if state == "choose" then
        -- Wide and short (landscape): the book and its editions beside where
        -- it goes, when one column would not fit.
        local W = BoardMetrics.layoutW()
        local one = DialogUI.sheet(page, sw, sh, function(w) return chooseBody(page, w) end, { pop_w = 500 })
        local rect = page.panel_rect
        if W < 760 or (rect.y >= px(8) and rect.y + rect.h <= sh) then return one end
        one:free()
        return DialogUI.sheet(page, sw, sh, function(w)
            local gap = px(24)
            local cw = math.floor((w - gap) / 2)
            local parts = chooseBody(page, cw, true)
            local left = vstack({ w = cw }, { head(page, cw, false), space(nil, px(14)), parts.left })
            local right_items = {}
            for _i, item in ipairs(parts.right) do
                if item then
                    if #right_items > 0 then table.insert(right_items, space(nil, px(14))) end
                    table.insert(right_items, item)
                end
            end
            local right = vstack({ w = cw }, right_items)
            local close = DialogUI.closeButton()
            local inset = MenuUI.tapInset(23)
            local h = math.max(left:getSize().h, right:getSize().h)
            return { BoardUI.overlay(w, h, {
                { widget = hstack({ w = w, gap = gap, valign = "top" }, { left, right }), x = 0, y = 0 },
                { widget = close, x = w - close:getSize().w + inset, y = -inset },
            }) }
        end, { pop_w = 880 })
    end
    local body = state == "progress" and progressBody or state == "done" and doneBody or errorBody
    return DialogUI.sheet(page, sw, sh, function(w) return body(page, w) end, { pop_w = 500 })
end

-- Hands the catalog's download the sheet to report to.
local function reporter(page)
    return {
        progress = function(received, total)
            if page.closed then return end
            page.progress = { received = received, total = total }
            page:refresh()
        end,
        finished = function(ok, err, path, linked)
            if page.closed then
                local Toast = require("bookorbit_toast")
                if ok then Toast.show(_("Downloaded")) elseif err ~= "cancelled" then Toast.error(page.catalog:errorText(err)) end
                return
            end
            if ok then
                page.state, page.result = "done", { path = path, linked = linked }
            elseif err == "cancelled" then
                return
            else
                page.state, page.error_text = "error", page.catalog:errorText(err)
                -- The file's selection expired: the next try asks for a new one.
                if err == 409 then page.needs_refresh = true end
            end
            page.dismiss_outside = true
            page:refresh()
        end,
    }
end

local function start(page)
    local catalog = page.catalog
    local e = page.editions[page.choice]
    local function go(detail, file)
        page.detail = detail
        local path = select(4, target(page))
        page.state, page.progress = "progress", nil
        page.dismiss_outside = false
        page.local_path = path
        page:refresh()
        catalog:downloadFile(catalog:getLocalDownloadPath(page.filename or CatalogUtil.safeFilenameBase(detail), string.lower(file.format or "bin"), file.devicePath, page.filename ~= nil), detail, file, reporter(page))
    end
    if not page.needs_refresh and (e.variant == nil or e.variant == e.file.downloadVariant) then
        return go(page.detail, e.file)
    end
    page.needs_refresh = nil
    -- The other form of a read-along EPUB comes from the server as its own
    -- selection, so it is asked for first.
    catalog:runConnected(function()
        local selected, err = catalog:fetchDeliveryBook(page.detail.id, e.variant or e.file.requestedVariant or e.file.downloadVariant, _("Getting that edition…"))
        if not selected then
            if err ~= "cancelled" then require("bookorbit_toast").error(catalog:errorText(err)) end
            return
        end
        for _i, candidate in ipairs(selected.files or {}) do
            if candidate.id == e.file.id then
                page.editions = DownloadSheet.editions(catalog:supportedFiles(selected))
                page.choice = DownloadSheet.indexOf(page.editions, candidate.id, e.variant)
                return go(selected, candidate)
            end
        end
        require("bookorbit_toast").error(catalog:errorText(404))
    end)
end

local function rename(page)
    local TextEntry = require("bookorbit_text_entry")
    local folder, name, ext = target(page)
    local BookPeek = require("bookorbit_book_peek")
    TextEntry.open{
        title = _("File name"),
        value = name,
        icon = "pen-line",
        below = function(_page, w)
            return {
                text(T(_("Saved as .%1 in %2"), ext, BookPeek.folderLabel(folder) or folder), { s = 13, col = INK.g4, w = w, data = true }),
                DialogUI.pillRow(w, {
                    { label = _("Cancel"), kind = "outline", action = { kind = "close" } },
                    { label = _("Save"), icon = "check", action = { kind = "submit" } },
                }),
            }
        end,
        on_submit = function(value)
            if page.closed then return end
            page.filename = value
            page:refresh()
        end,
    }
end

local function onAction(page, action)
    local kind = action.kind
    local catalog = page.catalog
    if kind == "edition" then
        page.choice = action.index
        page:refresh()
    elseif kind == "folder" then
        catalog:chooseDownloadDir(function() if not page.closed then page:refresh() end end)
    elseif kind == "rename" then
        rename(page)
    elseif kind == "start" then
        start(page)
    elseif kind == "cancel" then
        catalog:nextDownloadGeneration()
        page:close()
        require("bookorbit_toast").show(_("Download cancelled"), { icon = "x" })
    elseif kind == "read" then
        local path = page.result and page.result.path
        page:close()
        UIManager:tickAfterNext(function() catalog:openDownloadedFile(path) end)
    end
    return true
end

DownloadSheet.reporter = reporter

-- opts: file (preselected), variant, filename (a name already chosen),
-- start (begin downloading at once), progress (show a download the caller
-- has already started; report to DownloadSheet.reporter(page)).
function DownloadSheet.open(catalog, detail, opts)
    opts = opts or {}
    local editions = DownloadSheet.editions(catalog:supportedFiles(detail))
    if #editions == 0 then
        require("bookorbit_toast").error(_("No file this reader can open"))
        return nil
    end
    local first = opts.file or catalog:nextDownloadFile(catalog:supportedFiles(detail))
    local page = DialogUI.open{
        name = "bookorbit_download_sheet",
        build = build,
        on_action = onAction,
        catalog = catalog,
        detail = detail,
        editions = editions,
        choice = DownloadSheet.indexOf(editions, first and first.id, opts.variant or (first and first.downloadVariant)),
        filename = opts.filename,
        state = opts.progress and "progress" or "choose",
        dismiss_outside = not opts.progress,
    }
    if opts.start then start(page) end
    return page
end

return DownloadSheet