From 5d38e8c780a38a3344c43be4fff5c717fcf8a736 Mon Sep 17 00:00:00 2001 From: "Jakob L. Kreuze" Date: Wed, 26 Nov 2025 15:32:10 -0500 Subject: Document deck editor --- README.md | 6 ++ card-editor.html | 17 ----- card-editor.js | 185 ------------------------------------------------------- deck-editor.html | 17 +++++ deck-editor.js | 185 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 208 insertions(+), 202 deletions(-) delete mode 100644 card-editor.html delete mode 100644 card-editor.js create mode 100644 deck-editor.html create mode 100644 deck-editor.js diff --git a/README.md b/README.md index 043d1c7..d048e0b 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,12 @@ The "?" button will display the available keyboard shortcuts. When the review is complete, you will be prompted to download a JSON file. This contains the deck of cards updated with new scheduling parameters for your next review. +SRS Anywhere comes with a simple editor for adding, editing, and removing cards from a deck. +Open `deck-editor.html` and press the "Browse..." button in the top right-hand corner to load a deck of cards. +The page will populate with one or more tables, each corresponding to a specific type of card, and input fields for part of the card. +The button labeled with a plus sign (+) will add a card of that type to the deck, with an automatically-generated ID. +Press the "Download JSON" button in the top right-hand corner to download the modified deck. + ### Format for Cards The `cards.json` file is an array of objects containing at least the keys `id`, `front`, and `back`. diff --git a/card-editor.html b/card-editor.html deleted file mode 100644 index d4fca8a..0000000 --- a/card-editor.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - SRS Anywhere Card Editor - - - -
- - -
-

SRS Anywhere Card Editor

