Skip to content
Open
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
332 changes: 268 additions & 64 deletions DESIGN.md

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions PRODUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ Library members on their own phone, mostly in daylight or normal indoor light,
in a calm "browsing / reading" mood. Not power users; not a dark-room tool.

## Brand & tone
Pinakes. One signature colour: **magenta `#D70161`** (the palm logo). Tone:
Pinakes. Default accent: **magenta `#D70161`** (the palm logo); other library themes supply their own accent. Tone:
quiet, precise, trustworthy, a little warm. Think a good reading app, not a
flashy consumer social app.

## Strategic principles
- **Minimal, not decorated.** Content (book covers, titles, availability) is the
hero. Chrome recedes.
- **One accent.** Magenta is the only colour with meaning: primary actions,
- **One accent per library theme.** It marks primary actions,
current selection, key status. Everything else is a clean neutral. No second
brand hue, no indigo/violet, no rainbow chips.
- **Readable first.** Real contrast on every label and field. Never colour-on-
Expand All @@ -30,8 +30,8 @@ flashy consumer social app.
surface. Dark is available, not assumed.

## Anti-references (what this must NOT look like)
- Gradients of any kind (hero banners, headers, buttons). Banned.
- Decorative gradients on buttons, text or cards. The subtle 2026 hero wash and the book spine/gloss are the deliberate exceptions.
- Purple/indigo selection states. The "random colours" look. Banned.
- Warm brown/dun surfaces. Neutrals stay cool and clean.
- Heavy brown surfaces. The 2026 warm off-white neutrals stay light and quiet.
- Decorated circular logo badges, ringed avatars, heavy cards everywhere.
- "AI slop": a washed-out gradient panel with light text on a light field.
72 changes: 63 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
Native Android client for a [Pinakes](https://github.com/fabiodalez-dev/Pinakes)
library instance. Browse the catalog, check real availability, borrow and reserve
books, read ebooks and listen to audiobooks, and manage your loans, all from your
phone.
phone. Optional Archives, Desiderata and analytic articles are available when the
server advertises their APIs. The five bottom navigation destinations are retained.

![Platform](https://img.shields.io/badge/platform-Android-3DDC84)
![minSdk](https://img.shields.io/badge/minSdk-26-blue)
Expand All @@ -30,8 +31,14 @@ The app is server-agnostic: it works against any Pinakes instance that has the
Mobile API enabled, and it adapts to that instance's settings (language,
catalogue-only mode, push availability).

## 2026 interface

The Android interface follows the public Pinakes 2026 restyle: shared theme tokens, Geist for controls, Fraunces for titles, fitted book artwork and a quiet warm background. `ThemePalette` accepts library colours and hero/card styles; Classic / Covers are the built-in defaults until the Mobile API exposes theme settings. Existing circulation, account and plugin flows remain available. See [DESIGN.md](DESIGN.md) for the component contract and API limitations.

## Screenshots

Login, home, catalog, book detail and calendar captures use the 2026 interface on Android 15 at 360 dp, with fixture data. The remaining account captures document the earlier flow layout.

<table>
<tr>
<td align="center"><img src="docs/screenshots/login.png" width="200" alt="Login screen"><br><sub>Login</sub></td>
Expand All @@ -53,10 +60,10 @@ catalogue-only mode, push availability).
|------|--------------|
| **Onboarding** | Enter the instance URL, `/health` discovery card (library name, logo, HTTPS check, mobile-access check) |
| **Sign in & sign up** | Email/password login, **in-app registration** and **password recovery**, mapped error messages, secure token storage |
| **Home** | An "Available now" landing showing what you can borrow today |
| **Catalog** | The full catalog with infinite scroll, search, and a filter sheet (availability, genre, author, publisher, language) |
| **Book detail** | HTML-rendered description, tap-to-zoom cover, full metadata block (ISBN, year, pages …), genre chip |
| **Availability** | Colour-coded state: green available, red on loan, amber reserved |
| **Home** | Searchable library hero, a fan of real shelf covers, and an "Available now" / recent shelf |
| **Catalog** | Two-column book grid or compact list, infinite scroll, search, sort and a filter sheet (availability, genre, author, publisher, language) |
| **Book detail** | HTML-rendered description, tap-to-zoom cover, full metadata block (ISBN, year, pages …), genre hierarchy |
| **Availability** | Neutral status pills with a coloured dot: green available, red on loan, amber reserved |
| **Loan calendar** | Pick a start date on a calendar that paints already-booked days and pre-selects the first free day |
| **Audiobooks** | In-app player (Media3 ExoPlayer) when the title has an audio file |
| **Ebooks** | In-app PDF reader (PdfRenderer); other formats open externally |
Expand All @@ -65,13 +72,13 @@ catalogue-only mode, push availability).
| **Book Club** | When the instance runs the **Book Club** plugin: browse your clubs and the directory, open a club's reading list / polls / meetings, join, vote in-app (simple / multi / weighted ballots), RSVP to meetings and track your reading progress — advanced poll modes and proposing a title deep-link to the web |
| **Profile** | Edit profile, change password, device list, theme switcher, language switcher, logout |
| **Notifications** | Loan due/overdue, reservation ready, book available |
| **Themes** | Material 3 light and dark, light by default; pick light/dark/system in Profile |
| **Themes** | 2026 warm neutrals, Geist + Fraunces, complete 3D covers and paired theme colours. Light by default; pick light/dark/system in Profile |
| **Languages** | Italian, English, French, German, following the device locale or an in-app choice |

