stuff 2: electric boogaloo

This commit is contained in:
Guy Sandler 2026-10-25 18:06:24 -07:00
parent 9bb1441c83
commit 1c140c7153
16 changed files with 251 additions and 22 deletions

View File

@ -281,6 +281,15 @@
"custom_background": { "custom_background": {
"message": "Benutzerdefinierter Hintergrund" "message": "Benutzerdefinierter Hintergrund"
}, },
"solid_color": {
"message": "Einfarbiger Hintergrund"
},
"solid_color_hex": {
"message": "#1e2a38"
},
"bg_transparency": {
"message": "Transparente Elemente"
},
"custom_background_desc": { "custom_background_desc": {
"message": "Wähle eine Voreinstellung oder verwende deine eigene Bild-URL." "message": "Wähle eine Voreinstellung oder verwende deine eigene Bild-URL."
}, },

View File

@ -281,6 +281,15 @@
"custom_background": { "custom_background": {
"message": "Custom Background" "message": "Custom Background"
}, },
"solid_color": {
"message": "Solid color"
},
"solid_color_hex": {
"message": "#1e2a38"
},
"bg_transparency": {
"message": "Transparent elements"
},
"custom_background_desc": { "custom_background_desc": {
"message": "Pick a preset or use your own image url." "message": "Pick a preset or use your own image url."
}, },

View File

@ -281,6 +281,15 @@
"custom_background": { "custom_background": {
"message": "Fondo personalizado" "message": "Fondo personalizado"
}, },
"solid_color": {
"message": "Color sólido"
},
"solid_color_hex": {
"message": "#1e2a38"
},
"bg_transparency": {
"message": "Elementos transparentes"
},
"custom_background_desc": { "custom_background_desc": {
"message": "Elige un preajuste o usa la URL de tu propia imagen." "message": "Elige un preajuste o usa la URL de tu propia imagen."
}, },

View File

@ -281,6 +281,15 @@
"custom_background": { "custom_background": {
"message": "Arrière-plan personnalisé" "message": "Arrière-plan personnalisé"
}, },
"solid_color": {
"message": "Couleur unie"
},
"solid_color_hex": {
"message": "#1e2a38"
},
"bg_transparency": {
"message": "Éléments transparents"
},
"custom_background_desc": { "custom_background_desc": {
"message": "Choisissez un préréglage ou utilisez l'URL de votre propre image." "message": "Choisissez un préréglage ou utilisez l'URL de votre propre image."
}, },

View File

@ -281,6 +281,15 @@
"custom_background": { "custom_background": {
"message": "Sfondo personalizzato" "message": "Sfondo personalizzato"
}, },
"solid_color": {
"message": "Colore pieno"
},
"solid_color_hex": {
"message": "#1e2a38"
},
"bg_transparency": {
"message": "Elementi trasparenti"
},
"custom_background_desc": { "custom_background_desc": {
"message": "Scegli un preset o usa l'URL della tua immagine." "message": "Scegli un preset o usa l'URL della tua immagine."
}, },

View File

@ -281,6 +281,15 @@
"custom_background": { "custom_background": {
"message": "カスタム背景" "message": "カスタム背景"
}, },
"solid_color": {
"message": "単色"
},
"solid_color_hex": {
"message": "#1e2a38"
},
"bg_transparency": {
"message": "要素を透過"
},
"custom_background_desc": { "custom_background_desc": {
"message": "プリセットを選ぶか、自分の画像URLを使用してください。" "message": "プリセットを選ぶか、自分の画像URLを使用してください。"
}, },

View File

@ -281,6 +281,15 @@
"custom_background": { "custom_background": {
"message": "Fundo personalizado" "message": "Fundo personalizado"
}, },
"solid_color": {
"message": "Cor sólida"
},
"solid_color_hex": {
"message": "#1e2a38"
},
"bg_transparency": {
"message": "Elementos transparentes"
},
"custom_background_desc": { "custom_background_desc": {
"message": "Escolhe uma predefinição ou usa o URL da tua própria imagem." "message": "Escolhe uma predefinição ou usa o URL da tua própria imagem."
}, },

View File

