fix(series): add KOReader series sorting

Expose series-specific sorting controls and gate publication ordering on server capability support.

Closes #2015
This commit is contained in:
neon
2026-10-31 02:10:36 +00:00
parent 14caaa58b9
commit ee2974459a
16 changed files with 206 additions and 24 deletions
@@ -1109,7 +1109,7 @@ function BookOrbitCatalog:titleForSection(section)
end
function BookOrbitCatalog:paramsForEntry(section, entry)
local params = { sort = "title" }
local params = { sort = section == "series" and "series" or "title" }
if section == "libraries" then
params.libraryId = tonumber(entry.id)
elseif section == "collections" then
@@ -54,11 +54,11 @@ end
function CatalogDetail.detailSeriesParams(detail)
if not detail then return nil end
if detail.seriesId then
return { seriesId = tonumber(detail.seriesId), sort = "title" }
return { seriesId = tonumber(detail.seriesId), sort = "series" }
end
local name = cleanInlineText(detail.seriesName)
if not name then return nil end
return { series = name, sort = "title" }
return { series = name, sort = "series" }
end
function CatalogDetail.detailSeriesText(detail)
@@ -34,6 +34,7 @@ local cloneParams = CatalogUtil.cloneParams
local CatalogLists = {}
CatalogLists.CAPABILITY = "catalogListsV2"
CatalogLists.PUBLISHED_SORT_CAPABILITY = "catalogPublishedSort"
local DUMP_DIR = os.getenv("BO_LIST_DUMP")
local SECTION_PAGED = { authors = true, series = true }
@@ -508,8 +509,32 @@ end
-- ----------------------------------------------------------------- sheets
local SORT_IDS = { "title", "author", "series", "recently_added", "recently_read" }
local SERIES_SORTS = {
{ id = "series", label = _("Series order") },
{ id = "published", label = _("Publication date"), capability = CatalogLists.PUBLISHED_SORT_CAPABILITY },
{ id = "title", label = _("Title") },
{ id = "recently_read", label = _("Last read") },
{ id = "recently_added", label = _("Added to library") },
}
local SHOW_IDS = { false, "unread", "reading", "finished" }
local function seriesSorts(self)
local known = Capabilities.cached(self.client) or {}
local sorts = {}
for _i, sort in ipairs(SERIES_SORTS) do
if not sort.capability or known[sort.capability] == true then table.insert(sorts, sort) end
end
return sorts
end
local function seriesOrder(sort)
if sort == "series" then return { _("First to last"), _("Last to first") }, "asc" end
if sort == "published" then return { _("Oldest"), _("Newest") }, "asc" end
if sort == "title" then return { _("A–Z"), _("Z–A") }, "asc" end
if sort == "recently_read" then return { _("Recent first"), _("Oldest first") }, "desc" end
return { _("Newest"), _("Oldest") }, "desc"
end
function CatalogLists:listSheetState(context)
context = context or self.current_context or {}
local sheet = context.list_sheet
@@ -522,15 +547,20 @@ function CatalogLists:listOpenSheet(kind, focus)
local list_kind = self:listKind(context)
if kind == "sort" then
local staged = {}
if list_kind == "books" or list_kind == "reading" then
if list_kind == "books" or list_kind == "reading" or list_kind == "seriespage" then
local params = context.params or {}
local sort = params.sort or (list_kind == "seriespage" and "series" or "title")
if list_kind == "seriespage" and sort == "published" then
local known = Capabilities.cached(self.client) or {}
if known[CatalogLists.PUBLISHED_SORT_CAPABILITY] ~= true then sort = "series" end
end
staged = {
sort = params.sort or "title",
order = self:effectiveOrder(params),
sort = sort,
order = params.order or CatalogUtil.NATURAL_ORDER[sort] or "asc",
readStatus = params.readStatus,
libraryId = params.libraryId,
format = params.format,
on_device = context.on_device_filter == true,
libraryId = list_kind ~= "seriespage" and params.libraryId or nil,
format = list_kind ~= "seriespage" and params.format or nil,
on_device = list_kind ~= "seriespage" and context.on_device_filter == true,
}
else
staged = { sort = context.sort or "name", filter = context.filter }
@@ -595,6 +625,16 @@ function CatalogLists:listApplySheet()
end)
return
end
if kind == "seriespage" then
local params = cloneParams(context.params or {})
params.sort = s.sort
params.order = s.order ~= (CatalogUtil.NATURAL_ORDER[s.sort] or "asc") and s.order or nil
params.readStatus = s.readStatus or nil
params.page = 1
params.size = nil
self:runConnected(function() self:fetchBookPage(params, context.title, false) end)
return
end
if kind == "authors" or kind == "series" then
self:reloadSection(context.section, { page = 1, sort = s.sort, filter = s.filter or false })
end
@@ -606,6 +646,8 @@ function CatalogLists:listResetSheet()
local kind = self:listKind()
if kind == "books" or kind == "reading" then
sheet.staged = { sort = "title", order = "asc", on_device = false }
elseif kind == "seriespage" then
sheet.staged = { sort = "series", order = "asc" }
else
sheet.staged = { sort = "name" }
end
@@ -617,6 +659,25 @@ end
function CatalogLists:listSortSheetModel(sheet)
local s = sheet.staged
local kind = self:listKind()
if kind == "seriespage" then
local sorts = seriesSorts(self)
local labels, sort_index = {}, 1
for i, sort in ipairs(sorts) do
labels[i] = sort.label
if sort.id == s.sort then sort_index = i end
end
local order_labels, first_order = seriesOrder(s.sort)
local show_index = 1
for i, id in ipairs(SHOW_IDS) do if (id or nil) == s.readStatus then show_index = i end end
return {
title = _("Sort and filter"),
sorts = { labels = labels, selected = sort_index },
order = { labels = order_labels, selected = (s.order or first_order) == first_order and 1 or 2 },
shows = { labels = { _("All"), _("Unread"), _("Reading"), _("Finished") }, selected = show_index },
rows = {},
note = _("Saved for this list"),
}
end
if kind == "books" or kind == "reading" then
local sort_index = 1
for i, id in ipairs(SORT_IDS) do if id == s.sort then sort_index = i end end
@@ -705,6 +766,10 @@ function CatalogLists:listRun(action, region, hold)
if lk == "books" or lk == "reading" then
sheet.staged.sort = SORT_IDS[action.index] or "title"
sheet.staged.order = CatalogUtil.NATURAL_ORDER[sheet.staged.sort] or "asc"
elseif lk == "seriespage" then
local sort = seriesSorts(self)[action.index] or seriesSorts(self)[1]
sheet.staged.sort = sort and sort.id or "series"
sheet.staged.order = CatalogUtil.NATURAL_ORDER[sheet.staged.sort] or "asc"
else
sheet.staged.sort = action.index == 2 and "books" or "name"
end
@@ -715,13 +780,16 @@ function CatalogLists:listRun(action, region, hold)
local time_sort = CatalogUtil.SORT_KIND[sheet.staged.sort] == "time"
local first = time_sort and "desc" or "asc"
sheet.staged.order = action.index == 1 and first or (first == "asc" and "desc" or "asc")
elseif lk == "seriespage" then
local _labels, first = seriesOrder(sheet.staged.sort)
sheet.staged.order = action.index == 1 and first or (first == "asc" and "desc" or "asc")
else
sheet.staged.sort = action.index == 2 and "name_desc" or "name"
end
return self:updateItems(nil, true)
elseif kind == "sheet-show" and sheet then
local lk = self:listKind()
if lk == "books" or lk == "reading" then
if lk == "books" or lk == "reading" or lk == "seriespage" then
sheet.staged.readStatus = SHOW_IDS[action.index] or nil
else
sheet.staged.filter = action.index == 2 and "reading" or action.index == 3 and "finished" or nil
@@ -1027,17 +1095,19 @@ end
local SORT_SHORT = {
title = _("Title A–Z"), author = _("Author A–Z"), series = _("Series order"), recently_added = _("Recently added"),
recently_updated = _("Recently updated"), recently_read = _("Last read"),
published = _("Publication date"), recently_updated = _("Recently updated"), recently_read = _("Last read"),
}
function CatalogLists:listSortLabel(params)
local sort = params.sort or "title"
local order = self:effectiveOrder(params)
local order = params.order or CatalogUtil.NATURAL_ORDER[sort] or "asc"
local label = SORT_SHORT[sort] or _("Title A–Z")
if CatalogUtil.SORT_KIND[sort] == "alpha" and order == "desc" then
label = sort == "title" and _("Title Z–A") or sort == "author" and _("Author Z–A") or label
label = sort == "title" and _("Title Z–A") or sort == "author" and _("Author Z–A") or sort == "series" and _("Reverse series order") or label
elseif CatalogUtil.SORT_KIND[sort] == "time" and order == "asc" then
label = label .. " " .. _("(oldest)")
label = sort == "published" and _("Publication date (oldest)") or label .. " " .. _("(oldest)")
elseif sort == "published" then
label = _("Publication date (newest)")
end
return label
end
@@ -1060,6 +1130,15 @@ function CatalogLists:listToolbar(kind, context)
{ icon = "library", label = lib_label, on = params.libraryId ~= nil, action = { kind = "sort-sheet", focus = "library" }, key = "tb-library" },
}
end
if kind == "seriespage" then
if not self:listsSupported() then return {} end
local show = params.readStatus
local show_label = show == "unread" and _("Unread") or show == "reading" and _("Reading") or show == "finished" and _("Finished") or _("All books")
return {
{ icon = "arrow-up-down", label = self:listSortLabel(params), action = { kind = "sort-sheet", focus = "sort" }, key = "tb-sort" },
{ icon = "list-filter", label = show_label, on = show ~= nil, action = { kind = "sort-sheet", focus = "show" }, key = "tb-show" },
}
end
if not self:listsSupported() then return {} end
local sort = context.sort or "name"
local sort_label = sort == "books" and _("Most books") or sort == "name_desc" and _("Name Z–A") or _("Name A–Z")
@@ -1212,7 +1291,8 @@ function CatalogLists:listBody(kind, context)
if not next_book then for i, b in ipairs(books) do if states[i] ~= "finished" then next_book, cur = b, i; break end end end
local finished, reading = 0, 0
for _i, s in ipairs(states) do if s == "finished" then finished = finished + 1 elseif s == "reading" then reading = reading + 1 end end
local layout = ListPages.seriesLayout(sw, sh, self:listView(kind), #books, context.list_offline == true)
local toolbar = context.list_offline ~= true
local layout = ListPages.seriesLayout(sw, sh, self:listView(kind), #books, context.list_offline == true, toolbar)
context.local_pages = layout.pages
context.local_page = math.min(context.local_page or 1, layout.pages)
local lp = context.local_page
@@ -1220,10 +1300,10 @@ function CatalogLists:listBody(kind, context)
local last = math.min(#books, lp == 1 and layout.first or (first + layout.rest - 1))
local m = {
entries = books, view = self:listView(kind), screen_w = sw, screen_h = sh, local_page = context.local_page,
offline = context.list_offline and {} or nil,
offline = context.list_offline and {} or nil, toolbar = toolbar,
select = self:isBulkSelectionActive(),
selected = function(b) return self.bulk_select_all or self:bulkIsBookSelected(b) end,
order_label = (context.params or {}).sort == "title" and _("Title A–Z") or _("Reading order"),
order_label = self:listSortLabel(context.params or {}),
states = states, indexes = indexes, current_index = cur, next_book = next_book,
range = #books > 0 and T(_("Books %1 – %2"), first, last) or nil,
next_minutes = next_book and self:listMinutesLeft(next_book),
@@ -1289,7 +1369,8 @@ function CatalogLists:updateListPage(select_number, no_recalculate_dimen)
local body, m = self:listBody(kind, context)
-- Offline the band stands in for the toolbar: sorting, filtering and the
-- view all need the server.
local has_toolbar = (kind == "books" or kind == "reading" or kind == "authors" or kind == "series") and not context.list_offline
local has_toolbar = (kind == "books" or kind == "reading" or kind == "authors" or kind == "series" or kind == "seriespage")
and not context.list_offline
local toolbar = nil
if has_toolbar then
local buttons = self:listToolbar(kind, context)
@@ -42,6 +42,7 @@ CatalogUtil.SORTS = {
CatalogUtil.SORT_LABELS = {
title = _("Title"),
author = _("Author"),
published = _("Publication date"),
recently_added = _("Recently added"),
recently_updated = _("Recently updated"),
recently_read = _("Recently read"),
@@ -51,6 +52,7 @@ CatalogUtil.SORT_LABELS = {
CatalogUtil.NATURAL_ORDER = {
title = "asc",
author = "asc",
published = "asc",
series = "asc",
recently_added = "desc",
recently_updated = "desc",
@@ -60,6 +62,7 @@ CatalogUtil.NATURAL_ORDER = {
CatalogUtil.SORT_KIND = {
title = "alpha",
author = "alpha",
published = "time",
series = "alpha",
recently_added = "time",
recently_updated = "time",
@@ -121,8 +121,8 @@ end
-- with the list alone (landscape keeps the summary column on every page).
-- A grid shrinks its covers to keep a series on one page only down to three
-- quarters of the All books size; past that it pages at full size.
function Pages.seriesLayout(screen_w, screen_h, view, n, offline)
local f = ListView.frame(screen_w, screen_h, { toolbar = false, offline = offline })
function Pages.seriesLayout(screen_w, screen_h, view, n, offline, toolbar)
local f = ListView.frame(screen_w, screen_h, { toolbar = toolbar == true, offline = offline })
local side = BoardMetrics.get().w > BoardMetrics.get().h
local side_w = side and math.floor(f.w * 0.38 + 0.5) or f.w
local list_w = side and (f.w - side_w - px(28)) or f.w
@@ -155,7 +155,7 @@ end
-- puts the summary beside the books.
function Pages.seriesBody(m, f)
local all_books = m.entries
local layout = Pages.seriesLayout(m.screen_w, m.screen_h, m.view, #all_books, m.offline ~= nil)
local layout = Pages.seriesLayout(m.screen_w, m.screen_h, m.view, #all_books, m.offline ~= nil, m.toolbar == true)
local page = math.max(1, math.min(layout.pages, m.local_page or 1))
local books = {}
local start = page == 1 and 1 or (layout.first + (page - 2) * layout.rest + 1)