Commit graph

18 commits

Author SHA1 Message Date
Chmouel Boudjnah
eb24db77af
feat(sync): keep your place through a KOReader sync server (#101)
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
2026-08-29 00:30:31 +02:00
Chmouel Boudjnah
744fca3594 feat(reader): show footnotes on the page instead of at the back of the book
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
2026-08-19 14:38:21 +02:00
Chmouel Boudjnah
b2776890fc feat: Add book series manual ordering and refiling
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>
2026-08-14 14:43:03 +02:00
dependabot[bot]
8a1a63a89c chore(deps): bump the gradle group with 2 updates
Bumps the gradle group with 2 updates: [org.robolectric:robolectric](https://github.com/robolectric/robolectric) and [org.jetbrains.kotlinx:kotlinx-coroutines-test](https://github.com/Kotlin/kotlinx.coroutines).


Updates `org.robolectric:robolectric` from 4.16 to 4.16.1
- [Release notes](https://github.com/robolectric/robolectric/releases)
- [Commits](https://github.com/robolectric/robolectric/compare/robolectric-4.16...robolectric-4.16.1)

Updates `org.jetbrains.kotlinx:kotlinx-coroutines-test` from 1.10.2 to 1.11.0
- [Release notes](https://github.com/Kotlin/kotlinx.coroutines/releases)
- [Changelog](https://github.com/Kotlin/kotlinx.coroutines/blob/master/CHANGES.md)
- [Commits](https://github.com/Kotlin/kotlinx.coroutines/compare/1.10.2...1.11.0)

---
updated-dependencies:
- dependency-name: org.robolectric:robolectric
  dependency-version: 4.16.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: gradle
- dependency-name: org.jetbrains.kotlinx:kotlinx-coroutines-test
  dependency-version: 1.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: gradle
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-02 17:05:16 +02:00
Chmouel Boudjnah
553495d3de
test: prove an upgrade never loses your library
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.
2026-07-29 16:50:37 +02:00
Chmouel Boudjnah
c45f85b648
test: prove a page turn cannot be lost mid-sync
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.
2026-07-29 16:44:39 +02:00
Chmouel Boudjnah
d64c582d39
test: pin down what happens when two syncs overlap
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.
2026-07-29 16:20:09 +02:00
Chmouel Boudjnah
431374d2b4
fix: sync where you got to when you open the app, not just when it starts
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.
2026-07-29 14:25:14 +02:00
dependabot[bot]
14cd22a16f chore(deps): bump the gradle group with 3 updates
Bumps the gradle group with 3 updates: [org.json:json](https://github.com/douglascrockford/JSON-java), com.android.application and [org.jetbrains.kotlin.plugin.compose](https://github.com/JetBrains/kotlin).


Updates `org.json:json` from 20250107 to 20260719
- [Release notes](https://github.com/douglascrockford/JSON-java/releases)
- [Changelog](https://github.com/stleary/JSON-java/blob/master/docs/RELEASES.md)
- [Commits](20250107...20260719)

Updates `com.android.application` from 9.3.0 to 9.3.1

Updates `org.jetbrains.kotlin.plugin.compose` from 2.4.0 to 2.4.10
- [Release notes](https://github.com/JetBrains/kotlin/releases)
- [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md)
- [Commits](https://github.com/JetBrains/kotlin/compare/v2.4.0...v2.4.10)

---
updated-dependencies:
- dependency-name: org.json:json
  dependency-version: '20260719'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: gradle
- dependency-name: com.android.application
  dependency-version: 9.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: gradle
- dependency-name: org.jetbrains.kotlin.plugin.compose
  dependency-version: 2.4.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: gradle
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-29 12:35:06 +02:00
Chmouel Boudjnah
b5e0853d2d
chore: make the build depend on less trust
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.
2026-07-29 11:17:06 +02:00
Chmouel Boudjnah
a73229adf8
fix: connect without encryption, when you ask for it
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.
2026-07-29 10:40:55 +02:00
Chmouel Boudjnah
bd83da72d5 feat: find words in a book, and find out what they mean
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.
2026-07-29 01:40:53 +02:00
Chmouel Boudjnah
f6cab58f22
feat: show the books on your calibre-web server
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.
2026-07-29 00:13:26 +02:00
Chmouel Boudjnah
3169c4b7a6
feat: connect to a calibre-web server
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.
2026-07-29 00:04:36 +02:00
Chmouel Boudjnah
f515723c0b
feat: reading settings with fonts, themes and brightness
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.
2026-07-28 22:12:03 +02:00
Chmouel Boudjnah
3983bb70dc
feat: local library with folder scanning and cover grid
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.
2026-07-28 21:50:46 +02:00
Chmouel Boudjnah
b03ec3e1a6
feat: add EPUB reading with Readium navigator
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.
2026-07-28 21:38:21 +02:00
Chmouel Boudjnah
697d773ee8
feat: scaffold Android project with Compose app shell
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.
2026-07-28 21:21:07 +02:00