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 icon +

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 @@ - + + + +
+ + +
+

Other configuration

+ + + + + + + + + + +
Enable settings button on popup page + + Hint: You can use / to open this page anytime +
Drawing guide video playback speed + 🐢 0.7x + + 🐇 2.5x +
+
+ + + +