mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 08:21:39 -06:00
e28f45452d
Layout breakpoints use the screen width at 1x, so Interface size scales text, icons and spacing without dropping Home columns or detail panes. Every page, sheet and widget picks a layout that fits instead of cutting text or leaving gaps; search becomes balanced list-row sections, Edit Home keeps hidden widgets, stock KOReader messages become BookOrbit toasts, and the plugin is 2.1.0.
368 lines
16 KiB
Lua
368 lines
16 KiB
Lua
--[[--
|
|
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
|