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 @@
@@ -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