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.