Skip to content

feat(courses): Add PDF download functionality with watermarking - #216

Merged
pugal66 merged 2 commits into
mainfrom
feat/download-pdf-watermark
Jul 27, 2026
Merged

feat(courses): Add PDF download functionality with watermarking#216
pugal66 merged 2 commits into
mainfrom
feat/download-pdf-watermark

Conversation

@pugal66

@pugal66 pugal66 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor
  • Adds a download button to the PDF viewer that is only visible when the backend returns allow_download: true.
  • Automatically stamps the downloaded PDF with the user's username if the backend returns watermark_before_download: true.

@claude

claude Bot commented Jul 21, 2026

Copy link
Copy Markdown

Claude finished @pugal66's task in 5m 17s —— View job


Review complete

  • Read AGENTS.md, ai_context.md, architecture.md
  • Check for relevant ADRs (read 0005-user-state-in-core.md — this PR correctly uses userProvider from core for the username watermark)
  • Fetch full PR diff
  • Review OpenSpec compliance
  • Review SDK boundaries & imports
  • Review design system & widget usage
  • Review accessibility semantics
  • Review motion/animation
  • Review platform neutrality
  • Review navigation patterns
  • Review Riverpod provider patterns
  • Review data layer architecture
  • Review generated files (.g.dart)
  • Review dependency management
  • Post inline comments and final summary

PR Review Summary

✅ What's good

  • OpenSpec change is present and thorough (proposal.md, design.md, tasks, and modified/added capability specs) — good compliance with the spec-driven workflow.
  • Correctly reuses userProvider from package:core (per ADR 0005) instead of reaching into profile, keeping SDK boundaries clean — no cross-package import violations found.
  • Watermark stamping runs in a background isolate (compute) to avoid blocking the UI thread, and shares WatermarkParams between the in-app overlay and the PDF-stamping pipeline for visual consistency.
  • app_database.g.dart diff correctly reflects the new allowDownload column added in lessons_table.dart — no hand-edited generated code.
  • pdfDownloadServiceProvider as a manual Provider(...) is appropriate here (infra/DI wiring), consistent with existing patterns.
  • Icon(icon) LucideIcons pattern and AppSemantics.button usage on the header's existing download button are followed correctly.

🔴 CRITICAL — Must fix before merge

  1. Spec vs. implementation mismatch on watermarking. The OpenSpec (specs/pdf-watermarking/spec.md, design.md) explicitly requires the downloaded PDF to be watermarked "only if watermarking is enabled for that PDF." PdfDownloadService.downloadAndWatermarkPdf (pdf_download_service.dart:76-91) stamps unconditionally — no such per-lesson flag exists anywhere in the diff. Either thread through the missing config flag or update the spec to match actual (always-on) behavior.
  2. Missing accessibility semantics on new interactive elements — violates the mandatory AppSemantics.* pattern used elsewhere in this same PR:
    • download_progress_banner.dart:54-58 — the "View" tap target has no semantics wrapper at all (compare to _HeaderActionButton in lesson_detail_header.dart, which always wraps with AppSemantics.button).
    • download_progress_banner.dart — the $progress% text has no AppSemantics.progressValue() wrapping.
    • already_downloaded_sheet.dart:35-40 — uses a raw Semantics(button: true, ...) instead of AppSemantics.button(), and is missing the onTap action wiring that the helper provides, which may prevent assistive-tech activation from working.

🟡 WARNING — Should fix

  1. New PDF downloads bypass the existing Downloads registry. Video/attachment downloads are tracked centrally via DownloadsTable/DownloadType and shown in downloads_screen.dart. PdfDownloadService writes directly to public storage and checks "already downloaded" via ad-hoc File.exists() (pdf_download_service.dart:15-30), creating a second, filesystem-based source of truth outside the Repository-owned registry — downloaded PDFs won't appear in the app's Downloads screen.
  2. Missing DB migration for new column. lessons_table.dart adds allowDownload, but AppDatabase.schemaVersion (app_database.dart:59) wasn't bumped. The current onUpgrade only creates missing tables, never adds columns to existing ones — existing local DBs will never get this column. This repo's convention (see archived changes bumping to versions 22/29/31) is to bump schemaVersion for any table alteration.
  3. Dead code with a divergent, duplicate download flow. PdfLessonDetailScreen (pdf_lesson_detail_screen.dart) doesn't appear to be referenced anywhere — the live PDF path is LessonDetailOrchestrator (lesson_detail_orchestrator.dart:341). This PR added a second PdfDownloadService call into the unused screen, missing the already-downloaded check, progress banner, and error toast that the orchestrator's flow has.
  4. Icon library violation. already_downloaded_sheet.dart:40 (Icons.close) and download_progress_banner.dart:38 (Icons.check_circle) use Material's icon font instead of LucideIcons, which is used consistently everywhere else in packages/courses.
  5. Hardcoded color instead of design token. download_progress_banner.dart:29 uses Colors.black.withValues(alpha: 0.1) for a shadow instead of design.colors.shadow, which exists and is even used in this same PR's pdf_lesson_detail_screen.dart:183.

