Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,28 @@ All notable changes to Tome are documented here. Format loosely follows
## [Unreleased]

### Added
- **Search your library from the device (KOReader plugin, build 33).** The
series browser gains a "Search library…" entry: submit-based free-text search
over title, author, and series, with your last searches one tap away. Results
open the same drill-down list as a series — tap to download, hold to set read
status.
- **Browse by author on the device (build 33).** A new author axis in the
series browser — the natural way into standalone books, which previously all
hid behind the single "No Series" bucket. Downloads file each book by its own
series/author identity, exactly like the web.
- **Set read status from the device browser (build 33).** Hold any book row in
a series, author, or search list to mark it unread / reading / read on the
server — no need to open the book. The lists also show each book's current
status alongside the existing "on device" marker.

- **Position pull strategy (KOReader plugin, build 32).** What happens on book
open when the server position differs from the device is now configurable,
like stock KOSync: "Server position is ahead" and "Server position is
behind" each offer *Ask before jumping / Jump automatically / Do nothing*
(TomeSync settings). Defaults keep the historic behavior — forward jumps
happen silently, backward jumps never — and the ask-dialog is deferred a
moment after open so it can never swallow a "on book opening" profile the
way the old layout-reset bug did.
- **Highlights page polish.** The deferred cluster from the original
commonplace-book release: an **only-notes filter** (show just the highlights
carrying your own notes — composes with search and on-this-day), a real
Expand All @@ -29,6 +51,25 @@ All notable changes to Tome are documented here. Format loosely follows
pagination is not a property of the book). Backfilled by the same admin job.

### Fixed
- **A fast server clock can no longer silently swallow highlights (build 32).**
Highlight edits and deletes made in the web reader are stamped with the
server's clock; on a device whose clock runs behind (UTC container vs local
device time), those stamps landed in the device's future and outranked every
later local change — a re-highlight after a web delete just vanished until
the clocks crossed. The plugin now stamps its wall-clock on every highlight
sync and the server shifts the stamps it minted itself into that device's
clock frame, both when comparing and in what it returns; the plugin
additionally refuses to store any stamp from the future. Older plugins keep
the previous behavior.
- **The TomeSync plugin no longer bloats KOReader's global settings file
(build 32).** Its data tables (book map, pending sessions and ratings, sync
baselines, repair aliases) — which grow with your library and were parsed by
KOReader at every boot — moved into a dedicated `tomesync_state.lua`,
migrated automatically and crash-safely on first launch. State for books no
longer on the device is pruned; pending queues are never pruned.
- **The plugin's series list no longer runs one query per series.** Loading the
series browser was an N+1 that scaled with the library; it's now a single
query with the same response.
- **The Highlights "copy all as Markdown" export never worked.** It requested
more highlights than the API's page cap allowed, got a 422, and failed
without any feedback. The cap is raised, the export stays under it, and a
Expand Down
22 changes: 19 additions & 3 deletions backend/api/annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,8 @@ def create_annotation(
if len(text_) > 20_000:
raise HTTPException(status_code=422, detail="highlighted_text too long")

now = (payload.datetime or "").strip()[:19] or func_now_str()
client_dt = (payload.datetime or "").strip()[:19]
now = client_dt or func_now_str()
row = Annotation(
user_id=current_user.id,
book_id=payload.book_id,
Expand All @@ -188,6 +189,9 @@ def create_annotation(
color=(payload.color or None),
koreader_datetime=now,
koreader_datetime_updated=now,
# Stamp origin: only a server-clock fallback is "server-minted" —
# plugin syncs shift such stamps into the device's clock frame.
server_minted=not client_dt,
)
db.add(row)
db.commit()
Expand Down Expand Up @@ -225,14 +229,20 @@ def edit_annotation(
# The edit must be STRICTLY newer than the current mtime to win LWW on
# devices; guard against a server clock at/behind the device's wall-clock.
new_mtime = func_now_str()
minted = True
if new_mtime <= (row.effective_mtime or ""):
from datetime import datetime as _dt, timedelta as _td
try:
base = _dt.strptime(row.effective_mtime, "%Y-%m-%d %H:%M:%S")
new_mtime = (base + _td(seconds=1)).strftime("%Y-%m-%d %H:%M:%S")
# Bumped off the row's existing mtime, so the new stamp lives in
# whatever frame that stamp was in (device unless it was itself
# server-minted) — inherit, don't assume server frame.
minted = row.server_minted
except ValueError:
pass
row.koreader_datetime_updated = new_mtime
row.server_minted = minted
db.commit()
db.refresh(row)
return _annotation_out(row)
Expand Down Expand Up @@ -269,7 +279,12 @@ def delete_annotation(
# drop copies with mtime <= tombstone — so maxing with effective_mtime guarantees
# the delete holds against the exact copy that was deleted, while a genuinely
# newer re-add (strictly greater mtime) still wins and clears the tombstone.
now = max(func_now_str(), annotation.effective_mtime)
server_now = func_now_str()
now = max(server_now, annotation.effective_mtime)
# The stamp is only "server-minted" (shifted into device frames on plugin
# sync) when the server clock actually won the max — if the highlight's own
# device mtime won, the stamp is already in that device's frame.
minted = now == server_now and now != annotation.effective_mtime

db.delete(annotation)

Expand All @@ -286,10 +301,11 @@ def delete_annotation(
# Keep the latest deletion time so a stale re-add can't slip under it.
if now > (tomb.client_deleted_at or ""):
tomb.client_deleted_at = now
tomb.server_minted = minted
else:
db.add(AnnotationTombstone(
user_id=current_user.id, book_id=book_id, anchor=anchor,
client_deleted_at=now,
client_deleted_at=now, server_minted=minted,
))

db.commit()
Expand Down
Loading
Loading