mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
feat(kobo): add KOReader catalog plugin integration
Add the KOReader catalog surfaces, plugin package metadata, and device progress handling needed for the plugin workflow. Keep progress status updates non-blocking so secondary status failures do not reject successful sync writes.
This commit is contained in:
+15
-11
@@ -2,7 +2,7 @@
|
||||
|
||||
Syncs your KOReader reading life into BookOrbit:
|
||||
|
||||
- Native BookOrbit catalog browsing from KOReader: "Continue reading", recently added books, libraries, collections, SmartScopes, authors, series, all books, "On device" and scoped or global search. Browse as a cover mosaic or a detailed list, with read-status, reading-progress bars and on-device indicators, read-status and format filters, a configurable grid, persisted view/sort/grid, recent-search history, loading feedback, download progress and inline retry on failures. Navigation runs off the UI thread (cancellable, no freeze) and the next page's covers are prefetched. The detail page shows genres, tags, page count and an expandable full description, and can set read status and star rating directly (synced back like any other status/rating change). Series open as an ordered read-through view with a "read N/total" header. Authors and series lists are paginated with a filter-within box. Downloads are linked to BookOrbit sync after the file is saved, and books already on the device offer a "Read" action that opens the local file. The thumbnail cache is bounded and evicts the oldest covers.
|
||||
- Native BookOrbit catalog browsing from KOReader: a reading-first dashboard with compact Continue reading rows, goal/streak/highlight summaries, "On device", libraries, collections, SmartScopes, authors, series, all books and scoped or global search. The dashboard can auto-open on startup and falls back to its last cached snapshot when offline. Browse books as a cover mosaic or a detailed list, with read-status, reading-progress bars and on-device indicators, read-status and format filters, a configurable grid, persisted view/sort/grid, recent-search history, loading feedback, download progress and inline retry on failures. Navigation runs off the UI thread (cancellable, no freeze) and the next page's covers are prefetched. The detail page shows genres, tags, page count and an expandable full description, and can set read status and star rating directly (synced back like any other status/rating change). Series open as an ordered read-through view with a "read N/total" header. Authors and series lists are paginated with footer arrows and filtered from the catalog action menu. Downloads are linked to BookOrbit sync after the file is saved, and books already on the device offer a "Read" action that opens the local file. The thumbnail cache is bounded and evicts the oldest covers.
|
||||
- Live reading progress of the open book (pull on open with conflict prompt, optional periodic push every N page turns, push on close and suspend), compatible with the kosync flow BookOrbit already speaks.
|
||||
- Reading statistics: the raw per-page time events from KOReader's statistics database. BookOrbit turns them into reading sessions and daily stats, so they show up in the existing dashboards.
|
||||
- Highlights and notes, two ways: device highlights appear in the BookOrbit web reader as native highlights, and highlights, note edits and deletions made in BookOrbit come back to the device.
|
||||
@@ -29,13 +29,14 @@ Re-downloading later (for example after changing the sync password or server add
|
||||
4. On the device, open the main menu, Tools tab: "BookOrbit sync" sits directly below "Calibre" on the first page.
|
||||
- Set the BookOrbit server address (e.g. `https://bookorbit.example.com`; the `/api/v1` suffix is added automatically).
|
||||
- Login with the credentials from step 3.
|
||||
- Use "Browse BookOrbit" to browse and download books from the same account.
|
||||
- Use "Open dashboard" to open the BookOrbit dashboard, browse and download books from the same account.
|
||||
|
||||
## Setup
|
||||
|
||||
1. In the "BookOrbit sync" menu, enable "Auto sync this book" if you want the open book synced automatically. Leave the stock "Progress sync" plugin unconfigured to avoid double syncing.
|
||||
2. Optional: tune "Periodically sync every # pages" (default 10, 0 disables mid-session pushes).
|
||||
3. Optional: assign the "BookOrbit: sync this book" and "BookOrbit: sync all books" actions to gestures.
|
||||
2. Optional: in "Sync settings", tune "Periodically sync every # pages" (default 10, 0 disables mid-session pushes).
|
||||
3. Optional: in "Sync settings", set "Open dashboard on startup" to "File manager only" or "Every startup" if you want the BookOrbit dashboard to open automatically after KOReader starts.
|
||||
4. Optional: assign the "BookOrbit: sync this book" and "BookOrbit: sync all books" actions to gestures.
|
||||
|
||||
Menu placement note: the plugin generates `settings/reader_menu_order.lua` and `settings/filemanager_menu_order.lua` to pin its entry below Calibre, and keeps them up to date across KOReader updates. Delete those files to reset the menu order. If you maintain your own menu order files, only the single `bookorbit_sync` entry is inserted (a one-time rewrite normalizes formatting and strips comments).
|
||||
|
||||
@@ -75,19 +76,22 @@ Years of statistics upload in batches of 500 events. Roughly 200k events means a
|
||||
## Hand-test checklist
|
||||
|
||||
- Menu placement: "BookOrbit sync" appears on the Tools first page directly below Calibre, in both the file manager and the reader; `settings/reader_menu_order.lua` and `settings/filemanager_menu_order.lua` exist with the bookorbit marker line; a restart does not rewrite them.
|
||||
- Catalog browser: "Browse BookOrbit" appears in the Tools menu after login. "Continue reading", recently added books, libraries, collections, SmartScopes, authors, series, all books and "On device" load through the BookOrbit JSON API, with page navigation, scoped/global search and sorting.
|
||||
- Catalog search: on the root and browse screens the title-bar left button is a magnifier that opens global search; on a book page it becomes the actions menu (which still offers "Search in this scope"). Search is no longer a row in the root list.
|
||||
- Catalog view and filters (actions menu, title-bar left button on a book page): toggle mosaic/list view, change the mosaic grid size, filter by read status (all/unread/reading/finished) and by format, sort, and reverse the sort direction (asc/desc). View mode, sort and grid size persist across sessions; the active sort, direction and filters show in the page subtitle.
|
||||
- Menu layout: the first BookOrbit menu page starts with Open dashboard, Installed plugin/update, Last sync, manual sync actions, Auto sync, Two-way highlight sync, Sync settings and Account & setup.
|
||||
- Catalog browser: "Open dashboard" appears in the Tools menu after login and opens the dashboard root. The dashboard shows compact Continue reading rows, reading goal/streak/highlight summaries, "On device", libraries, collections, SmartScopes, authors, series and all books. Tapping dashboard books/highlights opens the book detail; tapping shortcuts opens the matching existing catalog screen.
|
||||
- Catalog dashboard cache: open the dashboard successfully, turn wifi off, then open it again through startup auto-open; the last successful dashboard appears with an offline-cache label. Against an older BookOrbit server without the dashboard endpoint, the plugin falls back to the old catalog root and reports that the server needs an update.
|
||||
- Startup dashboard: with "Open dashboard on startup" set to Off, restart does not open it. With File manager only, restarting into the file manager opens the dashboard once. With Every startup, restarting into the file manager or reader opens the dashboard once. A fresh preconfigured install still shows the provision confirmation without auto-opening over it.
|
||||
- Catalog search: the title-bar left button opens the catalog action menu everywhere. Dashboard and section pages offer "Search BookOrbit"; book lists offer "Search in this list". Search is no longer a row in the root list.
|
||||
- Catalog view and filters (actions menu): toggle mosaic/list view, change the mosaic grid size, filter by read status (all/unread/reading/finished) and by format, sort, and change order (asc/desc). View mode, sort and grid size persist across sessions; the active sort, direction and filters show in the page subtitle.
|
||||
- Catalog responsiveness: tapping any section, book page or detail shows a dismissable loading message and does not freeze the reader (network runs in a subprocess); tapping the loading message cancels the request. The next page's covers are prefetched a second after a page settles, so paging forward shows covers immediately.
|
||||
- Catalog detail edits (detail page actions menu): "Set read status" and "Set rating" write straight to BookOrbit and update the page in place; against an older server without these endpoints the action reports it is unsupported instead of erroring. "Full description" opens the complete text, "All genres"/"All tags" list every value (the detail page itself only shows the first few with a "+N more" hint).
|
||||
- Catalog detail edits (detail page actions menu): "Set read status" and "Set rating" write straight to BookOrbit and update the page in place; against an older server without these endpoints the action reports it is unsupported instead of erroring. "Description" opens the complete text, and "Genres"/"Tags" list every value (the detail page itself only shows the first few with a "+N more" hint).
|
||||
- Catalog series view: opening a series shows its books in series order with a "Read N/total" header.
|
||||
- Catalog authors/series lists: a "Filter..." row narrows the list as you type and "Next/Previous page" rows move through large libraries.
|
||||
- Catalog authors/series lists: "Filter..." in the catalog action menu narrows the list as you type, and footer arrows move through large libraries.
|
||||
- Catalog manual refresh: the actions menu "Refresh" re-fetches the current scope and retries any covers that failed.
|
||||
- Catalog indicators: each card/row shows reading progress, read status and an "On device" badge when a matching file already exists locally. "On device" lists those matched books via a single `?ids=` lookup.
|
||||
- Catalog search history: the search dialog offers a "Recent" button to re-run one of the last searches (stored locally).
|
||||
- Catalog feedback: section/page/detail fetches show a loading message; failures show an inline "Retry" dialog; downloads show throttled byte/percent progress.
|
||||
- Catalog download: open a book detail, choose a file format, change folder/name if needed, confirm overwrite, watch the download progress, then accept "Open now". The downloaded file is matched by partial MD5 and linked in `bookorbit_sync_state.lua`; the "On device" badge refreshes after a successful link.
|
||||
- Catalog read vs download: when a book is already on the device, the detail page shows a primary "Read" button that opens the local file. "Download" is shown next to it only when there are formats not yet on the device, and is hidden once every supported format is downloaded. The file chooser marks already-downloaded formats as "Open" instead of "Download". On-device state is refreshed when the detail opens.
|
||||
- Catalog download: open a book detail, tap "Download" for the default folder/name, confirm overwrite if needed, watch the download progress, then accept "Open now". Use "Download options" in the detail action menu to change folder/name. The downloaded file is matched by partial MD5 and linked in `bookorbit_sync_state.lua`; the "On device" badge refreshes after a successful link.
|
||||
- Catalog read vs download: when a book is already on the device, the detail page shows a primary "Read" button that opens the local file. "Download" is shown next to it only when there are formats not yet on the device, starts with the default folder/name, and is hidden once every supported format is downloaded. The detail action menu offers "Download options" for folder/name changes. The file chooser marks already-downloaded formats as "Open" instead of "Download". On-device state is refreshed when the detail opens and after a successful download.
|
||||
- Custom menu order: remove the marker line and shuffle the tools list; on restart only the `bookorbit_sync` id is re-inserted, the rest is preserved. A syntactically broken file is left untouched (warning in the log).
|
||||
- Login: wrong password shows a login error; server URL with and without trailing `/` and with `/api/v1` already appended all work.
|
||||
- Preconfigured install: download the zip from web settings, install, restart; the "set up for ... as ..." message appears once, the server URL and login are filled in, `bookorbit_provision.lua` is gone from the plugin folder, and a second restart shows no message.
|
||||
|
||||
@@ -41,6 +41,7 @@ function BookOrbitApi.normalizeServerUrl(input)
|
||||
local url = util.trim(input)
|
||||
if url == "" then return nil end
|
||||
url = url:gsub("/+$", "")
|
||||
url = url:gsub("/api/v1/koreader$", "/api/v1")
|
||||
if not url:match("/api/v1$") then
|
||||
url = url .. "/api/v1"
|
||||
end
|
||||
@@ -252,6 +253,14 @@ function BookOrbitApi:catalogRoot()
|
||||
return self:request("GET", "/koreader/plugin/catalog/root")
|
||||
end
|
||||
|
||||
function BookOrbitApi:catalogDashboard()
|
||||
return self:request("GET", "/koreader/plugin/catalog/dashboard")
|
||||
end
|
||||
|
||||
function BookOrbitApi:catalogDiscover()
|
||||
return self:request("GET", "/koreader/plugin/catalog/dashboard/discover")
|
||||
end
|
||||
|
||||
function BookOrbitApi:catalogSection(section, params)
|
||||
return self:request("GET", self:query("/koreader/plugin/catalog/sections/" .. util.urlEncode(section), params))
|
||||
end
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -40,7 +40,7 @@ function CatalogDownload.install(Catalog)
|
||||
if path then
|
||||
self:openDownloadedFile(path)
|
||||
else
|
||||
self:showDownloadDialog(detail, files[1])
|
||||
self:downloadDefaultFile(detail, files[1])
|
||||
end
|
||||
return
|
||||
end
|
||||
@@ -60,7 +60,7 @@ function CatalogDownload.install(Catalog)
|
||||
if path then
|
||||
self:openDownloadedFile(path)
|
||||
else
|
||||
self:showDownloadDialog(detail, file)
|
||||
self:downloadDefaultFile(detail, file)
|
||||
end
|
||||
end,
|
||||
},
|
||||
@@ -84,6 +84,46 @@ function CatalogDownload.install(Catalog)
|
||||
return (download_dir ~= "/" and download_dir or "") .. "/" .. filename
|
||||
end
|
||||
|
||||
function Catalog:downloadDefaultFile(detail, file)
|
||||
local filename = safeFilenameBase(detail)
|
||||
local filetype = string.lower(file.format or "bin")
|
||||
local local_path = self:getLocalDownloadPath(filename, filetype)
|
||||
self:checkDownloadFile(local_path, detail, file)
|
||||
end
|
||||
|
||||
function Catalog:showDownloadOptions(detail)
|
||||
local files = self:supportedFiles(detail)
|
||||
|
||||
if #files == 0 then
|
||||
UIManager:show(InfoMessage:new{ text = _("No KOReader-supported file found."), timeout = 3 })
|
||||
return
|
||||
end
|
||||
if #files == 1 then
|
||||
self:showDownloadDialog(detail, files[1])
|
||||
return
|
||||
end
|
||||
|
||||
local dialog
|
||||
local buttons = {}
|
||||
for _, file in ipairs(files) do
|
||||
local label = self:fileLabel(file, false)
|
||||
table.insert(buttons, {
|
||||
{
|
||||
text = T(_("Options - %1"), label),
|
||||
callback = function()
|
||||
UIManager:close(dialog)
|
||||
self:showDownloadDialog(detail, file)
|
||||
end,
|
||||
},
|
||||
})
|
||||
end
|
||||
dialog = ButtonDialog:new{
|
||||
title = _("Download options"),
|
||||
buttons = buttons,
|
||||
}
|
||||
UIManager:show(dialog)
|
||||
end
|
||||
|
||||
function Catalog:showDownloadDialog(detail, file)
|
||||
local filename = safeFilenameBase(detail)
|
||||
local filetype = string.lower(file.format or "bin")
|
||||
@@ -222,6 +262,10 @@ function CatalogDownload.install(Catalog)
|
||||
local linked = self:linkDownloadedFile(local_path)
|
||||
if linked then
|
||||
self:refreshOnDevice()
|
||||
if self.markStackDirty then self:markStackDirty() end
|
||||
if self.detailMode and self:detailMode() then
|
||||
self:refreshDetailView()
|
||||
end
|
||||
end
|
||||
self:showDownloadedDialog(local_path, linked)
|
||||
end
|
||||
|
||||
@@ -16,9 +16,14 @@ local Geom = require("ui/geometry")
|
||||
local GestureRange = require("ui/gesturerange")
|
||||
local HorizontalGroup = require("ui/widget/horizontalgroup")
|
||||
local HorizontalSpan = require("ui/widget/horizontalspan")
|
||||
local IconWidget = require("ui/widget/iconwidget")
|
||||
local ImageWidget = require("ui/widget/imagewidget")
|
||||
local InputContainer = require("ui/widget/container/inputcontainer")
|
||||
local LeftContainer = require("ui/widget/container/leftcontainer")
|
||||
local LineWidget = require("ui/widget/linewidget")
|
||||
local OverlapGroup = require("ui/widget/overlapgroup")
|
||||
local ProgressWidget = require("ui/widget/progresswidget")
|
||||
local RightContainer = require("ui/widget/container/rightcontainer")
|
||||
local Size = require("ui/size")
|
||||
local TextBoxWidget = require("ui/widget/textboxwidget")
|
||||
local VerticalGroup = require("ui/widget/verticalgroup")
|
||||
@@ -31,7 +36,17 @@ local Screen = Device.screen
|
||||
local shortText = CatalogUtil.shortText
|
||||
local firstAuthor = CatalogUtil.firstAuthor
|
||||
|
||||
local PROGRESS_BAR_HEIGHT = Screen:scaleBySize(5)
|
||||
local PROGRESS_BAR_HEIGHT = Screen:scaleBySize(3)
|
||||
local SCALE_BY_SIZE = Screen:scaleBySize(1000000) * (1 / 1000000)
|
||||
local READ_STATUS_BADGE_ICONS = {
|
||||
want_to_read = "bookmark",
|
||||
reading = "dogear.reading",
|
||||
on_hold = "dogear.abandoned",
|
||||
rereading = "cre.render.reload",
|
||||
read = "check",
|
||||
skimmed = "check",
|
||||
abandoned = "dogear.abandoned",
|
||||
}
|
||||
|
||||
local CatalogWidgets = {}
|
||||
|
||||
@@ -39,8 +54,20 @@ local function hasProgress(book)
|
||||
return book and book.progressPercentage and book.progressPercentage > 0
|
||||
end
|
||||
|
||||
-- A slim solid progress bar, e-ink friendly (no animation), used to visualize
|
||||
-- reading progress on cards and rows.
|
||||
local function readStatusBadgeIcon(book)
|
||||
local status = book and book.readStatus
|
||||
if not status or status == "" or status == "unread" then return nil end
|
||||
return READ_STATUS_BADGE_ICONS[status] or "dogear.reading"
|
||||
end
|
||||
|
||||
local function rowFontSize(nominal, max_size, row_h)
|
||||
local size = math.floor(nominal * row_h * (1 / 64) / SCALE_BY_SIZE)
|
||||
if max_size and size > max_size then return max_size end
|
||||
return math.max(8, size)
|
||||
end
|
||||
|
||||
-- A slim borderless progress bar, e-ink friendly (no animation): a light-gray
|
||||
-- track with a solid black fill, used to visualize reading progress on cards.
|
||||
function CatalogWidgets.buildProgressBar(percentage, width)
|
||||
if not percentage or percentage <= 0 then return nil end
|
||||
local bar = ProgressWidget:new{
|
||||
@@ -49,8 +76,8 @@ function CatalogWidgets.buildProgressBar(percentage, width)
|
||||
percentage = math.min(1, percentage / 100),
|
||||
margin_h = 0,
|
||||
margin_v = 0,
|
||||
bordersize = Size.border.thin,
|
||||
bgcolor = Blitbuffer.COLOR_WHITE,
|
||||
bordersize = 0,
|
||||
bgcolor = Blitbuffer.COLOR_LIGHT_GRAY,
|
||||
fillcolor = Blitbuffer.COLOR_BLACK,
|
||||
}
|
||||
return bar
|
||||
@@ -134,6 +161,19 @@ function CatalogWidgets.buildCoverWidget(book, width, height, path, state)
|
||||
return CatalogWidgets.buildFakeCover(book, width, height, footer)
|
||||
end
|
||||
|
||||
function CatalogWidgets.buildReadStatusBadge(book, max_width)
|
||||
local icon = readStatusBadgeIcon(book)
|
||||
if not icon then return nil end
|
||||
|
||||
local size = math.max(Screen:scaleBySize(12), math.min(max_width, Screen:scaleBySize(20)))
|
||||
return IconWidget:new{
|
||||
icon = icon,
|
||||
rotation_angle = 270,
|
||||
width = size,
|
||||
height = size,
|
||||
}
|
||||
end
|
||||
|
||||
local MosaicItem = InputContainer:extend{
|
||||
entry = nil,
|
||||
dimen = nil,
|
||||
@@ -222,45 +262,114 @@ function ListItem:init()
|
||||
}
|
||||
|
||||
local book = self.entry.book
|
||||
local pad = Size.padding.default
|
||||
local inner_h = math.max(1, self.dimen.h - 2 * Size.border.thin)
|
||||
local cover_h = math.max(Screen:scaleBySize(40), inner_h - 2 * Size.padding.small)
|
||||
local pad = Size.padding.small
|
||||
local gap = Size.span.horizontal_default
|
||||
local separator_h = Size.line.medium
|
||||
local inner_h = math.max(1, self.dimen.h - separator_h)
|
||||
local content_h = math.max(1, inner_h - 2 * Size.padding.small)
|
||||
local cover_h = math.max(Screen:scaleBySize(38), math.min(Screen:scaleBySize(74), content_h))
|
||||
local cover_w = math.floor(cover_h * 0.68)
|
||||
local text_w = math.max(1, self.dimen.w - cover_w - 3 * pad)
|
||||
local bar_reserve = hasProgress(book) and (PROGRESS_BAR_HEIGHT + Size.span.vertical_default) or 0
|
||||
local left_w = cover_w + 2 * pad
|
||||
local side_meta_text = self.menu:listSideMetaText(book)
|
||||
local show_side_meta = side_meta_text ~= "" and self.dimen.w >= Screen:scaleBySize(520)
|
||||
local right_w = show_side_meta and math.min(Screen:scaleBySize(168), math.floor(self.dimen.w * 0.20)) or 0
|
||||
local right_pad = show_side_meta and pad or 0
|
||||
local main_x = left_w + gap
|
||||
local main_w = math.max(1, self.dimen.w - main_x - right_w - right_pad - gap)
|
||||
local text_h = math.max(1, content_h - bar_reserve)
|
||||
local title_font = rowFontSize(18, 20, inner_h)
|
||||
local subtitle_font = rowFontSize(14, 16, inner_h)
|
||||
local side_font = rowFontSize(13, 15, inner_h)
|
||||
local title_h = math.max(1, math.floor(text_h * 0.52))
|
||||
local subtitle_h = math.max(1, text_h - title_h)
|
||||
|
||||
local path = self.menu:cachedThumbnailPath(book)
|
||||
local state = self.menu:thumbnailState(book)
|
||||
|
||||
local text_col = VerticalGroup:new{ align = "left" }
|
||||
table.insert(text_col, TextBoxWidget:new{
|
||||
text = self.menu:listText(book),
|
||||
width = text_w,
|
||||
height = math.max(1, inner_h - 2 * Size.padding.small - bar_reserve),
|
||||
text = BD.auto(shortText(book and book.title or _("Untitled"), 58)),
|
||||
width = main_w,
|
||||
height = title_h,
|
||||
height_adjust = true,
|
||||
alignment = "left",
|
||||
face = Font:getFace("smallinfofont"),
|
||||
bold = true,
|
||||
face = Font:getFace("cfont", title_font),
|
||||
height_overflow_show_ellipsis = true,
|
||||
})
|
||||
local bar = CatalogWidgets.buildProgressBar(book and book.progressPercentage, text_w)
|
||||
if bar then
|
||||
table.insert(text_col, VerticalSpan:new{ width = Size.span.vertical_default })
|
||||
table.insert(text_col, bar)
|
||||
local subtitle = self.menu:listSubtitleLine(book)
|
||||
if subtitle then
|
||||
table.insert(text_col, TextBoxWidget:new{
|
||||
text = BD.auto(subtitle),
|
||||
width = main_w,
|
||||
height = subtitle_h,
|
||||
height_adjust = true,
|
||||
alignment = "left",
|
||||
face = Font:getFace("cfont", subtitle_font),
|
||||
height_overflow_show_ellipsis = true,
|
||||
})
|
||||
end
|
||||
|
||||
local row = HorizontalGroup:new{ align = "center" }
|
||||
table.insert(row, HorizontalSpan:new{ width = pad })
|
||||
table.insert(row, CatalogWidgets.buildCoverWidget(book, cover_w, cover_h, path, state))
|
||||
table.insert(row, HorizontalSpan:new{ width = pad })
|
||||
table.insert(row, text_col)
|
||||
local body_col = VerticalGroup:new{ align = "left" }
|
||||
table.insert(body_col, text_col)
|
||||
local bar = CatalogWidgets.buildProgressBar(book and book.progressPercentage, main_w)
|
||||
if bar then
|
||||
table.insert(body_col, VerticalSpan:new{ width = Size.span.vertical_default })
|
||||
table.insert(body_col, bar)
|
||||
end
|
||||
|
||||
local row_dimen = Geom:new{ w = self.dimen.w, h = inner_h }
|
||||
local row = OverlapGroup:new{
|
||||
dimen = row_dimen:copy(),
|
||||
LeftContainer:new{
|
||||
dimen = row_dimen:copy(),
|
||||
CenterContainer:new{
|
||||
dimen = Geom:new{ w = left_w, h = inner_h },
|
||||
CatalogWidgets.buildCoverWidget(book, cover_w, cover_h, path, state),
|
||||
},
|
||||
},
|
||||
LeftContainer:new{
|
||||
dimen = row_dimen:copy(),
|
||||
HorizontalGroup:new{
|
||||
HorizontalSpan:new{ width = main_x },
|
||||
body_col,
|
||||
},
|
||||
},
|
||||
}
|
||||
if show_side_meta then
|
||||
table.insert(row, RightContainer:new{
|
||||
dimen = row_dimen:copy(),
|
||||
HorizontalGroup:new{
|
||||
TextBoxWidget:new{
|
||||
text = side_meta_text,
|
||||
width = right_w,
|
||||
height = text_h,
|
||||
height_adjust = true,
|
||||
alignment = "right",
|
||||
face = Font:getFace("cfont", side_font),
|
||||
height_overflow_show_ellipsis = true,
|
||||
},
|
||||
HorizontalSpan:new{ width = right_pad },
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
local content = VerticalGroup:new{ align = "left" }
|
||||
table.insert(content, row)
|
||||
table.insert(content, LineWidget:new{
|
||||
background = Blitbuffer.COLOR_LIGHT_GRAY,
|
||||
dimen = Geom:new{ w = self.dimen.w, h = separator_h },
|
||||
})
|
||||
|
||||
self[1] = FrameContainer:new{
|
||||
width = self.dimen.w,
|
||||
height = self.dimen.h,
|
||||
margin = 0,
|
||||
padding = 0,
|
||||
bordersize = Size.border.thin,
|
||||
bordersize = 0,
|
||||
background = Blitbuffer.COLOR_WHITE,
|
||||
row,
|
||||
content,
|
||||
}
|
||||
end
|
||||
|
||||
@@ -274,7 +383,229 @@ function ListItem:onHoldSelect()
|
||||
return true
|
||||
end
|
||||
|
||||
-- Shared card chrome for dashboard tiles: thin rounded border on a white fill.
|
||||
local CARD_BORDER = Size.border.thin
|
||||
local CARD_RADIUS = Size.radius.window
|
||||
local COVER_CARD_PAD = Size.padding.small
|
||||
-- Typical book-cover aspect ratio (width / height); covers are sized to this so
|
||||
-- the card hugs the cover with no horizontal whitespace.
|
||||
local COVER_ASPECT = 0.66
|
||||
|
||||
local function cardFrame(width, height, padding, child)
|
||||
return FrameContainer:new{
|
||||
width = width,
|
||||
height = height,
|
||||
margin = 0,
|
||||
padding = padding,
|
||||
bordersize = CARD_BORDER,
|
||||
radius = CARD_RADIUS,
|
||||
background = Blitbuffer.COLOR_WHITE,
|
||||
child,
|
||||
}
|
||||
end
|
||||
|
||||
-- The cover height a cover card derives from its total height (mirrors
|
||||
-- DashboardCoverCard:init), reserving room for a progress bar when present.
|
||||
local function coverCardCoverHeight(card_h, with_progress)
|
||||
local inner_h = math.max(1, card_h - 2 * COVER_CARD_PAD - 2 * CARD_BORDER)
|
||||
local bar_h = with_progress and (PROGRESS_BAR_HEIGHT + Size.span.vertical_default) or 0
|
||||
return math.max(Screen:scaleBySize(60), inner_h - bar_h)
|
||||
end
|
||||
|
||||
-- The width a cover card occupies for a given height, so the layout can lay out
|
||||
-- a row of cards that tightly wrap their covers.
|
||||
function CatalogWidgets.coverCardWidth(card_h, with_progress)
|
||||
local cover_w = math.floor(coverCardCoverHeight(card_h, with_progress) * COVER_ASPECT)
|
||||
return cover_w + 2 * COVER_CARD_PAD + 2 * CARD_BORDER
|
||||
end
|
||||
|
||||
function CatalogWidgets.buildDashboardCoverWidget(book, width, height, path, state)
|
||||
local cover = CatalogWidgets.buildCoverWidget(book, width, height, path, state)
|
||||
local badge = CatalogWidgets.buildReadStatusBadge(book, math.floor(math.min(width, height) * 0.18))
|
||||
if not badge then return cover end
|
||||
badge.overlap_align = "right"
|
||||
|
||||
local dimen = Geom:new{ w = width, h = height }
|
||||
return OverlapGroup:new{
|
||||
dimen = dimen:copy(),
|
||||
allow_mirroring = false,
|
||||
cover,
|
||||
badge,
|
||||
}
|
||||
end
|
||||
|
||||
-- A thin horizontal rule used to separate dashboard sections.
|
||||
function CatalogWidgets.buildDivider(width)
|
||||
return LineWidget:new{
|
||||
background = Blitbuffer.COLOR_GRAY,
|
||||
dimen = Geom:new{ w = width, h = Size.line.medium },
|
||||
}
|
||||
end
|
||||
|
||||
-- A bold section header (Continue reading / Browse).
|
||||
function CatalogWidgets.buildSectionHeader(text, width, height)
|
||||
return TextBoxWidget:new{
|
||||
text = text,
|
||||
width = width,
|
||||
height = height,
|
||||
alignment = "left",
|
||||
bold = true,
|
||||
face = Font:getFace("cfont", 18),
|
||||
height_overflow_show_ellipsis = true,
|
||||
}
|
||||
end
|
||||
|
||||
-- A muted status line (Updated / offline cache / unavailable).
|
||||
function CatalogWidgets.buildStatusLabel(text, width, height, alignment)
|
||||
return TextBoxWidget:new{
|
||||
text = text,
|
||||
width = width,
|
||||
height = height,
|
||||
alignment = alignment or "left",
|
||||
fgcolor = Blitbuffer.COLOR_DARK_GRAY,
|
||||
face = Font:getFace("xx_smallinfofont"),
|
||||
height_overflow_show_ellipsis = true,
|
||||
}
|
||||
end
|
||||
|
||||
-- A cover-only book card: the cover fills the card, with an optional slim
|
||||
-- progress bar pinned at the bottom. Used in the Continue reading / Discover rows.
|
||||
local DashboardCoverCard = InputContainer:extend{
|
||||
entry = nil,
|
||||
dimen = nil,
|
||||
menu = nil,
|
||||
}
|
||||
|
||||
function DashboardCoverCard:init()
|
||||
self.ges_events = {
|
||||
TapSelect = { GestureRange:new{ ges = "tap", range = self.dimen } },
|
||||
HoldSelect = { GestureRange:new{ ges = "hold", range = self.dimen } },
|
||||
}
|
||||
|
||||
local book = self.entry.book
|
||||
local pad = COVER_CARD_PAD
|
||||
local inner_w = math.max(1, self.dimen.w - 2 * pad - 2 * CARD_BORDER)
|
||||
local inner_h = math.max(1, self.dimen.h - 2 * pad - 2 * CARD_BORDER)
|
||||
|
||||
local bar_h = hasProgress(book) and (PROGRESS_BAR_HEIGHT + Size.span.vertical_default) or 0
|
||||
local cover_h = math.max(Screen:scaleBySize(60), inner_h - bar_h)
|
||||
local cover_w = math.min(inner_w, math.floor(cover_h * COVER_ASPECT))
|
||||
|
||||
local path = self.menu:cachedThumbnailPath(book)
|
||||
local state = self.menu:thumbnailState(book)
|
||||
|
||||
local col = VerticalGroup:new{ align = "center" }
|
||||
table.insert(col, CenterContainer:new{
|
||||
dimen = Geom:new{ w = inner_w, h = cover_h },
|
||||
CatalogWidgets.buildDashboardCoverWidget(book, cover_w, cover_h, path, state),
|
||||
})
|
||||
if bar_h > 0 then
|
||||
table.insert(col, VerticalSpan:new{ width = Size.span.vertical_default })
|
||||
table.insert(col, CatalogWidgets.buildProgressBar(book.progressPercentage, cover_w))
|
||||
end
|
||||
|
||||
self[1] = cardFrame(self.dimen.w, self.dimen.h, pad, CenterContainer:new{
|
||||
dimen = Geom:new{ w = inner_w, h = inner_h },
|
||||
col,
|
||||
})
|
||||
end
|
||||
|
||||
function DashboardCoverCard:onTapSelect()
|
||||
self.menu:onMenuSelect(self.entry)
|
||||
return true
|
||||
end
|
||||
|
||||
function DashboardCoverCard:onHoldSelect()
|
||||
self.menu:onMenuSelect(self.entry)
|
||||
return true
|
||||
end
|
||||
|
||||
-- A browse-grid tile: centred icon over a label, with an optional count.
|
||||
local DashboardBrowseTile = InputContainer:extend{
|
||||
entry = nil,
|
||||
dimen = nil,
|
||||
menu = nil,
|
||||
}
|
||||
|
||||
function DashboardBrowseTile:init()
|
||||
self.ges_events = {
|
||||
TapSelect = { GestureRange:new{ ges = "tap", range = self.dimen } },
|
||||
HoldSelect = { GestureRange:new{ ges = "hold", range = self.dimen } },
|
||||
}
|
||||
|
||||
local entry = self.entry
|
||||
local pad = Size.padding.small
|
||||
local inner_w = math.max(1, self.dimen.w - 2 * pad - 2 * CARD_BORDER)
|
||||
local inner_h = math.max(1, self.dimen.h - 2 * pad - 2 * CARD_BORDER)
|
||||
local icon_size = math.min(Screen:scaleBySize(22), math.floor(inner_h * 0.45))
|
||||
|
||||
local label = entry.text or ""
|
||||
if entry.mandatory then
|
||||
label = label .. " (" .. tostring(entry.mandatory) .. ")"
|
||||
end
|
||||
|
||||
local col = VerticalGroup:new{ align = "center" }
|
||||
if entry.icon then
|
||||
table.insert(col, IconWidget:new{ icon = entry.icon, width = icon_size, height = icon_size })
|
||||
table.insert(col, VerticalSpan:new{ width = Size.span.vertical_default })
|
||||
end
|
||||
table.insert(col, TextBoxWidget:new{
|
||||
text = BD.auto(shortText(label, 22)),
|
||||
width = inner_w,
|
||||
alignment = "center",
|
||||
face = Font:getFace("x_smallinfofont"),
|
||||
height_overflow_show_ellipsis = true,
|
||||
})
|
||||
|
||||
self[1] = cardFrame(self.dimen.w, self.dimen.h, pad, CenterContainer:new{
|
||||
dimen = Geom:new{ w = inner_w, h = inner_h },
|
||||
col,
|
||||
})
|
||||
end
|
||||
|
||||
function DashboardBrowseTile:onTapSelect()
|
||||
self.menu:onMenuSelect(self.entry)
|
||||
return true
|
||||
end
|
||||
|
||||
function DashboardBrowseTile:onHoldSelect()
|
||||
self.menu:onMenuSelect(self.entry)
|
||||
return true
|
||||
end
|
||||
|
||||
-- A small tappable icon button, used for the Discover section's reroll control.
|
||||
local DashboardIconButton = InputContainer:extend{
|
||||
entry = nil,
|
||||
dimen = nil,
|
||||
menu = nil,
|
||||
}
|
||||
|
||||
function DashboardIconButton:init()
|
||||
self.ges_events = {
|
||||
TapSelect = { GestureRange:new{ ges = "tap", range = self.dimen } },
|
||||
HoldSelect = { GestureRange:new{ ges = "hold", range = self.dimen } },
|
||||
}
|
||||
local icon_size = math.max(1, math.min(self.dimen.w, self.dimen.h) - Size.padding.tiny)
|
||||
self[1] = CenterContainer:new{
|
||||
dimen = Geom:new{ w = self.dimen.w, h = self.dimen.h },
|
||||
IconWidget:new{ icon = self.entry.icon, width = icon_size, height = icon_size },
|
||||
}
|
||||
end
|
||||
|
||||
function DashboardIconButton:onTapSelect()
|
||||
self.menu:onMenuSelect(self.entry)
|
||||
return true
|
||||
end
|
||||
|
||||
function DashboardIconButton:onHoldSelect()
|
||||
self.menu:onMenuSelect(self.entry)
|
||||
return true
|
||||
end
|
||||
|
||||
CatalogWidgets.MosaicItem = MosaicItem
|
||||
CatalogWidgets.ListItem = ListItem
|
||||
CatalogWidgets.DashboardCoverCard = DashboardCoverCard
|
||||
CatalogWidgets.DashboardBrowseTile = DashboardBrowseTile
|
||||
CatalogWidgets.DashboardIconButton = DashboardIconButton
|
||||
|
||||
return CatalogWidgets
|
||||
|
||||
@@ -6,6 +6,7 @@ the plugin zip from the BookOrbit server and atomically replaces the current
|
||||
plugin directory. KOReader must be restarted for the new files to load.
|
||||
]]
|
||||
|
||||
local Device = require("device")
|
||||
local lfs = require("libs/libkoreader-lfs")
|
||||
|
||||
local BookOrbitUpdater = {}
|
||||
@@ -63,21 +64,28 @@ function BookOrbitUpdater.apply(api, plugin_dir, progress_cb)
|
||||
return nil, tostring(err or "download failed")
|
||||
end
|
||||
|
||||
-- Extract into a staging directory so a partial unzip never touches the
|
||||
-- live plugin directory.
|
||||
local ret = os.execute("unzip -o " .. sq(tmp_zip) .. " -d " .. sq(staging))
|
||||
os.remove(tmp_zip)
|
||||
|
||||
if ret ~= 0 then
|
||||
os.execute("rm -rf " .. sq(staging))
|
||||
return nil, "unzip failed with exit code " .. tostring(ret)
|
||||
if not lfs.mkdir(staging) and lfs.attributes(staging, "mode") ~= "directory" then
|
||||
os.remove(tmp_zip)
|
||||
return nil, "could not create update staging directory"
|
||||
end
|
||||
|
||||
-- The zip must contain exactly the plugin folder as its top-level entry.
|
||||
local extracted = staging .. "/" .. plugin_name
|
||||
if lfs.attributes(extracted, "mode") ~= "directory" then
|
||||
-- Extract into a staging directory so a partial unpack never touches the
|
||||
-- live plugin directory. Use KOReader's archive helper instead of the
|
||||
-- platform unzip command: unzip warning exit codes vary by platform.
|
||||
local ok_unpack, unpack_err = Device:unpackArchive(tmp_zip, staging, true)
|
||||
os.remove(tmp_zip)
|
||||
|
||||
if not ok_unpack then
|
||||
os.execute("rm -rf " .. sq(staging))
|
||||
return nil, "update zip does not contain expected directory: " .. plugin_name
|
||||
return nil, tostring(unpack_err or "archive extraction failed")
|
||||
end
|
||||
|
||||
-- The zip must contain the plugin folder as its top-level entry. We strip
|
||||
-- that root during extraction, so the staging directory should now look
|
||||
-- like the plugin directory itself.
|
||||
if lfs.attributes(staging .. "/main.lua", "mode") ~= "file" then
|
||||
os.execute("rm -rf " .. sq(staging))
|
||||
return nil, "update zip does not contain expected plugin files: " .. plugin_name
|
||||
end
|
||||
|
||||
-- Atomic-ish swap (all paths share the same filesystem):
|
||||
@@ -89,7 +97,7 @@ function BookOrbitUpdater.apply(api, plugin_dir, progress_cb)
|
||||
os.execute("rm -rf " .. sq(staging))
|
||||
return nil, "could not create plugin backup"
|
||||
end
|
||||
if not os.rename(extracted, dir) then
|
||||
if not os.rename(staging, dir) then
|
||||
-- Restore backup so the plugin remains usable.
|
||||
os.rename(backup, dir)
|
||||
os.execute("rm -rf " .. sq(staging))
|
||||
|
||||
@@ -19,6 +19,7 @@ local Math = require("optmath")
|
||||
local MultiInputDialog = require("ui/widget/multiinputdialog")
|
||||
local NetworkMgr = require("ui/network/manager")
|
||||
local Notification = require("ui/widget/notification")
|
||||
local PluginShare = require("pluginshare")
|
||||
local UIManager = require("ui/uimanager")
|
||||
local WidgetContainer = require("ui/widget/container/widgetcontainer")
|
||||
local logger = require("logger")
|
||||
@@ -37,7 +38,7 @@ local BookOrbitMenuPin = require("bookorbit_menu_pin")
|
||||
local BookOrbitSweep = require("bookorbit_sweep")
|
||||
local BookOrbitUpdater = require("bookorbit_updater")
|
||||
|
||||
local PLUGIN_VERSION = "0.1.0"
|
||||
local PLUGIN_VERSION = "1.0.0"
|
||||
|
||||
local SYNC_STRATEGY = {
|
||||
PROMPT = 1,
|
||||
@@ -75,6 +76,8 @@ BookOrbit.default_settings = {
|
||||
catalog_grid_cols = 3,
|
||||
catalog_grid_rows = 3,
|
||||
catalog_recent_searches = {},
|
||||
catalog_auto_open = "off",
|
||||
catalog_dashboard_cache = nil,
|
||||
update_check_last_at = 0,
|
||||
}
|
||||
|
||||
@@ -85,6 +88,7 @@ function BookOrbit:init()
|
||||
self.last_page_turn_timestamp = 0
|
||||
self.page_update_counter = 0
|
||||
self.periodic_push_scheduled = false
|
||||
self.provision_applied = false
|
||||
self.periodic_push_task = function()
|
||||
self.periodic_push_scheduled = false
|
||||
self.page_update_counter = 0
|
||||
@@ -132,6 +136,9 @@ function BookOrbit:init()
|
||||
if self.settings.update_check_last_at == nil then
|
||||
self.settings.update_check_last_at = 0
|
||||
end
|
||||
if self.settings.catalog_auto_open == nil then
|
||||
self.settings.catalog_auto_open = "off"
|
||||
end
|
||||
|
||||
self:applyProvision()
|
||||
|
||||
@@ -143,6 +150,7 @@ function BookOrbit:init()
|
||||
pcall(BookOrbitMenuPin.ensure)
|
||||
self:onDispatcherRegisterActions()
|
||||
self.ui.menu:registerToMainMenu(self)
|
||||
self:onStart()
|
||||
UIManager:scheduleIn(5, function()
|
||||
self:maybeCheckForUpdate(false)
|
||||
end)
|
||||
@@ -181,6 +189,7 @@ function BookOrbit:applyProvision()
|
||||
self.settings.username = provision.username
|
||||
self.settings.userkey = provision.userkey
|
||||
self.settings.provision_fingerprint = fingerprint
|
||||
self.provision_applied = true
|
||||
G_reader_settings:flush()
|
||||
logger.info("BookOrbit: applied provision file for", provision.username)
|
||||
UIManager:nextTick(function()
|
||||
@@ -212,6 +221,28 @@ function BookOrbit:isLoggedIn()
|
||||
return self.settings.server_url ~= nil and self.settings.username ~= nil and self.settings.userkey ~= nil
|
||||
end
|
||||
|
||||
function BookOrbit:onStart()
|
||||
if PluginShare.bookorbit_auto_open_done then return end
|
||||
PluginShare.bookorbit_auto_open_done = true
|
||||
|
||||
local mode = self.settings.catalog_auto_open or "off"
|
||||
if mode == "off" then return end
|
||||
UIManager:scheduleIn(1.2, function()
|
||||
self:maybeAutoOpenCatalog(mode)
|
||||
end)
|
||||
end
|
||||
|
||||
function BookOrbit:maybeAutoOpenCatalog(mode)
|
||||
if mode ~= "filemanager" and mode ~= "always" then return end
|
||||
if self.provision_applied then return end
|
||||
if not self:isLoggedIn() then return end
|
||||
if self.catalog_browser ~= nil then return end
|
||||
if mode == "filemanager" and not (self.ui and self.ui.file_chooser ~= nil) then
|
||||
return
|
||||
end
|
||||
self:browseCatalog(true)
|
||||
end
|
||||
|
||||
local function showSyncError()
|
||||
UIManager:show(InfoMessage:new{
|
||||
text = _("Something went wrong when syncing to BookOrbit, please check your network connection and try again later."),
|
||||
@@ -331,6 +362,34 @@ function BookOrbit:updateCheckMenuText()
|
||||
return T(_("Installed plugin: v%1 (Login required)"), PLUGIN_VERSION)
|
||||
end
|
||||
|
||||
function BookOrbit:catalogAutoOpenLabel()
|
||||
local mode = self.settings.catalog_auto_open or "off"
|
||||
if mode == "filemanager" then
|
||||
return _("File manager only")
|
||||
elseif mode == "always" then
|
||||
return _("Every startup")
|
||||
end
|
||||
return _("Off")
|
||||
end
|
||||
|
||||
function BookOrbit:catalogAutoOpenMenu()
|
||||
local function item(text, value)
|
||||
return {
|
||||
text = text,
|
||||
checked_func = function() return (self.settings.catalog_auto_open or "off") == value end,
|
||||
callback = function()
|
||||
self.settings.catalog_auto_open = value
|
||||
G_reader_settings:flush()
|
||||
end,
|
||||
}
|
||||
end
|
||||
return {
|
||||
item(_("Off"), "off"),
|
||||
item(_("File manager only"), "filemanager"),
|
||||
item(_("Every startup"), "always"),
|
||||
}
|
||||
end
|
||||
|
||||
function BookOrbit:addToMainMenu(menu_items)
|
||||
menu_items.bookorbit = {
|
||||
text = _("BookOrbit"),
|
||||
@@ -339,108 +398,42 @@ function BookOrbit:addToMainMenu(menu_items)
|
||||
sorting_hint = "tools",
|
||||
sub_item_table = {
|
||||
{
|
||||
text = _("BookOrbit server address"),
|
||||
keep_menu_open = true,
|
||||
callback = function()
|
||||
self:setServerAddress()
|
||||
end,
|
||||
},
|
||||
{
|
||||
text_func = function()
|
||||
return self.settings.userkey and _("Logout") or _("Login")
|
||||
end,
|
||||
enabled_func = function()
|
||||
return self.settings.server_url ~= nil
|
||||
end,
|
||||
keep_menu_open = true,
|
||||
callback_func = function()
|
||||
if self.settings.userkey then
|
||||
return function(menu)
|
||||
self:logout(menu)
|
||||
end
|
||||
else
|
||||
return function(menu)
|
||||
self:login(menu)
|
||||
end
|
||||
end
|
||||
end,
|
||||
separator = true,
|
||||
},
|
||||
{
|
||||
text = _("Browse library"),
|
||||
text = _("Open dashboard"),
|
||||
enabled_func = function()
|
||||
return self:isLoggedIn()
|
||||
end,
|
||||
callback = function()
|
||||
self:browseCatalog()
|
||||
end,
|
||||
separator = true,
|
||||
},
|
||||
{
|
||||
text = _("Auto sync this book"),
|
||||
checked_func = function() return self.settings.auto_sync end,
|
||||
help_text = _([[Pulls progress when a book is opened; pushes progress, highlights, status, rating and reading time when it is closed and on suspend.]]),
|
||||
callback = function()
|
||||
self:onBookOrbitToggleAutoSync(nil, true)
|
||||
text_func = function()
|
||||
return self:updateCheckMenuText()
|
||||
end,
|
||||
},
|
||||
{
|
||||
text = _("Two-way highlight sync"),
|
||||
checked_func = function() return self.settings.annotation_sync end,
|
||||
help_text = _([[Also applies highlights, notes and deletions made in BookOrbit to this device: on book open, after the manual book sync, and during the full sweep for closed books. Turning this off keeps uploads only.]]),
|
||||
enabled_func = function()
|
||||
return self:isLoggedIn()
|
||||
and not BookOrbitSweep.isRunning()
|
||||
and not BookOrbitBookSync.isRunning()
|
||||
end,
|
||||
keep_menu_open = true,
|
||||
callback = function()
|
||||
self.settings.annotation_sync = not self.settings.annotation_sync
|
||||
self:checkForUpdate()
|
||||
end,
|
||||
},
|
||||
{
|
||||
text_func = function()
|
||||
return T(_("Periodically sync every # pages (%1)"), self:getSyncPeriod())
|
||||
local status = BookOrbitSweep.syncStatus()
|
||||
local when = (status.lastSweepAt == 0) and _("never")
|
||||
or os.date("%Y-%m-%d %H:%M", status.lastSweepAt)
|
||||
if status.unmatched > 0 then
|
||||
return T(_("Last sync: %1 (%2 linked, %3 unmatched)"), when, status.matched, status.unmatched)
|
||||
elseif status.matched > 0 then
|
||||
return T(_("Last sync: %1 (%2 linked)"), when, status.matched)
|
||||
end
|
||||
return T(_("Last sync: %1"), when)
|
||||
end,
|
||||
enabled_func = function() return self.settings.auto_sync end,
|
||||
keep_menu_open = true,
|
||||
callback = function(touchmenu_instance)
|
||||
local SpinWidget = require("ui/widget/spinwidget")
|
||||
local items = SpinWidget:new{
|
||||
text = _([[This value determines how many page turns it takes to push book progress.
|
||||
If set to 0, updating progress based on page turns will be disabled.]]),
|
||||
value = self.settings.pages_before_update or 0,
|
||||
value_min = 0,
|
||||
value_max = 999,
|
||||
value_step = 1,
|
||||
value_hold_step = 10,
|
||||
ok_text = _("Set"),
|
||||
title_text = _("Number of pages before update"),
|
||||
default_value = 10,
|
||||
callback = function(spin)
|
||||
self:setPagesBeforeUpdate(spin.value)
|
||||
if touchmenu_instance then touchmenu_instance:updateItems() end
|
||||
end,
|
||||
}
|
||||
UIManager:show(items)
|
||||
end,
|
||||
},
|
||||
{
|
||||
text = _("Sync behavior"),
|
||||
sub_item_table = {
|
||||
{
|
||||
text_func = function()
|
||||
return T(_("Sync to a newer state (%1)"), getNameStrategy(self.settings.sync_forward))
|
||||
end,
|
||||
sub_item_table = self:strategyMenu(
|
||||
function() return self.settings.sync_forward end,
|
||||
function(value) self.settings.sync_forward = value end
|
||||
),
|
||||
},
|
||||
{
|
||||
text_func = function()
|
||||
return T(_("Sync to an older state (%1)"), getNameStrategy(self.settings.sync_backward))
|
||||
end,
|
||||
sub_item_table = self:strategyMenu(
|
||||
function() return self.settings.sync_backward end,
|
||||
function(value) self.settings.sync_backward = value end
|
||||
),
|
||||
},
|
||||
},
|
||||
enabled = false,
|
||||
separator = true,
|
||||
},
|
||||
{
|
||||
text = _("Sync this book now"),
|
||||
@@ -463,61 +456,198 @@ If set to 0, updating progress based on page turns will be disabled.]]),
|
||||
separator = true,
|
||||
},
|
||||
{
|
||||
text_func = function()
|
||||
return self:updateCheckMenuText()
|
||||
end,
|
||||
enabled_func = function()
|
||||
return self:isLoggedIn()
|
||||
and not BookOrbitSweep.isRunning()
|
||||
and not BookOrbitBookSync.isRunning()
|
||||
end,
|
||||
keep_menu_open = true,
|
||||
text = _("Auto sync this book"),
|
||||
checked_func = function() return self.settings.auto_sync end,
|
||||
help_text = _([[Pulls progress when a book is opened; pushes progress, highlights, status, rating and reading time when it is closed and on suspend.]]),
|
||||
callback = function()
|
||||
self:checkForUpdate()
|
||||
self:onBookOrbitToggleAutoSync(nil, true)
|
||||
end,
|
||||
},
|
||||
{
|
||||
text = _("Two-way highlight sync"),
|
||||
checked_func = function() return self.settings.annotation_sync end,
|
||||
help_text = _([[Also applies highlights, notes and deletions made in BookOrbit to this device: on book open, after the manual book sync, and during the full sweep for closed books. Turning this off keeps uploads only.]]),
|
||||
callback = function()
|
||||
self.settings.annotation_sync = not self.settings.annotation_sync
|
||||
end,
|
||||
separator = true,
|
||||
},
|
||||
{
|
||||
text_func = function()
|
||||
local status = BookOrbitSweep.syncStatus()
|
||||
local when = (status.lastSweepAt == 0) and _("never")
|
||||
or os.date("%Y-%m-%d %H:%M", status.lastSweepAt)
|
||||
if status.unmatched > 0 then
|
||||
return T(_("Last sync: %1 (%2 linked, %3 unmatched)"), when, status.matched, status.unmatched)
|
||||
elseif status.matched > 0 then
|
||||
return T(_("Last sync: %1 (%2 linked)"), when, status.matched)
|
||||
end
|
||||
return T(_("Last sync: %1"), when)
|
||||
end,
|
||||
enabled = false,
|
||||
separator = true,
|
||||
text = _("Sync settings"),
|
||||
sub_item_table = {
|
||||
{
|
||||
text_func = function()
|
||||
return T(_("Open dashboard on startup (%1)"), self:catalogAutoOpenLabel())
|
||||
end,
|
||||
sub_item_table = self:catalogAutoOpenMenu(),
|
||||
},
|
||||
{
|
||||
text_func = function()
|
||||
return T(_("Periodically sync every # pages (%1)"), self:getSyncPeriod())
|
||||
end,
|
||||
enabled_func = function() return self.settings.auto_sync end,
|
||||
keep_menu_open = true,
|
||||
callback = function(touchmenu_instance)
|
||||
local SpinWidget = require("ui/widget/spinwidget")
|
||||
local items = SpinWidget:new{
|
||||
text = _([[This value determines how many page turns it takes to push book progress.
|
||||
If set to 0, updating progress based on page turns will be disabled.]]),
|
||||
value = self.settings.pages_before_update or 0,
|
||||
value_min = 0,
|
||||
value_max = 999,
|
||||
value_step = 1,
|
||||
value_hold_step = 10,
|
||||
ok_text = _("Set"),
|
||||
title_text = _("Number of pages before update"),
|
||||
default_value = 10,
|
||||
callback = function(spin)
|
||||
self:setPagesBeforeUpdate(spin.value)
|
||||
if touchmenu_instance then touchmenu_instance:updateItems() end
|
||||
end,
|
||||
}
|
||||
UIManager:show(items)
|
||||
end,
|
||||
},
|
||||
{
|
||||
text_func = function()
|
||||
return T(_("Sync to a newer state (%1)"), getNameStrategy(self.settings.sync_forward))
|
||||
end,
|
||||
sub_item_table = self:strategyMenu(
|
||||
function() return self.settings.sync_forward end,
|
||||
function(value) self.settings.sync_forward = value end
|
||||
),
|
||||
},
|
||||
{
|
||||
text_func = function()
|
||||
return T(_("Sync to an older state (%1)"), getNameStrategy(self.settings.sync_backward))
|
||||
end,
|
||||
sub_item_table = self:strategyMenu(
|
||||
function() return self.settings.sync_backward end,
|
||||
function(value) self.settings.sync_backward = value end
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
text = _("Account & setup"),
|
||||
sub_item_table = {
|
||||
{
|
||||
text = _("BookOrbit server address"),
|
||||
keep_menu_open = true,
|
||||
callback = function()
|
||||
self:setServerAddress()
|
||||
end,
|
||||
},
|
||||
{
|
||||
text_func = function()
|
||||
return self.settings.userkey and _("Logout") or _("Login")
|
||||
end,
|
||||
enabled_func = function()
|
||||
return self.settings.server_url ~= nil
|
||||
end,
|
||||
keep_menu_open = true,
|
||||
callback_func = function()
|
||||
if self.settings.userkey then
|
||||
return function(menu)
|
||||
self:logout(menu)
|
||||
end
|
||||
else
|
||||
return function(menu)
|
||||
self:login(menu)
|
||||
end
|
||||
end
|
||||
end,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
function BookOrbit:browseCatalog()
|
||||
function BookOrbit:dashboardMenuItems(catalog)
|
||||
local menu_items = {}
|
||||
self:addToMainMenu(menu_items)
|
||||
local bookorbit_items = menu_items.bookorbit.sub_item_table or {}
|
||||
local items = {
|
||||
icon = "appbar.menu",
|
||||
}
|
||||
for index, item in ipairs(bookorbit_items) do
|
||||
if index ~= 1 then
|
||||
table.insert(items, item)
|
||||
end
|
||||
end
|
||||
return items
|
||||
end
|
||||
|
||||
function BookOrbit:showDashboardMenu(catalog)
|
||||
local CenterContainer = require("ui/widget/container/centercontainer")
|
||||
local TouchMenu = require("ui/widget/touchmenu")
|
||||
if self.dashboard_menu_container then
|
||||
local old_container = self.dashboard_menu_container
|
||||
self.dashboard_menu_container = nil
|
||||
UIManager:close(old_container)
|
||||
end
|
||||
local menu_container = CenterContainer:new{
|
||||
covers_header = true,
|
||||
ignore = "height",
|
||||
dimen = Device.screen:getSize(),
|
||||
}
|
||||
local dashboard_menu = TouchMenu:new{
|
||||
width = Device.screen:getWidth(),
|
||||
last_index = 1,
|
||||
tab_item_table = { self:dashboardMenuItems(catalog) },
|
||||
show_parent = menu_container,
|
||||
}
|
||||
local closing = false
|
||||
dashboard_menu.close_callback = function()
|
||||
if closing then return true end
|
||||
closing = true
|
||||
if self.dashboard_menu_container == menu_container then
|
||||
self.dashboard_menu_container = nil
|
||||
end
|
||||
UIManager:close(menu_container)
|
||||
return true
|
||||
end
|
||||
menu_container[1] = dashboard_menu
|
||||
self.dashboard_menu_container = menu_container
|
||||
UIManager:show(menu_container)
|
||||
end
|
||||
|
||||
function BookOrbit:openCatalogBrowser(prefer_cached_dashboard)
|
||||
if self.catalog_browser ~= nil then return end
|
||||
self.catalog_browser = BookOrbitCatalog:new{
|
||||
title = _("BookOrbit"),
|
||||
api = self:apiOpts(),
|
||||
settings = self.settings,
|
||||
prefer_cached_dashboard = prefer_cached_dashboard,
|
||||
save_settings = function()
|
||||
G_reader_settings:flush()
|
||||
end,
|
||||
show_dashboard_menu = function(catalog)
|
||||
self:showDashboardMenu(catalog)
|
||||
end,
|
||||
_manager = self,
|
||||
close_callback = function()
|
||||
UIManager:close(self.catalog_browser)
|
||||
self.catalog_browser = nil
|
||||
end,
|
||||
}
|
||||
UIManager:show(self.catalog_browser)
|
||||
end
|
||||
|
||||
function BookOrbit:browseCatalog(allow_offline)
|
||||
if not self:isLoggedIn() then
|
||||
promptLogin()
|
||||
return
|
||||
end
|
||||
|
||||
NetworkMgr:runWhenConnected(function()
|
||||
self.catalog_browser = BookOrbitCatalog:new{
|
||||
title = _("BookOrbit"),
|
||||
api = self:apiOpts(),
|
||||
settings = self.settings,
|
||||
save_settings = function()
|
||||
G_reader_settings:flush()
|
||||
end,
|
||||
_manager = self,
|
||||
close_callback = function()
|
||||
UIManager:close(self.catalog_browser)
|
||||
self.catalog_browser = nil
|
||||
end,
|
||||
}
|
||||
UIManager:show(self.catalog_browser)
|
||||
end)
|
||||
if allow_offline then
|
||||
self:openCatalogBrowser(true)
|
||||
else
|
||||
NetworkMgr:runWhenConnected(function()
|
||||
self:openCatalogBrowser(false)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
function BookOrbit:setServerAddress()
|
||||
@@ -1006,7 +1136,7 @@ function BookOrbit:onBookOrbitToggleAutoSync(toggle, from_menu)
|
||||
and Device:hasSeamlessWifiToggle()
|
||||
and G_reader_settings:readSetting("wifi_enable_action") ~= "turn_on" then
|
||||
UIManager:show(InfoMessage:new{
|
||||
text = _("You will have to switch the 'Action when Wi-Fi is off' Network setting to 'turn on' to be able to enable this feature!"),
|
||||
text = _("Auto sync needs KOReader to turn Wi-Fi on without asking. Open Network settings, set 'Action when Wi-Fi is off' to 'Turn on', then enable Auto sync again."),
|
||||
})
|
||||
return true
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user