Pair a Grimmory connection with a KOReader (kosync) server so a book's
position follows the reader between Liseur and KOReader. The pairing is
offered only where the connected server carries no position of its own,
which today means Grimmory; a server that syncs natively would give one
book two sources and a conflict the reader cannot see.
Closes#95
Tapping a footnote marker threw the reader to the book's endnotes, several
hundred pages away, with nothing recording where the tap came from. Readium
had already done the hard half of the work — `R2BasicWebView.handleFootnote`
recognises `epub:type="noteref"`, fetches the target and sanitises it into a
`HyperlinkNavigator.FootnoteContext` — but `createFragmentFactory` was called
without a `listener`, so `EpubNavigatorViewModel` had nobody to ask and let
every link navigate, discarding the note it had just extracted.
Supply that listener. Two paths now converge on one card:
- Readium's, when the marker carries `epub:type="noteref"` and the note
content arrives with the callback.
- Liseur's own, for everything Readium does not recognise: `role`
`doc-noteref`, endnotes and rearnotes, `<aside>`, and plain fragment
links whose target turns out to be a note. `shouldFollowInternalLink`
is not suspending, so these always answer "no" and resolve on
`Dispatchers.IO`; a target that is not a note is navigated to by hand,
landing the reader exactly where they would have been.
That detour has a second use. Following an internal link now calls `onJump()`
first, so a cross-reference finally leaves a "Back to page N" pill behind —
which it never did, footnote or not.
The card is painted in the reading theme rather than in Material colours, and
its scrim always darkens, so a night page recedes instead of being washed
pale. It is an overlay inside the reader's own box rather than a `Popup`,
because the scrim has to cover the same area the tap zones do or the tap
meant to dismiss a note turns a page. Long notes keep an escape hatch: "Go to
note" navigates and leaves its own way back.
External links are no longer followed silently. A dialog names the host
first, so the promise that the app only talks to your own book server is not
quietly broken by a stray tap.
Jsoup does the parsing. It was already on the classpath through
`readium-navigator`; it is now declared rather than leaned on.
Closes#30
A series is only ever as right as whoever filed it. Long-press a book to
put it into a series, one that exists or a new name, to give it a volume
number, or to take it out of a series it never belonged in; the answer
outranks the server's, survives every refresh, and can be given back.
Where the numbers themselves are wrong, open the series and put it in
order by the drag handle or the arrows. Done renumbers it 1, 2, 3, which
is the order everything asking what to read next then reads. Clearing
the custom numbers hands the numbering back to the server and the files.
Two books make a series. calibre gives most standalones a series of
their own, so a book alone in one is shown as a book, with no card and
no volume number, until a second is filed beside it.
Signed-off-by: Chmouel Boudjnah <chmouel@chmouel.com>
Every database change since the first release is now replayed in a test,
from each old version all the way to the current one, checking that a
book and a reading position written by the oldest version are still
there at the end.
Removing a single upgrade step makes all three tests fail, so this
really is checking the upgrades rather than just that the app starts.
The reading-position table is written by conditional SQL, and every way
of getting it wrong loses a position without leaving a mark: the row
still looks settled, it is just settled somewhere the reader never was.
None of it was covered, because it needs a real database.
Room now runs on the JVM under Robolectric, so these run in CI rather
than only on a device. They cover the transitions a page turn can land
in the middle of: a pull abandoned because the reader moved on, keeping
both positions instead of overwriting one; a push whose acknowledgement
is refused because it named a revision that was overtaken; dirtiness
that survives a clock going backwards; and an account switch that keeps
the reading, drops the stranger's baseline, and uploads nothing until
somebody says so.
Deciding whether a request can be answered by a sync already running is
the trickiest piece of the app, and getting it wrong is invisible: the
answer still reads as "synced", it just did not sync the thing that was
asked about. That was the pull-to-refresh bug, and nothing stopped it
coming back.
The coordinator now takes a small interface rather than the repository
itself, so the rules can be exercised on their own with a controlled
clock: a request that arrived too late to be covered gets its own run,
one already covered joins in, two of the same wait together, a full sync
never settles for a single book's, and a run that throws does not leave
everyone behind it stuck. Clock skew in the other direction is covered
alongside the existing merge cases.
Reading positions only travelled between devices when the app's process
was created from scratch, which Android may not do for days, or when the
sync button was pressed by hand. Everything else left them behind.
Now positions are exchanged when the app is brought to the foreground,
when the library is pulled to refresh, and when a book is opened, so the
page you resume on is the one you actually stopped at on the other
device. Opening a book waits only briefly for the server and falls back
to the local position, so a book always opens, online or not.
Background syncing also runs hourly rather than every six hours and no
longer waits for an unmetered network, and the new schedule now replaces
the old one on existing installs instead of being ignored.
The wrapper downloaded Gradle from a URL and ran whatever came back, and
every GitHub Action was pinned to a tag, which is a name someone else
can move. Now the wrapper checks the distribution against a known
checksum and each action is pinned to the commit it was when it was
reviewed, with the tag kept alongside as a comment.
CI also builds the release APK on every pull request. That is the build
people install and the only one that runs R8, so a shrinking rule that
strips something Readium needs should fail in review rather than on the
evening a tag is cut.
The button offering plain HTTP never switched to it. It changed the
wording of the error and retried the same HTTPS address, so a
calibre-web on a home network with no certificate could not be reached
at all, however many times you pressed it.
HTTPS is still tried first every time, including when refreshing a saved
account, so a server that has a certificate is never quietly downgraded.
Agreeing to plain HTTP now says plainly what it costs: the password
travels with every request, and anyone on the network can read it and
change what comes back.
Dictionary lookups are held to HTTPS regardless.
Search inside the book you are reading from the reader's top bar:
results stream in as the book is scanned, each with the chapter it
came from and a snippet trimmed around the match, and tapping one
takes you there with the matched words still tinted on the page so
your eye lands on them.
Define now answers in the app instead of throwing you at whatever
happens to handle text on the device. Tapping a word opens a card
with its senses from Wiktionary, in the book's language first and
English as a fallback, with the full entry and any dictionary app
you have installed one tap away.
The library now lists everything in your calibre collection alongside
the books already on the phone, each with its cover and a small cloud
mark for the ones that have yet to be fetched.
The list is kept on the device, so it is all still there to look through
on a train with no signal.
Adding a server now asks for nothing more than its address and the login
you already use. Liseur finds the catalogue, checks whether the account
is allowed to download books, and quietly sets up reading position sync
so your place follows you between devices.
When something is wrong it says which thing: a refused password reads
differently from an address where no library answered.
The password is kept encrypted on the device, since the server insists
on being told it with every request.
A new Aa menu in the reader lets you change how books look, with
every change applied instantly and remembered for the next time:
- Four color themes: light, sepia, dark, and pitch black.
- A choice of quality reading fonts bundled with the app: Literata,
Vollkorn, Atkinson Hyperlegible, and Inter, plus the book's own
font. Each name in the menu is shown in its own typeface.
- A font size slider and a screen brightness slider, with a button
to go back to the system brightness.
- Line spacing and margin options.
The menu bar and page colors follow the chosen theme. Fonts are
licensed under the SIL Open Font License.
The library screen now shows your books instead of a blank page.
You can add a folder of EPUBs and Liseur will scan it, pull out
each book's title, author, and cover, and lay them out in a grid.
A card at the top lets you jump back into the book you were last
reading, with a progress bar showing how far along you are.
Books opened one at a time are also remembered in the library.
Removed files disappear from the library on the next scan.
Books open from the file picker or from other apps and render in a
paginated reader with a table of contents and the paper theme. The
reading position is saved locally and restored when the book is
reopened, even after the app is killed.
Set up the Gradle build (AGP 9, Kotlin, Compose, version catalog),
a paper-and-ink Material 3 theme with splash screen and adaptive
book icon, an empty library screen, CI workflow, F-Droid fastlane
metadata, and project documentation.