## Tech stack

- **Kotlin 2.0** + **Jetpack Compose** (Material 3), single-module app
- **Navigation-Compose** + `ViewModel`/`StateFlow`, manual DI via a `ServiceLocator`
- **Navigation-Compose** + `ViewModel`/`StateFlow` + Hilt
- **Retrofit + OkHttp + kotlinx.serialization** for the `/api/v1` client (`{data, meta, error}` envelope)
- **Coil** for cover images, **Media3 ExoPlayer** for audio, platform `PdfRenderer` for PDFs
- **AndroidX Security** (`EncryptedSharedPreferences`) for the bearer token and instance URL
Expand All @@ -89,6 +96,9 @@ catalogue-only mode, push availability).
```bash
./gradlew assembleDebug # debug APK → app/build/outputs/apk/debug/app-debug.apk
./gradlew lintDebug # static analysis
./gradlew testDebugUnitTest
./gradlew connectedDebugAndroidTest # UI regression tests, with a running emulator
./gradlew assembleRelease # R8 + resource shrinking, unsigned without release credentials
```

Create a `local.properties` with `sdk.dir=/path/to/android-sdk` (or set `ANDROID_HOME`).
Expand All @@ -102,6 +112,29 @@ adb install -r app/build/outputs/apk/debug/app-debug.apk

A prebuilt debug APK is published on the [Releases](../../releases) page.

### Standalone emulator on macOS

```bash
./tools/run-emulator.sh my_avd -no-snapshot-load -gpu auto
```

The launcher forwards the remaining options to the Android SDK emulator. On macOS,
it holds a `caffeinate` assertion for that emulator PID, then releases it when the
emulator exits; Ctrl+C stops both. This reduces host power throttling during local
testing. It does not change global power
settings or Android crash reporting.

