diff --git a/js/background.js b/js/background.js index 88c1a0f..913167a 100644 --- a/js/background.js +++ b/js/background.js @@ -1,333 +1,387 @@ -chrome.runtime.onInstalled.addListener(function () { - - let default_options = { - "local": { - "previous_colors": null, - "previous_theme": null, - "errors": [], - "saved_themes": {}, - "liked_themes": [], - }, - "sync": { - "dark_preset": { - "background-0": "#161616", - "background-1": "#1e1e1e", - "background-2": "#262626", - "borders": "#3c3c3c", - "text-0": "#f5f5f5", - "text-1": "#e2e2e2", - "text-2": "#ababab", - "links": "#56Caf0", - "sidebar": "#1e1e1e", - "sidebar-text": "#f5f5f5" - }, - "new_install": true, - "assignments_due": true, - "gpa_calc": true, - "dark_mode": true, - "gradent_cards": false, - "disable_color_overlay": false, - "auto_dark": false, - "auto_dark_start": { "hour": "20", "minute": "00" }, - "auto_dark_end": { "hour": "08", "minute": "00" }, - "num_assignments": 4, - "custom_domain": [""], - "custom_domain_denied": [], - "auto_detect_disabled": false, - "assignments_done": [], - "dashboard_grades": true, - "assignment_date_format": false, - "dashboard_notes": false, - "dashboard_notes_text": "", - "dashboard_notes_mode": "edit", - "dashboard_notes_position": "top", - "widget_order": {}, - "focus_timer": false, - "focus_timer_position": "cards_end", - "focus_timer_preset_1": { "h": 0, "m": 15, "s": 0 }, - "focus_timer_preset_2": { "h": 0, "m": 25, "s": 0 }, - "focus_timer_preset_3": { "h": 0, "m": 45, "s": 0 }, - "breathing": false, - "breathing_position": "cards_end", - "breathing_preset_1": { "h": 0, "m": 3, "s": 0 }, - "breathing_preset_2": { "h": 0, "m": 5, "s": 0 }, - "breathing_preset_3": { "h": 0, "m": 10, "s": 0 }, - "daily_quotes": false, - "daily_quotes_position": "top", - "quick_links": false, - "quick_links_position": "cards_end", - "heatmap": false, - "heatmap_position": "below", - "heatmap_excluded_courses": [], - "widget_top_height": 0, - "widget_bottom_height": 0, - "widget_row_height": 0, - "dashboard_notes_height": 0, - "gpa_calc_height": 0, - "focus_timer_height": 0, - "breathing_height": 0, - "daily_quotes_height": 0, - "quick_links_height": 0, - "heatmap_height": 0, - "quick_links_font_size": 12, - "quick_links_icon_size": 36, - "better_todo": true, - "todo_hr24": false, - "todo_separate_scrollbar": false, - "better_sidebar": false, - "condensed_cards": false, - "custom_cards": {}, - "custom_cards_2": {}, - "custom_cards_3": {}, - "custom_assignments": [], - "custom_assignments_overflow": ["custom_assignments"], - "grade_hover": false, - "card_letter": true, - "hide_completed_cards": false, - "num_todo_items": 10, - "custom_font": { "link": "", "family": "" }, - "hover_preview": true, - "full_width": null, - "remlogo": null, - "gpa_calc_bounds": { - "A+": { "cutoff": 97, "gpa": 4.0 }, - "A": { "cutoff": 93, "gpa": 4 }, - "A-": { "cutoff": 90, "gpa": 3.7 }, - "B+": { "cutoff": 87, "gpa": 3.3 }, - "B": { "cutoff": 83, "gpa": 3 }, - "B-": { "cutoff": 80, "gpa": 2.7 }, - "C+": { "cutoff": 77, "gpa": 2.3 }, - "C": { "cutoff": 73, "gpa": 2 }, - "C-": { "cutoff": 70, "gpa": 1.7 }, - "D+": { "cutoff": 67, "gpa": 1.3 }, - "D": { "cutoff": 63, "gpa": 1 }, - "D-": { "cutoff": 60, "gpa": .7 }, - "F": { "cutoff": 0, "gpa": 0 } - }, - "card_overdues": false, - "relative_dues": false, - "equal_height_cards": false, - "hide_new_canvas": true, - "hide_sequence_footer": false, - "hide_navbar": false, - "hide_sidebar": false, - "quiz_safe_mode": false, - "dark_mode_fix": [], - "assignment_states": {}, - "tab_icons": false, - "todo_hide_feedback": false, - "todo_full_height": true, - "todo_progress_rings": "rings", - "todo_timeframe": "all", - "todo_timeframe_custom_past": 7, - "todo_timeframe_custom_future": 14, - "todo_celebrations_enabled": true, - "todo_celebrations": ["confetti"], - "todo_celebration_amount": "normal", - "todo_show_scores": false, - "custom_font_skip_p": false, - "device_dark": false, - "cumulative_gpa": { "name": "Cumulative GPA", "hidden": false, "weight": "dnc", "credits": 999, "gr": 3.21 }, - "card_method_date": false, - "card_method_dashboard": true, - "card_limit": 25, - "remind": false, - "reminders": [], - "reminder_count": 1, - "multi_remind": false, - "id": "", - "new_browser": null, - "gpa_calc_cumulative": false, - "gpa_calc_weighted": true, - "gpa_calc_show_on_hover": false, - "browser_show_likes": false, - "custom_styles": "", - "imageSize": 100, - "cardRoundness": 5, - "imageRoundness": 0, - 'cardSpacing': 0, - "cardWidth": 262, - "cardHeight": 146, - "customCardStyles": false, - "customBackgroundLink": "", - "customBackgroundColor": "", - "customBackgroundColorTransparent": false, - "customBackgroundScale": 100, - "customBackgroundDaily": false, - "customBackgroundNasaDaily": false, - "nasaInfoOverlay": false, - "fitImageToScreen": false, - "bg_opacity": 65, - "sidebar_opacity": 100, - "bg_blur": 8, - "sidebar_blur": 0, - "global_search": false, - "grade_analytics": true, - "grade_analytics_zones": false, - } - }; - - const updateMsg = "Canvas Refined was just updated!\nThis version added new themes, cumulative GPA into the GPA calculator, and a new sorting function for themes."; - - chrome.storage.local.get(null, local => { - chrome.storage.sync.get(null, async sync => { - let newSyncOptions = {"update_msg": updateMsg}; - let newLocalOptions = {}; - Object.keys(default_options["sync"]).forEach(option => { - if (sync[option] !== undefined) return; - newSyncOptions[option] = default_options["sync"][option]; - }); - Object.keys(default_options["local"]).forEach(option => { - if (local[option] !== undefined) return; - newLocalOptions[option] = default_options["local"][option]; - }) - - if (sync["nasaFitToScreen"] !== undefined && sync["fitImageToScreen"] === undefined) { - newSyncOptions["fitImageToScreen"] = sync["nasaFitToScreen"]; - } - - // The Top/Bottom widget rows used to share one height: give both rows the - // user's old value instead of dropping it to "auto". - if (sync["widget_top_height"] === undefined && sync["widget_bottom_height"] === undefined) { - const legacyRowHeight = Number(sync["widget_row_height"]); - if (Number.isFinite(legacyRowHeight) && legacyRowHeight > 0) { - newSyncOptions["widget_top_height"] = legacyRowHeight; - newSyncOptions["widget_bottom_height"] = legacyRowHeight; - } - } - - if (Object.keys(newLocalOptions).length > 0) { - chrome.storage.local.set(newLocalOptions); - } - - if (Object.keys(newSyncOptions).length > 0) { - chrome.storage.sync.set(newSyncOptions).then(() => { - console.log(newSyncOptions); - if (newSyncOptions.new_install === true) { - chrome.runtime.openOptionsPage(); - chrome.storage.sync.set({ new_install: false }); - } - }); - } - }); - }); -}); - -// APOD demo key allows 30 req/hour, 50/day; the API's own 429s do the limiting. -chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { - if (message?.type === "getNasaBackground") { - getNasaBackground().then(sendResponse); - return true; - } - if (message?.type === "getDailyQuote") { - getDailyQuote().then(sendResponse); - return true; - } -}); - -let nasaRequestQueue = Promise.resolve(); -function queuedNasaTask(task) { - const run = nasaRequestQueue.then(task, task); - nasaRequestQueue = run.catch(() => {}); - return run; -} - -async function getNasaBackground() { - return queuedNasaTask(async () => { - const date = new Date(); - for (let i = 0; i < 7; i++) { - const dateStr = date.toISOString().slice(0, 10); - const cacheKey = `nasa_apod_${dateStr}`; - const metadataKey = `nasa_apod_meta_${dateStr}`; - const cached = await chrome.storage.local.get([cacheKey, metadataKey]); - if (cached[cacheKey]) return cached[cacheKey]; - - // never re-probe dates the API reported as missing - const missingKey = `nasa_apod_missing_${dateStr}`; - const missing = await chrome.storage.local.get(missingKey); - if (missing[missingKey]) { - date.setDate(date.getDate() - 1); - continue; - } - - const data = await callNasaApi(dateStr); - if (data === "ratelimited" || data === null) return null; - if (data === "missing") { - await chrome.storage.local.set({ [missingKey]: true }); - date.setDate(date.getDate() - 1); - continue; - } - - const url = data.thumbnail_url || data.hdurl || data.url; - if (!url) return null; - const result = { url, scale: 100, date: dateStr }; - const meta = { title: data.title || "", date: data.date || dateStr, copyright: data.copyright || "", explanation: data.explanation || "" }; - // one write, so the overlay never sees an image without its metadata - await chrome.storage.local.set({ [cacheKey]: result, [metadataKey]: meta }); - return result; - } - return null; - }); -} - -async function callNasaApi(dateStr) { - let response; - try { - response = await fetch(`https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY&thumbs=true&date=${dateStr}`); - } catch (error) { - console.error("[CanvasRefined] Failed to fetch NASA APOD:", error); - return null; - } - - if (response.status === 429) return "ratelimited"; - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - if (errorData.code === 404 && (errorData.msg || "").toLowerCase().includes("no data available")) { - return "missing"; - } - return null; - } - - return await response.json().catch(() => null); -} - -// Cached in storage.local for one day; fetched from the worker (no CORS headers). -function dailyQuoteTodayKey() { - const d = new Date(); - return d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0"); -} - -let dailyQuoteInFlight = null; -async function getDailyQuote() { - const today = dailyQuoteTodayKey(); - let cached = null; - try { - const st = await chrome.storage.local.get("daily_quote"); - cached = st && st.daily_quote; - } catch (e) { /* ignore storage issues */ } - - if (cached && cached.date === today && cached.quote) return cached; - if (dailyQuoteInFlight) return dailyQuoteInFlight; - - const request = (async () => { - try { - const response = await fetch("https://stoic.tekloon.net/stoic-quote", { cache: "no-store" }); - if (!response.ok) throw new Error(`HTTP ${response.status}`); - const json = await response.json(); - const data = (json && json.data) || {}; - const quote = String(data.quote || "").replace(/\s+/g, " ").trim(); - const author = String(data.author || "").trim(); - if (!quote) throw new Error("empty quote"); - const entry = { date: today, quote, author }; - await chrome.storage.local.set({ daily_quote: entry }); - return entry; - } catch (e) { - console.warn("[CanvasRefined] Daily quote fetch failed:", e); - return cached || null; - } finally { - dailyQuoteInFlight = null; - } - })(); - dailyQuoteInFlight = request; - return request; -} +chrome.runtime.onInstalled.addListener(function () { + + let default_options = { + "local": { + "previous_colors": null, + "previous_theme": null, + "errors": [], + "saved_themes": {}, + "liked_themes": [], + }, + "sync": { + "dark_preset": { + "background-0": "#161616", + "background-1": "#1e1e1e", + "background-2": "#262626", + "borders": "#3c3c3c", + "text-0": "#f5f5f5", + "text-1": "#e2e2e2", + "text-2": "#ababab", + "links": "#56Caf0", + "sidebar": "#1e1e1e", + "sidebar-text": "#f5f5f5" + }, + "new_install": true, + "assignments_due": true, + "gpa_calc": true, + "dark_mode": true, + "gradent_cards": false, + "disable_color_overlay": false, + "auto_dark": false, + "auto_dark_start": { "hour": "20", "minute": "00" }, + "auto_dark_end": { "hour": "08", "minute": "00" }, + "num_assignments": 4, + "custom_domain": [""], + "custom_domain_denied": [], + "auto_detect_disabled": false, + "assignments_done": [], + "dashboard_grades": true, + "assignment_date_format": false, + "dashboard_notes": false, + "dashboard_notes_text": "", + "dashboard_notes_mode": "edit", + "dashboard_notes_position": "top", + "widget_order": {}, + "focus_timer": false, + "focus_timer_position": "cards_end", + "focus_timer_preset_1": { "h": 0, "m": 15, "s": 0 }, + "focus_timer_preset_2": { "h": 0, "m": 25, "s": 0 }, + "focus_timer_preset_3": { "h": 0, "m": 45, "s": 0 }, + "breathing": false, + "breathing_position": "cards_end", + "breathing_preset_1": { "h": 0, "m": 3, "s": 0 }, + "breathing_preset_2": { "h": 0, "m": 5, "s": 0 }, + "breathing_preset_3": { "h": 0, "m": 10, "s": 0 }, + "daily_quotes": false, + "daily_quotes_position": "top", + "quick_links": false, + "quick_links_position": "cards_end", + "heatmap": false, + "heatmap_position": "below", + "heatmap_excluded_courses": [], + "widget_top_height": 0, + "widget_bottom_height": 0, + "widget_row_height": 0, + "dashboard_notes_height": 0, + "gpa_calc_height": 0, + "focus_timer_height": 0, + "breathing_height": 0, + "daily_quotes_height": 0, + "quick_links_height": 0, + "heatmap_height": 0, + "quick_links_font_size": 12, + "quick_links_icon_size": 36, + "better_todo": true, + "todo_hr24": false, + "todo_separate_scrollbar": false, + "better_sidebar": false, + "condensed_cards": false, + "custom_cards": {}, + "custom_cards_2": {}, + "custom_cards_3": {}, + "custom_assignments": [], + "custom_assignments_overflow": ["custom_assignments"], + "grade_hover": false, + "card_letter": true, + "hide_completed_cards": false, + "num_todo_items": 10, + "custom_font": { "link": "", "family": "" }, + "hover_preview": true, + "full_width": null, + "remlogo": null, + "gpa_calc_bounds": { + "A+": { "cutoff": 97, "gpa": 4.0 }, + "A": { "cutoff": 93, "gpa": 4 }, + "A-": { "cutoff": 90, "gpa": 3.7 }, + "B+": { "cutoff": 87, "gpa": 3.3 }, + "B": { "cutoff": 83, "gpa": 3 }, + "B-": { "cutoff": 80, "gpa": 2.7 }, + "C+": { "cutoff": 77, "gpa": 2.3 }, + "C": { "cutoff": 73, "gpa": 2 }, + "C-": { "cutoff": 70, "gpa": 1.7 }, + "D+": { "cutoff": 67, "gpa": 1.3 }, + "D": { "cutoff": 63, "gpa": 1 }, + "D-": { "cutoff": 60, "gpa": .7 }, + "F": { "cutoff": 0, "gpa": 0 } + }, + "card_overdues": false, + "relative_dues": false, + "equal_height_cards": false, + "hide_new_canvas": true, + "hide_sequence_footer": false, + "hide_navbar": false, + "hide_sidebar": false, + "quiz_safe_mode": false, + "dark_mode_fix": [], + "assignment_states": {}, + "tab_icons": false, + "todo_hide_feedback": false, + "todo_full_height": true, + "todo_progress_rings": "rings", + "todo_timeframe": "all", + "todo_timeframe_custom_past": 7, + "todo_timeframe_custom_future": 14, + "todo_celebrations_enabled": true, + "todo_celebrations": ["confetti"], + "todo_celebration_amount": "normal", + "todo_show_scores": false, + "custom_font_skip_p": false, + "device_dark": false, + "cumulative_gpa": { "name": "Cumulative GPA", "hidden": false, "weight": "dnc", "credits": 999, "gr": 3.21 }, + "card_method_date": false, + "card_method_dashboard": true, + "card_limit": 25, + "remind": false, + "reminders": [], + "reminder_count": 1, + "multi_remind": false, + "id": "", + "new_browser": null, + "gpa_calc_cumulative": false, + "gpa_calc_weighted": true, + "gpa_calc_show_on_hover": false, + "browser_show_likes": false, + "custom_styles": "", + "imageSize": 100, + "cardRoundness": 5, + "imageRoundness": 0, + 'cardSpacing': 0, + "cardWidth": 262, + "cardHeight": 146, + "customCardStyles": false, + "customBackgroundLink": "", + "customBackgroundColor": "", + "customBackgroundColorTransparent": false, + "customBackgroundScale": 100, + "customBackgroundDaily": false, + "customBackgroundNasaDaily": false, + "nasaInfoOverlay": false, + "fitImageToScreen": false, + "bg_opacity": 65, + "sidebar_opacity": 100, + "bg_blur": 8, + "sidebar_blur": 0, + "global_search": false, + "grade_analytics": true, + "grade_analytics_zones": false, + } + }; + + const updateMsg = "Canvas Refined was just updated!\nThis version added new themes, cumulative GPA into the GPA calculator, and a new sorting function for themes."; + + chrome.storage.local.get(null, local => { + chrome.storage.sync.get(null, async sync => { + let newSyncOptions = {"update_msg": updateMsg}; + let newLocalOptions = {}; + Object.keys(default_options["sync"]).forEach(option => { + if (sync[option] !== undefined) return; + newSyncOptions[option] = default_options["sync"][option]; + }); + Object.keys(default_options["local"]).forEach(option => { + if (local[option] !== undefined) return; + newLocalOptions[option] = default_options["local"][option]; + }) + + if (sync["nasaFitToScreen"] !== undefined && sync["fitImageToScreen"] === undefined) { + newSyncOptions["fitImageToScreen"] = sync["nasaFitToScreen"]; + } + + // The Top/Bottom widget rows used to share one height: give both rows the + // user's old value instead of dropping it to "auto". + if (sync["widget_top_height"] === undefined && sync["widget_bottom_height"] === undefined) { + const legacyRowHeight = Number(sync["widget_row_height"]); + if (Number.isFinite(legacyRowHeight) && legacyRowHeight > 0) { + newSyncOptions["widget_top_height"] = legacyRowHeight; + newSyncOptions["widget_bottom_height"] = legacyRowHeight; + } + } + + if (Object.keys(newLocalOptions).length > 0) { + chrome.storage.local.set(newLocalOptions); + } + + if (Object.keys(newSyncOptions).length > 0) { + chrome.storage.sync.set(newSyncOptions).then(() => { + console.log(newSyncOptions); + if (newSyncOptions.new_install === true) { + chrome.runtime.openOptionsPage(); + chrome.storage.sync.set({ new_install: false }); + } + }); + } + }); + }); +}); + +// NASA serves APOD from its own site now: /wp-json/wp/v2/apod-basic/. +// No API key and no per-key quota, but the response carries no usable CORS +// header, so science.nasa.gov needs a host permission (see manifest.json). +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message?.type === "getNasaBackground") { + getNasaBackground().then(sendResponse); + return true; + } + if (message?.type === "getDailyQuote") { + getDailyQuote().then(sendResponse); + return true; + } +}); + +let nasaRequestQueue = Promise.resolve(); +function queuedNasaTask(task) { + const run = nasaRequestQueue.then(task, task); + nasaRequestQueue = run.catch(() => {}); + return run; +} + +async function getNasaBackground() { + return queuedNasaTask(async () => { + const date = new Date(); + const todayStr = date.toISOString().slice(0, 10); + for (let i = 0; i < 7; i++) { + const dateStr = date.toISOString().slice(0, 10); + const cacheKey = `nasa_apod_${dateStr}`; + const metadataKey = `nasa_apod_meta_${dateStr}`; + const cached = await chrome.storage.local.get([cacheKey, metadataKey]); + if (cached[cacheKey]) return cached[cacheKey]; + + // never re-probe dates the API reported as missing + const missingKey = `nasa_apod_missing_${dateStr}`; + const missing = await chrome.storage.local.get(missingKey); + if (missing[missingKey]) { + date.setDate(date.getDate() - 1); + continue; + } + + const data = await callNasaApi(dateStr); + if (data === "ratelimited" || data === null) return null; + const url = nasaApodImageUrl(data); + if (data === "missing" || !url) { + // Video days carry only a generic NASA card in hdurl, so they make a + // poor wallpaper; skip them like a missing date. Only persist the + // marker for past dates: today's APOD may simply not be posted yet, + // and a permanent marker would hide it for good. + if (dateStr < todayStr || data !== "missing") { + await chrome.storage.local.set({ [missingKey]: true }); + } + date.setDate(date.getDate() - 1); + continue; + } + + const result = { url, scale: 100, date: dateStr }; + // The feed ships HTML (links, , entities) where the old API + // sent plain text; the overlay renders textContent, so strip it. + const meta = { + title: nasaPlainText(data.title), + date: data.date || dateStr, + copyright: nasaPlainText(data.copyright || data.credit), + explanation: nasaPlainText(data.explanation) + }; + // one write, so the overlay never sees an image without its metadata + await chrome.storage.local.set({ [cacheKey]: result, [metadataKey]: meta }); + return result; + } + return null; + }); +} + +// "2026-10-27" -> "261027", the date path this endpoint expects. +function nasaApodPathKey(dateStr) { + return String(dateStr).replace(/-/g, "").slice(2); +} + +// Video entries point hdurl at a generic placeholder, not a still frame. +const NASA_APOD_PLACEHOLDER = /news-thumbnail\.png/i; + +function nasaApodImageUrl(data) { + if (!data || typeof data !== "object") return null; + if (data.media_type && data.media_type !== "image") return null; + // hdurl is the asset itself; url is the article page, so only trust it when + // it actually points at a picture. + const url = data.hdurl || (/\/\S*\.(jpe?g|png|webp|gif)(\?|$)/i.test(data.url || "") ? data.url : null); + if (typeof url !== "string" || !/^https?:\/\//i.test(url)) return null; + if (NASA_APOD_PLACEHOLDER.test(url)) return null; + return url; +} + +function nasaPlainText(html) { + if (!html) return ""; + return String(html) + .replace(//gi, " ") + .replace(/<\/(p|div|li)>/gi, " ") + .replace(/<[^>]*>/g, "") + .replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCodePoint(parseInt(hex, 16))) + .replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(Number(dec))) + .replace(/ /gi, " ") + .replace(/"/gi, '"') + .replace(/'/gi, "'") + .replace(/—/gi, "\u2014") + .replace(/–/gi, "\u2013") + .replace(/&/gi, "&") + .replace(/\s+/g, " ") + .trim(); +} + +async function callNasaApi(dateStr) { + // _fields trims the payload: the default also inlines the whole article HTML. + const fields = "date,title,media_type,hdurl,url,permalink,credit,copyright,explanation,alt"; + let response; + try { + response = await fetch(`https://science.nasa.gov/wp-json/wp/v2/apod-basic/${nasaApodPathKey(dateStr)}?_fields=${fields}`); + } catch (error) { + console.error("[CanvasRefined] Failed to fetch NASA APOD:", error); + return null; + } + + if (response.status === 429) return "ratelimited"; + if (!response.ok) { + if (response.status === 404) { + const errorData = await response.json().catch(() => null); + if (errorData?.code === "apod_basic_not_found") return "missing"; + } + return null; + } + + return await response.json().catch(() => null); +} + +// Cached in storage.local for one day; fetched from the worker (no CORS headers). +function dailyQuoteTodayKey() { + const d = new Date(); + return d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0"); +} + +let dailyQuoteInFlight = null; +async function getDailyQuote() { + const today = dailyQuoteTodayKey(); + let cached = null; + try { + const st = await chrome.storage.local.get("daily_quote"); + cached = st && st.daily_quote; + } catch (e) { /* ignore storage issues */ } + + if (cached && cached.date === today && cached.quote) return cached; + if (dailyQuoteInFlight) return dailyQuoteInFlight; + + const request = (async () => { + try { + const response = await fetch("https://stoic.tekloon.net/stoic-quote", { cache: "no-store" }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + const json = await response.json(); + const data = (json && json.data) || {}; + const quote = String(data.quote || "").replace(/\s+/g, " ").trim(); + const author = String(data.author || "").trim(); + if (!quote) throw new Error("empty quote"); + const entry = { date: today, quote, author }; + await chrome.storage.local.set({ daily_quote: entry }); + return entry; + } catch (e) { + console.warn("[CanvasRefined] Daily quote fetch failed:", e); + return cached || null; + } finally { + dailyQuoteInFlight = null; + } + })(); + dailyQuoteInFlight = request; + return request; +} diff --git a/manifest.json b/manifest.json index 465254e..6780931 100644 --- a/manifest.json +++ b/manifest.json @@ -31,7 +31,7 @@ } ], "permissions": ["storage", "unlimitedStorage"], - "host_permissions": ["https://stoic.tekloon.net/*"], + "host_permissions": ["https://stoic.tekloon.net/*", "https://science.nasa.gov/*"], "default_locale": "en", "browser_specific_settings": { "gecko": {