diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/BUG_REPORT.md similarity index 100% rename from .github/ISSUE_TEMPLATE/bug_report.md rename to .github/ISSUE_TEMPLATE/BUG_REPORT.md diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md similarity index 100% rename from .github/ISSUE_TEMPLATE/feature_request.md rename to .github/ISSUE_TEMPLATE/FEATURE_REQUEST.md diff --git a/.github/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE.md similarity index 100% rename from .github/pull_request_template.md rename to .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c9b1681..7029a6fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ concurrency: cancel-in-progress: true env: - FLUTTER_VERSION: '3.41.2' + FLUTTER_VERSION: '3.47.9' jobs: icons: diff --git a/.github/workflows/linux-packaging.yml b/.github/workflows/linux-packaging.yml index 64ef6bf6..2a0d6795 100644 --- a/.github/workflows/linux-packaging.yml +++ b/.github/workflows/linux-packaging.yml @@ -39,7 +39,7 @@ jobs: run: bash ../tools/install_linux_build_dependencies.sh - uses: subosito/flutter-action@v2 with: - flutter-version: '3.41.2' + flutter-version: '3.47.9' cache: true - name: Install locked dependencies run: flutter pub get --enforce-lockfile diff --git a/.github/workflows/release-checks.yml b/.github/workflows/release-checks.yml index ab2144a0..2589b3b7 100644 --- a/.github/workflows/release-checks.yml +++ b/.github/workflows/release-checks.yml @@ -34,7 +34,7 @@ jobs: cache: gradle - uses: subosito/flutter-action@v2 with: - flutter-version: '3.41.2' + flutter-version: '3.47.9' cache: true - run: flutter pub get --enforce-lockfile working-directory: app diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index baf5b817..2f05ff5b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ concurrency: cancel-in-progress: false env: - FLUTTER_VERSION: '3.41.2' + FLUTTER_VERSION: '3.47.9' jobs: version: diff --git a/.github/workflows/windows-packaging.yml b/.github/workflows/windows-packaging.yml index a9cb2c7e..6d4e85ec 100644 --- a/.github/workflows/windows-packaging.yml +++ b/.github/workflows/windows-packaging.yml @@ -37,7 +37,7 @@ jobs: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 with: - flutter-version: '3.41.2' + flutter-version: '3.47.9' cache: true - run: flutter pub get --enforce-lockfile - name: Validate watched folders on Windows diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c658fe75..7b091a9b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -100,7 +100,12 @@ Address review findings and required checks before merge. A PR's existence does not make a requirement implemented or verified. After merge, record acceptance evidence and close the completed implementation issue. Review the parent requirement's remaining criteria before updating its specification status. -Create separate issues for deferred follow-up work. +Create separate issues for deferred follow-up work. Keep implementation diaries, +rollout results and review screenshots with the issue or PR; generated local +evidence belongs under ignored `app/build/`. Product behavior belongs in the +central documentation repository. Keep component documentation limited to setup, +public integration contracts and operating procedures. Do not add a feature +summary or a prose copy of code, tests or design tokens for each implementation. ## Releases and hotfixes diff --git a/DESIGN.md b/DESIGN.md deleted file mode 100644 index 2e110e7e..00000000 --- a/DESIGN.md +++ /dev/null @@ -1,192 +0,0 @@ ---- -name: "Papyrus" -description: "Existing Flutter visual system for the cross-platform library and reader." -colors: - light-primary: "#5654A8" - light-on-primary: "#FFFFFF" - light-primary-container: "#E2DFFF" - light-on-primary-container: "#100563" - light-secondary: "#5D5C71" - light-secondary-container: "#E3E0F9" - light-on-secondary-container: "#1A1A2C" - light-tertiary: "#7A5368" - light-surface: "#FFFBFF" - light-on-surface: "#1C1B1F" - light-surface-container-highest: "#E4E1EC" - light-on-surface-variant: "#47464F" - light-outline: "#787680" - light-outline-variant: "#C8C5D0" - light-error: "#BA1A1A" - dark-primary: "#C3C0FF" - dark-on-primary: "#272377" - dark-primary-container: "#3E3C8F" - dark-on-primary-container: "#E2DFFF" - dark-secondary: "#C7C4DD" - dark-secondary-container: "#464559" - dark-on-secondary-container: "#E3E0F9" - dark-tertiary: "#EAB9D2" - dark-surface: "#1C1B1F" - dark-on-surface: "#E5E1E6" - dark-surface-container-highest: "#47464F" - dark-on-surface-variant: "#C8C5D0" - dark-outline: "#928F9A" - dark-outline-variant: "#47464F" - dark-error: "#FFB4AB" - eink-primary: "#000000" - eink-on-primary: "#FFFFFF" - eink-primary-container: "#000000" - eink-on-primary-container: "#FFFFFF" - eink-secondary: "#404040" - eink-secondary-container: "#F5F5F5" - eink-on-secondary-container: "#000000" - eink-tertiary: "#404040" - eink-surface: "#FFFFFF" - eink-on-surface: "#000000" - eink-surface-container-highest: "#F5F5F5" - eink-on-surface-variant: "#404040" - eink-outline: "#000000" - eink-outline-variant: "#000000" - eink-error: "#000000" - eink-black: "#000000" - eink-white: "#FFFFFF" - eink-dark-gray: "#404040" - eink-light-gray: "#C0C0C0" - eink-container: "#F5F5F5" -typography: - display-large: {"fontSize": "57px", "fontWeight": 400, "letterSpacing": "-0.25px"} - display-medium: {"fontSize": "45px", "fontWeight": 400, "letterSpacing": "0px"} - display-small: {"fontSize": "36px", "fontWeight": 400, "letterSpacing": "0px"} - headline-large: {"fontSize": "32px", "fontWeight": 400, "letterSpacing": "0px"} - headline-medium: {"fontSize": "28px", "fontWeight": 400, "letterSpacing": "0px"} - headline-small: {"fontSize": "24px", "fontWeight": 400, "letterSpacing": "0px"} - title-large: {"fontSize": "22px", "fontWeight": 400, "letterSpacing": "0px"} - title-medium: {"fontSize": "16px", "fontWeight": 500, "letterSpacing": "0.15px"} - title-small: {"fontSize": "14px", "fontWeight": 500, "letterSpacing": "0.1px"} - body-large: {"fontSize": "16px", "fontWeight": 400, "letterSpacing": "0.5px"} - body-medium: {"fontSize": "14px", "fontWeight": 400, "letterSpacing": "0.25px"} - body-small: {"fontSize": "12px", "fontWeight": 400, "letterSpacing": "0.4px"} - label-large: {"fontSize": "14px", "fontWeight": 500, "letterSpacing": "0.1px"} - label-medium: {"fontSize": "12px", "fontWeight": 500, "letterSpacing": "0.5px"} - label-small: {"fontSize": "11px", "fontWeight": 500, "letterSpacing": "0.5px"} -rounded: - none: "0px" - sm: "4px" - md: "8px" - lg: "12px" - xl: "16px" - xxl: "28px" - full: "9999px" -spacing: - xs: "4px" - sm: "8px" - md: "16px" - lg: "24px" - xl: "32px" - xxl: "48px" -components: - button-outlined: {"rounded": "{rounded.md}", "padding": "12px 24px"} - button-text: {"padding": "8px 12px"} - input: {"rounded": "{rounded.sm}", "padding": "16px"} - card: {"rounded": "{rounded.lg}"} - button-eink: {"backgroundColor": "{colors.eink-black}", "textColor": "{colors.eink-white}", "rounded": "{rounded.none}", "padding": "12px 24px"} - button-outlined-eink: {"backgroundColor": "{colors.eink-white}", "textColor": "{colors.eink-black}", "rounded": "{rounded.none}", "padding": "12px 24px"} - input-eink: {"backgroundColor": "{colors.eink-container}", "textColor": "{colors.eink-black}", "rounded": "{rounded.none}", "padding": "16px"} - chip-eink: {"backgroundColor": "{colors.eink-white}", "textColor": "{colors.eink-black}"} - chip-eink-selected: {"backgroundColor": "{colors.eink-light-gray}", "textColor": "{colors.eink-black}"} ---- - -# Design System: Papyrus - -## Overview - -Papyrus uses its existing Material 3 theme for a readable, adaptive library and reader. Muted violet accents sit on neutral surfaces, with separate light, dark, high-contrast and e-ink themes. This record describes the implemented system, without introducing a new visual identity. - -Sources: `app/lib/themes/app_theme.dart`, `design_tokens.dart`, -`color_schemes.g.dart` and `app_motion.dart`. Flutter source remains authoritative. -Frontmatter `px` values represent Flutter logical pixels. Mode prefixes identify -actual palette variants, not interchangeable colors within one theme. - -**Key Characteristics:** -- Semantic theme colors and text roles. -- Shared spacing and shape tokens. -- Dedicated e-ink and reduced-motion behavior. - -## Colors - -The primary accent is muted violet in light mode and pale violet in dark mode. -Secondary colors support tonal controls. Tertiary colors provide a muted rose -accent. Surfaces, text and outlines use the corresponding semantic neutral roles. -E-ink resolves these roles to solid grayscale colors. - -The frontmatter records explicit source values. Surface-container roles omitted -from `color_schemes.g.dart`, including `surfaceContainerLow`, retain Flutter's -runtime defaults and are deliberately not assigned inferred hex values here. -High-contrast schemes are generated from the primary seed with contrast level 1. - -## Typography - -The frontmatter mirrors the explicit Material text scale. The theme does not -specify a custom default font family or line heights. Flutter resolves those -values. Headline, title, body and label roles provide hierarchy without a new -brand font. - -E-ink strengthens body-medium/body-small to weight 500 and label-medium/label-small -to 600. Accessibility settings can substitute OpenDyslexic and promote text to -bold. Components inherit these adaptations through the active text theme. - -## Layout - -The shared spacing scale supplies gaps, padding and margins. Breakpoint tokens -start tablet at 600, small desktop at 840 and large desktop at 1200 logical pixels. -They define 4/8/12 grid columns and 16/24/24 gutters for mobile/tablet/desktop. -Actual pages choose their composition within available width and text scaling. -Statistics defaults to left alignment with shared page margins. Its optional -center alignment limits content width to 1120 logical pixels. - -## Elevation & Depth - -Standard themes combine Material elevation and tonal surfaces. Shared cards and -elevated buttons use elevation 1, bottom navigation 3 and popup menus 6. These -are Flutter elevation values, not CSS shadow recipes. E-ink component themes use -zero elevation and visible borders. Motion durations live in the sidecar. -`AppMotion` disables motion when requested by the theme or platform settings. - -## Shapes - -Shared buttons use the md radius, inputs sm, cards lg, dialogs xxl and bottom -sheets xl. E-ink button, input and card themes have square corners. Default input -borders are 1 logical pixel, increasing to 2 for focus/error. E-ink input borders -use 2 at rest and 4 for focus/error. E-ink card themes use a 2-pixel border. - -## Components - -- **Buttons:** elevated and outlined themes use shared horizontal/vertical - padding, minimum height 50, and 16-point medium text. Text buttons have smaller - padding. E-ink buttons become square, instant and semibold. Standard interactive - state layers otherwise come from Flutter Material. -- **Inputs:** outlined fields use semantic outline, primary focus and error - colors. E-ink fields use a solid container fill and persistent bold labels. -- **Cards:** standard shared cards have a small elevation and rounded shape. - Statistics section containers are flat, use `surfaceContainerLow` and a - semantic outline, and inherit the card shape. Their local 1-pixel border is - an implementation detail, not an override of the shared e-ink card theme. -- **Chips:** standard choices inherit Material styling. The e-ink theme uses - white or selected light-gray fills, black text and 2-pixel black borders. -- **Navigation:** bottom navigation maps selected items to primary and other - items to on-surface-variant. E-ink navigation removes elevation and strengthens - selected labels. App bars use the surface and on-surface roles. - -The sidecar's small HTML/CSS examples preview explicit e-ink theme properties. -They are reference swatches, not substitutes for Flutter widgets or their full -platform interaction behavior. - -## Do's and Don'ts - -### Do: -- Do resolve colors and typography through the active Flutter theme. -- Do preserve text scaling, accessibility typography and reduced-motion settings. -- Do use existing spacing and radius tokens for shared components. - -### Don't: -- Don't hard-code the light palette into adaptive widgets. -- Don't carry standard-theme elevation and animation into e-ink components. diff --git a/PRODUCT.md b/PRODUCT.md deleted file mode 100644 index b003810f..00000000 --- a/PRODUCT.md +++ /dev/null @@ -1,28 +0,0 @@ -# Product - - - -## Platform - -adaptive - -## Product Purpose - -Papyrus is an offline-first, cross-platform book library and reader. The reading -statistics page helps readers understand their reading habits. This priority was -confirmed for the statistics redesign. - -## Capabilities and Constraints - -The Flutter client uses Provider and DataStore. Preserve guest, user and server -profile isolation and durable reading activity. Statistics must reflect existing -provider calculations, with explicit labels for different date scopes. - -## Accessibility & Inclusion - -Preserve light, dark and e-ink themes, reduced motion, keyboard access, text scaling -and responsive layouts. Follow existing design tokens and shared controls. - -## Open Decisions - -No broader audience or brand changes were requested in this page redesign. diff --git a/README.md b/README.md index 3be130bc..58e3c6c0 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@

