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
-
-
-
-
- Download JSON
-
-
- 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
+
+
+
+
+ Download JSON
+
+
+ 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