Files
bookorbit/koreader-plugin/bookorbit.koplugin/bookorbit_book_peek.lua
T
neon 80244ea4e2 feat(koreader): rebuild the plugin interface and fix four-way annotation sync
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.
2026-10-20 16:05:27 -06:00

180 lines
7.7 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
--[[--
The sheet a long-press on a book opens, anywhere in the catalog.
The book's cover, where it stands and whether it is on this device, then the
two things a long-press is for (Read or Download, and Details), then About,
Genres & tags and either Download options or Remove from this device. Book
info is not a separate door: Details opens the detail page, whose second page
holds those facts.
]]
local UIManager = require("ui/uimanager")
local T = require("ffi/util").template
local _ = require("gettext")
local BoardMetrics = require("bookorbit_board_metrics")
local CatalogUtil = require("bookorbit_catalog_util")
local DetailData = require("bookorbit_detail_data")
local DialogUI = require("bookorbit_dialog_ui")
local MenuUI = require("bookorbit_menu_ui")
local INK = BoardMetrics.INK
local BookPeek = {}
-- "Books › Standalone": the last two folders of where a download lands.
function BookPeek.folderLabel(path)
if type(path) ~= "string" or path == "" then return nil end
local parts = {}
for part in path:gmatch("[^/]+") do table.insert(parts, part) end
if #parts == 0 then return "/" end
return table.concat(parts, " › ", math.max(1, #parts - 1))
end
-- What the page knows about the book, as plain data the sheet draws.
function BookPeek.data(catalog, detail, opts)
opts = opts or {}
catalog:refreshOnDevice()
local supported = catalog:supportedFiles(detail)
local read_path = catalog:detailReadPath(detail)
local file = #supported > 0 and catalog:nextDownloadFile(supported) or nil
local authors = {}
for _i, a in ipairs(detail.authors or {}) do
local name = CatalogUtil.cleanInlineText(a)
if name then table.insert(authors, name) end
end
local description = DetailData.cleanDescription(detail.description)
local status = detail.readStatus or "unread"
local pct = tonumber(detail.progressPercentage)
local d = {
detail = detail,
title = detail.title or _("Untitled"),
author = table.concat(authors, ", "),
cover_path = catalog:cachedThumbnailPath(detail),
read_path = read_path,
on_device = read_path ~= nil,
supported = supported,
file = file,
about = description and (description:match("^[^\n]+") or description) or nil,
chips = DetailData.chips(detail),
status = status,
status_label = DetailData.statusLabel(status),
pct = (status == "reading" or status == "rereading") and pct and pct > 0 and pct or nil,
allow_select = opts.allow_select == true,
selected = opts.allow_select and catalog.bulkIsBookSelected and catalog:bulkIsBookSelected(detail) or false,
}
if file and not read_path then
local folder = catalog:getLocalDownloadPreview(CatalogUtil.safeFilenameBase(detail), string.lower(file.format or "bin"), file.devicePath, false)
local size = CatalogUtil.formatBytes(catalog:fileDisplaySize(file))
d.file_note = T(_("%1, to %2"), catalog:fileFormatLabel(file) .. (size ~= "" and (", " .. size) or ""), BookPeek.folderLabel(folder) or folder)
end
return d
end
local function body(page, w)
local d = page.peek
local chips = {}
if d.pct then
table.insert(chips, MenuUI.chip(T(_("%1 · %2%"), d.status_label, math.floor(d.pct + 0.5)), { icon = "book-open" }))
elseif d.status ~= "unread" then
table.insert(chips, MenuUI.chip(d.status_label, { icon = d.status == "read" and "book-check" or "bookmark" }))
end
table.insert(chips, d.on_device and MenuUI.chip(_("On this device"), { icon = "bo-device" }) or MenuUI.chip(_("Not on this device"), { icon = "cloud" }))
local items = {
DialogUI.bookHead(w, { title = d.title, author = d.author }, { cover_path = d.cover_path, chips = chips }),
}
local pills = {}
if d.on_device then
table.insert(pills, { label = _("Read"), icon = "book-open", action = { kind = "read" } })
elseif d.file then
table.insert(pills, { label = _("Download"), icon = "download", action = { kind = "download" } })
end
table.insert(pills, { label = _("Details"), icon = "book-open-text", kind = "outline", action = { kind = "details" } })
table.insert(items, DialogUI.pillRow(w, pills))
if d.file_note then
table.insert(items, DialogUI.note(w, "file-text", d.file_note, { col = INK.g4, data = true, lines = 1 }))
end
local iw = MenuUI.rowsIn(w)
local rows = {}
if d.about then
table.insert(rows, MenuUI.row(iw, { icon = "file-text", label = _("About this book"), sub = d.about, sub_data = true, chevron = true, action = { kind = "about" } }))
end
if #d.chips > 0 then
table.insert(rows, MenuUI.row(iw, { icon = "tag", label = _("Genres & tags"), sub = table.concat(d.chips, ", "), sub_data = true, chevron = true, action = { kind = "tags" } }))
end
if d.on_device then
table.insert(rows, MenuUI.row(iw, { icon = "trash", label = _("Remove from this device"), sub = _("Stays in your library"), chevron = true, action = { kind = "remove" } }))
elseif #d.supported > 0 then
table.insert(rows, MenuUI.row(iw, { icon = "folder-open", label = _("Download options"), sub = _("Edition, folder and file name"), chevron = true, action = { kind = "download-options" } }))
end
if d.allow_select then
table.insert(rows, MenuUI.row(iw, { icon = "list-checks", label = d.selected and _("Unselect") or _("Select"), sub = _("Pick books to download or mark read together"), chevron = true, action = { kind = "select" } }))
end
if #rows > 0 then table.insert(items, MenuUI.group(w, rows)) end
return items
end
local function build(page, sw, sh)
return DialogUI.sheet(page, sw, sh, function(w) return body(page, w) end, { pop_w = 460 })
end
local function onAction(page, action)
local catalog, d = page.catalog, page.peek
local detail = d.detail
local kind = action.kind
page:close()
-- The sheet's backdrop is restored first, so the next page or sheet
-- copies a clean screen.
UIManager:tickAfterNext(function()
if kind == "read" then
catalog:openDownloadedFile(d.read_path)
elseif kind == "download" then
catalog:downloadDefaultFile(detail, d.file)
elseif kind == "details" then
catalog:showBookDetail(detail)
elseif kind == "about" then
catalog:showFullText(_("About this book"), detail.description, detail)
elseif kind == "tags" then
catalog:showGenresAndTags(detail)
elseif kind == "download-options" then
catalog:showDownloadOptions(detail)
elseif kind == "remove" then
BookPeek.remove(catalog, d.read_path)
elseif kind == "select" then
if catalog:isBulkSelectionActive() then
catalog:bulkToggleBook(detail)
else
catalog:bulkEnterSelectionMode({ kind = "book", book = detail })
end
end
end)
return true
end
-- Deletes the file and its sidecar through KOReader's file manager, which
-- asks first. The book stays in the library.
function BookPeek.remove(catalog, path)
local fm = require("apps/filemanager/filemanager").instance
if not fm or not fm.showDeleteFileDialog then
require("bookorbit_toast").error(_("Open the file browser to remove this book."))
return
end
fm:showDeleteFileDialog(path, function()
catalog:refreshOnDevice()
catalog:markStackDirty()
catalog:updateItems(nil, true)
end)
end
function BookPeek.open(catalog, detail, opts)
return DialogUI.open{
name = "bookorbit_book_peek",
build = build,
on_action = onAction,
catalog = catalog,
peek = BookPeek.data(catalog, detail, opts),
}
end
return BookPeek