mirror of
https://github.com/GuySandler/CanvasRefined.git
synced 2026-11-03 22:07:18 +01:00
card grid and custom styles to widgets
This commit is contained in:
parent
189783febd
commit
d0d3ba4de2
@ -578,9 +578,6 @@
|
||||
"flexible_grid": {
|
||||
"message": "Flexibles Raster"
|
||||
},
|
||||
"flexible_grid_on": {
|
||||
"message": "Das flexible Raster ist aktiv – Karten umbrechen, um in schmale Fenster zu passen, statt abgeschnitten zu werden."
|
||||
},
|
||||
"focus_timer": {
|
||||
"message": "Fokus-Timer"
|
||||
},
|
||||
@ -860,8 +857,8 @@
|
||||
"gradient_desc": {
|
||||
"message": "Wähle 2 Farben, um einen Verlauf zwischen ihnen zu erstellen:"
|
||||
},
|
||||
"grid_includes_gpa": {
|
||||
"message": " (einschließlich des GPA-Rechners)"
|
||||
"grid_includes_widgets": {
|
||||
"message": " (einschließlich {{NAMES}})"
|
||||
},
|
||||
"grid_warning": {
|
||||
"message": "Dein Raster bietet Platz für {{TOTAL}} Karte{{TOTAL_S}}, aber du hast {{ACTIVE}} Karte{{ACTIVE_S}}{{GPA_NOTE}} – einige Karten werden möglicherweise abgeschnitten."
|
||||
|
||||
@ -578,9 +578,6 @@
|
||||
"flexible_grid": {
|
||||
"message": "Flexible grid"
|
||||
},
|
||||
"flexible_grid_on": {
|
||||
"message": "Flexible grid is on - cards wrap to fit narrow windows instead of being cut off."
|
||||
},
|
||||
"focus_timer": {
|
||||
"message": "Focus Timer"
|
||||
},
|
||||
@ -863,8 +860,8 @@
|
||||
"gradient_desc": {
|
||||
"message": "Choose 2 colors to create a gradient between them:"
|
||||
},
|
||||
"grid_includes_gpa": {
|
||||
"message": " (including the GPA calculator)"
|
||||
"grid_includes_widgets": {
|
||||
"message": " (including {{NAMES}})"
|
||||
},
|
||||
"grid_warning": {
|
||||
"message": "Your grid holds {{TOTAL}} card{{TOTAL_S}} but you have {{ACTIVE}} card{{ACTIVE_S}}{{GPA_NOTE}} - some cards may be cut off."
|
||||
|
||||
@ -578,9 +578,6 @@
|
||||
"flexible_grid": {
|
||||
"message": "Cuadrícula flexible"
|
||||
},
|
||||
"flexible_grid_on": {
|
||||
"message": "La cuadrícula flexible está activada: las tarjetas se ajustan al ancho de las ventanas estrechas en lugar de quedar cortadas."
|
||||
},
|
||||
"focus_timer": {
|
||||
"message": "Temporizador de enfoque"
|
||||
},
|
||||
@ -860,8 +857,8 @@
|
||||
"gradient_desc": {
|
||||
"message": "Elige 2 colores para crear un degradado entre ellos:"
|
||||
},
|
||||
"grid_includes_gpa": {
|
||||
"message": " (incluida la calculadora de GPA)"
|
||||
"grid_includes_widgets": {
|
||||
"message": " (incluidos {{NAMES}})"
|
||||
},
|
||||
"grid_warning": {
|
||||
"message": "Tu cuadrícula admite {{TOTAL}} tarjeta{{TOTAL_S}} pero tienes {{ACTIVE}} tarjeta{{ACTIVE_S}}{{GPA_NOTE}} - algunas tarjetas pueden quedar cortadas."
|
||||
|
||||
@ -578,9 +578,6 @@
|
||||
"flexible_grid": {
|
||||
"message": "Grille flexible"
|
||||
},
|
||||
"flexible_grid_on": {
|
||||
"message": "La grille flexible est active — les cartes s'enroulent pour s'adapter aux fenêtres étroites au lieu d'être coupées."
|
||||
},
|
||||
"focus_timer": {
|
||||
"message": "Minuteur de concentration"
|
||||
},
|
||||
@ -860,8 +857,8 @@
|
||||
"gradient_desc": {
|
||||
"message": "Choisissez 2 couleurs pour créer un dégradé entre elles :"
|
||||
},
|
||||
"grid_includes_gpa": {
|
||||
"message": " (y compris le calculateur GPA)"
|
||||
"grid_includes_widgets": {
|
||||
"message": " (y compris {{NAMES}})"
|
||||
},
|
||||
"grid_warning": {
|
||||
"message": "Votre grille contient {{TOTAL}} carte{{TOTAL_S}} mais vous avez {{ACTIVE}} carte{{ACTIVE_S}}{{GPA_NOTE}} — certaines cartes peuvent être coupées."
|
||||
|
||||
@ -578,9 +578,6 @@
|
||||
"flexible_grid": {
|
||||
"message": "Griglia flessibile"
|
||||
},
|
||||
"flexible_grid_on": {
|
||||
"message": "La griglia flessibile è attiva: le card si adattano alle finestre strette invece di essere tagliate."
|
||||
},
|
||||
"focus_timer": {
|
||||
"message": "Timer di concentrazione"
|
||||
},
|
||||
@ -860,8 +857,8 @@
|
||||
"gradient_desc": {
|
||||
"message": "Scegli 2 colori per creare una sfumatura tra loro:"
|
||||
},
|
||||
"grid_includes_gpa": {
|
||||
"message": " (incluso il calcolatore GPA)"
|
||||
"grid_includes_widgets": {
|
||||
"message": " (inclusi {{NAMES}})"
|
||||
},
|
||||
"grid_warning": {
|
||||
"message": "La tua griglia contiene {{TOTAL}} card{{TOTAL_S}} ma ne hai {{ACTIVE}} card{{ACTIVE_S}}{{GPA_NOTE}} - alcune card potrebbero essere tagliate."
|
||||
|
||||
@ -578,9 +578,6 @@
|
||||
"flexible_grid": {
|
||||
"message": "フレキシブルグリッド"
|
||||
},
|
||||
"flexible_grid_on": {
|
||||
"message": "フレキシブルグリッドが有効です。カードは切れずに折り返して狭いウィンドウに収まります。"
|
||||
},
|
||||
"focus_timer": {
|
||||
"message": "集中タイマー"
|
||||
},
|
||||
@ -860,8 +857,8 @@
|
||||
"gradient_desc": {
|
||||
"message": "2色を選んでグラデーションを作成します:"
|
||||
},
|
||||
"grid_includes_gpa": {
|
||||
"message": " (GPA計算を含む)"
|
||||
"grid_includes_widgets": {
|
||||
"message": " ({{NAMES}}を含む)"
|
||||
},
|
||||
"grid_warning": {
|
||||
"message": "グリッドには{{TOTAL}}枚のカード{{TOTAL_S}}が入りますが、{{ACTIVE}}枚のカード{{ACTIVE_S}}{{GPA_NOTE}}があります - 一部のカードが切れる可能性があります。"
|
||||
|
||||
@ -578,9 +578,6 @@
|
||||
"flexible_grid": {
|
||||
"message": "Grelha flexível"
|
||||
},
|
||||
"flexible_grid_on": {
|
||||
"message": "A grelha flexível está ativa — os cartões ajustam-se para caber em janelas estreitas em vez de serem cortados."
|
||||
},
|
||||
"focus_timer": {
|
||||
"message": "Temporizador de foco"
|
||||
},
|
||||
@ -860,8 +857,8 @@
|
||||
"gradient_desc": {
|
||||
"message": "Escolhe 2 cores para criar um gradiente entre elas:"
|
||||
},
|
||||
"grid_includes_gpa": {
|
||||
"message": " (incluindo a calculadora de GPA)"
|
||||
"grid_includes_widgets": {
|
||||
"message": " (incluindo {{NAMES}})"
|
||||
},
|
||||
"grid_warning": {
|
||||
"message": "A tua grelha comporta {{TOTAL}} vaga{{TOTAL_S}}, mas tens {{ACTIVE}} vaga{{ACTIVE_S}}{{GPA_NOTE}} - alguns cartões podem ficar cortados."
|
||||
|
||||
@ -578,9 +578,6 @@
|
||||
"flexible_grid": {
|
||||
"message": "Гибкая сетка"
|
||||
},
|
||||
"flexible_grid_on": {
|
||||
"message": "Включена гибкая сетка — карточки переносятся, чтобы помещаться в узкие окна, а не обрезаются."
|
||||
},
|
||||
"focus_timer": {
|
||||
"message": "Таймер фокуса"
|
||||
},
|
||||
@ -860,8 +857,8 @@
|
||||
"gradient_desc": {
|
||||
"message": "Выберите 2 цвета, чтобы создать между ними градиент:"
|
||||
},
|
||||
"grid_includes_gpa": {
|
||||
"message": " (включая калькулятор GPA)"
|
||||
"grid_includes_widgets": {
|
||||
"message": " (включая {{NAMES}})"
|
||||
},
|
||||
"grid_warning": {
|
||||
"message": "В вашей сетке помещается {{TOTAL}} карточек{{TOTAL_S}}, но у вас {{ACTIVE}} карточка{{ACTIVE_S}}{{GPA_NOTE}} — некоторые карточки могут обрезаться."
|
||||
|
||||
@ -578,9 +578,6 @@
|
||||
"flexible_grid": {
|
||||
"message": "Flexibelt rutnät"
|
||||
},
|
||||
"flexible_grid_on": {
|
||||
"message": "Flexibelt rutnät är på – korten radbryts för att passa smala fönster i stället för att klippas av."
|
||||
},
|
||||
"focus_timer": {
|
||||
"message": "Fokustimer"
|
||||
},
|
||||
@ -860,8 +857,8 @@
|
||||
"gradient_desc": {
|
||||
"message": "Välj 2 färger för att skapa en gradient mellan dem:"
|
||||
},
|
||||
"grid_includes_gpa": {
|
||||
"message": " (inklusive GPA-kalkylatorn)"
|
||||
"grid_includes_widgets": {
|
||||
"message": " (inklusive {{NAMES}})"
|
||||
},
|
||||
"grid_warning": {
|
||||
"message": "Ditt rutnät har plats för {{TOTAL}} kort{{TOTAL_S}} men du har {{ACTIVE}} kort{{ACTIVE_S}}{{GPA_NOTE}} – vissa kort kan klippas av."
|
||||
|
||||
@ -578,9 +578,6 @@
|
||||
"flexible_grid": {
|
||||
"message": "弹性网格"
|
||||
},
|
||||
"flexible_grid_on": {
|
||||
"message": "弹性网格已开启 - 卡片会自动换行以适应窄窗口,而不会被裁切。"
|
||||
},
|
||||
"focus_timer": {
|
||||
"message": "专注计时器"
|
||||
},
|
||||
@ -860,8 +857,8 @@
|
||||
"gradient_desc": {
|
||||
"message": "选择 2 种颜色以在它们之间创建渐变:"
|
||||
},
|
||||
"grid_includes_gpa": {
|
||||
"message": " (包括 GPA 计算器)"
|
||||
"grid_includes_widgets": {
|
||||
"message": " (包括 {{NAMES}})"
|
||||
},
|
||||
"grid_warning": {
|
||||
"message": "你的网格可容纳 {{TOTAL}} 张卡片{{TOTAL_S}},但你有 {{ACTIVE}} 张卡片{{ACTIVE_S}}{{GPA_NOTE}} - 部分卡片可能会被裁切。"
|
||||
|
||||
@ -578,9 +578,6 @@
|
||||
"flexible_grid": {
|
||||
"message": "彈性格線"
|
||||
},
|
||||
"flexible_grid_on": {
|
||||
"message": "彈性格線已開啟——卡片會自動換行以配合狹窄視窗,而不會被裁切。"
|
||||
},
|
||||
"focus_timer": {
|
||||
"message": "專注計時器"
|
||||
},
|
||||
@ -860,8 +857,8 @@
|
||||
"gradient_desc": {
|
||||
"message": "選擇 2 種顏色,在它們之間建立漸層:"
|
||||
},
|
||||
"grid_includes_gpa": {
|
||||
"message": " (包含 GPA 計算器)"
|
||||
"grid_includes_widgets": {
|
||||
"message": " (包含 {{NAMES}})"
|
||||
},
|
||||
"grid_warning": {
|
||||
"message": "你的格線可容納 {{TOTAL}} 張卡片{{TOTAL_S}},但你目前有 {{ACTIVE}} 張卡片{{ACTIVE_S}}{{GPA_NOTE}}——部分卡片可能會被裁切。"
|
||||
|
||||
@ -1149,8 +1149,13 @@
|
||||
<div class="option-container">
|
||||
<h3 class="header-small" data-i18n="top_bottom_height">Top / Bottom height</h3>
|
||||
<div class="sub-option widget-inline-row">
|
||||
<label for="widget_row_height" class="sub-text widget-inline-label" data-i18n="height">Height</label>
|
||||
<input type="number" id="widget_row_height" class="card-input" min="0" max="2000" data-i18n-placeholder="auto" placeholder="auto" style="width:92px;margin:0;">
|
||||
<label for="widget_top_height" class="sub-text widget-inline-label" data-i18n="pos_top">Top</label>
|
||||
<input type="number" id="widget_top_height" class="card-input" min="0" max="2000" data-i18n-placeholder="auto" placeholder="auto" style="width:92px;margin:0;">
|
||||
<span class="sub-text">px</span>
|
||||
</div>
|
||||
<div class="sub-option widget-inline-row" style="margin-top:6px;">
|
||||
<label for="widget_bottom_height" class="sub-text widget-inline-label" data-i18n="pos_below">Bottom</label>
|
||||
<input type="number" id="widget_bottom_height" class="card-input" min="0" max="2000" data-i18n-placeholder="auto" placeholder="auto" style="width:92px;margin:0;">
|
||||
<span class="sub-text">px</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -58,6 +58,8 @@ chrome.runtime.onInstalled.addListener(function () {
|
||||
"quick_links_position": "cards_end",
|
||||
"heatmap": false,
|
||||
"heatmap_position": "below",
|
||||
"widget_top_height": 0,
|
||||
"widget_bottom_height": 0,
|
||||
"widget_row_height": 0,
|
||||
"dashboard_notes_height": 0,
|
||||
"gpa_calc_height": 0,
|
||||
@ -180,6 +182,16 @@ chrome.runtime.onInstalled.addListener(function () {
|
||||
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);
|
||||
}
|
||||
|
||||
237
js/content.js
237
js/content.js
@ -1090,6 +1090,7 @@ function applyOptionsChanges(changes) {
|
||||
case "dashboard_notes_position":
|
||||
case "dashboard_notes_height":
|
||||
loadDashboardNotes();
|
||||
refreshCardGridLayout();
|
||||
break;
|
||||
case "focus_timer":
|
||||
case "focus_timer_position":
|
||||
@ -1098,6 +1099,7 @@ function applyOptionsChanges(changes) {
|
||||
case "focus_timer_preset_2":
|
||||
case "focus_timer_preset_3":
|
||||
loadFocusTimer();
|
||||
refreshCardGridLayout();
|
||||
break;
|
||||
case "breathing":
|
||||
case "breathing_position":
|
||||
@ -1106,21 +1108,25 @@ function applyOptionsChanges(changes) {
|
||||
case "breathing_preset_2":
|
||||
case "breathing_preset_3":
|
||||
loadBreathingWidget();
|
||||
refreshCardGridLayout();
|
||||
break;
|
||||
case "daily_quotes":
|
||||
case "daily_quotes_position":
|
||||
case "daily_quotes_height":
|
||||
loadDailyQuotesWidget();
|
||||
refreshCardGridLayout();
|
||||
break;
|
||||
case "quick_links":
|
||||
case "quick_links_position":
|
||||
case "quick_links_height":
|
||||
loadQuickLinksWidget();
|
||||
refreshCardGridLayout();
|
||||
break;
|
||||
case "heatmap":
|
||||
case "heatmap_position":
|
||||
case "heatmap_height":
|
||||
loadHeatmapWidget();
|
||||
refreshCardGridLayout();
|
||||
break;
|
||||
case "quick_links_font_size":
|
||||
case "quick_links_icon_size":
|
||||
@ -1144,7 +1150,10 @@ function applyOptionsChanges(changes) {
|
||||
if (options.dashboard_notes === true) loadDashboardNotes();
|
||||
if (options.gpa_calc === true && current_page === "/") setupGPACalc();
|
||||
loadAllSimpleWidgets();
|
||||
refreshCardGridLayout();
|
||||
break;
|
||||
case "widget_top_height":
|
||||
case "widget_bottom_height":
|
||||
case "widget_row_height":
|
||||
document.querySelectorAll(".canvasrefined-widget-row").forEach(row => {
|
||||
applyWidgetRowLayout(row, row.dataset.area);
|
||||
@ -1267,7 +1276,7 @@ function applyOptionsChanges(changes) {
|
||||
setupGPACalc();
|
||||
updateGpaHoverBlur();
|
||||
// GPA elements take part in the grid layout, so regenerate the grid CSS.
|
||||
if (options.card_grid === true) debouncedApplyAestheticChanges();
|
||||
refreshCardGridLayout();
|
||||
break;
|
||||
case "gpa_calc_bounds":
|
||||
calculateGPA2();
|
||||
@ -7369,8 +7378,7 @@ function widgetContainerAnchor(container) {
|
||||
return document.querySelector("#DashboardCard_Container") || container;
|
||||
}
|
||||
|
||||
function ensureWidgetRow(area, container) {
|
||||
const parent = container.parentNode;
|
||||
function ensureWidgetRow(area, container) { const parent = container.parentNode;
|
||||
if (!parent) return null;
|
||||
let row = parent.querySelector(":scope > .canvasrefined-widget-row[data-area='" + area + "']");
|
||||
if (!row) {
|
||||
@ -7383,12 +7391,24 @@ function ensureWidgetRow(area, container) {
|
||||
return row;
|
||||
}
|
||||
|
||||
// Row height in px for the Top / Bottom widget rows (0 = auto). The two rows have
|
||||
// their own setting now; older builds shared one value (widget_row_height), which is
|
||||
// still honoured until storage has been migrated. Row areas are "top"/"below", the
|
||||
// stored settings are "Top"/"Bottom".
|
||||
const WIDGET_ROW_HEIGHT_KEYS = { top: "widget_top_height", below: "widget_bottom_height" };
|
||||
function widgetRowHeight(area) {
|
||||
const own = parseInt(options[WIDGET_ROW_HEIGHT_KEYS[area]], 10);
|
||||
if (Number.isFinite(own)) return Math.max(0, own);
|
||||
const legacy = parseInt(options.widget_row_height, 10);
|
||||
return Number.isFinite(legacy) ? Math.max(0, legacy) : 0;
|
||||
}
|
||||
|
||||
function applyWidgetRowLayout(row, area) {
|
||||
const slots = [...row.querySelectorAll(":scope > .canvasrefined-widget-slot")];
|
||||
if (!slots.length) { row.remove(); return; }
|
||||
// Shared row height (px; 0 = auto) so every widget in the row matches.
|
||||
const rowHeight = parseInt(options.widget_row_height, 10);
|
||||
if (Number.isFinite(rowHeight) && rowHeight > 0) row.style.height = rowHeight + "px";
|
||||
const rowHeight = widgetRowHeight(area);
|
||||
if (rowHeight > 0) row.style.height = rowHeight + "px";
|
||||
else row.style.height = "";
|
||||
const entries = containerWidgetOrder(area);
|
||||
const byId = {};
|
||||
@ -9493,6 +9513,74 @@ function debouncedApplyAestheticChanges(delay = 150) {
|
||||
}, delay);
|
||||
}
|
||||
|
||||
// In-card widgets take grid cells and rows, so the template has to be rebuilt when
|
||||
// one is toggled, moved or resized.
|
||||
function refreshCardGridLayout() {
|
||||
if (options.card_grid === true) debouncedApplyAestheticChanges();
|
||||
}
|
||||
|
||||
/*
|
||||
Card Grid geometry (shared by the layout CSS and centerUnevenGridRows)
|
||||
|
||||
The grid has to know about every child that takes a cell or a row: the course
|
||||
cards, the widgets placed Before/After cards (notes, focus timer, breathing,
|
||||
quotes, quick links, heatmap) and the GPA summary card. The calculator itself
|
||||
spans a full row when it is open inside the grid.
|
||||
*/
|
||||
|
||||
const CARD_GRID_HOST = ".ic-DashboardCard__box__container";
|
||||
|
||||
// One card-sized cell: course cards, in-card widgets and the GPA summary card.
|
||||
// (The GPA calculator `.canvasrefined-gpa` is not one: it spans a whole row.)
|
||||
// `:not(.container-mode)` on the GPA keeps these rules above the card-style margins
|
||||
// that follow them in the sheet.
|
||||
const CARD_GRID_CELLS = [
|
||||
`${CARD_GRID_HOST} > .ic-DashboardCard`,
|
||||
`${CARD_GRID_HOST} > .canvasrefined-widget-card.in-cards`,
|
||||
`${CARD_GRID_HOST} > .canvasrefined-dashboard-notes.in-cards`,
|
||||
`${CARD_GRID_HOST} > .canvasrefined-gpa-card:not(.container-mode)`,
|
||||
].join(",");
|
||||
|
||||
function isCardGridCell(el) {
|
||||
if (!el || !el.classList) return false;
|
||||
if (el.classList.contains("ic-DashboardCard") || el.classList.contains("canvasrefined-gpa-card")) return true;
|
||||
return el.classList.contains("in-cards")
|
||||
&& (el.classList.contains("canvasrefined-widget-card") || el.classList.contains("canvasrefined-dashboard-notes"));
|
||||
}
|
||||
|
||||
// Enabled widgets rendered before/after the cards, i.e. inside the grid.
|
||||
function cardGridWidgets() {
|
||||
return CR_SIMPLE_WIDGET_IDS.map(id => ({ id, position: crWidgetPosition(id) }))
|
||||
.concat([{ id: "dashboard_notes", position: crWidgetPosition("dashboard_notes") }])
|
||||
.filter(w => options[w.id] === true
|
||||
&& (w.position === "cards_start" || w.position === "cards_end"));
|
||||
}
|
||||
|
||||
function cardGridMetrics() {
|
||||
const cols = Math.max(1, parseInt(options.card_grid_columns, 10) || 4);
|
||||
const rows = Math.max(1, parseInt(options.card_grid_rows, 10) || 3);
|
||||
const colGap = Math.max(0, !isNaN(parseInt(options.card_grid_column_gap, 10)) ? parseInt(options.card_grid_column_gap, 10) : 12);
|
||||
const rowGap = Math.max(0, !isNaN(parseInt(options.card_grid_row_gap, 10)) ? parseInt(options.card_grid_row_gap, 10) : 12);
|
||||
const gpaPos = gpaCalcPosition();
|
||||
const gpaInGrid = options.gpa_calc === true && (gpaPos === "cards_start" || gpaPos === "cards_end");
|
||||
const widgets = cardGridWidgets();
|
||||
// Every widget and the GPA card need a cell, and the calculator a whole row.
|
||||
// Nothing may rely on implicit rows: the template clips them to zero height.
|
||||
const extraRows = Math.ceil((widgets.length + (gpaInGrid ? 1 : 0)) / cols) + (gpaInGrid ? 1 : 0);
|
||||
return {
|
||||
cols,
|
||||
rows,
|
||||
colGap,
|
||||
rowGap,
|
||||
widgets,
|
||||
startWidgets: widgets.filter(w => w.position === "cards_start"),
|
||||
gpaInGrid,
|
||||
gpaFirst: gpaPos === "cards_start",
|
||||
extraRows,
|
||||
totalRows: rows + extraRows,
|
||||
};
|
||||
}
|
||||
|
||||
function applyAestheticChanges() {
|
||||
if (quizSafeModeActive()) return;
|
||||
let style = document.querySelector("#canvasrefined-aesthetics") || document.createElement('style');
|
||||
@ -9504,44 +9592,58 @@ function applyAestheticChanges() {
|
||||
if (options.full_width === true) style.textContent += "#wrapper,.ic-Layout-wrapper{max-width:100%!important}";
|
||||
if (options.center_cards === true) style.textContent += ".ic-DashboardCard__box__container{display:flex!important;flex-wrap:wrap!important;justify-content:center!important;align-items:flex-start!important}";
|
||||
// Card Grid: strict columns x rows layout, emitted after center_cards to win the
|
||||
// !important tie-break. Columns are doubled into sub-columns (each card spans 2) so
|
||||
// "center uneven rows" can shift a partial row by half a card; two spare rows keep
|
||||
// the GPA card/calculator out of the clipped implicit rows.
|
||||
// !important tie-break. Columns are doubled into sub-columns (each cell spans 2) so
|
||||
// "center uneven rows" can shift a partial row by half a card; spare rows keep the
|
||||
// in-grid widgets (notes, timers, quotes, links, heatmap, GPA card/calculator) out
|
||||
// of the clipped implicit rows.
|
||||
if (options.card_grid === true) {
|
||||
const gridCols = Math.max(1, parseInt(options.card_grid_columns, 10) || 4);
|
||||
const gridRows = Math.max(1, parseInt(options.card_grid_rows, 10) || 3);
|
||||
const gridColGap = Math.max(0, !isNaN(parseInt(options.card_grid_column_gap, 10)) ? parseInt(options.card_grid_column_gap, 10) : 12);
|
||||
const gridRowGap = Math.max(0, !isNaN(parseInt(options.card_grid_row_gap, 10)) ? parseInt(options.card_grid_row_gap, 10) : 12);
|
||||
// strip Canvas' natural top/left card margins; the grid supplies its own gaps
|
||||
style.textContent += `.ic-DashboardCard__box__container{margin-top:0!important;margin-left:0!important}`;
|
||||
style.textContent += `.ic-DashboardCard__box__container > .ic-DashboardCard,.ic-DashboardCard__box__container > .canvasrefined-gpa-card{margin-top:0!important;margin-left:0!important}`;
|
||||
// CanvasRefined's own widgets carry 36px outer margins meant for the flex
|
||||
// card flow; in the grid every side is spacing the gaps already provide
|
||||
style.textContent += `.ic-DashboardCard__box__container > .canvasrefined-widget-card,.ic-DashboardCard__box__container > .canvasrefined-dashboard-notes,.ic-DashboardCard__box__container > .canvasrefined-gpa-card,.ic-DashboardCard__box__container > .canvasrefined-gpa{margin:0!important}`;
|
||||
const grid = cardGridMetrics();
|
||||
const gridCols = grid.cols;
|
||||
const gridColGap = grid.colGap;
|
||||
const gridRowGap = grid.rowGap;
|
||||
const centerGrid = options.center_cards === true;
|
||||
const gpaOn = options.gpa_calc === true;
|
||||
const gpaPos = gpaCalcPosition();
|
||||
const gpaInsideGrid = gpaPos === "cards_start" || gpaPos === "cards_end";
|
||||
const gpaFirst = gpaPos === "cards_start";
|
||||
const flexGrid = options.card_grid_flex === true;
|
||||
// content.css keeps this container a flex row whenever an in-card widget exists
|
||||
// (so widgets line up with Canvas' loading skeletons). Those selectors carry more
|
||||
// specificity than a bare class, so they are repeated here: grid mode has to
|
||||
// out-specify that fallback instead of silently losing to it.
|
||||
const gridHost = [
|
||||
CARD_GRID_HOST,
|
||||
`${CARD_GRID_HOST}:has(> .canvasrefined-widget-card.in-cards)`,
|
||||
`${CARD_GRID_HOST}:has(> .canvasrefined-dashboard-notes.in-cards)`,
|
||||
`${CARD_GRID_HOST}:has(> .canvasrefined-gpa-card:not(.container-mode))`,
|
||||
].join(",");
|
||||
// strip Canvas' natural top/left card margins; the grid supplies its own gaps
|
||||
style.textContent += `${CARD_GRID_HOST}{margin-top:0!important;margin-left:0!important}`;
|
||||
// CanvasRefined's own widgets carry 36px outer margins meant for the flex
|
||||
// card flow (plus the card-style spacing); in the grid every side is spacing
|
||||
// the gaps already provide
|
||||
style.textContent += `${CARD_GRID_CELLS}{margin:0!important}`;
|
||||
style.textContent += `${CARD_GRID_HOST} > .canvasrefined-gpa{margin:0!important}`;
|
||||
if (flexGrid) {
|
||||
// flexible grid: fixed-width tracks; @container queries step the column count
|
||||
// down when the next column stops fitting (no window-width guessing)
|
||||
const cardW = options.customCardStyles === true && parseInt(options.cardWidth, 10) > 0
|
||||
// content-box card padding grows the cards past the set width, so the track
|
||||
// (and the widget cards in it) has to follow
|
||||
const cardPad = options.customCardStyles === true ? Math.max(0, parseInt(options.cardPadding, 10) || 0) : 0;
|
||||
const cardW = (options.customCardStyles === true && parseInt(options.cardWidth, 10) > 0
|
||||
? parseInt(options.cardWidth, 10)
|
||||
: 262;
|
||||
: 262) + 2 * cardPad;
|
||||
// container queries need a size container; the card wrapper is full-width
|
||||
style.textContent += `#DashboardCard_Container{container-type:inline-size}`;
|
||||
style.textContent += `.ic-DashboardCard__box__container{display:grid!important;grid-template-columns:repeat(${gridCols},${cardW}px)!important;grid-auto-rows:auto!important;column-gap:${gridColGap}px!important;row-gap:${gridRowGap}px!important;align-items:start!important${centerGrid ? ";justify-content:center!important" : ""}}`;
|
||||
style.textContent += `${gridHost}{display:grid!important;grid-template-columns:repeat(${gridCols},${cardW}px)!important;grid-auto-rows:auto!important;column-gap:${gridColGap}px!important;row-gap:${gridRowGap}px!important;align-items:start!important${centerGrid ? ";justify-content:center!important" : ""}}`;
|
||||
// grid gaps alone control spacing (beats the later customCardStyles margins)
|
||||
style.textContent += `.ic-DashboardCard__box__container > .ic-DashboardCard,.ic-DashboardCard__box__container > .canvasrefined-gpa-card{margin-right:0!important;margin-bottom:0!important}`;
|
||||
if (gpaOn && gpaInsideGrid) {
|
||||
if (gpaFirst) {
|
||||
style.textContent += `.ic-DashboardCard__box__container > .canvasrefined-gpa-card{grid-row:1!important;grid-column:1!important}`;
|
||||
style.textContent += `.ic-DashboardCard__box__container > .canvasrefined-gpa{grid-row:2!important;grid-column:1/-1!important}`;
|
||||
style.textContent += `${CARD_GRID_CELLS}{margin-right:0!important;margin-bottom:0!important}`;
|
||||
if (grid.gpaInGrid) {
|
||||
// Pin only while the GPA is alone in that slot: with another before-card
|
||||
// widget present, auto-placement in DOM order follows the saved widget
|
||||
// order, which the pin would override.
|
||||
if (grid.gpaFirst && !grid.startWidgets.length) {
|
||||
style.textContent += `${CARD_GRID_HOST} > .canvasrefined-gpa-card{grid-row:1!important;grid-column:1!important}`;
|
||||
style.textContent += `${CARD_GRID_HOST} > .canvasrefined-gpa{grid-row:2!important;grid-column:1/-1!important}`;
|
||||
} else {
|
||||
// auto-placed: flows into the next open slot after the last card
|
||||
style.textContent += `.ic-DashboardCard__box__container > .canvasrefined-gpa{grid-column:1/-1!important}`;
|
||||
style.textContent += `${CARD_GRID_HOST} > .canvasrefined-gpa{grid-column:1/-1!important}`;
|
||||
}
|
||||
}
|
||||
// step down to m columns once m+1 no longer fit; widest-first so the
|
||||
@ -9549,35 +9651,49 @@ function applyAestheticChanges() {
|
||||
for (let m = gridCols - 1; m >= 1; m--) {
|
||||
const tooWideFor = (m + 1) * cardW + m * gridColGap;
|
||||
if (m === 1) {
|
||||
style.textContent += `@container (max-width:${tooWideFor - 1}px){.ic-DashboardCard__box__container{grid-template-columns:minmax(0,1fr)!important}.ic-DashboardCard__box__container > .ic-DashboardCard${gpaInsideGrid ? ",.ic-DashboardCard__box__container > .canvasrefined-gpa-card" : ""}{width:100%!important;min-width:0!important}}`;
|
||||
style.textContent += `@container (max-width:${tooWideFor - 1}px){${gridHost}{grid-template-columns:minmax(0,1fr)!important}${CARD_GRID_CELLS}{width:100%!important;min-width:0!important}}`;
|
||||
} else {
|
||||
style.textContent += `@container (max-width:${tooWideFor - 1}px){.ic-DashboardCard__box__container{grid-template-columns:repeat(${m},${cardW}px)!important}}`;
|
||||
style.textContent += `@container (max-width:${tooWideFor - 1}px){${gridHost}{grid-template-columns:repeat(${m},${cardW}px)!important}}`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const totalRows = gridRows + (gpaOn && gpaInsideGrid ? 2 : 0);
|
||||
style.textContent += `.ic-DashboardCard__box__container{display:grid!important;grid-template-columns:repeat(${gridCols * 2},minmax(0,max-content))!important;grid-template-rows:repeat(${totalRows},minmax(0,auto))!important;grid-auto-rows:0!important;column-gap:${gridColGap}px!important;row-gap:${gridRowGap}px!important;align-items:start!important;overflow:hidden!important${centerGrid ? ";justify-content:center!important" : ""}}`;
|
||||
style.textContent += `.ic-DashboardCard__box__container > .ic-DashboardCard,.ic-DashboardCard__box__container > .canvasrefined-gpa-card{grid-column:span 2!important}`;
|
||||
if (gpaOn && gpaInsideGrid) {
|
||||
style.textContent += `${gridHost}{display:grid!important;grid-template-columns:repeat(${gridCols * 2},minmax(0,max-content))!important;grid-template-rows:repeat(${grid.totalRows},minmax(0,auto))!important;grid-auto-rows:0!important;column-gap:${gridColGap}px!important;row-gap:${gridRowGap}px!important;align-items:start!important;overflow:hidden!important${centerGrid ? ";justify-content:center!important" : ""}}`;
|
||||
// every cell-sized child spans one card (two sub-columns)
|
||||
style.textContent += `${CARD_GRID_CELLS}{grid-column:span 2!important}`;
|
||||
if (grid.gpaInGrid) {
|
||||
// GPA card pinned to the first slot, or auto-placed after the cards; the
|
||||
// expanded calculator always takes a full-width row
|
||||
if (gpaFirst) {
|
||||
style.textContent += `.ic-DashboardCard__box__container > .canvasrefined-gpa-card{grid-row:1!important;grid-column:1/span 2!important}`;
|
||||
style.textContent += `.ic-DashboardCard__box__container > .canvasrefined-gpa{grid-row:2!important;grid-column:1/-1!important}`;
|
||||
if (grid.gpaFirst && !grid.startWidgets.length) {
|
||||
style.textContent += `${CARD_GRID_HOST} > .canvasrefined-gpa-card{grid-row:1!important;grid-column:1/span 2!important}`;
|
||||
style.textContent += `${CARD_GRID_HOST} > .canvasrefined-gpa{grid-row:2!important;grid-column:1/-1!important}`;
|
||||
} else {
|
||||
style.textContent += `.ic-DashboardCard__box__container > .canvasrefined-gpa{grid-column:1/-1!important}`;
|
||||
style.textContent += `${CARD_GRID_HOST} > .canvasrefined-gpa{grid-column:1/-1!important}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (options.customCardStyles === true) {
|
||||
// CanvasRefined's own widget cards are card faces too, so the card width, padding,
|
||||
// roundness and spacing settings reach them as well. Only the faces that sit in
|
||||
// the card flow are resized/spaced: a Top/Bottom row widget is sized by the
|
||||
// Widgets tab, so it just inherits the face styling.
|
||||
const widgetFace = ".canvasrefined-widget-card, .canvasrefined-dashboard-notes, .canvasrefined-gpa-card";
|
||||
const widgetCard = ".canvasrefined-widget-card.in-cards, .canvasrefined-dashboard-notes.in-cards, .canvasrefined-gpa-card:not(.container-mode)";
|
||||
const cardRoundness = parseInt(options.cardRoundness, 10);
|
||||
const cardSpacing = parseInt(options.cardSpacing, 10);
|
||||
const cardWidth = parseInt(options.cardWidth, 10);
|
||||
const cardPadding = parseInt(options.cardPadding, 10);
|
||||
if (options.imageSize !== undefined && options.imageSize !== 100) style.textContent += `.ic-DashboardCard__header_image {transform: scale(${options.imageSize / 100})!important; }`;
|
||||
if (options.cardRoundness !== undefined && options.cardRoundness !== 5) style.textContent += `.ic-DashboardCard {border-radius: ${options.cardRoundness}px!important;}`;
|
||||
// widgets default to 4px, cards to 5px: follow the setting instead of the stock value
|
||||
if (Number.isFinite(cardRoundness) && cardRoundness !== 5) style.textContent += `${widgetFace} {border-radius: ${cardRoundness}px!important;}`;
|
||||
// the hero overlay sits on top of the photo, so both need the radius — rounding
|
||||
// one alone leaves the other's square corners showing
|
||||
if (options.imageRoundness !== undefined && options.imageRoundness !== 0) style.textContent += `.ic-DashboardCard__header_image, .ic-DashboardCard__header_hero {border-radius: ${options.imageRoundness}px!important;}`;
|
||||
if (options.cardSpacing !== undefined && options.cardSpacing !== 0) style.textContent += `.ic-DashboardCard {margin-right: ${options.cardSpacing / 2}px!important; margin-bottom: ${options.cardSpacing / 2}px!important;}`;
|
||||
if (Number.isFinite(cardSpacing) && cardSpacing !== 0) style.textContent += `${widgetCard} {margin-right: ${cardSpacing / 2}px!important; margin-bottom: ${cardSpacing / 2}px!important;}`;
|
||||
if (options.cardWidth !== undefined && options.cardWidth !== 262) style.textContent += `.ic-DashboardCard {width: ${options.cardWidth}px!important;}`;
|
||||
if (Number.isFinite(cardWidth) && cardWidth > 0 && cardWidth !== 262) style.textContent += `${widgetCard} {width: ${cardWidth}px!important;}`;
|
||||
// card height goes on the hero band (146px stock): the card and header are
|
||||
// content-sized, so a height on either just clips or adds dead space
|
||||
if (options.condensed_cards !== true && options.cardHeight !== undefined && options.cardHeight !== null && options.cardHeight !== "") {
|
||||
@ -9588,6 +9704,11 @@ function applyAestheticChanges() {
|
||||
if (options.cardPadding !== undefined && Number(options.cardPadding) > 0) {
|
||||
style.textContent += `.ic-DashboardCard {padding: ${options.cardPadding}px!important; box-sizing: content-box!important;}`;
|
||||
}
|
||||
if (Number.isFinite(cardPadding) && cardPadding > 0) {
|
||||
style.textContent += `${widgetFace} {padding: ${cardPadding}px!important; box-sizing: border-box!important;}`;
|
||||
// fixed-width faces grow like Canvas cards; a row widget must stay inside its slot
|
||||
style.textContent += `${widgetCard} {box-sizing: content-box!important;}`;
|
||||
}
|
||||
}
|
||||
|
||||
// margins always cleared so the bar lines up with the content column; hiding it is opt-in
|
||||
@ -9604,16 +9725,22 @@ Center uneven grid rows.
|
||||
|
||||
CSS grid always packs rows from the left, so a last row with fewer cards
|
||||
than the chosen column count sits flush-left while the rows above are full.
|
||||
Pure CSS can't center a partial row, so we offset the first card of the last
|
||||
Pure CSS can't center a partial row, so we offset the first cell of the last
|
||||
partial row with grid-column-start.
|
||||
|
||||
The grid runs on double sub-columns (each card spans 2 — see
|
||||
The grid runs on double sub-columns (each cell spans 2 — see
|
||||
applyAestheticChanges), which is what makes this possible at all: with 3
|
||||
cards under a 4-card row the leftover space is one column, and centering
|
||||
needs a HALF-column shift — impossible on plain column lines. On sub-columns
|
||||
the shift is a whole sub-column (half a card + half a gap), so every
|
||||
remainder can be centered exactly.
|
||||
|
||||
In-card widgets are cells just like cards, so they count here too (a grid
|
||||
filled by cards plus widgets has a partial last row all the same). Rows are
|
||||
detected from the rendered boxes, not from a flat index: a full-width
|
||||
calculator row (or a hidden card) leaves holes that index math would read as
|
||||
the wrong row.
|
||||
|
||||
Only runs while Card Grid is on. Skips when the cards overflow the explicit
|
||||
template (those extra rows are clipped anyway, and the last visible row is
|
||||
full). Any previously applied offset is cleared first so re-runs (card
|
||||
@ -9622,14 +9749,14 @@ hidden/unhidden, dashboard re-render, setting changes) start clean.
|
||||
function centerUnevenGridRows() {
|
||||
const container = document.querySelector(".ic-DashboardCard__box__container");
|
||||
if (!container) return;
|
||||
// DOM order matches auto-placement order, so chunking this list matches the real rows
|
||||
const items = Array.from(container.children).filter(el => el.classList && (el.classList.contains("ic-DashboardCard") || el.classList.contains("canvasrefined-gpa-card")));
|
||||
// DOM order matches auto-placement order, so this list is in visual order
|
||||
const items = Array.from(container.children).filter(isCardGridCell);
|
||||
items.forEach(el => el.style.removeProperty("grid-column"));
|
||||
if (options.card_grid !== true || options.card_grid_center_rows !== true) return;
|
||||
// flexible grid changes the column count live, so fixed offsets would be wrong
|
||||
if (options.card_grid_flex === true) return;
|
||||
const cols = Math.max(1, parseInt(options.card_grid_columns, 10) || 4);
|
||||
const rows = Math.max(1, parseInt(options.card_grid_rows, 10) || 3);
|
||||
const grid = cardGridMetrics();
|
||||
const cols = grid.cols;
|
||||
const visible = items.filter(el => getComputedStyle(el).display !== "none");
|
||||
const count = visible.length;
|
||||
// overflow past the template is clipped and the last visible row is full — nothing to center
|
||||
@ -9637,15 +9764,23 @@ function centerUnevenGridRows() {
|
||||
const expanded = container.querySelector(":scope > .canvasrefined-gpa");
|
||||
return !!expanded && getComputedStyle(expanded).display !== "none";
|
||||
})();
|
||||
const capacity = cols * (rows + (options.gpa_calc === true ? 2 : 0)) - (gpaExpanded ? cols : 0);
|
||||
const capacity = cols * grid.totalRows - (gpaExpanded ? cols : 0);
|
||||
if (count === 0 || count > capacity) return;
|
||||
const lastRowStart = Math.floor((count - 1) / cols) * cols;
|
||||
const inLastRow = count - lastRowStart;
|
||||
// Group by rendered row: cells of one row share a top edge.
|
||||
const rows = [];
|
||||
visible.forEach(el => {
|
||||
const top = Math.round(el.getBoundingClientRect().top);
|
||||
const row = rows.find(r => r.top === top);
|
||||
if (row) row.els.push(el);
|
||||
else rows.push({ top, els: [el] });
|
||||
});
|
||||
const lastRow = rows[rows.length - 1];
|
||||
const inLastRow = lastRow ? lastRow.els.length : 0;
|
||||
if (inLastRow >= cols) return;
|
||||
// half the leftover, in sub-columns, paired with an explicit span; !important because
|
||||
// the stylesheet span rule is !important too
|
||||
const offset = cols - inLastRow;
|
||||
if (offset > 0) visible[lastRowStart].style.setProperty("grid-column", `${offset + 1} / span 2`, "important");
|
||||
if (offset > 0) lastRow.els[0].style.setProperty("grid-column", `${offset + 1} / span 2`, "important");
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
127
js/popup.js
127
js/popup.js
@ -31,6 +31,8 @@ const syncedSubOptions = [
|
||||
"daily_quotes_position",
|
||||
"quick_links_position",
|
||||
"heatmap_position",
|
||||
"widget_top_height",
|
||||
"widget_bottom_height",
|
||||
"widget_row_height",
|
||||
"dashboard_notes_height",
|
||||
"gpa_calc_height",
|
||||
@ -278,7 +280,10 @@ const defaultOptions = {
|
||||
"quick_links_position": "cards_end",
|
||||
"heatmap": false,
|
||||
"heatmap_position": "below",
|
||||
// Height in px (0 = auto); widget_row_height is the shared Top/Bottom row.
|
||||
// Row height in px (0 = auto) for the Top and Bottom widget rows.
|
||||
// widget_row_height is the pre-split shared value (legacy only).
|
||||
"widget_top_height": 0,
|
||||
"widget_bottom_height": 0,
|
||||
"widget_row_height": 0,
|
||||
"dashboard_notes_height": 0,
|
||||
"gpa_calc_height": 0,
|
||||
@ -405,6 +410,12 @@ chrome.storage.onChanged.addListener((changes) => {
|
||||
}
|
||||
updateCardGridPreview();
|
||||
scheduleGridPreviewFollowUp();
|
||||
return;
|
||||
}
|
||||
// Widget toggles/positions decide which widgets take grid cells in the preview.
|
||||
if (cardGridWidgetKeys().some(key => changes[key])) {
|
||||
updateCardGridPreview();
|
||||
scheduleGridPreviewFollowUp();
|
||||
}
|
||||
});
|
||||
|
||||
@ -909,6 +920,40 @@ function countActiveDashboardCards(cards) {
|
||||
return Object.values(cards || {}).filter(c => c && typeof c === "object" && !c.hidden && !c.hide).length;
|
||||
}
|
||||
|
||||
// Card cells are blue, in-grid widgets yellow, empty slots grey.
|
||||
const CARD_GRID_CARD_COLOR = "#56Caf0";
|
||||
const CARD_GRID_WIDGET_COLOR = "#f5c451";
|
||||
const CARD_GRID_EMPTY_COLOR = "#3c3c3c";
|
||||
|
||||
// Storage keys the preview needs to know which widgets land inside the grid.
|
||||
function cardGridWidgetKeys() {
|
||||
return WIDGET_LIST
|
||||
.reduce((keys, w) => keys.concat([w.id, w.id + "_position"]), [])
|
||||
.concat(["gpa_calc_prepend", "widget_order"]);
|
||||
}
|
||||
|
||||
// Enabled widgets rendered before/after the cards, split by anchor. They take grid
|
||||
// cells just like course cards (see content.js cardGridWidgets).
|
||||
function cardGridWidgetCells(sync) {
|
||||
const saved = Array.isArray(sync.widget_order && sync.widget_order.cards) ? sync.widget_order.cards : [];
|
||||
const savedIds = saved.map(e => (typeof e === "string" ? e : (e && e.id) || null)).filter(Boolean);
|
||||
const rank = id => {
|
||||
const i = savedIds.indexOf(id);
|
||||
return i === -1 ? savedIds.length : i;
|
||||
};
|
||||
const cells = { start: [], end: [] };
|
||||
WIDGET_LIST.forEach(w => {
|
||||
if (sync[w.id] !== true) return;
|
||||
const pos = widgetPosFromSync(w.id, sync);
|
||||
if (pos === "cards_start") cells.start.push(w.id);
|
||||
else if (pos === "cards_end") cells.end.push(w.id);
|
||||
});
|
||||
// The saved order wins; WIDGET_LIST order breaks ties (sort is stable).
|
||||
cells.start.sort((a, b) => rank(a) - rank(b));
|
||||
cells.end.sort((a, b) => rank(a) - rank(b));
|
||||
return cells;
|
||||
}
|
||||
|
||||
function updateCardGridPreview() {
|
||||
const panel = document.getElementById("card-grid-options");
|
||||
const preview = document.getElementById("card-grid-preview");
|
||||
@ -917,33 +962,58 @@ function updateCardGridPreview() {
|
||||
const rows = Math.max(1, parseInt(document.getElementById("card_grid_rows").value, 10) || 1);
|
||||
const colGapRaw = parseInt(document.getElementById("card_grid_column_gap").value, 10);
|
||||
const colGap = Math.max(0, Math.min(10, isNaN(colGapRaw) ? 12 : colGapRaw));
|
||||
const total = cols * rows;
|
||||
preview.style.gridTemplateColumns = `repeat(${cols}, 18px)`;
|
||||
// Clamped so extreme values don't blow the layout out.
|
||||
preview.style.columnGap = `${colGap}px`;
|
||||
preview.style.rowGap = "4px";
|
||||
preview.innerHTML = "";
|
||||
chrome.storage.sync.get(["custom_cards", "gpa_calc"], async s => {
|
||||
chrome.storage.sync.get(["custom_cards", "card_grid_flex"].concat(cardGridWidgetKeys()), async s => {
|
||||
if (document.getElementById("card-grid-options").style.display === "none") return;
|
||||
// Widgets in the cards area occupy cells too, so they count against the grid.
|
||||
const widgetCells = cardGridWidgetCells(s);
|
||||
const widgetIds = widgetCells.start.concat(widgetCells.end);
|
||||
const gpaInGrid = widgetIds.includes("gpa_calc");
|
||||
// Same row budget as content.js cardGridMetrics: nothing may rely on implicit
|
||||
// rows (the grid clips those), so each in-grid widget gets rows of its own.
|
||||
// The flexible grid grows rows on its own, so there it is the plain rows x cols.
|
||||
const spareRows = s.card_grid_flex === true
|
||||
? 0
|
||||
: Math.ceil((widgetIds.filter(id => id !== "gpa_calc").length + (gpaInGrid ? 1 : 0)) / cols) + (gpaInGrid ? 1 : 0);
|
||||
const total = cols * (rows + spareRows);
|
||||
// Prefer the live count from a dashboard tab; it knows which cards are
|
||||
// actually rendered. Falls back to the storage-derived count.
|
||||
const live = await sendFromPopup("getGridCardCount");
|
||||
let active = (live && typeof live.count === "number" && live.count >= 0)
|
||||
const cardCount = (live && typeof live.count === "number" && live.count >= 0)
|
||||
? live.count
|
||||
: countActiveDashboardCards(s.custom_cards);
|
||||
if (s.gpa_calc === true) active += 1;
|
||||
// Auto-placement order: before-card widgets, the cards, after-card widgets.
|
||||
const cells = widgetCells.start.map(id => ({ kind: "widget", id }))
|
||||
.concat(Array.from({ length: cardCount }, () => ({ kind: "card" })),
|
||||
widgetCells.end.map(id => ({ kind: "widget", id })));
|
||||
const active = cells.length;
|
||||
// The flexible grid grows rows on its own, so nothing is cut off there: draw
|
||||
// every cell. The fixed grid clips, so it stays within its template.
|
||||
const cellCount = s.card_grid_flex === true ? Math.max(total, active) : total;
|
||||
let lastRowStart = -1, offset = 0;
|
||||
if (document.getElementById("card_grid_center_rows")?.checked === true && active > 0 && active < total) {
|
||||
lastRowStart = Math.floor((active - 1) / cols) * cols;
|
||||
const inLastRow = active - lastRowStart;
|
||||
if (inLastRow < cols) offset = cols - inLastRow;
|
||||
}
|
||||
for (let i = 0; i < total; i++) {
|
||||
for (let i = 0; i < cellCount; i++) {
|
||||
const inShiftedRow = lastRowStart !== -1 && i >= lastRowStart;
|
||||
if (inShiftedRow && i >= active) continue;
|
||||
const cell = cells[i];
|
||||
const square = document.createElement("div");
|
||||
square.style.cssText = "width:18px;height:18px;border-radius:4px;box-sizing:border-box;";
|
||||
square.style.background = i < active ? "#56Caf0" : "#3c3c3c";
|
||||
if (!cell) {
|
||||
square.style.background = CARD_GRID_EMPTY_COLOR;
|
||||
} else if (cell.kind === "widget") {
|
||||
square.style.background = CARD_GRID_WIDGET_COLOR;
|
||||
square.title = widgetFullName(cell.id);
|
||||
} else {
|
||||
square.style.background = CARD_GRID_CARD_COLOR;
|
||||
}
|
||||
if (inShiftedRow && offset > 0) {
|
||||
square.style.marginLeft = `${offset * (18 + colGap) / 2}px`;
|
||||
}
|
||||
@ -951,12 +1021,13 @@ function updateCardGridPreview() {
|
||||
}
|
||||
const warning = document.getElementById("card-grid-warning");
|
||||
if (warning) {
|
||||
if (s.card_grid_flex === true) {
|
||||
warning.textContent = getMsg("flexible_grid_on") || "Flexible grid is on - cards wrap to fit narrow windows instead of being cut off.";
|
||||
warning.style.display = "block";
|
||||
} else if (active > total) {
|
||||
const gpaNote = s.gpa_calc === true ? getMsg("grid_includes_gpa", " (including the GPA calculator)") : "";
|
||||
warning.textContent = "\u26A0 " + (getMsg("grid_warning") || "Your grid holds {{TOTAL}} card{{TOTAL_S}} but you have {{ACTIVE}} card{{ACTIVE_S}}{{GPA_NOTE}} - some cards may be cut off.").replace("{{TOTAL}}", total).replace("{{TOTAL_S}}", total === 1 ? "" : "s").replace("{{ACTIVE}}", active).replace("{{ACTIVE_S}}", active === 1 ? "" : "s").replace("{{GPA_NOTE}}", gpaNote);
|
||||
// The flexible grid wraps instead of clipping, so only the fixed grid can
|
||||
// overflow its template.
|
||||
if (s.card_grid_flex !== true && active > total) {
|
||||
const widgetNote = widgetIds.length
|
||||
? (getMsg("grid_includes_widgets", " (including {{NAMES}})")).replace("{{NAMES}}", widgetIds.map(widgetFullName).join(", "))
|
||||
: "";
|
||||
warning.textContent = "\u26A0 " + (getMsg("grid_warning") || "Your grid holds {{TOTAL}} card{{TOTAL_S}} but you have {{ACTIVE}} card{{ACTIVE_S}}{{GPA_NOTE}} - some cards may be cut off.").replace("{{TOTAL}}", total).replace("{{TOTAL_S}}", total === 1 ? "" : "s").replace("{{ACTIVE}}", active).replace("{{ACTIVE_S}}", active === 1 ? "" : "s").replace("{{GPA_NOTE}}", widgetNote);
|
||||
warning.style.display = "block";
|
||||
} else {
|
||||
warning.style.display = "none";
|
||||
@ -1140,6 +1211,13 @@ function widgetDisplayName(id) {
|
||||
return id.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase());
|
||||
}
|
||||
|
||||
// Unabbreviated name, for sentences (the preview warning lists widget names).
|
||||
function widgetFullName(id) {
|
||||
const info = WIDGET_LIST.find(w => w.id === id);
|
||||
const msg = info && getMsg(info.nameKey);
|
||||
return msg || widgetDisplayName(id);
|
||||
}
|
||||
|
||||
let refreshWidgetOrder = null;
|
||||
|
||||
function setupWidgetOrder() {
|
||||
@ -1338,14 +1416,25 @@ function setupWidgetPosition() {
|
||||
});
|
||||
}
|
||||
|
||||
function setupWidgetHeightInput(id, initial) {
|
||||
// Height fields (px; empty = auto). `legacyKey` seeds an empty field from a key that
|
||||
// has since been split in two (the Top/Bottom row height used to be shared), so users
|
||||
// who set it keep their value until they edit the field.
|
||||
function setupWidgetHeightInput(id, initial, legacyKey) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
const parse = (v) => {
|
||||
const n = parseInt(v, 10);
|
||||
return Number.isFinite(n) && n > 0 ? n : 0;
|
||||
};
|
||||
el.value = parse(initial) || "";
|
||||
const apply = (v) => { el.value = parse(v) || ""; };
|
||||
apply(initial);
|
||||
if (legacyKey) {
|
||||
chrome.storage.sync.get([id, legacyKey], sync => {
|
||||
// A stored 0 means "auto" and must not be overwritten by the old value.
|
||||
if (sync[id] !== undefined) { if (!parse(el.value)) apply(sync[id]); return; }
|
||||
if (!parse(el.value)) apply(sync[legacyKey]);
|
||||
});
|
||||
}
|
||||
el.addEventListener("change", function () {
|
||||
const n = Math.min(2000, Math.max(0, parseInt(this.value, 10) || 0));
|
||||
this.value = n > 0 ? n : "";
|
||||
@ -1979,8 +2068,12 @@ async function setup() {
|
||||
setup: (initial) => setupAnonProgressInput("anon_progress_total", initial, false),
|
||||
},
|
||||
{
|
||||
identifier: "widget_row_height",
|
||||
setup: (initial) => setupWidgetHeightInput("widget_row_height", initial),
|
||||
identifier: "widget_top_height",
|
||||
setup: (initial) => setupWidgetHeightInput("widget_top_height", initial, "widget_row_height"),
|
||||
},
|
||||
{
|
||||
identifier: "widget_bottom_height",
|
||||
setup: (initial) => setupWidgetHeightInput("widget_bottom_height", initial, "widget_row_height"),
|
||||
},
|
||||
{
|
||||
identifier: "dashboard_notes_height",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user