An Android 15 startup ANR was reproduced before application initialization, along
with system/launcher stalls. With the same debug APK, host priority dropped to 4
without the assertion and startup exceeded 21 seconds; with the assertion, three
cold starts completed in 2.3–3.1 seconds. After restarting the VM through this
launcher, five more cold starts completed in 1.66–1.80 seconds with no ANR events.
An older AVD still stalled after a cold boot, and a restored snapshot retained a
system-not-responding dialog. The assertion alone does not repair an unhealthy
AVD; use a fresh development AVD without deleting needed user data. This is a
development-emulator mitigation;
device ANRs still require their own [trace diagnosis](https://developer.android.com/topic/performance/anrs/diagnose-and-fix-anrs).

## Point it at a Pinakes instance

On first launch the app asks for the instance URL.
Expand Down Expand Up @@ -188,6 +221,27 @@ Released under the same license as Pinakes: **AGPL-3.0**.

On compatible servers, **Emeroteca → Articles** searches and displays standalone
newspaper and magazine articles, without requiring ownership of their issues.
Publication screens also link to their associated articles. Public PDFs use the
URL supplied by the server. Older servers retain the existing periodicals browser.
Publication screens also link to their associated articles. Article covers, subtitles
and published online resources follow the server data. Public PDFs use the
URL supplied by the server; the website action opens the article’s full page. Older servers retain the existing periodicals browser.
See [the article integration notes](docs/emeroteca-standalone-articles.md).

## Android 1.6 / current server parity

The updated client reads Mobile API 1.5.0, Desiderata 1.2.0, Emeroteca 1.13.0 and
Archives 1.5.1. Update the server/plugins before expecting the new optional
collection screens. Older instances remain usable; unsupported article facets
show an upgrade message rather than silently returning unfiltered results.

Desiderata is the library's wanted collection, separate from a member's wishlist.
Donation contact details come from the verified account. Proposals require
consent and survive transport retries/process death without creating inventory.
Archives supports paged hierarchy, text/level/year search, authorities, all
public documents and export links. Article/book citations are formatted by the
server, with five styles and text/HTML clipboard copies. Every digital book
attachment is shown; audio switches one native player at a time.

Administrative cataloguing and uploads open the protected PHP website, including
the existing article form. No native administrative CRUD API is invented.

See [the request-by-request Uwe verification](docs/UWE-PARITY.md).
38 changes: 21 additions & 17 deletions STATUS.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
# Pinakes Android — Build Status

**Build: GREEN.** `./gradlew assembleDebug` succeeds; `./gradlew lintDebug` passes (0 errors). Kotlin compiles clean.
## 2026 restyle (2026-10-08)

- **APK:** `pinakes-debug.apk` (repo root, ~20 MB) — copied from `app/build/outputs/apk/debug/app-debug.apk`.
Install: `adb install -r pinakes-debug.apk`.
- **Package:** `com.pinakes.app` · versionName `1.0` · minSdk 26 · target/compileSdk 35 · launchable `MainActivity`.
- **Verified on an emulator against a live Pinakes instance** (Android 15 / API 35 AVD → `http://10.0.2.2:8081`):
onboarding → `/health` discovery → login (`200`) → catalog search with real books → book cards with
correct titles/authors/availability. Two real bugs were found and fixed during this smoke test — see
**Fixes applied** below. The app is **fully localized in 4 languages** (German verified live) — see **i18n**.
The app now uses the 2026 web design: Geist / Fraunces, warm neutrals, a server-ready `ThemePalette`, complete 3D book covers, a searchable home hero with a cover fan, catalog grid/list views, dot status pills, grouped availability/actions and one card per exposed digital asset. Shared tokens apply to all existing account and plugin screens. The five-section bottom navigation is retained. Article lists and details now show server-resolved covers, subtitles, published online resources and access conditions, with a website action for the existing staff management flows. Native collections require the additive Mobile API 1.5.0 server update; existing circulation and authentication routes are retained.

- Debug APK: `pinakes-debug.apk`, copied from `app/build/outputs/apk/debug/app-debug.apk` (generated locally and ignored by Git).
- Package: `com.pinakes.app`, version `1.6.0` (17), minSdk 26, target/compileSdk 35.
- Local verification: **185 unit tests, 19 Compose device tests, zero lint errors**, and successful debug + R8 release builds.
- Verification commands: `assembleDebug`, `testDebugUnitTest`, `lintDebug`, `assembleRelease`, `assembleDebugAndroidTest`; the 19 device tests run through `am instrument` on a separate Android 15 AVD, preserving the authenticated demo device.
- Unit tests cover the existing contracts and theme contrast/mixing. Compose device tests cover full tall artwork, missing metadata, grid actions, view selection, digital-file cards, narrow search placeholders, circulation actions theme pairings, reachable empty-home actions and query preservation during loading.
- Release builds exercise R8 and resource shrinking. Without release credentials the output is unsigned; no store release is published by this change.
- macOS standalone emulator startup: `tools/run-emulator.sh` keeps a power assertion scoped to the emulator PID. A control with the identical debug APK reproduced a 21-second startup timeout at host priority 4; three starts with the assertion completed in 2.3–3.1 seconds. After a VM restart through the launcher, five more cold starts completed in 1.66–1.80 seconds with no ANR events. The original startup ANR also coincided with system/launcher stalls. An older AVD still stalled on a later cold boot, and a snapshot retained a system ANR dialog: the assertion does not repair an unhealthy AVD. No Sentry events are suppressed, and no production-device ANR fix is claimed.
- New native sections: Desiderata with verified-account donation proposals and durable retry recovery; Archives with hierarchy, filters, authorities, documents and exports; analytic articles with all bibliographic fields, five citation styles, rich clipboard, shared-author navigation and issue contributions. Home and catalog support author sorting; every digital attachment and publisher is shown. Staff management uses protected website pages. See [Uwe parity](docs/UWE-PARITY.md).
- Headless Android 15 completed the device tests while older GUI AVDs stalled despite the scoped power assertion. GUI and headless emulator processes had different host scheduling priorities; this is development-environment evidence, not a production ANR fix.
- `ThemePalette` defaults to Classic / Covers. Discovery does not expose theme, CMS home sections, richer catalog facets, CMS-driven home ordering or share data yet; these are documented as future API work in DESIGN.md.

## Install & point at an instance

Expand Down Expand Up @@ -39,22 +44,21 @@ On first launch the app shows **Onboarding**: enter your Pinakes instance URL.
| 8. Notifications | ✅ | Feed w/ per-type icons, read/unread styling, **pull-to-refresh** |
| 9. Contact | ✅ | `POST /messages` subject+body form, success state |

- **Bottom nav:** Search / Library / Wishlist / Profile. **Nested routes:** Book Detail, Notifications, Contact.
- **Design system:** Material 3 light **and** dark, brand magenta `#D70161` + indigo `#6366F1`,
Inter (bundled), rounded cards, soft shadows, brand-gradient header on onboarding/login,
navigation transitions + list/press animations, adaptive launcher icon.
- **Architecture:** Navigation-Compose + ViewModel/StateFlow, manual DI (`ServiceLocator` via a
`LocalServices` composition local), Retrofit + OkHttp + kotlinx.serialization, Coil for covers.
- **Bottom nav:** Home / Catalog / Library / Wishlist / Profile. **Nested routes:** Book Detail, Notifications, Contact and optional collection details.
- **Design system:** Material 3 light **and** dark, 2026 theme-derived colours (Classic magenta by default),
bundled Geist / Fraunces, warm neutrals, complete book covers, rounded controls and subtle hero washes.
Calendar and status colours follow the app theme. The adaptive launcher icon is preserved.
- **Architecture:** Navigation-Compose + ViewModel/StateFlow + Hilt, Retrofit + OkHttp + kotlinx.serialization, Coil for covers.
All loading/empty/error states handled per screen.

## Internationalization (i18n)

The app is **fully localized in 4 languages — Italian, English, French, German** — matching the
Pinakes backend locales. It follows the **device locale** by default and offers an **in-app language
The app supports **4 languages — Italian, English, French, German**; the PHP server also supports Danish.
It follows the **device locale** by default and offers an **in-app language
switcher** in Profile (System default / Italiano / English / Français / Deutsch) via
`AppCompatDelegate.setApplicationLocales(...)`, persisted across restarts (`autoStoreLocales`).

- **Single source of truth = JSON.** Translations live in `i18n/{en,it,fr,de}.json` (209 keys each,
- **Single source of truth = JSON.** Translations live in `i18n/{en,it,fr,de}.json` (622 keys each,
en = default/source). A Gradle task (`GenerateI18nResTask`) generates `res/values*/strings.xml` from
those JSONs at build time, so the app uses standard Android string resources but the editable source
stays JSON — syncable with the web app's `locale/*.json`.
Expand Down
10 changes: 9 additions & 1 deletion _contract/MOBILE_API_SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@

## Goal

### Implemented extension: Mobile API 1.5.0 / Android 1.6.0

The coordinated changes in Pinakes #458 and Android #41 add capability-gated Archives and library Desiderata, separate from circulation inventory and personal wishlist. Authenticated collection routes provide independent cursors, public archive hierarchy/metadata/documents/exports, and verified-account donation proposals with consent. Proposal UUIDs survive retries and process death; the server deduplicates per account and exposes account-scoped outcome recovery.

Analytic article search supports shared author IDs, publisher, complete genre ancestry, language, container and keyword filters. Unsupported analytic filters require a server upgrade rather than returning unfiltered results. Book and article details expose complete bibliographic metadata, five server-generated citation styles and digital attachments; article RIS/MARCXML and protected staff-management links are retained. Old circulation routes stay compatible. See [the parity matrix](../docs/UWE-PARITY.md) and [release preparation](../docs/PLAY-RELEASES.md) for version dependencies and verified boundaries.

### Original API scope

Expose **everything a logged-in library user can do on the web** through a
versioned REST API, so a native app can deliver: catalog search, book detail,
loan/reservation requests, wishlist, profile, contact messaging, and push
Expand All @@ -31,7 +39,7 @@ stock). The app stores the instance URL + a long-lived per-device token.
| Pagination | **Cursor-based** for catalog/lists: `meta.next_cursor` (opaque), `?cursor=...&limit=...`. |
| Localization | Strings in the **instance locale** (decided at install); **dates ISO-8601 UTC**; the app formats locally. |
| Write actions | reserve / request loan, **cancel reservation**, wishlist add/remove, **edit profile + change password**, **send contact message**. |
| Search filters | text (title/author/keyword), author/publisher, **genre cascade (3 levels) + language**, **availability (loanable now)**. |
| Search filters | text (title/author/keyword), author identity/publisher, **complete genre ancestry + language**, **availability (loanable now)**. |
| Book detail | **Full** payload (availability, copies, shelf/location, absolute cover URL, full metadata, related) **+ personal history** (has the user read/reserved/wishlisted it). |
| Caching | **ETag / Last-Modified** + cache headers on read endpoints; honor `If-None-Match` → 304. |
| Push transport | **UnifiedPush** primary (the library manager self-registers a provider and creates the credentials — minimal setup). Behind a **pluggable `PushProvider` abstraction** (UnifiedPush impl now; FCM impl optional/stub). |
Expand Down
Loading
Loading