@ -281,6 +281,15 @@
"custom_background": { "custom_background": {
"message": "Свой фон" "message": "Свой фон"
}, },
"solid_color": {
"message": "Сплошной цвет"
},
"solid_color_hex": {
"message": "#1e2a38"
},
"bg_transparency": {
"message": "Прозрачные элементы"
},
"custom_background_desc": { "custom_background_desc": {
"message": "Выберите пресет или используйте свою ссылку на изображение." "message": "Выберите пресет или используйте свою ссылку на изображение."
}, },

View File

@ -281,6 +281,15 @@
"custom_background": { "custom_background": {
"message": "Anpassad bakgrund" "message": "Anpassad bakgrund"
}, },
"solid_color": {
"message": "Enfärgad"
},
"solid_color_hex": {
"message": "#1e2a38"
},
"bg_transparency": {
"message": "Genomskinliga element"
},
"custom_background_desc": { "custom_background_desc": {
"message": "Välj en förinställning eller använd din egen bild-URL." "message": "Välj en förinställning eller använd din egen bild-URL."
}, },

View File

@ -281,6 +281,15 @@
"custom_background": { "custom_background": {
"message": "自定义背景" "message": "自定义背景"
}, },
"solid_color": {
"message": "纯色"
},
"solid_color_hex": {
"message": "#1e2a38"
},
"bg_transparency": {
"message": "元素透明"
},
"custom_background_desc": { "custom_background_desc": {
"message": "选择预设,或使用你自己的图片 URL。" "message": "选择预设,或使用你自己的图片 URL。"
}, },

View File

@ -281,6 +281,15 @@
"custom_background": { "custom_background": {
"message": "自訂背景" "message": "自訂背景"
}, },
"solid_color": {
"message": "純色"
},
"solid_color_hex": {
"message": "#1e2a38"
},
"bg_transparency": {
"message": "元素透明"
},
"custom_background_desc": { "custom_background_desc": {
"message": "選擇預設背景,或使用你自己的圖片網址。" "message": "選擇預設背景,或使用你自己的圖片網址。"
}, },

View File

