No description
  • Ruby 41.4%
  • C++ 27.9%
  • Kotlin 24.2%
  • Python 3.2%
  • Shell 1.7%
  • Other 1.6%
Find a file
brad b81f9c60cb
All checks were successful
/ Build arm64 package (cross-compiled) (push) Successful in 1m0s
/ Build x86_64 package (push) Successful in 1m41s
/ Run cppcheck static analysis (push) Successful in 38s
/ Check for duplicate code (push) Successful in 39s
/ Build Android APK (push) Successful in 4m11s
/ Ruby syntax check (push) Successful in 42s
/ Run server tests (push) Successful in 3m35s
/ Run gcc static analyzer to detect possible bugs (push) Successful in 1m22s
/ Run end-to-end client-server test (push) Successful in 3m35s
Deduplicate login/registerUser via authRequest helper
2026-08-15 13:00:27 -05:00
.forgejo/workflows
android
client Deduplicate login/registerUser via authRequest helper 2026-08-15 13:00:27 -05:00
doc Sync time tracking to server per-device (Phase 6.3) 2026-08-15 12:03:25 -05:00
scripts
server Sync time tracking to server per-device (Phase 6.3) 2026-08-15 12:03:25 -05:00
.gitignore
docker-compose.yml
Dockerfile
Dockerfile.cross-aarch64
duplo.ignore
PKGBUILD
README.md

NovelReader

An offline-first web novel reader. It consists of three components in one monorepo:

  • client/ — Qt5 C++ reader for mobile Linux phones (PinePhone running Arch Linux ARM). x86_64 builds are maintained for debugging during development.
  • server/ — Lightweight Ruby API server (WEBrick + Sequel + ERB) with auth, catalog/chapter/update endpoints, a polite scraping pipeline, search, per-user sync endpoints (reading progress, library, time tracking), and admin tooling (takedown, novelpack import/export, registrations).
  • android/ — Feature-parity Android reader (Kotlin + Jetpack Compose).

Features

Client (PinePhone Qt5 + Android)

  • Library Browse novels organized in a hierarchical group/leaf structure (SQLite-backed)
  • Reader Rich-text chapter view with scroll-position persistence and progress tracking
  • Automatic updates Pulls novel updates from a configurable server via API (GET /api/v1/head + GET /api/v1/updates)
  • Offline-first caching Opportunistic download on unmetered connections, LRU eviction within a configurable storage limit, favorites protection
  • Novelpack import Import .novelpack archives from local/USB storage
  • Time tracking Per-novel and per-day reading statistics (opt-in, local-first)
  • Reading history Daily activity view
  • Touch-friendly Custom TouchScrollArea and TouchButton widgets for finger scrolling

Server (Ruby)

  • Auth with bcrypt, session/bearer tokens, registration requiring admin approval (first user auto-activated)
  • Catalog, chapter, update, search, and per-user sync endpoints (/api/v1/...)
  • Per-endpoint rate limiting (login, chapter downloads, search, general API abuse)
  • Polite scraping pipeline (Scraper::Base) with rate limiting, robots.txt respect, caching, backoff, and DB-backed retry state
  • Admin endpoints: trigger scraping, view logs, approve/reject registrations, toggle registration, content takedown (410 Gone), per-user data export, novelpack export/import
  • Data retention pruning (configurable period, default 365 days) and user account deletion
  • bin/novelpack CLI for creating/inspecting/extracting .novelpack archives

Building the PinePhone client

Native build (x86_64)

cd client/src && make -j$(nproc)

Cross-compilation (aarch64)

The PKGBUILD supports cross-compilation for aarch64 (PinePhone) using the Arch Linux cross-toolchain. Set CROSS_COMPILE=1 to enable:

CROSS_COMPILE=1 makepkg

See Dockerfile.cross-aarch64 for the full cross-compilation environment setup.

Building the Android client

cd android && ./build-android.sh

This builds and signs the release APK entirely inside a Docker container (JDK + Android SDK + Gradle); the host is never polluted with a toolchain.

Android Docker images

Two images are used:

  • novelreader-android-build — the SDK/Gradle toolchain, built locally by build-android.sh for APK builds and tests. It installs Build Tools 34 and 35.
  • git.kargulok.net/brad/novelreader-android-ci:latest — the CI image. It extends the build image and pre-warms the Gradle and Robolectric caches so the CI android-build job can run fully offline. It is a multi-stage build that copies only the offline-essential caches (~/.gradle/caches/modules-2 and ~/.m2) into separate layers, keeping each image layer small for reliable registry pushes. The Gradle transform cache is intentionally omitted: Gradle re-transforms the cached jars locally, so it is not needed offline.

Rebuild and publish the CI image whenever Gradle dependencies change (build.gradle.kts / settings.gradle.kts), otherwise the offline CI steps fail on missing artifacts:

cd android && ./build-ci-image.sh

This builds both images and pushes the CI image to the registry. The signing keystore is excluded from the image via android/.dockerignore; CI uses the keystore from the checked-out repo, never the image.

Running the server

cd server && bundle exec ruby app.rb

See server/README.md for details.

Testing

  • Server integration/unit tests: cd server && bundle exec ruby test/all_tests.rb && bundle exec ruby test/rate_limit_tests.rb
  • Android JVM unit/integration tests: gradle :app:testDebugUnitTest (in the android/ Docker build container)
  • PinePhone end-to-end test: scripts/e2e-test

Building the duplo-check image

scripts/duplo-check wraps the duplo duplicate-code checker with an ignore list (duplo.ignore) so approved blocks can be excluded from CI failures. It runs inside a dedicated image:

docker build -f scripts/Dockerfile.duplo -t novelreader-duplo .
docker tag novelreader-duplo git.kargulok.net/brad/novelreader-duplo:latest
docker push git.kargulok.net/brad/novelreader-duplo:latest

To seed the ignore list with the current findings:

find ./client/src -name '*.cpp' -o -name '*.hpp' | grep -v moc_ | python3 scripts/duplo-check --list-keys

Installing the PinePhone package

Build the Arch Linux package with makepkg and install it:

makepkg -si

The binary is installed to /opt/novel-reader/NovelReader.

Project structure

├── client/
│   └── src/                     # Qt5 C++ client
│       ├── NovelReader.pro      # qmake project file
│       ├── main.cpp             # Entry point
│       ├── top_level.cpp/hpp    # Main window with bottom tab navigation
│       ├── library.cpp/hpp      # Novel library browser
│       ├── reader.cpp/hpp       # Chapter reader view with time tracking
│       ├── updates.cpp/hpp      # Update fetcher and resolver
│       ├── history.cpp/hpp      # Daily reading history
│       ├── more.cpp/hpp         # Settings (server URL configuration)
│       └── widgets/             # Touch-friendly UI widgets
│           ├── touch_button.cpp/hpp
│           └── touch_scroll_area.cpp/hpp
├── server/                      # Ruby API server
│   ├── app.rb                   # WEBrick entry point + routes
│   ├── app/                     # controllers (incl. admin), models, scrapers, pipeline, scheduler, novelpack
│   ├── lib/                     # Router, ControllerBase, rate limiter, retention
│   ├── db/migrations/           # Sequel migrations
│   └── test/                    # Integration + unit tests
├── android/                     # Kotlin + Jetpack Compose client
│   ├── app/src/main/            # Android app source
│   ├── app/src/test/            # JVM unit/integration tests
│   └── build-android.sh         # Dockerized APK build
├── doc/                         # Spec, plan, requirements, testing matrix
└── PKGBUILD                     # Arch Linux package definition

License

GPL-3.0-or-later