koreader-plugin/bookorbit.koplugin/bookorbit_status_page.lua
Rama main · réplica de lectura del repositorio original.
--[[--
Sync status: the diagnostics list rewritten as an answer.
A verdict first (all synced, waiting, offline, or the last failure), three
numbers, what happened recently, then the fixes. The old key-value list stays
one tap down as Technical details.
]]
local NetworkMgr = require("ui/network/manager")
local UIManager = require("ui/uimanager")
local _ = require("gettext")
local T = require("ffi/util").template
local Activity = require("bookorbit_activity")
local BoardMetrics = require("bookorbit_board_metrics")
local BoardUI = require("bookorbit_board_ui")
local MenuPage = require("bookorbit_menu_page")
local MenuUI = require("bookorbit_menu_ui")
local INK = BoardMetrics.INK
local px = BoardMetrics.px
local text, vstack, hstack, flex, space, icon = BoardUI.text, BoardUI.vstack, BoardUI.hstack, BoardUI.flex, BoardUI.space, BoardUI.icon
local Status = {}
function Status.data(plugin)
local settings = plugin.settings
local summary = require("bookorbit_sweep").syncStatus() or {}
local outbox = plugin.getLifecycleOutboxStatus and plugin:getLifecycleOutboxStatus() or { count = 0 }
local coordinator = plugin.getSyncCoordinatorStatus and plugin:getSyncCoordinatorStatus() or { pending_count = 0 }
local last_sync = type(settings.last_sync) == "table" and settings.last_sync or {}
local last_error = type(settings.last_error) == "table" and settings.last_error or nil
local d = {
online = NetworkMgr:isConnected(),
matched = summary.matched or 0,
unmatched = summary.unmatched or 0,
waiting = (outbox.count or 0) + (coordinator.pending_count or 0),
synced = last_sync.event ~= "connection_test" and MenuUI.ago(last_sync.at) or nil,
last_message = last_sync.event ~= "connection_test" and last_sync.message or nil,
user = settings.username or "",
host = MenuUI.serverHost(settings.server_url) or "",
activity = Activity.list(settings),
}
-- A failure newer than the last success is the verdict until it clears.
if last_error and last_error.event ~= "connection_test"
and (tonumber(last_error.at) or 0) > (last_sync.event ~= "connection_test" and tonumber(last_sync.at) or 0) then
d.error = { message = last_error.message, when = MenuUI.ago(last_error.at) }
end
return d
end
local function hero(d, w, sync)
local pad = px(18)
local iw = w - 2 * pad
local glyph, title, sub, inverted
if sync and sync.running then
glyph, inverted = "refresh-cw", true
title = sync.recheck and _("Rechecking books…") or _("Syncing…")
sub = sync.text
elseif not d.online then
glyph, inverted = "wifi-off", true
title = d.waiting > 0 and T(_("Offline: %1 changes waiting"), d.waiting) or _("Offline")
sub = _("Kept on this device and sent when Wi-Fi is back")
elseif d.error then
glyph, inverted = "triangle-alert", true
title = _("The last sync did not finish")
sub = d.error.when and T(_("%1, %2"), d.error.message or _("unknown error"), d.error.when) or d.error.message
elseif d.waiting > 0 then
glyph, inverted = "clock", true
title = T(_("%1 changes waiting to send"), d.waiting)
sub = _("They go out with the next sync")
elseif not d.synced then
glyph, inverted = "refresh-cw", true
title = _("Not synced yet")
sub = _("Sync now to match the books on this device with your library")
else
glyph = "check"
title = _("Everything is synced")
sub = d.last_message and T(_("Last sync %1, %2"), d.synced, d.last_message) or T(_("Last sync %1"), d.synced)
end
local disc = inverted and BoardUI.circle(px(52), { border = math.max(2, px(2)), bg = INK.white }, icon(glyph, px(26), { sw = 2.75 }))
or BoardUI.circle(px(52), { bg = INK.black }, icon(glyph, px(26), { col = "white", sw = 2.75 }))
local tw = iw - px(52) - px(16)
return MenuUI.card(w, {
hstack({ w = iw, gap = px(16) }, {
disc,
vstack({ w = tw }, {
text(title, { s = 20, b = true, w = tw }),
text(sub or "", { s = 13, col = INK.g4, w = tw, lines = 2, data = true }),
}),
}),
}, { pad = 18, vpad = 18, strong = true })
end
local function statTile(w, value, label, action, label_s)
local pad = px(14)
local iw = w - 2 * pad
local head = action and hstack({ gap = px(6) }, { text(tostring(value), { s = 26, b = true }), icon("chevron-right", px(16), { sw = 2.5 }) })
or text(tostring(value), { s = 26, b = true })
local card = MenuUI.card(w, { vstack({ w = iw }, { head, space(nil, px(2)), text(label, { s = label_s or 13, col = INK.g4, lines = 2, w = iw }) }) }, { pad = 14, vpad = 12 })
return action and BoardUI.hit(card, action) or card
end
local function stats(d, w)
local gap = px(10)
local tw = math.floor((w - 2 * gap) / 3)
local tiles = {
{ tw, d.matched, _("books matched") },
{ w - 2 * tw - 2 * gap, d.unmatched, _("not in BookOrbit"), d.unmatched > 0 and { kind = "unmatched" } or nil },
{ tw, d.waiting, _("waiting to send") },
}
-- One caption size for the row, stepped down until every caption fits
-- its two lines.
local linesNeeded = require("bookorbit_text_pages").linesNeeded
local label_s = 13
while label_s > 11 do
local fits = true
for _i, t in ipairs(tiles) do
if linesNeeded(t[3], { s = label_s }, t[1] - 2 * px(14)) > 2 then fits = false end
end
if fits then break end
label_s = label_s - 1
end
local cells = {}
for _i, t in ipairs(tiles) do table.insert(cells, statTile(t[1], t[2], t[3], t[4], label_s)) end
return hstack({ w = w, gap = gap, valign = "top" }, cells)
end
-- As many recent events as fit in max_h.
local function activityList(d, w, max_h)
local iw = MenuUI.rowsIn(w)
local tw = px(78)
local rows = {}
local used = 0
for _i, a in ipairs(d.activity) do
local cw = iw - tw - px(10) - (a.warn and px(30) or 0)
local r = hstack({ w = iw, gap = px(10) }, {
text(MenuUI.ago(a.at) or "", { s = 13, col = INK.g4, w = tw }),
vstack({ w = cw, pad = { px(8), 0, px(8), 0 } }, {
text(a.text, { s = 15, w = cw, lines = 2, data = true }),
a.sub and text(a.sub, { s = 13, col = INK.g4, w = cw, lines = 2, data = true }) or false,
}),
a.warn and icon("triangle-alert", px(20), { sw = 2 }) or false,
})
local h = math.max(px(60), r:getSize().h)
r = hstack({ w = iw, h = h }, { r, flex(0) })
r._no_icon = true
if max_h and used + h + MenuUI.hair() > max_h and #rows > 0 then break end
used = used + h + MenuUI.hair()
table.insert(rows, r)
end
if #rows == 0 then
local empty = hstack({ w = iw, h = px(60) }, { text(_("Sync events will show here."), { s = 14, col = INK.g4, w = iw }) })
empty._no_icon = true
table.insert(rows, empty)
end
return MenuUI.group(w, rows, { inset = 0 })
end
-- Side by side, or one above the other in a narrow column. While a sync
-- runs, the first one stops it; each says how its last run went.
local function actions(d, w, stacked, sync, test)
local half = stacked and w or math.floor((w - px(10)) / 2)
local first
if sync and sync.running then
first = MenuUI.pill(_("Stop syncing"), { kind = "outline", action = { kind = "sync-stop" }, w = half })
elseif sync and sync.done and not sync.recheck then
first = MenuUI.pill(_("Synced"), { icon = "check", action = { kind = "sync" }, w = half })
else
first = MenuUI.pill(d.online and _("Sync now") or _("Connect and sync"), { icon = d.online and "refresh-cw" or "wifi", action = { kind = d.online and "sync" or "connect" }, w = half })
end
local second_w = stacked and w or (w - half - px(10))
local second
if test and test.running then
second = MenuUI.pill(_("Testing…"), { kind = "outline", action = { kind = "noop" }, w = second_w })
elseif test and test.ok then
second = MenuUI.pill(_("Connection works"), { kind = "outline", icon = "check", action = { kind = "test" }, w = second_w })
elseif test and test.err then
second = MenuUI.pill(_("Connection failed"), { kind = "outline", icon = "triangle-alert", action = { kind = "test" }, w = second_w })
else
second = MenuUI.pill(_("Test connection"), { kind = "outline", action = { kind = "test" }, w = second_w })
end
if stacked then return vstack({ w = w }, { first, space(nil, px(10)), second }) end
return hstack({ w = w, gap = px(10) }, { first, second })
end
local function fixes(w, compact, sync)
local iw = MenuUI.rowsIn(w)
local recheck_sub = not compact and _("Matches every book on this device again") or nil
if sync and sync.recheck then
if sync.running then recheck_sub = sync.text
elseif sync.done then recheck_sub = _("Done: every book was checked again")
elseif sync.err then recheck_sub = T(_("Could not recheck: %1"), sync.err) end
end
return MenuUI.group(w, {
MenuUI.row(iw, { icon = "list-checks", label = _("Recheck all books"), sub = recheck_sub, sub_data = sync and sync.running,
chevron = not (sync and sync.running), action = (sync and sync.running) and { kind = "noop" } or { kind = "recheck" } }),
MenuUI.row(iw, { icon = "file-text", label = _("Technical details"), sub = not compact and _("Everything the sync knows, for bug reports") or nil, chevron = true, action = { kind = "diagnostics" } }),
})
end
local function build(page, sw, sh)
local d = page.status_data
local mx = MenuUI.mx()
local top, bottom = px(6), px(12)
local w = sw - 2 * mx
local subtitle = not page.plugin:isLoggedIn() and _("Not signed in")
or d.host == "" and d.user or T(_("%1 on %2"), d.user, d.host)
local header = MenuUI.pageHeader(w, _("Sync status"), { sub = subtitle, sub_data = true })
if MenuUI.landscape() then
local gutter = px(24)
local lw = math.floor((w - gutter) / 2)
local rw = w - lw - gutter
local h_hero = hero(d, w, page.sync)
local room = sh - top - bottom - header:getSize().h - px(8) - h_hero:getSize().h - px(12)
-- The first arrangement whose left column fits a short screen with
-- nothing cut: the buttons stacked, then side by side, then with the
-- fixes moved under the recent activity, stacked and side by side.
local function left(stacked, with_fixes)
return vstack({ w = lw }, {
stats(d, lw), space(nil, px(10)), actions(d, lw, stacked, page.sync, page.test),
with_fixes and space(nil, px(10)) or false, with_fixes and fixes(lw, true, page.sync) or false,
})
end
local choice
for _i, v in ipairs({ { true, true }, { false, true }, { true, false }, { false, false } }) do
local clean, probe = BoardUI.fits(function() return BoardUI.collectHits(function() return left(v[1], v[2]) end) end)
local h = probe and probe.getSize and probe:getSize().h or math.huge
if probe and probe.free then probe:free() end
if clean and h <= room then choice = v; break end
end
choice = choice or { true, false }
local right_fixes = not choice[2] and fixes(rw, true, page.sync) or nil
local list_room = room - BoardMetrics.lh1(13) - px(8) - (right_fixes and (right_fixes:getSize().h + px(10)) or 0)
local right = vstack({ w = rw }, {
MenuUI.label(rw, _("Recent activity")), space(nil, px(8)), activityList(d, rw, list_room),
right_fixes and space(nil, px(10)) or false, right_fixes or false,
})
return vstack({ w = sw, h = sh, pad = { top, mx, bottom, mx } }, {
header, space(nil, px(8)), h_hero, space(nil, px(12)),
hstack({ w = w, gap = gutter, valign = "top" }, { left(choice[1], choice[2]), right }),
})
end
local fixed = { header, space(nil, px(8)), hero(d, w, page.sync), space(nil, px(10)), stats(d, w), space(nil, px(16)) }
local tail = { space(nil, px(16)), actions(d, w, false, page.sync, page.test), space(nil, px(16)), fixes(w, sh / BoardMetrics.get().dp < 1000, page.sync) }
local used = 0
for _i, item in ipairs(fixed) do used = used + item:getSize().h end
for _i, item in ipairs(tail) do used = used + item:getSize().h end
local avail = sh - top - bottom - used - BoardMetrics.lh1(13) - px(8)
local items = fixed
table.insert(items, MenuUI.section(w, _("Recent activity"), activityList(d, w, avail)))
for _i, item in ipairs(tail) do table.insert(items, item) end
return vstack({ w = sw, h = sh, pad = { top, mx, bottom, mx } }, items)
end
local function onAction(page, action)
local plugin = page.plugin
local kind = action.kind
-- The page stays open for all three: its verdict follows the run.
local refresh = function(reload)
if reload then page.status_data = Status.data(plugin) end
page:refresh()
end
if kind == "sync" or kind == "connect" then
local start = function() plugin:startPageSync(page, refresh) end
if kind == "connect" then NetworkMgr:runWhenConnected(start) else start() end
elseif kind == "sync-stop" then
plugin:confirmStopSync()
elseif kind == "test" then
plugin:startPageTest(page, refresh)
elseif kind == "recheck" then
plugin:startPageSync(page, refresh, { full_recheck = true })
elseif kind == "diagnostics" then
plugin:showDiagnostics()
elseif kind == "unmatched" then
require("bookorbit_dialog_ui").notice{
title = _("Books not in BookOrbit"),
text = _("Some books on this device are not in your BookOrbit library: files copied by hand, or edited since they were downloaded. Recheck all books to try matching them again."),
}
end
return true
end
function Status.open(plugin)
local page = MenuPage:new{
name = "bookorbit_sync_status",
build = build,
on_action = onAction,
plugin = plugin,
status_data = Status.data(plugin),
}
UIManager:show(page, "ui")
return page
end
return Status