@ -179,6 +179,11 @@ h2 {margin-top: 20px;font-weight:600}
.theme-button-creator {font-size:10px;white-space:nowrap} .theme-button-creator {font-size:10px;white-space:nowrap}
.theme-button-title {font-weight: 600;font-size:12px;white-space:nowrap;} .theme-button-title {font-weight: 600;font-size:12px;white-space:nowrap;}
.background-preset-grid {display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; margin-top: 10px;} .background-preset-grid {display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; margin-top: 10px;}
/* a solid colour outranks the presets, so they are inert until it is cleared */
.background-preset-grid.is-disabled {opacity: .45; filter: grayscale(1); pointer-events: none;}
.background-color-picker {width: 38px; height: 30px; padding: 0; border: none; border-radius: 7px; background: var(--inputbg); cursor: pointer;}
.background-color-picker::-webkit-color-swatch-wrapper {padding: 2px;}
.background-color-picker::-webkit-color-swatch {border: none; border-radius: 5px;}
.background-preset-card {min-height: 96px; display: flex; flex-direction: column; justify-content: flex-end; align-items: flex-start; border-radius: 8px;} .background-preset-card {min-height: 96px; display: flex; flex-direction: column; justify-content: flex-end; align-items: flex-start; border-radius: 8px;}
.background-preset-card.selected {outline: 2px solid #56Caf0; outline-offset: 2px;} .background-preset-card.selected {outline: 2px solid #56Caf0; outline-offset: 2px;}
.background-preset-scale {font-size:10px;white-space:nowrap;opacity:0.9;} .background-preset-scale {font-size:10px;white-space:nowrap;opacity:0.9;}

View File

@ -1485,6 +1485,22 @@
<input type="range" min="50" max="200" step="1" id="customBackgroundScale"> <input type="range" min="50" max="200" step="1" id="customBackgroundScale">
</div> </div>
<input style="width:85%;" class="card-input" id="customBackgroundLink" placeholder="https://..."> <input style="width:85%;" class="card-input" id="customBackgroundLink" placeholder="https://...">
<!-- Solid colour: when set it wins over the presets and the image URL -->
<div id="customBackgroundSolidRow" style="margin-top:12px;">
<span class="sub-text" style="font-weight:600; color:#e2e2e2;" data-i18n="solid_color">Solid color</span>
<div style="display:flex;align-items:center;gap:8px;margin-top:6px;">
<input type="color" id="customBackgroundColorPicker" class="background-color-picker" value="#1e2a38" data-i18n-title="solid_color" title="Solid color">
<input class="card-input" id="customBackgroundColorHex" maxlength="7" style="width:110px;margin:0;" data-i18n-placeholder="solid_color_hex" placeholder="#1e2a38">
<button type="button" class="opacity-reset-btn" id="clearCustomBackgroundColor" title="Clear" data-i18n-title="clear_background">
<svg viewBox="0 0 24 24" width="11" height="11" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
</button>
</div>
</div>
<!-- Only meaningful with a colour set -->
<div class="sub-option" id="customBackgroundColorTransparencyRow" style="margin-top:8px;display:none;">
<input type="checkbox" id="customBackgroundColorTransparent" name="customBackgroundColorTransparent">
<label for="customBackgroundColorTransparent" class="sub-text" data-i18n="bg_transparency">Background transparency</label>
</div>
<button id="clearCustomBackground" class="big-button" style="margin-top: 8px;" data-i18n="clear_background">Clear Background</button> <button id="clearCustomBackground" class="big-button" style="margin-top: 8px;" data-i18n="clear_background">Clear Background</button>
</div> </div>
<div class="sub-option" style="margin-top:10px;"> <div class="sub-option" style="margin-top:10px;">

View File

@ -150,6 +150,8 @@ chrome.runtime.onInstalled.addListener(function () {
"cardHeight": 146, "cardHeight": 146,
"customCardStyles": false, "customCardStyles": false,
"customBackgroundLink": "", "customBackgroundLink": "",
"customBackgroundColor": "",
"customBackgroundColorTransparent": false,
"customBackgroundScale": 100, "customBackgroundScale": 100,
"customBackgroundDaily": false, "customBackgroundDaily": false,
"customBackgroundNasaDaily": false, "customBackgroundNasaDaily": false,

View File

@ -492,8 +492,16 @@ async function getActiveCustomBackground() {
"customBackgroundNasaDaily", "customBackgroundNasaDaily",
"customBackgroundLink", "customBackgroundLink",
"customBackgroundScale", "customBackgroundScale",
"customBackgroundColor",
"customBackgroundColorTransparent",
]); ]);
// A solid colour always wins over the image sources.
const color = normalizeBackgroundColor(syncOpts.customBackgroundColor);
if (color) {
return { color, transparent: syncOpts.customBackgroundColorTransparent === true };
}
if (syncOpts.customBackgroundNasaDaily === true) { if (syncOpts.customBackgroundNasaDaily === true) {
return await getNasaDailyBackground(); return await getNasaDailyBackground();
} }
@ -518,6 +526,16 @@ async function getActiveCustomBackground() {
return null; return null;
} }
// Accepts "#1e2a38", "1e2a38" or "#abc"; anything else is "no colour".
function normalizeBackgroundColor(value) {
const hex = String(value || "").trim().replace(/^#/, "");
if (/^[0-9a-f]{3}$/i.test(hex)) {
return "#" + hex.split("").map(c => c + c).join("").toLowerCase();
}
if (/^[0-9a-f]{6}$/i.test(hex)) return "#" + hex.toLowerCase();
return "";
}
async function getDailyBackgroundPreset() { async function getDailyBackgroundPreset() {
const today = new Date(); const today = new Date();
const dateStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`; const dateStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`;
@ -1431,6 +1449,8 @@ function applyOptionsChanges(changes) {
debouncedApplyAestheticChanges(); debouncedApplyAestheticChanges();
break; break;
case "customBackgroundLink": case "customBackgroundLink":
case "customBackgroundColor":
case "customBackgroundColorTransparent":
applyCustomBackground(); applyCustomBackground();
break; break;
case "bg_opacity": case "bg_opacity":
@ -1583,19 +1603,30 @@ async function applyCustomBackground() {
const cardOpacity = Math.max(0, Math.min(100, Number(options.card_opacity ?? 80))); const cardOpacity = Math.max(0, Math.min(100, Number(options.card_opacity ?? 80)));
const cardBlur = Math.max(0, Math.min(30, Number(options.card_blur ?? 8))); const cardBlur = Math.max(0, Math.min(30, Number(options.card_blur ?? 8)));
const cardTransparent = 100 - cardOpacity; const cardTransparent = 100 - cardOpacity;
style.textContent = ` // Solid colour: one flat fill, no image rules. The transparency opt-in is not
#wrapper { // about the colour — it decides whether the page's surfaces turn see-through over
background-image: url(${backgroundUrl}) !important; // it (the glass treatment an image background always gets).
const solid = activeBackground.color || "";
const surfaceTransparent = solid
? (activeBackground.transparent ? bgTransparent : 0)
: bgTransparent;
const wrapperBackground = solid
? `background-image: none !important;
background-color: ${solid} !important;`
: `background-image: url(${backgroundUrl}) !important;
background-repeat: no-repeat !important; background-repeat: no-repeat !important;
background-position: center center !important; background-position: center center !important;
background-attachment: fixed !important; background-attachment: fixed !important;`;
style.textContent = `
#wrapper {
${wrapperBackground}
} }
@media (orientation: landscape) { ${solid ? "" : `@media (orientation: landscape) {
#wrapper { background-size: ${fitToScreen ? 'cover' : backgroundScale + '% auto'} !important; } #wrapper { background-size: ${fitToScreen ? 'cover' : backgroundScale + '% auto'} !important; }
} }
@media (orientation: portrait) { @media (orientation: portrait) {
#wrapper { background-size: cover !important; } #wrapper { background-size: cover !important; }
} }`}
.ic-Dashboard-header__layout { .ic-Dashboard-header__layout {
background: none !important; background: none !important;
border-radius: 5px; border-radius: 5px;
@ -1605,7 +1636,7 @@ async function applyCustomBackground() {
margin-left: -35px !important; margin-left: -35px !important;
margin-right: -35px !important; margin-right: -35px !important;
box-sizing: border-box !important; box-sizing: border-box !important;
background-color: color-mix(in srgb, var(--bcbackground-0), transparent ${bgTransparent}%) !important; background-color: color-mix(in srgb, var(--bcbackground-0), transparent ${surfaceTransparent}%) !important;
border: 1px solid color-mix(in srgb, var(--bcborders) 60%, transparent) !important; border: 1px solid color-mix(in srgb, var(--bcborders) 60%, transparent) !important;
border-radius: 10px !important; border-radius: 10px !important;
position: sticky !important; position: sticky !important;
@ -1617,7 +1648,7 @@ async function applyCustomBackground() {
/* glass day groups so a background image shows through between day cards */ /* glass day groups so a background image shows through between day cards */
#dashboard-planner .planner-day, #dashboard-planner .planner-day,
#dashboard-planner .planner-empty-days { #dashboard-planner .planner-empty-days {
background: color-mix(in srgb, var(--bcbackground-0), transparent ${bgTransparent}%) !important; background: color-mix(in srgb, var(--bcbackground-0), transparent ${surfaceTransparent}%) !important;
backdrop-filter: blur(${bgBlur}px) !important; backdrop-filter: blur(${bgBlur}px) !important;
-webkit-backdrop-filter: blur(${bgBlur}px) !important; -webkit-backdrop-filter: blur(${bgBlur}px) !important;
border-radius: 12px !important; border-radius: 12px !important;
@ -1644,14 +1675,14 @@ async function applyCustomBackground() {
#right-side-wrapper { #right-side-wrapper {
backdrop-filter: blur(${bgBlur}px) !important; backdrop-filter: blur(${bgBlur}px) !important;
-webkit-backdrop-filter: blur(${bgBlur}px) !important; -webkit-backdrop-filter: blur(${bgBlur}px) !important;
background-color: color-mix(in srgb, var(--bcbackground-0), transparent ${bgTransparent}%); background-color: color-mix(in srgb, var(--bcbackground-0), transparent ${surfaceTransparent}%);
border-radius: 5px; border-radius: 5px;
} }
/* tint the whole #left-side column (the inner frame only wraps the links) */ /* tint the whole #left-side column (the inner frame only wraps the links) */
#left-side { #left-side {
backdrop-filter: blur(${bgBlur}px) !important; backdrop-filter: blur(${bgBlur}px) !important;
-webkit-backdrop-filter: blur(${bgBlur}px) !important; -webkit-backdrop-filter: blur(${bgBlur}px) !important;
background-color: color-mix(in srgb, var(--bcbackground-0), transparent ${bgTransparent}%) !important; background-color: color-mix(in srgb, var(--bcbackground-0), transparent ${surfaceTransparent}%) !important;
} }
/* #right-side sub-text keeps Canvas gray in light mode; recolor for contrast */ /* #right-side sub-text keeps Canvas gray in light mode; recolor for contrast */
.recent_feedback .event-details { .recent_feedback .event-details {
@ -1736,14 +1767,14 @@ async function applyCustomBackground() {
border-radius: 0 !important; border-radius: 0 !important;
} }
#assignments.ui-tabs-panel { #assignments.ui-tabs-panel {
background-color: color-mix(in srgb, var(--bcbackground-0), transparent ${bgTransparent}%) !important; background-color: color-mix(in srgb, var(--bcbackground-0), transparent ${surfaceTransparent}%) !important;
backdrop-filter: blur(${bgBlur}px) !important; backdrop-filter: blur(${bgBlur}px) !important;
-webkit-backdrop-filter: blur(${bgBlur}px) !important; -webkit-backdrop-filter: blur(${bgBlur}px) !important;
border-radius: 5px !important; border-radius: 5px !important;
} }
/* InstUI flex View ships an opaque surface; give it the glass treatment */ /* InstUI flex View ships an opaque surface; give it the glass treatment */
.css-fiozi7-view--flex-flex { .css-fiozi7-view--flex-flex {
background-color: color-mix(in srgb, var(--bcbackground-0), transparent ${bgTransparent}%) !important; background-color: color-mix(in srgb, var(--bcbackground-0), transparent ${surfaceTransparent}%) !important;
backdrop-filter: blur(${bgBlur}px) !important; backdrop-filter: blur(${bgBlur}px) !important;
-webkit-backdrop-filter: blur(${bgBlur}px) !important; -webkit-backdrop-filter: blur(${bgBlur}px) !important;
border-radius: 5px !important; border-radius: 5px !important;
@ -1770,7 +1801,7 @@ async function applyCustomBackground() {
} }
/* threads toolbar sits under the header panel; same glass treatment */ /* threads toolbar sits under the header panel; same glass treatment */
.css-1n94jrf-view { .css-1n94jrf-view {
background-color: color-mix(in srgb, var(--bcbackground-0), transparent ${bgTransparent}%) !important; background-color: color-mix(in srgb, var(--bcbackground-0), transparent ${surfaceTransparent}%) !important;
backdrop-filter: blur(${bgBlur}px) !important; backdrop-filter: blur(${bgBlur}px) !important;
-webkit-backdrop-filter: blur(${bgBlur}px) !important; -webkit-backdrop-filter: blur(${bgBlur}px) !important;
border-radius: 5px !important; border-radius: 5px !important;
@ -1786,7 +1817,7 @@ async function applyCustomBackground() {
#content { #content {
margin: 36px 48px 48px !important; margin: 36px 48px 48px !important;
padding: 10px !important; padding: 10px !important;
background-color: color-mix(in srgb, var(--bcbackground-0), transparent ${bgTransparent}%) !important; background-color: color-mix(in srgb, var(--bcbackground-0), transparent ${surfaceTransparent}%) !important;
backdrop-filter: blur(${bgBlur}px) !important; backdrop-filter: blur(${bgBlur}px) !important;
-webkit-backdrop-filter: blur(${bgBlur}px) !important; -webkit-backdrop-filter: blur(${bgBlur}px) !important;
border-radius: 5px !important; border-radius: 5px !important;
@ -1797,7 +1828,7 @@ async function applyCustomBackground() {
#content { #content {
margin: 36px 48px 48px !important; margin: 36px 48px 48px !important;
padding: 10px !important; padding: 10px !important;
background-color: color-mix(in srgb, var(--bcbackground-0), transparent ${bgTransparent}%) !important; background-color: color-mix(in srgb, var(--bcbackground-0), transparent ${surfaceTransparent}%) !important;
backdrop-filter: blur(${bgBlur}px) !important; backdrop-filter: blur(${bgBlur}px) !important;
-webkit-backdrop-filter: blur(${bgBlur}px) !important; -webkit-backdrop-filter: blur(${bgBlur}px) !important;
border-radius: 5px !important; border-radius: 5px !important;
@ -1809,7 +1840,7 @@ async function applyCustomBackground() {
.ic-Layout-contentMain { .ic-Layout-contentMain {
margin: 26px 38px 38px !important; margin: 26px 38px 38px !important;
padding: 10px !important; padding: 10px !important;
background-color: color-mix(in srgb, var(--bcbackground-0), transparent ${bgTransparent}%) !important; background-color: color-mix(in srgb, var(--bcbackground-0), transparent ${surfaceTransparent}%) !important;
backdrop-filter: blur(${bgBlur}px) !important; backdrop-filter: blur(${bgBlur}px) !important;
-webkit-backdrop-filter: blur(${bgBlur}px) !important; -webkit-backdrop-filter: blur(${bgBlur}px) !important;
border-radius: 10px !important; border-radius: 10px !important;
@ -1822,7 +1853,7 @@ async function applyCustomBackground() {
it" signal -- and without it the tint above never lands, leaving dark it" signal -- and without it the tint above never lands, leaving dark
text straight on the background image. */ text straight on the background image. */
#content:not(.ic-Layout-contentMain) { #content:not(.ic-Layout-contentMain) {
background-color: color-mix(in srgb, var(--bcbackground-0), transparent ${bgTransparent}%) !important; background-color: color-mix(in srgb, var(--bcbackground-0), transparent ${surfaceTransparent}%) !important;
backdrop-filter: blur(${bgBlur}px) !important; backdrop-filter: blur(${bgBlur}px) !important;
-webkit-backdrop-filter: blur(${bgBlur}px) !important; -webkit-backdrop-filter: blur(${bgBlur}px) !important;
border-radius: 10px !important; border-radius: 10px !important;
@ -1838,7 +1869,7 @@ async function applyCustomBackground() {
} }
${isConversationsPage() ? ` ${isConversationsPage() ? `
.css-1nh4pc4-view-flexItem { .css-1nh4pc4-view-flexItem {
background-color: color-mix(in srgb, var(--bcbackground-0), transparent ${bgTransparent}%) !important; background-color: color-mix(in srgb, var(--bcbackground-0), transparent ${surfaceTransparent}%) !important;
backdrop-filter: blur(${bgBlur}px) !important; backdrop-filter: blur(${bgBlur}px) !important;
-webkit-backdrop-filter: blur(${bgBlur}px) !important; -webkit-backdrop-filter: blur(${bgBlur}px) !important;
border-radius: 5px !important; border-radius: 5px !important;

View File

@ -219,7 +219,7 @@ const exportLayout = ["full_width", "center_cards", "condensed_cards", "equal_he
const exportSidebar = ["better_sidebar", "sidebar_scale"]; const exportSidebar = ["better_sidebar", "sidebar_scale"];
const exportTodo = ["better_todo", "todo_hide_feedback", "todo_hide_read", "todo_full_height", "todo_celebrations_enabled", "todo_celebrations", "todo_celebration_amount", "todo_progress_rings", "todo_timeframe", "todo_timeframe_custom_past", "todo_timeframe_custom_future", "todo_hr24", "todo_separate_scrollbar", "todo_alternate_colors", "todo_ignore_card_colors", "todo_remove_icons", "todo_show_scores", "hover_preview"]; const exportTodo = ["better_todo", "todo_hide_feedback", "todo_hide_read", "todo_full_height", "todo_celebrations_enabled", "todo_celebrations", "todo_celebration_amount", "todo_progress_rings", "todo_timeframe", "todo_timeframe_custom_past", "todo_timeframe_custom_future", "todo_hr24", "todo_separate_scrollbar", "todo_alternate_colors", "todo_ignore_card_colors", "todo_remove_icons", "todo_show_scores", "hover_preview"];
const exportGpa = ["gpa_calc", "gpa_calc_prepend", "gpa_calc_cumulative", "gpa_calc_weighted", "gpa_calc_show_on_hover"]; const exportGpa = ["gpa_calc", "gpa_calc_prepend", "gpa_calc_cumulative", "gpa_calc_weighted", "gpa_calc_show_on_hover"];
const exportBackground = ["customBackgroundLink", "customBackgroundScale", "customBackgroundDaily", "customBackgroundNasaDaily", "fitImageToScreen", "card_transparency", "bg_opacity", "sidebar_opacity", "bg_blur", "sidebar_blur", "card_opacity", "card_blur"]; const exportBackground = ["customBackgroundLink", "customBackgroundScale", "customBackgroundDaily", "customBackgroundNasaDaily", "customBackgroundColor", "customBackgroundColorTransparent", "fitImageToScreen", "card_transparency", "bg_opacity", "sidebar_opacity", "bg_blur", "sidebar_blur", "card_opacity", "card_blur"];
// Master "On/off toggles": every visual toggle, minus GPA/schedule/productivity. // Master "On/off toggles": every visual toggle, minus GPA/schedule/productivity.
const exportToggles = ["dark_mode", "quiz_safe_mode", "grade_analytics", "grade_analytics_zones"].concat(exportCardColorToggles, exportLayout, exportSidebar, exportTodo); const exportToggles = ["dark_mode", "quiz_safe_mode", "grade_analytics", "grade_analytics_zones"].concat(exportCardColorToggles, exportLayout, exportSidebar, exportTodo);
const fontsDropdownStateKey = "fonts_dropdown_open"; const fontsDropdownStateKey = "fonts_dropdown_open";
@ -401,6 +401,8 @@ const defaultOptions = {
"card_grid_flex": false, "card_grid_flex": false,
"customCardStyles": false, "customCardStyles": false,
"customBackgroundLink": "", "customBackgroundLink": "",
"customBackgroundColor": "",
"customBackgroundColorTransparent": false,
"customBackgroundScale": 100, "customBackgroundScale": 100,
"customBackgroundDaily": false, "customBackgroundDaily": false,
"customBackgroundNasaDaily": false, "customBackgroundNasaDaily": false,
@ -1106,6 +1108,73 @@ function setupCustomBackgroundLink(initial) {
}); });
} }
// Accepts "#1e2a38", "1e2a38" or "#abc"; anything else means "no colour".
function normalizeBackgroundColor(value) {
const hex = String(value || "").trim().replace(/^#/, "");
if (/^[0-9a-f]{3}$/i.test(hex)) return "#" + hex.split("").map(c => c + c).join("").toLowerCase();
if (/^[0-9a-f]{6}$/i.test(hex)) return "#" + hex.toLowerCase();
return "";
}
function currentBackgroundColor() {
return normalizeBackgroundColor(document.querySelector("#customBackgroundColorHex")?.value || "");
}
// Solid colour area: hex field + picker, either one sets it. The colour outranks the
// presets and the image URL, so those are greyed out while it is set.
function setupCustomBackgroundColor(initial) {
const hexEl = document.querySelector("#customBackgroundColorHex");
const picker = document.querySelector("#customBackgroundColorPicker");
const clearBtn = document.querySelector("#clearCustomBackgroundColor");
const transparency = document.querySelector("#customBackgroundColorTransparent");
if (!hexEl || !picker) return;
const initialColor = normalizeBackgroundColor(initial);
if (initialColor) {
hexEl.value = initialColor;
picker.value = initialColor;
}
chrome.storage.sync.get("customBackgroundColorTransparent", sync => {
if (transparency) transparency.checked = sync.customBackgroundColorTransparent === true;
});
const commit = (raw, { writeField = false } = {}) => {
const color = normalizeBackgroundColor(raw);
if (color) {
if (writeField || !hexEl.value) hexEl.value = color;
picker.value = color;
} else if (writeField) {
hexEl.value = "";
}
chrome.storage.sync.set({ "customBackgroundColor": color });
syncSolidBackgroundState();
toggleOpacityOptions();
};
hexEl.addEventListener("input", (e) => commit(e.target.value));
hexEl.addEventListener("blur", (e) => commit(e.target.value, { writeField: true }));
picker.addEventListener("input", (e) => commit(e.target.value, { writeField: true }));
clearBtn?.addEventListener("click", () => {
hexEl.value = "";
chrome.storage.sync.set({ "customBackgroundColor": "" });
syncSolidBackgroundState();
toggleOpacityOptions();
});
transparency?.addEventListener("change", function () {
chrome.storage.sync.set({ "customBackgroundColorTransparent": this.checked });
});
syncSolidBackgroundState();
}
// Grey the presets out while a colour is set, and only offer transparency with one.
function syncSolidBackgroundState() {
const hasColor = currentBackgroundColor() !== "";
const presets = document.getElementById("background-presets");
if (presets) presets.classList.toggle("is-disabled", hasColor);
const transparencyRow = document.getElementById("customBackgroundColorTransparencyRow");
if (transparencyRow) transparencyRow.style.display = hasColor ? "flex" : "none";
}
function setupCustomBackgroundScale(initial) { function setupCustomBackgroundScale(initial) {
const el = document.querySelector("#customBackgroundScale"); const el = document.querySelector("#customBackgroundScale");
const output = document.querySelector("#customBackgroundScaleValue"); const output = document.querySelector("#customBackgroundScaleValue");
@ -1139,7 +1208,7 @@ function syncCustomBackgroundDailyState(isDaily) {
manualControls.style.filter = isDaily ? "grayscale(1)" : "none"; manualControls.style.filter = isDaily ? "grayscale(1)" : "none";
} }
["customBackgroundLink", "customBackgroundScale", "clearCustomBackground"].forEach(id => { ["customBackgroundLink", "customBackgroundScale", "clearCustomBackground", "customBackgroundColorHex", "customBackgroundColorPicker", "clearCustomBackgroundColor"].forEach(id => {
const el = document.getElementById(id); const el = document.getElementById(id);
if (el) el.disabled = isDaily; if (el) el.disabled = isDaily;
}); });
@ -1152,6 +1221,7 @@ function syncCustomBackgroundDailyState(isDaily) {
} }
function renderBackgroundPresetSelection() { function renderBackgroundPresetSelection() {
syncSolidBackgroundState();
const isDaily = document.querySelector("#customBackgroundDaily")?.checked === true || document.querySelector("#customBackgroundNasaDaily")?.checked === true; const isDaily = document.querySelector("#customBackgroundDaily")?.checked === true || document.querySelector("#customBackgroundNasaDaily")?.checked === true;
const dailyPreset = isDaily ? getDailyBackgroundPreset() : null; const dailyPreset = isDaily ? getDailyBackgroundPreset() : null;
const currentLink = isDaily ? (dailyPreset?.url || "") : (document.querySelector("#customBackgroundLink")?.value || ""); const currentLink = isDaily ? (dailyPreset?.url || "") : (document.querySelector("#customBackgroundLink")?.value || "");
@ -1211,7 +1281,7 @@ function customBackgroundActive() {
const link = document.querySelector("#customBackgroundLink"); const link = document.querySelector("#customBackgroundLink");
const daily = document.querySelector("#customBackgroundDaily"); const daily = document.querySelector("#customBackgroundDaily");
const nasa = document.querySelector("#customBackgroundNasaDaily"); const nasa = document.querySelector("#customBackgroundNasaDaily");
return (link && link.value.trim() !== "") || (daily && daily.checked) || (nasa && nasa.checked); return (currentBackgroundColor() !== "") || (link && link.value.trim() !== "") || (daily && daily.checked) || (nasa && nasa.checked);
} }
function toggleOpacityOptions() { function toggleOpacityOptions() {
@ -2127,6 +2197,10 @@ async function setup() {
{ {
identifier: "customBackgroundScale", identifier: "customBackgroundScale",
setup: (initial) => setupCustomBackgroundScale(initial), setup: (initial) => setupCustomBackgroundScale(initial),
},
{
identifier: "customBackgroundColor",
setup: (initial) => setupCustomBackgroundColor(initial),
}, },
{ {
identifier: "todo_celebrations", identifier: "todo_celebrations",
@ -2795,8 +2869,10 @@ async function setup() {
document.getElementById("clearCustomBackground").addEventListener("click", () => { document.getElementById("clearCustomBackground").addEventListener("click", () => {
chrome.storage.sync.set({ "customBackgroundLink": "", "customBackgroundScale": 100 }); chrome.storage.sync.set({ "customBackgroundLink": "", "customBackgroundScale": 100, "customBackgroundColor": "" });
document.querySelector("#customBackgroundLink").value = ""; document.querySelector("#customBackgroundLink").value = "";
document.querySelector("#customBackgroundColorHex").value = "";
syncSolidBackgroundState();
document.querySelector("#customBackgroundScale").value = 100; document.querySelector("#customBackgroundScale").value = 100;
document.querySelector("#customBackgroundScaleValue").textContent = "100%"; document.querySelector("#customBackgroundScaleValue").textContent = "100%";
renderBackgroundPresetSelection(); renderBackgroundPresetSelection();