Join the Papyrus community on Slack GitHub Project - Documentation + Documentation Coverage License

@@ -64,7 +64,7 @@ selection-based reader annotations remain future work. ### Prerequisites -- Flutter 3.41.2, matching the CI SDK: [installation guide](https://docs.flutter.dev/get-started/install) +- Flutter 3.47.9, matching the CI SDK: [installation guide](https://docs.flutter.dev/get-started/install) ### Patched dependencies @@ -127,7 +127,10 @@ For local web auth links, add these entries to `/etc/hosts`: ## Documentation -See [PapyrusReader/docs](https://github.com/PapyrusReader/docs). +See [PapyrusReader/docs](https://github.com/PapyrusReader/docs) for the user manual +and specification. This repository keeps [installation instructions](docs/INSTALL.md), +[release setup](docs/RELEASING.md) and [Windows acceptance checks](docs/WINDOWS_TESTING.md) +with the packaging tools that consume them. ## Technology stack @@ -148,7 +151,7 @@ Keep actionable bug reports and technical decisions in GitHub issues and pull re ### Setup 1. Fork and clone the repository -2. Install dependencies from `app/` using the Flutter version in CI (currently 3.41.2): +2. Install dependencies from `app/` using the Flutter version in CI (currently 3.47.9): ```bash cd app @@ -196,8 +199,6 @@ Keep actionable bug reports and technical decisions in GitHub issues and pull re | [docs](https://github.com/PapyrusReader/docs) | Documentation | | [papyrus](https://github.com/PapyrusReader/papyrus) | Development workspace | -See [About and support](docs/about-and-support.md) for release notes, licenses, and feedback. - ## Google Play testing builds See [the release guide](docs/RELEASING.md) for signed Android App Bundles, diff --git a/app/android/gradle.properties b/app/android/gradle.properties index f018a618..475a6280 100644 --- a/app/android/gradle.properties +++ b/app/android/gradle.properties @@ -1,3 +1,7 @@ org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true android.enableJetifier=true +# This builtInKotlin flag was added automatically by Flutter migrator +android.builtInKotlin=false +# This newDsl flag was added automatically by Flutter migrator +android.newDsl=false diff --git a/app/android/gradle/wrapper/gradle-wrapper.properties b/app/android/gradle/wrapper/gradle-wrapper.properties index ac3b4792..74b269f3 100644 --- a/app/android/gradle/wrapper/gradle-wrapper.properties +++ b/app/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip diff --git a/app/android/settings.gradle.kts b/app/android/settings.gradle.kts index fb605bc8..ca7fe065 100644 --- a/app/android/settings.gradle.kts +++ b/app/android/settings.gradle.kts @@ -19,8 +19,8 @@ pluginManagement { plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" - id("com.android.application") version "8.9.1" apply false - id("org.jetbrains.kotlin.android") version "2.1.0" apply false + id("com.android.application") version "8.11.1" apply false + id("org.jetbrains.kotlin.android") version "2.2.20" apply false } include(":app") diff --git a/app/lib/goals/goal_calendar.dart b/app/lib/goals/goal_calendar.dart index ca2fb3a7..c73fefe4 100644 --- a/app/lib/goals/goal_calendar.dart +++ b/app/lib/goals/goal_calendar.dart @@ -38,22 +38,23 @@ class GoalCalendar { static DateTime local(DateTime date, String zone) => tz.TZDateTime.from(date, location(zone)); + // TZDateTime.toUtc() retains timezone-specific equality. Calendar keys need native UTC dates. static DateTime midnight(DateTime date, String zone) { final day = local(date, zone); - return tz.TZDateTime(location(zone), day.year, day.month, day.day).toUtc(); + return tz.TZDateTime(location(zone), day.year, day.month, day.day).native; } static DateTime nextDay(DateTime date, String zone) { final day = local(date, zone); - return tz.TZDateTime(location(zone), day.year, day.month, day.day + 1).toUtc(); + return tz.TZDateTime(location(zone), day.year, day.month, day.day + 1).native; } static DateTime deadline(DateTime civilDate, String zone) => - tz.TZDateTime(location(zone), civilDate.year, civilDate.month, civilDate.day + 1).toUtc(); + tz.TZDateTime(location(zone), civilDate.year, civilDate.month, civilDate.day + 1).native; static DateTime dayOffset(DateTime date, int offset, String zone) { final day = local(date, zone); - return tz.TZDateTime(location(zone), day.year, day.month, day.day + offset).toUtc(); + return tz.TZDateTime(location(zone), day.year, day.month, day.day + offset).native; } static GoalRange calendarPeriod(GoalPeriod period, DateTime date, String zone) { @@ -76,7 +77,7 @@ class GoalCalendar { GoalPeriod.custom => throw ArgumentError('A deadline needs explicit dates'), }; - return GoalRange(start.toUtc(), end.toUtc()); + return GoalRange(start.native, end.native); } static DateTime effectiveNow(ReadingGoal goal, DateTime now) => diff --git a/app/pubspec.lock b/app/pubspec.lock index 0a1ecfeb..caf24167 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -499,10 +499,10 @@ packages: dependency: transitive description: name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd" url: "https://pub.dev" source: hosted - version: "0.12.18" + version: "0.12.20" material_color_utilities: dependency: transitive description: @@ -515,10 +515,10 @@ packages: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.19.0" mobile_scanner: dependency: "direct main" description: @@ -579,8 +579,8 @@ packages: dependency: "direct main" description: path: "." - ref: fa667ae96c64e2d1d80c06e83d326265673abc75 - resolved-ref: fa667ae96c64e2d1d80c06e83d326265673abc75 + ref: a52d0e4f3b78f37234b917cec8ef11cec71a914b + resolved-ref: a52d0e4f3b78f37234b917cec8ef11cec71a914b url: "https://github.com/PapyrusReader/reader.git" source: git version: "0.0.1" @@ -668,18 +668,18 @@ packages: dependency: transitive description: name: pdfrx - sha256: acce61944c31bd36f0c3031b1bbf41f59d0b0d4e5e27614fbdc74ff093d55464 + sha256: "85a87117ae6358e0ef2cc90d4db9f5e689eabbd7f19ab98c97164584d0be4a4a" url: "https://pub.dev" source: hosted - version: "2.4.7" + version: "2.4.8" pdfrx_engine: dependency: transitive description: name: pdfrx_engine - sha256: "8043527245e4c35a2a3b7df8494c066b9efa97fe031945951e46b378318b3d5f" + sha256: "4399846809a75aed03881d8b8a19e5e70addd4b46b22fd94a0effd8859258f07" url: "https://pub.dev" source: hosted - version: "0.4.5" + version: "0.4.7" petitparser: dependency: transitive description: @@ -1001,10 +1001,10 @@ packages: dependency: transitive description: name: test_api - sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11" url: "https://pub.dev" source: hosted - version: "0.7.9" + version: "0.7.12" timezone: dependency: "direct main" description: @@ -1138,10 +1138,10 @@ packages: dependency: transitive description: name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + sha256: fa49e1674cb84acb900141714e6c6e1f9ae099827c24d141430ff6819db4e1bc url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.4.4" vm_service: dependency: transitive description: @@ -1215,5 +1215,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.10.3 <4.0.0" - flutter: ">=3.41.0" + dart: ">=3.11.0-0 <4.0.0" + flutter: ">=3.47.0" diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 46ebeef2..64318f13 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -27,7 +27,7 @@ dependencies: papyrus_reader: git: url: https://github.com/PapyrusReader/reader.git - ref: fa667ae96c64e2d1d80c06e83d326265673abc75 + ref: a52d0e4f3b78f37234b917cec8ef11cec71a914b epub_pro: ^5.6.0 syncfusion_flutter_pdf: ^32.2.3 dart_mobi: ^1.0.2 diff --git a/docs/DELIVERY_ROLLOUT.md b/docs/DELIVERY_ROLLOUT.md deleted file mode 100644 index 423376ed..00000000 --- a/docs/DELIVERY_ROLLOUT.md +++ /dev/null @@ -1,129 +0,0 @@ -# Delivery rollout evidence - -Status recorded on 2026-10-11. This is an implementation checkpoint, not a claim -that the new release process has delivered a working release to every platform. -Client/server `0.0.2` (Android build 2) is being prepared through the normal -release PRs. Publication and production delivery remain pending. - -## Configured - -- GitHub `release` environment contains the separate web deployment SSH secret - and host/user/known-host variables. -- The web host uses versioned directories and `current`; the previous flat files - were preserved during migration. Caddy was reloaded without restarting API, - PowerSync or database containers. -- `/` and `/login` still serve the existing app with revalidation headers. The - restricted SSH receiver rejects invalid commands and a checksum mismatch - without changing the active release. -- The dedicated Play service account and its JSON key have been created. The key - is stored as `PLAY_SERVICE_ACCOUNT_JSON` in GitHub's `release` environment. - No Google Cloud project roles were granted. -- The Android Publisher API is enabled. Play access is active for only - `com.papyrus.reader`, with app visibility and testing-release permission; - production, finance, administration and tester-list management are ungranted. -- A live API probe created and deleted an inspection edit successfully. Build 1 - is completed on `internal`, with no production/alpha/beta releases. Its bundle - SHA-256 matches the existing GitHub `v0.0.1+1` AAB, and the retry preflight - returns `complete` without changing any track. The temporary local JSON key - was removed after verification. - -## Local verification - -- 40 client release-tool tests, 22 server tooling tests and 18 workspace tooling - tests pass. Checks cover release gates, tag/version identity, checksums, unsafe - archives, immutable publication, retry behavior, web rollback and migration. -- Client formatting/analysis/web bootstrap checks, workflow lint, shell lint, - deployment-layout checks and website tests/build pass. -- Linux tar, `.deb` and AppImage assembly was exercised with the existing - `v0.0.1+1` Linux binary as a fixture, on Ubuntu 24.04 x64 under macOS emulation. - This is not a build of the current client revision. -- The `.deb` installed, reinstalled and uninstalled successfully; its desktop - launcher validates and a user-data sentinel survives removal. -- The extracted AppImage starts, and its bundled WebKit runtime renders HTML, - canvas and JavaScript with the sandbox enabled in an isolated Ubuntu container. - The container's namespace capability is needed for nested sandbox testing. -- Debian 13 validation is blocked locally by the x64 emulator's unsupported - syscall in Debian's bubblewrap. This is not a Debian compatibility pass. - -## Native and device verification - -- Windows run [38086067580](https://github.com/PapyrusReader/client/actions/runs/38086067580) - at client commit `5a48d6f` passed installer lifecycle, Start menu presence, - upgrade from installer version `0.0.0+1` to `0.0.0+2`, WebView2 rendering and - the installed instrumented reader's EPUB/PDF, offline import, persisted resume - and guest/account/server-profile isolation tests. Its uploaded JSON report - records success. The test entrypoint uses production reader and storage code; - it is not the production application's interactive entrypoint. -- Native Ubuntu launched the AppImage with a scoped AppArmor namespace profile. - The launcher now supports minimal hosts without an existing WebKit helper - directory, and the bundle includes non-base text libraries and font configuration. -- Linux run [38090665629](https://github.com/PapyrusReader/client/actions/runs/38090665629) - at `e02fa79` passed native Ubuntu 24.04 `.deb` and AppImage checks and Debian 13 - AppImage checks without system WebKit. All three packaged reader JSON reports - confirm EPUB/PDF rendering, offline import, resume and profile isolation. -- Windows run [38090105345](https://github.com/PapyrusReader/client/actions/runs/38090105345) - at `ae506c6` passed the missing-runtime path as well as the installed reader. - Its disposable runner quarantines existing WebView2 binaries and registrations, - verifies the offline installer restores a working runtime, and confirms a - second install retains it. This is an isolated-runtime test, not a clean OS image. -- Windows run [38091317526](https://github.com/PapyrusReader/client/actions/runs/38091317526) - at `ff49d52` additionally passed persistence probes across upgrade, uninstall - and reinstall. A real imported EPUB, SQLite book/reading position, saved - settings and a namespaced Windows credential all survived. Fixtures use unique - test profiles and clean up only their own data. -- The stronger Linux lifecycle probe caught an application-data path mismatch. - Updating `path_provider_linux` from 2.2.1 to 2.2.2 uses the versioned GLib SONAME - consistently in installed and bundled packages. Linux run - [38092080634](https://github.com/PapyrusReader/client/actions/runs/38092080634) - at `f337831` passed the real imported-library, reading-position and settings - preservation probes across Ubuntu upgrade/removal/reinstall, plus all three - Ubuntu/Debian packaged reader reports. The headless Linux fixture does not - exercise a desktop Secret Service credential store. -- Windows run [38092080651](https://github.com/PapyrusReader/client/actions/runs/38092080651) - at the same `f337831` revision passed all installer, runtime, reader and - credential-preservation checks again. -- A connected Android device reports `versionName=0.0.1`, `versionCode=1` and - both installer and initiating package `com.android.vending`. This confirms - the existing Play installation, not installation of the next release. -- Coordinated server preparation: 372 backend tests pass, including the additive - goal-table migration, existing-library preservation and metadata comparison; - two external provider tests are excluded. Ruff and Mypy pass. Server PR #15 - also passes GitHub CI. Production is still server `0.0.1`, migration - `af0fea8d6317`; the next server must deploy before its client. -- Server release review fixes retain session position/device metadata in the - JSON ledger, serialize concurrent retry lookup and correct statistics period - boundaries/current streaks. The client preserves these fields through SQLite, - queued upload and grouped presentation. Focused server/client regressions pass; - the retry regression fails with the previous lock ordering. No extra database - migration is needed for these optional JSON fields. -- An encrypted, off-host production database backup was restored into an isolated - PostgreSQL 17 container and upgraded to `b5c6d7e8f901`. Row fingerprints for all - 20 existing application tables remained identical. The disposable restore - container and volume were removed. This database-only rehearsal does not - replace the fresh database/media/configuration backup required at rollout. - -## Required before declaring delivery operational - -- [x] Verify restricted Papyrus testing access and confirm API access. -- [x] Run the new native Linux and Windows packaging workflows on the current - source revision. Windows checks include installer lifecycle and WebView2 - HTML/canvas/JavaScript rendering; Linux checks include Debian without system - WebKit. The successful run links and exact tested revisions are recorded above. -- [x] Check Windows installation with WebView2 absent on a disposable runner, - upgrade from the previous fixture version, Start menu/icon, uninstall and - preservation of an imported library, SQLite reading position, settings and - credentials. Runtime quarantine is not a clean Windows OS installation. -- [x] Check actual packaged EPUB/PDF reading, offline imports, reader resume and - guest/account/server-profile isolation on Windows and both Linux targets. -- [ ] Promote the completed changes through the normal development-to-master - release process with the next properly incremented committed version. -- [ ] Verify deployed `/release.json`, entrypoint and client-side route against - that release, and exercise a delivery retry without rebuilding. -- [ ] Install that exact release from Google Play internal testing on a device. - An active internal track or successful API upload alone is not installation - verification. - -The existing release predates the new manifest and cannot be retroactively used -as a delivery retry. The currently deployed web app likewise has no release -metadata yet. Keep Apple, ARM64, public store launches and automatic desktop -updates deferred. diff --git a/docs/RELEASING.md b/docs/RELEASING.md index c3ccf605..a610db3b 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -2,7 +2,7 @@ Android identity is `com.papyrus.reader`; the first uploaded bundle establishes it in Play Console. Keep this identity for all subsequent uploads. -The app name is Papyrus. The pinned Flutter 3.41.2 toolchain targets Android API 36. +The app name is Papyrus. The pinned Flutter 3.47.9 toolchain targets Android API 36. ## Branch workflow @@ -101,7 +101,7 @@ an actionable message; they cannot silently use the debug certificate. upload key. Keep the same upload key for later releases. 3. Configure the public API, PowerSync, SMTP and Google OAuth settings using the server deployment runbook. Check the endpoints from outside your local network. -4. Merge the initial `0.0.1+1` version change (or dispatch Release on `master`), download the +4. Publish the prepared version (or dispatch Release on `master`), download the `android-release` artifact, and upload the `*-android-universal.aab` to internal testing. The local validation bundle uses a temporary certificate and placeholder URLs and must **never** be submitted to Play. @@ -126,13 +126,9 @@ content rating, target audience, app access instructions/test login, screenshots 512 px store icon and feature graphic. Declare the actual server/account/book file behavior rather than claiming that all data stays on-device. -Papyrus currently supports account creation but has no account-deletion flow. -Google Play requires account deletion within the app and an external deletion -request URL for applicable distribution tracks. Implement and validate deletion -of the user's account and associated data before broader distribution. Publishing -an invented privacy policy or marking this requirement complete is not part of -build preparation. Check the Console requirements for your developer account; -new personal accounts may require closed testing before production access. +Validate the in-app account-deletion flow and public server `/v1/account-deletion` +page (or its configured API prefix), including recovery and permanent cleanup. Check +the linked Play requirements and Console for the intended distribution track. Official references: - [Flutter Android release and signing](https://docs.flutter.dev/deployment/android) @@ -161,8 +157,8 @@ included, and Microsoft's Authenticode-verified offline WebView2 installer runs only when needed. Test a clean Windows x64 machine both with and without WebView2, then upgrade and uninstall while retaining a real library. No trusted signing service or Microsoft Store submission is configured. Installed Windows and -AppImage builds include integrated update delivery. See [update delivery](UPDATE_DELIVERY.md) -for the plan, APT signing configuration and native acceptance checks. +AppImage builds include integrated update delivery. See [APT signing](#apt-signing) +and [update acceptance](#update-acceptance) below. The Windows packaging workflow also runs on app changes and supports manual dispatch. It saves a `windows-preview-RUN_ID` artifact of the production entrypoint @@ -180,10 +176,47 @@ Packaging PR checks need no production credentials. The release packaging jobs use the production endpoints, preserve the signed Android bundle alignment checks, and only publish after every required build/package job succeeds. +## APT signing + +Configure these settings in the client repository's `release` environment: + +- Secret `APT_SIGNING_KEY`: ASCII-armored private OpenPGP signing key usable by batch GPG. +- Variable `APT_SIGNING_FINGERPRINT`: the complete 40-character fingerprint. +- Variable `APT_UPDATES_ENABLED`: `true`. + +Back up the key and keep its identity stable across releases, with a rotation +procedure before expiry. Once enabled, keep APT enabled: subscriptions follow the +latest GitHub release. Missing signing material fails publication when enabled. +The workflow publishes the signed package indexes and keyring with the `.deb`; +installation instructions are in [INSTALL.md](INSTALL.md). + +## Update acceptance + +Test an upgrade from the published build to the candidate and verify local books, +reading positions, settings and profiles survive. Record the source revisions and +results in the release PR. + +- Play: install through internal testing, accept an eligible update, and check + cancellation and offline reading. +- Direct APK: check installation permission, cancellation/retry, and rejection of + a package signed with a different key. Only the `direct` flavor may request + package-install permission; Play builds must use Play delivery. +- Windows: follow [Windows acceptance](WINDOWS_TESTING.md), including update on + normal shutdown, relaunch, failed installation and portable ZIP fallback. +- Linux: update a running writable AppImage and reopen it; check read-only and + extracted-image fallback. Upgrade a `.deb` through APT and verify a tampered + index or package is rejected. +- Web: leave the old build open during deployment, update outside the reader, + and verify local data survives. Check offline behavior and multiple open tabs. + +Native timer releases also require the Android special-use foreground-service +Play declaration and a signed iOS Live Activity extension matching Runner's version. + ## Play service account -See [delivery rollout evidence](DELIVERY_ROLLOUT.md) for completed setup and the -remaining operational acceptance checks. +Use these steps when provisioning or replacing the delivery account. Verify each +release with the workspace release manager and record its delivery evidence in +the release PR. 1. In the existing Google Cloud project, enable the Google Play Android Developer API and create a service account dedicated to this client. diff --git a/docs/TIMER_NOTIFICATIONS.md b/docs/TIMER_NOTIFICATIONS.md deleted file mode 100644 index 0d3d3c8f..00000000 --- a/docs/TIMER_NOTIFICATIONS.md +++ /dev/null @@ -1,66 +0,0 @@ -# Reading timer notifications - -The native surfaces mirror the existing reading timer. They do not own a second -PowerSync database or create their own reading sessions. - -- Android shows a silent notification with the book, a native chronometer, - pause/resume and finish. Running timers use a `specialUse` foreground service. - Pausing stops the service but retains the ongoing notification and its controls. - Android 13+ asks for notification permission when an active timer is first shown. -- iOS 16.2+ shows a Live Activity on the Lock Screen and Dynamic Island. iOS 17+ - provides pause/resume and finish through `LiveActivityIntent`, without opening - the app. Earlier supported versions link back to the timer. ActivityKit and the - user's system settings control visibility and lifetime. -- Finish saves time without page input. Subsecond sessions are discarded quietly. - Countdown time is capped at the goal. A completed countdown offers Save. -- Other platforms retain the in-app timer and do not request notification access. - -## Background actions and synchronization - -Native controls first write a private, durable action journal containing the -session ID, base version, command IDs and tap timestamps. They update the native -surface immediately. Dart replays those actions through `ReadingTimerProvider` -and the current profile's repository when it is available. On a suspended or -terminated iPhone app, replay may wait until the app next runs. PowerSync receives -the normal timer and reading-activity writes at that point. - -Command IDs become timer versions, making replay recoverable if the process stops -between persisting an action and acknowledging it. A remote version change or a -different active session rejects stale controls. Switching library profiles clears -the old native surface and journal. Native acknowledgements preserve newer actions -that arrived while Dart was writing. - -The clock renders from timestamps rather than per-second native channel calls. -iOS does not keep a Dart engine or network connection running indefinitely. Remote -PowerSync changes update the Live Activity when the app is running again. There is -no APNs push-to-update integration. Android does not acquire a wake lock. On screen -wake, its service refreshes countdown completion. - -## Validation - -Dart tests cover delayed timestamps, save-once replay, interrupted finish recovery, -stale remote versions, subsecond finish, countdown limits, persistence failures and -profile isolation. Native XCTest covers the durable journal, concurrent -acknowledgement, countdown clamping and real Live Activity intent execution. - -The native smoke-test host at -`app/integration_test/support/timer_notification_app.dart` uses synthetic data and -no user library. Build it only for isolated test simulators/emulators. The normal -application entry point remains `app/lib/main.dart`. Use the fixture for native -XCTest so the production provider does not clear synthetic sessions during a test. -From `app/ios`, with an isolated iOS simulator booted: - -```sh -xcodebuild test -workspace Runner.xcworkspace -scheme Runner \ - -destination 'platform=iOS Simulator,name=YOUR_TEST_SIMULATOR' \ - -only-testing:RunnerTests -parallel-testing-enabled NO CODE_SIGNING_ALLOWED=NO \ - FLUTTER_TARGET=integration_test/support/timer_notification_app.dart -``` - -Use `flutter run -t integration_test/support/timer_notification_app.dart` on an -isolated Android emulator to check notification actions after backgrounding or -killing the app. The fixture is never the release entry point. - -Android special-use foreground service declarations must be included in the Play -Console declaration when this feature is distributed. The widget extension is -embedded in the iOS Runner target and uses the app's build version and signing team. diff --git a/docs/UPDATE_DELIVERY.md b/docs/UPDATE_DELIVERY.md deleted file mode 100644 index ca81ef1a..00000000 --- a/docs/UPDATE_DELIVERY.md +++ /dev/null @@ -1,93 +0,0 @@ -# App update delivery - -## Plan and scope - -1. Add a shared update controller and About → App updates. Release builds check - at startup and at most once daily while running, with an opt-out. Checks never - block library startup. Notices stay hidden while reading and can be dismissed. -2. Keep Android updates in their distribution channel. Play builds ask Google - Play for account-eligible updates, including internal testing releases. A - separate `direct` flavor installs verified APKs using Android's confirmation - flow. Only that flavor declares installation permission and a narrow file - provider. Both retain `com.papyrus.reader` and existing signing identities. -3. Installed Windows builds download and verify the Inno installer, then wait for - normal app shutdown. A helper rechecks the checksum, upgrades the existing - directory without forcing shutdown or reboot, and relaunches Papyrus. Portable - ZIP installations retain manual delivery. Installer failures leave a log and - the verified installer in the temporary update directory for recovery. -4. AppImage builds download and verify the full replacement, stage it beside the - existing file and rename atomically. The running process keeps its old image - until the user closes it. This uses the same manifest updater as Windows rather - than introducing a separate delta-update dependency. Extracted and tarball - builds retain manual delivery. -5. Publish an optional signed flat APT repository as GitHub release assets. Once - configured, `.deb` users subscribe once and use their normal software updater. -6. Web builds compare their compiled build number with the deployed `release.json` - and activate the waiting offline worker before reloading when requested. Other -open Papyrus tabs prevent activation until they are closed. Existing centrally deployed delivery is retained. - -## Trust and release contract - -The native manifest URL is fixed to the public PapyrusReader/client GitHub -repository over HTTPS. It accepts only increasing build numbers, expected asset -names, and SHA-256 hashes. Download URLs are constructed from the validated tag -and platform, never taken from arbitrary feed URLs. Partial and corrupt downloads -are discarded. GitHub release publication permissions are the trust boundary. -Checksums do not provide an independent signature if the release account itself -is compromised. Windows Authenticode signing remains a separate future setup. -Android additionally verifies the installed package identity and signing -certificate before handing the APK to the OS installer. - -The Play AAB has no sideload-install permission. Build it with `--flavor play`. Build the direct APK from the `--flavor direct` AAB. The release -workflow produces both from the same revision. Never redirect Play builds to the -APK feed or switch keys to make an update install. - -The update controller and preferences are app-wide. Update packages use temporary -storage. No updater migrates, clears, or replaces library databases, profiles, -books, credentials, or reading positions. Normal database migrations still apply -when the new app starts. - -## APT activation - -Before the first APT-enabled release, create a dedicated OpenPGP signing key, -back it up securely, and configure the client repository's `release` environment: - -- Secret `APT_SIGNING_KEY`: ASCII-armored private signing key usable by batch GPG. -- Variable `APT_SIGNING_FINGERPRINT`: the complete 40-character fingerprint. -- Variable `APT_UPDATES_ENABLED`: `true`. - -Keep that signing identity stable across releases. Set a rotation procedure before -expiry. Once enabled, keep APT enabled for all subsequent releases, because the -subscription follows GitHub's latest published release. Missing signing material -fails publication when enabled. Disabled APT leaves existing release delivery -working but does not provide a repository. No secret is stored in this repository. - -The release job publishes `Packages`, `Packages.gz`, `Release`, `Release.gpg`, `InRelease`, -and `papyrus-archive-keyring.gpg` together with the `.deb`. The common release -manifest and checksum index cover them too. APT verifies the signed index and -package hash independently. If a release changes between `apt update` and -installation, repeat `apt update` to refresh the flat repository snapshot. - -## Acceptance before enabling distribution - -- Run the controller, downloader, widget, release-tool and package checks. -- Build web and both Android flavors. Inspect merged manifests to verify only - `direct` requests package installation permission. -- Play: install build N through internal testing, publish N+1 to the same track, - check and accept the update. Also cancel it and confirm offline reading works. -- APK: install N, decline then allow installation permission, update to N+1, - cancel and retry. Confirm Android rejects a package signed with another key. -- Windows x64: install N, download N+1 through the app, keep reading, close - normally and verify upgrade/relaunch and retained local books/positions. Test - paths with spaces, a failed installer, and portable ZIP fallback. -- Linux: update a writable AppImage while running, restart and verify version and - retained data. Test read-only paths and extracted-image fallback. Install the - APT signing key once, upgrade a real `.deb` through the repository and verify - a tampered index/package is rejected. -- Web: leave N open, deploy N+1, check/reload outside the reader and verify local - data survives. Test an offline check and a narrow e-ink layout. - -Older installed desktop/APK builds need one manual upgrade to acquire this code. -Play-installed testers already receive updates through Play. Source changes do -not activate a release, provision APT signing material, or establish native-device -acceptance on their own. diff --git a/docs/about-and-support.md b/docs/about-and-support.md deleted file mode 100644 index 9efeb401..00000000 --- a/docs/about-and-support.md +++ /dev/null @@ -1,30 +0,0 @@ -# About and support - -On phones, Settings opens a grouped index. Account, Appearance, Storage, -Accessibility, and About each open their own page. Desktop Settings retains its -sidebar. Settings navigation is immediate, including Back. - -About → App updates opens a dedicated page with the installed version, latest -check result, automatic-check preference, and the action appropriate to the -installation channel. Fast retries preserve the previous result while checking, -and longer checks show progress without moving the action. - -About reads the installed package version and build number. What's new opens the -matching `v+` release in `PapyrusReader/client`, using the release notes -generated by the release workflow. Development versions without a published tag -do not have release notes yet. Licenses are bundled with the app and work offline. -About prepares the license list before opening it to avoid a blank first frame. -Direct license routes retain delayed loading feedback and retry on failure. - -Support submits a bug report or feature request to the selected Papyrus server. -No GitHub account is required. Its text, attachments, and app -version/platform become a public client-repository issue. The server must enable -`SUPPORT_GITHUB_TOKEN`, as documented in the [server support guide](https://github.com/PapyrusReader/server/blob/development/docs/support.md). Failed submissions -keep the current form text and selected files, and uncertain submissions are not retried automatically. - -Attach up to five images or videos, 10 MB per file. PNG, JPEG, GIF, WebP, MP4, MOV, -and WebM are supported. Files can be removed before submission and upload only -when Submit is pressed. Images and videos are embedded in the GitHub issue. - -The Support form follows Appearance → Page alignment (Left or Center). Changing -alignment preserves the draft and selected attachments. diff --git a/docs/catalogs-ui-redesign.md b/docs/catalogs-ui-redesign.md deleted file mode 100644 index fd1e2944..00000000 --- a/docs/catalogs-ui-redesign.md +++ /dev/null @@ -1,50 +0,0 @@ -# Catalogs UI redesign - -Catalogs now uses the library's page gutters, shared search field, grid-density rules, typography, and theme surfaces. Sources and navigation links are flat rows. Publication grids retain distinct editions and show covers, authors, and format descriptions without elevated card wrappers. - -Book details are full pages at `/library/catalogs/:catalogId/book?feed=…&publication=…&q=…`. The source-feed URL and publication identifier are encoded query values; credentials are never included. In-memory metadata provides a quick preview, while direct navigation resolves the source feed and its publication document. Missing publications and catalog-store failures expose retry and return navigation. Returning preserves the source feed's position and view mode. - -The main book action opens a format-selection sheet. Catalog forms, removal confirmation, and transfer activity also use bottom sheets. The Downloads header control replaces the persistent bottom bar and displays activity and failure indicators. Jobs continue after sheet dismissal and page navigation, preserve cancellation restrictions during commit, and link completed imports to their library book. - -Catalog sheets follow the Import books styling: a left-aligned title and top-right close icon, a divider below the header, and a fixed footer with a top divider for actions. Content scrolls independently above the keyboard. Save, Cancel, and Remove stay in the footer; per-format and per-download actions stay with their corresponding items. Both close controls are disabled while a catalog is saving. - -The existing relay, import pipeline, account isolation, and application-level job reset on catalog-setting changes remain in place. No backend API, database, or global theme changes are required. - -## Verification and visual review - -Regression coverage includes encoded deep links, detail resolution, edition identity, return navigation, stale sheets after account/catalog changes, storage-error recovery, grid/list switching, removal failures, keyboard-safe forms, progress/cancel/retry/commit behavior, and reduced motion. - -The layout journey renders the actual app shell at 360, 600, 840, 1280, and 2507 logical pixels, in light, dark, and e-ink themes, plus 360px at twice the normal text size. It checks source lists, feeds, details, format sheets, transfer sheets, and the editor with an open keyboard. Screenshots use fixture data and existing repository cover artwork, using controlled catalog metadata. - -Generate review images in `app/build/catalog-review`: - -```sh -cd app -flutter test --no-pub test/opds/layout_test.dart \ - --dart-define=CAPTURE_OPDS=true \ - --dart-define=FLUTTER_FONT_DIR="$FLUTTER_ROOT/bin/cache/artifacts/material_fonts" -``` - -The font directory is optional for layout assertions. For screenshots it replaces Flutter's test-only Ahem fallback with the platform font. Desktop reference images render the existing library book header and grid for comparison. - - -Review images are generated build artifacts and are kept outside source control. - -## Mobile spacing refinement - -The catalog browsing header keeps Back, the catalog name, Downloads, and settings together, above a full-width divider matching Edit book. The redundant Catalog home action and hostname subtitle have been removed. Search submission sits inside the search field, and a single grid/list switch shares the feed-heading row with refresh. - -The feed heading and its controls stay below search while entries scroll. Refresh replaces only the content area with loading or error feedback; the heading stays visible through retry. A single grid/list icon button is used at every screen size. - -On first load, the heading waits for the actual feed title. Link labels and placeholder titles are not used, so the title does not change when metadata arrives. Changing feeds or accounts clears the previous section's heading; refreshing the current feed keeps its loaded heading visible. - -Book covers keep a fixed 2:3 ratio. Titles use their natural height, authors follow immediately, and edition captions can occupy two lines. This removes the empty title slot and makes similar editions easier to distinguish. - -The visual fixture now also reproduces a two-edition feed with the long Pride and Prejudice heading and format labels, including the reported 424px screen width. - - -## Mobile catalog creation - -Below the library's 840px desktop breakpoint, Add catalog uses the same floating action button as Add book. Downloads stays beside the page heading, removing the separate actions row. The source list includes bottom clearance so its last menu can scroll above the floating button. Desktop keeps its existing header action. - -The Catalogs source page and catalog book details use the same header height, padding, title weight, and full-width divider as catalog browsing, without a subtitle. Book details keep Back and Downloads in the header, with compact Downloads on mobile. diff --git a/docs/goals-tracking.md b/docs/goals-tracking.md deleted file mode 100644 index 8eee4fa0..00000000 --- a/docs/goals-tracking.md +++ /dev/null @@ -1,116 +0,0 @@ -# Goals and reading activity - -Goals project progress from the profile's durable reading ledger. The Goals page, -Dashboard, and Statistics use this same source; none seed example progress. - -## Reading and completion - -The reader reports readiness and visible document coverage through an optional, -Goals-independent callback. The host records foreground intervals every ten -seconds and flushes when leaving. Backgrounding, settings/contents panels, and -other routes pause recording; there is no inactivity timeout. Checkpoints use -stable UUIDs and the originating profile's repository, so retries do not increment -an editable counter or write into another profile. - -PDF pages qualify after ten seconds of exposure. A jump contributes only exposed -pages, including spreads. EPUB estimates use normalized chapter content coverage -and the book's page-count metadata; screen pages and chapter numbers never count -as pages. A cover-only chapter or a book without page metadata still records time. -EPUB estimates assume equal calibration weight per spine chapter and are labeled -as estimates. Coverage is deduplicated per book and calendar day. - -Reaching the end offers explicit completion confirmation. Library and book-details -status changes use the same operation. Rereading preserves completed status, and -undo appends reversals rather than deleting activity. Manual logs support physical -books, dates, duration, pages, completion, notes, and audited corrections. - -## Rules and history - -Goal definitions capture an IANA timezone, creation time, scope, and rule history. -Monday weeks and calendar days/months/years respect timezone transitions. Activity -before creation or during a pause does not count. Target/title revisions affect -the current and future periods; metric, cadence, scope, timezone, and reading-day -threshold changes save as a replacement goal, atomically archiving the original -definition with its history. The edit sheet previews this before saving. Recurring goals remain visible after -achievement and advance at calendar boundaries. Archive and deletion preserve -period history; activity retains book identity/title and shelf snapshots after -book deletion. - -Duration is the union of eligible intervals, including concurrent devices. Pages -union stable document coverage; confirmed books count once per period. Reading -days default to five cumulative minutes. Period records retain rules, not editable -progress, and can be reprojected after corrections arrive. - -## Sync contract - -`GET /v1/sync/settings` advertises the current `tracking_schema_version: 2`. -All tracking uses this contract; the client does not select older payload variants. -Goals can cover multiple selected books through `book_ids`. A books-finished target -cannot exceed the selected book count, and the server validates ownership of every -selected book. `reading_goals`, `reading_activities`, and `goal_periods` hold -owner-scoped JSON payloads. - -Until discovery confirms the current contract, tracking writes remain in local-only -`tracking_staging` while ordinary library synchronization continues. Discovery -promotes staged writes atomically. Upload failures retain queued writes for retry. - -Deploy the server migration, PostgreSQL publication/grants, and PowerSync streams -before releasing this client. Versions are unchanged; merge feature PRs into -`development` and use the normal release workflow. - -## UI review - -The current Goals page uses swipeable Overview and Activity tabs with shared -collection-page spacing and controls. Overview groups goals by progress and offers -a compact completed-goal filter; phones use a New goal FAB. Creation defaults to -Daily and supports an editable target, recurrence, shelves, and selected books. -Activity groups book/day entries with expandable details and archived history. -Expansion state is stored separately from scroll positions. - -These screenshots show the production widgets in an isolated preview with -synthetic library and activity fixtures; they demonstrate presentation, not live -reader or synchronization validation. - -![Desktop Overview](images/goals-review/overview-desktop.png) - -![Activity and full-width heatmap](images/goals-review/activity-desktop.png) - -![Daily goal creation with editable target](images/goals-review/new-goal.png) - -![Mobile Overview and New goal FAB](images/goals-review/overview-mobile.png) - -## Validation - -The isolated validation host uses real EPUB/PDF files and records actual reading. -Its fixture-opening buttons are not part of the production Goals page. - -![Desktop Goals, dark theme](images/goals-desktop.png) - -![Phone Goals, light theme](images/goals-mobile.png) - - -Focused regressions cover calendar/DST rules, cross-device overlap/coverage, -completion undo, corrections, recurrence, profile switches, SQLite restart, -atomic position/activity writes, and older-server staging/promotion. Shared JSON -projection fixtures match server aggregation. - -For real reader validation, serve an EPUB and PDF on a local HTTP fixture endpoint -as `/reader.epub` and `/reader.pdf` with CORS enabled. Fixtures are not committed. - -```sh -# From app/, with an Android emulator running: -../../tools/flutter test --no-pub --no-enable-impeller --timeout=3m integration_test/reading_tracking_test.dart \ - -d emulator-5554 --dart-define=TRACKING_FIXTURE_ORIGIN=http://10.0.2.2:7311 - -# Local browser inspection, from app/: -../../tools/flutter run -d chrome -t tool/preview_goals.dart \ - --dart-define=TRACKING_FIXTURE_ORIGIN=http://127.0.0.1:7311 -``` - -Manual logs, completion confirmations, and corrections share the installation -identity persisted in preferences; reader records retain their source. - -The preview uses an isolated guest database and exposes development-only browser -inspection hooks. It is never included in the production entry point. Real -multi-device PowerSync transport testing remains a separate opt-in integration -lane requiring configured local services. diff --git a/docs/images/auth/welcome-android.png b/docs/images/auth/welcome-android.png deleted file mode 100644 index 26049649..00000000 Binary files a/docs/images/auth/welcome-android.png and /dev/null differ diff --git a/docs/images/bottom-sheet-headers/desktop.png b/docs/images/bottom-sheet-headers/desktop.png deleted file mode 100644 index b347dbe8..00000000 Binary files a/docs/images/bottom-sheet-headers/desktop.png and /dev/null differ diff --git a/docs/images/bottom-sheet-headers/mobile.png b/docs/images/bottom-sheet-headers/mobile.png deleted file mode 100644 index 68b289dd..00000000 Binary files a/docs/images/bottom-sheet-headers/mobile.png and /dev/null differ diff --git a/docs/images/card-contrast/dark.png b/docs/images/card-contrast/dark.png deleted file mode 100644 index f22ecc29..00000000 Binary files a/docs/images/card-contrast/dark.png and /dev/null differ diff --git a/docs/images/card-contrast/eink.png b/docs/images/card-contrast/eink.png deleted file mode 100644 index 1dff02e7..00000000 Binary files a/docs/images/card-contrast/eink.png and /dev/null differ diff --git a/docs/images/card-contrast/high-contrast-dark.png b/docs/images/card-contrast/high-contrast-dark.png deleted file mode 100644 index 951e3ed8..00000000 Binary files a/docs/images/card-contrast/high-contrast-dark.png and /dev/null differ diff --git a/docs/images/card-contrast/high-contrast-light.png b/docs/images/card-contrast/high-contrast-light.png deleted file mode 100644 index db93db0e..00000000 Binary files a/docs/images/card-contrast/high-contrast-light.png and /dev/null differ diff --git a/docs/images/card-contrast/light.png b/docs/images/card-contrast/light.png deleted file mode 100644 index c1b02d36..00000000 Binary files a/docs/images/card-contrast/light.png and /dev/null differ diff --git a/docs/images/folders-catalogs/catalogs-desktop.png b/docs/images/folders-catalogs/catalogs-desktop.png deleted file mode 100644 index f26a37b2..00000000 Binary files a/docs/images/folders-catalogs/catalogs-desktop.png and /dev/null differ diff --git a/docs/images/folders-catalogs/catalogs-mobile.png b/docs/images/folders-catalogs/catalogs-mobile.png deleted file mode 100644 index 17189212..00000000 Binary files a/docs/images/folders-catalogs/catalogs-mobile.png and /dev/null differ diff --git a/docs/images/folders-catalogs/details-mobile.png b/docs/images/folders-catalogs/details-mobile.png deleted file mode 100644 index 60156736..00000000 Binary files a/docs/images/folders-catalogs/details-mobile.png and /dev/null differ diff --git a/docs/images/folders-catalogs/edit-desktop.png b/docs/images/folders-catalogs/edit-desktop.png deleted file mode 100644 index e08cc6c9..00000000 Binary files a/docs/images/folders-catalogs/edit-desktop.png and /dev/null differ diff --git a/docs/images/folders-catalogs/folders-desktop.png b/docs/images/folders-catalogs/folders-desktop.png deleted file mode 100644 index 72b4fca3..00000000 Binary files a/docs/images/folders-catalogs/folders-desktop.png and /dev/null differ diff --git a/docs/images/folders-catalogs/folders-mobile.png b/docs/images/folders-catalogs/folders-mobile.png deleted file mode 100644 index a6e79935..00000000 Binary files a/docs/images/folders-catalogs/folders-mobile.png and /dev/null differ diff --git a/docs/images/folders-catalogs/reconnect-desktop.png b/docs/images/folders-catalogs/reconnect-desktop.png deleted file mode 100644 index c58d6dca..00000000 Binary files a/docs/images/folders-catalogs/reconnect-desktop.png and /dev/null differ diff --git a/docs/images/goals-desktop.png b/docs/images/goals-desktop.png deleted file mode 100644 index 652a63e1..00000000 Binary files a/docs/images/goals-desktop.png and /dev/null differ diff --git a/docs/images/goals-mobile.png b/docs/images/goals-mobile.png deleted file mode 100644 index 7e6e5e5f..00000000 Binary files a/docs/images/goals-mobile.png and /dev/null differ diff --git a/docs/images/goals-review/activity-desktop.png b/docs/images/goals-review/activity-desktop.png deleted file mode 100644 index 0a65e8b7..00000000 Binary files a/docs/images/goals-review/activity-desktop.png and /dev/null differ diff --git a/docs/images/goals-review/new-goal.png b/docs/images/goals-review/new-goal.png deleted file mode 100644 index ec01cf45..00000000 Binary files a/docs/images/goals-review/new-goal.png and /dev/null differ diff --git a/docs/images/goals-review/overview-desktop.png b/docs/images/goals-review/overview-desktop.png deleted file mode 100644 index 7a7284b0..00000000 Binary files a/docs/images/goals-review/overview-desktop.png and /dev/null differ diff --git a/docs/images/goals-review/overview-mobile.png b/docs/images/goals-review/overview-mobile.png deleted file mode 100644 index b0a1d2dd..00000000 Binary files a/docs/images/goals-review/overview-mobile.png and /dev/null differ diff --git a/docs/images/library-ui/desktop-selection.png b/docs/images/library-ui/desktop-selection.png deleted file mode 100644 index 26552c55..00000000 Binary files a/docs/images/library-ui/desktop-selection.png and /dev/null differ diff --git a/docs/images/library-ui/mobile-list.png b/docs/images/library-ui/mobile-list.png deleted file mode 100644 index 8dd67a57..00000000 Binary files a/docs/images/library-ui/mobile-list.png and /dev/null differ diff --git a/docs/images/library-ui/mobile-picker.png b/docs/images/library-ui/mobile-picker.png deleted file mode 100644 index eb8f39b4..00000000 Binary files a/docs/images/library-ui/mobile-picker.png and /dev/null differ diff --git a/docs/images/settings/licenses-scrolled.png b/docs/images/settings/licenses-scrolled.png deleted file mode 100644 index db9ee57a..00000000 Binary files a/docs/images/settings/licenses-scrolled.png and /dev/null differ diff --git a/docs/images/settings/mobile-appearance.png b/docs/images/settings/mobile-appearance.png deleted file mode 100644 index 00258a9b..00000000 Binary files a/docs/images/settings/mobile-appearance.png and /dev/null differ diff --git a/docs/images/settings/mobile-index.png b/docs/images/settings/mobile-index.png deleted file mode 100644 index e92ce628..00000000 Binary files a/docs/images/settings/mobile-index.png and /dev/null differ diff --git a/docs/images/settings/storage-wide.png b/docs/images/settings/storage-wide.png deleted file mode 100644 index ef9763a4..00000000 Binary files a/docs/images/settings/storage-wide.png and /dev/null differ diff --git a/docs/images/settings/updates-retry.png b/docs/images/settings/updates-retry.png deleted file mode 100644 index 172fb198..00000000 Binary files a/docs/images/settings/updates-retry.png and /dev/null differ diff --git a/docs/opds-browser-access-analysis.md b/docs/opds-browser-access-analysis.md deleted file mode 100644 index 6d425f23..00000000 --- a/docs/opds-browser-access-analysis.md +++ /dev/null @@ -1,17 +0,0 @@ -# OPDS browser transport - -The web client routes OPDS feeds, covers and book downloads through the Papyrus -server relay. Public browsing and guest imports do not require an account, but -the configured server must be reachable. Guest imports stay in the local library. -See [OPDS setup and behavior](opds-support.md). - -A catalog response allowing browser access does not guarantee that its downloads -allow it: redirects and final file responses can have different CORS policies. -A direct-first fallback would make imports depend on each publisher's response -headers. The relay keeps this behavior consistent and applies backend origin, -redirect and credential rules. It does not bypass catalog authentication. - -The server implementation and security checks live in `papyrus/services/opds.py` -and `tests/api/routes/test_opds.py`; the client transport is -`app/lib/opds/opds_http_client.dart`. Keep relay/account isolation and cancellation -regressions when changing this boundary. diff --git a/docs/opds-support.md b/docs/opds-support.md deleted file mode 100644 index c0053c5a..00000000 --- a/docs/opds-support.md +++ /dev/null @@ -1,88 +0,0 @@ -# OPDS support - -Papyrus browses OPDS 1.2 (Atom/XML) and OPDS 2.0 (JSON) catalogs through its backend relay and imports book downloads automatically into the active library. A running Papyrus backend is required; a Papyrus account is not. - -## Using catalogs - -1. Open **Library → Catalogs**. On a narrow screen, open **Library sections** first. -2. Select **Add catalog** and enter a name and the catalog's HTTP or HTTPS URL. Enter a username and password if the catalog uses HTTP Basic authentication. -3. Open the catalog, browse its sections or use keyword search, and select a publication to open its book details page. Switch between cover grids and lists using the view control. Details preserve description paragraphs and can be refreshed or opened directly from their URL. Back returns through each visited feed, edition selection and details page, preserving search, scroll position and view mode. A shared subsection with no prior visit returns to its catalog's root, then to all catalogs. Browser URLs continue to identify the visible feed or publication. -4. Select **Add to library** to open the download-options sheet, then choose a supported format. The **Downloads** control in the page header opens progress, cancellation, retry, and completed transfers. Downloads continue when you close a sheet or navigate elsewhere in Papyrus. Use **Open book** after the import finishes. - -Catalog creation, settings, removal confirmation, download options, and transfer activity use bottom sheets on all screen sizes. Forms keep their actions above the keyboard; catalog and book lists use the library's page alignment and theme. - -Web imports support EPUB. Native imports support EPUB, PDF, MOBI, AZW3, TXT, CBZ, and CBR, matching the existing file importer. The short final **Adding to library** step cannot be cancelled; earlier download and processing steps can. Failed downloads offer Retry. - -Catalog settings stay on this device, separately for each server/account and for the guest library. Credentials are stored through the platform's secure-storage implementation, separately from catalog preferences and bound to the catalog origin. Removing a catalog also removes its credentials; downloaded books remain in your library. On an existing catalog, leave the credentials blank to retain them, or select **Remove saved credentials**. Changing its URL origin clears saved credentials unless replacement credentials are entered. - -Imported books use the existing local file storage, metadata persistence, cover handling, and media-upload queue. Embedded book metadata takes precedence; catalog metadata fills missing values. Signed-in libraries retain their existing synchronization behavior. Switching accounts invalidates in-flight OPDS work before it can import into the newly selected library. - -## Previously imported books and saved catalog content - -After a successful import, publications show **In library** and their details offer **Open book**, including after restarting Papyrus. **Download options** remains available for other formats. Recognition uses the saved catalog, source URL, exact publication identifier and acquisition URL; matching titles or ISBNs never combine editions. Samples do not mark the full publication as imported. Removing a book from the active library makes it downloadable again. - -Recognition is local to this device and library account, including the guest library. Older imports and manually added books have no OPDS provenance and are not matched automatically. Renaming a catalog preserves recognition; removing it, changing its source URL, or replacing/removing its credentials clears that catalog's recognition without deleting any books. - -Previously visited feeds appear immediately and refresh through the relay in the background. If refreshing fails, Papyrus keeps saved content visible with a retry notice. Previously cached publication details can also open from their URLs offline. Cached cover images are reused for up to one day before refreshing. Offline browsing covers saved pages only; new searches/pages and book downloads still need a connection, though saved search descriptions can resolve previously visited search URLs. - -The response cache is shared across scopes with a total serialized limit of 2 MiB, 64 entries, and 512 KiB per entry. Least recently used entries are evicted; larger responses still load online but are not saved. The cache retains response bytes, final URLs, timestamps and content types, never authorization headers or passwords. It does not cache book files. Account switching hides other accounts' content; source/credential edits, removal and authorization failures invalidate affected cached content and outstanding requests. Corruption or unavailable browser storage does not block browsing or importing. - -## Compatibility and troubleshooting - -- Feeds, search descriptions, covers, and book files all pass through the selected Papyrus backend. Catalogs do not need browser CORS support. The app must be able to reach its backend, whose `CORS_ORIGINS` must allow the web app. Use an HTTPS backend when hosting the web app over HTTPS. -- Authentication supports HTTP Basic. The selected backend receives catalog credentials for the request and forwards them only to that catalog's origin. Use a trusted backend and HTTPS for the backend and protected catalog. Papyrus account tokens and browser cookies are never forwarded upstream. -- The relay only permits public network destinations. Loopback, private LAN, link-local, and metadata-service addresses are rejected, including after redirects. Administrators can further restrict catalog hosts. Private LAN catalogs are not supported by this relay. -- Browsing includes groups, facets, pagination, complete publication entries, and advertised keyword search. OPDS 1.2 uses OpenSearch descriptions; OPDS 2.0 uses URI templates. -- Purchases, loans, subscriptions, DRM, indirect acquisition, and OAuth are displayed as unsupported acquisition methods. Papyrus does not follow those links as book downloads. -- Catalog settings, import recognition and cached catalog content do not sync between devices. Downloads do not resume after the app closes. -- Feed, search-description, and image responses are limited to 8 MiB; book downloads to 256 MiB. DNS and opening a response each have a 30-second limit, stalled reads time out after 30 seconds, and each upstream request has a five-minute total deadline. These limits protect the current in-memory import pipeline. -- The client queues excess requests and briefly retries temporary relay-capacity errors. Authentication errors offer guidance to edit catalog credentials. Other failures remain in the download panel with Retry; no manual save-and-import step is required. - -### Server setup - -The official backend comes from `PAPYRUS_API_BASE_URL` (default `http://localhost:8080`). Selecting a custom server uses that server's relay, including in guest mode. Deploy the updated client and backend together. If OPDS requests return 404, update the backend and check its API prefix and reverse-proxy routing. - -The relay is enabled by default. Operators can disable it with `OPDS_RELAY_ENABLED=false` or restrict destinations using `OPDS_RELAY_ALLOWED_HOSTS`, a JSON list of exact hostnames. All redirect and image/CDN hosts must also be permitted. An empty list allows public hosts. Guest imports stay local; relaying does not create an account or upload the guest library. - -## Implementation - -Catalog book details share the library's section headings, description typography and metadata rows. Description and Information use two columns when space permits, and stack on mobile; subjects are read-only chips. The parser retains available publication dates, page counts, rights and subjects. Atom entry timestamps are not treated as publication dates. - -For catalogs hosted on `gutenberg.org` or its subdomains, a presentation adapter recognizes Gutenberg's labeled description paragraphs. Summary, edition prose, credits, notes and unfamiliar paragraphs appear in Description. Metadata, including reading level and classification, appears in Information; conflicting values remain visible in the corresponding row. Subject chips exclude classification and category labels already shown in Information. All content is visible without expanding a section. The original description remains unchanged in the publication model. Other catalogs retain their supplied descriptions without this extraction. - -The `lib/opds` module separates normalized models, XML/JSON parsing, search expansion, scoped persistence, HTTP transport, browsing state, and application-scoped downloads. `BookImportSession` captures the existing import destination and supplies the shared `BookImportCommitService` composition used by OPDS and file-import widgets. - -The routes `/library/catalogs` and `/library/catalogs/:catalogId` live inside the existing Library shell. The `feed` query parameter stores the current resource URL, and `q` retains the displayed keyword query. Neither contains Basic credentials. Browser history and refresh reload the selected feed. - -`OpdsHttpClient` posts resource URLs and optional catalog credentials to `/v1/opds/relay`. It reads the final upstream URL from `X-OPDS-URL` so relative links resolve against the catalog. The backend streams bytes without storing books. No database schema or dependency changes are required. - -## Verification - -From `client/app`, run: - -```sh -flutter test --coverage -flutter analyze --no-pub -dart format --output=none --set-exit-if-changed lib test -flutter build web --no-pub -flutter build linux --no-pub -``` - -Network smoke tests use a local fixture server that emulates the relay contract for its own loopback resources. These tests are skipped in the normal suite. Start it in a separate terminal: - -```sh -python3 tool/opds_fixture_server.py --port 8766 -``` - -Then run the same tests on the Linux Dart VM and Chrome: - -```sh -flutter test test/opds/network_smoke_test.dart --no-pub --dart-define=OPDS_SMOKE_URL=http://127.0.0.1:8766 -flutter test test/opds/network_smoke_test.dart --no-pub --platform chrome --dart-define=OPDS_SMOKE_URL=http://127.0.0.1:8766 -``` - -The fixtures cover public and Basic-auth catalogs in both formats, browsing, details, search, pagination, EPUB bytes, redirect URL resolution, authentication failures, and cross-origin credential stripping. They use the test credentials `reader` / `secret` and bind only to localhost. - -The smoke suite checks that missing CORS headers on upstream redirects and book responses do not prevent relay downloads on Chrome or native platforms. The catalog at `/public/v2/blocked.json` exercises this flow. - -For visual fixture checks, launch the client with `--dart-define=PAPYRUS_API_BASE_URL=http://127.0.0.1:8766` and add `http://127.0.0.1:8766/public/v2/showcase.json` as a catalog. This fixture includes books, covers, sections, and long descriptions. Replace `public` with `protected` to check Basic authentication. This loopback exception exists only in the fixture server; the production backend rejects these destinations. diff --git a/docs/pdf-import-validation.md b/docs/pdf-import-validation.md deleted file mode 100644 index 1de372c7..00000000 --- a/docs/pdf-import-validation.md +++ /dev/null @@ -1,55 +0,0 @@ -# PDF import validation — 2026-10-10 - -Native PDF import and the EPUB/PDF reader already existed. Browser imports were -restricted to EPUB in both the picker and the import service. Web imports now -accept PDF; the same allowlist also enables PDF acquisitions from OPDS catalogs. - -PDF metadata extraction is shared by native and browser import paths using the -existing Syncfusion dependency. It reads title, author, subject and page count, -with the filename as the title fallback. The browser worker hashes the original -bytes and stores them in OPFS before the normal library commit. Selected bytes -are retained for failed-import retries. The worker URL has a protocol revision -so existing clients do not keep using a cached EPUB-only worker. - -PDF parsing runs in Dart; hashing and browser storage run in the import worker. -No new runtime downloads, dependencies, database/locator schemas or release pins -were introduced. Password entry during import and generated PDF cover thumbnails -are not implemented; PDFs without a cover use the existing library placeholder. - -## Verification - -- 98 focused client tests passed, including shared PDF metadata, native file - storage/hash/reload/delete, filename fallback, malformed input, worker storage - errors, import UI/controller/commit and reader locator/session tests. -- Eight pre-existing metadata tests skipped because their local EPUB/MOBI - fixtures were unavailable. The new PDF tests did not skip. -- Client analysis, formatting/bootstrap checks and production web build passed. -- Actual desktop Chrome: selected a PDF through Add book → Import digital books, - imported it, checked metadata, opened its rendered pages, turned pages, sought - to page 435, left the reader, restarted the app and resumed at page 435 (49%). -- Native import storage was checked with Flutter tests. A new native-device PDF - picker session and authenticated server synchronization were not run. - -The real-book browser input was [Structure and Interpretation of Computer -Programs, second edition](https://web.mit.edu/6.001/6.037/sicp.pdf), hosted by MIT: -883 pages, approximately 7.1 MiB. SHA-256: - -``` -08709a87567d8311d6fd29c4f4a5386801153e71450e628c4a5a5d7e85feda8b -``` - -The downloaded book, screenshots and logs remain local under ignored -`app/build/pdf-validation/`; the PDF is not committed. - -From `client/app`, using the workspace-pinned Flutter SDK and ignored local reader -override: - -```sh -../../tools/flutter test test/services/pdf_book_import_test.dart \ - test/services/file_metadata_service_test.dart \ - test/services/book_cover_storage_test.dart \ - test/services/book_import_commit_service_test.dart \ - test/widgets/add_book test/reader/reader_book_adapter_test.dart \ - test/reader/reader_session_test.dart -../../tools/flutter build web --no-pub -``` diff --git a/docs/web-hot-restart.md b/docs/web-hot-restart.md deleted file mode 100644 index 544c1722..00000000 --- a/docs/web-hot-restart.md +++ /dev/null @@ -1,56 +0,0 @@ -# Web hot restart - -Papyrus performs a full browser refresh when Flutter requests a web hot restart -(`R` in the development terminal). Normal hot reload (`r`) is unchanged. - -## Why the previous startup failed - -The old web cleanup helper stored a Dart callback on `window`, then called it -from the next invocation of `main()`. By that point, hot restart had already -reset Dart's runtime types. PowerSync/SQLite worker messages and other browser -callbacks could still reach objects from the previous runtime. This produced -type errors involving `LegacyJavaScriptObject`, update notifications, and -Flutter diagnostics. - -Cleanup was awaited before `runApp()`. When it failed, startup stopped, leaving -a blank page. The callback was cleared before invocation, which explains why a -second restart could succeed. A unit test calling cleanup twice within a single -Dart runtime could not detect this failure. - -## Current behavior - -`app/web/flutter_bootstrap.js` uses Flutter's documented -[`onEntrypointLoaded` callback](https://docs.flutter.dev/platform-integration/web/initialization#the-onentrypointloaded-callback), -which runs on initial loading and each hot restart. Its JavaScript state survives -a Dart restart. The second entrypoint invocation reloads the document instead of -initializing another Dart runtime alongside stale browser listeners. - -This retains the current URL and browser storage, including the offline library. -It does not clear databases or other tabs. Normal widget disposal still closes -the app's data services; no Dart cleanup closure is retained across restarts. - -Refresh the browser once after installing this change so the page loads the new -bootstrap. Subsequent terminal restarts refresh automatically. Flutter's debugger -may log `Cannot find context with specified id` during reconnection to the new -document; this is separate from the previous Dart startup failure and did not -prevent the app from rendering in validation. - -## Verification - -- Reproduced stale-runtime errors and a blank first restart in an isolated Chrome - session before the change. -- Created a physical book in an offline library, then performed three consecutive - terminal hot restarts. Each returned to `/library/books`, rendered the saved - book, and preserved a browser-storage marker without a second restart. -- Normal hot reload retained the same browser document and rendered library. -- Four bootstrap tests passed, covering cold startup, repeated restarts, a fresh - document, and a restart during initialization. -- 37 relevant PowerSync, auth, and composition tests passed; one case was skipped - by the existing test configuration. Flutter analysis and the - production web build passed. - -Run the bootstrap tests from `app/`: - -```sh -node --test test/web/flutter_bootstrap_test.cjs -```