PageSourceSearch

https://garfieldapp.pages.dev/dayFilter.js

js garfieldapp.pages.dev collected 2026-10-03 05:14:45 UTC 5,583 bytes, 143 lines download raw bytes

1import { CONFIG } from './config.js';
2import { translations } from './translations.js';
3
4// "Days to show" preference: every strip, only the Sunday colour strips, or
5// only the daily (Monday–Saturday) strips. Navigation, random/shuffle picks,
6// favorites-only browsing and the date picker all route through these helpers.
7export const DAY_FILTERS = Object.freeze(['all', 'sundays', 'no-sundays']);
8
9const OPTION_LABEL_KEYS = Object.freeze({
10    all: 'dayFilterAll',
11    sundays: 'dayFilterSundays',
12    'no-sundays': 'dayFilterNoSundays'
13});
14
15const MAX_RANDOM_ATTEMPTS = 50;
16
17export function getValidDayFilter(value) {
18    return DAY_FILTERS.includes(value) ? value : 'all';
19}
20
21export function getDayFilter() {
22    try {
23        return getValidDayFilter(localStorage.getItem(CONFIG.STORAGE_KEYS.DAY_FILTER));
24    } catch {
25        return 'all';
26    }
27}
28
29export function setDayFilter(value) {
30    const filter = getValidDayFilter(value);
31    try {
32        localStorage.setItem(CONFIG.STORAGE_KEYS.DAY_FILTER, filter);
33    } catch { /* storage blocked — the in-memory control still reflects the choice */ }
34    const select = document.getElementById('dayFilter');
35    if (select) select.value = filter;
36    return filter;
37}
38
39export function isDateAllowed(date, filter = getDayFilter()) {
40    if (filter === 'sundays') return date.getDay() === 0;
41    if (filter === 'no-sundays') return date.getDay() !== 0;
42    return true;
43}
44
45function dayIndex(date) {
46    return Math.round(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()) / 86400000);
47}
48
49function addDays(date, days) {
50    const result = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 12, 0, 0, 0);
51    result.setDate(result.getDate() + days);
52    return result;
53}
54
55function favoriteToDate(favorite) {
56    const [year, month, day] = favorite.split('/').map(Number);
57    return new Date(year, month - 1, day, 12, 0, 0, 0);
58}
59
60export function filterFavoritesByDay(favorites, filter = getDayFilter()) {
61    if (filter === 'all') return favorites;
62    return favorites.filter(favorite => isDateAllowed(favoriteToDate(favorite), filter));
63}
64
65/**
66 * The nearest allowed day strictly after (step 1) or before (step -1) `date`.
67 * Every filter allows at least one weekday, so this terminates within a week.
68 */
69export function stepToAllowedDate(date, step, filter = getDayFilter()) {
70    let next = addDays(date, step);
71    while (!isDateAllowed(next, filter)) next = addDays(next, step);
72    return next;
73}
74
75/**
76 * Move `date` onto an allowed day inside [min, max]. `preferStep` picks the
77 * search direction (1 = on/after, -1 = on/before, 0 = nearest, ties earlier).
78 * Returns null when the range holds no allowed day.
79 */
80export function snapToAllowedDate(date, { min = null, max = null, preferStep = 0, filter = getDayFilter() } = {}) {
81    const inRange = candidate => (!min || dayIndex(candidate) >= dayIndex(min)) && (!max || dayIndex(candidate) <= dayIndex(max));
82    const current = addDays(date, 0);
83    if (isDateAllowed(current, filter) && inRange(current)) return current;
84
85    const before = stepToAllowedDate(current, -1, filter);
86    const after = stepToAllowedDate(current, 1, filter);
87    const candidates = preferStep > 0 ? [after, before]
88        : preferStep < 0 ? [before, after]
89            : [before, after].sort((a, b) => Math.abs(dayIndex(a) - dayIndex(current)) - Math.abs(dayIndex(b) - dayIndex(current)));
90
91    const inBounds = candidates.find(inRange);
92    if (inBounds) return inBounds;
93
94    // The date sits outside the range: clamp to the closest allowed edge.
95    if (min && dayIndex(current) < dayIndex(min)) return snapToAllowedDate(min, { min, max, preferStep: 1, filter });
96    if (max && dayIndex(current) > dayIndex(max)) return snapToAllowedDate(max, { min, max, preferStep: -1, filter });
97    return null;
98}
99
100/**
101 * Uniformly pick an allowed day inside [start, end] (inclusive).
102 */
103export function randomAllowedDate(start, end, { filter = getDayFilter(), random = Math.random } = {}) {
104    const from = dayIndex(start);
105    const to = dayIndex(end);
106    if (to < from) return null;
107
108    if (filter === 'sundays') {
109        const first = snapToAllowedDate(start, { min: start, max: end, preferStep: 1, filter });
110        if (!first) return null;
111        const count = Math.floor((to - dayIndex(first)) / 7) + 1;
112        return addDays(first, 7 * Math.floor(random() * count));
113    }
114
115    for (let attempt = 0; attempt < MAX_RANDOM_ATTEMPTS; attempt += 1) {
116        const pick = addDays(start, Math.floor(random() * (to - from + 1)));
117        if (isDateAllowed(pick, filter)) return pick;
118    }
119    return snapToAllowedDate(start, { min: start, max: end, preferStep: 1, filter });
120}
121
122function translateDayFilterControl() {
123    const select = document.getElementById('dayFilter');
124    if (!select) return;
125    const t = translations[document.documentElement.lang === 'es' ? 'es' : 'en'] || translations.en;
126    for (const option of select.options) {
127        const key = OPTION_LABEL_KEYS[option.value];
128        if (key && t[key]) option.textContent = t[key];
129    }
130}
131
132/**
133 * Restore the persisted choice into the settings control and report changes.
134 * @param {(filter: string) => void} onChange
135 */
136export function initializeDayFilterControl(onChange) {
137    const select = document.getElementById('dayFilter');
138    if (!select) return;
139    select.value = getDayFilter();
140    translateDayFilterControl();
141    window.addEventListener('language-changed', translateDayFilterControl);
142    select.addEventListener('change', () => onChange(setDayFilter(select.value)));
143}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.