Compare commits

...

6 Commits

Author SHA1 Message Date
luxick 2d2f053e4a Update game-datcards-improved.user.js 2026-07-22 19:11:46 +02:00
luxick 11a7eb5f9b Update game-datcards-improved.user.js 2026-07-22 19:03:13 +02:00
luxick 24e91899f4 Update game-datcards-improved.user.js 2026-07-22 19:02:47 +02:00
luxick b7b01b327c Update game-datcards-improved.user.js 2026-07-22 18:58:31 +02:00
luxick d0d58fa386 Bump version 2026-07-22 18:50:56 +02:00
luxick 891b0734ca Fix font size 2026-07-22 18:50:45 +02:00
+204 -62
View File
@@ -1,7 +1,7 @@
// ==UserScript==
// @name Game Datacards - Improved
// @namespace local
// @version 1.1
// @version 1.5
// @description Adds card image export/import (ZIP) to the header, a button to rotate card backs for printing and a toggle for the official 10th edition card font
// @match *://game-datacards.eu/*
// @grant none
@@ -196,65 +196,11 @@
const FONT_STYLE_ID = 'gdc-card-font';
const FONT_PREF_KEY = 'gdc-card-font-enabled';
// Sizes below are the official ones for a full size card. The app draws cards
// scaled by --card-scaling-factor, so every size is scaled along with it.
function pt(size) {
return `calc(${size}pt * var(--card-scaling-factor, 1))`;
}
// Family swap only: sizes, weights and casing are left to the app so the two
// renderings can be compared side by side.
function cardFontCss() {
return `
.unit, .unit * { font-family: ${CARD_FONT}, sans-serif !important; }
/* Unit name: extra bold 15pt */
.unit .name_container .name {
font-weight: 800 !important;
font-size: ${pt(15)} !important;
}
/* Unit description: light italic 8pt */
.unit .name_container .subname,
.unit .header .description {
font-weight: 300 !important;
font-style: italic !important;
font-size: ${pt(8)} !important;
}
/* Unit values: bold 14pt */
.unit .stats_container .value,
.unit .stats_container .value-text {
font-weight: 700 !important;
font-size: ${pt(14)} !important;
}
/* General weapon and abilities text: regular or bold 7.5pt */
.unit .stats_container .caption,
.unit .invul .title,
.unit .heading .title,
.unit .ability .name,
.unit .ability .title,
.unit .footer .title,
.unit .footer .value {
font-weight: 700 !important;
font-size: ${pt(7.5)} !important;
}
.unit .weapon .value,
.unit .ability .value,
.unit .ability .description,
.unit .description-container .description {
font-weight: 400 !important;
font-size: ${pt(7.5)} !important;
}
/* Weapon abilities (the keyword/rule chips): bold 5.5pt */
.unit .keyword-button,
.unit .keyword-button span,
.unit .rule-button,
.unit .rule-button span {
font-weight: 700 !important;
font-size: ${pt(5.5)} !important;
}
`;
}
@@ -264,10 +210,12 @@
function applyCardFont(enabled) {
const existing = document.getElementById(FONT_STYLE_ID);
if (!enabled) {
if (existing) existing.remove();
return;
}
const style = existing || document.createElement('style');
style.id = FONT_STYLE_ID;
style.textContent = cardFontCss();
@@ -290,6 +238,193 @@
syncCardFontButton();
}
// ---------------------------------------------------------------------------
// Header background image
// ---------------------------------------------------------------------------
// Kept in a database of our own rather than the app's CardImagesDB: that store
// is keyed by card id and round-tripped by the app's own import/export, so a
// foreign record in it would be fragile.
const STYLING_DB = 'GdcCardStylingDB';
const STYLING_STORE = 'settings';
const HEADER_BG_ID = 'header-background';
const HEADER_BG_STYLE_ID = 'gdc-header-bg-style';
function openStylingDB() {
return new Promise((resolve, reject) => {
const req = indexedDB.open(STYLING_DB, 1);
req.onupgradeneeded = () => {
const db = req.result;
if (!db.objectStoreNames.contains(STYLING_STORE)) {
db.createObjectStore(STYLING_STORE, { keyPath: 'id' });
}
};
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
}
function stylingRequest(mode, run) {
return openStylingDB().then(
(db) =>
new Promise((resolve, reject) => {
const req = run(db.transaction(STYLING_STORE, mode).objectStore(STYLING_STORE));
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
})
);
}
function readHeaderBackground() {
return stylingRequest('readonly', (store) => store.get(HEADER_BG_ID));
}
function writeHeaderBackground(blob) {
return stylingRequest('readwrite', (store) =>
store.put({ id: HEADER_BG_ID, image: blob, type: blob.type, size: blob.size })
);
}
function eraseHeaderBackground() {
return stylingRequest('readwrite', (store) => store.delete(HEADER_BG_ID));
}
var headerBgUrl = null;
async function refreshHeaderBackground() {
const record = await readHeaderBackground();
if (headerBgUrl) {
URL.revokeObjectURL(headerBgUrl);
headerBgUrl = null;
}
let style = document.getElementById(HEADER_BG_STYLE_ID);
if (!style) {
style = document.createElement('style');
style.id = HEADER_BG_STYLE_ID;
document.head.appendChild(style);
}
if (record && record.image instanceof Blob) {
headerBgUrl = URL.createObjectURL(record.image);
// The background sits on .header so it spans the stat line too, and the
// container holding the model artwork is cleared so it doesn't cover it.
style.textContent = `
.unit .header {
background-image: url("${headerBgUrl}") !important;
background-size: cover !important;
background-position: center !important;
background-repeat: no-repeat !important;
}
.unit .header .header_container { background-color: transparent !important; }
`;
} else {
style.textContent = '';
}
updateHeaderBgBox();
}
async function pickHeaderBackground() {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = async () => {
const file = input.files[0];
if (!file) return;
await writeHeaderBackground(file);
await refreshHeaderBackground();
};
input.click();
}
async function removeHeaderBackground() {
await eraseHeaderBackground();
await refreshHeaderBackground();
}
function mkStylingButton(label, fn) {
const b = document.createElement('button');
b.type = 'button';
b.textContent = label;
b.style.flex = '1';
b.style.padding = '4px 8px';
b.style.background = '#381a3a';
b.style.color = 'white';
b.style.border = 'none';
b.style.borderRadius = '4px';
b.style.cursor = 'pointer';
b.style.fontSize = '12px';
b.onclick = () => {
const r = fn();
if (r && typeof r.catch === 'function') r.catch((e) => alert('Error: ' + e.message));
};
return b;
}
function createHeaderBgBox() {
const box = document.createElement('div');
box.id = 'gdc-header-bg-box';
box.style.marginTop = '8px';
const label = document.createElement('div');
label.textContent = 'Header background';
label.style.fontSize = '12px';
label.style.marginBottom = '4px';
box.appendChild(label);
const preview = document.createElement('img');
preview.id = 'gdc-header-bg-preview';
preview.style.width = '100%';
preview.style.height = '48px';
preview.style.objectFit = 'cover';
preview.style.borderRadius = '4px';
preview.style.marginBottom = '4px';
box.appendChild(preview);
const row = document.createElement('div');
row.style.display = 'flex';
row.style.gap = '4px';
row.appendChild(mkStylingButton('Upload', pickHeaderBackground));
row.appendChild(mkStylingButton('Remove', removeHeaderBackground));
box.appendChild(row);
return box;
}
function updateHeaderBgBox() {
const preview = document.getElementById('gdc-header-bg-preview');
if (!preview) return;
preview.src = headerBgUrl || '';
preview.style.display = headerBgUrl ? 'block' : 'none';
}
// Panels are matched on their visible label because the app's own class names
// are hashed and change between builds.
function findCollapseBox(label) {
const headers = document.querySelectorAll('.ant-collapse-header-text');
for (const header of headers) {
if (header.textContent.trim().toLowerCase() !== label) continue;
const item = header.closest('.ant-collapse-item');
const box = item && item.querySelector('.ant-collapse-content > .ant-collapse-content-box');
if (box) return box;
}
return null;
}
function addHeaderBgBox() {
if (document.getElementById('gdc-header-bg-box')) return true;
// Ant only renders the panel body once it has been expanded for the first time.
const box = findCollapseBox('styling');
if (!box) return false;
box.appendChild(createHeaderBgBox());
updateHeaderBgBox();
return true;
}
// ---------------------------------------------------------------------------
// Rotate card backs for printing
// ---------------------------------------------------------------------------
@@ -350,20 +485,27 @@
// Bootstrap
// ---------------------------------------------------------------------------
// The app renders after load, and the print sidebar only exists on the print
// view, so keep watching the DOM until both mount points have been handled.
applyCardFont(isCardFontEnabled());
refreshHeaderBackground().catch((e) => console.warn('Header background:', e));
var headerDone = addHeaderButtons();
var sidebarDone = addRotateButton();
var mountPending = false;
if (!headerDone || !sidebarDone) {
// The app renders after load, the print sidebar only exists on the print view,
// and the editor form is re-rendered whenever another card is selected, so the
// observer stays connected: every add* below is a no-op once its target is in
// place, and re-runs if the app throws it away.
var observer = new MutationObserver(function () {
if (mountPending) return;
mountPending = true;
requestAnimationFrame(function () {
mountPending = false;
if (!headerDone) headerDone = addHeaderButtons();
if (!sidebarDone) sidebarDone = addRotateButton();
if (headerDone && sidebarDone) observer.disconnect();
addHeaderBgBox();
});
});
observer.observe(document.documentElement, { childList: true, subtree: true });
}
})();