🔵 SUGGESTION — Nice to have

  1. lesson_detail_orchestrator.dart's import was widened from flutter/widgets.dart to flutter/material.dart, but no Material API is actually used in the file — unnecessary widening of a neutral-UI screen's dependency surface.
  2. pdf_download_service.dart has leftover step-numbered comments (jumps from // 1. to // 3. to // 4. to // 6. to // 7., skipping 2 and 5) — minor readability nit from a refactor.

Verdict

CHANGES REQUESTED — the accessibility gaps and the spec/implementation mismatch on watermarking should be resolved before merge; the DB migration and Downloads-registry issues are functional risks worth addressing too.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a conditional PDF download flow based on an allow_download flag from the backend, applying a permanent watermark to downloaded files while ensuring in-app watermarks are always visible. Key changes include updating domain models, integrating the syncfusion_flutter_pdf package, implementing a background isolate watermarking pipeline in PdfDownloadService, and adding UI components like DownloadProgressBanner and AlreadyDownloadedSheet. The review feedback highlights several critical improvements: ensuring filename safety for non-Latin scripts, correcting Android storage permission handling for newer API levels, aligning the download UX in PdfLessonDetailScreen with the orchestrator, preventing potential crashes and resource leaks in the download service, and localizing hardcoded English strings in the new UI components.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread packages/courses/lib/utils/pdf_download_service.dart Outdated
Comment thread packages/courses/lib/utils/pdf_download_service.dart Outdated
Comment thread packages/courses/lib/screens/pdf_lesson_detail_screen.dart Outdated
Comment thread packages/courses/lib/utils/pdf_download_service.dart Outdated
Comment thread packages/courses/lib/utils/pdf_download_service.dart Outdated
Comment thread packages/courses/lib/widgets/lesson_detail/already_downloaded_sheet.dart Outdated
Comment thread packages/courses/lib/widgets/lesson_detail/download_progress_banner.dart Outdated
Comment thread packages/courses/lib/utils/pdf_download_service.dart Outdated
Comment thread packages/courses/lib/widgets/lesson_detail/download_progress_banner.dart Outdated
Comment thread packages/courses/lib/widgets/lesson_detail/already_downloaded_sheet.dart Outdated
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING — Hardcoded color instead of design token: Colors.black.withValues(alpha: 0.1) should be design.colors.shadow, which exists and is even used elsewhere in this very PR (pdf_lesson_detail_screen.dart:183).

Comment thread packages/core/lib/data/db/tables/lessons_table.dart
Comment thread packages/courses/lib/screens/pdf_lesson_detail_screen.dart Outdated
Comment thread packages/courses/lib/utils/pdf_download_service.dart Outdated
Comment thread packages/courses/lib/screens/lesson_detail_orchestrator.dart Outdated
@pugal66

pugal66 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

@claude review

@claude

claude Bot commented Jul 21, 2026

Copy link
Copy Markdown

Claude finished @pugal66's task in 3m 18s —— View job


Claude finished @pugal66's task in 6m 12s —

  • Read AGENTS.md, ai_context.md, architecture.md
  • Check relevant ADRs (0003 accessibility contract, 0005 user-state-in-core)
  • Diff origin/main...HEAD (36 files, +884/-203) and the "Address review comments" fix-up commit specifically
  • Verify fixes applied for previously flagged issues
  • Review for new issues
  • Post final review summary

PR Review Summary

Re-reviewed after the 1ecb1b8 "Address review comments" commit. Most prior CRITICAL/WARNING findings were genuinely fixed — nice turnaround.

✅ What's good / confirmed fixed since last review

  • Spec/implementation mismatch resolved: openspec/.../pdf-watermarking/spec.md now explicitly says the watermark is stamped "unconditionally", matching pdf_download_service.dart's actual behavior. Spec and code agree.
  • A11y fixed: download_progress_banner.dart's "View" tap target and the $progress% text now correctly use AppSemantics.button() / AppSemantics.progressValue(); already_downloaded_sheet.dart's close button now uses AppSemantics.button() instead of raw Semantics.
  • Icons fixed: Icons.closeLucideIcons.x, Icons.check_circleLucideIcons.checkCircle2.
  • Downloads registry integration added: PdfDownloadService.downloadAndWatermarkPdf now calls downloadsRepo.upsertDownload(...) at start/progress/completion and the orchestrator watches watchDownloadItemProvider, so PDFs now show up through the same DownloadsTable-backed flow as other content — the ad-hoc filesystem-only tracking is gone.
  • Dead code removed: pdf_lesson_detail_screen.dart deleted, no leftover references anywhere.
  • Resource leak fixed: _applyWatermarkToPdfSync now wraps in try { ... } finally { document.dispose(); }.
  • userProvider fetch wrapped in try/catch, gracefully falling back to 'Downloaded'.
  • Filename sanitization fixed: regex now strips only [\\/:*?"<>|] instead of all non-ASCII, so non-Latin lesson titles (Hindi/Tamil/Arabic/etc.) no longer collapse to 'lesson'.
  • Android storage permission now gated on sdkInt < 29 instead of requesting unconditionally.
  • lesson_detail_orchestrator.dart import reverted to flutter/widgets.dart.
  • Step-numbered comments cleaned up in pdf_download_service.dart.

🟡 WARNING — Should fix

  1. Localization is incomplete — two new ARB keys were added but never wired up. already_downloaded_sheet.dart:58,67 still hardcodes label: 'Open File' and label: 'Download Again', even though openFileAction / downloadAgainAction were added to app_en.arb (and translated in app_ar.arb) in this very commit. Should be l10n.openFileAction / l10n.downloadAgainAction.
  2. New hardcoded English string: lesson_detail_orchestrator.dart:82AppToast.show(context, message: 'Failed to download PDF: $e', ...) is not localized (and also surfaces a raw exception message to end users, which is a poor UX/security-hygiene practice — consider a generic localized string and logging e separately via debugPrint).
  3. Android permission denial still isn't handled. The sdkInt < 29 gating fix is correct, but if the user denies the (legacy) storage permission on older Android versions, the code proceeds anyway and will fail later with an unhandled FileSystemException when writing to public storage, rather than surfacing a clear error to the user.
  4. DB schema migration still not bumped. lessons_table.dart still adds allowDownload while AppDatabase.schemaVersion (app_database.dart:59) remains 1, and onUpgrade still only creates missing tables. Per your comment this is being deferred deliberately since the project is pre-release — noting it here so it isn't lost track of before this ships to any device with an existing local DB.

🔵 SUGGESTION — Nice to have

  1. download_progress_banner.dart:30 still uses Colors.black.withValues(alpha: 0.1) for the shadow instead of design.colors.shadow (flagged last round, not addressed — low priority given it's a subtle shadow color, but worth a follow-up for token consistency).
  2. The 'Download progress' label passed to AppSemantics.progressValue in download_progress_banner.dart:75 is also hardcoded English — screen readers will announce it in English regardless of locale. Same class of issue as feat(ui): implement LMS core primitives #1/feat(navigation-shell): implement unified routing architecture #2.

Verdict

CHANGES REQUESTED (minor) — the core CRITICAL issues from the previous round (spec mismatch, missing a11y semantics, dead-code duplicate flow, Downloads-registry bypass) are all resolved. What's left is a small, low-risk localization/error-handling cleanup — no functional or architectural blockers remain.

@pugal66
pugal66 force-pushed the feat/download-pdf-watermark branch 4 times, most recently from 4ffd529 to a1f11b7 Compare July 23, 2026 04:54
@pugal66

pugal66 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

@claude review

@claude

claude Bot commented Jul 23, 2026

Copy link
Copy Markdown

Claude finished @pugal66's task in 4m 43s —— View job


Claude finished @pugal66's task in 5m 6s

  • Read AGENTS.md, ai_context.md, architecture.md
  • Check relevant ADRs (0003 accessibility contract, 0005 user-state-in-core)
  • Diff origin/main...HEAD (single squashed commit, 39 files, +898/-29)
  • Verify fixes applied for previously flagged issues
  • Review for new issues (traced DownloadType.pdf through the full Downloads pipeline)
  • Post inline comment + final review summary

PR Review Summary

Re-reviewed the current state of the branch (all prior review rounds are now squashed into a single commit a1f11b7). Great turnaround on the earlier feedback — but tracing the new DownloadType.pdf value through the existing Downloads registry surfaced a real data-loss bug that wasn't caught before.

✅ What's good / confirmed fixed from earlier rounds

  • Spec/implementation match: openspec/.../pdf-watermarking/spec.md and design.md now both say the watermark is stamped unconditionally — matches pdf_download_service.dart.
  • Accessibility: download_progress_banner.dart's "View" target and already_downloaded_sheet.dart's close button both correctly use AppSemantics.button(); icons use LucideIcons throughout.
  • Localization: all new UI strings (alreadyDownloadedTitle/Message, openFileAction, downloadAgainAction, downloadCompleted, downloadingFile, viewAction) are wired via l10n.* and present across app_en/ar/ml/ta.arb with matching generated AppLocalizationsXx output — verified none are hand-edited (arb → generated diffs match 1:1).
  • Error handling: _startDownload now shows a generic localized toast (l10n.errorGenericMessage) instead of leaking the raw exception; userProvider fetch is wrapped in try/catch with a safe fallback to 'Downloaded'.
  • Resource safety: _applyWatermarkToPdfSync wraps document operations in try { ... } finally { document.dispose(); }.
  • Filename sanitization: regex now strips only [\\/:*?"<>|], so non-Latin lesson titles no longer collapse to 'lesson'.
  • Android permissions: storage permission is now gated on sdkInt < 29, and denial is handled (throws instead of silently proceeding to a FileSystemException later).
  • Dead code removed: pdf_lesson_detail_screen.dart is gone, no dangling references.
  • Clean shared constants: WatermarkParams (angle/font/opacity) is now shared between WatermarkOverlay and the PDF-stamping isolate, deduplicating what was previously two copies of the same values.
  • Data layer: allowDownload is threaded correctly through LessonDto → Lesson → providers → CourseRepository (Drift ↔ DTO ↔ domain), all through the Repository/Provider layers — no direct DB/DataSource access from widgets. app_database.g.dart diff for the new allow_download column matches the lessons_table.dart source change exactly (not hand-edited).

🔴 CRITICAL — Must fix before merge

  1. DownloadsRepository.synchronize() deletes every downloaded PDF's registry row on every Downloads-screen visit. (Inline comment posted on downloads_repository.dart:207.) synchronize() builds activeIds from only activeVideoIds and _db.select(...).where(typeIndex.equals(DownloadType.attachment.index)) — it has no branch for the new DownloadType.pdf this PR introduces. batch.deleteWhere(tbl.id.isNotIn(activeIds)) then deletes any row not in that set, including every PDF download (completed or in-progress). Since DownloadsScreen.initState() calls synchronize() on every visit, this means:

    • Downloaded PDFs disappear from the DB the moment the Downloads screen is opened.
    • LessonDetailOrchestrator._handleDownload depends on watchDownloadItemProvider reporting DownloadStatus.completed to show the "already downloaded" sheet — once the row is gone, the app silently re-downloads and re-watermarks a file that's already on disk, and the (1), (2), ... conflict-suffix logic in pdf_download_service.dart means duplicate files accumulate in public storage over time.
    • This directly undermines the Downloads-registry integration added to address the previous round's feedback — the write path (upsertDownload) works, but the sync/reconciliation path was never updated for the third DownloadType.
  2. downloads_screen.dart has no tab/list for DownloadType.pdf, so downloaded PDFs are invisible even when the registry row does survive. _activeTab only toggles between DownloadType.video and DownloadType.attachment (lines 37, 220-235), and activeItems is computed as _activeTab == DownloadType.video ? videos : attachments (line 60), where both videos/attachments come from byType() filtered to their own single type. There's no code path that ever surfaces a DownloadType.pdf item in this screen. (Not inline-commentable — downloads_screen.dart isn't touched by this PR's diff, but it's directly broken by the new enum value this PR adds.) Combined with feat(ui): implement LMS core primitives #1, downloaded PDFs are both undiscoverable in the UI and get purged from the DB on every screen visit.

🟡 WARNING — Carried forward, still open

  1. DB schema migration still not bumpedlessons_table.dart adds allowDownload but AppDatabase.schemaVersion remains 1, and onUpgrade only creates missing tables. Per your comment this is intentionally deferred since the project is pre-release — flagging again only so it's not lost before any release build ships with existing local DBs.

🔵 SUGGESTION — Nice to have (low priority, unchanged from prior rounds)

  1. download_progress_banner.dart:30 still uses Colors.black.withValues(alpha: 0.1) instead of design.colors.shadow for the shadow — flagged twice already, cosmetic only.
  2. The close button in already_downloaded_sheet.dart (bare Icon(LucideIcons.x, ...) with no padding/min size) has a tap target smaller than the WCAG 2.5.5 48×48dp minimum. Note the codebase's existing _HeaderButton pattern in core/lib/widgets/lesson_detail_shell.dart is also only 36×36dp (pre-existing, out of scope here), so this isn't a regression, just an opportunity to do slightly better.

Verdict

CHANGES REQUESTED — the localization/a11y/error-handling items from earlier rounds are all genuinely resolved. What's blocking now is new: the Downloads registry synchronize() logic needs a branch for DownloadType.pdf (or PDFs will keep getting silently deleted from the DB), and downloads_screen.dart needs a way to actually display them. The schema-migration item remains an acknowledged, deliberately deferred risk.
· Branch: feat/download-pdf-watermark

duration: Value(item.duration),
fileType: Value(item.fileType),
contentUrl: Value(item.contentUrl),
filePath: Value(item.filePath),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL — synchronize() (lines 161-213, unchanged by this PR but broken by it) deletes every downloaded PDF's registry row. activeIds is built only from activeVideoIds and activeAttachmentIds — the query at line 169 filters typeIndex.equals(DownloadType.attachment.index), so the new DownloadType.pdf rows introduced in this PR are never included. Then batch.deleteWhere(_db.downloadsTable, (tbl) => tbl.id.isNotIn(activeIds)) at line 186 deletes any row not in that set — which includes every PDF download, completed or in-progress.

DownloadsScreen.initState() calls synchronize() on every visit (downloads_screen.dart:43-45), so simply opening the Downloads tab wipes the DB record for any downloaded PDF. Concretely:

  • Downloaded PDFs vanish from the Downloads screen the moment it's opened (compounding the tab-display issue flagged separately).
  • LessonDetailOrchestrator._handleDownload relies on watchDownloadItemProvider(lesson.id) reporting DownloadStatus.completed to show the "already downloaded" sheet. Once the row is deleted, re-opening the lesson silently falls through to _startDownload, re-downloading and re-watermarking a PDF that's already on disk — and since the save-path logic appends (1), (2), ... on conflict, duplicate files accumulate in public storage over time.

synchronize() predates DownloadType.pdf; it needs a matching branch now that this PR introduces a third DownloadType.

Fix this →

@pugal66
pugal66 force-pushed the feat/download-pdf-watermark branch from a1f11b7 to c4f5e4a Compare July 24, 2026 11:18
@pugal66

pugal66 commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

@claude review

@claude

claude Bot commented Jul 24, 2026

Copy link
Copy Markdown

Claude finished @pugal66's task in 3m 47s —— View job


Claude finished @pugal66's task in 8m 4s

  • Read AGENTS.md, ai_context.md, architecture.md
  • Check relevant ADRs (0005 user-state-in-core)
  • Diff origin/main...HEAD (single commit, 48 files, +1424/-185)
  • Verify fixes applied for previously flagged issues
  • Review for new issues
  • Post inline comments + final summary

PR Review Summary

All prior-round history is squashed into one commit now. Verified the two CRITICAL bugs from the last review round are genuinely fixed — but found a new, closely-related bug in the tap-to-open path, plus a new architectural regression.

✅ What's good / confirmed fixed from earlier rounds

  • synchronize() no longer wipes PDF downloads: downloads_repository.dart:169-194 now builds activeIds from both DownloadType.attachment and DownloadType.pdf rows (verifying file existence on disk / via verifyAttachmentExists), so opening the Downloads screen no longer deletes completed PDF registry rows.
  • PDFs now show up in the Downloads screen: downloads_screen.dart:58-61 merges DownloadType.pdf into the "attachments" tab list and count, so downloaded PDFs are no longer invisible.
  • Spec/implementation match, accessibility semantics, localization, icons, resource cleanup (try/finally around PdfDocument), Android permission gating (sdkInt < 29 + denial handling via ensurePublicStoragePermission), and the dead pdf_lesson_detail_screen.dart removal all remain correctly resolved.
  • pdf_cache_service.dart (new) does atomic temp-file → rename promotion with proper cleanup on failure — solid pattern.
  • Generated files (app_database.g.dart, *.g.dart for providers) correctly reflect their source changes — no hand-edits detected.

🔴 CRITICAL — Must fix before merge

  1. Tapping a completed PDF download opens the video player instead of the file. (Inline comment on downloads_screen.dart:61.) _handleAction's DownloadStatus.completed branch only special-cases item.type == DownloadType.attachment before calling _openAttachment; for the new DownloadType.pdf it falls through to pushing OfflineVideoPlayerScreen(item: item). PdfDownloadService populates contentUrl/filePath on PDF items identically to attachments, so this is a straightforward oversight — but it means the primary "open my downloaded PDF from the Downloads screen" flow is broken. Same root cause as the tab-display/synchronize() bugs fixed elsewhere in this same commit (a new DownloadType added without updating every place that switched on the old two-value enum).

🟡 WARNING — Should fix

  1. New direct AppDatabase/Drift access from the courses package, bypassing userProvider. (Inline comment on pdf_download_service.dart:130.) Both pdf_download_service.dart:129-134 and pdf_viewer.dart:110-111 now do (await ref.read(appDatabaseProvider.future)).select(db.usersTable).getSingleOrNull() instead of ref.read(userProvider.future). Earlier review rounds specifically praised this codebase's use of userProvider here as correct ADR-0005 compliance ("domain packages access user state only via userProvider, never by reaching into the DB directly"); this version regresses that by querying core's internal UsersTableData table straight from courses.
  2. DB schema migration still not bumpedlessons_table.dart adds allowDownload, AppDatabase.schemaVersion remains 1. Carried forward from prior rounds; per your earlier comment this is intentionally deferred pre-release, flagging again only so it isn't lost before any release build ships with existing local DBs.

🔵 SUGGESTION — Nice to have (carried forward, unaddressed, low priority)

  1. download_progress_banner.dart:30 still uses Colors.black.withValues(alpha: 0.1) instead of design.colors.shadow — flagged in two prior rounds.
  2. already_downloaded_sheet.dart's close icon (bare Icon(LucideIcons.x, ...)) still has no explicit min tap-target sizing (WCAG 2.5.5 48×48dp) — matches an existing pre-PR pattern elsewhere in the codebase, so not a regression, just an opportunity.

Verdict

CHANGES REQUESTED — great progress on the previous round's synchronize()/tab-display bugs, but the same "new enum value, not every switch statement updated" class of bug resurfaced in the tap-to-open handler, and the userProvider bypass is a real architectural regression worth reverting before merge. Schema migration remains a knowingly-deferred, pre-release risk.
· Branch: feat/download-pdf-watermark

String watermarkText = fallbackWatermarkText;
try {
final db = await _ref.read(appDatabaseProvider.future);
final currentUser = await db.select(db.usersTable).getSingleOrNull();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING — Bypasses UserRepository/userProvider, reads the core package's Drift table directly. This queries db.usersTable straight from the courses package instead of going through userProvider (which was specifically praised in earlier review rounds for correctly following ADR 0005 — "single source of truth for the authenticated user's profile"). The same pattern also appears in packages/courses/lib/widgets/lesson_detail/pdf_viewer.dart:110-111.

This is a Data Layer Architecture violation (never access AppDatabase/Drift directly — always go through a Repository/Provider) and re-couples courses to core's internal table schema (UsersTableData), which ADR 0005 was written specifically to avoid. Please switch both call sites back to ref.read(userProvider.future) (or the equivalent UserRepository call).

[Fix this →](https://claude.ai/code?q=In+packages%2Fcourses%2Flib%2Futils%2Fpdf_download_service.dart+around+line+129-134+and+packages%2Fcourses%2Flib%2Fwidgets%2Flesson_detail%2Fpdf_viewer.dart+around+line+110-111%2C+the+code+reads+db.usersTable+directly+via+appDatabaseProvider+instead+of+using+userProvider+from+package%3Acore.+Per+ADR+0005+%28packages%2Fcore%2Fdocs%2Fdecisions%2F0005-user-state-in-core.md%29%2C+domain+packages+should+access+the+current+user+only+through+userProvider%2FUserRepository%2C+not+by+querying+the+Drift+AppDatabase+directly.+Replace+both+call+sites+with+ref.read%28userProvider.future%29+%28or+equivalent%29+wrapped+in+the+existing+try%2Fcatch+fallback+logic.&repo=testpress/cortex

final attachments = [
...downloads.byType(DownloadType.attachment),
...downloads.byType(DownloadType.pdf),
];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL — DownloadType.pdf is now shown in this tab, but tapping a completed PDF still opens the video player, not the file. _handleAction further down (around line 146-158, not touched by this PR's diff) only special-cases DownloadType.attachment in the completed branch:

case DownloadStatus.completed:
  if (item.type == DownloadType.attachment && item.contentUrl != null) {
    await _openAttachment(item);
  } else {
    // DownloadType.pdf falls through here
    Navigator.of(context).push(MaterialPageRoute(builder: (_) => OfflineVideoPlayerScreen(item: item)));
  }

PdfDownloadService sets both contentUrl and filePath on PDF DownloadItems exactly like attachments do (pdf_download_service.dart:106,180), so a completed PDF download should also route through _openAttachment. As written, tapping a downloaded PDF card here pushes OfflineVideoPlayerScreen with a PDF item instead of opening the file — this file wasn't touched by the diff at that line, but it's directly broken by the new DownloadType.pdf value this PR introduces, the same way the tab-count/synchronize() bugs were in the previous round.

Fix this →

String watermarkText = fallbackWatermarkText;
try {
final db = await _ref.read(appDatabaseProvider.future);
final currentUser = await db.select(db.usersTable).getSingleOrNull();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING — Bypasses UserRepository/userProvider, reads the core package's Drift table directly. This queries db.usersTable straight from the courses package instead of going through userProvider (which was specifically praised in earlier review rounds for correctly following ADR 0005 — "single source of truth for the authenticated user's profile"). The same pattern also appears in packages/courses/lib/widgets/lesson_detail/pdf_viewer.dart:110-111.

This is a Data Layer Architecture violation (rule: never access AppDatabase/Drift directly — always go through a Repository/Provider) and re-couples courses to core's internal table schema (UsersTableData), which ADR 0005 was written specifically to avoid. Please switch both call sites back to ref.read(userProvider.future) (or the equivalent UserRepository call).

[Fix this →](https://claude.ai/code?q=In+packages%2Fcourses%2Flib%2Futils%2Fpdf_download_service.dart+around+line+129-134+and+packages%2Fcourses%2Flib%2Fwidgets%2Flesson_detail%2Fpdf_viewer.dart+around+line+110-111%2C+the+code+reads+db.usersTable+directly+via+appDatabaseProvider+instead+of+using+userProvider+from+package%3Acore.+Per+ADR+0005+%28packages%2Fcore%2Fdocs%2Fdecisions%2F0005-user-state-in-core.md%29%2C+domain+packages+should+access+the+current+user+only+through+userProvider%2FUserRepository%2C+not+by+querying+the+Drift+AppDatabase+directly.+Replace+both+call+sites+with+ref.read%28userProvider.future%29+%28or+equivalent%29+wrapped+in+the+existing+try%2Fcatch+fallback+logic.&repo=testpress/cortex

Comment thread packages/core/lib/data/config/institute_settings.dart
Comment thread packages/core/lib/data/models/lesson_dto.dart
@pugal66
pugal66 force-pushed the feat/download-pdf-watermark branch 2 times, most recently from 388a466 to a851fc0 Compare July 25, 2026 10:04
@pugal66

pugal66 commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

@claude review

@claude

claude Bot commented Jul 25, 2026

Copy link
Copy Markdown

Claude finished @pugal66's task in 6m 18s —— View job


Claude finished @pugal66's task

  • Read AGENTS.md, ai_context.md, architecture.md
  • Check relevant ADRs (0005 user-state-in-core)
  • Diff origin/main...HEAD (single commit, 46 files, +1499/-183)
  • Verify fixes applied for previously flagged issues
  • Review the significant architectural refactor since last round
  • Post inline comments + final summary

PR Review Summary

Significant rework since the last round: PDF download/watermark logic moved from courses' pdf_download_service.dart into a new core/lib/data/services/pdf_downloader.dart, DownloadType.pdf was removed in favor of reusing DownloadType.attachment, and watermarking is now correctly conditional on the backend's per-lesson watermark_before_download flag (addressing syed-tp's comment). Good direction overall, but the refactor introduced new bugs and reverted a couple of previously-fixed ones.

✅ What's good / confirmed fixed from earlier rounds

  • Spec matches implementation: openspec/.../pdf-watermarking/spec.md now correctly documents the conditional stamping behavior keyed on watermark_before_download, matching lesson.watermarkBeforeDownloadapplyWatermark in lesson_detail_orchestrator.dart:120.
  • synchronize() correctly handles both attachments and PDFs: now checks file.filePath first, falling back to the URL-hash check — this specific method got the filePath-aware fix that other call sites (see below) didn't.
  • DownloadType.pdf display/tap bugs resolved by design: folding PDFs into DownloadType.attachment means they now share the existing tab and count logic in downloads_screen.dart — no more invisible/uncounted PDFs.
  • lesson_dto.dart: field order updated per syed-tp's comment; allowDownload/watermarkBeforeDownload are threaded cleanly through LessonDto → Lesson → providers → CourseRepository.
  • Accessibility, localization, and icon fixes from earlier rounds (download_progress_banner.dart, already_downloaded_sheet.dart) remain intact — not touched in this round's changes.

🔴 CRITICAL — Must fix before merge

  1. Tapping a completed PDF download in the Downloads screen shows "File not found" and deletes the registry entry — even though the file exists. downloads_screen.dart:159-166's _openAttachment (unchanged by this PR but broken by it) recomputes the path via downloader.getLocalPath(item.contentUrl!, StorageType.publicDownload), which hashes the URL (SHA-256) to build the filename. But PDFs are saved via PdfDownloader.downloadAndWatermark at $title.pdf (pdf_downloader.dart:66) — a completely different scheme — and the real path is already stored in item.filePath, which _openAttachment never reads. Result: the file lookup always misses for PDFs, showing a false "deleted" snackbar and calling downloadsProvider.notifier.delete(item), silently removing the completed download from the DB.
  2. "Delete all downloads" leaves watermarked PDFs (containing the user's name) orphaned on the device. DownloadsRepository.purgeAllDownloads() (downloads_repository.dart:341-355) builds its per-row DownloadItem without filePath: row.filePath — the only other DB→DownloadItem mapping in the file (watchAllDownloads, watchDownload) sets it. DownloadsService.deleteDownloadItem then falls through to the URL-hash path for PDFs (since filePath is null) and fails to find/delete the real file, so the DB record is purged but the identity-watermarked PDF physically remains in public storage.
  3. Filename sanitization was dropped when the download logic moved to core. (Inline comment on pdf_downloader.dart:66.) The old pdf_download_service.dart had a fix (from round 2) stripping [\\/:*?"<>|] from the lesson title before using it as a filename; the new PdfDownloader.downloadAndWatermark uses the raw title directly. A title containing / will misdirect or fail the write; other unsafe characters can break on some filesystems.
  4. pdf_viewer.dart's watermark fetch reverts the previously-fixed userProvider bypass. (Inline comment on pdf_viewer.dart:101-102.) The diff shows this literally changes await ref.read(userProvider.future) back to ref.read(appDatabaseProvider.future) + db.select(db.usersTable).getSingleOrNull() — the exact ADR-0005 violation flagged and fixed in the last two rounds is back.

🟡 WARNING — Should fix

  1. Silent watermark skip when username is unavailable. (Inline comment on downloads_repository.dart:185-189.) startWatermarkedPdfDownload leaves watermarkText as null if the cached user row/username is missing, and PdfDownloader only stamps when the text is non-empty — so a PDF that should be watermarked per institute policy can silently download unwatermarked with no fallback (round 2 fixed the equivalent case with a 'Downloaded' fallback; that's gone here).
  2. Duplicated user-lookup logic in downloads_repository.dart:187 — re-implements UserRepository.getCurrentProfile() instead of reusing it.
  3. Watermark visuals now diverge between in-app and downloaded PDF. (Inline comment on pdf_downloader.dart:50-51.) courses/utils/watermark_params.dart (angle -60°, size 70) drives the in-app overlay, but core/pdf_downloader.dart hardcodes different values (angle -45°, size 60) for the actual stamped file — the shared-constants approach this PR was previously praised for no longer applies since the stamping logic lives in core, which can't import courses.
  4. enableCoursePdfWatermark field kept but now fully dead code. syed-tp asked for it to drive watermark logic; pugal66 replied "we are currently using watermark_before_download... so removing this" — but the commit only renamed the JSON key/field (enableCoursePdfWatermarkingenableCoursePdfWatermark), it's still defined in institute_settings.dart and referenced nowhere else in the codebase.
  5. DB schema migration still not bumpedlessons_table.dart now has two new columns (allow_download, watermark_before_download), AppDatabase.schemaVersion remains 1. Carried forward from every prior round; per your earlier comment this is intentionally deferred pre-release.

Verdict

CHANGES REQUESTED — the conditional-watermark and spec-alignment work from syed-tp's feedback is done correctly, but moving the download pipeline into core introduced two file-path bugs that make "open downloaded PDF" and "delete all downloads" behave incorrectly, and reverted the userProvider fix in pdf_viewer.dart. Recommend fixing items 1-4 before merge; 5-8 are lower risk but worth addressing since they touch the feature's core purpose (traceable watermarking) and code-quality expectations from earlier rounds. Schema migration remains a knowingly-deferred, pre-release risk.

Branch: feat/download-pdf-watermark

Comment thread packages/courses/lib/widgets/lesson_detail/pdf_viewer.dart Outdated
Comment thread packages/core/lib/data/services/pdf_downloader.dart Outdated
Comment thread packages/core/lib/data/repositories/downloads_repository.dart
Comment thread packages/core/lib/data/services/pdf_downloader.dart Outdated
@pugal66
pugal66 force-pushed the feat/download-pdf-watermark branch 2 times, most recently from 910dcf5 to ba5cb9e Compare July 25, 2026 11:19
@pugal66

pugal66 commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

@claude review

@claude

claude Bot commented Jul 25, 2026

Copy link
Copy Markdown

Claude finished @pugal66's task in 3m 42s —— View job


Claude finished @pugal66's task

  • Read AGENTS.md, ai_context.md, architecture.md
  • Check relevant ADRs (0005 user-state-in-core)
  • Diff origin/main...HEAD (single commit, 47 files, +1519/-187)
  • Verify fixes applied for previously flagged issues
  • Review for new issues
  • Post inline comments + final summary

PR Review Summary

Re-reviewed after the latest "Address review comments" commit (ba5cb9e), which addressed syed-tp's feedback plus the four CRITICAL/WARNING items from the last round. Great turnaround — all previously-flagged CRITICAL bugs are now genuinely fixed. Found two smaller, related issues in the newly-refactored _handleDownload logic in lesson_detail_orchestrator.dart.

✅ What's good / confirmed fixed from earlier rounds

  • pdf_viewer.dart ADR-0005 regression fixed: back to ref.read(userProvider.future), no more direct usersTable access.
  • Filename sanitization restored in PdfDownloader: pdf_downloader.dart:66 now strips [\\/:*?"<>|] before building the save path, matching DownloadsService.getExistingPdfSize.
  • Watermark rendering unified: WatermarkParams moved to packages/core/lib/utils/watermark_params.dart and is now imported by both WatermarkOverlay (in-app) and PdfDownloader (download pipeline) — angle/size/opacity match again.
  • Silent watermark skip fixed: downloads_repository.dart:188-195 now falls back to 'Downloaded' when the cached username is null/empty, and reuses UserRepository.getCurrentProfile() instead of re-querying usersTable directly.
  • enableCoursePdfWatermarking dead field removed from institute_settings.dart per syed-tp's request — no lingering references anywhere.
  • lesson_dto.dart field order fixed per syed-tp's comment (exam moved back to the end of the constructor/mergeWith).
  • Tap-to-open / delete-all bugs resolved by design: DownloadType.pdf was folded into DownloadType.attachment, so PDFs share downloads_screen.dart's existing tab/count/_openAttachment logic — _openAttachment now checks item.filePath first (falling back to URL-hash lookup), which correctly resolves title-based PDF paths. purgeAllDownloads now also carries filePath through, so deleteDownloadItem can find and delete watermarked PDFs instead of orphaning them.
  • Spec/implementation match: openspec/.../pdf-watermarking/spec.md and design.md accurately describe the conditional watermark_before_download behavior.

🟡 WARNING — Should fix

  1. Filename sanitization mismatch reintroduces the duplicate-download risk, in a new spot. (Inline comment on lesson_detail_orchestrator.dart:79.) _handleDownload's "already downloaded" probe builds canonicalPath from the raw lesson.title, while the actual file is saved via a sanitized title (PdfDownloader/DownloadsService.getExistingPdfSize). For any title containing \/:*?"<>|, this check always misses, causing silent re-download/re-watermarking of a file that already exists on disk — the same class of bug fixed twice already elsewhere in this PR, now present via a third, unsynchronized copy of the path-construction logic.
  2. "Open File" can silently no-op for orphaned-but-present files. (Inline comment on lesson_detail_orchestrator.dart:330.) The "already downloaded" sheet opens whenever canonicalFileExists is true, but onOpenFile/onView only work off downloadItem?.filePath, which is null when there's no matching DB row (e.g. the registry entry was purged while the physical file survived). Tapping "Open File" in that case does nothing, with no error shown.
  3. DB schema migration still not bumpedlessons_table.dart now has two new columns (allow_download, watermark_before_download), AppDatabase.schemaVersion remains 1. Carried forward from every prior round; per your earlier comment this is intentionally deferred pre-release. Note design.md's Decisions section says "Schema v2" — worth a quick edit to keep the doc consistent with the deferred-migration decision.

🔵 SUGGESTION — Nice to have (low priority)

  1. download_progress_banner.dart:30 still uses Colors.black.withValues(alpha: 0.1) instead of design.colors.shadow — flagged in three prior rounds, cosmetic only.
  2. downloadAgainAction (added to all four ARB files this round) is now dead — already_downloaded_sheet.dart only renders an "Open File" button, no "Download Again" action references the key anywhere.
  3. watermark_params.dart's doc comment still references the now-deleted PdfDownloadService — minor staleness from the core refactor.

Verdict

CHANGES REQUESTED (minor) — all CRITICAL issues from every prior round are resolved; what's left is a small, self-inflicted filename-sanitization duplication in the newly-added _handleDownload orchestrator logic (items 1-2) and the long-acknowledged, deliberately-deferred schema migration (item 3). No blocking architectural or spec-compliance issues remain.

Branch: feat/download-pdf-watermark

final fileDownloader = ref.read(fileDownloaderProvider);
final pubDir =
await fileDownloader.getDirectory(StorageType.publicDownload);
final canonicalPath = '${pubDir.path}/${lesson.title}.pdf';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING — Filename sanitization mismatch causes false "not downloaded" state. canonicalPath is built from the raw lesson.title with no sanitization, but the actual save path (computed in PdfDownloader.downloadAndWatermark, pdf_downloader.dart:66 and mirrored in DownloadsService.getExistingPdfSize, downloads_service.dart:152) strips [\\/:*?"<>|] first:

var safeTitle = title.replaceAll(RegExp(r'[\\/:*?"<>|]'), '_').trim();
if (safeTitle.isEmpty) safeTitle = 'lesson';
final path = '${dir.path}/$safeTitle.pdf';

For any lesson title containing one of those characters, this "already downloaded" probe checks a path that was never actually written to, so canonicalFileExists is always false even when the file exists — the app will silently re-download and re-watermark a PDF that's already on disk (the exact duplicate-download scenario flagged in earlier review rounds, now reintroduced via a third, unsynchronized copy of the path-construction logic). Consider extracting the sanitization into a shared helper (e.g. on PdfDownloader) and reusing it here instead of re-deriving the path independently.

Fix this →

onClose: () =>
setState(() => _isAlreadyDownloadedSheetOpen = false),
onOpenFile: () {
if (downloadedFilePath != null) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING — "Open File" can silently no-op when the file exists but the DB record doesn't. _handleDownload (line 79) opens this sheet whenever canonicalFileExists is true, regardless of whether there's a matching DownloadItem in the DB (e.g. the registry row was purged/cleared while the physical file survived). But downloadedFilePath here comes only from downloadItem?.filePath (line 220), which is null when no DB record exists. In that case tapping "Open File" does nothing — no error, no fallback. Consider passing canonicalPath through as a fallback when downloadItem is absent, or re-registering the orphaned file in the DB when it's detected in _handleDownload.

@pugal66
pugal66 force-pushed the feat/download-pdf-watermark branch from ba5cb9e to dbb22e3 Compare July 27, 2026 10:27
@pugal66
pugal66 force-pushed the feat/download-pdf-watermark branch from dbb22e3 to d3f6b50 Compare July 27, 2026 10:56
@pugal66

pugal66 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

@claude review

@claude

claude Bot commented Jul 27, 2026

Copy link
Copy Markdown

Claude finished @pugal66's task in 3m 50s —— View job


Claude finished @pugal66's task

  • Read AGENTS.md, ai_context.md, architecture.md
  • Check relevant ADRs (0005 user-state-in-core)
  • Diff origin/main...HEAD (single commit d3f6b50, 50 files, +1413/-209)
  • Verify fixes applied for previously flagged issues
  • Review the new architecture (progress-banner/already-downloaded-sheet UI removed, PDF downloads folded into DownloadType.attachment, new PdfCacheService for in-app viewing)
  • Post final summary

PR Review Summary

Substantial rework since the last round: the download_progress_banner/already_downloaded_sheet widgets and their associated "already downloaded" logic in the orchestrator were removed entirely, replaced by a simple AppToast.show(... 'Download started') + relying on the Downloads screen. PDF downloads now share DownloadType.attachment (no more separate enum value), and a new PdfCacheService handles in-app PDF viewing/caching independently from the watermark-and-download-to-public-storage pipeline. This resolved several previously-flagged issues but introduced a new critical regression.

✅ What's good / confirmed fixed from earlier rounds

  • Spec matches implementation: openspec/.../pdf-watermarking/spec.md correctly documents the conditional watermark_before_download behavior; PdfDownloader.downloadAndWatermark (pdf_downloader.dart:23-103) matches it exactly.
  • userProvider used correctly, no regression this round: pdf_viewer.dart:105 (AppPdfViewer._fetchWatermark) uses ref.read(userProvider.future); downloads_repository.dart:211 uses _userRepo.getCurrentProfile() — no direct usersTable/appDatabaseProvider access from courses or ad-hoc DB queries in core's repository layer.
  • Filename sanitization: PdfDownloader.safeTitle() (pdf_downloader.dart:16-19) is now the single, shared source of truth, reused by DownloadsService.getExistingPdfSize/deleteExistingPdf — no more triplicated/unsynchronized sanitization logic.
  • Watermark rendering unified: WatermarkParams (packages/core/lib/utils/watermark_params.dart) is now imported by both WatermarkOverlay (in-app, watermark_overlay.dart:27,31) and PdfDownloader (stamped file) — angle/size/opacity match.
  • enableCoursePdfWatermarking fully removed from institute_settings.dart, no lingering references anywhere.
  • Resource cleanup: _applyWatermarkToPdfSync still wraps in try { ... } finally { document.dispose(); }.
  • lesson_dto.dart field order and mergeWith handling for allowDownload/watermarkBeforeDownload look correct and consistent with Lesson/CourseRepository/chapter_detail_provider.dart.
  • Generated files verified: app_database.g.dart correctly reflects the new allow_download, watermark_before_download, and isWatermarked columns — no hand-edits detected.
  • Nice touch: lesson_detail_provider.dart now prefetches the PDF into PdfCacheService as soon as lesson details load, so opening a PDF lesson feels instant.

🔴 CRITICAL — Must fix before merge

  1. Downloaded PDFs are unconditionally filtered out of the Downloads screen — the exact "invisible download" bug from earlier rounds is back, via a new mechanism. downloads_provider.dart:17-22:

    Stream<List<DownloadItem>> build() async* {
      final repo = await ref.watch(downloadsRepositoryProvider.future);
      yield* repo.watchAllDownloads().map(
            (items) => items.where((item) => item.fileType != 'pdf').toList(),
          );
    }

    downloads_screen.dart renders exclusively from this filtered stream. Since startPdfLessonDownload (downloads_provider.dart:38-62) always sets fileType: 'pdf', every watermarked PDF download is excluded from both the Videos and Attachments tabs — completed downloads never appear, can't be reopened from the Downloads screen, and can't be deleted from the app (only by browsing the file manager directly). Given how many rounds were spent making sure PDF downloads surface correctly in this screen, this filter looks like debug/test leftover rather than an intentional decision — nothing in design.md/proposal.md mentions hiding PDFs from Downloads. If it's intentional, the reasoning should be documented in the spec; otherwise this line should be removed.

    Fix this →

🟡 WARNING — Should fix

  1. No completion/failure feedback for PDF downloads anymore. With the progress banner and already-downloaded sheet removed, _startDownload (lesson_detail_orchestrator.dart:63-81) only shows a toast on tap ("Download started") and on failure; there's no success toast, no progress indicator, and (per issue feat(ui): implement LMS core primitives #1) no way to see the download in the Downloads screen either. Combined, a user who taps "Download" gets zero confirmation the operation ever finished. Consider at least a completion toast, or ensure issue feat(ui): implement LMS core primitives #1 is fixed so the Downloads screen serves as the status source of truth.
  2. DownloadsService.getExistingPdfSize() and deleteExistingPdf() are dead code (downloads_service.dart:147-171) — defined but never called. startWatermarkedPdfDownload (downloads_repository.dart:200-248) has no equivalent to startAttachmentDownload's existing-file check (downloads_repository.dart:154-164), so every tap re-downloads and re-watermarks from scratch. design.md's Decisions section explicitly accepts this ("Multiple clicks will download duplicates sequentially") — if that's still the intent, remove the two unused methods; if not, wire getExistingPdfSize into the download-start flow.
  3. DB schema migration still not bumpedlessons_table.dart and downloads_table.dart add four new columns total (allow_download, watermark_before_download, isWatermarked) across this PR's life, AppDatabase.schemaVersion (app_database.dart:59) remains 1. Carried forward from every prior round; per your earlier comment this is intentionally deferred pre-release — flagging again only so it isn't lost. design.md's Decisions section still says "Schema v2," which is inconsistent with the deferred-migration decision — worth a quick doc edit.

🔵 SUGGESTION — Nice to have

  1. watermark_params.dart's doc comment still references the now-deleted PdfDownloadService (should say PdfDownloader) — stale from the earlier core refactor, flagged last round too.

Verdict

CHANGES REQUESTED — the architectural simplification (folding PDFs into DownloadType.attachment, removing the divergent progress-banner/sheet UI) is a reasonable direction and fixed several previously-flagged bugs cleanly. But the new fileType != 'pdf' filter in downloads_provider.dart reintroduces the core "downloaded PDFs are invisible" problem this PR has already fixed twice before — recommend removing/fixing that one line before merge. The dead-code existing-file check and schema migration are lower-risk carryovers.
· Branch: feat/download-pdf-watermark

Comment thread packages/courses/lib/providers/downloads_provider.dart Outdated
@pugal66

pugal66 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

@claude review

@claude

claude Bot commented Jul 27, 2026

Copy link
Copy Markdown

Claude finished @pugal66's task in 1m 40s —— View job


Claude finished @pugal66's task

  • Read AGENTS.md, ai_context.md, architecture.md
  • Check relevant ADR (0005 user-state-in-core)
  • Diff origin/main...HEAD — focused on latest commit bf0183b ("Address review comments", 6 files, +13/-32)
  • Verify the fileType: 'lesson_pdf' fix and spec updates from the last round
  • Review for new issues
  • Post final summary

PR Review Summary

Reviewed the latest commit (bf0183b), which responds to the previous round's CRITICAL finding about PDFs being hidden from the Downloads screen.

✅ What's good / confirmed resolved this round

  • The "invisible PDF downloads" filter is no longer a bug — it's now a documented decision. fileType was renamed from 'pdf' to 'lesson_pdf' (downloads_provider.dart:21,54), and openspec/.../design.md, proposal.md, and specs/pdf-downloads/spec.md were all updated with an explicit new scenario: "the system SHALL NOT display the PDF lesson in the [Downloads] list... PDF Lessons are accessed and managed via the course syllabus." Spec and implementation now agree — this resolves last round's CRITICAL as a legitimate product decision rather than a bug.
  • Dead code removed: DownloadsService.getExistingPdfSize() / deleteExistingPdf() (flagged last round as unused) are deleted; verified no dangling references to them or to PdfDownloader.safeTitle elsewhere.
  • Completion feedback added: _startDownload (lesson_detail_orchestrator.dart:71-74) now shows a toast on success, not just on failure — addresses last round's "zero confirmation the operation ever finished" warning.

🟡 WARNING — Should fix

  1. New hardcoded English string, duplicating an existing unused l10n key. lesson_detail_orchestrator.dart:73 adds AppToast.show(context, message: 'Download completed successfully') — hardcoded, not localized. This is especially avoidable here: this very PR added L10n.of(context).downloadCompleted ("Download completed") to all four ARB files back in an earlier round, and it has sat unused ever since. The new toast should use it (and the pre-existing 'Download started' toast at line 67 has the same issue — consider adding/using a downloadStarted key too while touching this code).

    Fix this →

  2. DB schema migration still not bumped — carried forward from every prior round; per your earlier comment this is intentionally deferred pre-release. design.md still says "Schema v2" in its Decisions section (design.md:20), which is inconsistent with the deferred-migration decision — worth a quick doc edit whenever convenient.

🔵 SUGGESTION — Nice to have (low priority, carried forward, unaddressed)

  1. watermark_params.dart's doc comment still references the deleted PdfDownloadService instead of PdfDownloader — flagged in the last two rounds, purely cosmetic.
  2. downloadAgainAction (in all four ARB files) remains dead code — no button in the current UI renders it since already_downloaded_sheet.dart was removed.

Verdict

CHANGES REQUESTED (minor) — the core architectural question from last round (PDFs excluded from Downloads screen) is now properly resolved via an explicit, spec-documented product decision rather than an accidental filter. What's left is a small, self-inflicted localization gap in the exact toast just added this round, plus long-acknowledged low-risk carryovers (schema migration, two stale doc/dead-code nits). No blocking architectural, spec-compliance, or data-integrity issues remain.

Branch: feat/download-pdf-watermark

@pugal66
pugal66 force-pushed the feat/download-pdf-watermark branch from bf0183b to dcea9d1 Compare July 27, 2026 12:12
@pugal66

pugal66 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

@claude review

@claude

claude Bot commented Jul 27, 2026

Copy link
Copy Markdown

Claude finished @pugal66's task in 1m 34s —— View job


Claude finished @pugal66's task

  • Read AGENTS.md, ai_context.md, architecture.md
  • Check relevant ADR (0005 user-state-in-core)
  • Diff origin/main...HEAD — focused on the latest commit (dcea9d1, "Address review comments", 15 files, +36/-33)
  • Verify the localization fix from the last round
  • Review for new issues
  • Post final summary

PR Review Summary

Reviewed the latest commit (dcea9d1), which responds to the last round's WARNING about hardcoded toast strings.

✅ What's good / confirmed resolved this round

  • Localization gap closed. lesson_detail_orchestrator.dart:67,73 now use L10n.of(context).downloadStarted and L10n.of(context).downloadCompleted instead of hardcoded English strings. The new downloadStarted key ("Download started") was correctly added to all four ARB files (app_en/ar/ml/ta.arb) with proper translations, and the corresponding generated AppLocalizationsXx.downloadStarted getters are present and match — no hand-edited generated files.
  • OpenSpec docs kept in sync: design.md, proposal.md, and specs/pdf-downloads/spec.md were updated with explicit wording confirming PDF lessons are intentionally excluded from the in-app Downloads screen — this documents the product decision from last round's discussion rather than leaving it implicit.

🟡 WARNING — Carried forward, unchanged

  1. DB schema migration still not bumpedlessons_table.dart/downloads_table.dart add several new columns, AppDatabase.schemaVersion remains 1. This has been flagged in every round; per your earlier comment it's intentionally deferred pre-release, noting again only so it isn't lost before any release build ships with existing local DBs.

🔵 SUGGESTION — Nice to have (low priority, carried forward, unaddressed)

  1. watermark_params.dart's doc comment still references the deleted PdfDownloadService instead of PdfDownloader (cosmetic).
  2. downloadAgainAction (all four ARB files) remains dead code — no button in the current UI renders it.

Verdict

APPROVED — the last remaining actionable item (hardcoded download toast strings) is fixed cleanly and correctly across all locales. What's left is the long-acknowledged, deliberately-deferred schema migration and two purely cosmetic nits carried over from earlier rounds. No blocking architectural, spec-compliance, accessibility, or data-integrity issues remain.
· Branch: feat/download-pdf-watermark

@pugal66
pugal66 merged commit 0c95e57 into main Jul 27, 2026
7 checks passed
@pugal66
pugal66 deleted the feat/download-pdf-watermark branch July 27, 2026 12:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants