diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 91ce075..924b679 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,8 +17,8 @@ jobs: - uses: actions/checkout@v4 with: repository: PapyrusReader/server - # Combined account-deletion (#40) and support (#41) API snapshot revision. - ref: cc69c75abfb5b48954fcbe0593f247ef32bb5244 + # Includes the private profile-photo API from server PR #42. + ref: 235fd051e2bf72283bcc6da4ecb2d1860b6ef332 path: .server - uses: astral-sh/setup-uv@v6 with: diff --git a/_static/openapi.json b/_static/openapi.json index e04a57d..371715b 100644 --- a/_static/openapi.json +++ b/_static/openapi.json @@ -1613,6 +1613,27 @@ "title": "Body_create_support_issue_v1_support_issues_post", "type": "object" }, + "Body_upload_avatar_v1_users_me_avatar_post": { + "properties": { + "display_name": { + "maxLength": 100, + "minLength": 1, + "title": "Display Name", + "type": "string" + }, + "file": { + "contentMediaType": "application/octet-stream", + "title": "File", + "type": "string" + } + }, + "required": [ + "file", + "display_name" + ], + "title": "Body_upload_avatar_v1_users_me_avatar_post", + "type": "object" + }, "Body_upload_book_cover_v1_books__book_id__cover_put": { "properties": { "file": { @@ -15046,6 +15067,98 @@ ] } }, + "/v1/users/me/avatar": { + "post": { + "operationId": "upload_avatar_v1_users_me_avatar_post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_upload_avatar_v1_users_me_avatar_post" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "security": [ + { + "HTTPBearer": [] + } + ], + "summary": "Save profile with private photo", + "tags": [ + "Users" + ] + } + }, + "/v1/users/me/avatar/{avatar_id}": { + "get": { + "operationId": "download_avatar_v1_users_me_avatar__avatar_id__get", + "parameters": [ + { + "in": "path", + "name": "avatar_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Avatar Id", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": {} + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "security": [ + { + "HTTPBearer": [] + } + ], + "summary": "Download current private profile photo", + "tags": [ + "Users" + ] + } + }, "/v1/users/me/change-password": { "post": { "description": "Change the user's password and revoke existing sessions.", diff --git a/design/server-architecture.rst b/design/server-architecture.rst index b7c5858..e1bf0e5 100644 --- a/design/server-architecture.rst +++ b/design/server-architecture.rst @@ -56,6 +56,26 @@ switch the active library without transferring guest data. Server profiles and users also have separate database and media scopes. Becoming connected does not turn a guest library into a synchronized account library. +Profile photos +-------------- + +``POST /v1/users/me/avatar`` accepts a multipart JPEG/PNG ``file`` and +``display_name``. The server bounds each photo to 5 MiB, checks its image +signature, serializes writes on the owner row, and commits the photo reference +and name together. Photos use a separate fixed allowance from library storage. +The client resizes photos to at most 512 pixels on the longest side and removes +source metadata before uploading. + +The existing ``avatar_url`` field stores an API-prefix-relative reference such +as ``/users/me/avatar/{uuid}``. Fetching it requires the owner's access token +against the configured server. External provider photo URLs remain supported, +but are fetched without Papyrus credentials. Downloads are private and marked +``no-store``. Replacing a photo removes the previous file after commit, and +permanent account deletion removes the entire owner media directory, including +photos. Failed writes retain the previous reference and file. This increment +does not change database schemas or PowerSync projections. Deploy the updated +server before clients that offer photo uploads. + Account deletion and recovery ----------------------------- diff --git a/implementation/accessibility.rst b/implementation/accessibility.rst new file mode 100644 index 0000000..a102a1e --- /dev/null +++ b/implementation/accessibility.rst @@ -0,0 +1,194 @@ +Accessibility foundation audit +============================== + +Scope and standard +------------------ + +This increment targets `WCAG 2.2 Level AA `_, +with `WCAG2ICT `_ guidance for native software. +WCAG2ICT interprets WCAG for non-web documents and software. It is not a separate +conformance certification. Requirement :need:`NFR-5_2` remains open. + +The scope is client authentication, primary navigation, library search and +selection, book details/editing, file import, Settings, and the shared controls, +menus, dialogs, and sheets those journeys use. Passing the checks below is not +full-app conformance. Reader-engine remediation, EPUB/PDF content, read-aloud, +reading guides, and broader pages such as goals, statistics, and online catalogs +are outside this increment's audit. + +Implementation contract +----------------------- + +``PreferencesProvider`` stores device-wide interface enlargement, increased +contrast, and bold text alongside the existing font and motion settings. +Unsupported saved enlargement values fall back to System default. No server +schema, synchronization payload, or database migration changes are involved. + +``AppAccessibilityScope`` retains the system text scaler and bold-text flag before +applying overrides. Enlargement multiplies each result of the original scaler, +rather than replacing its nonlinear curve with a linear estimate. System bold +text and reduced motion cannot be disabled by an app preference. Light and dark +high-contrast themes use Material's maximum contrast setting and build component +themes from that palette. E-ink retains its separate palette and motion policy. +OpenDyslexic is bundled with its license and applies to all theme variants. + +``ReaderSystemTextScope`` restores system text metrics and bold text around the +embedded reader and reader panels. Reader preferences, book rendering settings, +and stored locators are unchanged. Tests of the adapter boundary do not evaluate +the accessibility of book content. + +Audit matrix +------------ + +Results refer to local verification on 24 October 2026. **Automated** means the +specified regression scenario passes, not that every instance of the criterion +has been evaluated. **Pending** means acceptance evidence is still required. +Test paths below are relative to ``client/app/test``. + +.. list-table:: Criteria, scenarios, and evidence + :header-rows: 1 + :widths: 19 28 35 18 + + * - Criteria + - Scenario and remediation + - Evidence + - Result / limit + * - 1.3.1, 4.1.2: information, relationships, names, roles, states + - Settings toggles expose one labeled state and one action, including + disabled state. Picker choices expose radio selection. Search and icon actions + have names. Decorative covers do not duplicate book titles. + - ``themes/app_accessibility_test.dart``, + ``widgets/profile/accessibility_settings_section_test.dart``, + ``widgets/accessibility_journeys_test.dart``. + - Automated for sampled controls. Native announcements pending. + * - 1.3.2, 2.4.3: reading and focus order + - Nested page navigators must not remove the primary navigation from the + accessibility tree. The shell provides a semantics boundary. Settings pickers use the root + modal layer so background navigation is hidden until dismissal. + - ``widgets/shell/navigation_counts_test.dart`` checks mobile and desktop + navigation remains exposed. Chrome accessibility-tree inspection found + the original defect and verified navigation after remediation. The + regression also checks picker selection and restoration of focus. + - Automated regression. Complete native reading-order review pending. + * - 1.4.3, 1.4.11: contrast + - Normal, dark, e-ink, and high-contrast palettes. Auth errors pair error + background with on-error foreground. Divider labels use readable text + color. Cover overlays retain their original translucent styling. + - Flutter text-contrast guideline checks for shared settings toggles, + book actions, and search in all five themes. + - Automated sample. Image content and every color/state combination are + not certified by these checks. + * - 1.4.4, 1.4.10: text enlargement and reflow + - Settings and auth at 320, 390, and 1280 logical pixels. System 200% + combined with app +100% retains 400% effective enlargement. Keyboard + inset does not hide auth submission. Navigation labels remain scalable. + - ``widgets/accessibility_journeys_test.dart``, + ``widgets/profile/accessibility_settings_section_test.dart``, + ``widgets/accessibility_library_journeys_test.dart``. + - Automated scenarios. Device font rendering and browser zoom still + require platform acceptance. + * - 2.1.1, 2.1.2, 2.4.7, 2.4.11: keyboard and focus + - Whole-row toggle activation, keyboard activation, reduced-motion focus + retention, explicit library action menus, keyboard-operable star ratings, + and dialog dismissal. + - ``themes/app_accessibility_test.dart``, + ``widgets/app_motion_control_test.dart``, + ``themes/eink_input_focus_test.dart``. Chrome keyboard inspection checks + dialog Escape dismissal and focus restoration. + - Automated shared controls and sampled browser behavior. Exhaustive + focus visibility/containment across native dialogs pending. + * - 2.5.1, 2.5.7: alternatives to gestures and dragging + - Desktop card actions appear on hover or keyboard focus and retain + programmatic labels. Compact cover overlays retain the original design. + File import offers Browse files alongside desktop drop. Sheets retain + explicit actions and dismiss behavior. + - ``widgets/library/book_card_test.dart``, + ``widgets/library/book_list_item_test.dart``, + ``widgets/add_book/book_import_sheet_test.dart``. + - Automated sampled actions. Touch card gesture alternatives and + compact overlay target sizing remain gaps. No reader gesture audit. + * - 2.5.8: target size + - Standalone touch controls use at least 48 logical-pixel hit areas even + when artwork is smaller. This product target is stricter than WCAG AA's + 24 CSS-pixel minimum, which has defined spacing and other exceptions. + - Flutter Android (48) and iOS (44) tap-target guidelines on sampled + settings, search, and book detail action controls. Compact cover + overlays retain their original 30-pixel targets and are an exception + to the product target, not covered by those guideline checks. + - Automated sample. Flutter logical pixels and CSS pixels/points are + platform-specific units, not a universal physical-size equivalence. + * - 1.4.1, 3.3.1, 3.3.2, 4.1.3: errors and status + - Invalid auth input has text errors and retains focus/input. Import + failures have a live region. Selection has checked/selected state. + Completion snackbars use Flutter's live-region semantics. Progress + updates are not individually announced. + - Auth validation tests, import state tests, existing completion/error + paths, and settings semantics tests. + - Automated behavior and code inspection. Actual announcement timing + with VoiceOver/TalkBack pending. + * - 3.3.8: accessible authentication + - Login permits password paste and provides email/password autofill hints. + Registration/recovery provide new-password hints. Visibility buttons + are named Show password / Hide password. + - ``widgets/accessibility_journeys_test.dart`` exercises paste and + visibility and inspects autofill hints. + - Automated. External password-manager integration pending. + * - System preferences and reader isolation + - Live system/app changes, nonlinear scaling, storage reload, malformed + preferences, fonts, contrast, bold text, reduced motion, and reader + boundary behavior. + - ``providers/accessibility_preferences_test.dart``, + ``themes/app_accessibility_test.dart``, + ``widgets/profile/accessibility_settings_section_test.dart``, existing + reader adapter and persistence tests. + - Automated. No reader-package or stored-position schema change. + +Platform acceptance +------------------- + +Chrome on macOS was used to inspect the actual Flutter web accessibility tree, +verify exposed primary navigation and filter-button roles, activate Settings +controls, preview +100% text enlargement and OpenDyslexic, confirm preferences +survive reload, and verify that the font picker hides background navigation +and exposes radio choices. Escape dismissal and restoration of focus were checked +from a confirmation dialog. Accessibility-tree inspection is not equivalent to +listening to a screen reader. + +The following remain pending and must not be reported as passed: + +- VoiceOver on macOS/iOS: navigate representative auth, library, details/edit, + import, and Settings journeys. Check order, single announcements, live-region + timing, rotor access, and restoration after dialogs and sheets. +- TalkBack on Android: repeat those journeys with system large text, bold text, + high contrast where available, and reduced motion. No Android target was + available in the local device inventory for this increment. +- Native keyboard traversal and external password-manager acceptance on each + supported platform. Confirm focus is visible and not obscured at every step. +- Broader app pages and EPUB/PDF content accessibility, including document + structure, alternative text, reading order, and reader-engine interactions. + +Local validation results +------------------------ + +- Full client suite: 1,927 passed, 19 skipped. The skipped tests are not counted + as accessibility acceptance evidence. +- Client quality checks: formatting, analysis, and four web bootstrap tests. +- Chrome regressions verify saved guest avatars decode after browser-storage + reload, Settings scrolling works over empty space, and short desktop sections + remain aligned at the top. Account form checks cover 320, 390, and 1280 logical + pixels at normal and 200% text size. +- Release web compilation: passed, including Flutter's Wasm dry-run check. +- Documentation: warning-as-error Sphinx build passed. +- Chrome inspection: primary navigation and Settings controls exposed, live + enlargement observed, and a confirmation dialog dismissed with Escape with + focus restored. VoiceOver and TalkBack are not included in these results. + +Reproduction +------------ + +Run focused preference, theme, and journey tests, followed by the full client +suite and ``tools/papyrus check client``. Build the web client with the pinned +Flutter SDK and local dart defines. Build this documentation with ``make build`` +from ``docs``. Validation used the workspace's existing local reader path override, which was +preserved. Remote CI was not run. Test success, compilation, browser inspection, +and assistive technology acceptance are separate forms of evidence. diff --git a/manual/index.rst b/manual/index.rst index fdb1d38..30ab659 100644 --- a/manual/index.rst +++ b/manual/index.rst @@ -40,12 +40,46 @@ DRM-protected content are not supported. Book-level notes, annotations, and bookmarks should not be confused with selecting and highlighting text in the reader, which remains unfinished. +Accessibility settings +---------------------- + +Open **Settings > Accessibility**. These preferences take effect immediately +and are stored on this device, shared across guest and signed-in profiles. + +- **Interface text enlargement** adds 15%, 30%, 50%, or 100% to the system's + existing text scaling. **System default** adds no enlargement. The system's + nonlinear scaling curve is preserved. A live preview shows the result. +- **Increase contrast** follows the system when off and forces increased + contrast when on, in either light or dark mode. E-ink uses its own contrast + treatment, so this control is disabled while that appearance is selected. +- **Bold interface text** follows the system when off and adds bold interface + text when on, including editable fields and navigation. +- **Interface font** offers **System default** and **OpenDyslexic** with a live + preview. This is a personal reading preference, not a claim that a particular + font improves reading for every person with dyslexia. +- **Reduce animations** limits interface motion. System reduced motion is + always honored, and e-ink also reduces motion. + +Additional interface text enlargement and bold text do not change embedded +reader text metrics. Reader controls retain system scaling and bold-text +settings, and book rendering retains the reader's own preferences. These +settings do not change books, saved positions, or reading preferences. + Accounts and offline access --------------------------- An account connects a library to a Papyrus server for synchronization. Different users and server profiles have separate libraries and cached media. +In **Settings > Account**, users can edit their display name and choose +a profile photo directly. **Save** applies both changes together. JPEG and PNG +photos up to 5 MB and 25 megapixels are supported. Photos are resized and image +metadata is removed before saving. In anonymous mode, the name and photo are +saved on this device without a network connection and survive app restarts. +They stay separate from signed-in profiles and are removed when the anonymous +account is deleted. Signed-in profile changes are saved to the account's server. +Email is shown for reference and cannot be edited here. + Books need to be available locally to read without a network connection. Synchronization and online catalogs require connectivity. Full-library content search is a product requirement and is not currently an available feature. diff --git a/requirements/non-functional.rst b/requirements/non-functional.rst index 5836d63..5adccf2 100644 --- a/requirements/non-functional.rst +++ b/requirements/non-functional.rst @@ -348,17 +348,26 @@ missing. See :doc:`index` for status definitions. :status: open :priority: P0 - The application shall be accessible to users with disabilities (WCAG 2.1 AA compliance). + The application shall target WCAG 2.2 Level AA, using WCAG2ICT guidance + to interpret the criteria for native software. Conformance requires a + complete evaluation of the declared scope, not only automated checks. **Acceptance criteria:** - Screen reader compatibility (TalkBack, VoiceOver) - - Minimum color contrast ratio 4.5:1 - - All interactive elements keyboard accessible - - Focus indicators visible - - Text scalable up to 200% + - Text contrast of at least 4.5:1, or 3:1 for qualifying large text + - Meaningful control boundaries and state indicators have at least 3:1 contrast + - All functionality keyboard accessible, with visible, unobscured focus + - Text scalable to at least 200% without loss of content or functionality - No information conveyed by color alone - - Touch targets minimum 44x44 points + - Programmatic names, roles, states, and important status messages + - Accessible authentication permits paste and password-manager assistance + - Standalone client touch controls have at least 48 logical-pixel hit areas + (a product target exceeding WCAG 2.2 AA target-size minimums) + + The :doc:`/implementation/accessibility` matrix records foundation coverage + and outstanding checks. This requirement remains open until the complete + application scope has been evaluated. .. nfr:: RTL and internationalization :id: NFR-5_3 diff --git a/technical/index.rst b/technical/index.rst index 3a4f463..4054168 100644 --- a/technical/index.rst +++ b/technical/index.rst @@ -187,6 +187,7 @@ Related documents /implementation/technologies /implementation/github-workflow + /implementation/accessibility .. toctree:: :maxdepth: 1