Fix plan days with no chapters, add a way out of ink mode, harden ntfy reminders
Build and Push Docker Image / build-and-push (push) Failing after 19s
Build and Push Docker Image / build-and-push (push) Failing after 19s
Plans can no longer have more days than chapters (existing ones heal), and opening a plan never strands the Reader on an empty day. The Reader shows Done drawing while inking (Escape also exits). Reminders use the user's own timezone, send within a 3 hour grace window, and Settings has a Send test notification button. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ecb1c701fa
commit
297f2c7e84
14 files changed
+335
-58
No files matched your search
@@ -180,6 +180,26 @@ export function getPlan(planId) {
|
||||
return PLAN_BY_ID[planId] ?? null;
|
||||
}
|
||||
|
||||
// A plan can't have more days than it has chapters (the extra days would be empty, with
|
||||
// nothing to read). Existing custom plans with too many days heal to one chapter a day.
|
||||
export function clampPlanDays(books, chapterRanges, days) {
|
||||
const total = (books ?? []).reduce((sum, b) => sum + bookChapterCount(b, chapterRanges), 0);
|
||||
const wanted = Math.max(1, Math.round(Number(days) || 1));
|
||||
return total > 0 ? Math.min(wanted, total) : wanted;
|
||||
}
|
||||
|
||||
export function normalizeCustomPlan(plan) {
|
||||
const days = clampPlanDays(plan.books, plan.chapterRanges, plan.days);
|
||||
return days === plan.days ? plan : { ...plan, days };
|
||||
}
|
||||
|
||||
// Nearest day at or after `from` that has something to read, else the nearest one before it.
|
||||
export function nearestReadingDay(schedule, from) {
|
||||
for (let i = Math.max(0, from); i < schedule.length; i += 1) if (schedule[i]?.length) return i;
|
||||
for (let i = Math.min(from, schedule.length) - 1; i >= 0; i -= 1) if (schedule[i]?.length) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Registers/updates/removes user-created plans in the lookup used by getPlan/getPlanSchedule.
|
||||
// Called by useReadingPlans() whenever the custom-plans list changes.
|
||||
export function registerCustomPlans(customPlans) {
|
||||
@@ -190,7 +210,7 @@ export function registerCustomPlans(customPlans) {
|
||||
}
|
||||
}
|
||||
for (const cp of customPlans) {
|
||||
PLAN_BY_ID[cp.id] = { category: 'My Plans', icon: '✨', ...cp, custom: true };
|
||||
PLAN_BY_ID[cp.id] = { category: 'My Plans', icon: '✨', ...normalizeCustomPlan(cp), custom: true };
|
||||
scheduleCache.delete(cp.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { test, expect } from 'vitest';
|
||||
import {
|
||||
clampPlanDays, getPlan, getPlanSchedule, nearestReadingDay, normalizeCustomPlan, registerCustomPlans,
|
||||
} from './readingPlans.js';
|
||||
|
||||
const luke = (days) => ({ id: `custom-luke-${days}`, title: 'Luke', days, books: ['LUK'], custom: true });
|
||||
|
||||
test('a plan can never have more days than chapters', () => {
|
||||
expect(clampPlanDays(['LUK'], undefined, 90)).toBe(24);
|
||||
expect(clampPlanDays(['LUK'], undefined, 24)).toBe(24);
|
||||
expect(clampPlanDays(['LUK'], undefined, 10)).toBe(10);
|
||||
expect(clampPlanDays(['LUK'], { LUK: { start: 1, end: 5 } }, 30)).toBe(5); // honours chapter ranges
|
||||
expect(clampPlanDays(['LUK'], undefined, 0)).toBe(1);
|
||||
});
|
||||
|
||||
test('normalizeCustomPlan leaves a fine plan alone and heals one with too many days', () => {
|
||||
const fine = luke(12);
|
||||
expect(normalizeCustomPlan(fine)).toBe(fine);
|
||||
expect(normalizeCustomPlan(luke(90)).days).toBe(24);
|
||||
});
|
||||
|
||||
test('a 90-day Luke plan registers as 24 days with a chapter on every day (no empty days)', () => {
|
||||
registerCustomPlans([luke(90)]);
|
||||
const plan = getPlan('custom-luke-90');
|
||||
expect(plan.days).toBe(24);
|
||||
const schedule = getPlanSchedule('custom-luke-90');
|
||||
expect(schedule).toHaveLength(24);
|
||||
expect(schedule.every((day) => day.length === 1)).toBe(true);
|
||||
expect(schedule[2]).toEqual([{ book: 'LUK', chapter: 3 }]); // "Day 3" opens Luke 3
|
||||
});
|
||||
|
||||
test('nearestReadingDay skips empty days, looking forward first and then back', () => {
|
||||
const schedule = [[{ book: 'LUK', chapter: 1 }], [], [], [{ book: 'LUK', chapter: 2 }], []];
|
||||
expect(nearestReadingDay(schedule, 1)).toBe(3);
|
||||
expect(nearestReadingDay(schedule, 3)).toBe(3);
|
||||
expect(nearestReadingDay(schedule, 4)).toBe(3);
|
||||
expect(nearestReadingDay([[], []], 0)).toBe(-1);
|
||||
});
|
||||
Reference in new issue
Block a user