-
- - - diff --git a/card-editor.js b/card-editor.js deleted file mode 100644 index b60185e..0000000 --- a/card-editor.js +++ /dev/null @@ -1,185 +0,0 @@ -/* Copyright © 2025 Jakob L. Kreuze - - This file is part of SRS Anywhere. - - SRS Anywhere is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as - published by the Free Software Foundation, either version 3 of the - License, or (at your option) any later version. - - SRS Anywhere is distributed in the hope that it will be useful, but - WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public - License along with SRS Anywhere. If not, see . */ - -let data = []; -let fieldKeys = {}; - -function loadJSON(text) { - try { - const parsed = JSON.parse(text); - if (!Array.isArray(parsed)) throw new Error('JSON must be an array of objects'); - data = parsed.map(obj => ({...obj})); - computeFieldKeys(); - renderTable(); - } catch (e) { - alert('Error parsing JSON: ' + e.message); - } -} - -function partition(arr, key = 'type') { - return arr.reduce((acc, item) => { - const k = item[key] || 'default'; - const prop = String(k); - if (!acc[prop]) acc[prop] = []; - acc[prop].push(item); - return acc; - }, {}); -} - -function computeFieldKeys() { - const partitioned = partition(data); - Object.entries(partitioned).forEach(([type, cards]) => { - const keys = new Set(); - cards.forEach(card => { - Object.keys(card).forEach(k => { - if (!k.startsWith('_')) keys.add(k); - }); - }) - fieldKeys[type] = Array.from(keys); - }); -} - -function renderTable() { - const container = document.getElementById('table-container'); - container.innerHTML = ''; // clear previous - - if (!data.length) return; - - Object.entries(fieldKeys).forEach(([type, keys]) => { - const table = document.createElement('table'); - const thead = document.createElement('thead'); - const headerRow = document.createElement('tr'); - const addButton = document.createElement('button'); - - table.style = "margin-bottom: 32px;" - - // Build header - keys.forEach(k => { - const th = document.createElement('th'); - th.textContent = k; - headerRow.appendChild(th); - }); - thead.appendChild(headerRow); - table.appendChild(thead); - - // Extra header for Delete button - const delTh = document.createElement('th'); - delTh.textContent = 'Delete'; - headerRow.appendChild(delTh); - - // Build body - const tbody = document.createElement('tbody'); - data.forEach((obj, rowIndex) => { - if ((obj.type || 'default') != type) { - return; - } - const tr = document.createElement('tr'); - keys.forEach(k => { - const td = document.createElement('td'); - const input = document.createElement('textarea'); - input.rows = 2; - input.value = obj[k] !== undefined ? obj[k] : ''; - input.dataset.row = rowIndex; - input.dataset.key = k; - input.addEventListener('input', onFieldChange); - td.appendChild(input); - tr.appendChild(td); - }); - tbody.appendChild(tr); - - // Delete button column - const delTd = document.createElement('td'); - const delBtn = document.createElement('button'); - delBtn.textContent = 'x'; - delBtn.className = 'delete'; - delBtn.dataset.row = rowIndex; - delBtn.addEventListener('click', () => { - data.splice(idx, 1); - renderTable(); - }); - delTd.appendChild(delBtn); - tr.appendChild(delTd); - }); - - table.appendChild(tbody); - - addButton.innerHTML = '+'; - addButton.addEventListener('click', () => { - const newObj = {}; - keys.forEach(k => newObj[k] = ''); - newObj.id = crypto.randomUUID(); - data.push(newObj); - renderTable(); - }); - - container.appendChild(addButton); - container.appendChild(table); - }); -} - -function onFieldChange(e) { - const row = e.target.dataset.row; - const key = e.target.dataset.key; - const value = e.target.value; - data[row][key] = value; -} - -function removeEmptyStrings(obj, inPlace = false) { - if (typeof obj !== 'object' || obj === null) { - throw new TypeError('Expected an object'); - } - - // If we’re mutating, work directly on the original - const target = inPlace ? obj : {}; - - for (const key in obj) { - if (!Object.prototype.hasOwnProperty.call(obj, key)) continue; - - const value = obj[key]; - - if (value === '') { - if (!inPlace) continue; // skip adding it to the new object - delete target[key]; // remove from the original - } else { - target[key] = value; // copy non‑empty value - } - } - - return target; -} - -window.addEventListener('load', function() { - document.getElementById('cards-input').addEventListener('change', evt => { - const file = evt.target.files[0]; - if (!file) return; - const reader = new FileReader(); - reader.onload = e => loadJSON(e.target.result); - reader.readAsText(file); - }); - - document.getElementById('save-button').addEventListener('click', () => { - if (!data.length) return; - data.forEach(removeEmptyStrings); - const blob = new Blob([JSON.stringify(data, null, 2)], {type: 'application/json'}); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = 'edited.json'; - a.click(); - URL.revokeObjectURL(url); - }); -}) diff --git a/deck-editor.html b/deck-editor.html new file mode 100644 index 0000000..d4fca8a --- /dev/null +++ b/deck-editor.html @@ -0,0 +1,17 @@ + + + + + SRS Anywhere Card Editor + + + +
+ + +
+

SRS Anywhere Card Editor

+
+ + + diff --git a/deck-editor.js b/deck-editor.js new file mode 100644 index 0000000..b60185e --- /dev/null +++ b/deck-editor.js @@ -0,0 +1,185 @@ +/* Copyright © 2025 Jakob L. Kreuze + + This file is part of SRS Anywhere. + + SRS Anywhere is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as + published by the Free Software Foundation, either version 3 of the + License, or (at your option) any later version. + + SRS Anywhere is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public + License along with SRS Anywhere. If not, see . */ + +let data = []; +let fieldKeys = {}; + +function loadJSON(text) { + try { + const parsed = JSON.parse(text); + if (!Array.isArray(parsed)) throw new Error('JSON must be an array of objects'); + data = parsed.map(obj => ({...obj})); + computeFieldKeys(); + renderTable(); + } catch (e) { + alert('Error parsing JSON: ' + e.message); + } +} + +function partition(arr, key = 'type') { + return arr.reduce((acc, item) => { + const k = item[key] || 'default'; + const prop = String(k); + if (!acc[prop]) acc[prop] = []; + acc[prop].push(item); + return acc; + }, {}); +} + +function computeFieldKeys() { + const partitioned = partition(data); + Object.entries(partitioned).forEach(([type, cards]) => { + const keys = new Set(); + cards.forEach(card => { + Object.keys(card).forEach(k => { + if (!k.startsWith('_')) keys.add(k); + }); + }) + fieldKeys[type] = Array.from(keys); + }); +} + +function renderTable() { + const container = document.getElementById('table-container'); + container.innerHTML = ''; // clear previous + + if (!data.length) return; + + Object.entries(fieldKeys).forEach(([type, keys]) => { + const table = document.createElement('table'); + const thead = document.createElement('thead'); + const headerRow = document.createElement('tr'); + const addButton = document.createElement('button'); + + table.style = "margin-bottom: 32px;" + + // Build header + keys.forEach(k => { + const th = document.createElement('th'); + th.textContent = k; + headerRow.appendChild(th); + }); + thead.appendChild(headerRow); + table.appendChild(thead); + + // Extra header for Delete button + const delTh = document.createElement('th'); + delTh.textContent = 'Delete'; + headerRow.appendChild(delTh); + + // Build body + const tbody = document.createElement('tbody'); + data.forEach((obj, rowIndex) => { + if ((obj.type || 'default') != type) { + return; + } + const tr = document.createElement('tr'); + keys.forEach(k => { + const td = document.createElement('td'); + const input = document.createElement('textarea'); + input.rows = 2; + input.value = obj[k] !== undefined ? obj[k] : ''; + input.dataset.row = rowIndex; + input.dataset.key = k; + input.addEventListener('input', onFieldChange); + td.appendChild(input); + tr.appendChild(td); + }); + tbody.appendChild(tr); + + // Delete button column + const delTd = document.createElement('td'); + const delBtn = document.createElement('button'); + delBtn.textContent = 'x'; + delBtn.className = 'delete'; + delBtn.dataset.row = rowIndex; + delBtn.addEventListener('click', () => { + data.splice(idx, 1); + renderTable(); + }); + delTd.appendChild(delBtn); + tr.appendChild(delTd); + }); + + table.appendChild(tbody); + + addButton.innerHTML = '+'; + addButton.addEventListener('click', () => { + const newObj = {}; + keys.forEach(k => newObj[k] = ''); + newObj.id = crypto.randomUUID(); + data.push(newObj); + renderTable(); + }); + + container.appendChild(addButton); + container.appendChild(table); + }); +} + +function onFieldChange(e) { + const row = e.target.dataset.row; + const key = e.target.dataset.key; + const value = e.target.value; + data[row][key] = value; +} + +function removeEmptyStrings(obj, inPlace = false) { + if (typeof obj !== 'object' || obj === null) { + throw new TypeError('Expected an object'); + } + + // If we’re mutating, work directly on the original + const target = inPlace ? obj : {}; + + for (const key in obj) { + if (!Object.prototype.hasOwnProperty.call(obj, key)) continue; + + const value = obj[key]; + + if (value === '') { + if (!inPlace) continue; // skip adding it to the new object + delete target[key]; // remove from the original + } else { + target[key] = value; // copy non‑empty value + } + } + + return target; +} + +window.addEventListener('load', function() { + document.getElementById('cards-input').addEventListener('change', evt => { + const file = evt.target.files[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = e => loadJSON(e.target.result); + reader.readAsText(file); + }); + + document.getElementById('save-button').addEventListener('click', () => { + if (!data.length) return; + data.forEach(removeEmptyStrings); + const blob = new Blob([JSON.stringify(data, null, 2)], {type: 'application/json'}); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'edited.json'; + a.click(); + URL.revokeObjectURL(url); + }); +}) -- cgit v1.3