diff --git a/.gitignore b/.gitignore
index 85d6a00..1fe422e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,8 @@
+# Project custom
local/
.env
-# Development JS libs
-js/libs/image.js
+# Editor settings folders
+.vscode/
+.vs/
+
diff --git a/README.md b/README.md
index c746098..f52481a 100644
--- a/README.md
+++ b/README.md
@@ -58,17 +58,17 @@ $ curl -L http://kanjithing-backend.chocolatejade42.repl.co/kanji/車
```
## :memo: Future features
+- [ ] Able to star/favourite kanji to add them quickly to a custom set.
- [ ] Make the user guess readings of kanji in words
-- [ ] Add a check for `loadKanjiDetails` in case the user has loaded a new kanji by the time info loads
-- [ ] Index page
- - Version links and details
- [ ] Help page
- How to use the extention, info about tooltips, etc
- Open on the first install
-- [ ] Settings page
- - Load custom kanji sets
- - Log in to save
- - Change the speed with which the drawing guide video is played
+- [x] Settings page
+ - [ ] `customsets` -> `sets`
+ - [ ] Use the `var { sets } = ...` thing
+ - [ ] Ability to toggle starred/all kanji
+ - [ ] Fall back to default set if the set we are trying to load is deleted
+ - [ ] Show alert to confirm settings were saved
- [ ] Flashcard thing where you get the meaning of the kanji and sample words and have to draw it
- [ ] Custom flashcards to remember kanji/words
- Import from quizlet
@@ -78,9 +78,8 @@ $ curl -L http://kanjithing-backend.chocolatejade42.repl.co/kanji/車
- [Available on GitHub](https://github.com/kanjialive/kanji-data-media/blob/master/kanji-animations/stroke_timings) with timestamps
- [ ] Grey loading screen should only appear after a certain amount of time to account for cached requests that resolve quickly
- To prevent grey/white flashes that occur when the next character loads quickly
-- [ ] Fix whatever weirdness broke the dynamic browser icon
-- [ ] Able to star/favourite kanji to add them quickly to a custom set.
- [ ] Right click to remove drawing (all connected strokes)
+- [ ] Add tooltip banner when extension updates
- [x] Keybinds to navigate the application via keyboard
- Up/down arrow to navigate between kanji sets
- Left/right arrow to navigate between kanji in the currently selected set
@@ -91,3 +90,9 @@ $ curl -L http://kanjithing-backend.chocolatejade42.repl.co/kanji/車
- S to star/unstar selected kanji
- Keybinds visible in tooltips
- [ ] Use static assets for the emojis to keep design consistent between operating systems
+- [ ] Event listener on the popup script to determine when the set storage has changed
+- [ ] Use data from the KanjiAlive API to do pronuncation/sounds
+- [ ] Make CSS for buttons/inputs be consistent throughout the popup/settings/index pages
+- [ ] Fix overlap interaction with especially long word descriptions (同 kanji)
+- [ ] Use a RapidAPI key in the application to fetch data (Replit downtime)
+- [ ] Unspaghettify everything
diff --git a/background.js b/background.js
index 7bcc0cb..ed596e2 100644
--- a/background.js
+++ b/background.js
@@ -1,8 +1,30 @@
/* Save the current kanji */
var current;
+var defaultsets = [
+ {"Unit one": "学校名前父母生高姉妹兄弟住所色"},
+ {"Unit two": "好同手紙英語何年私友行毎教場"},
+ {"Unit three": "早新家入出思来島午後朝夜牛魚族"},
+ {"Unit four": "会社持待道近町番屋店駅神様区"},
+ {"Unit five": "時間国先長話見言休聞今食勉強"},
+ {"Unit six": "帰買電車左右目口書物飲肉昼乗"},
+ {"Unit seven": "曜気分多少元半使天病心楽方作文"},
+ {"Unit eight": "週夏立自赤外西川旅州晩洗持活去"},
+ {"Unit nine": "正冬着安広海古寺東京都北市県"},
+ {"Unit ten": "森山知雪雨字読急洋服動止院漢和"},
+ {"Unit eleven": "春秋花南田売耳青白仕事銀犬飯"},
+ {"Unit twelve": "林黒羊地夕次体発馬才鳥茶歩鉄"}
+];
-/* Set up a listener so we can receive messages from the console */
-chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => {
+
+/* Set up a listener so we can receive messages from the console
+ Because chrome is incredibly dumb and stupid, they won't allow
+ the listener to be an async function, so we have to instead put
+ the async code inside an immediately invoked function, and then
+ return true so that chrome knows to wait for a callback to complete
+ before continuing. The wrapper looks really ugly but it's the best
+ I can do without weird indentation so it's the lesser of two evils
+*/
+chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {(async () => {
// {type: ..., data: ...}
switch (message.type) {
@@ -12,21 +34,77 @@ chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => {
case "setIcon":
setBrowserIcon(message.data);
break;
- default:
+ case "resetKanjiSets":
+ await chrome.storage.local.remove("customsets");
+ await createKanjiSets();
+ sendResponse();
+ break;
+ case "ensureDefaultConfig":
+ await ensureDefaultConfiguration();
+ sendResponse();
break;
}
-});
-/* Set up a listener for when the extension is installed */
-chrome.runtime.onInstalled.addListener(reason => {
+})(); return true});
+
+/* Set up a listener for when the extension is installed/chrome restarts */
+chrome.runtime.onInstalled.addListener(async reason => {
console.log("Install event fired with", reason);
- chrome.runtime.setUninstallURL("https://kanjithing-backend.chocolatejade42.repl.co/uninstall");
+
+ if ((await chrome.management.getSelf()).installType !== "development")
+ chrome.runtime.setUninstallURL("https://kanjithing-backend.chocolatejade42.repl.co/uninstall");
+
+ await ensureDefaultConfiguration();
+ await ensureCorrectKanjiIcon();
+ await ensureBetaBadge();
+});
+
+chrome.runtime.onStartup.addListener(async () => {
+ await ensureDefaultConfiguration();
+ await ensureCorrectKanjiIcon();
+ await ensureBetaBadge();
});
+chrome.storage.onChanged.addListener(async (changes, namespace) => {
+ // Console log when storage values change
+ if ((await chrome.management.getSelf()).installType !== "development") return;
+
+ for (let [key, { oldValue, newValue }] of Object.entries(changes)) {
+ console.debug(`${key} : ${oldValue} -> ${newValue}`);
+ }
+});
+
+/* Configuration functions called above */
+async function ensureCorrectKanjiIcon() {
+ var { customsets, selectedset, selectedkanji } = await chrome.storage.local.get();
+ if ([ customsets, selectedset, selectedkanji ].includes(undefined)) return;
+
+ setBrowserIcon(customsets[selectedset].kanji[selectedkanji], bypass=true);
+}
+
+async function ensureBetaBadge() {
+ // Ensure that the "Beta" badge is present if necessary
+
+ if ((await chrome.management.getSelf()).installType === "development") {
+ chrome.action.setBadgeText({ text: "B" });
+ chrome.action.setBadgeBackgroundColor({ color: "#304db6" });
+ }
+}
+
+async function ensureDefaultConfiguration() {
+ // Create default sets
+ var sets = (await chrome.storage.local.get("customsets")).customsets;
+ (sets === undefined) && await createKanjiSets();
+
+ // Create default settings
+ var { config } = await chrome.storage.local.get("config");
+ (config === undefined) && await createDefaultConfig();
+}
+
/* Script to change the browser icon */
-function setBrowserIcon(kanji) {
+function setBrowserIcon(kanji, bypass=false) {
// https://jsfiddle.net/1u37ovj9/
- if (current === kanji) return;
+ if (current === kanji && !bypass) return;
var canvas = new OffscreenCanvas(64, 64);
var context = canvas.getContext("2d");
@@ -43,5 +121,25 @@ function setBrowserIcon(kanji) {
current = kanji;
var imageData = context.getImageData(0, 0, 64, 64);
- chrome.action.setIcon({imageData}, () => console.log(`Set browser icon to %c${kanji}`, "color: #7289da"));
-};
+ chrome.action.setIcon({ imageData }, () => console.log(`Set browser icon to %c${kanji}`, "color: #7289da"));
+}
+
+/* Creates defult configuration as required by ensureDefaultConfiguration */
+async function createKanjiSets() {
+ // {id: ..., name: ..., kanji: ..., enabled: ...}
+
+ var customsets = defaultsets.map((item, index) => {
+ var name = Object.keys(item)[0];
+ var value = Object.values(item)[0];
+
+ return {id: index, name: name, kanji: value, enabled: true}
+ });
+
+ await chrome.storage.local.set({ customsets });
+}
+
+async function createDefaultConfig() {
+ var { videoSpeed, settingsbtn } = await chrome.storage.local.get(["videoSpeed", "settingsbtn"]);
+ (videoSpeed !== undefined) || await chrome.storage.local.set({ videoSpeed: 0.8 });
+ (settingsbtn !== undefined) || await chrome.storage.local.set({ settingsbtn: true });
+}
diff --git a/css/index.css b/css/index.css
new file mode 100644
index 0000000..8400c5d
--- /dev/null
+++ b/css/index.css
@@ -0,0 +1,64 @@
+body {
+ color: #7289da;
+ background-color: #36393e;
+ font-family: "Segoe UI", Tahoma, sans-serif;
+}
+
+/* Header with image and title */
+header {
+ margin: auto;
+ width: fit-content;
+ display: flex;
+ align-items: center;
+}
+
+header * {
+ vertical-align: middle;
+ float: left;
+}
+
+header h1 {
+ margin-left: 10px;
+ font-size: 600%;
+}
+
+/* Install button */
+#installbutton {
+ position: fixed;
+ right: 10px;
+ top: 10px;
+
+ min-width: fit-content;
+ font-size: 2.75vh;
+ color: white;
+ background-color: #7289da;
+
+ border: none;
+ border-radius: 5px;
+ margin: auto;
+ display: flex;
+ align-items: center;
+ padding: 0.75vh 1vw;
+
+ opacity: 0;
+ transition: opacity 300ms ease-in-out 500ms, color 300ms ease-in-out;
+}
+
+#installbutton:hover {
+ color: #36393e;
+}
+
+#installbutton img {
+ vertical-align: middle;
+ margin-right: 0.25vw;
+}
+
+#installbutton.available {
+ opacity: 1;
+ cursor: pointer;
+}
+
+div#itemdescription {
+ text-align: center;
+ display: block;
+}
diff --git a/css/popup.css b/css/popup.css
index 809f0fc..3afabf9 100644
--- a/css/popup.css
+++ b/css/popup.css
@@ -117,3 +117,26 @@ div#infosection #exampleslist * {
#popup-inner span {
margin: 15px;
}
+
+#noscript {
+ font-size: 300%;
+ display: block;
+}
+
+#settings {
+ position: fixed;
+ right: 4px;
+ bottom: 4px;
+
+ height: 4ch;
+ vertical-align: middle;
+ cursor: pointer;
+ border: none;
+ border-radius: 5px;
+ background-color: #4e5259;
+ transition: background-color 300ms ease-in-out;
+}
+
+#settings:hover {
+ background-color: #36393e;
+}
diff --git a/css/settings.css b/css/settings.css
new file mode 100644
index 0000000..2ee9f6a
--- /dev/null
+++ b/css/settings.css
@@ -0,0 +1,78 @@
+body {
+ max-width: 550px;
+ margin: auto;
+ zoom: 2vh;
+}
+
+#setscontainer {
+ display: table;
+ max-width: 500px;
+ margin: auto;
+}
+
+#setscontainer div {
+ display: table-row;
+}
+
+#setscontainer div * {
+ display: table-cell;
+}
+
+#setscontainer div :is(b, span) {
+ border: none;
+ text-indent: 0.5em;
+ padding-right: 0.5em;
+ cursor: default;
+}
+
+#setscontainer div button {
+ cursor: pointer;
+ border: none;
+ border-radius: 5px;
+ background-color: #4e5259;
+ margin: 0.75px;
+ transition: background-color 300ms ease-in-out;
+}
+
+#setscontainer div button:hover {
+ background-color: #36393e;
+}
+
+#setscontainer div span {
+ max-width: 50ch;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+#setscontainer div [contentEditable=true] {
+ border-radius: 5px;
+ border-bottom: 2px solid #7289da;
+ background-color: #4e5259;
+ border-radius: 5px 5px 0px 0px;
+ outline: none;
+ cursor: text;
+}
+
+#setsconfig input[type=button] {
+ border: none;
+ border-bottom: 3px solid #7289da;
+ border-radius: 5px 0px;
+ margin-top: 5px;
+ background-color: #4e5259;
+ color: #7289da;
+ cursor: pointer;
+ transition: background-color 300ms ease-in-out;
+}
+
+#setsconfig input[type=button]:hover {
+ background-color: #36393e;
+}
+
+code {
+ padding: 0 5px 0 5px;
+ background: #2F3136;
+ border-radius: 5px;
+ font-family: monospace;
+ font-weight: bold;
+}
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..8144bb6
--- /dev/null
+++ b/index.html
@@ -0,0 +1,31 @@
+
+
+
+
+ KanjiThing
+
+
+
+
+
+
+
+ KanjiThing
+
+
+
+
+
+
+
+
+
+ Learn kanji stroke order from the browser
+
+
+
diff --git a/js/index.js b/js/index.js
new file mode 100644
index 0000000..c71a3e4
--- /dev/null
+++ b/js/index.js
@@ -0,0 +1,15 @@
+// (async () => {
+// var resp = await fetch("https://api.github.com/repos/aiden2480/kanjithing/releases");
+// var data = await resp.json();
+
+// data.forEach((element, index) => {
+// console.log(index, element.zipball_url);
+// });
+// console.log(data);
+// })();
+
+// Display extension install button if needed
+chrome.extension || document.getElementById("installbutton").classList.add("available");
+chrome.extension || document.getElementById("installbutton").addEventListener("click", () => {
+ window.open("https://chrome.google.com/webstore/detail/kanjithing/nccfelhkfpbnefflolffkclhenplhiab/");
+});
diff --git a/js/popup.js b/js/popup.js
index d46728a..ee18fdf 100644
--- a/js/popup.js
+++ b/js/popup.js
@@ -1,10 +1,11 @@
-import { checkRembrandt, fetchKanjiDetails } from "/js/utilities.js"
+import * as utils from "/js/utilities.js";
/* Define elements */
var canvas = document.getElementById("drawcanvas");
var video = document.getElementById("kanjiguide");
var playpause = document.getElementById("playpause");
var eraseall = document.getElementById("eraseall");
+var selectedset = document.getElementById("selectedset");
var selectedkanji = document.getElementById("selectedkanji");
var remcheck = document.getElementById("remcheck");
var ctx = canvas.getContext("2d");
@@ -13,40 +14,24 @@ var ctx = canvas.getContext("2d");
var lastPopupTimestamp = 0;
var currentlyPressedKeys = {};
-/* TODO: Find a better home for this variable */
-var wakattaunits = [
- "学校名前父母生高姉妹兄弟住所色",
- "好同手紙英語何年私友行毎教場",
- "早新家入出思来島午後朝夜牛魚族",
- "会社持待道近町番屋店駅神様区",
- "時間国先長話見言休聞今食勉強",
- "帰買電車左右目口書物飲肉昼乗",
- "曜気分多少元半使天病心楽方作文",
- "週夏立自赤外西川旅州晩洗持活去",
- "正冬着安広海古寺東京都北市県",
- "森山知雪雨字読急洋服動止院漢和",
- "春秋花南田売耳青白仕事銀犬飯",
- "林黒羊地夕次体発馬才鳥茶歩鉄"
-];
-wakattaunits.unshift(wakattaunits.join(""));
-
/* Main function to load a selected kanji */
-function loadKanji(kanji) {
- // TODO Check if the kanji isn't in dropdown list, and add it if so.
+async function loadKanjiIndex(index) {
+ var kanji = (await utils.fetchSetFromID(selectedset.value)).kanji[index];
+
console.log(`Loading kanji %c${kanji}`, "color: #3498db");
+ selectedkanji.value = index;
eraseall.click();
- selectedkanji.value = kanji;
// Load in examples
populateInformation(kanji);
// Update saved kanji in database
- chrome.storage.local.set({ "selectedkanji": kanji });
+ chrome.storage.local.set({ "selectedkanji": index });
// Get video URL and set source
vidloading = true;
video.src = "/media/loading.png";
- fetchKanjiDetails(kanji).then(details => {
+ utils.fetchKanjiDetails(kanji).then(details => {
video.src = details.video;
})
@@ -57,38 +42,52 @@ function loadKanji(kanji) {
});
}
-function loadKanjiSet(setindex, defaultkanji=null) {
- var set = wakattaunits[parseInt(setindex)];
+async function loadKanjiSet(setID, index=0) {
+ selectedset.value = setID;
+ var set = await utils.fetchSetFromID(setID);
- console.log(`Loading set %c${setindex} %c${set}`, "color: #9b59b6", "color: #2ecc71");
- selectedkanji.innerHTML = "";
+ console.log("Loading set ", set);
+ selectedkanji.innerHTML = null;
// Update current unit in database
- chrome.storage.local.set({ "selectedunit": setindex });
+ chrome.storage.local.set({ "selectedset": setID });
- for (let index in set) {
+ set.kanji.split("").forEach((char, index) => {
let elem = document.createElement("option");
+ elem.value = index;
- elem.textContent = set[index];
+ elem.textContent = char;
selectedkanji.appendChild(elem);
- };
+ });
// Load the kanji
- defaultkanji ? loadKanji(defaultkanji) : loadKanji(set[0]);
+ loadKanjiIndex(index);
}
/* Add event listeners for the various elements */
-window.addEventListener("load", () => {
- // Load the selected kanji once prepared
- chrome.storage.local.get(["selectedunit", "selectedkanji"], result => {
- console.log(`Retrieved from storage %cset ${result.selectedunit} %ckanji ${result.selectedkanji}`, "color: #e67e22", "color: #fee75c");
- let unit = result.selectedunit !== undefined ? parseInt(result.selectedunit) : wakattaunits.length - 1;
- let kanji = result.selectedkanji !== undefined ? result.selectedkanji : wakattaunits[unit][0];
-
- loadKanjiSet(unit, kanji);
- selectedunit.value = unit;
- selectedkanji.value = kanji;
+window.addEventListener("load", async () => {
+ // Load the custom sets into the selector menu
+ (await utils.fetchAllSets()).forEach(set => {
+ if (!set.enabled) return;
+ let elem = document.createElement("option");
+
+ elem.value = set.id;
+ elem.innerText = set.name;
+ selectedset.appendChild(elem);
});
+
+ // Hide/show settings button
+ var { settingsbtn } = await chrome.storage.local.get("settingsbtn");
+ document.getElementById("settings").style.visibility = settingsbtn ? "visible" : "hidden";
+
+ // Load the selected kanji once prepared
+ var result = await chrome.storage.local.get(["selectedset", "selectedkanji"]);
+ let setID = result.selectedset !== undefined ? parseInt(result.selectedset) : (await utils.fetchAnySet()).id;
+ let kanjiIndex = result.selectedkanji !== undefined ? parseInt(result.selectedkanji) : 0;
+
+ await loadKanjiSet(setID, kanjiIndex);
+ selectedset.value = setID;
+ selectedkanji.value = kanjiIndex;
});
video.addEventListener("loadeddata", () => {
@@ -99,17 +98,19 @@ video.addEventListener("loadeddata", () => {
selectedkanji.addEventListener("change", () => {
// Load the selected kanji upon dropdown value change
- loadKanji(selectedkanji.value);
+ loadKanjiIndex(selectedkanji.value);
});
-selectedunit.addEventListener("change", () => {
+selectedset.addEventListener("change", () => {
// Load the selected unit upon dropdown value change
- loadKanjiSet(selectedunit.value);
+ loadKanjiSet(selectedset.value);
});
-video.addEventListener("play", () => {
+video.addEventListener("play", async () => {
// Sets the options for the video element (once only)
- video.playbackRate = 0.85;
+ var { videoSpeed } = await chrome.storage.local.get("videoSpeed");
+ video.playbackRate = videoSpeed;
+
canvas.width = video.offsetWidth;
canvas.height = video.offsetHeight;
}, {once: true});
@@ -136,7 +137,7 @@ eraseall.addEventListener("click", () => {
});
remcheck.addEventListener("click", async () => {
- var percent = await checkRembrandt();
+ var percent = await utils.checkRembrandt();
var style = document.getElementById("popup-outer").style;
var inner = document.getElementById("popup-inner");
var result = "Try again";
@@ -177,7 +178,7 @@ remcheck.addEventListener("click", async () => {
}, 2000);
});
-document.addEventListener("keydown", (event) => {
+document.addEventListener("keydown", event => {
// Disable custom key events when special keys held
if (event.ctrlKey || event.shiftKey) return;
@@ -188,44 +189,49 @@ document.addEventListener("keydown", (event) => {
if (new Date().getTime() - currentlyPressedKeys[event.code] < 200) return;
currentlyPressedKeys[event.code] = new Date().getTime();
+ // Pull out the IDs from the two select elements
+ var setopts = Array.from(selectedset.children).map(item => item.value);
+ var pickopts = Array.from(selectedkanji.children).map(item => item.value);
+
// Hotkey callbacks for each key
switch (event.code) {
+ // TODO Use modulo for ArrowDown and ArrowRight
+
case "KeyR":
- var set = wakattaunits[selectedunit.value].replace(selectedkanji.value, "");
- var index = Math.floor(Math.random() * set.length);
-
- loadKanji(set[index]);
+ var removed = pickopts.filter(item => item != selectedkanji.value);
+ loadKanjiIndex(parseInt(removed.random()));
break;
case "ArrowUp":
- var thispos = parseInt(selectedunit.value);
- var nextpos = thispos - 1 < 0 ? wakattaunits.length - 1 : thispos - 1;
+ var thispos = setopts.indexOf(selectedset.value);
+ var nextpos = setopts.at(thispos - 1);
- selectedunit.value = nextpos;
loadKanjiSet(nextpos);
break;
case "ArrowDown":
- var thispos = parseInt(selectedunit.value);
- var nextpos = thispos + 1 >= wakattaunits.length ? 0 : thispos + 1;
+ var thispos = setopts.indexOf(selectedset.value);
+ var nextpos = thispos < setopts.length - 1 ? setopts.at(thispos + 1) : 0;
- selectedunit.value = nextpos;
loadKanjiSet(nextpos);
break;
case "ArrowLeft":
- var thispos = wakattaunits[selectedunit.value].indexOf(selectedkanji.value);
- var nextpos = thispos > 0 ? thispos - 1 : wakattaunits[selectedunit.value].length - 1;
+ var thispos = pickopts.indexOf(selectedkanji.value);
+ var nextpos = pickopts.at(thispos - 1);
- loadKanji(wakattaunits[selectedunit.value][nextpos]);
+ loadKanjiIndex(nextpos);
break;
case "ArrowRight":
- var thispos = wakattaunits[selectedunit.value].indexOf(selectedkanji.value);
- var nextpos = thispos >= wakattaunits[selectedunit.value].length - 1 ? 0 : thispos + 1;
+ var thispos = pickopts.indexOf(selectedkanji.value);
+ var nextpos = thispos < pickopts.length - 1 ? pickopts.at(thispos + 1) : 0;
- loadKanji(wakattaunits[selectedunit.value][nextpos]);
+ loadKanjiIndex(nextpos);
break;
case "Backspace":
case "Delete":
ctx.clearRect(0, 0, canvas.width, canvas.height);
break;
+ case "Slash":
+ document.getElementById("settings").click();
+ break;
}
});
@@ -233,17 +239,26 @@ document.addEventListener("keyup", (event) => {
delete currentlyPressedKeys[event.code];
});
+document.getElementById("settings").addEventListener("click", () => {
+ chrome.runtime.openOptionsPage();
+});
+
canvas.addEventListener("contextmenu", (event) => {
event.preventDefault();
});
+// Add random function to the array prototype
+Array.prototype.random = function () {
+ return this[Math.floor(Math.random() * this.length)];
+}
+
/* API call functions */
async function populateInformation(kanji) {
- var json = await fetchKanjiDetails(kanji);
- if (selectedkanji.value != kanji) return;
+ // TODO move this to the utilities.js function
+ var json = await utils.fetchKanjiDetails(kanji);
+ if (selectedkanji.selectedOptions[0].innerText != kanji) return;
var listelem = document.getElementById("exampleslist");
- console.debug("populating kanji", kanji, JSON.parse(JSON.stringify(json)));
// Establish readings
var on = json.onyomi_ja ? json.onyomi_ja.split("、") : [];
@@ -263,7 +278,7 @@ async function populateInformation(kanji) {
parent.title += "Onyomi are in katakana, while kunyomi are in hiragana";
// Populate examples
- listelem.textContent = "";
+ listelem.textContent = null;
(json.examples || []).splice(0, 6).map(item => {
let elem = document.createElement("li");
let reading = document.createElement("b");
diff --git a/js/settings.js b/js/settings.js
new file mode 100644
index 0000000..ff90454
--- /dev/null
+++ b/js/settings.js
@@ -0,0 +1,281 @@
+const KANJI_REGEX = /^[\u4E00-\u9FAF]+$/;
+generateSettingsPage();
+
+async function generateKanjiSets() {
+ var container = document.getElementById("setscontainer");
+ var sets = (await chrome.storage.local.get("customsets")).customsets;
+ container.innerHTML = null;
+
+ sets.forEach(item => {
+ var div = document.createElement("div");
+ var input = document.createElement("input");
+ var bold = document.createElement("b");
+ var span = document.createElement("span");
+ var editbtn = document.createElement("button");
+ var delbtn = document.createElement("button");
+
+ div.title = "Set #" + item.id;
+ div.id = "set" + item.id;
+ input.type = "checkbox";
+ input.checked = item.enabled;
+ bold.innerText = item.name;
+ span.innerText = item.kanji;
+ editbtn.innerText = "✏️";
+ delbtn.innerText = "🗑️";
+ editbtn.classList.add("edit");
+ delbtn.classList.add("del");
+
+ div.appendChild(input);
+ div.appendChild(bold);
+ div.appendChild(span);
+ div.appendChild(editbtn);
+ div.appendChild(delbtn);
+ container.appendChild(div);
+ });
+
+ // Attach event listeners to each checkbox
+ [...container.getElementsByTagName("input")].forEach(item => {
+ item.addEventListener("change", async (event) => {
+ var sets = (await chrome.storage.local.get("customsets")).customsets;
+ var id = event.path[1].id.slice(3);
+ var target = sets.find(x => x.id == id);
+
+ target.enabled = event.target.checked;
+ chrome.storage.local.set({ customsets: sets });
+ });
+ });
+
+ // Attach event listeners to each editable element
+ [...container.querySelectorAll("span, b")].forEach(item => {
+ item.addEventListener("keydown", event => {
+ if (event.code == "Enter") {
+ event.path[1].children[3].click();
+ event.preventDefault();
+ }
+ });
+
+ // Stop it pasting with all that stupid formatting
+ item.addEventListener("paste", event => {
+ var paste = (event.clipboardData || window.clipboardData).getData("text");
+ var selection = window.getSelection();
+
+ if (!selection.rangeCount) return false;
+ selection.deleteFromDocument();
+ selection.getRangeAt(0).insertNode(document.createTextNode(paste));
+
+ event.preventDefault();
+ })
+ });
+
+ // Attach event listeners to edit button
+ [...container.getElementsByClassName("edit")].forEach(item => {
+ item.addEventListener("click", async (event) => {
+ if (item.classList.contains("editing")) return;
+
+ event.path[0].innerText = "✔️";
+ item.classList.add("editing");
+ var targetdiv = event.path[1];
+ var id = targetdiv.id.slice(3);
+
+ // Find target elements
+ var namenode = targetdiv.getElementsByTagName("b")[0];
+ var kanjinode = targetdiv.getElementsByTagName("span")[0];
+ document.createElement("a").contentEditable
+
+ // Mutate elements
+ namenode.contentEditable = true;
+ kanjinode.contentEditable = true;
+
+ // Attach new event listener
+ async function callback(event) {
+ var oldset = await retrieveSet(id);
+ var error = false;
+
+ // Check if errors, otherwise update
+ (namenode.innerText !== oldset.name) && await renameSet(id, namenode.innerText).catch(err => {
+ error = true;
+ alert(err);
+ });
+
+ (kanjinode.innerText !== oldset.kanji) && await editSetKanji(id, kanjinode.innerText).catch(err => {
+ error = true;
+ alert(err);
+ });
+
+ // In the event of an error
+ if (error) {
+ namenode.innerText = oldset.name;
+ kanjinode.innerText = oldset.kanji;
+ return;
+ }
+
+ // Disable editing
+ namenode.contentEditable = false;
+ kanjinode.contentEditable = false;
+
+ // Update state
+ event.path[0].innerText = "✏️";
+ item.classList.remove("editing");
+ item.removeEventListener("click", callback);
+ }
+
+ item.addEventListener("click", callback);
+ });
+ });
+
+ // Attach event listeners to delete button
+ [...container.getElementsByClassName("del")].forEach(item => {
+ item.addEventListener("click", async (event) => {
+ var id = event.path[1].id.slice(3);
+ var target = await retrieveSet(id);
+
+ if (!confirm(`Are you sure you want to delete this set?\n${target.name} ${target.kanji}`)) return;
+ await deleteSet(id);
+ generateKanjiSets();
+ })
+ });
+}
+
+async function generateMisc() {
+ var { settingsbtn, videoSpeed } = await chrome.storage.local.get(["settingsbtn", "videoSpeed"]);
+
+ document.getElementById("showsettings").checked = settingsbtn;
+ document.getElementById("videoslider").value = videoSpeed * 100;
+}
+
+async function generateSettingsPage() {
+ // Ensure default settings are available
+ await ensureDefaultConfiguration();
+
+ generateKanjiSets();
+ generateMisc();
+}
+
+function ensureDefaultConfiguration() {
+ return new Promise((resolve, reject) => {
+ chrome.runtime.sendMessage({type: "ensureDefaultConfig"}, resolve);
+ });
+}
+
+// Set editing functions
+function retrieveSet(id) {
+ return new Promise(async (resolve, reject) => {
+ var sets = (await chrome.storage.local.get("customsets")).customsets;
+ var target = sets.find(x => x.id == id);
+
+ target ? resolve(target) : reject(`No set with ID ${id} exists`);
+ });
+}
+
+function createSet(name, setstr) {
+ return new Promise(async (resolve, reject) => {
+ var sets = (await chrome.storage.local.get("customsets")).customsets;
+ var id = sets.slice(-1)[0].id + 1;
+ var kanji = setstr.replace(" ", "");
+
+ // Ensure set is correct
+ if (!(name.length >= 1 && name.length <= 12)) return reject("Name must be 12 or less characters");
+ if (!KANJI_REGEX.test(kanji)) return reject("Invalid kanji string");
+
+ sets.push({ id, name, kanji, enabled: true });
+ chrome.storage.local.set({customsets: sets}, resolve);
+ });
+}
+
+function deleteSet(id) {
+ return new Promise(async (resolve, reject) => {
+ var sets = (await chrome.storage.local.get("customsets")).customsets;
+ var updated = sets.filter(value => value.id != id);
+
+ chrome.storage.local.set({ customsets: updated }, resolve);
+ });
+}
+
+function renameSet(id, name) {
+ return new Promise(async (resolve, reject) => {
+ var sets = (await chrome.storage.local.get("customsets")).customsets;
+ var target = sets.find(x => x.id == id);
+ var index = sets.indexOf(target);
+
+ if (!(name.length >= 1 && name.length <= 12)) return reject("Name must be 12 or less characters");
+ if (target == undefined) return reject(`Set with ID ${id} does not exist`);
+
+ target.name = name;
+ sets[index] = target;
+
+ chrome.storage.local.set({ customsets: sets }, resolve);
+ });
+}
+
+function editSetKanji(id, kanji) {
+ return new Promise(async (resolve, reject) => {
+ var sets = (await chrome.storage.local.get("customsets")).customsets;
+ var target = sets.find(x => x.id == id);
+ var index = sets.indexOf(target);
+
+ // Ensure set is correct
+ if (!KANJI_REGEX.test(kanji)) return reject("Invalid kanji string");
+ if (target == undefined) return reject(`Set with ID ${id} does not exist`);
+
+ target.kanji = kanji;
+ sets[index] = target;
+
+ chrome.storage.local.set({ customsets: sets }, resolve);
+ });
+}
+
+// Event listeners
+document.getElementById("createset").addEventListener("click", async () => {
+ await createSet(prompt("Unit name:"), prompt("Kanji in unit:")).then(() => {
+ generateKanjiSets();
+ }).catch(err => {
+ alert(err);
+ });
+});
+
+window.addEventListener("beforeunload", event => {
+ if (document.querySelector("[contentEditable=true]") !== null) {
+ return event.returnValue = "Are you sure you want to exit? You have unsaved changes";
+ }
+});
+
+document.getElementById("export").addEventListener("click", async () => {
+ var sets = (await chrome.storage.local.get("customsets")).customsets;
+ var encoded = transform(JSON.stringify(sets), 1);
+
+ prompt("Your exported configuration: (Right click to copy)", encoded);
+});
+
+document.getElementById("import").addEventListener("click", async () => {
+ try {
+ var encoded = prompt("Paste in your configuration string below");
+ var decoded = JSON.parse(transform(encoded, -1));
+ } catch (error) {
+ return alert("Invalid configuration string. Couldn't import configuration");
+ }
+
+ await chrome.storage.local.set({ customsets: decoded });
+ generateKanjiSets();
+});
+
+document.getElementById("reset").addEventListener("click", async () => {
+ if (!confirm("Are you sure you want to reset your sets to the default?")) return;
+ chrome.runtime.sendMessage({type: "resetKanjiSets"}, generateKanjiSets);
+});
+
+document.getElementById("showsettings").addEventListener("change", async (event) => {
+ var settingsbtn = event.target.checked;
+ chrome.storage.local.set({ settingsbtn });
+});
+
+document.getElementById("videoslider").addEventListener("change", async (event) => {
+ var videoSpeed = event.target.value / 100;
+ chrome.storage.local.set({ videoSpeed });
+})
+
+// Helper functions
+function transform(text, num=1) {
+ return text.split("").map(char => {
+ return String.fromCharCode(char.charCodeAt(0) + num);
+ }).join("");
+}
diff --git a/js/utilities.js b/js/utilities.js
index 728ce76..8829a71 100644
--- a/js/utilities.js
+++ b/js/utilities.js
@@ -1,3 +1,5 @@
+export const KANJI_REGEX = /^[\u4E00-\u9FAF]+$/;
+
function getLastFrameOfVideo(url) {
// Take in a video URL and get the last frame from that video.
// Used to compare video to canvas drawing via Rembrandt.
@@ -69,7 +71,8 @@ function convertCanvasToBlackAndWhite(canvas) {
}
export async function checkRembrandt() {
- var videoBase64 = await getLastFrameOfVideo((await fetchKanjiDetails(selectedkanji.value)).video);
+ var kanji = selectedkanji.selectedOptions[0].innerText;
+ var videoBase64 = await getLastFrameOfVideo((await fetchKanjiDetails(kanji)).video);
var blankcanv = document.createElement("canvas");
var blankctx = blankcanv.getContext("2d");
@@ -126,3 +129,26 @@ export async function fetchKanjiDetails(kanji) {
return json;
}
+
+// Fetching sets utility functions
+export async function fetchSetFromID(id) {
+ // Finds a set from a given ID
+ var sets = (await chrome.storage.local.get("customsets")).customsets;
+ return sets.find(x => x.id == id);
+}
+
+export async function fetchAnySet() {
+ // Fetch any set in the event that we cannot find the desired one
+ var sets = (await chrome.storage.local.get("customsets")).customsets;
+ return sets[0];
+}
+
+export async function fetchRandomSet() {
+ // Fetch a random set
+ var sets = (await chrome.storage.local.get("customsets")).customsets;
+ return sets[Math.floor(Math.random() * sets.length)];
+}
+
+export async function fetchAllSets() {
+ return (await chrome.storage.local.get("customsets")).customsets;
+}
diff --git a/manifest.json b/manifest.json
index 5813a76..0a31c4a 100644
--- a/manifest.json
+++ b/manifest.json
@@ -4,6 +4,7 @@
"version": "1.4.2",
"manifest_version": 3,
"permissions": ["storage"],
+ "options_page": "settings.html",
"action": {
"default_popup": "popup.html"
},
@@ -11,6 +12,8 @@
"service_worker": "background.js"
},
"icons": {
- "64": "media/defaulticon.png"
+ "64": "media/icon64x64.png",
+ "128": "media/icon128x128.png",
+ "256": "media/icon256x256.png"
}
}
\ No newline at end of file
diff --git a/media/icon128x128.png b/media/icon128x128.png
new file mode 100644
index 0000000..9f217bd
Binary files /dev/null and b/media/icon128x128.png differ
diff --git a/media/icon256x256.png b/media/icon256x256.png
new file mode 100644
index 0000000..f02dc96
Binary files /dev/null and b/media/icon256x256.png differ
diff --git a/media/defaulticon.png b/media/icon64x64.png
similarity index 100%
rename from media/defaulticon.png
rename to media/icon64x64.png
diff --git a/popup.html b/popup.html
index 62c3726..bbe0cb5 100644
--- a/popup.html
+++ b/popup.html
@@ -10,7 +10,7 @@
-
+
+
+
+