Skip to content

Feature/progress tracker - #201

Closed
ChristopherFritz wants to merge 34 commits into
Gnathonic:mainfrom
ChristopherFritz:feature/progress-tracker
Closed

Feature/progress tracker#201
ChristopherFritz wants to merge 34 commits into
Gnathonic:mainfrom
ChristopherFritz:feature/progress-tracker

Conversation

@ChristopherFritz

Copy link
Copy Markdown
Contributor

Adds a goal progress tracker.

image

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).

image

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:

image image

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:

  • No sync support for goals/progress.
  • An additional variable (completedAt) added to the volume data hasn't been tested for syncing.
  • The file "progress-tracker-helpers.ts" is sitting in the views folder; I wasn't certain of the most appropriate location for it.
  • There are probably still some minor rough edges that can use some polish.

- 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
- 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.
- 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
Copilot AI review requested due to automatic review settings March 22, 2026 20:01
@vercel

vercel Bot commented Mar 22, 2026

Copy link
Copy Markdown

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

Copilot AI 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.

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/goals module (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.

Comment thread src/lib/settings/misc.ts
Comment on lines 42 to 52
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

Copilot AI Mar 22, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +30 to +39
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
};

Copilot AI Mar 22, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +19
try {
const parsed = JSON.parse(stored);
return {
volumeDeadlines: parsed.volumeDeadlines || defaultSettings.volumeDeadlines
};

Copilot AI Mar 22, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +140 to +147
const periodStart = new Date(periodStartTimestamp);
const deadlineParts = deadline.split('-').map(Number);
const deadlineInclusiveEnd = new Date(
deadlineParts[0],
deadlineParts[1] - 1,
deadlineParts[2] + 1
);

Copilot AI Mar 22, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Suggested change
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);

Copilot uses AI. Check for mistakes.
Comment on lines +67 to +72
<a
href="#/reader/{seriesId}/{volumeId}"
onclick={(e) => {
e.preventDefault();
if (seriesId) nav.toReader(seriesId, volumeId);
}}

Copilot AI Mar 22, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +118 to +122
const progressPercent = totalPages > 0 ? (currentPage / totalPages) * 100 : 0;

stats[volume_uuid] = {
progressPercent,
progressPercentString: progressPercent.toFixed(0) + '%',

Copilot AI Mar 22, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
stats[volume_uuid] = {
progressPercent,
progressPercentString: progressPercent.toFixed(0) + '%',
remainingPages: totalPages - currentPage,

Copilot AI Mar 22, 2026

Copy link

Choose a reason for hiding this comment

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

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).

Suggested change
remainingPages: totalPages - currentPage,
remainingPages: Math.max(0, totalPages - currentPage),

Copilot uses AI. Check for mistakes.
Comment on lines 50 to +54
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' };

Copilot AI Mar 22, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +31 to +35
function persistCompletedAtMapToVolumes(map: CompletedAtMap) {
if (!browser) return;

const stored = window.localStorage.getItem('volumes');
if (!stored) return;

Copilot AI Mar 22, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
@vercel

vercel Bot commented Mar 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
mokuro-reader Ready Ready Preview, Comment Mar 23, 2026 10:38pm

Request Review

Gnathonic added a commit that referenced this pull request Jul 4, 2026
…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
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Thanks for helping make this project better!

We use a develop branch for external PRs to allow for safer review and integration. Please update your PR to target develop instead of main, then reopen it.

Looking forward to reviewing your changes!

@github-actions github-actions Bot closed this Jul 9, 2026
adrian-tompkins pushed a commit to adrian-tompkins/mokuro-reader that referenced this pull request Aug 2, 2026
feat: Add toggle to swap mouse wheel scroll/zoom behavior
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