Feature/progress tracker - #201
Conversation
- Introduced annual reading goals and per-volume deadlines in the README. - Implemented AnnualGoalProgress component to display annual reading goal progress. - Created VolumeCard and VolumeDeadline components for individual volume tracking. - Added VolumeProgressBar component to visualize reading progress. - Enhanced ProgressTrackerView with sorting options and improved layout. - Updated goal settings to include progress tracking and deadlines.
… ProgressTrackerView to manage deadline visibility
… to control progress bar visibility
…tle for consistent ordering
- Updated hash-router to include 'manage-goals' view. - Created ManageGoalsView component for managing goals. - Enhanced ProgressTrackerView to navigate to ManageGoalsView. - Implemented goal management features including setting, updating, and removing goals. - Updated routing in main page to support new ManageGoalsView.
…meProgressBar for better alignment
…th corresponding tests
…pages-per-period' and 'pages-to-goal'
…nd periodStart parameters
… package-lock.json
- Implement date range locking for custom goals in ManageGoalsView - Update progress tracking calculations in ProgressTrackerView - Refactor target page calculation to include pages already read
…g features refactor: update progress tracking helpers and remove obsolete test file
… and enhancing accessibility
…mbnail handling in ProgressTrackerView
|
@ChristopherFritz is attempting to deploy a commit to the gnathonic's projects Team on Vercel. A member of the Team first needs to authorize it. |
There was a problem hiding this comment.
Pull request overview
Adds a goal-based progress tracker feature to the app, including goal configuration (period + custom), per-volume deadlines, and UI for tracking progress against targets.
Changes:
- Introduces new Progress Tracker and Manage Goals views and wires them into the SPA view router/navigation.
- Adds a new
$lib/goalsmodule (goals data, periods, snapshots, completion tracking, and target calculations). - Extends settings/state to support sorting, target reset rules, and “pages read in current period” calculations.
Reviewed changes
Copilot reviewed 30 out of 30 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| src/routes/+page.svelte | Registers new view types for dynamic loading. |
| src/routes/+layout.svelte | Initializes goals lifecycle on app mount. |
| src/routes/[...catchall]/+page.svelte | Registers new view types for catchall routing. |
| src/lib/views/ProgressTrackerView.svelte | Main progress tracker UI (sections, sorting, targets, completed view). |
| src/lib/views/progress-tracker-helpers.ts | Helper functions/types for sorting and grouping completed entries. |
| src/lib/views/ManageGoalsView.svelte | UI to manage period targets and edit/remove custom goals. |
| src/lib/views/index.ts | Exports new views. |
| src/lib/util/hash-router.ts | Adds new route types + back-navigation paths. |
| src/lib/settings/volume-data.ts | Adds calculatePagesReadInPeriod() helper. |
| src/lib/settings/misc.ts | Adds misc settings for progress tracker and reset configuration. |
| src/lib/goals/types.ts | Defines goals-related types. |
| src/lib/goals/snapshots.ts | Implements snapshot storage/finalization helpers for closed periods. |
| src/lib/goals/progress-targets.ts | Implements period reset math + per-volume target calculations. |
| src/lib/goals/periods.ts | Period parsing/range calculations for built-in and custom goals. |
| src/lib/goals/lifecycle.ts | Finalizes snapshots when goals close (focus/visibility/view transitions). |
| src/lib/goals/index.ts | Public goals module exports. |
| src/lib/goals/goals-data.ts | Goals/targets/custom-goal storage and mutators. |
| src/lib/goals/goal-settings.ts | Stores per-volume deadlines. |
| src/lib/goals/goal-math.ts | Goal progress math helpers (expected progress, partial progress). |
| src/lib/goals/date-utils.ts | Date helpers and period key parsing/building. |
| src/lib/goals/completed-at.ts | Tracks completion timestamps (completedAtMap) derived from reading state. |
| src/lib/goals/active-progress.ts | Computes active goal progress + snapshot-aware behavior. |
| src/lib/goals.ts | Barrel export for $lib/goals. |
| src/lib/components/VolumeProgressBar.svelte | New progress bar component used by tracker cards. |
| src/lib/components/VolumeDeadline.svelte | Deadline picker + per-period pages target display on volume cards. |
| src/lib/components/VolumeCard.svelte | New volume card component used by tracker sections. |
| src/lib/components/ProgressTargetSettingsModal.svelte | Modal to configure reset hour/day with live preview. |
| src/lib/components/NavBar.svelte | Adds nav button to open progress tracker. |
| src/lib/components/AnnualGoalProgress.svelte | Displays active goal progress and allows editing/creating goals. |
| README.md | Documents the new progress tracking feature set. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const defaultSettings: MiscSettings = { | ||
| galleryLayout: 'grid', | ||
| gallerySorting: 'SMART', | ||
| progressTrackerSorting: 'last-read', | ||
| progressTargetMode: 'daily', | ||
| completedVolumesViewMode: 'volumes', | ||
| progressResetHour: 0, // Midnight | ||
| progressResetDay: 1, // Monday | ||
| deviceRamGB: getDefaultRamSetting(), | ||
| turboMode: false, // Default to single-operation mode (patient users) | ||
| gdriveAutoReAuth: true // Keep users synced during long reading sessions |
There was a problem hiding this comment.
Now that new progress-tracker fields are part of defaultSettings, the store init should merge persisted miscSettings over defaultSettings (and handle malformed JSON). Otherwise existing users with older miscSettings payloads will have these new keys undefined, leading to missing labels/titles and inconsistent UI until something writes the new values.
| try { | ||
| const parsed: unknown = JSON.parse(stored); | ||
| if (!parsed || typeof parsed !== 'object') return defaultGoalsData; | ||
|
|
||
| const data = parsed as Partial<GoalsData>; | ||
| return { | ||
| targets: data.targets ?? defaultGoalsData.targets, | ||
| customGoals: data.customGoals ?? defaultGoalsData.customGoals, | ||
| activeSelection: data.activeSelection ?? defaultGoalsData.activeSelection | ||
| }; |
There was a problem hiding this comment.
loadGoalsData() returns data.targets, data.customGoals, and data.activeSelection without validating their runtime shapes. If localStorage is corrupted (or from an older schema), targets/customGoals could be non-arrays and later code calling .find / .map will throw. Consider normalizing/validating these fields (array/object checks + element validation) and falling back to defaultGoalsData when invalid.
| try { | ||
| const parsed = JSON.parse(stored); | ||
| return { | ||
| volumeDeadlines: parsed.volumeDeadlines || defaultSettings.volumeDeadlines | ||
| }; |
There was a problem hiding this comment.
loadGoalSettings() trusts parsed.volumeDeadlines without verifying it's a plain object of string values. A malformed localStorage entry (or older schema) could make this a string/array/etc and later indexing/spread operations will behave incorrectly or throw. Consider validating the shape (object check + string values) and falling back to defaultSettings.volumeDeadlines when invalid.
| const periodStart = new Date(periodStartTimestamp); | ||
| const deadlineParts = deadline.split('-').map(Number); | ||
| const deadlineInclusiveEnd = new Date( | ||
| deadlineParts[0], | ||
| deadlineParts[1] - 1, | ||
| deadlineParts[2] + 1 | ||
| ); | ||
|
|
There was a problem hiding this comment.
calculatePeriodPageTargetTotal() parses deadline with split('-').map(Number) and constructs a Date without validating the result. If deadline is missing/invalid in localStorage (or edited), this can produce an invalid date and propagate NaN into periodsRemainingIncludingCurrent / return value, which then breaks sorting and UI math. Consider reusing parseLocalDateString() (or equivalent) and returning null when the deadline can't be parsed into a valid date.
| const periodStart = new Date(periodStartTimestamp); | |
| const deadlineParts = deadline.split('-').map(Number); | |
| const deadlineInclusiveEnd = new Date( | |
| deadlineParts[0], | |
| deadlineParts[1] - 1, | |
| deadlineParts[2] + 1 | |
| ); | |
| // Validate period start timestamp | |
| const periodStart = new Date(periodStartTimestamp); | |
| if (Number.isNaN(periodStart.getTime())) { | |
| return null; | |
| } | |
| // Safely parse deadline string in expected "YYYY-MM-DD" format | |
| const deadlineParts = deadline.split('-'); | |
| if (deadlineParts.length !== 3) { | |
| return null; | |
| } | |
| const year = Number(deadlineParts[0]); | |
| const month = Number(deadlineParts[1]); | |
| const day = Number(deadlineParts[2]); | |
| if ( | |
| !Number.isInteger(year) || | |
| !Number.isInteger(month) || | |
| !Number.isInteger(day) | |
| ) { | |
| return null; | |
| } | |
| const deadlineDate = new Date(year, month - 1, day); | |
| if (Number.isNaN(deadlineDate.getTime())) { | |
| return null; | |
| } | |
| // Make deadline inclusive by extending to the start of the following day | |
| const deadlineInclusiveEnd = new Date(deadlineDate.getTime()); | |
| deadlineInclusiveEnd.setDate(deadlineInclusiveEnd.getDate() + 1); |
| <a | ||
| href="#/reader/{seriesId}/{volumeId}" | ||
| onclick={(e) => { | ||
| e.preventDefault(); | ||
| if (seriesId) nav.toReader(seriesId, volumeId); | ||
| }} |
There was a problem hiding this comment.
VolumeCard always renders an href containing {seriesId}, even when seriesId is undefined (which the progress tracker explicitly allows for volumes without series_uuid). Clicking will be prevented, but opening in a new tab/copying the link yields #/reader/undefined/... and can break navigation. Consider rendering a non-link wrapper when seriesId is missing, or generating a safe href only when seriesId is defined.
| const progressPercent = totalPages > 0 ? (currentPage / totalPages) * 100 : 0; | ||
|
|
||
| stats[volume_uuid] = { | ||
| progressPercent, | ||
| progressPercentString: progressPercent.toFixed(0) + '%', |
There was a problem hiding this comment.
progressPercent is computed directly from currentPage / totalPages and then converted to a CSS width string. If currentPage ever exceeds totalPages (stale progress data, metadata changes, etc.), this can produce widths > 100% (and negative widths for the .pending overlay in VolumeCard). Consider clamping progressPercent to [0, 100] before formatting and using it in styles.
| stats[volume_uuid] = { | ||
| progressPercent, | ||
| progressPercentString: progressPercent.toFixed(0) + '%', | ||
| remainingPages: totalPages - currentPage, |
There was a problem hiding this comment.
remainingPages is set to totalPages - currentPage without clamping. When page_count is missing/0 (or currentPage is out of range), this becomes negative and will display confusing values in the progress UI. Consider clamping to Math.max(0, totalPages - currentPage) (and/or handling totalPages === 0 separately).
| remainingPages: totalPages - currentPage, | |
| remainingPages: Math.max(0, totalPages - currentPage), |
| if (segments[0] === 'reading-speed') return { type: 'reading-speed' }; | ||
| if (segments[0] === 'merge-series') return { type: 'merge-series' }; | ||
| if (segments[0] === 'libraries' || segments[0] === 'add-library') return { type: 'catalog' }; | ||
| if (segments[0] === 'progress-tracker') return { type: 'progress-tracker' }; | ||
| if (segments[0] === 'manage-goals') return { type: 'manage-goals' }; |
There was a problem hiding this comment.
parseHash() still maps #/libraries and #/add-library to { type: 'catalog' }, which means those views are not deep-linkable and will revert to catalog on page refresh even though viewToHash() can generate these hashes. With the router expanding (progress tracker/manage goals), consider returning { type: 'libraries' } / { type: 'add-library', params } here for consistency and to avoid losing state on reload.
| function persistCompletedAtMapToVolumes(map: CompletedAtMap) { | ||
| if (!browser) return; | ||
|
|
||
| const stored = window.localStorage.getItem('volumes'); | ||
| if (!stored) return; |
There was a problem hiding this comment.
Persisting completedAt by mutating the localStorage['volumes'] payload is fragile because src/lib/settings/volume-data.ts serializes _volumesInternal back into the same key via toJSON() (which currently does not include completedAt). On startup/import order, that serializer can wipe any previously stored completedAt before this module reads it, causing completion timestamps to be lost. Consider storing completedAtMap under its own localStorage key (or adding completedAt to VolumeData JSON + sync schema) to make persistence reliable.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…rite prompt - executeRenameSeries now commits each volume locally ONLY after its cloud rename succeeded, collecting per-volume failures instead of aborting the whole series mid-loop with a false 'kept in sync' message. Failed volumes keep the old title everywhere; retrying the same rename converges on just the stragglers. SeriesView navigates to the new series on full success and reports per-volume failures otherwise. - Series rename is blocked with a download-first notice when the cloud holds volumes missing from the local library — renaming around them would split the series across two cloud folders. TODO(data-update): the proper fix is downloading a volume's .mokuro/ metadata without the full archive (blocked on the metadata-persistence work; see PR #201). - The rename volume list now comes from the preview (which carries volumeTitle) instead of re-running the same Dexie query, closing a divergence window between the cloud rename list and the local commit list. - VolumeEditorModal offers overwrite-or-cancel when the new volume name collides with an existing cloud backup (TARGET_EXISTS). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…acker # Conflicts: # src/lib/util/hash-router.ts # src/routes/+page.svelte # src/routes/[...catchall]/+page.svelte
|
Thanks for helping make this project better! We use a Looking forward to reviewing your changes! |
feat: Add toggle to swap mouse wheel scroll/zoom behavior
Adds a goal progress tracker.
Set volume reading goals for a day, month, season, or year, or set a custom date range.
Tracks completed volumes and equivalent completed volumes (25% of one manga and 75% of another volume = equivalent of one volume completed).
Set volume-specific deadlines, and toggle between numbers of pages to read per day or per week to keep on target.
Sort volumes by number of pages left to finish a volume, nearest deadline, and other criteria.
View volumes completed in a reading period, optionally collapsed into series:
I've been using this since the start of the year to keep track of my progress, giving myself a clear measurement of how far ahead or behind I am.
Main issues: