koreader-plugin/bookorbit.koplugin/bookorbit_download_sheet.lua
Rama main · réplica de lectura del repositorio original.
--[[--
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